Compare commits

...

8 Commits

25 changed files with 361 additions and 180 deletions

47
docs/MSC2140_MSC2134.md Normal file
View File

@@ -0,0 +1,47 @@
# MSC2140
## V1 vs V2
In the [MSC2140](https://github.com/matrix-org/matrix-doc/pull/2140) the v2 prefix was introduced.
Default values:
```.yaml
matrix:
v1: true # deprecated
v2: true
```
To disable change value to `false`.
NOTE: the v1 is deprecated, therefore recommend to use only v2 and disable v1 (default value can be ommited):
```.yaml
matrix:
v1: false
```
## Terms
Example:
```.yaml
policy:
policies:
term_name: # term name
version: 1.0 # version
terms:
en: # lang
name: term name en # localized name
url: https://ma1sd.host.tld/term_en.html # localized url
fe: # lang
name: term name fr # localized name
url: https://ma1sd.host.tld/term_fr.html # localized url
regexp:
- '/_matrix/identity/v2/account.*'
- '/_matrix/identity/v2/hash_lookup'
```
Where:
- `term_name` -- name of the terms.
- `regexp` -- regexp patterns for API.
## Hash lookup

View File

@@ -48,6 +48,9 @@ Create a list under the label `myOtherServers` containing two Identity servers:
## Unbind (MSC1915)
- `session.policy.unbind.enabled`: Enable or disable unbind functionality (MSC1915). (Defaults to true).
## Hash lookups, Term and others (MSC2140, MSC2134)
See the [dedicated document](MSC2140_MSC2134.md) for configuration.
*Warning*: Unbind check incoming request by two ways:
- session validation.
- request signature via `X-Matrix` header and uses `server.publicUrl` property to construct the signing json;

View File

@@ -1,47 +0,0 @@
/*
* matrix-java-sdk - Matrix Client SDK for Java
* 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.matrix.codec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class MxSha256 {
private MessageDigest md;
public MxSha256() {
try {
md = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
public String hash(byte[] data) {
return MxBase64.encode(md.digest(data));
}
public String hash(String data) {
return hash(data.getBytes(StandardCharsets.UTF_8));
}
}

View File

@@ -52,7 +52,8 @@ import io.kamax.mxisd.http.undertow.handler.identity.share.SessionValidationGetH
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.v1.BulkLookupHandler;
import io.kamax.mxisd.http.undertow.handler.identity.v1.SingleLookupHandler;
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;
@@ -114,13 +115,6 @@ public class HttpMxisd {
.post(LoginHandler.Path, SaneHandler.around(new LoginPostHandler(m.getAuth())))
.post(RestAuthHandler.Path, SaneHandler.around(new RestAuthHandler(m.getAuth())))
// 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()))))
// Directory endpoints
.post(UserDirectorySearchHandler.Path, SaneHandler.around(new UserDirectorySearchHandler(m.getDirectory())))
@@ -150,6 +144,7 @@ public class HttpMxisd {
identityEndpoints(handler);
termsEndpoints(handler);
hashEndpoints(handler);
accountEndpoints(handler);
httpSrv = Undertow.builder().addHttpListener(m.getConfig().getServer().getPort(), "0.0.0.0").setHandler(handler).build();
httpSrv.start();
@@ -193,17 +188,25 @@ public class HttpMxisd {
);
}
private void accountEndpoints(RoutingHandler routingHandler) {
routingHandler.post(AccountRegisterHandler.Path, SaneHandler.around(new AccountRegisterHandler(m.getAccMgr())));
wrapWithTokenAndAuthorizationHandlers(routingHandler, Methods.GET, sane(new AccountGetUserInfoHandler(m.getAccMgr())),
AccountGetUserInfoHandler.Path, true);
wrapWithTokenAndAuthorizationHandlers(routingHandler, Methods.GET, sane(new AccountLogoutHandler(m.getAccMgr())),
AccountLogoutHandler.Path, true);
}
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()))));
wrapWithTokenAndAuthorizationHandlers(routingHandler, Methods.POST, sane(new AcceptTermsHandler(m.getAccMgr())),
AcceptTermsHandler.PATH, true);
}
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()))));
wrapWithTokenAndAuthorizationHandlers(routingHandler, Methods.GET, sane(new HashDetailsHandler(m.getHashManager())),
HashDetailsHandler.PATH, true);
wrapWithTokenAndAuthorizationHandlers(routingHandler, Methods.POST,
sane(new HashLookupHandler(m.getIdentity(), m.getHashManager())), HashLookupHandler.Path, true);
}
private void addEndpoints(RoutingHandler routingHandler, HttpString method, boolean useAuthorization, ApiHandler... handlers) {
@@ -219,20 +222,32 @@ public class HttpMxisd {
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);
String path = apiHandler.getPath(IdentityServiceAPI.V2);
wrapWithTokenAndAuthorizationHandlers(routingHandler, method, httpHandler, path, useAuthorization);
}
}
private void wrapWithTokenAndAuthorizationHandlers(RoutingHandler routingHandler, HttpString method, HttpHandler httpHandler,
String url, boolean useAuthorization) {
List<PolicyConfig.PolicyObject> policyObjects = getPolicyObjects(url);
HttpHandler wrappedHandler;
if (useAuthorization) {
wrappedHandler = policyObjects.isEmpty() ? httpHandler : CheckTermsHandler.around(m.getAccMgr(), httpHandler, policyObjects);
wrappedHandler = AuthorizationHandler.around(m.getAccMgr(), wrappedHandler);
} else {
wrappedHandler = httpHandler;
}
routingHandler.add(method, url, wrappedHandler);
}
@NotNull
private List<PolicyConfig.PolicyObject> getPolicyObjects(ApiHandler apiHandler) {
private List<PolicyConfig.PolicyObject> getPolicyObjects(String url) {
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()) {
if (pattern.matcher(url).matches()) {
policies.add(policy);
}
}

View File

@@ -125,7 +125,7 @@ public class Mxisd {
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);
sessMgr = new SessionManager(cfg, store, notifMgr, resolver, signMgr);
invMgr = new InvitationManager(cfg, store, idStrategy, keyMgr, signMgr, resolver, notifMgr, pMgr);
authMgr = new AuthManager(cfg, AuthProviders.get(), idStrategy, invMgr, clientDns, httpClient);
dirMgr = new DirectoryManager(cfg.getDirectory(), clientDns, httpClient, DirectoryProviders.get());

View File

@@ -16,7 +16,7 @@ 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.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -62,7 +62,7 @@ public class AccountManager {
String token = UUID.randomUUID().toString();
AccountDao account = new AccountDao(openIdToken.getAccessToken(), openIdToken.getTokenType(),
openIdToken.getMatrixServerName(), openIdToken.getExpiredIn(),
openIdToken.getMatrixServerName(), openIdToken.getExpiresIn(),
Instant.now().getEpochSecond(), userId, token);
storage.insertToken(account);
@@ -73,13 +73,14 @@ public class AccountManager {
private String getUserId(OpenIdToken openIdToken) {
String matrixServerName = openIdToken.getMatrixServerName();
String homeserverURL = resolver.resolve(matrixServerName).toString();
HomeserverFederationResolver.HomeserverTarget homeserverTarget = resolver.resolve(matrixServerName);
String homeserverURL = homeserverTarget.getUrl().toString();
LOGGER.info("Domain resolved: {} => {}", matrixServerName, homeserverURL);
HttpGet getUserInfo = new HttpGet(
homeserverURL + "/_matrix/federation/v1/openid/userinfo?access_token=" + openIdToken.getAccessToken());
String userId;
try (CloseableHttpClient httpClient = HttpClientBuilder.create()
.setSSLHostnameVerifier(new MatrixHostnameVerifier(matrixServerName)).build()) {
try (CloseableHttpClient httpClient = HttpClients.custom()
.setSSLHostnameVerifier(new MatrixHostnameVerifier(homeserverTarget.getDomain())).build()) {
try (CloseableHttpResponse response = httpClient.execute(getUserInfo)) {
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == HttpStatus.SC_OK) {

View File

@@ -1,14 +1,20 @@
package io.kamax.mxisd.auth;
import com.google.gson.annotations.SerializedName;
public class OpenIdToken {
@SerializedName("access_token")
private String accessToken;
@SerializedName("token_type")
private String tokenType;
@SerializedName("matrix_server_name")
private String matrixServerName;
private long expiredIn;
@SerializedName("expires_in")
private long expiresIn;
public String getAccessToken() {
return accessToken;
@@ -34,11 +40,11 @@ public class OpenIdToken {
this.matrixServerName = matrixServerName;
}
public long getExpiredIn() {
return expiredIn;
public long getExpiresIn() {
return expiresIn;
}
public void setExpiredIn(long expiredIn) {
this.expiredIn = expiredIn;
public void setExpiresIn(long expiresIn) {
this.expiresIn = expiresIn;
}
}

View File

@@ -11,7 +11,7 @@ public class HashingConfig {
private static final Logger LOGGER = LoggerFactory.getLogger(HashingConfig.class);
private boolean enabled = false;
private int pepperLength = 10;
private int pepperLength = 20;
private RotationPolicyEnum rotationPolicy;
private HashStorageEnum hashStorageType;
private long delay = 10;
@@ -23,27 +23,28 @@ public class HashingConfig {
LOGGER.info(" Pepper length: {}", getPepperLength());
LOGGER.info(" Rotation policy: {}", getRotationPolicy());
LOGGER.info(" Hash storage type: {}", getHashStorageType());
if (RotationPolicyEnum.PER_SECONDS == rotationPolicy) {
if (RotationPolicyEnum.per_seconds == rotationPolicy) {
LOGGER.info(" Rotation delay: {}", delay);
}
LOGGER.info(" Algorithms: {}", algorithms);
} else {
LOGGER.info("Hash configuration disabled, used only `none` pepper.");
}
}
public enum Algorithm {
NONE,
SHA256
none,
sha256
}
public enum RotationPolicyEnum {
PER_REQUESTS,
PER_SECONDS
per_requests,
per_seconds
}
public enum HashStorageEnum {
IN_MEMORY,
SQL
in_memory,
sql
}
public boolean isEnabled() {

View File

@@ -100,10 +100,12 @@ public class PolicyConfig {
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");
if (policyObject.getTerms() != null) {
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

@@ -125,7 +125,7 @@ public abstract class SqlConfig {
}
public static class Lookup {
private String query;
private String query = "SELECT user_id AS mxid, medium, address from user_threepids";
public String getQuery() {
return query;

View File

@@ -1,19 +1,22 @@
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.codec.digest.DigestUtils;
import org.apache.commons.lang3.RandomStringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
public class HashEngine {
private static final Logger LOGGER = LoggerFactory.getLogger(HashEngine.class);
private final List<? extends IThreePidProvider> providers;
private final HashStorage hashStorage;
private final MxSha256 sha256 = new MxSha256();
private final HashingConfig config;
private String pepper;
@@ -24,15 +27,21 @@ public class HashEngine {
}
public void updateHashes() {
LOGGER.info("Start update hashes.");
synchronized (hashStorage) {
this.pepper = newPepper();
hashStorage.clear();
for (IThreePidProvider provider : providers) {
for (ThreePidMapping pidMapping : provider.populateHashes()) {
hashStorage.add(pidMapping, hash(pidMapping));
try {
for (ThreePidMapping pidMapping : provider.populateHashes()) {
hashStorage.add(pidMapping, hash(pidMapping));
}
} catch (Exception e) {
LOGGER.error("Unable to update hashes of the provider: " + provider.toString(), e);
}
}
}
LOGGER.info("Finish update hashes.");
}
public String getPepper() {
@@ -42,10 +51,10 @@ public class HashEngine {
}
protected String hash(ThreePidMapping pidMapping) {
return sha256.hash(pidMapping.getMedium() + " " + pidMapping.getValue() + " " + getPepper());
return DigestUtils.sha256Hex(pidMapping.getValue() + " " + pidMapping.getMedium() + " " + getPepper());
}
protected String newPepper() {
return RandomStringUtils.random(config.getPepperLength());
return RandomStringUtils.random(config.getPepperLength(), true, true);
}
}

View File

@@ -40,10 +40,10 @@ public class HashManager {
private void initStorage() {
if (config.isEnabled()) {
switch (config.getHashStorageType()) {
case IN_MEMORY:
case in_memory:
this.hashStorage = new InMemoryHashStorage();
break;
case SQL:
case sql:
this.hashStorage = new SqlHashStorage(storage);
break;
default:
@@ -57,10 +57,10 @@ public class HashManager {
private void initRotationStrategy() {
if (config.isEnabled()) {
switch (config.getRotationPolicy()) {
case PER_REQUESTS:
case per_requests:
this.rotationStrategy = new RotationPerRequests();
break;
case PER_SECONDS:
case per_seconds:
this.rotationStrategy = new TimeBasedRotation(config.getDelay());
break;
default:

View File

@@ -12,6 +12,7 @@ public class RotationPerRequests implements HashRotationStrategy {
@Override
public void register(HashEngine hashEngine) {
this.hashEngine = hashEngine;
trigger();
}
@Override

View File

@@ -58,7 +58,8 @@ public class AuthorizationHandler extends BasicHttpHandler {
log.error("Account not found from request from: {}", exchange.getHostAndPort());
throw new InvalidCredentialsException();
}
if (account.getExpiresIn() < System.currentTimeMillis()) {
long expiredAt = (account.getCreatedAt() + account.getExpiresIn()) * 1000; // expired in milliseconds
if (expiredAt < System.currentTimeMillis()) {
log.error("Account for '{}' from: {}", account.getUserId(), exchange.getHostAndPort());
accountManager.deleteAccount(token);
throw new InvalidCredentialsException();

View File

@@ -23,7 +23,6 @@ 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;
@@ -54,6 +53,11 @@ public class CheckTermsHandler extends BasicHttpHandler {
@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
if (policies == null || policies.isEmpty()) {
child.handleRequest(exchange);
return;
}
String token = findAccessToken(exchange).orElse(null);
if (token == null) {
log.error("Unauthorized request from: {}", exchange.getHostAndPort());

View File

@@ -48,7 +48,7 @@ public class AccountRegisterHandler extends BasicHttpHandler {
if (LOGGER.isInfoEnabled()) {
LOGGER.info("Registration from domain: {}, expired at {}", openIdToken.getMatrixServerName(),
new Date(openIdToken.getExpiredIn()));
new Date(openIdToken.getExpiresIn()));
}
String token = accountManager.register(openIdToken);

View File

@@ -60,6 +60,7 @@ public class HashLookupHandler extends LookupHandler implements ApiHandler {
ClientHashLookupRequest input = parseJsonTo(exchange, ClientHashLookupRequest.class);
HashLookupRequest lookupRequest = new HashLookupRequest();
setRequesterInfo(lookupRequest, exchange);
lookupRequest.setHashes(input.getAddresses());
log.info("Got bulk lookup request from {} with client {} - Is recursive? {}",
lookupRequest.getRequester(), lookupRequest.getUserAgent(), lookupRequest.isRecursive());
@@ -84,7 +85,7 @@ public class HashLookupHandler extends LookupHandler implements ApiHandler {
}
private void noneAlgorithm(HttpServerExchange exchange, HashLookupRequest request, ClientHashLookupRequest input) throws Exception {
if (!hashManager.getConfig().getAlgorithms().contains(HashingConfig.Algorithm.NONE)) {
if (!hashManager.getConfig().getAlgorithms().contains(HashingConfig.Algorithm.none)) {
throw new InvalidParamException();
}
@@ -93,8 +94,8 @@ public class HashLookupHandler extends LookupHandler implements ApiHandler {
for (String address : input.getAddresses()) {
String[] parts = address.split(" ");
ThreePidMapping mapping = new ThreePidMapping();
mapping.setMedium(parts[0]);
mapping.setValue(parts[1]);
mapping.setMedium(parts[1]);
mapping.setValue(parts[0]);
mappings.add(mapping);
}
bulkLookupRequest.setMappings(mappings);
@@ -110,15 +111,19 @@ public class HashLookupHandler extends LookupHandler implements ApiHandler {
}
private void sha256Algorithm(HttpServerExchange exchange, HashLookupRequest request, ClientHashLookupRequest input) {
if (!hashManager.getConfig().getAlgorithms().contains(HashingConfig.Algorithm.SHA256)) {
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());
if (request.getHashes() != null && !request.getHashes().isEmpty()) {
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());
} else {
log.warn("Empty request");
}
log.info("Finished bulk lookup request from {}", request.getRequester());
respondJson(exchange, answer);
}

View File

@@ -2,7 +2,6 @@ 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;
@@ -28,7 +27,7 @@ public class AcceptTermsHandler extends BasicHttpHandler {
String token = getAccessToken(exchange);
JsonObject request = parseJsonObject(exchange);
JsonObject accepts = GsonUtil.getObj(request, "user_accepts");
JsonElement accepts = request.get("user_accepts");
AccountDao account = accountManager.findAccount(token);
if (account == null) {

View File

@@ -29,7 +29,11 @@ import io.kamax.matrix.json.GsonUtil;
import io.kamax.mxisd.config.InvitationConfig;
import io.kamax.mxisd.config.MxisdConfig;
import io.kamax.mxisd.config.ServerConfig;
import io.kamax.mxisd.crypto.*;
import io.kamax.mxisd.crypto.GenericKeyIdentifier;
import io.kamax.mxisd.crypto.KeyIdentifier;
import io.kamax.mxisd.crypto.KeyManager;
import io.kamax.mxisd.crypto.KeyType;
import io.kamax.mxisd.crypto.SignatureManager;
import io.kamax.mxisd.exception.BadRequestException;
import io.kamax.mxisd.exception.ConfigurationException;
import io.kamax.mxisd.exception.MappingAlreadyExistsException;
@@ -38,6 +42,7 @@ import io.kamax.mxisd.lookup.SingleLookupReply;
import io.kamax.mxisd.lookup.ThreePidMapping;
import io.kamax.mxisd.lookup.strategy.LookupStrategy;
import io.kamax.mxisd.matrix.HomeserverFederationResolver;
import io.kamax.mxisd.matrix.HomeserverVerifier;
import io.kamax.mxisd.notification.NotificationManager;
import io.kamax.mxisd.profile.ProfileManager;
import io.kamax.mxisd.storage.IStorage;
@@ -48,23 +53,26 @@ import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.TrustSelfSignedStrategy;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.ssl.SSLContextBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.DateTimeException;
import java.time.Instant;
import java.util.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.TimeUnit;
@@ -86,7 +94,6 @@ public class InvitationManager {
private NotificationManager notifMgr;
private ProfileManager profileMgr;
private CloseableHttpClient client;
private Timer refreshTimer;
private Map<String, IThreePidInviteReply> invitations = new ConcurrentHashMap<>();
@@ -129,17 +136,6 @@ public class InvitationManager {
});
log.info("Loaded saved invites");
// FIXME export such madness into matrix-java-sdk with a nice wrapper to talk to a homeserver
try {
SSLContext sslContext = SSLContextBuilder.create().loadTrustMaterial(new TrustSelfSignedStrategy()).build();
HostnameVerifier hostnameVerifier = new NoopHostnameVerifier();
SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(sslContext, hostnameVerifier);
client = HttpClients.custom().setSSLSocketFactory(sslSocketFactory).build();
} catch (Exception e) {
// FIXME do better...
throw new RuntimeException(e);
}
log.info("Setting up invitation mapping refresh timer");
refreshTimer = new Timer();
@@ -423,11 +419,11 @@ public class InvitationManager {
String address = reply.getInvite().getAddress();
String domain = reply.getInvite().getSender().getDomain();
log.info("Discovering HS for domain {}", domain);
String hsUrlOpt = resolver.resolve(domain).toString();
HomeserverFederationResolver.HomeserverTarget hsUrlOpt = resolver.resolve(domain);
// TODO this is needed as this will block if called during authentication cycle due to synapse implementation
new Thread(() -> { // FIXME need to make this retry-able and within a general background working pool
HttpPost req = new HttpPost(hsUrlOpt + "/_matrix/federation/v1/3pid/onbind");
HttpPost req = new HttpPost(hsUrlOpt.getUrl().toString() + "/_matrix/federation/v1/3pid/onbind");
// Expected body: https://matrix.to/#/!HUeDbmFUsWAhxHHvFG:matrix.org/$150469846739DCLWc:matrix.trancendances.fr
JsonObject obj = new JsonObject();
obj.addProperty("mxid", mxid);
@@ -459,36 +455,41 @@ public class InvitationManager {
Instant resolvedAt = Instant.now();
boolean couldPublish = false;
boolean shouldArchive = true;
try {
log.info("Posting onBind event to {}", req.getURI());
CloseableHttpResponse response = client.execute(req);
int statusCode = response.getStatusLine().getStatusCode();
log.info("Answer code: {}", statusCode);
if (statusCode >= 300 && statusCode != 403) {
log.info("Answer body: {}", IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8));
log.warn("HS returned an error.");
try (CloseableHttpClient httpClient = HttpClients.custom().setSSLHostnameVerifier(new HomeserverVerifier(hsUrlOpt.getDomain()))
.build()) {
try {
log.info("Posting onBind event to {}", req.getURI());
CloseableHttpResponse response = httpClient.execute(req);
int statusCode = response.getStatusLine().getStatusCode();
log.info("Answer code: {}", statusCode);
if (statusCode >= 300 && statusCode != 403) {
log.info("Answer body: {}", IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8));
log.warn("HS returned an error.");
shouldArchive = statusCode != 502;
shouldArchive = statusCode != 502;
if (shouldArchive) {
log.info("Invite can be found in historical storage for manual re-processing");
}
} else {
couldPublish = true;
if (statusCode == 403) {
log.info("Invite is obsolete or no longer under our control");
}
}
response.close();
} catch (IOException e) {
log.warn("Unable to tell HS {} about invite being mapped", domain, e);
} finally {
if (shouldArchive) {
log.info("Invite can be found in historical storage for manual re-processing");
}
} else {
couldPublish = true;
if (statusCode == 403) {
log.info("Invite is obsolete or no longer under our control");
synchronized (this) {
storage.insertHistoricalInvite(reply, mxid, resolvedAt, couldPublish);
removeInvite(reply);
log.info("Moved invite {} to historical table", reply.getId());
}
}
}
response.close();
} catch (IOException e) {
log.warn("Unable to tell HS {} about invite being mapped", domain, e);
} finally {
if (shouldArchive) {
synchronized (this) {
storage.insertHistoricalInvite(reply, mxid, resolvedAt, couldPublish);
removeInvite(reply);
log.info("Moved invite {} to historical table", reply.getId());
}
}
log.error("Unable to create client to the " + hsUrlOpt.getUrl().toString(), e);
}
}).start();
}

View File

@@ -178,26 +178,26 @@ public class HomeserverFederationResolver {
}
}
public URL resolve(String domain) {
public HomeserverTarget resolve(String domain) {
Optional<URL> s1 = resolveOverwrite(domain);
if (s1.isPresent()) {
URL dest = s1.get();
log.info("Resolution of {} via DNS overwrite to {}", domain, dest);
return dest;
return new HomeserverTarget(dest.getHost(), dest);
}
Optional<URL> s2 = resolveLiteral(domain);
if (s2.isPresent()) {
URL dest = s2.get();
log.info("Resolution of {} as IP literal or IP/hostname with explicit port to {}", domain, dest);
return dest;
return new HomeserverTarget(dest.getHost(), dest);
}
Optional<URL> s3 = resolveWellKnown(domain);
if (s3.isPresent()) {
URL dest = s3.get();
log.info("Resolution of {} via well-known to {}", domain, dest);
return dest;
return new HomeserverTarget(dest.getHost(), dest);
}
// The domain needs to be resolved
@@ -205,12 +205,30 @@ public class HomeserverFederationResolver {
if (s4.isPresent()) {
URL dest = s4.get();
log.info("Resolution of {} via DNS SRV record to {}", domain, dest);
return dest;
return new HomeserverTarget(domain, dest);
}
URL dest = build(domain + ":" + getDefaultPort());
log.info("Resolution of {} to {}", domain, dest);
return dest;
return new HomeserverTarget(dest.getHost(), dest);
}
public static class HomeserverTarget {
private final String domain;
private final URL url;
HomeserverTarget(String domain, URL url) {
this.domain = domain;
this.url = url;
}
public String getDomain() {
return domain;
}
public URL getUrl() {
return url;
}
}
}

View File

@@ -0,0 +1,85 @@
package io.kamax.mxisd.matrix;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.security.cert.Certificate;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
public class HomeserverVerifier implements HostnameVerifier {
private static final Logger LOGGER = LoggerFactory.getLogger(HomeserverVerifier.class);
private static final String ALT_DNS_NAME_TYPE = "2";
private static final String ALT_IP_ADDRESS_TYPE = "7";
private final String matrixHostname;
public HomeserverVerifier(String matrixHostname) {
this.matrixHostname = matrixHostname;
}
@Override
public boolean verify(String hostname, SSLSession session) {
try {
Certificate peerCertificate = session.getPeerCertificates()[0];
if (peerCertificate instanceof X509Certificate) {
X509Certificate x509Certificate = (X509Certificate) peerCertificate;
if (x509Certificate.getSubjectAlternativeNames() == null) {
return false;
}
for (String altSubjectName : getAltSubjectNames(x509Certificate)) {
if (match(altSubjectName)) {
return true;
}
}
}
} catch (SSLPeerUnverifiedException | CertificateParsingException e) {
LOGGER.error("Unable to check remote host", e);
return false;
}
return false;
}
private List<String> getAltSubjectNames(X509Certificate x509Certificate) {
List<String> subjectNames = new ArrayList<>();
try {
for (List<?> subjectAlternativeNames : x509Certificate.getSubjectAlternativeNames()) {
if (subjectAlternativeNames == null
|| subjectAlternativeNames.size() < 2
|| subjectAlternativeNames.get(0) == null
|| subjectAlternativeNames.get(1) == null) {
continue;
}
String subjectType = subjectAlternativeNames.get(0).toString();
switch (subjectType) {
case ALT_DNS_NAME_TYPE:
case ALT_IP_ADDRESS_TYPE:
subjectNames.add(subjectAlternativeNames.get(1).toString());
break;
default:
LOGGER.trace("Unusable subject type: " + subjectType);
}
}
} catch (CertificateParsingException e) {
LOGGER.error("Unable to parse the certificate", e);
return Collections.emptyList();
}
return subjectNames;
}
private boolean match(String altSubjectName) {
if (altSubjectName.startsWith("*.")) {
return altSubjectName.toLowerCase().endsWith(matrixHostname.toLowerCase());
} else {
return matrixHostname.equalsIgnoreCase(altSubjectName);
}
}
}

View File

@@ -39,6 +39,7 @@ import io.kamax.mxisd.lookup.SingleLookupReply;
import io.kamax.mxisd.lookup.SingleLookupRequest;
import io.kamax.mxisd.lookup.ThreePidValidation;
import io.kamax.mxisd.matrix.HomeserverFederationResolver;
import io.kamax.mxisd.matrix.HomeserverVerifier;
import io.kamax.mxisd.notification.NotificationManager;
import io.kamax.mxisd.storage.IStorage;
import io.kamax.mxisd.storage.dao.IThreePidSessionDao;
@@ -53,6 +54,7 @@ import org.apache.commons.lang3.StringUtils;
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.impl.client.HttpClients;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -73,7 +75,6 @@ public class SessionManager {
private IStorage storage;
private NotificationManager notifMgr;
private HomeserverFederationResolver resolver;
private CloseableHttpClient client;
private SignatureManager signatureManager;
public SessionManager(
@@ -81,14 +82,12 @@ public class SessionManager {
IStorage storage,
NotificationManager notifMgr,
HomeserverFederationResolver resolver,
CloseableHttpClient client,
SignatureManager signatureManager
) {
this.cfg = cfg;
this.storage = storage;
this.notifMgr = notifMgr;
this.resolver = resolver;
this.client = client;
this.signatureManager = signatureManager;
}
@@ -308,25 +307,34 @@ public class SessionManager {
String canonical = MatrixJson.encodeCanonical(jsonObject);
String originUrl = resolver.resolve(origin).toString();
HomeserverFederationResolver.HomeserverTarget homeserverTarget = resolver.resolve(origin);
validateServerKey(key, sig, canonical, originUrl);
validateServerKey(key, sig, canonical, homeserverTarget);
}
private String removeQuotes(String origin) {
return origin.startsWith("\"") && origin.endsWith("\"") ? origin.substring(1, origin.length() - 1) : origin;
}
private void validateServerKey(String key, String signature, String canonical, String originUrl) {
private void validateServerKey(String key, String signature, String canonical,
HomeserverFederationResolver.HomeserverTarget homeserverTarget) {
String originUrl = homeserverTarget.getUrl().toString();
HttpGet request = new HttpGet(originUrl + "/_matrix/key/v2/server");
log.info("Get keys from the server {}", request.getURI());
try (CloseableHttpResponse response = client.execute(request)) {
int statusCode = response.getStatusLine().getStatusCode();
log.info("Answer code: {}", statusCode);
if (statusCode == 200) {
verifyKey(key, signature, canonical, response);
} else {
throw new RemoteHomeServerException("Unable to fetch server keys.");
try (CloseableHttpClient httpClient = HttpClients.custom()
.setSSLHostnameVerifier(new HomeserverVerifier(homeserverTarget.getDomain())).build()) {
try (CloseableHttpResponse response = httpClient.execute(request)) {
int statusCode = response.getStatusLine().getStatusCode();
log.info("Answer code: {}", statusCode);
if (statusCode == 200) {
verifyKey(key, signature, canonical, response);
} else {
throw new RemoteHomeServerException("Unable to fetch server keys.");
}
} catch (IOException e) {
String message = "Unable to get server keys: " + originUrl;
log.error(message, e);
throw new IllegalArgumentException(message);
}
} catch (IOException e) {
String message = "Unable to get server keys: " + originUrl;

View File

@@ -294,6 +294,13 @@ public class OrmLiteSqlStorage implements IStorage {
public void acceptTerm(String token, String url) {
withCatcher(() -> {
AccountDao account = findAccount(token).orElseThrow(InvalidCredentialsException::new);
List<AcceptedDao> acceptedTerms = acceptedDao.queryForEq("userId", account.getUserId());
for (AcceptedDao acceptedTerm : acceptedTerms) {
if (acceptedTerm.getUrl().equalsIgnoreCase(url)) {
// already accepted
return;
}
}
int created = acceptedDao.create(new AcceptedDao(url, account.getUserId(), System.currentTimeMillis()));
if (created != 1) {
throw new RuntimeException("Unexpected row count after DB action: " + created);

View File

@@ -0,0 +1,15 @@
package io.kamax.mxisd.test.hash;
import static org.junit.Assert.assertEquals;
import org.apache.commons.codec.digest.DigestUtils;
import org.junit.Test;
public class HashEngineTest {
@Test
public void sha256test() {
assertEquals("a26de61ae3055f84b33ac1a179b9ad5301f9109024f4db1ae653ea525d2136f4",
DigestUtils.sha256Hex("user2@mail.homeserver.tld email I9x4vpcWjqp9X8iiOY4a"));
}
}

View File

@@ -51,13 +51,13 @@ public class HomeserverFederationResolverTest {
@Test
public void hostnameWithoutPort() {
URL url = resolver.resolve("example.org");
URL url = resolver.resolve("example.org").getUrl();
assertEquals("https://example.org:8448", url.toString());
}
@Test
public void hostnameWithPort() {
URL url = resolver.resolve("example.org:443");
URL url = resolver.resolve("example.org:443").getUrl();
assertEquals("https://example.org:443", url.toString());
}