Compare commits
22 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
6216113400 | ||
|
cb32441959 | ||
|
0ec4df2c06 | ||
|
86b880069b | ||
|
a97273fe77 | ||
|
f9daf4d58a | ||
|
9e4cabb69b | ||
|
0b81de3cd0 | ||
|
698a16ec17 | ||
|
619b70d860 | ||
|
494c9e3941 | ||
|
94441d0446 | ||
|
b4776b50e2 | ||
|
2458b38b75 | ||
|
249e28a8b5 | ||
|
ba9e2d6121 | ||
|
f042b82a50 | ||
|
59071177ad | ||
|
6450cd1f20 | ||
|
90bc244f3e | ||
|
6e52a509db | ||
|
5ca666981a |
47
docs/MSC2140_MSC2134.md
Normal file
47
docs/MSC2140_MSC2134.md
Normal 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
|
||||
|
@@ -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;
|
||||
|
@@ -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.
|
||||
|
5
gradle/wrapper/gradle-wrapper.properties
vendored
5
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -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
6
gradlew
vendored
@@ -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
2
gradlew.bat
vendored
@@ -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,
|
||||
|
@@ -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));
|
||||
}
|
||||
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -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() {
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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() {
|
||||
|
@@ -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());
|
||||
}
|
||||
|
@@ -48,6 +48,8 @@ public class SessionConfig {
|
||||
|
||||
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();
|
||||
|
@@ -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;
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -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:
|
||||
|
@@ -12,6 +12,7 @@ public class RotationPerRequests implements HashRotationStrategy {
|
||||
@Override
|
||||
public void register(HashEngine hashEngine) {
|
||||
this.hashEngine = hashEngine;
|
||||
trigger();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -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();
|
||||
|
@@ -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());
|
||||
|
@@ -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);
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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) {
|
||||
|
@@ -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();
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
85
src/main/java/io/kamax/mxisd/matrix/HomeserverVerifier.java
Normal file
85
src/main/java/io/kamax/mxisd/matrix/HomeserverVerifier.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
|
@@ -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);
|
||||
|
15
src/test/java/io/kamax/mxisd/test/hash/HashEngineTest.java
Normal file
15
src/test/java/io/kamax/mxisd/test/hash/HashEngineTest.java
Normal 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"));
|
||||
}
|
||||
}
|
@@ -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());
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user