Compare commits

...

22 Commits

Author SHA1 Message Date
Anatoly Sablin
0786a6520f Bump gradle version. 2019-11-07 00:16:18 +03:00
Anatoly Sablin
430136c391 Bump dependency verions. 2019-11-06 23:26:56 +03:00
Anatoly Sablin
eda4404335 MSC2140 Add populating hashes via exec identity store. 2019-11-06 23:16:27 +03:00
Anatoly Sablin
c52034b18a MSC2140 Add populating hashes via sql and memory stores. 2019-11-06 23:07:42 +03:00
Anatoly Sablin
8d346037b7 MSC2140 Add hash configuration. 2019-11-06 00:20:39 +03:00
Anatoly Sablin
14ad4435bc MSC2140 Add SQL storage for hashes and the time-based rotation policy. 2019-11-05 23:18:11 +03:00
Anatoly Sablin
8ba8756871 Fix account registration. 2019-10-21 23:48:47 +03:00
Anatoly Sablin
43fe8b1aec Add the hash lookup handler. 2019-10-18 22:52:13 +03:00
Anatoly Sablin
a0270c7d01 Add NoOp configuration. Split classes into packages. 2019-10-16 23:07:14 +03:00
Anatoly Sablin
703044d06a Add initial Hash configuration. Add the HashDetailsHandler. 2019-10-15 23:38:32 +03:00
Anatoly Sablin
add6ed8fd9 Add the TOS API. 2019-10-09 23:12:23 +03:00
Anatoly Sablin
baed894ff8 Update policy configuration. Add Handler to check that user accepts terms. 2019-10-08 00:13:40 +03:00
Anatoly Sablin
14e095a147 Add policy configuration. 2019-10-04 00:03:51 +03:00
Anatoly Sablin
bc8795e940 Add authorization handler. 2019-10-01 23:52:01 +03:00
Anatoly Sablin
5521c0c338 Add account handlers. 2019-09-30 23:53:38 +03:00
Anatoly Sablin
614b3440e2 Registration API. Add DAO, Manager. 2019-09-30 23:16:58 +03:00
Anatoly Sablin
d0fd9fb9b0 Get domain from public url. 2019-09-17 23:17:30 +03:00
Anatoly Sablin
1232e9ce79 MSC2140 MSC2134 Remove the unused path. 2019-09-01 22:38:59 +03:00
Anatoly Sablin
a47a983c10 MSC2140 MSC2134 Refactoring. Move common classes to the share package. 2019-09-01 22:33:03 +03:00
Anatoly Sablin
fbb0d7c7ba MSC2140 Mirroring V1 to V2. 2019-08-31 23:35:58 +03:00
Anatoly Sablin
f1dd309551 MSC2140 Add option to enable/disable v1 and v2 api. 2019-08-31 23:09:20 +03:00
Anatoly Sablin
36f22e5ca6 Update remarks of the fork. 2019-08-15 22:46:53 +03:00
75 changed files with 2456 additions and 174 deletions

View File

@@ -15,7 +15,8 @@ ma1sd - Federated Matrix Identity Server
---
* This project is a fork of the https://github.com/kamax-matrix/mxisd which has been archived and no longer supported. *
* This project is a fork (not successor) of the https://github.com/kamax-matrix/mxisd, which has been archived and no longer maintained as a standalone product.
Also, ma1sd is supported by the volunteer not developers of the original project.
---

View File

