Compare commits

...

22 Commits

Author SHA1 Message Date
Anatoly Sablin
6216113400 FIx terms. 2019-11-29 23:38:52 +03:00
Anatoly Sablin
cb32441959 Fix sha256 hashing. Fix v2 lookup. 2019-11-29 00:26:08 +03:00
Anatoly Sablin
0ec4df2c06 Fix bug with token expiration. Increase the default length of the pepper. Update hashes on startup with RotationPerRequest strategy. Don't check for existing pepper on the none hash algorithm. 2019-11-28 00:28:11 +03:00
Anatoly Sablin
86b880069b Wrap with the CheckTermsHandler handlers only with authorization. 2019-11-27 22:55:34 +03:00
Anatoly Sablin
a97273fe77 Wrap with the CheckTermsHandler is necessary. 2019-11-25 23:35:56 +03:00
Anatoly Sablin
f9daf4d58a Make configuration enums in lowercase. Wrap create hashes by try-catch. Add initial part of the documentation. 2019-11-15 23:39:45 +03:00
Anatoly Sablin
9e4cabb69b Fix the token expiration period. 2019-11-15 22:50:08 +03:00
Anatoly Sablin
0b81de3cd0 Make the federation homeserver resolve more accurate (on resolve via DNS record check that the certificate present for the original host). 2019-11-13 23:08:34 +03:00
Anatoly Sablin
698a16ec17 Fix matrix server hostname verification. 2019-11-11 23:48:49 +03:00
Anatoly Sablin
619b70d860 Bump gradle to 6.0. 2019-11-11 23:48:49 +03:00
Anatoly Sablin
494c9e3941 Merge branch 'MSC2140'
# Conflicts:
#	src/main/java/io/kamax/mxisd/session/SessionManager.java
2019-11-07 22:29:25 +03:00
ma1uta
94441d0446 Merge pull request #6 from ma1uta/issues/3
Allow extended character sets for backward compatibility.
2019-10-22 21:14:10 +00:00
Anatoly Sablin
b4776b50e2 https://github.com/ma1uta/ma1sd/issues/3 Allow extended character sets for backward compatibility. 2019-10-23 00:09:29 +03:00
Anatoly Sablin
2458b38b75 Add the configuration description for enable/disable unbind feature in the session.md 2019-10-22 23:54:53 +03:00
ma1uta
249e28a8b5 Merge pull request #5 from eyecreate/patch-2
Allow configuring unbind notifications to be sent or not.
2019-10-22 20:52:46 +00:00
ma1uta
ba9e2d6121 Merge pull request #4 from eyecreate/patch-1
make sure destination only contains the hostname value and not whole URL
2019-10-21 20:27:33 +00:00
eyecreate
f042b82a50 add missing import 2019-10-21 16:06:23 -04:00
eyecreate
59071177ad typo 2019-10-21 15:33:28 -04:00
eyecreate
6450cd1f20 have session manager check session config for sending notifications on unbinds. 2019-10-21 15:30:46 -04:00
eyecreate
90bc244f3e update docs to something that works. 2019-10-21 15:29:55 -04:00
eyecreate
6e52a509db update session config to allow unbindin emails to not be sent. 2019-10-21 15:28:30 -04:00
eyecreate
5ca666981a make sure destination only contains the hostname value and not whole URL
When testing this, the public URL is containing the protocol "https://" which differs from synapse's values. This code makes sure to remove that extra data to signatures match. Is there ever a situation in which the public url is any different?
2019-10-21 14:31:40 -04:00
31 changed files with 482 additions and 206 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

@@ -103,8 +103,8 @@ session:
validation:
enabled: true
unbind:
notification:
enabled: true
notifications: true
enabled: true
# DO NOT COPY/PASTE AS-IS IN YOUR CONFIGURATION
# CONFIGURATION EXAMPLE
@@ -115,7 +115,7 @@ are allowed to do in terms of 3PID sessions. The policy has a global on/off swit
---
`unbind` controls warning notifications for 3PID removal.
`unbind` controls warning notifications for 3PID removal. Setting `notifications` for `unbind` to false will prevent unbind emails from sending.
### Web views
Once a user click on a validation link, it is taken to the Identity Server validation page where the token is submitted.