@@ -78,7 +78,7 @@ buildscript {
dependencies {
classpath 'com.github.jengelman.gradle.plugins:shadow:5.1.0'
classpath 'com.github.ben-manes:gradle-versions-plugin:0.21.0'
classpath 'com.github.ben-manes:gradle-versions-plugin:0.27.0'
}
}
@@ -94,12 +94,12 @@ dependencies {
compile 'commons-io:commons-io:2.6'
// Config management
compile 'org.yaml:snakeyaml:1.24'
compile 'org.yaml:snakeyaml:1.25'
// Dependencies from old Matrix-java-sdk
compile 'org.apache.commons:commons-lang3:3.9'
compile 'com.squareup.okhttp3:okhttp:4.0.1'
compile 'commons-codec:commons-codec:1.12'
compile 'com.squareup.okhttp3:okhttp:4.2.2'
compile 'commons-codec:commons-codec:1.13'
// ORMLite
compile 'com.j256.ormlite:ormlite-jdbc:5.1'
@@ -114,10 +114,10 @@ dependencies {
compile 'dnsjava:dnsjava:2.1.9'
// HTTP connections
compile 'org.apache.httpcomponents:httpclient:4.5.9'
compile 'org.apache.httpcomponents:httpclient:4.5.10'
// Phone numbers validation
compile 'com.googlecode.libphonenumber:libphonenumber:8.10.15'
compile 'com.googlecode.libphonenumber:libphonenumber:8.10.22'
// E-mail sending
compile 'javax.mail:javax.mail-api:1.6.2'
@@ -133,13 +133,13 @@ dependencies {
compile 'org.xerial:sqlite-jdbc:3.28.0'
// PostgreSQL
compile 'org.postgresql:postgresql:42.2.6'
compile 'org.postgresql:postgresql:42.2.8'
// MariaDB/MySQL
compile 'org.mariadb.jdbc:mariadb-java-client:2.4.2'
compile 'org.mariadb.jdbc:mariadb-java-client:2.5.1'
// Twilio SDK for SMS
compile 'com.twilio.sdk:twilio:7.40.1'
compile 'com.twilio.sdk:twilio:7.45.0'
// SendGrid SDK to send emails from GCE
compile 'com.sendgrid:sendgrid-java:2.2.2'
@@ -148,15 +148,15 @@ dependencies {
compile 'org.zeroturnaround:zt-exec:1.11'
// HTTP server
compile 'io.undertow:undertow-core:2.0.22.Final'
compile 'io.undertow:undertow-core:2.0.27.Final'
// Command parser for AS interface
implementation 'commons-cli:commons-cli:1.4'
testCompile 'junit:junit:4.13-beta-3'
testCompile 'com.github.tomakehurst:wiremock:2.24.0'
testCompile 'com.unboundid:unboundid-ldapsdk:4.0.11'
testCompile 'com.icegreen:greenmail:1.5.10'
testCompile 'junit:junit:4.13-rc-1'
testCompile 'com.github.tomakehurst:wiremock:2.25.1'
testCompile 'com.unboundid:unboundid-ldapsdk:4.0.12'
testCompile 'com.icegreen:greenmail:1.5.11'
}
jar {

View File

@@ -1,5 +1,5 @@
#Thu Jul 04 22:47:59 MSK 2019
distributionUrl=https\://services.gradle.org/distributions/gradle-5.5.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStorePath=wrapper/dists

View File

@@ -20,7 +20,12 @@
package io.kamax.mxisd;
import io.kamax.mxisd.config.MatrixConfig;
import io.kamax.mxisd.config.MxisdConfig;
import io.kamax.mxisd.config.PolicyConfig;
import io.kamax.mxisd.http.undertow.handler.ApiHandler;
import io.kamax.mxisd.http.undertow.handler.AuthorizationHandler;
import io.kamax.mxisd.http.undertow.handler.CheckTermsHandler;
import io.kamax.mxisd.http.undertow.handler.InternalInfoHandler;
import io.kamax.mxisd.http.undertow.handler.OptionsHandler;
import io.kamax.mxisd.http.undertow.handler.SaneHandler;
@@ -31,19 +36,46 @@ import io.kamax.mxisd.http.undertow.handler.auth.RestAuthHandler;
import io.kamax.mxisd.http.undertow.handler.auth.v1.LoginGetHandler;
import io.kamax.mxisd.http.undertow.handler.auth.v1.LoginHandler;
import io.kamax.mxisd.http.undertow.handler.auth.v1.LoginPostHandler;
import io.kamax.mxisd.http.undertow.handler.auth.v2.AccountGetUserInfoHandler;
import io.kamax.mxisd.http.undertow.handler.auth.v2.AccountLogoutHandler;
import io.kamax.mxisd.http.undertow.handler.auth.v2.AccountRegisterHandler;
import io.kamax.mxisd.http.undertow.handler.directory.v1.UserDirectorySearchHandler;
import io.kamax.mxisd.http.undertow.handler.identity.share.EphemeralKeyIsValidHandler;
import io.kamax.mxisd.http.undertow.handler.identity.share.HelloHandler;
import io.kamax.mxisd.http.undertow.handler.identity.share.KeyGetHandler;
import io.kamax.mxisd.http.undertow.handler.identity.share.RegularKeyIsValidHandler;
import io.kamax.mxisd.http.undertow.handler.identity.share.SessionStartHandler;
import io.kamax.mxisd.http.undertow.handler.identity.share.SessionTpidBindHandler;
import io.kamax.mxisd.http.undertow.handler.identity.share.SessionTpidGetValidatedHandler;
import io.kamax.mxisd.http.undertow.handler.identity.share.SessionTpidUnbindHandler;
import io.kamax.mxisd.http.undertow.handler.identity.share.SessionValidationGetHandler;
import io.kamax.mxisd.http.undertow.handler.identity.share.SessionValidationPostHandler;
import io.kamax.mxisd.http.undertow.handler.identity.share.SignEd25519Handler;
import io.kamax.mxisd.http.undertow.handler.identity.share.StoreInviteHandler;
import io.kamax.mxisd.http.undertow.handler.identity.v1.*;
import io.kamax.mxisd.http.undertow.handler.identity.v2.HashDetailsHandler;
import io.kamax.mxisd.http.undertow.handler.identity.v2.HashLookupHandler;
import io.kamax.mxisd.http.undertow.handler.invite.v1.RoomInviteHandler;
import io.kamax.mxisd.http.undertow.handler.profile.v1.InternalProfileHandler;
import io.kamax.mxisd.http.undertow.handler.profile.v1.ProfileHandler;
import io.kamax.mxisd.http.undertow.handler.register.v1.Register3pidRequestTokenHandler;
import io.kamax.mxisd.http.undertow.handler.status.StatusHandler;
import io.kamax.mxisd.http.undertow.handler.status.VersionHandler;
import io.kamax.mxisd.http.undertow.handler.term.v2.AcceptTermsHandler;
import io.kamax.mxisd.http.undertow.handler.term.v2.GetTermsHandler;
import io.kamax.mxisd.matrix.IdentityServiceAPI;
import io.undertow.Handlers;
import io.undertow.Undertow;
import io.undertow.server.HttpHandler;
import io.undertow.server.RoutingHandler;
import io.undertow.util.HttpString;
import io.undertow.util.Methods;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.regex.Pattern;
public class HttpMxisd {
@@ -66,81 +98,150 @@ public class HttpMxisd {
public void start() {
m.start();
HttpHandler helloHandler = SaneHandler.around(new HelloHandler());
HttpHandler asUserHandler = SaneHandler.around(new AsUserHandler(m.getAs()));
HttpHandler asTxnHandler = SaneHandler.around(new AsTransactionHandler(m.getAs()));
HttpHandler asNotFoundHandler = SaneHandler.around(new AsNotFoundHandler(m.getAs()));
HttpHandler storeInvHandler = SaneHandler.around(new StoreInviteHandler(m.getConfig().getServer(), m.getInvite(), m.getKeyManager()));
final RoutingHandler handler = Handlers.routing()
.add("OPTIONS", "/**", SaneHandler.around(new OptionsHandler()))
httpSrv = Undertow.builder().addHttpListener(m.getConfig().getServer().getPort(), "0.0.0.0").setHandler(Handlers.routing()
// Status endpoints
.get(StatusHandler.Path, SaneHandler.around(new StatusHandler()))
.get(VersionHandler.Path, SaneHandler.around(new VersionHandler()))
.add("OPTIONS", "/**", SaneHandler.around(new OptionsHandler()))
// Authentication endpoints
.get(LoginHandler.Path, SaneHandler.around(new LoginGetHandler(m.getAuth(), m.getHttpClient())))
.post(LoginHandler.Path, SaneHandler.around(new LoginPostHandler(m.getAuth())))
.post(RestAuthHandler.Path, SaneHandler.around(new RestAuthHandler(m.getAuth())))
// Status endpoints
.get(StatusHandler.Path, SaneHandler.around(new StatusHandler()))
.get(VersionHandler.Path, SaneHandler.around(new VersionHandler()))
// Account endpoints
.post(AccountRegisterHandler.Path, SaneHandler.around(new AccountRegisterHandler(m.getAccMgr())))
.get(AccountGetUserInfoHandler.Path,
SaneHandler.around(AuthorizationHandler.around(m.getAccMgr(), new AccountGetUserInfoHandler(m.getAccMgr()))))
.post(AccountLogoutHandler.Path,
SaneHandler.around(AuthorizationHandler.around(m.getAccMgr(), new AccountLogoutHandler(m.getAccMgr()))))
// Authentication endpoints
.get(LoginHandler.Path, SaneHandler.around(new LoginGetHandler(m.getAuth(), m.getHttpClient())))
.post(LoginHandler.Path, SaneHandler.around(new LoginPostHandler(m.getAuth())))
.post(RestAuthHandler.Path, SaneHandler.around(new RestAuthHandler(m.getAuth())))
// Directory endpoints
.post(UserDirectorySearchHandler.Path, SaneHandler.around(new UserDirectorySearchHandler(m.getDirectory())))
// Directory endpoints
.post(UserDirectorySearchHandler.Path, SaneHandler.around(new UserDirectorySearchHandler(m.getDirectory())))
// Profile endpoints
.get(ProfileHandler.Path, SaneHandler.around(new ProfileHandler(m.getProfile())))
.get(InternalProfileHandler.Path, SaneHandler.around(new InternalProfileHandler(m.getProfile())))
// Key endpoints
.get(KeyGetHandler.Path, SaneHandler.around(new KeyGetHandler(m.getKeyManager())))
.get(RegularKeyIsValidHandler.Path, SaneHandler.around(new RegularKeyIsValidHandler(m.getKeyManager())))
.get(EphemeralKeyIsValidHandler.Path, SaneHandler.around(new EphemeralKeyIsValidHandler(m.getKeyManager())))
// Registration endpoints
.post(Register3pidRequestTokenHandler.Path,
SaneHandler.around(new Register3pidRequestTokenHandler(m.getReg(), m.getClientDns(), m.getHttpClient())))
// Identity endpoints
.get(HelloHandler.Path, helloHandler)
.get(HelloHandler.Path + "/", helloHandler) // Be lax with possibly trailing slash
.get(SingleLookupHandler.Path, SaneHandler.around(new SingleLookupHandler(m.getConfig(), m.getIdentity(), m.getSign())))
.post(BulkLookupHandler.Path, SaneHandler.around(new BulkLookupHandler(m.getIdentity())))
.post(StoreInviteHandler.Path, storeInvHandler)
.post(SessionStartHandler.Path, SaneHandler.around(new SessionStartHandler(m.getSession())))
.get(SessionValidateHandler.Path, SaneHandler.around(new SessionValidationGetHandler(m.getSession(), m.getConfig())))
.post(SessionValidateHandler.Path, SaneHandler.around(new SessionValidationPostHandler(m.getSession())))
.get(SessionTpidGetValidatedHandler.Path, SaneHandler.around(new SessionTpidGetValidatedHandler(m.getSession())))
.post(SessionTpidBindHandler.Path, SaneHandler.around(new SessionTpidBindHandler(m.getSession(), m.getInvite(), m.getSign())))
.post(SessionTpidUnbindHandler.Path, SaneHandler.around(new SessionTpidUnbindHandler(m.getSession())))
.post(SignEd25519Handler.Path, SaneHandler.around(new SignEd25519Handler(m.getConfig(), m.getInvite(), m.getSign())))
// Invite endpoints
.post(RoomInviteHandler.Path, SaneHandler.around(new RoomInviteHandler(m.getHttpClient(), m.getClientDns(), m.getInvite())))
// Profile endpoints
.get(ProfileHandler.Path, SaneHandler.around(new ProfileHandler(m.getProfile())))
.get(InternalProfileHandler.Path, SaneHandler.around(new InternalProfileHandler(m.getProfile())))
// Application Service endpoints
.get(AsUserHandler.Path, asUserHandler)
.get("/_matrix/app/v1/rooms/**", asNotFoundHandler)
.put(AsTransactionHandler.Path, asTxnHandler)
// Registration endpoints
.post(Register3pidRequestTokenHandler.Path, SaneHandler.around(new Register3pidRequestTokenHandler(m.getReg(), m.getClientDns(), m.getHttpClient())))
.get("/users/{" + AsUserHandler.ID + "}", asUserHandler) // Legacy endpoint
.get("/rooms/**", asNotFoundHandler) // Legacy endpoint
.put("/transactions/{" + AsTransactionHandler.ID + "}", asTxnHandler) // Legacy endpoint
// Invite endpoints
.post(RoomInviteHandler.Path, SaneHandler.around(new RoomInviteHandler(m.getHttpClient(), m.getClientDns(), m.getInvite())))
// Application Service endpoints
.get(AsUserHandler.Path, asUserHandler)
.get("/_matrix/app/v1/rooms/**", asNotFoundHandler)
.put(AsTransactionHandler.Path, asTxnHandler)
.get("/users/{" + AsUserHandler.ID + "}", asUserHandler) // Legacy endpoint
.get("/rooms/**", asNotFoundHandler) // Legacy endpoint
.put("/transactions/{" + AsTransactionHandler.ID + "}", asTxnHandler) // Legacy endpoint
// Banned endpoints
.get(InternalInfoHandler.Path, SaneHandler.around(new InternalInfoHandler()))
).build();
// Banned endpoints
.get(InternalInfoHandler.Path, SaneHandler.around(new InternalInfoHandler()));
keyEndpoints(handler);
identityEndpoints(handler);
termsEndpoints(handler);
hashEndpoints(handler);
httpSrv = Undertow.builder().addHttpListener(m.getConfig().getServer().getPort(), "0.0.0.0").setHandler(handler).build();
httpSrv.start();
}
public void stop() {
// Because it might have never been initialized if an exception is thrown early
if (Objects.nonNull(httpSrv)) httpSrv.stop();
if (Objects.nonNull(httpSrv)) {
httpSrv.stop();
}
m.stop();
}
private void keyEndpoints(RoutingHandler routingHandler) {
addEndpoints(routingHandler, Methods.GET, false,
new KeyGetHandler(m.getKeyManager()),
new RegularKeyIsValidHandler(m.getKeyManager()),
new EphemeralKeyIsValidHandler(m.getKeyManager())
);
}
private void identityEndpoints(RoutingHandler routingHandler) {
// Legacy v1
routingHandler.get(SingleLookupHandler.Path, sane(new SingleLookupHandler(m.getConfig(), m.getIdentity(), m.getSign())));
routingHandler.post(BulkLookupHandler.Path, sane(new BulkLookupHandler(m.getIdentity())));
addEndpoints(routingHandler, Methods.GET, false, new HelloHandler());
addEndpoints(routingHandler, Methods.GET, true,
new SessionValidationGetHandler(m.getSession(), m.getConfig()),
new SessionTpidGetValidatedHandler(m.getSession())
);
addEndpoints(routingHandler, Methods.POST, true,
new StoreInviteHandler(m.getConfig().getServer(), m.getInvite(), m.getKeyManager()),
new SessionStartHandler(m.getSession()),
new SessionValidationPostHandler(m.getSession()),
new SessionTpidBindHandler(m.getSession(), m.getInvite(), m.getSign()),
new SessionTpidUnbindHandler(m.getSession()),
new SignEd25519Handler(m.getConfig(), m.getInvite(), m.getSign())
);
}
private void termsEndpoints(RoutingHandler routingHandler) {
routingHandler.get(GetTermsHandler.PATH, new GetTermsHandler(m.getConfig().getPolicy()));
routingHandler
.post(AcceptTermsHandler.PATH, AuthorizationHandler.around(m.getAccMgr(), sane(new AcceptTermsHandler(m.getAccMgr()))));
}
private void hashEndpoints(RoutingHandler routingHandler) {
routingHandler
.get(HashDetailsHandler.PATH, AuthorizationHandler.around(m.getAccMgr(), sane(new HashDetailsHandler(m.getHashManager()))));
routingHandler.post(HashLookupHandler.Path,
AuthorizationHandler.around(m.getAccMgr(), sane(new HashLookupHandler(m.getIdentity(), m.getHashManager()))));
}
private void addEndpoints(RoutingHandler routingHandler, HttpString method, boolean useAuthorization, ApiHandler... handlers) {
for (ApiHandler handler : handlers) {
attachHandler(routingHandler, method, handler, useAuthorization, sane(handler));
}
}
private void attachHandler(RoutingHandler routingHandler, HttpString method, ApiHandler apiHandler, boolean useAuthorization,
HttpHandler httpHandler) {
MatrixConfig matrixConfig = m.getConfig().getMatrix();
if (matrixConfig.isV1()) {
routingHandler.add(method, apiHandler.getPath(IdentityServiceAPI.V1), httpHandler);
}
if (matrixConfig.isV2()) {
HttpHandler handlerWithTerms = CheckTermsHandler.around(m.getAccMgr(), httpHandler, getPolicyObjects(apiHandler));
HttpHandler wrappedHandler = useAuthorization ? AuthorizationHandler.around(m.getAccMgr(), handlerWithTerms) : handlerWithTerms;
routingHandler.add(method, apiHandler.getPath(IdentityServiceAPI.V2), wrappedHandler);
}
}
@NotNull
private List<PolicyConfig.PolicyObject> getPolicyObjects(ApiHandler apiHandler) {
PolicyConfig policyConfig = m.getConfig().getPolicy();
List<PolicyConfig.PolicyObject> policies = new ArrayList<>();
if (!policyConfig.getPolicies().isEmpty()) {
for (PolicyConfig.PolicyObject policy : policyConfig.getPolicies().values()) {
for (Pattern pattern : policy.getPatterns()) {
if (pattern.matcher(apiHandler.getHandlerPath()).matches()) {
policies.add(policy);
}
}
}
}
return policies;
}
private HttpHandler sane(HttpHandler httpHandler) {
return SaneHandler.around(httpHandler);
}
}

View File

@@ -21,6 +21,7 @@
package io.kamax.mxisd;
import io.kamax.mxisd.as.AppSvcManager;
import io.kamax.mxisd.auth.AccountManager;
import io.kamax.mxisd.auth.AuthManager;
import io.kamax.mxisd.auth.AuthProviders;
import io.kamax.mxisd.backend.IdentityStoreSupplier;
@@ -34,6 +35,7 @@ import io.kamax.mxisd.directory.DirectoryManager;
import io.kamax.mxisd.directory.DirectoryProviders;
import io.kamax.mxisd.dns.ClientDnsOverwrite;
import io.kamax.mxisd.dns.FederationDnsOverwrite;
import io.kamax.mxisd.hash.HashManager;
import io.kamax.mxisd.invitation.InvitationManager;
import io.kamax.mxisd.lookup.ThreePidProviders;
import io.kamax.mxisd.lookup.fetcher.IRemoteIdentityServerFetcher;
@@ -85,6 +87,8 @@ public class Mxisd {
private SessionManager sessMgr;
private NotificationManager notifMgr;
private RegistrationManager regMgr;
private AccountManager accMgr;
private HashManager hashManager;
// HS-specific classes
private Synapse synapse;
@@ -115,7 +119,10 @@ public class Mxisd {
ServiceLoader.load(IdentityStoreSupplier.class).iterator().forEachRemaining(p -> p.accept(this));
ServiceLoader.load(NotificationHandlerSupplier.class).iterator().forEachRemaining(p -> p.accept(this));
idStrategy = new RecursivePriorityLookupStrategy(cfg.getLookup(), ThreePidProviders.get(), bridgeFetcher);
hashManager = new HashManager();
hashManager.init(cfg.getHashing(), ThreePidProviders.get(), store);
idStrategy = new RecursivePriorityLookupStrategy(cfg.getLookup(), ThreePidProviders.get(), bridgeFetcher, hashManager);
pMgr = new ProfileManager(ProfileProviders.get(), clientDns, httpClient);
notifMgr = new NotificationManager(cfg.getNotification(), NotificationHandlers.get());
sessMgr = new SessionManager(cfg, store, notifMgr, resolver, httpClient, signMgr);
@@ -124,6 +131,7 @@ public class Mxisd {
dirMgr = new DirectoryManager(cfg.getDirectory(), clientDns, httpClient, DirectoryProviders.get());
regMgr = new RegistrationManager(cfg.getRegister(), httpClient, clientDns, invMgr);
asHander = new AppSvcManager(this);
accMgr = new AccountManager(store, resolver, getHttpClient(), cfg.getAccountConfig(), cfg.getMatrix());
}
public MxisdConfig getConfig() {
@@ -194,6 +202,14 @@ public class Mxisd {
return synapse;
}
public AccountManager getAccMgr() {
return accMgr;
}
public HashManager getHashManager() {
return hashManager;
}
public void start() {
build();
}

View File

@@ -0,0 +1,160 @@
package io.kamax.mxisd.auth;
import com.google.gson.JsonObject;
import io.kamax.matrix.MatrixID;
import io.kamax.matrix.json.GsonUtil;
import io.kamax.mxisd.config.AccountConfig;
import io.kamax.mxisd.config.MatrixConfig;
import io.kamax.mxisd.config.PolicyConfig;
import io.kamax.mxisd.exception.BadRequestException;
import io.kamax.mxisd.exception.InvalidCredentialsException;
import io.kamax.mxisd.exception.NotFoundException;
import io.kamax.mxisd.http.undertow.handler.auth.v1.LoginGetHandler;
import io.kamax.mxisd.matrix.HomeserverFederationResolver;
import io.kamax.mxisd.storage.IStorage;
import io.kamax.mxisd.storage.ormlite.dao.AccountDao;
import org.apache.http.HttpStatus;
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.time.Instant;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
public class AccountManager {
private static final Logger LOGGER = LoggerFactory.getLogger(AccountManager.class);
private final IStorage storage;
private final HomeserverFederationResolver resolver;
private final CloseableHttpClient httpClient;
private final AccountConfig accountConfig;
private final MatrixConfig matrixConfig;
public AccountManager(IStorage storage, HomeserverFederationResolver resolver,
CloseableHttpClient httpClient, AccountConfig accountConfig, MatrixConfig matrixConfig) {
this.storage = storage;
this.resolver = resolver;
this.httpClient = httpClient;
this.accountConfig = accountConfig;
this.matrixConfig = matrixConfig;
}
public String register(OpenIdToken openIdToken) {
Objects.requireNonNull(openIdToken.getAccessToken(), "Missing required access_token");
Objects.requireNonNull(openIdToken.getTokenType(), "Missing required token type");
Objects.requireNonNull(openIdToken.getMatrixServerName(), "Missing required matrix domain");
LOGGER.info("Registration from the server: {}", openIdToken.getMatrixServerName());
String userId = getUserId(openIdToken);
LOGGER.info("UserId: {}", userId);
String token = UUID.randomUUID().toString();
AccountDao account = new AccountDao(openIdToken.getAccessToken(), openIdToken.getTokenType(),
openIdToken.getMatrixServerName(), openIdToken.getExpiredIn(),
Instant.now().getEpochSecond(), userId, token);
storage.insertToken(account);
LOGGER.info("User {} registered", userId);
return token;
}
private String getUserId(OpenIdToken openIdToken) {
String homeserverURL = resolver.resolve(openIdToken.getMatrixServerName()).toString();
LOGGER.info("Domain resolved: {} => {}", openIdToken.getMatrixServerName(), homeserverURL);
HttpGet getUserInfo = new HttpGet(
homeserverURL + "/_matrix/federation/v1/openid/userinfo?access_token=" + openIdToken.getAccessToken());
String userId;
try (CloseableHttpResponse response = httpClient.execute(getUserInfo)) {
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == HttpStatus.SC_OK) {
String content = EntityUtils.toString(response.getEntity());
LOGGER.trace("Response: {}", content);
JsonObject body = GsonUtil.parseObj(content);
userId = GsonUtil.getStringOrThrow(body, "sub");
} else {
LOGGER.error("Wrong response status: {}", statusCode);
throw new InvalidCredentialsException();
}
} catch (IOException e) {
LOGGER.error("Unable to get user info.", e);
throw new InvalidCredentialsException();
}
checkMXID(userId);
return userId;
}
private void checkMXID(String userId) {
MatrixID mxid;
try {
mxid = MatrixID.asValid(userId);
} catch (IllegalArgumentException e) {
LOGGER.error("Wrong MXID: " + userId, e);
throw new BadRequestException("Wrong MXID");
}
if (getAccountConfig().isAllowOnlyTrustDomains()) {
LOGGER.info("Allow registration only for trust domain.");
if (getMatrixConfig().getDomain().equals(mxid.getDomain())) {
LOGGER.info("Allow user {} to registration", userId);
} else {
LOGGER.error("Deny user {} to registration", userId);
throw new InvalidCredentialsException();
}
} else {
LOGGER.info("Allow registration from any server.");
}
}
public String getUserId(String token) {
return storage.findAccount(token).orElseThrow(NotFoundException::new).getUserId();
}
public AccountDao findAccount(String token) {
AccountDao accountDao = storage.findAccount(token).orElse(null);
if (LOGGER.isInfoEnabled()) {
if (accountDao != null) {
LOGGER.info("Found account for user: {}", accountDao.getUserId());
} else {
LOGGER.warn("Account not found.");
}
}
return accountDao;
}
public void logout(String token) {
String userId = storage.findAccount(token).orElseThrow(InvalidCredentialsException::new).getUserId();
LOGGER.info("Logout: {}", userId);
deleteAccount(token);
}
public void deleteAccount(String token) {
storage.deleteAccepts(token);
storage.deleteToken(token);
}
public void acceptTerm(String token, String url) {
storage.acceptTerm(token, url);
}
public boolean isTermAccepted(String token, List<PolicyConfig.PolicyObject> policies) {
return policies.isEmpty() || storage.isTermAccepted(token, policies);
}
public AccountConfig getAccountConfig() {
return accountConfig;
}
public MatrixConfig getMatrixConfig() {
return matrixConfig;
}
}

View File

@@ -0,0 +1,44 @@
package io.kamax.mxisd.auth;
public class OpenIdToken {
private String accessToken;
private String tokenType;
private String matrixServerName;
private long expiredIn;
public String getAccessToken() {
return accessToken;
}
public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}
public String getTokenType() {
return tokenType;
}
public void setTokenType(String tokenType) {
this.tokenType = tokenType;
}
public String getMatrixServerName() {
return matrixServerName;
}
public void setMatrixServerName(String matrixServerName) {
this.matrixServerName = matrixServerName;
}
public long getExpiredIn() {
return expiredIn;
}
public void setExpiredIn(long expiredIn) {
this.expiredIn = expiredIn;
}
}

View File

@@ -164,6 +164,26 @@ public class ExecIdentityStore extends ExecStore implements IThreePidProvider {
return input.toString();
});
addBulkSuccessMapper(p);
p.withFailureDefault(output -> Collections.emptyList());
return p.execute();
}
@Override
public Iterable<ThreePidMapping> populateHashes() {
Processor<List<ThreePidMapping>> p = new Processor<>();
p.withConfig(cfg.getLookup().getBulk());
addBulkSuccessMapper(p);
p.withFailureDefault(output -> Collections.emptyList());
return p.execute();
}
private void addBulkSuccessMapper(Processor<List<ThreePidMapping>> p) {
p.addSuccessMapper(JsonType, output -> {
if (StringUtils.isBlank(output)) {
return Collections.emptyList();
@@ -188,10 +208,5 @@ public class ExecIdentityStore extends ExecStore implements IThreePidProvider {
throw new InternalServerError("Invalid user type: " + item.getId().getType());
}).collect(Collectors.toList());
});
p.withFailureDefault(output -> Collections.emptyList());
return p.execute();
}
}

View File

@@ -48,6 +48,7 @@ import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public class MemoryIdentityStore implements AuthenticatorProvider, DirectoryProvider, IThreePidProvider, ProfileProvider {
@@ -171,4 +172,11 @@ public class MemoryIdentityStore implements AuthenticatorProvider, DirectoryProv
}).orElseGet(BackendAuthResult::failure);
}
@Override
public Iterable<ThreePidMapping> populateHashes() {
return cfg.getIdentities().stream()
.map(mic -> mic.getThreepids().stream().map(mtp -> new ThreePidMapping(mtp.getMedium(), mtp.getAddress(), mic.getUsername())))
.flatMap(s -> s).collect(
Collectors.toList());
}
}