View File

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

6
gradlew vendored
View File

@@ -7,7 +7,7 @@
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
@@ -125,8 +125,8 @@ if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`

2
gradlew.bat vendored
View File

@@ -5,7 +5,7 @@
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem http://www.apache.org/licenses/LICENSE-2.0
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,

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,13 +125,13 @@ 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());
regMgr = new RegistrationManager(cfg.getRegister(), httpClient, clientDns, invMgr);
asHander = new AppSvcManager(this);
accMgr = new AccountManager(store, resolver, getHttpClient(), cfg.getAccountConfig(), cfg.getMatrix());
accMgr = new AccountManager(store, resolver, cfg.getAccountConfig(), cfg.getMatrix());
}
public MxisdConfig getConfig() {

View File

@@ -9,7 +9,6 @@ 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;
@@ -17,15 +16,24 @@ 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.HttpClients;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.security.cert.Certificate;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
public class AccountManager {
@@ -33,15 +41,12 @@ public class AccountManager {
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) {
public AccountManager(IStorage storage, HomeserverFederationResolver resolver, AccountConfig accountConfig, MatrixConfig matrixConfig) {
this.storage = storage;
this.resolver = resolver;
this.httpClient = httpClient;
this.accountConfig = accountConfig;
this.matrixConfig = matrixConfig;
}
@@ -57,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);
@@ -67,24 +72,32 @@ public class AccountManager {
}
private String getUserId(OpenIdToken openIdToken) {
String homeserverURL = resolver.resolve(openIdToken.getMatrixServerName()).toString();
LOGGER.info("Domain resolved: {} => {}", openIdToken.getMatrixServerName(), homeserverURL);
String matrixServerName = openIdToken.getMatrixServerName();
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 (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);
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) {
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();
}
} catch (IOException e) {
LOGGER.error("Unable to get user info.", e);
LOGGER.error("Unable to create a connection to host: " + homeserverURL, e);
throw new InvalidCredentialsException();
}
@@ -157,4 +170,74 @@ public class AccountManager {
public MatrixConfig getMatrixConfig() {
return matrixConfig;
}
public static class MatrixHostnameVerifier implements HostnameVerifier {
private static final String ALT_DNS_NAME_TYPE = "2";
private static final String ALT_IP_ADDRESS_TYPE = "7";
private final String matrixHostname;
public MatrixHostnameVerifier(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

@@ -140,7 +140,7 @@ public class AuthManager {
}
try {
MatrixID.asValid(mxId);
MatrixID.asAcceptable(mxId);
} catch (IllegalArgumentException e) {
log.warn("The returned User ID {} is not a valid Matrix ID. Login might fail at the Homeserver level", mxId);
}

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

@@ -47,6 +47,8 @@ public class SessionConfig {
public static class PolicyUnbind {
private boolean enabled = true;
private boolean notifications = true;
public boolean getEnabled() {
return enabled;
@@ -55,11 +57,20 @@ public class SessionConfig {
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public boolean shouldNotify() {
return notifications;
}
public void setNotifications(boolean notifications) {
this.notifications = notifications;
}
}
public Policy() {
validation.enabled = true;
unbind.enabled = true;
unbind.notifications = true;
}
private PolicyTemplate validation = new PolicyTemplate();

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,12 +54,14 @@ 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;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Calendar;
@@ -72,7 +75,6 @@ public class SessionManager {
private IStorage storage;
private NotificationManager notifMgr;
private HomeserverFederationResolver resolver;
private CloseableHttpClient client;
private SignatureManager signatureManager;
public SessionManager(
@@ -80,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;
}
@@ -236,7 +236,9 @@ public class SessionManager {
}
log.info("Unbinding of {} {} to {} is accepted", tpid.getMedium(), tpid.getAddress(), mxid.getId());
notifMgr.sendForUnbind(tpid);
if (cfg.getSession().getPolicy().getUnbind().shouldNotify()) {
notifMgr.sendForUnbind(tpid);
}
}
private String getDomain(String publicUrl) {
@@ -305,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());
}