View File

@@ -36,6 +36,7 @@ import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
@@ -104,4 +105,27 @@ public abstract class SqlThreePidProvider implements IThreePidProvider {
return new ArrayList<>();
}
@Override
public Iterable<ThreePidMapping> populateHashes() {
if (StringUtils.isBlank(cfg.getLookup().getQuery())) {
log.warn("Lookup query not configured, skip.");
return Collections.emptyList();
}
List<ThreePidMapping> result = new ArrayList<>();
try (Connection connection = pool.get()) {
PreparedStatement statement = connection.prepareStatement(cfg.getLookup().getQuery());
try (ResultSet resultSet = statement.executeQuery()) {
while (resultSet.next()) {
String mxid = resultSet.getString("mxid");
String medium = resultSet.getString("medium");
String address = resultSet.getString("address");
result.add(new ThreePidMapping(medium, address, mxid));
}
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
return result;
}
}

View File

@@ -0,0 +1,8 @@
package io.kamax.mxisd.config;
public enum AcceptingPolicy {
ALL,
ANY
}

View File

@@ -0,0 +1,24 @@
package io.kamax.mxisd.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class AccountConfig {
private final static Logger log = LoggerFactory.getLogger(DirectoryConfig.class);
private boolean allowOnlyTrustDomains = true;
public boolean isAllowOnlyTrustDomains() {
return allowOnlyTrustDomains;
}
public void setAllowOnlyTrustDomains(boolean allowOnlyTrustDomains) {
this.allowOnlyTrustDomains = allowOnlyTrustDomains;
}
public void build() {
log.info("--- Account config ---");
log.info("Allow registration only for trust domain: {}", isAllowOnlyTrustDomains());
}
}

View File

@@ -0,0 +1,96 @@
package io.kamax.mxisd.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
public class HashingConfig {
private static final Logger LOGGER = LoggerFactory.getLogger(HashingConfig.class);
private boolean enabled = false;
private int pepperLength = 10;
private RotationPolicyEnum rotationPolicy;
private HashStorageEnum hashStorageType;
private long delay = 10;
private List<Algorithm> algorithms = new ArrayList<>();
public void build() {
if (isEnabled()) {
LOGGER.info("--- Hash configuration ---");
LOGGER.info(" Pepper length: {}", getPepperLength());
LOGGER.info(" Rotation policy: {}", getRotationPolicy());
LOGGER.info(" Hash storage type: {}", getHashStorageType());
if (RotationPolicyEnum.PER_SECONDS == rotationPolicy) {
LOGGER.info(" Rotation delay: {}", delay);
}
} else {
LOGGER.info("Hash configuration disabled, used only `none` pepper.");
}
}
public enum Algorithm {
NONE,
SHA256
}
public enum RotationPolicyEnum {
PER_REQUESTS,
PER_SECONDS
}
public enum HashStorageEnum {
IN_MEMORY,
SQL
}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public int getPepperLength() {
return pepperLength;
}
public void setPepperLength(int pepperLength) {
this.pepperLength = pepperLength;
}
public RotationPolicyEnum getRotationPolicy() {
return rotationPolicy;
}
public void setRotationPolicy(RotationPolicyEnum rotationPolicy) {
this.rotationPolicy = rotationPolicy;
}
public HashStorageEnum getHashStorageType() {
return hashStorageType;
}
public void setHashStorageType(HashStorageEnum hashStorageType) {
this.hashStorageType = hashStorageType;
}
public long getDelay() {
return delay;
}
public void setDelay(long delay) {
this.delay = delay;
}
public List<Algorithm> getAlgorithms() {
return algorithms;
}
public void setAlgorithms(List<Algorithm> algorithms) {
this.algorithms = algorithms;
}
}

View File

@@ -63,6 +63,8 @@ public class MatrixConfig {
private String domain;
private Identity identity = new Identity();
private boolean v1 = true;
private boolean v2 = true;
public String getDomain() {
return domain;
@@ -80,6 +82,22 @@ public class MatrixConfig {
this.identity = identity;
}
public boolean isV1() {
return v1;
}
public void setV1(boolean v1) {
this.v1 = v1;
}
public boolean isV2() {
return v2;
}
public void setV2(boolean v2) {
this.v2 = v2;
}
public void build() {
log.info("--- Matrix config ---");
@@ -90,6 +108,11 @@ public class MatrixConfig {
log.info("Domain: {}", getDomain());
log.info("Identity:");
log.info("\tServers: {}", GsonUtil.get().toJson(identity.getServers()));
log.info("API v1: {}", v1);
log.info("API v2: {}", v2);
if (v1) {
log.warn("API v1 is deprecated via MSC2140: https://github.com/matrix-org/matrix-doc/pull/2140 and will be deleted in future releases.");
log.warn("Please upgrade your homeserver and enable only API v2.");
}
}
}

View File

@@ -92,6 +92,7 @@ public class MxisdConfig {
private AppServiceConfig appsvc = new AppServiceConfig();
private AuthenticationConfig auth = new AuthenticationConfig();
private DirectoryConfig directory = new DirectoryConfig();
private AccountConfig accountConfig = new AccountConfig();
private Dns dns = new Dns();
private ExecConfig exec = new ExecConfig();
private FirebaseConfig firebase = new FirebaseConfig();
@@ -114,6 +115,8 @@ public class MxisdConfig {
private ThreePidConfig threepid = new ThreePidConfig();
private ViewConfig view = new ViewConfig();
private WordpressConfig wordpress = new WordpressConfig();
private PolicyConfig policy = new PolicyConfig();
private HashingConfig hashing = new HashingConfig();
public AppServiceConfig getAppsvc() {
return appsvc;
@@ -131,6 +134,14 @@ public class MxisdConfig {
this.auth = auth;
}
public AccountConfig getAccountConfig() {
return accountConfig;
}
public void setAccountConfig(AccountConfig accountConfig) {
this.accountConfig = accountConfig;
}
public DirectoryConfig getDirectory() {
return directory;
}
@@ -315,6 +326,22 @@ public class MxisdConfig {
this.wordpress = wordpress;
}
public PolicyConfig getPolicy() {
return policy;
}
public void setPolicy(PolicyConfig policy) {
this.policy = policy;
}
public HashingConfig getHashing() {
return hashing;
}
public void setHashing(HashingConfig hashing) {
this.hashing = hashing;
}
public MxisdConfig inMemory() {
getKey().setPath(":memory:");
getStorage().getProvider().getSqlite().setDatabase(":memory:");
@@ -330,6 +357,7 @@ public class MxisdConfig {
getAppsvc().build();
getAuth().build();
getAccountConfig().build();
getDirectory().build();
getExec().build();
getFirebase().build();
@@ -352,6 +380,8 @@ public class MxisdConfig {
getThreepid().build();
getView().build();
getWordpress().build();
getPolicy().build();
getHashing().build();
return this;
}

View File

@@ -0,0 +1,112 @@
package io.kamax.mxisd.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
public class PolicyConfig {
private static final Logger LOGGER = LoggerFactory.getLogger(PolicyConfig.class);
private Map<String, PolicyObject> policies = new HashMap<>();
public static class TermObject {
private String name;
private String url;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
}
public static class PolicyObject {
private String version;
private Map<String, TermObject> terms;
private List<String> regexp = new ArrayList<>();
private transient List<Pattern> patterns = new ArrayList<>();
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
public Map<String, TermObject> getTerms() {
return terms;
}
public void setTerms(Map<String, TermObject> terms) {
this.terms = terms;
}
public List<String> getRegexp() {
return regexp;
}
public void setRegexp(List<String> regexp) {
this.regexp = regexp;
}
public List<Pattern> getPatterns() {
return patterns;
}
}
public Map<String, PolicyObject> getPolicies() {
return policies;
}
public void setPolicies(Map<String, PolicyObject> policies) {
this.policies = policies;
}
public void build() {
LOGGER.info("--- Policy Config ---");
if (getPolicies().isEmpty()) {
LOGGER.info("Empty");
} else {
for (Map.Entry<String, PolicyObject> policyObjectItem : getPolicies().entrySet()) {
PolicyObject policyObject = policyObjectItem.getValue();
StringBuilder sb = new StringBuilder();
sb.append("Policy \"").append(policyObjectItem.getKey()).append("\"\n");
sb.append(" version: ").append(policyObject.getVersion()).append("\n");
for (String regexp : policyObjectItem.getValue().getRegexp()) {
sb.append(" - ").append(regexp).append("\n");
policyObjectItem.getValue().getPatterns().add(Pattern.compile(regexp));
}
sb.append(" terms:\n");
for (Map.Entry<String, TermObject> termItem : policyObject.getTerms().entrySet()) {
sb.append(" - lang: ").append(termItem.getKey()).append("\n");
sb.append(" name: ").append(termItem.getValue().getName()).append("\n");
sb.append(" url: ").append(termItem.getValue().getUrl()).append("\n");
}
LOGGER.info(sb.toString());
}
}
}
}

View File

@@ -79,5 +79,4 @@ public class ServerConfig {
log.info("Port: {}", getPort());
log.info("Public URL: {}", getPublicUrl());
}
}

View File

@@ -124,6 +124,18 @@ public abstract class SqlConfig {
}
public static class Lookup {
private String query;
public String getQuery() {
return query;
}
public void setQuery(String query) {
this.query = query;
}
}
public static class Identity {
private Boolean enabled;
@@ -264,6 +276,7 @@ public abstract class SqlConfig {
private Directory directory = new Directory();
private Identity identity = new Identity();
private Profile profile = new Profile();
private Lookup lookup = new Lookup();
public boolean isEnabled() {
return enabled;
@@ -321,6 +334,14 @@ public abstract class SqlConfig {
this.profile = profile;
}
public Lookup getLookup() {
return lookup;
}
public void setLookup(Lookup lookup) {
this.lookup = lookup;
}
protected abstract String getProviderName();
public void build() {
@@ -354,6 +375,7 @@ public abstract class SqlConfig {
log.info("Identity type: {}", getIdentity().getType());
log.info("3PID mapping query: {}", getIdentity().getQuery());
log.info("Identity medium queries: {}", GsonUtil.build().toJson(getIdentity().getMedium()));
log.info("Lookup query: {}", getLookup().getQuery());
log.info("Profile:");
log.info(" Enabled: {}", getProfile().isEnabled());
if (getProfile().isEnabled()) {

View File

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

View File

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

View File

@@ -0,0 +1,51 @@
package io.kamax.mxisd.hash;
import io.kamax.matrix.codec.MxSha256;
import io.kamax.mxisd.config.HashingConfig;
import io.kamax.mxisd.hash.storage.HashStorage;
import io.kamax.mxisd.lookup.ThreePidMapping;
import io.kamax.mxisd.lookup.provider.IThreePidProvider;
import org.apache.commons.lang3.RandomStringUtils;
import java.util.List;
public class HashEngine {
private final List<? extends IThreePidProvider> providers;
private final HashStorage hashStorage;
private final MxSha256 sha256 = new MxSha256();
private final HashingConfig config;
private String pepper;
public HashEngine(List<? extends IThreePidProvider> providers, HashStorage hashStorage, HashingConfig config) {
this.providers = providers;
this.hashStorage = hashStorage;
this.config = config;
}
public void updateHashes() {
synchronized (hashStorage) {
this.pepper = newPepper();
hashStorage.clear();
for (IThreePidProvider provider : providers) {
for (ThreePidMapping pidMapping : provider.populateHashes()) {
hashStorage.add(pidMapping, hash(pidMapping));
}
}
}
}
public String getPepper() {
synchronized (hashStorage) {
return pepper;
}
}
protected String hash(ThreePidMapping pidMapping) {
return sha256.hash(pidMapping.getMedium() + " " + pidMapping.getValue() + " " + getPepper());
}
protected String newPepper() {
return RandomStringUtils.random(config.getPepperLength());
}
}

View File

@@ -0,0 +1,91 @@
package io.kamax.mxisd.hash;
import io.kamax.mxisd.config.HashingConfig;
import io.kamax.mxisd.hash.rotation.HashRotationStrategy;
import io.kamax.mxisd.hash.rotation.NoOpRotationStrategy;
import io.kamax.mxisd.hash.rotation.RotationPerRequests;
import io.kamax.mxisd.hash.rotation.TimeBasedRotation;
import io.kamax.mxisd.hash.storage.EmptyStorage;
import io.kamax.mxisd.hash.storage.HashStorage;
import io.kamax.mxisd.hash.storage.InMemoryHashStorage;
import io.kamax.mxisd.hash.storage.SqlHashStorage;
import io.kamax.mxisd.lookup.provider.IThreePidProvider;
import io.kamax.mxisd.storage.IStorage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
public class HashManager {
private static final Logger LOGGER = LoggerFactory.getLogger(HashManager.class);
private HashEngine hashEngine;
private HashRotationStrategy rotationStrategy;
private HashStorage hashStorage;
private HashingConfig config;
private IStorage storage;
private AtomicBoolean configured = new AtomicBoolean(false);
public void init(HashingConfig config, List<? extends IThreePidProvider> providers, IStorage storage) {
this.config = config;
this.storage = storage;
initStorage();
hashEngine = new HashEngine(providers, getHashStorage(), config);
initRotationStrategy();
configured.set(true);
}
private void initStorage() {
if (config.isEnabled()) {
switch (config.getHashStorageType()) {
case IN_MEMORY:
this.hashStorage = new InMemoryHashStorage();
break;
case SQL:
this.hashStorage = new SqlHashStorage(storage);
break;
default:
throw new IllegalArgumentException("Unknown storage type: " + config.getHashStorageType());
}
} else {
this.hashStorage = new EmptyStorage();
}
}
private void initRotationStrategy() {
if (config.isEnabled()) {
switch (config.getRotationPolicy()) {
case PER_REQUESTS:
this.rotationStrategy = new RotationPerRequests();
break;
case PER_SECONDS:
this.rotationStrategy = new TimeBasedRotation(config.getDelay());
break;
default:
throw new IllegalArgumentException("Unknown rotation type: " + config.getHashStorageType());
}
} else {
this.rotationStrategy = new NoOpRotationStrategy();
}
this.rotationStrategy.register(getHashEngine());
}
public HashEngine getHashEngine() {
return hashEngine;
}
public HashRotationStrategy getRotationStrategy() {
return rotationStrategy;
}
public HashStorage getHashStorage() {
return hashStorage;
}
public HashingConfig getConfig() {
return config;
}
}

View File

@@ -0,0 +1,16 @@
package io.kamax.mxisd.hash.rotation;
import io.kamax.mxisd.hash.HashEngine;
public interface HashRotationStrategy {
void register(HashEngine hashEngine);
HashEngine getHashEngine();
void newRequest();
default void trigger() {
getHashEngine().updateHashes();
}
}

View File

@@ -0,0 +1,23 @@
package io.kamax.mxisd.hash.rotation;
import io.kamax.mxisd.hash.HashEngine;
public class NoOpRotationStrategy implements HashRotationStrategy {
private HashEngine hashEngine;
@Override
public void register(HashEngine hashEngine) {
this.hashEngine = hashEngine;
}
@Override
public HashEngine getHashEngine() {
return hashEngine;
}
@Override
public void newRequest() {
// nothing to do
}
}

View File

@@ -0,0 +1,30 @@
package io.kamax.mxisd.hash.rotation;
import io.kamax.mxisd.hash.HashEngine;
import java.util.concurrent.atomic.AtomicInteger;
public class RotationPerRequests implements HashRotationStrategy {
private HashEngine hashEngine;
private final AtomicInteger counter = new AtomicInteger(0);
@Override
public void register(HashEngine hashEngine) {
this.hashEngine = hashEngine;
}
@Override
public HashEngine getHashEngine() {
return hashEngine;
}
@Override
public synchronized void newRequest() {
int newValue = counter.incrementAndGet();
if (newValue >= 10) {
counter.set(0);
trigger();
}
}
}

View File

@@ -0,0 +1,34 @@
package io.kamax.mxisd.hash.rotation;
import io.kamax.mxisd.hash.HashEngine;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class TimeBasedRotation implements HashRotationStrategy {
private final long delay;
private HashEngine hashEngine;
private final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
public TimeBasedRotation(long delay) {
this.delay = delay;
}
@Override
public void register(HashEngine hashEngine) {
this.hashEngine = hashEngine;
Runtime.getRuntime().addShutdownHook(new Thread(executorService::shutdown));
executorService.scheduleWithFixedDelay(this::trigger, 0, delay, TimeUnit.SECONDS);
}
@Override
public HashEngine getHashEngine() {
return hashEngine;
}
@Override
public void newRequest() {
}
}

View File

@@ -0,0 +1,25 @@
package io.kamax.mxisd.hash.storage;
import io.kamax.mxisd.lookup.ThreePidMapping;
import org.apache.commons.lang3.tuple.Pair;
import java.util.Collection;
import java.util.Collections;
public class EmptyStorage implements HashStorage {
@Override
public Collection<Pair<String, ThreePidMapping>> find(Iterable<String> hashes) {
return Collections.emptyList();
}
@Override
public void add(ThreePidMapping pidMapping, String hash) {
}
@Override
public void clear() {
}
}

View File

@@ -0,0 +1,15 @@
package io.kamax.mxisd.hash.storage;
import io.kamax.mxisd.lookup.ThreePidMapping;
import org.apache.commons.lang3.tuple.Pair;
import java.util.Collection;
public interface HashStorage {
Collection<Pair<String, ThreePidMapping>> find(Iterable<String> hashes);
void add(ThreePidMapping pidMapping, String hash);
void clear();
}

View File

@@ -0,0 +1,37 @@
package io.kamax.mxisd.hash.storage;
import io.kamax.mxisd.lookup.ThreePidMapping;
import org.apache.commons.lang3.tuple.Pair;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class InMemoryHashStorage implements HashStorage {
private final Map<String, ThreePidMapping> mapping = new ConcurrentHashMap<>();
@Override
public Collection<Pair<String, ThreePidMapping>> find(Iterable<String> hashes) {
List<Pair<String, ThreePidMapping>> result = new ArrayList<>();
for (String hash : hashes) {
ThreePidMapping pidMapping = mapping.get(hash);
if (pidMapping != null) {
result.add(Pair.of(hash, pidMapping));
}
}
return result;
}
@Override
public void add(ThreePidMapping pidMapping, String hash) {
mapping.put(hash, pidMapping);
}
@Override
public void clear() {
mapping.clear();
}
}

View File

@@ -0,0 +1,31 @@
package io.kamax.mxisd.hash.storage;
import io.kamax.mxisd.lookup.ThreePidMapping;
import io.kamax.mxisd.storage.IStorage;
import org.apache.commons.lang3.tuple.Pair;
import java.util.Collection;
public class SqlHashStorage implements HashStorage {
private final IStorage storage;
public SqlHashStorage(IStorage storage) {
this.storage = storage;
}
@Override
public Collection<Pair<String, ThreePidMapping>> find(Iterable<String> hashes) {
return storage.findHashes(hashes);
}
@Override
public void add(ThreePidMapping pidMapping, String hash) {
storage.addHash(pidMapping.getMxid(), pidMapping.getMedium(), pidMapping.getValue(), hash);
}
@Override
public void clear() {
storage.clearHashes();
}
}

View File

@@ -25,8 +25,6 @@ public class IsAPIv1 {
public static final String Base = "/_matrix/identity/api/v1";
public static String getValidate(String medium, String sid, String secret, String token) {
// FIXME use some kind of URLBuilder
return Base + "/validate/" + medium + "/submitToken?sid=" + sid + "&client_secret=" + secret + "&token=" + token;
return String.format("%s/validate/%s/submitToken?sid=%s&client_secret=%s&token=%s", Base, medium, sid, secret, token);
}
}

View File

@@ -0,0 +1,31 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Kamax Sarl
*
* https://www.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.http;
public class IsAPIv2 {
public static final String Base = "/_matrix/identity/v2";
public static String getValidate(String medium, String sid, String secret, String token) {
return String.format("%s/validate/%s/submitToken?sid=%s&client_secret=%s&token=%s", Base, medium, sid, secret, token);
}
}

View File

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

View File

@@ -0,0 +1,55 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Kamax Sarl
*
* https://www.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.http.io.identity;
import java.util.ArrayList;
import java.util.List;
public class ClientHashLookupRequest {
private String algorithm;
private String pepper;
private List<String> addresses = new ArrayList<>();
public String getAlgorithm() {
return algorithm;
}
public void setAlgorithm(String algorithm) {
this.algorithm = algorithm;
}
public String getPepper() {
return pepper;
}
public void setPepper(String pepper) {
this.pepper = pepper;
}
public List<String> getAddresses() {
return addresses;
}
public void setAddresses(List<String> addresses) {
this.addresses = addresses;
}
}

View File

@@ -0,0 +1,22 @@
package io.kamax.mxisd.http.undertow.handler;
import io.kamax.mxisd.http.IsAPIv1;
import io.kamax.mxisd.http.IsAPIv2;
import io.kamax.mxisd.matrix.IdentityServiceAPI;
import io.undertow.server.HttpHandler;
public interface ApiHandler extends HttpHandler {
default String getPath(IdentityServiceAPI api) {
switch (api) {
case V2:
return IsAPIv2.Base + getHandlerPath();
case V1:
return IsAPIv1.Base + getHandlerPath();
default:
throw new IllegalArgumentException("Unknown api version: " + api);
}
}
String getHandlerPath();
}

View File

@@ -0,0 +1,69 @@
/*
* 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.http.undertow.handler;
import io.kamax.mxisd.auth.AccountManager;
import io.kamax.mxisd.exception.InvalidCredentialsException;
import io.kamax.mxisd.storage.ormlite.dao.AccountDao;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class AuthorizationHandler extends BasicHttpHandler {
private static final Logger log = LoggerFactory.getLogger(AuthorizationHandler.class);
private final AccountManager accountManager;
private final HttpHandler child;
public static AuthorizationHandler around(AccountManager accountManager, HttpHandler child) {
return new AuthorizationHandler(accountManager, child);
}
private AuthorizationHandler(AccountManager accountManager, HttpHandler child) {
this.accountManager = accountManager;
this.child = child;
}
@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
String token = findAccessToken(exchange).orElse(null);
if (token == null) {
log.error("Unauthorized request from: {}", exchange.getHostAndPort());
throw new InvalidCredentialsException();
}
AccountDao account = accountManager.findAccount(token);
if (account == null) {
log.error("Account not found from request from: {}", exchange.getHostAndPort());
throw new InvalidCredentialsException();
}
if (account.getExpiresIn() < System.currentTimeMillis()) {
log.error("Account for '{}' from: {}", account.getUserId(), exchange.getHostAndPort());
accountManager.deleteAccount(token);
throw new InvalidCredentialsException();
}
log.trace("Access for '{}' allowed", account.getUserId());
child.handleRequest(exchange);
}
}

View File

@@ -28,6 +28,7 @@ import io.kamax.mxisd.exception.AccessTokenNotFoundException;
import io.kamax.mxisd.exception.HttpMatrixException;
import io.kamax.mxisd.exception.InternalServerError;
import io.kamax.mxisd.proxy.Response;
import io.kamax.mxisd.util.OptionalUtil;
import io.kamax.mxisd.util.RestClientUtils;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
@@ -55,6 +56,24 @@ public abstract class BasicHttpHandler implements HttpHandler {
private static final Logger log = LoggerFactory.getLogger(BasicHttpHandler.class);
protected final static String headerName = "Authorization";
protected final static String headerValuePrefix = "Bearer ";
private final static String parameterName = "access_token";
Optional<String> findAccessTokenInHeaders(HttpServerExchange exchange) {
return Optional.ofNullable(exchange.getRequestHeaders().getFirst(headerName))
.filter(header -> StringUtils.startsWith(header, headerValuePrefix))
.map(header -> header.substring(headerValuePrefix.length()));
}
Optional<String> findAccessTokenInQuery(HttpServerExchange exchange) {
return Optional.ofNullable(exchange.getQueryParameters().getOrDefault(parameterName, new LinkedList<>()).peekFirst());
}
public Optional<String> findAccessToken(HttpServerExchange exchange) {
return OptionalUtil.findFirst(() -> findAccessTokenInHeaders(exchange), () -> findAccessTokenInQuery(exchange));
}
protected String getAccessToken(HttpServerExchange exchange) {
return Optional.ofNullable(exchange.getRequestHeaders().getFirst("Authorization"))
.flatMap(v -> {

View File

@@ -0,0 +1,70 @@
/*
* 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.http.undertow.handler;
import io.kamax.mxisd.auth.AccountManager;
import io.kamax.mxisd.config.PolicyConfig;
import io.kamax.mxisd.exception.InvalidCredentialsException;
import io.kamax.mxisd.storage.ormlite.dao.AccountDao;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
public class CheckTermsHandler extends BasicHttpHandler {
private static final Logger log = LoggerFactory.getLogger(CheckTermsHandler.class);
private final AccountManager accountManager;
private final HttpHandler child;
private final List<PolicyConfig.PolicyObject> policies;
public static CheckTermsHandler around(AccountManager accountManager, HttpHandler child, List<PolicyConfig.PolicyObject> policies) {
return new CheckTermsHandler(accountManager, child, policies);
}
private CheckTermsHandler(AccountManager accountManager, HttpHandler child,
List<PolicyConfig.PolicyObject> policies) {
this.accountManager = accountManager;
this.child = child;
this.policies = policies;
}
@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
String token = findAccessToken(exchange).orElse(null);
if (token == null) {
log.error("Unauthorized request from: {}", exchange.getHostAndPort());
throw new InvalidCredentialsException();
}
if (!accountManager.isTermAccepted(token, policies)) {
log.error("Non accepting request from: {}", exchange.getHostAndPort());
throw new InvalidCredentialsException();
}
log.trace("Access granted");
child.handleRequest(exchange);
}
}

View File

@@ -21,35 +21,11 @@
package io.kamax.mxisd.http.undertow.handler;
import io.kamax.mxisd.exception.AccessTokenNotFoundException;
import io.kamax.mxisd.util.OptionalUtil;
import io.undertow.server.HttpServerExchange;
import org.apache.commons.lang3.StringUtils;
import java.util.LinkedList;
import java.util.Optional;
public abstract class HomeserverProxyHandler extends BasicHttpHandler {
protected final static String headerName = "Authorization";
protected final static String headerValuePrefix = "Bearer ";
private final static String parameterName = "access_token";
Optional<String> findAccessTokenInHeaders(HttpServerExchange exchange) {
return Optional.ofNullable(exchange.getRequestHeaders().getFirst(headerName))
.filter(header -> StringUtils.startsWith(header, headerValuePrefix))
.map(header -> header.substring(headerValuePrefix.length()));
}
Optional<String> findAccessTokenInQuery(HttpServerExchange exchange) {
return Optional.ofNullable(exchange.getQueryParameters().getOrDefault(parameterName, new LinkedList<>()).peekFirst());
}
public Optional<String> findAccessToken(HttpServerExchange exchange) {
return OptionalUtil.findFirst(() -> findAccessTokenInHeaders(exchange), () -> findAccessTokenInQuery(exchange));
}
public String getAccessToken(HttpServerExchange exchange) {
return findAccessToken(exchange).orElseThrow(AccessTokenNotFoundException::new);
}
}

View File

@@ -87,6 +87,10 @@ public class SaneHandler extends BasicHttpHandler {
respond(exchange, HttpStatus.SC_NOT_FOUND, "M_NOT_FOUND", e.getMessage());
} catch (NotImplementedException e) {
respond(exchange, HttpStatus.SC_NOT_IMPLEMENTED, "M_NOT_IMPLEMENTED", e.getMessage());
} catch (InvalidPepperException e) {
respond(exchange, HttpStatus.SC_BAD_REQUEST, "M_INVALID_PEPPER", e.getMessage());
} catch (InvalidParamException e) {
respond(exchange, HttpStatus.SC_BAD_REQUEST, "M_INVALID_PARAM", e.getMessage());
} catch (FeatureNotAvailable e) {
if (StringUtils.isNotBlank(e.getInternalReason())) {
log.error("Feature not available: {}", e.getInternalReason());

View File

@@ -0,0 +1,52 @@
/*
* 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.http.undertow.handler.auth.v2;
import io.kamax.matrix.json.GsonUtil;
import io.kamax.mxisd.auth.AccountManager;
import io.kamax.mxisd.exception.InvalidCredentialsException;
import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler;
import io.undertow.server.HttpServerExchange;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class AccountGetUserInfoHandler extends BasicHttpHandler {
public static final String Path = "/_matrix/identity/v2/account";
private static final Logger LOGGER = LoggerFactory.getLogger(AccountGetUserInfoHandler.class);
private final AccountManager accountManager;
public AccountGetUserInfoHandler(AccountManager accountManager) {
this.accountManager = accountManager;
}
@Override
public void handleRequest(HttpServerExchange exchange) {
LOGGER.info("Get User Info.");
String token = findAccessToken(exchange).orElseThrow(InvalidCredentialsException::new);
String userId = accountManager.getUserId(token);
LOGGER.info("Account found: {}", userId);
respond(exchange, GsonUtil.makeObj("user_id", userId));
}
}

View File

@@ -0,0 +1,51 @@
/*
* 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.http.undertow.handler.auth.v2;
import io.kamax.mxisd.auth.AccountManager;
import io.kamax.mxisd.exception.InvalidCredentialsException;
import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler;
import io.undertow.server.HttpServerExchange;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class AccountLogoutHandler extends BasicHttpHandler {
public static final String Path = "/_matrix/identity/v2/account/logout";
private static final Logger LOGGER = LoggerFactory.getLogger(AccountLogoutHandler.class);
private final AccountManager accountManager;
public AccountLogoutHandler(AccountManager accountManager) {
this.accountManager = accountManager;
}
@Override
public void handleRequest(HttpServerExchange exchange) {
LOGGER.info("Logout.");
String token = findAccessToken(exchange).orElseThrow(InvalidCredentialsException::new);
accountManager.logout(token);
respondJson(exchange, "{}");
}
}

View File

@@ -0,0 +1,57 @@
/*
* 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.http.undertow.handler.auth.v2;
import io.kamax.matrix.json.GsonUtil;
import io.kamax.mxisd.auth.AccountManager;
import io.kamax.mxisd.auth.OpenIdToken;
import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler;
import io.undertow.server.HttpServerExchange;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Date;
public class AccountRegisterHandler extends BasicHttpHandler {
public static final String Path = "/_matrix/identity/v2/account/register";
private static final Logger LOGGER = LoggerFactory.getLogger(AccountRegisterHandler.class);
private final AccountManager accountManager;
public AccountRegisterHandler(AccountManager accountManager) {
this.accountManager = accountManager;
}
@Override
public void handleRequest(HttpServerExchange exchange) {
OpenIdToken openIdToken = parseJsonTo(exchange, OpenIdToken.class);
if (LOGGER.isInfoEnabled()) {
LOGGER.info("Registration from domain: {}, expired at {}", openIdToken.getMatrixServerName(),
new Date(openIdToken.getExpiredIn()));
}
String token = accountManager.register(openIdToken);
respond(exchange, GsonUtil.makeObj("token", token));
}
}

View File

@@ -18,18 +18,16 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.http.undertow.handler.identity.v1;
package io.kamax.mxisd.http.undertow.handler.identity.share;
import io.kamax.mxisd.crypto.KeyManager;
import io.kamax.mxisd.crypto.KeyType;
import io.kamax.mxisd.http.IsAPIv1;
import io.kamax.mxisd.http.undertow.handler.ApiHandler;
import io.undertow.server.HttpServerExchange;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class EphemeralKeyIsValidHandler extends KeyIsValidHandler {
public static final String Path = IsAPIv1.Base + "/pubkey/ephemeral/isvalid";
public class EphemeralKeyIsValidHandler extends KeyIsValidHandler implements ApiHandler {
private static final Logger log = LoggerFactory.getLogger(EphemeralKeyIsValidHandler.class);
@@ -48,4 +46,8 @@ public class EphemeralKeyIsValidHandler extends KeyIsValidHandler {
respondJson(exchange, mgr.isValid(KeyType.Ephemeral, pubKey) ? validKey : invalidKey);
}
@Override
public String getHandlerPath() {
return "/pubkey/ephemeral/isvalid";
}
}

View File

@@ -18,19 +18,21 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.http.undertow.handler.identity.v1;
package io.kamax.mxisd.http.undertow.handler.identity.share;
import io.kamax.mxisd.http.IsAPIv1;
import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler;
import io.kamax.mxisd.http.undertow.handler.ApiHandler;
import io.undertow.server.HttpServerExchange;
public class HelloHandler extends BasicHttpHandler {
public static final String Path = IsAPIv1.Base;
public class HelloHandler extends BasicHttpHandler implements ApiHandler {
@Override
public void handleRequest(HttpServerExchange exchange) {
respondJson(exchange, "{}");
}
@Override
public String getHandlerPath() {
return "";
}
}

View File

@@ -18,23 +18,22 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.http.undertow.handler.identity.v1;
package io.kamax.mxisd.http.undertow.handler.identity.share;
import com.google.gson.JsonObject;
import io.kamax.mxisd.crypto.GenericKeyIdentifier;
import io.kamax.mxisd.crypto.KeyManager;
import io.kamax.mxisd.crypto.KeyType;
import io.kamax.mxisd.http.IsAPIv1;
import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler;
import io.kamax.mxisd.http.undertow.handler.ApiHandler;
import io.undertow.server.HttpServerExchange;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class KeyGetHandler extends BasicHttpHandler {
public class KeyGetHandler extends BasicHttpHandler implements ApiHandler {
public static final String Key = "key";
public static final String Path = IsAPIv1.Base + "/pubkey/{" + Key + "}";
private transient final Logger log = LoggerFactory.getLogger(KeyGetHandler.class);
@@ -61,4 +60,8 @@ public class KeyGetHandler extends BasicHttpHandler {
respond(exchange, obj);
}
@Override
public String getHandlerPath() {
return "/pubkey/{" + Key + "}";
}
}

View File

@@ -18,7 +18,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.http.undertow.handler.identity.v1;
package io.kamax.mxisd.http.undertow.handler.identity.share;
import io.kamax.matrix.json.GsonUtil;
import io.kamax.mxisd.http.io.identity.KeyValidityJson;

View File

@@ -18,7 +18,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.http.undertow.handler.identity.v1;
package io.kamax.mxisd.http.undertow.handler.identity.share;
import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler;
import io.kamax.mxisd.lookup.ALookupRequest;

View File

@@ -18,18 +18,16 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.http.undertow.handler.identity.v1;
package io.kamax.mxisd.http.undertow.handler.identity.share;
import io.kamax.mxisd.crypto.KeyManager;
import io.kamax.mxisd.crypto.KeyType;
import io.kamax.mxisd.http.IsAPIv1;
import io.kamax.mxisd.http.undertow.handler.ApiHandler;
import io.undertow.server.HttpServerExchange;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class RegularKeyIsValidHandler extends KeyIsValidHandler {
public static final String Path = IsAPIv1.Base + "/pubkey/isvalid";
public class RegularKeyIsValidHandler extends KeyIsValidHandler implements ApiHandler {
private transient final Logger log = LoggerFactory.getLogger(RegularKeyIsValidHandler.class);
@@ -48,4 +46,8 @@ public class RegularKeyIsValidHandler extends KeyIsValidHandler {
respondJson(exchange, mgr.isValid(KeyType.Regular, pubKey) ? validKey : invalidKey);
}
@Override
public String getHandlerPath() {
return "/pubkey/isvalid";
}
}

View File

@@ -18,26 +18,25 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.http.undertow.handler.identity.v1;
package io.kamax.mxisd.http.undertow.handler.identity.share;
import com.google.gson.JsonObject;
import io.kamax.matrix.ThreePid;
import io.kamax.matrix.ThreePidMedium;
import io.kamax.mxisd.http.IsAPIv1;
import io.kamax.mxisd.http.io.identity.RequestTokenResponse;
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.http.undertow.handler.ApiHandler;
import io.kamax.mxisd.session.SessionManager;
import io.undertow.server.HttpServerExchange;
import org.apache.http.HttpStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class SessionStartHandler extends BasicHttpHandler {
public class SessionStartHandler extends BasicHttpHandler implements ApiHandler {
public static final String Medium = "medium";
public static final String Path = IsAPIv1.Base + "/validate/{" + Medium + "}/requestToken";
private transient final Logger log = LoggerFactory.getLogger(SessionStartHandler.class);
@@ -84,4 +83,8 @@ public class SessionStartHandler extends BasicHttpHandler {
}
}
@Override
public String getHandlerPath() {
return "/validate/{" + Medium + "}/requestToken";
}
}

View File

@@ -18,16 +18,16 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.http.undertow.handler.identity.v1;
package io.kamax.mxisd.http.undertow.handler.identity.share;
import com.google.gson.JsonObject;
import io.kamax.matrix.json.GsonUtil;
import io.kamax.mxisd.crypto.SignatureManager;
import io.kamax.mxisd.exception.BadRequestException;
import io.kamax.mxisd.http.IsAPIv1;
import io.kamax.mxisd.http.io.identity.BindRequest;
import io.kamax.mxisd.http.io.identity.SingeLookupReplyJson;
import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler;
import io.kamax.mxisd.http.undertow.handler.ApiHandler;
import io.kamax.mxisd.invitation.InvitationManager;
import io.kamax.mxisd.lookup.SingleLookupReply;
import io.kamax.mxisd.session.SessionManager;
@@ -42,9 +42,7 @@ import java.nio.charset.StandardCharsets;
import java.util.Deque;
import java.util.Map;
public class SessionTpidBindHandler extends BasicHttpHandler {
public static final String Path = IsAPIv1.Base + "/3pid/bind";
public class SessionTpidBindHandler extends BasicHttpHandler implements ApiHandler {
private static final Logger log = LoggerFactory.getLogger(SessionTpidBindHandler.class);
@@ -97,4 +95,8 @@ public class SessionTpidBindHandler extends BasicHttpHandler {
}
}
@Override
public String getHandlerPath() {
return "/3pid/bind";
}
}

View File

@@ -18,21 +18,19 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.http.undertow.handler.identity.v1;
package io.kamax.mxisd.http.undertow.handler.identity.share;
import com.google.gson.JsonObject;
import io.kamax.mxisd.exception.SessionNotValidatedException;
import io.kamax.mxisd.http.IsAPIv1;
import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler;
import io.kamax.mxisd.http.undertow.handler.ApiHandler;
import io.kamax.mxisd.lookup.ThreePidValidation;
import io.kamax.mxisd.session.SessionManager;
import io.undertow.server.HttpServerExchange;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class SessionTpidGetValidatedHandler extends BasicHttpHandler {
public static final String Path = IsAPIv1.Base + "/3pid/getValidated3pid";
public class SessionTpidGetValidatedHandler extends BasicHttpHandler implements ApiHandler {
private static final Logger log = LoggerFactory.getLogger(SessionTpidGetValidatedHandler.class);
@@ -62,4 +60,8 @@ public class SessionTpidGetValidatedHandler extends BasicHttpHandler {
}
}
@Override
public String getHandlerPath() {
return "/3pid/getValidated3pid";
}
}

View File

@@ -18,19 +18,17 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.http.undertow.handler.identity.v1;
package io.kamax.mxisd.http.undertow.handler.identity.share;
import com.google.gson.JsonObject;
import io.kamax.mxisd.http.IsAPIv1;
import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler;
import io.kamax.mxisd.http.undertow.handler.ApiHandler;
import io.kamax.mxisd.session.SessionManager;
import io.undertow.server.HttpServerExchange;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class SessionTpidUnbindHandler extends BasicHttpHandler {
public static final String Path = IsAPIv1.Base + "/3pid/unbind";
public class SessionTpidUnbindHandler extends BasicHttpHandler implements ApiHandler {
private static final Logger log = LoggerFactory.getLogger(SessionTpidUnbindHandler.class);
@@ -48,4 +46,9 @@ public class SessionTpidUnbindHandler extends BasicHttpHandler {
sessionMgr.unbind(auth, body);
writeBodyAsUtf8(exchange, "{}");
}
@Override
public String getHandlerPath() {
return "/3pid/unbind";
}
}

View File

@@ -18,19 +18,17 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.http.undertow.handler.identity.v1;
package io.kamax.mxisd.http.undertow.handler.identity.share;
import io.kamax.mxisd.http.IsAPIv1;
import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler;
import io.kamax.mxisd.http.undertow.handler.ApiHandler;
import io.kamax.mxisd.session.SessionManager;
import io.kamax.mxisd.session.ValidationResult;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public abstract class SessionValidateHandler extends BasicHttpHandler {
public static final String Path = IsAPIv1.Base + "/validate/{medium}/submitToken";
public abstract class SessionValidateHandler extends BasicHttpHandler implements ApiHandler {
private transient final Logger log = LoggerFactory.getLogger(SessionValidateHandler.class);
@@ -52,4 +50,8 @@ public abstract class SessionValidateHandler extends BasicHttpHandler {
return mgr.validate(sid, secret, token);
}
@Override
public String getHandlerPath() {
return "/validate/{medium}/submitToken";
}
}

View File

@@ -18,7 +18,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.http.undertow.handler.identity.v1;
package io.kamax.mxisd.http.undertow.handler.identity.share;
import io.kamax.mxisd.config.MxisdConfig;
import io.kamax.mxisd.config.ServerConfig;

View File

@@ -18,7 +18,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.http.undertow.handler.identity.v1;
package io.kamax.mxisd.http.undertow.handler.identity.share;
import com.google.gson.JsonObject;
import io.kamax.matrix.json.GsonUtil;

View File

@@ -18,7 +18,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.http.undertow.handler.identity.v1;
package io.kamax.mxisd.http.undertow.handler.identity.share;
import com.google.gson.JsonObject;
import io.kamax.matrix.MatrixID;
@@ -27,17 +27,15 @@ 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.http.undertow.handler.ApiHandler;
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";
public class SignEd25519Handler extends BasicHttpHandler implements ApiHandler {
private static final Logger log = LoggerFactory.getLogger(SignEd25519Handler.class);
@@ -72,4 +70,8 @@ public class SignEd25519Handler extends BasicHttpHandler {
respondJson(exchange, res);
}
@Override
public String getHandlerPath() {
return "/sign-ed25519";
}
}

View File

@@ -18,7 +18,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.http.undertow.handler.identity.v1;
package io.kamax.mxisd.http.undertow.handler.identity.share;
import com.google.gson.JsonObject;
import com.google.gson.reflect.TypeToken;
@@ -28,10 +28,10 @@ import io.kamax.matrix.json.GsonUtil;
import io.kamax.mxisd.config.ServerConfig;
import io.kamax.mxisd.crypto.KeyManager;
import io.kamax.mxisd.exception.BadRequestException;
import io.kamax.mxisd.http.IsAPIv1;
import io.kamax.mxisd.http.io.identity.StoreInviteRequest;
import io.kamax.mxisd.http.io.identity.ThreePidInviteReplyIO;
import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler;
import io.kamax.mxisd.http.undertow.handler.ApiHandler;
import io.kamax.mxisd.invitation.IThreePidInvite;
import io.kamax.mxisd.invitation.IThreePidInviteReply;
import io.kamax.mxisd.invitation.InvitationManager;
@@ -45,9 +45,7 @@ import java.nio.charset.StandardCharsets;
import java.util.Deque;
import java.util.Map;
public class StoreInviteHandler extends BasicHttpHandler {
public static final String Path = IsAPIv1.Base + "/store-invite";
public class StoreInviteHandler extends BasicHttpHandler implements ApiHandler {
private ServerConfig cfg;
private InvitationManager invMgr;
@@ -100,4 +98,8 @@ public class StoreInviteHandler extends BasicHttpHandler {
respondJson(exchange, new ThreePidInviteReplyIO(reply, keyMgr.getPublicKeyBase64(keyMgr.getServerSigningKey().getId()), cfg.getPublicUrl()));
}
@Override
public String getHandlerPath() {
return "/store-invite";
}
}

View File

@@ -23,6 +23,8 @@ package io.kamax.mxisd.http.undertow.handler.identity.v1;
import io.kamax.mxisd.http.IsAPIv1;
import io.kamax.mxisd.http.io.identity.ClientBulkLookupAnswer;
import io.kamax.mxisd.http.io.identity.ClientBulkLookupRequest;
import io.kamax.mxisd.http.undertow.handler.ApiHandler;
import io.kamax.mxisd.http.undertow.handler.identity.share.LookupHandler;
import io.kamax.mxisd.lookup.BulkLookupRequest;
import io.kamax.mxisd.lookup.ThreePidMapping;
import io.kamax.mxisd.lookup.strategy.LookupStrategy;
@@ -33,7 +35,7 @@ import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
public class BulkLookupHandler extends LookupHandler {
public class BulkLookupHandler extends LookupHandler implements ApiHandler {
public static final String Path = IsAPIv1.Base + "/bulk_lookup";
@@ -69,4 +71,8 @@ public class BulkLookupHandler extends LookupHandler {
respondJson(exchange, answer);
}
@Override
public String getHandlerPath() {
return "/bulk_lookup";
}
}

View File

@@ -27,6 +27,8 @@ import io.kamax.mxisd.config.ServerConfig;
import io.kamax.mxisd.crypto.SignatureManager;
import io.kamax.mxisd.http.IsAPIv1;
import io.kamax.mxisd.http.io.identity.SingeLookupReplyJson;
import io.kamax.mxisd.http.undertow.handler.ApiHandler;
import io.kamax.mxisd.http.undertow.handler.identity.share.LookupHandler;
import io.kamax.mxisd.lookup.SingleLookupReply;
import io.kamax.mxisd.lookup.SingleLookupRequest;
import io.kamax.mxisd.lookup.strategy.LookupStrategy;
@@ -36,7 +38,7 @@ import org.slf4j.LoggerFactory;
import java.util.Optional;
public class SingleLookupHandler extends LookupHandler {
public class SingleLookupHandler extends LookupHandler implements ApiHandler {
public static final String Path = IsAPIv1.Base + "/lookup";
@@ -77,4 +79,8 @@ public class SingleLookupHandler extends LookupHandler {
}
}
@Override
public String getHandlerPath() {
return "/lookup";
}
}

View File

@@ -0,0 +1,38 @@
package io.kamax.mxisd.http.undertow.handler.identity.v2;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import io.kamax.mxisd.config.HashingConfig;
import io.kamax.mxisd.hash.HashManager;
import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler;
import io.undertow.server.HttpServerExchange;
public class HashDetailsHandler extends BasicHttpHandler {
public static final String PATH = "/_matrix/identity/v2/hash_details";
private final HashManager hashManager;
public HashDetailsHandler(HashManager hashManager) {
this.hashManager = hashManager;
}
@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
respond(exchange, getResponse());
}
private JsonObject getResponse() {
JsonObject response = new JsonObject();
response.addProperty("lookup_pepper", hashManager.getHashEngine().getPepper());
JsonArray algorithms = new JsonArray();
HashingConfig config = hashManager.getConfig();
if (config.isEnabled()) {
for (HashingConfig.Algorithm algorithm : config.getAlgorithms()) {
algorithms.add(algorithm.name().toLowerCase());
}
}
response.add("algorithms", algorithms);
return response;
}
}

View File

@@ -0,0 +1,130 @@
/*
* 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.http.undertow.handler.identity.v2;
import io.kamax.mxisd.config.HashingConfig;
import io.kamax.mxisd.exception.InvalidParamException;
import io.kamax.mxisd.exception.InvalidPepperException;
import io.kamax.mxisd.hash.HashManager;
import io.kamax.mxisd.http.IsAPIv2;
import io.kamax.mxisd.http.io.identity.ClientHashLookupAnswer;
import io.kamax.mxisd.http.io.identity.ClientHashLookupRequest;
import io.kamax.mxisd.http.undertow.handler.ApiHandler;
import io.kamax.mxisd.http.undertow.handler.identity.share.LookupHandler;
import io.kamax.mxisd.lookup.BulkLookupRequest;
import io.kamax.mxisd.lookup.HashLookupRequest;
import io.kamax.mxisd.lookup.ThreePidMapping;
import io.kamax.mxisd.lookup.strategy.LookupStrategy;
import io.undertow.server.HttpServerExchange;
import org.apache.commons.lang3.tuple.Pair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
public class HashLookupHandler extends LookupHandler implements ApiHandler {
public static final String Path = IsAPIv2.Base + "/lookup";
private static final Logger log = LoggerFactory.getLogger(HashLookupHandler.class);
private LookupStrategy strategy;
private HashManager hashManager;
public HashLookupHandler(LookupStrategy strategy, HashManager hashManager) {
this.strategy = strategy;
this.hashManager = hashManager;
}
@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
ClientHashLookupRequest input = parseJsonTo(exchange, ClientHashLookupRequest.class);
HashLookupRequest lookupRequest = new HashLookupRequest();
setRequesterInfo(lookupRequest, exchange);
log.info("Got bulk lookup request from {} with client {} - Is recursive? {}",
lookupRequest.getRequester(), lookupRequest.getUserAgent(), lookupRequest.isRecursive());
if (!hashManager.getConfig().isEnabled()) {
throw new InvalidParamException();
}
if (!hashManager.getHashEngine().getPepper().equals(input.getPepper())) {
throw new InvalidPepperException();
}
switch (input.getAlgorithm()) {
case "none":
noneAlgorithm(exchange, lookupRequest, input);
break;
case "sha256":
sha256Algorithm(exchange, lookupRequest, input);
break;
default:
throw new InvalidParamException();
}
}
private void noneAlgorithm(HttpServerExchange exchange, HashLookupRequest request, ClientHashLookupRequest input) throws Exception {
if (!hashManager.getConfig().getAlgorithms().contains(HashingConfig.Algorithm.NONE)) {
throw new InvalidParamException();
}
BulkLookupRequest bulkLookupRequest = new BulkLookupRequest();
List<ThreePidMapping> mappings = new ArrayList<>();
for (String address : input.getAddresses()) {
String[] parts = address.split(" ");
ThreePidMapping mapping = new ThreePidMapping();
mapping.setMedium(parts[0]);
mapping.setValue(parts[1]);
mappings.add(mapping);
}
bulkLookupRequest.setMappings(mappings);
ClientHashLookupAnswer answer = new ClientHashLookupAnswer();
for (ThreePidMapping mapping : strategy.find(bulkLookupRequest).get()) {
answer.getMappings().put(mapping.getMedium() + " " + mapping.getValue(), mapping.getMxid());
}
log.info("Finished bulk lookup request from {}", request.getRequester());
respondJson(exchange, answer);
}
private void sha256Algorithm(HttpServerExchange exchange, HashLookupRequest request, ClientHashLookupRequest input) {
if (!hashManager.getConfig().getAlgorithms().contains(HashingConfig.Algorithm.SHA256)) {
throw new InvalidParamException();
}
ClientHashLookupAnswer answer = new ClientHashLookupAnswer();
for (Pair<String, ThreePidMapping> pair : hashManager.getHashStorage().find(request.getHashes())) {
answer.getMappings().put(pair.getKey(), pair.getValue().getMxid());
}
log.info("Finished bulk lookup request from {}", request.getRequester());
respondJson(exchange, answer);
}
@Override
public String getHandlerPath() {
return "/bulk_lookup";
}
}

View File

@@ -0,0 +1,57 @@
package io.kamax.mxisd.http.undertow.handler.term.v2;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import io.kamax.matrix.json.GsonUtil;
import io.kamax.mxisd.auth.AccountManager;
import io.kamax.mxisd.exception.InvalidCredentialsException;
import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler;
import io.kamax.mxisd.storage.ormlite.dao.AccountDao;
import io.undertow.server.HttpServerExchange;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class AcceptTermsHandler extends BasicHttpHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(AcceptTermsHandler.class);
public static final String PATH = "/_matrix/identity/v2/terms";
private final AccountManager accountManager;
public AcceptTermsHandler(AccountManager accountManager) {
this.accountManager = accountManager;
}
@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
String token = getAccessToken(exchange);
JsonObject request = parseJsonObject(exchange);
JsonObject accepts = GsonUtil.getObj(request, "user_accepts");
AccountDao account = accountManager.findAccount(token);
if (account == null) {
throw new InvalidCredentialsException();
}
if (accepts == null || accepts.isJsonNull()) {
respondJson(exchange, "{}");
return;
}
if (accepts.isJsonArray()) {
for (JsonElement acceptItem : accepts.getAsJsonArray()) {
String termUrl = acceptItem.getAsString();
LOGGER.info("User {} accepts the term: {}", account.getUserId(), termUrl);
accountManager.acceptTerm(token, termUrl);
}
} else {
String termUrl = accepts.getAsString();
LOGGER.info("User {} accepts the term: {}", account.getUserId(), termUrl);
accountManager.acceptTerm(token, termUrl);
}
respondJson(exchange, "{}");
}
}

View File

@@ -0,0 +1,37 @@
package io.kamax.mxisd.http.undertow.handler.term.v2;
import com.google.gson.JsonObject;
import io.kamax.mxisd.config.PolicyConfig;
import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler;
import io.undertow.server.HttpServerExchange;
import java.util.Map;
public class GetTermsHandler extends BasicHttpHandler {
public static final String PATH = "/_matrix/identity/v2/terms";
private final JsonObject policyResponse;
public GetTermsHandler(PolicyConfig config) {
policyResponse = new JsonObject();
JsonObject policies = new JsonObject();
for (Map.Entry<String, PolicyConfig.PolicyObject> policyItem : config.getPolicies().entrySet()) {
JsonObject policy = new JsonObject();
policy.addProperty("version", policyItem.getValue().getVersion());
for (Map.Entry<String, PolicyConfig.TermObject> termEntry : policyItem.getValue().getTerms().entrySet()) {
JsonObject term = new JsonObject();
term.addProperty("name", termEntry.getValue().getName());
term.addProperty("url", termEntry.getValue().getUrl());
policy.add(termEntry.getKey(), term);
}
policies.add(policyItem.getKey(), policy);
}
policyResponse.add("policies", policies);
}
@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
respond(exchange, policyResponse);
}
}

View File

@@ -0,0 +1,16 @@
package io.kamax.mxisd.lookup;
import java.util.List;
public class HashLookupRequest extends ALookupRequest {
private List<String> hashes;
public List<String> getHashes() {
return hashes;
}
public void setHashes(List<String> hashes) {
this.hashes = hashes;
}
}

View File

@@ -24,6 +24,7 @@ import io.kamax.mxisd.lookup.SingleLookupReply;
import io.kamax.mxisd.lookup.SingleLookupRequest;
import io.kamax.mxisd.lookup.ThreePidMapping;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
@@ -40,4 +41,7 @@ public interface IThreePidProvider {
List<ThreePidMapping> populate(List<ThreePidMapping> mappings);
default Iterable<ThreePidMapping> populateHashes() {
return Collections.emptyList();
}
}

View File

@@ -21,6 +21,7 @@
package io.kamax.mxisd.lookup.strategy;
import io.kamax.mxisd.lookup.BulkLookupRequest;
import io.kamax.mxisd.lookup.HashLookupRequest;
import io.kamax.mxisd.lookup.SingleLookupReply;
import io.kamax.mxisd.lookup.SingleLookupRequest;
import io.kamax.mxisd.lookup.ThreePidMapping;
@@ -46,4 +47,5 @@ public interface LookupStrategy {
CompletableFuture<List<ThreePidMapping>> find(BulkLookupRequest requests);
CompletableFuture<List<ThreePidMapping>> find(HashLookupRequest request);
}

View File

@@ -25,10 +25,13 @@ import io.kamax.matrix.json.GsonUtil;
import io.kamax.matrix.json.MatrixJson;
import io.kamax.mxisd.config.MxisdConfig;
import io.kamax.mxisd.exception.ConfigurationException;
import io.kamax.mxisd.hash.HashManager;
import io.kamax.mxisd.hash.storage.HashStorage;
import io.kamax.mxisd.lookup.*;
import io.kamax.mxisd.lookup.fetcher.IBridgeFetcher;
import io.kamax.mxisd.lookup.provider.IThreePidProvider;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -50,10 +53,14 @@ public class RecursivePriorityLookupStrategy implements LookupStrategy {
private List<CIDRUtils> allowedCidr = new ArrayList<>();
public RecursivePriorityLookupStrategy(MxisdConfig.Lookup cfg, List<? extends IThreePidProvider> providers, IBridgeFetcher bridge) {
private HashManager hashManager;
public RecursivePriorityLookupStrategy(MxisdConfig.Lookup cfg, List<? extends IThreePidProvider> providers, IBridgeFetcher bridge,
HashManager hashManager) {
this.cfg = cfg;
this.bridge = bridge;
this.providers = new ArrayList<>(providers);
this.hashManager = hashManager;
try {
log.info("Found {} providers", providers.size());
@@ -65,6 +72,8 @@ public class RecursivePriorityLookupStrategy implements LookupStrategy {
log.info("{} is allowed for recursion", cidr);
allowedCidr.add(new CIDRUtils(cidr));
}
log.info("Hash lookups enabled: {}", hashManager.getConfig().isEnabled());
} catch (UnknownHostException e) {
throw new ConfigurationException("lookup.recursive.allowedCidrs", "Allowed CIDRs");
}
@@ -154,20 +163,20 @@ public class RecursivePriorityLookupStrategy implements LookupStrategy {
Optional<SingleLookupReply> lookupDataOpt = provider.find(request);
if (lookupDataOpt.isPresent()) {
log.info("Found 3PID mapping: {medium: '{}', address: '{}', mxid: '{}'}",
request.getType(), request.getThreePid(), lookupDataOpt.get().getMxid().getId());
request.getType(), request.getThreePid(), lookupDataOpt.get().getMxid().getId());
return lookupDataOpt;
}
}
if (
cfg.getRecursive().getBridge() != null &&
cfg.getRecursive().getBridge().getEnabled() &&
(!cfg.getRecursive().getBridge().getRecursiveOnly() || isAllowedForRecursive(request.getRequester()))
cfg.getRecursive().getBridge() != null &&
cfg.getRecursive().getBridge().getEnabled() &&
(!cfg.getRecursive().getBridge().getRecursiveOnly() || isAllowedForRecursive(request.getRequester()))
) {
log.info("Using bridge failover for lookup");
Optional<SingleLookupReply> lookupDataOpt = bridge.find(request);
log.info("Found 3PID mapping: {medium: '{}', address: '{}', mxid: '{}'}",
request.getThreePid(), request.getId(), lookupDataOpt.get().getMxid().getId());
request.getThreePid(), request.getId(), lookupDataOpt.get().getMxid().getId());
return lookupDataOpt;
}
@@ -230,4 +239,12 @@ public class RecursivePriorityLookupStrategy implements LookupStrategy {
return bulkLookupInProgress.remove(payloadId);
}
@Override
public CompletableFuture<List<ThreePidMapping>> find(HashLookupRequest request) {
HashStorage hashStorage = hashManager.getHashStorage();
CompletableFuture<List<ThreePidMapping>> result = new CompletableFuture<>();
result.complete(hashStorage.find(request.getHashes()).stream().map(Pair::getValue).collect(Collectors.toList()));
hashManager.getRotationStrategy().newRequest();
return result;
}
}

View File

@@ -0,0 +1,9 @@
package io.kamax.mxisd.matrix;
public enum IdentityServiceAPI {
@Deprecated
V1,
V2
}

View File

@@ -57,6 +57,8 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Calendar;
@@ -237,12 +239,26 @@ public class SessionManager {
notifMgr.sendForUnbind(tpid);
}
private String getDomain(String publicUrl) {
URL url;
try {
url = new URL(publicUrl);
} catch (MalformedURLException e) {
log.error("Malformed public url, use as is");
return publicUrl;
}
int port = url.getPort();
return url.getHost() + (port != -1 ? ":" + url.getPort() : "");
}
private void checkAuthorization(String auth, JsonObject reqData) {
if (!auth.startsWith("X-Matrix ")) {
throw new NotAllowedException("Wrong authorization header");
}
if (StringUtils.isBlank(cfg.getServer().getPublicUrl())) {
String domain = getDomain(cfg.getServer().getPublicUrl());
if (StringUtils.isBlank(domain)) {
throw new NotAllowedException("Unable to verify request, missing `server.publicUrl` property");
}
@@ -284,7 +300,7 @@ public class SessionManager {
jsonObject.addProperty("method", "POST");
jsonObject.addProperty("uri", "/_matrix/identity/api/v1/3pid/unbind");
jsonObject.addProperty("origin", origin);
jsonObject.addProperty("destination_is", cfg.getServer().getPublicUrl());
jsonObject.addProperty("destination_is", domain);
jsonObject.add("content", reqData);
String canonical = MatrixJson.encodeCanonical(jsonObject);

View File

@@ -21,13 +21,18 @@
package io.kamax.mxisd.storage;
import io.kamax.matrix.ThreePid;
import io.kamax.mxisd.config.PolicyConfig;
import io.kamax.mxisd.invitation.IThreePidInviteReply;
import io.kamax.mxisd.lookup.ThreePidMapping;
import io.kamax.mxisd.storage.dao.IThreePidSessionDao;
import io.kamax.mxisd.storage.ormlite.dao.ASTransactionDao;
import io.kamax.mxisd.storage.ormlite.dao.AccountDao;
import io.kamax.mxisd.storage.ormlite.dao.ThreePidInviteIO;
import org.apache.commons.lang3.tuple.Pair;
import java.time.Instant;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
public interface IStorage {
@@ -52,4 +57,21 @@ public interface IStorage {
Optional<ASTransactionDao> getTransactionResult(String localpart, String txnId);
void insertToken(AccountDao accountDao);
Optional<AccountDao> findAccount(String token);
void deleteToken(String token);
void acceptTerm(String token, String url);
void deleteAccepts(String token);
boolean isTermAccepted(String token, List<PolicyConfig.PolicyObject> policies);
void clearHashes();
void addHash(String mxid, String medium, String address, String hash);
Collection<Pair<String, ThreePidMapping>> findHashes(Iterable<String> hashes);
}

View File

@@ -24,25 +24,38 @@ import com.j256.ormlite.dao.CloseableWrappedIterable;
import com.j256.ormlite.dao.Dao;
import com.j256.ormlite.dao.DaoManager;
import com.j256.ormlite.jdbc.JdbcConnectionSource;
import com.j256.ormlite.stmt.QueryBuilder;
import com.j256.ormlite.support.ConnectionSource;
import com.j256.ormlite.table.TableUtils;
import io.kamax.matrix.ThreePid;
import io.kamax.mxisd.config.MxisdConfig;
import io.kamax.mxisd.config.PolicyConfig;
import io.kamax.mxisd.exception.ConfigurationException;
import io.kamax.mxisd.exception.InternalServerError;
import io.kamax.mxisd.exception.InvalidCredentialsException;
import io.kamax.mxisd.invitation.IThreePidInviteReply;
import io.kamax.mxisd.lookup.ThreePidMapping;
import io.kamax.mxisd.storage.IStorage;
import io.kamax.mxisd.storage.dao.IThreePidSessionDao;
import io.kamax.mxisd.storage.ormlite.dao.ASTransactionDao;
import io.kamax.mxisd.storage.ormlite.dao.AccountDao;
import io.kamax.mxisd.storage.ormlite.dao.HashDao;
import io.kamax.mxisd.storage.ormlite.dao.HistoricalThreePidInviteIO;
import io.kamax.mxisd.storage.ormlite.dao.AcceptedDao;
import io.kamax.mxisd.storage.ormlite.dao.ThreePidInviteIO;
import io.kamax.mxisd.storage.ormlite.dao.ThreePidSessionDao;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import java.io.IOException;
import java.sql.SQLException;
import java.time.Instant;
import java.util.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;
public class OrmLiteSqlStorage implements IStorage {
@@ -64,6 +77,9 @@ public class OrmLiteSqlStorage implements IStorage {
private Dao<HistoricalThreePidInviteIO, String> expInvDao;
private Dao<ThreePidSessionDao, String> sessionDao;
private Dao<ASTransactionDao, String> asTxnDao;
private Dao<AccountDao, String> accountDao;
private Dao<AcceptedDao, String> acceptedDao;
private Dao<HashDao, String> hashDao;
public OrmLiteSqlStorage(MxisdConfig cfg) {
this(cfg.getStorage().getBackend(), cfg.getStorage().getProvider().getSqlite().getDatabase());
@@ -84,6 +100,9 @@ public class OrmLiteSqlStorage implements IStorage {
expInvDao = createDaoAndTable(connPool, HistoricalThreePidInviteIO.class);
sessionDao = createDaoAndTable(connPool, ThreePidSessionDao.class);
asTxnDao = createDaoAndTable(connPool, ASTransactionDao.class);
accountDao = createDaoAndTable(connPool, AccountDao.class);
acceptedDao = createDaoAndTable(connPool, AcceptedDao.class);
hashDao = createDaoAndTable(connPool, HashDao.class);
});
}
@@ -175,7 +194,7 @@ public class OrmLiteSqlStorage implements IStorage {
List<ThreePidSessionDao> daoList = sessionDao.queryForMatchingArgs(new ThreePidSessionDao(tpid, secret));
if (daoList.size() > 1) {
throw new InternalServerError("Lookup for 3PID Session " +
tpid + " returned more than one result");
tpid + " returned more than one result");
}
if (daoList.isEmpty()) {
@@ -226,7 +245,7 @@ public class OrmLiteSqlStorage implements IStorage {
if (daoList.size() > 1) {
throw new InternalServerError("Lookup for Transaction " +
txnId + " for localpart " + localpart + " returned more than one result");
txnId + " for localpart " + localpart + " returned more than one result");
}
if (daoList.isEmpty()) {
@@ -237,4 +256,103 @@ public class OrmLiteSqlStorage implements IStorage {
});
}
@Override
public void insertToken(AccountDao account) {
withCatcher(() -> {
int created = accountDao.create(account);
if (created != 1) {
throw new RuntimeException("Unexpected row count after DB action: " + created);
}
});
}
@Override
public Optional<AccountDao> findAccount(String token) {
return withCatcher(() -> {
List<AccountDao> accounts = accountDao.queryForEq("token", token);
if (accounts.isEmpty()) {
return Optional.empty();
}
if (accounts.size() != 1) {
throw new RuntimeException("Unexpected rows for access token: " + accounts.size());
}
return Optional.of(accounts.get(0));
});
}
@Override
public void deleteToken(String token) {
withCatcher(() -> {
int updated = accountDao.deleteById(token);
if (updated != 1) {
throw new RuntimeException("Unexpected row count after DB action: " + updated);
}
});
}
@Override
public void acceptTerm(String token, String url) {
withCatcher(() -> {
AccountDao account = findAccount(token).orElseThrow(InvalidCredentialsException::new);
int created = acceptedDao.create(new AcceptedDao(url, account.getUserId(), System.currentTimeMillis()));
if (created != 1) {
throw new RuntimeException("Unexpected row count after DB action: " + created);
}
});
}
@Override
public void deleteAccepts(String token) {
withCatcher(() -> {
AccountDao account = findAccount(token).orElseThrow(InvalidCredentialsException::new);
acceptedDao.delete(acceptedDao.queryForEq("userId", account.getUserId()));
});
}
@Override
public boolean isTermAccepted(String token, List<PolicyConfig.PolicyObject> policies) {
return withCatcher(() -> {
AccountDao account = findAccount(token).orElseThrow(InvalidCredentialsException::new);
List<AcceptedDao> acceptedTerms = acceptedDao.queryForEq("userId", account.getUserId());
for (AcceptedDao acceptedTerm : acceptedTerms) {
for (PolicyConfig.PolicyObject policy : policies) {
for (PolicyConfig.TermObject termObject : policy.getTerms().values()) {
if (termObject.getUrl().equalsIgnoreCase(acceptedTerm.getUrl())) {
return true;
}
}
}
}
return false;
});
}
@Override
public void clearHashes() {
withCatcher(() -> {
List<HashDao> allHashes = hashDao.queryForAll();
int deleted = hashDao.delete(allHashes);
if (deleted != allHashes.size()) {
throw new RuntimeException("Not all hashes deleted: " + deleted);
}
});
}
@Override
public void addHash(String mxid, String medium, String address, String hash) {
withCatcher(() -> {
hashDao.create(new HashDao(mxid, medium, address, hash));
});
}
@Override
public Collection<Pair<String, ThreePidMapping>> findHashes(Iterable<String> hashes) {
return withCatcher(() -> {
QueryBuilder<HashDao, String> builder = hashDao.queryBuilder();
builder.where().in("hash", hashes);
return hashDao.query(builder.prepare()).stream()
.map(dao -> Pair.of(dao.getHash(), new ThreePidMapping(dao.getMedium(), dao.getAddress(), dao.getMxid()))).collect(
Collectors.toList());
});
}
}

View File

@@ -0,0 +1,71 @@
/*
* 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.storage.ormlite.dao;
import com.j256.ormlite.field.DatabaseField;
import com.j256.ormlite.table.DatabaseTable;
@DatabaseTable(tableName = "accepted")
public class AcceptedDao {
@DatabaseField(canBeNull = false, id = true)
private String url;
@DatabaseField(canBeNull = false)
private String userId;
@DatabaseField(canBeNull = false)
private long acceptedAt;
public AcceptedDao() {
// Needed for ORMLite
}
public AcceptedDao(String url, String userId, long acceptedAt) {
this.url = url;
this.userId = userId;
this.acceptedAt = acceptedAt;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public long getAcceptedAt() {
return acceptedAt;
}
public void setAcceptedAt(long acceptedAt) {
this.acceptedAt = acceptedAt;
}
}

View File

@@ -0,0 +1,119 @@
/*
* 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.storage.ormlite.dao;
import com.j256.ormlite.field.DatabaseField;
import com.j256.ormlite.table.DatabaseTable;
@DatabaseTable(tableName = "account")
public class AccountDao {
@DatabaseField(canBeNull = false, id = true)
private String token;
@DatabaseField(canBeNull = false)
private String accessToken;
@DatabaseField(canBeNull = false)
private String tokenType;
@DatabaseField(canBeNull = false)
private String matrixServerName;
@DatabaseField(canBeNull = false)
private long expiresIn;
@DatabaseField(canBeNull = false)
private long createdAt;
@DatabaseField(canBeNull = false)
private String userId;
public AccountDao() {
// Needed for ORMLite
}
public AccountDao(String accessToken, String tokenType, String matrixServerName, long expiresIn, long createdAt, String userId, String token) {
this.accessToken = accessToken;
this.tokenType = tokenType;
this.matrixServerName = matrixServerName;
this.expiresIn = expiresIn;
this.createdAt = createdAt;
this.userId = userId;
this.token = token;
}
public String getAccessToken() {
return accessToken;
}
public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}
public String getTokenType() {
return tokenType;
}
public void setTokenType(String tokenType) {
this.tokenType = tokenType;
}
public String getMatrixServerName() {
return matrixServerName;
}
public void setMatrixServerName(String matrixServerName) {
this.matrixServerName = matrixServerName;
}
public long getExpiresIn() {
return expiresIn;
}
public void setExpiresIn(long expiresIn) {
this.expiresIn = expiresIn;
}
public long getCreatedAt() {
return createdAt;
}
public void setCreatedAt(long createdAt) {
this.createdAt = createdAt;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
}

View File

@@ -0,0 +1,62 @@
package io.kamax.mxisd.storage.ormlite.dao;
import com.j256.ormlite.field.DatabaseField;
import com.j256.ormlite.table.DatabaseTable;
@DatabaseTable(tableName = "hashes")
public class HashDao {
@DatabaseField(canBeNull = false, id = true)
private String mxid;
@DatabaseField(canBeNull = false)
private String medium;
@DatabaseField(canBeNull = false)
private String address;
@DatabaseField(canBeNull = false)
private String hash;
public HashDao() {
}
public HashDao(String mxid, String medium, String address, String hash) {
this.mxid = mxid;
this.medium = medium;
this.address = address;
this.hash = hash;
}
public String getMxid() {
return mxid;
}
public void setMxid(String mxid) {
this.mxid = mxid;
}
public String getMedium() {
return medium;
}
public void setMedium(String medium) {
this.medium = medium;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String getHash() {
return hash;
}
public void setHash(String hash) {
this.hash = hash;
}
}