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)
|
## Unbind (MSC1915)
|
||||||
- `session.policy.unbind.enabled`: Enable or disable unbind functionality (MSC1915). (Defaults to true).
|
- `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:
|
*Warning*: Unbind check incoming request by two ways:
|
||||||
- session validation.
|
- session validation.
|
||||||
- request signature via `X-Matrix` header and uses `server.publicUrl` property to construct the signing json;
|
- request signature via `X-Matrix` header and uses `server.publicUrl` property to construct the signing json;
|
||||||
|
@@ -103,7 +103,7 @@ session:
|
|||||||
validation:
|
validation:
|
||||||
enabled: true
|
enabled: true
|
||||||
unbind:
|
unbind:
|
||||||
notification:
|
notifications: true
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|
||||||
# DO NOT COPY/PASTE AS-IS IN YOUR CONFIGURATION
|
# DO NOT COPY/PASTE AS-IS IN YOUR CONFIGURATION
|
||||||
@@ -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
|
### Web views
|
||||||
Once a user click on a validation link, it is taken to the Identity Server validation page where the token is submitted.
|
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
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStorePath=wrapper/dists
|
distributionUrl=https\://services.gradle.org/distributions/gradle-6.0-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
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 not use this file except in compliance with the License.
|
||||||
# You may obtain a copy of the License at
|
# 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
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
# 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\""
|
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# For Cygwin, switch paths to Windows format before running java
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
if $cygwin ; then
|
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
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 not use this file except in compliance with the License.
|
||||||
@rem You may obtain a copy of the License at
|
@rem You may obtain a copy of the License at
|
||||||
@rem
|
@rem
|
||||||
@rem http://www.apache.org/licenses/LICENSE-2.0
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
@rem
|
@rem
|
||||||
@rem Unless required by applicable law or agreed to in writing, software
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
@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.SessionValidationPostHandler;
|
||||||
import io.kamax.mxisd.http.undertow.handler.identity.share.SignEd25519Handler;
|
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.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.HashDetailsHandler;
|
||||||
import io.kamax.mxisd.http.undertow.handler.identity.v2.HashLookupHandler;
|
import io.kamax.mxisd.http.undertow.handler.identity.v2.HashLookupHandler;
|
||||||
import io.kamax.mxisd.http.undertow.handler.invite.v1.RoomInviteHandler;
|
import io.kamax.mxisd.http.undertow.handler.invite.v1.RoomInviteHandler;
|
||||||
@@ -114,13 +115,6 @@ public class HttpMxisd {
|
|||||||
.post(LoginHandler.Path, SaneHandler.around(new LoginPostHandler(m.getAuth())))
|
.post(LoginHandler.Path, SaneHandler.around(new LoginPostHandler(m.getAuth())))
|
||||||
.post(RestAuthHandler.Path, SaneHandler.around(new RestAuthHandler(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
|
// Directory endpoints
|
||||||
.post(UserDirectorySearchHandler.Path, SaneHandler.around(new UserDirectorySearchHandler(m.getDirectory())))
|
.post(UserDirectorySearchHandler.Path, SaneHandler.around(new UserDirectorySearchHandler(m.getDirectory())))
|
||||||
|
|
||||||
@@ -150,6 +144,7 @@ public class HttpMxisd {
|
|||||||
identityEndpoints(handler);
|
identityEndpoints(handler);
|
||||||
termsEndpoints(handler);
|
termsEndpoints(handler);
|
||||||
hashEndpoints(handler);
|
hashEndpoints(handler);
|
||||||
|
accountEndpoints(handler);
|
||||||
httpSrv = Undertow.builder().addHttpListener(m.getConfig().getServer().getPort(), "0.0.0.0").setHandler(handler).build();
|
httpSrv = Undertow.builder().addHttpListener(m.getConfig().getServer().getPort(), "0.0.0.0").setHandler(handler).build();
|
||||||
|
|
||||||
httpSrv.start();
|
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) {
|
private void termsEndpoints(RoutingHandler routingHandler) {
|
||||||
routingHandler.get(GetTermsHandler.PATH, new GetTermsHandler(m.getConfig().getPolicy()));
|
routingHandler.get(GetTermsHandler.PATH, new GetTermsHandler(m.getConfig().getPolicy()));
|
||||||
routingHandler
|
wrapWithTokenAndAuthorizationHandlers(routingHandler, Methods.POST, sane(new AcceptTermsHandler(m.getAccMgr())),
|
||||||
.post(AcceptTermsHandler.PATH, AuthorizationHandler.around(m.getAccMgr(), sane(new AcceptTermsHandler(m.getAccMgr()))));
|
AcceptTermsHandler.PATH, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void hashEndpoints(RoutingHandler routingHandler) {
|
private void hashEndpoints(RoutingHandler routingHandler) {
|
||||||
routingHandler
|
wrapWithTokenAndAuthorizationHandlers(routingHandler, Methods.GET, sane(new HashDetailsHandler(m.getHashManager())),
|
||||||
.get(HashDetailsHandler.PATH, AuthorizationHandler.around(m.getAccMgr(), sane(new HashDetailsHandler(m.getHashManager()))));
|
HashDetailsHandler.PATH, true);
|
||||||
routingHandler.post(HashLookupHandler.Path,
|
wrapWithTokenAndAuthorizationHandlers(routingHandler, Methods.POST,
|
||||||
AuthorizationHandler.around(m.getAccMgr(), sane(new HashLookupHandler(m.getIdentity(), m.getHashManager()))));
|
sane(new HashLookupHandler(m.getIdentity(), m.getHashManager())), HashLookupHandler.Path, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addEndpoints(RoutingHandler routingHandler, HttpString method, boolean useAuthorization, ApiHandler... handlers) {
|
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);
|
routingHandler.add(method, apiHandler.getPath(IdentityServiceAPI.V1), httpHandler);
|
||||||
}
|
}
|
||||||
if (matrixConfig.isV2()) {
|
if (matrixConfig.isV2()) {
|
||||||
HttpHandler handlerWithTerms = CheckTermsHandler.around(m.getAccMgr(), httpHandler, getPolicyObjects(apiHandler));
|
String path = apiHandler.getPath(IdentityServiceAPI.V2);
|
||||||
HttpHandler wrappedHandler = useAuthorization ? AuthorizationHandler.around(m.getAccMgr(), handlerWithTerms) : handlerWithTerms;
|
wrapWithTokenAndAuthorizationHandlers(routingHandler, method, httpHandler, path, useAuthorization);
|
||||||
routingHandler.add(method, apiHandler.getPath(IdentityServiceAPI.V2), wrappedHandler);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
@NotNull
|
||||||
private List<PolicyConfig.PolicyObject> getPolicyObjects(ApiHandler apiHandler) {
|
private List<PolicyConfig.PolicyObject> getPolicyObjects(String url) {
|
||||||
PolicyConfig policyConfig = m.getConfig().getPolicy();
|
PolicyConfig policyConfig = m.getConfig().getPolicy();
|
||||||
List<PolicyConfig.PolicyObject> policies = new ArrayList<>();
|
List<PolicyConfig.PolicyObject> policies = new ArrayList<>();
|
||||||
if (!policyConfig.getPolicies().isEmpty()) {
|
if (!policyConfig.getPolicies().isEmpty()) {
|
||||||
for (PolicyConfig.PolicyObject policy : policyConfig.getPolicies().values()) {
|
for (PolicyConfig.PolicyObject policy : policyConfig.getPolicies().values()) {
|
||||||
for (Pattern pattern : policy.getPatterns()) {
|
for (Pattern pattern : policy.getPatterns()) {
|
||||||
if (pattern.matcher(apiHandler.getHandlerPath()).matches()) {
|
if (pattern.matcher(url).matches()) {
|
||||||
policies.add(policy);
|
policies.add(policy);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -125,13 +125,13 @@ public class Mxisd {
|
|||||||
idStrategy = new RecursivePriorityLookupStrategy(cfg.getLookup(), ThreePidProviders.get(), bridgeFetcher, hashManager);
|
idStrategy = new RecursivePriorityLookupStrategy(cfg.getLookup(), ThreePidProviders.get(), bridgeFetcher, hashManager);
|
||||||
pMgr = new ProfileManager(ProfileProviders.get(), clientDns, httpClient);
|
pMgr = new ProfileManager(ProfileProviders.get(), clientDns, httpClient);
|
||||||
notifMgr = new NotificationManager(cfg.getNotification(), NotificationHandlers.get());
|
notifMgr = new NotificationManager(cfg.getNotification(), NotificationHandlers.get());
|
||||||
sessMgr = new SessionManager(cfg, store, notifMgr, resolver, httpClient, signMgr);
|
sessMgr = new SessionManager(cfg, store, notifMgr, resolver, signMgr);
|
||||||
invMgr = new InvitationManager(cfg, store, idStrategy, keyMgr, signMgr, resolver, notifMgr, pMgr);
|
invMgr = new InvitationManager(cfg, store, idStrategy, keyMgr, signMgr, resolver, notifMgr, pMgr);
|
||||||
authMgr = new AuthManager(cfg, AuthProviders.get(), idStrategy, invMgr, clientDns, httpClient);
|
authMgr = new AuthManager(cfg, AuthProviders.get(), idStrategy, invMgr, clientDns, httpClient);
|
||||||
dirMgr = new DirectoryManager(cfg.getDirectory(), clientDns, httpClient, DirectoryProviders.get());
|
dirMgr = new DirectoryManager(cfg.getDirectory(), clientDns, httpClient, DirectoryProviders.get());
|
||||||
regMgr = new RegistrationManager(cfg.getRegister(), httpClient, clientDns, invMgr);
|
regMgr = new RegistrationManager(cfg.getRegister(), httpClient, clientDns, invMgr);
|
||||||
asHander = new AppSvcManager(this);
|
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() {
|
public MxisdConfig getConfig() {
|
||||||
|
@@ -9,7 +9,6 @@ import io.kamax.mxisd.config.PolicyConfig;
|
|||||||
import io.kamax.mxisd.exception.BadRequestException;
|
import io.kamax.mxisd.exception.BadRequestException;
|
||||||
import io.kamax.mxisd.exception.InvalidCredentialsException;
|
import io.kamax.mxisd.exception.InvalidCredentialsException;
|
||||||
import io.kamax.mxisd.exception.NotFoundException;
|
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.matrix.HomeserverFederationResolver;
|
||||||
import io.kamax.mxisd.storage.IStorage;
|
import io.kamax.mxisd.storage.IStorage;
|
||||||
import io.kamax.mxisd.storage.ormlite.dao.AccountDao;
|
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.CloseableHttpResponse;
|
||||||
import org.apache.http.client.methods.HttpGet;
|
import org.apache.http.client.methods.HttpGet;
|
||||||
import org.apache.http.impl.client.CloseableHttpClient;
|
import org.apache.http.impl.client.CloseableHttpClient;
|
||||||
|
import org.apache.http.impl.client.HttpClients;
|
||||||
import org.apache.http.util.EntityUtils;
|
import org.apache.http.util.EntityUtils;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.security.cert.Certificate;
|
||||||
|
import java.security.cert.CertificateParsingException;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import javax.net.ssl.HostnameVerifier;
|
||||||
|
import javax.net.ssl.SSLPeerUnverifiedException;
|
||||||
|
import javax.net.ssl.SSLSession;
|
||||||
|
|
||||||
public class AccountManager {
|
public class AccountManager {
|
||||||
|
|
||||||
@@ -33,15 +41,12 @@ public class AccountManager {
|
|||||||
|
|
||||||
private final IStorage storage;
|
private final IStorage storage;
|
||||||
private final HomeserverFederationResolver resolver;
|
private final HomeserverFederationResolver resolver;
|
||||||
private final CloseableHttpClient httpClient;
|
|
||||||
private final AccountConfig accountConfig;
|
private final AccountConfig accountConfig;
|
||||||
private final MatrixConfig matrixConfig;
|
private final MatrixConfig matrixConfig;
|
||||||
|
|
||||||
public AccountManager(IStorage storage, HomeserverFederationResolver resolver,
|
public AccountManager(IStorage storage, HomeserverFederationResolver resolver, AccountConfig accountConfig, MatrixConfig matrixConfig) {
|
||||||
CloseableHttpClient httpClient, AccountConfig accountConfig, MatrixConfig matrixConfig) {
|
|
||||||
this.storage = storage;
|
this.storage = storage;
|
||||||
this.resolver = resolver;
|
this.resolver = resolver;
|
||||||
this.httpClient = httpClient;
|
|
||||||
this.accountConfig = accountConfig;
|
this.accountConfig = accountConfig;
|
||||||
this.matrixConfig = matrixConfig;
|
this.matrixConfig = matrixConfig;
|
||||||
}
|
}
|
||||||
@@ -57,7 +62,7 @@ public class AccountManager {
|
|||||||
|
|
||||||
String token = UUID.randomUUID().toString();
|
String token = UUID.randomUUID().toString();
|
||||||
AccountDao account = new AccountDao(openIdToken.getAccessToken(), openIdToken.getTokenType(),
|
AccountDao account = new AccountDao(openIdToken.getAccessToken(), openIdToken.getTokenType(),
|
||||||
openIdToken.getMatrixServerName(), openIdToken.getExpiredIn(),
|
openIdToken.getMatrixServerName(), openIdToken.getExpiresIn(),
|
||||||
Instant.now().getEpochSecond(), userId, token);
|
Instant.now().getEpochSecond(), userId, token);
|
||||||
storage.insertToken(account);
|
storage.insertToken(account);
|
||||||
|
|
||||||
@@ -67,11 +72,15 @@ public class AccountManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private String getUserId(OpenIdToken openIdToken) {
|
private String getUserId(OpenIdToken openIdToken) {
|
||||||
String homeserverURL = resolver.resolve(openIdToken.getMatrixServerName()).toString();
|
String matrixServerName = openIdToken.getMatrixServerName();
|
||||||
LOGGER.info("Domain resolved: {} => {}", openIdToken.getMatrixServerName(), homeserverURL);
|
HomeserverFederationResolver.HomeserverTarget homeserverTarget = resolver.resolve(matrixServerName);
|
||||||
|
String homeserverURL = homeserverTarget.getUrl().toString();
|
||||||
|
LOGGER.info("Domain resolved: {} => {}", matrixServerName, homeserverURL);
|
||||||
HttpGet getUserInfo = new HttpGet(
|
HttpGet getUserInfo = new HttpGet(
|
||||||
homeserverURL + "/_matrix/federation/v1/openid/userinfo?access_token=" + openIdToken.getAccessToken());
|
homeserverURL + "/_matrix/federation/v1/openid/userinfo?access_token=" + openIdToken.getAccessToken());
|
||||||
String userId;
|
String userId;
|
||||||
|
try (CloseableHttpClient httpClient = HttpClients.custom()
|
||||||
|
.setSSLHostnameVerifier(new MatrixHostnameVerifier(homeserverTarget.getDomain())).build()) {
|
||||||
try (CloseableHttpResponse response = httpClient.execute(getUserInfo)) {
|
try (CloseableHttpResponse response = httpClient.execute(getUserInfo)) {
|
||||||
int statusCode = response.getStatusLine().getStatusCode();
|
int statusCode = response.getStatusLine().getStatusCode();
|
||||||
if (statusCode == HttpStatus.SC_OK) {
|
if (statusCode == HttpStatus.SC_OK) {
|
||||||
@@ -87,6 +96,10 @@ public class AccountManager {
|
|||||||
LOGGER.error("Unable to get user info.", e);
|
LOGGER.error("Unable to get user info.", e);
|
||||||
throw new InvalidCredentialsException();
|
throw new InvalidCredentialsException();
|
||||||
}
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOGGER.error("Unable to create a connection to host: " + homeserverURL, e);
|
||||||
|
throw new InvalidCredentialsException();
|
||||||
|
}
|
||||||
|
|
||||||
checkMXID(userId);
|
checkMXID(userId);
|
||||||
return userId;
|
return userId;
|
||||||
@@ -157,4 +170,74 @@ public class AccountManager {
|
|||||||
public MatrixConfig getMatrixConfig() {
|
public MatrixConfig getMatrixConfig() {
|
||||||
return matrixConfig;
|
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 {
|
try {
|
||||||
MatrixID.asValid(mxId);
|
MatrixID.asAcceptable(mxId);
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
log.warn("The returned User ID {} is not a valid Matrix ID. Login might fail at the Homeserver level", mxId);
|
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;
|
package io.kamax.mxisd.auth;
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName;
|
||||||
|
|
||||||
public class OpenIdToken {
|
public class OpenIdToken {
|
||||||
|
|
||||||
|
@SerializedName("access_token")
|
||||||
private String accessToken;
|
private String accessToken;
|
||||||
|
|
||||||
|
@SerializedName("token_type")
|
||||||
private String tokenType;
|
private String tokenType;
|
||||||
|
|
||||||
|
@SerializedName("matrix_server_name")
|
||||||
private String matrixServerName;
|
private String matrixServerName;
|
||||||
|
|
||||||
private long expiredIn;
|
@SerializedName("expires_in")
|
||||||
|
private long expiresIn;
|
||||||
|
|
||||||
public String getAccessToken() {
|
public String getAccessToken() {
|
||||||
return accessToken;
|
return accessToken;
|
||||||
@@ -34,11 +40,11 @@ public class OpenIdToken {
|
|||||||
this.matrixServerName = matrixServerName;
|
this.matrixServerName = matrixServerName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getExpiredIn() {
|
public long getExpiresIn() {
|
||||||
return expiredIn;
|
return expiresIn;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setExpiredIn(long expiredIn) {
|
public void setExpiresIn(long expiresIn) {
|
||||||
this.expiredIn = expiredIn;
|
this.expiresIn = expiresIn;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -11,7 +11,7 @@ public class HashingConfig {
|
|||||||
private static final Logger LOGGER = LoggerFactory.getLogger(HashingConfig.class);
|
private static final Logger LOGGER = LoggerFactory.getLogger(HashingConfig.class);
|
||||||
|
|
||||||
private boolean enabled = false;
|
private boolean enabled = false;
|
||||||
private int pepperLength = 10;
|
private int pepperLength = 20;
|
||||||
private RotationPolicyEnum rotationPolicy;
|
private RotationPolicyEnum rotationPolicy;
|
||||||
private HashStorageEnum hashStorageType;
|
private HashStorageEnum hashStorageType;
|
||||||
private long delay = 10;
|
private long delay = 10;
|
||||||
@@ -23,27 +23,28 @@ public class HashingConfig {
|
|||||||
LOGGER.info(" Pepper length: {}", getPepperLength());
|
LOGGER.info(" Pepper length: {}", getPepperLength());
|
||||||
LOGGER.info(" Rotation policy: {}", getRotationPolicy());
|
LOGGER.info(" Rotation policy: {}", getRotationPolicy());
|
||||||
LOGGER.info(" Hash storage type: {}", getHashStorageType());
|
LOGGER.info(" Hash storage type: {}", getHashStorageType());
|
||||||
if (RotationPolicyEnum.PER_SECONDS == rotationPolicy) {
|
if (RotationPolicyEnum.per_seconds == rotationPolicy) {
|
||||||
LOGGER.info(" Rotation delay: {}", delay);
|
LOGGER.info(" Rotation delay: {}", delay);
|
||||||
}
|
}
|
||||||
|
LOGGER.info(" Algorithms: {}", algorithms);
|
||||||
} else {
|
} else {
|
||||||
LOGGER.info("Hash configuration disabled, used only `none` pepper.");
|
LOGGER.info("Hash configuration disabled, used only `none` pepper.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum Algorithm {
|
public enum Algorithm {
|
||||||
NONE,
|
none,
|
||||||
SHA256
|
sha256
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum RotationPolicyEnum {
|
public enum RotationPolicyEnum {
|
||||||
PER_REQUESTS,
|
per_requests,
|
||||||
PER_SECONDS
|
per_seconds
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum HashStorageEnum {
|
public enum HashStorageEnum {
|
||||||
IN_MEMORY,
|
in_memory,
|
||||||
SQL
|
sql
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isEnabled() {
|
public boolean isEnabled() {
|
||||||
|
@@ -100,11 +100,13 @@ public class PolicyConfig {
|
|||||||
policyObjectItem.getValue().getPatterns().add(Pattern.compile(regexp));
|
policyObjectItem.getValue().getPatterns().add(Pattern.compile(regexp));
|
||||||
}
|
}
|
||||||
sb.append(" terms:\n");
|
sb.append(" terms:\n");
|
||||||
|
if (policyObject.getTerms() != null) {
|
||||||
for (Map.Entry<String, TermObject> termItem : policyObject.getTerms().entrySet()) {
|
for (Map.Entry<String, TermObject> termItem : policyObject.getTerms().entrySet()) {
|
||||||
sb.append(" - lang: ").append(termItem.getKey()).append("\n");
|
sb.append(" - lang: ").append(termItem.getKey()).append("\n");
|
||||||
sb.append(" name: ").append(termItem.getValue().getName()).append("\n");
|
sb.append(" name: ").append(termItem.getValue().getName()).append("\n");
|
||||||
sb.append(" url: ").append(termItem.getValue().getUrl()).append("\n");
|
sb.append(" url: ").append(termItem.getValue().getUrl()).append("\n");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
LOGGER.info(sb.toString());
|
LOGGER.info(sb.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -48,6 +48,8 @@ public class SessionConfig {
|
|||||||
|
|
||||||
private boolean enabled = true;
|
private boolean enabled = true;
|
||||||
|
|
||||||
|
private boolean notifications = true;
|
||||||
|
|
||||||
public boolean getEnabled() {
|
public boolean getEnabled() {
|
||||||
return enabled;
|
return enabled;
|
||||||
}
|
}
|
||||||
@@ -55,11 +57,20 @@ public class SessionConfig {
|
|||||||
public void setEnabled(boolean enabled) {
|
public void setEnabled(boolean enabled) {
|
||||||
this.enabled = enabled;
|
this.enabled = enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean shouldNotify() {
|
||||||
|
return notifications;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNotifications(boolean notifications) {
|
||||||
|
this.notifications = notifications;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Policy() {
|
public Policy() {
|
||||||
validation.enabled = true;
|
validation.enabled = true;
|
||||||
unbind.enabled = true;
|
unbind.enabled = true;
|
||||||
|
unbind.notifications = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private PolicyTemplate validation = new PolicyTemplate();
|
private PolicyTemplate validation = new PolicyTemplate();
|
||||||
|
@@ -125,7 +125,7 @@ public abstract class SqlConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static class Lookup {
|
public static class Lookup {
|
||||||
private String query;
|
private String query = "SELECT user_id AS mxid, medium, address from user_threepids";
|
||||||
|
|
||||||
public String getQuery() {
|
public String getQuery() {
|
||||||
return query;
|
return query;
|
||||||
|
@@ -1,19 +1,22 @@
|
|||||||
package io.kamax.mxisd.hash;
|
package io.kamax.mxisd.hash;
|
||||||
|
|
||||||
import io.kamax.matrix.codec.MxSha256;
|
|
||||||
import io.kamax.mxisd.config.HashingConfig;
|
import io.kamax.mxisd.config.HashingConfig;
|
||||||
import io.kamax.mxisd.hash.storage.HashStorage;
|
import io.kamax.mxisd.hash.storage.HashStorage;
|
||||||
import io.kamax.mxisd.lookup.ThreePidMapping;
|
import io.kamax.mxisd.lookup.ThreePidMapping;
|
||||||
import io.kamax.mxisd.lookup.provider.IThreePidProvider;
|
import io.kamax.mxisd.lookup.provider.IThreePidProvider;
|
||||||
|
import org.apache.commons.codec.digest.DigestUtils;
|
||||||
import org.apache.commons.lang3.RandomStringUtils;
|
import org.apache.commons.lang3.RandomStringUtils;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class HashEngine {
|
public class HashEngine {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(HashEngine.class);
|
||||||
|
|
||||||
private final List<? extends IThreePidProvider> providers;
|
private final List<? extends IThreePidProvider> providers;
|
||||||
private final HashStorage hashStorage;
|
private final HashStorage hashStorage;
|
||||||
private final MxSha256 sha256 = new MxSha256();
|
|
||||||
private final HashingConfig config;
|
private final HashingConfig config;
|
||||||
private String pepper;
|
private String pepper;
|
||||||
|
|
||||||
@@ -24,16 +27,22 @@ public class HashEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void updateHashes() {
|
public void updateHashes() {
|
||||||
|
LOGGER.info("Start update hashes.");
|
||||||
synchronized (hashStorage) {
|
synchronized (hashStorage) {
|
||||||
this.pepper = newPepper();
|
this.pepper = newPepper();
|
||||||
hashStorage.clear();
|
hashStorage.clear();
|
||||||
for (IThreePidProvider provider : providers) {
|
for (IThreePidProvider provider : providers) {
|
||||||
|
try {
|
||||||
for (ThreePidMapping pidMapping : provider.populateHashes()) {
|
for (ThreePidMapping pidMapping : provider.populateHashes()) {
|
||||||
hashStorage.add(pidMapping, hash(pidMapping));
|
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() {
|
public String getPepper() {
|
||||||
synchronized (hashStorage) {
|
synchronized (hashStorage) {
|
||||||
@@ -42,10 +51,10 @@ public class HashEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected String hash(ThreePidMapping pidMapping) {
|
protected String hash(ThreePidMapping pidMapping) {
|
||||||
return sha256.hash(pidMapping.getMedium() + " " + pidMapping.getValue() + " " + getPepper());
|
return DigestUtils.sha256Hex(pidMapping.getValue() + " " + pidMapping.getMedium() + " " + getPepper());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected String newPepper() {
|
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() {
|
private void initStorage() {
|
||||||
if (config.isEnabled()) {
|
if (config.isEnabled()) {
|
||||||
switch (config.getHashStorageType()) {
|
switch (config.getHashStorageType()) {
|
||||||
case IN_MEMORY:
|
case in_memory:
|
||||||
this.hashStorage = new InMemoryHashStorage();
|
this.hashStorage = new InMemoryHashStorage();
|
||||||
break;
|
break;
|
||||||
case SQL:
|
case sql:
|
||||||
this.hashStorage = new SqlHashStorage(storage);
|
this.hashStorage = new SqlHashStorage(storage);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@@ -57,10 +57,10 @@ public class HashManager {
|
|||||||
private void initRotationStrategy() {
|
private void initRotationStrategy() {
|
||||||
if (config.isEnabled()) {
|
if (config.isEnabled()) {
|
||||||
switch (config.getRotationPolicy()) {
|
switch (config.getRotationPolicy()) {
|
||||||
case PER_REQUESTS:
|
case per_requests:
|
||||||
this.rotationStrategy = new RotationPerRequests();
|
this.rotationStrategy = new RotationPerRequests();
|
||||||
break;
|
break;
|
||||||
case PER_SECONDS:
|
case per_seconds:
|
||||||
this.rotationStrategy = new TimeBasedRotation(config.getDelay());
|
this.rotationStrategy = new TimeBasedRotation(config.getDelay());
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
@@ -12,6 +12,7 @@ public class RotationPerRequests implements HashRotationStrategy {
|
|||||||
@Override
|
@Override
|
||||||
public void register(HashEngine hashEngine) {
|
public void register(HashEngine hashEngine) {
|
||||||
this.hashEngine = hashEngine;
|
this.hashEngine = hashEngine;
|
||||||
|
trigger();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@@ -58,7 +58,8 @@ public class AuthorizationHandler extends BasicHttpHandler {
|
|||||||
log.error("Account not found from request from: {}", exchange.getHostAndPort());
|
log.error("Account not found from request from: {}", exchange.getHostAndPort());
|
||||||
throw new InvalidCredentialsException();
|
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());
|
log.error("Account for '{}' from: {}", account.getUserId(), exchange.getHostAndPort());
|
||||||
accountManager.deleteAccount(token);
|
accountManager.deleteAccount(token);
|
||||||
throw new InvalidCredentialsException();
|
throw new InvalidCredentialsException();
|
||||||
|
@@ -23,7 +23,6 @@ package io.kamax.mxisd.http.undertow.handler;
|
|||||||
import io.kamax.mxisd.auth.AccountManager;
|
import io.kamax.mxisd.auth.AccountManager;
|
||||||
import io.kamax.mxisd.config.PolicyConfig;
|
import io.kamax.mxisd.config.PolicyConfig;
|
||||||
import io.kamax.mxisd.exception.InvalidCredentialsException;
|
import io.kamax.mxisd.exception.InvalidCredentialsException;
|
||||||
import io.kamax.mxisd.storage.ormlite.dao.AccountDao;
|
|
||||||
import io.undertow.server.HttpHandler;
|
import io.undertow.server.HttpHandler;
|
||||||
import io.undertow.server.HttpServerExchange;
|
import io.undertow.server.HttpServerExchange;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -54,6 +53,11 @@ public class CheckTermsHandler extends BasicHttpHandler {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleRequest(HttpServerExchange exchange) throws Exception {
|
public void handleRequest(HttpServerExchange exchange) throws Exception {
|
||||||
|
if (policies == null || policies.isEmpty()) {
|
||||||
|
child.handleRequest(exchange);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
String token = findAccessToken(exchange).orElse(null);
|
String token = findAccessToken(exchange).orElse(null);
|
||||||
if (token == null) {
|
if (token == null) {
|
||||||
log.error("Unauthorized request from: {}", exchange.getHostAndPort());
|
log.error("Unauthorized request from: {}", exchange.getHostAndPort());
|
||||||
|
@@ -48,7 +48,7 @@ public class AccountRegisterHandler extends BasicHttpHandler {
|
|||||||
|
|
||||||
if (LOGGER.isInfoEnabled()) {
|
if (LOGGER.isInfoEnabled()) {
|
||||||
LOGGER.info("Registration from domain: {}, expired at {}", openIdToken.getMatrixServerName(),
|
LOGGER.info("Registration from domain: {}, expired at {}", openIdToken.getMatrixServerName(),
|
||||||
new Date(openIdToken.getExpiredIn()));
|
new Date(openIdToken.getExpiresIn()));
|
||||||
}
|
}
|
||||||
|
|
||||||
String token = accountManager.register(openIdToken);
|
String token = accountManager.register(openIdToken);
|
||||||
|
@@ -60,6 +60,7 @@ public class HashLookupHandler extends LookupHandler implements ApiHandler {
|
|||||||
ClientHashLookupRequest input = parseJsonTo(exchange, ClientHashLookupRequest.class);
|
ClientHashLookupRequest input = parseJsonTo(exchange, ClientHashLookupRequest.class);
|
||||||
HashLookupRequest lookupRequest = new HashLookupRequest();
|
HashLookupRequest lookupRequest = new HashLookupRequest();
|
||||||
setRequesterInfo(lookupRequest, exchange);
|
setRequesterInfo(lookupRequest, exchange);
|
||||||
|
lookupRequest.setHashes(input.getAddresses());
|
||||||
log.info("Got bulk lookup request from {} with client {} - Is recursive? {}",
|
log.info("Got bulk lookup request from {} with client {} - Is recursive? {}",
|
||||||
lookupRequest.getRequester(), lookupRequest.getUserAgent(), lookupRequest.isRecursive());
|
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 {
|
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();
|
throw new InvalidParamException();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,8 +94,8 @@ public class HashLookupHandler extends LookupHandler implements ApiHandler {
|
|||||||
for (String address : input.getAddresses()) {
|
for (String address : input.getAddresses()) {
|
||||||
String[] parts = address.split(" ");
|
String[] parts = address.split(" ");
|
||||||
ThreePidMapping mapping = new ThreePidMapping();
|
ThreePidMapping mapping = new ThreePidMapping();
|
||||||
mapping.setMedium(parts[0]);
|
mapping.setMedium(parts[1]);
|
||||||
mapping.setValue(parts[1]);
|
mapping.setValue(parts[0]);
|
||||||
mappings.add(mapping);
|
mappings.add(mapping);
|
||||||
}
|
}
|
||||||
bulkLookupRequest.setMappings(mappings);
|
bulkLookupRequest.setMappings(mappings);
|
||||||
@@ -110,15 +111,19 @@ public class HashLookupHandler extends LookupHandler implements ApiHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void sha256Algorithm(HttpServerExchange exchange, HashLookupRequest request, ClientHashLookupRequest input) {
|
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();
|
throw new InvalidParamException();
|
||||||
}
|
}
|
||||||
|
|
||||||
ClientHashLookupAnswer answer = new ClientHashLookupAnswer();
|
ClientHashLookupAnswer answer = new ClientHashLookupAnswer();
|
||||||
|
if (request.getHashes() != null && !request.getHashes().isEmpty()) {
|
||||||
for (Pair<String, ThreePidMapping> pair : hashManager.getHashStorage().find(request.getHashes())) {
|
for (Pair<String, ThreePidMapping> pair : hashManager.getHashStorage().find(request.getHashes())) {
|
||||||
answer.getMappings().put(pair.getKey(), pair.getValue().getMxid());
|
answer.getMappings().put(pair.getKey(), pair.getValue().getMxid());
|
||||||
}
|
}
|
||||||
log.info("Finished bulk lookup request from {}", request.getRequester());
|
log.info("Finished bulk lookup request from {}", request.getRequester());
|
||||||
|
} else {
|
||||||
|
log.warn("Empty request");
|
||||||
|
}
|
||||||
|
|
||||||
respondJson(exchange, answer);
|
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.JsonElement;
|
||||||
import com.google.gson.JsonObject;
|
import com.google.gson.JsonObject;
|
||||||
import io.kamax.matrix.json.GsonUtil;
|
|
||||||
import io.kamax.mxisd.auth.AccountManager;
|
import io.kamax.mxisd.auth.AccountManager;
|
||||||
import io.kamax.mxisd.exception.InvalidCredentialsException;
|
import io.kamax.mxisd.exception.InvalidCredentialsException;
|
||||||
import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler;
|
import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler;
|
||||||
@@ -28,7 +27,7 @@ public class AcceptTermsHandler extends BasicHttpHandler {
|
|||||||
String token = getAccessToken(exchange);
|
String token = getAccessToken(exchange);
|
||||||
|
|
||||||
JsonObject request = parseJsonObject(exchange);
|
JsonObject request = parseJsonObject(exchange);
|
||||||
JsonObject accepts = GsonUtil.getObj(request, "user_accepts");
|
JsonElement accepts = request.get("user_accepts");
|
||||||
AccountDao account = accountManager.findAccount(token);
|
AccountDao account = accountManager.findAccount(token);
|
||||||
|
|
||||||
if (account == null) {
|
if (account == null) {
|
||||||
|
@@ -29,7 +29,11 @@ import io.kamax.matrix.json.GsonUtil;
|
|||||||
import io.kamax.mxisd.config.InvitationConfig;
|
import io.kamax.mxisd.config.InvitationConfig;
|
||||||
import io.kamax.mxisd.config.MxisdConfig;
|
import io.kamax.mxisd.config.MxisdConfig;
|
||||||
import io.kamax.mxisd.config.ServerConfig;
|
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.BadRequestException;
|
||||||
import io.kamax.mxisd.exception.ConfigurationException;
|
import io.kamax.mxisd.exception.ConfigurationException;
|
||||||
import io.kamax.mxisd.exception.MappingAlreadyExistsException;
|
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.ThreePidMapping;
|
||||||
import io.kamax.mxisd.lookup.strategy.LookupStrategy;
|
import io.kamax.mxisd.lookup.strategy.LookupStrategy;
|
||||||
import io.kamax.mxisd.matrix.HomeserverFederationResolver;
|
import io.kamax.mxisd.matrix.HomeserverFederationResolver;
|
||||||
|
import io.kamax.mxisd.matrix.HomeserverVerifier;
|
||||||
import io.kamax.mxisd.notification.NotificationManager;
|
import io.kamax.mxisd.notification.NotificationManager;
|
||||||
import io.kamax.mxisd.profile.ProfileManager;
|
import io.kamax.mxisd.profile.ProfileManager;
|
||||||
import io.kamax.mxisd.storage.IStorage;
|
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.commons.lang3.StringUtils;
|
||||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||||
import org.apache.http.client.methods.HttpPost;
|
import org.apache.http.client.methods.HttpPost;
|
||||||
import org.apache.http.conn.ssl.NoopHostnameVerifier;
|
|
||||||
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
|
|
||||||
import org.apache.http.conn.ssl.TrustSelfSignedStrategy;
|
|
||||||
import org.apache.http.entity.StringEntity;
|
import org.apache.http.entity.StringEntity;
|
||||||
import org.apache.http.impl.client.CloseableHttpClient;
|
import org.apache.http.impl.client.CloseableHttpClient;
|
||||||
import org.apache.http.impl.client.HttpClients;
|
import org.apache.http.impl.client.HttpClients;
|
||||||
import org.apache.http.ssl.SSLContextBuilder;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import javax.net.ssl.HostnameVerifier;
|
|
||||||
import javax.net.ssl.SSLContext;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.time.DateTimeException;
|
import java.time.DateTimeException;
|
||||||
import java.time.Instant;
|
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.ConcurrentHashMap;
|
||||||
import java.util.concurrent.ForkJoinPool;
|
import java.util.concurrent.ForkJoinPool;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
@@ -86,7 +94,6 @@ public class InvitationManager {
|
|||||||
private NotificationManager notifMgr;
|
private NotificationManager notifMgr;
|
||||||
private ProfileManager profileMgr;
|
private ProfileManager profileMgr;
|
||||||
|
|
||||||
private CloseableHttpClient client;
|
|
||||||
private Timer refreshTimer;
|
private Timer refreshTimer;
|
||||||
|
|
||||||
private Map<String, IThreePidInviteReply> invitations = new ConcurrentHashMap<>();
|
private Map<String, IThreePidInviteReply> invitations = new ConcurrentHashMap<>();
|
||||||
@@ -129,17 +136,6 @@ public class InvitationManager {
|
|||||||
});
|
});
|
||||||
log.info("Loaded saved invites");
|
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");
|
log.info("Setting up invitation mapping refresh timer");
|
||||||
refreshTimer = new Timer();
|
refreshTimer = new Timer();
|
||||||
|
|
||||||
@@ -423,11 +419,11 @@ public class InvitationManager {
|
|||||||
String address = reply.getInvite().getAddress();
|
String address = reply.getInvite().getAddress();
|
||||||
String domain = reply.getInvite().getSender().getDomain();
|
String domain = reply.getInvite().getSender().getDomain();
|
||||||
log.info("Discovering HS for domain {}", domain);
|
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
|
// 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
|
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
|
// Expected body: https://matrix.to/#/!HUeDbmFUsWAhxHHvFG:matrix.org/$150469846739DCLWc:matrix.trancendances.fr
|
||||||
JsonObject obj = new JsonObject();
|
JsonObject obj = new JsonObject();
|
||||||
obj.addProperty("mxid", mxid);
|
obj.addProperty("mxid", mxid);
|
||||||
@@ -459,9 +455,11 @@ public class InvitationManager {
|
|||||||
Instant resolvedAt = Instant.now();
|
Instant resolvedAt = Instant.now();
|
||||||
boolean couldPublish = false;
|
boolean couldPublish = false;
|
||||||
boolean shouldArchive = true;
|
boolean shouldArchive = true;
|
||||||
|
try (CloseableHttpClient httpClient = HttpClients.custom().setSSLHostnameVerifier(new HomeserverVerifier(hsUrlOpt.getDomain()))
|
||||||
|
.build()) {
|
||||||
try {
|
try {
|
||||||
log.info("Posting onBind event to {}", req.getURI());
|
log.info("Posting onBind event to {}", req.getURI());
|
||||||
CloseableHttpResponse response = client.execute(req);
|
CloseableHttpResponse response = httpClient.execute(req);
|
||||||
int statusCode = response.getStatusLine().getStatusCode();
|
int statusCode = response.getStatusLine().getStatusCode();
|
||||||
log.info("Answer code: {}", statusCode);
|
log.info("Answer code: {}", statusCode);
|
||||||
if (statusCode >= 300 && statusCode != 403) {
|
if (statusCode >= 300 && statusCode != 403) {
|
||||||
@@ -490,6 +488,9 @@ public class InvitationManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Unable to create client to the " + hsUrlOpt.getUrl().toString(), e);
|
||||||
|
}
|
||||||
}).start();
|
}).start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -178,26 +178,26 @@ public class HomeserverFederationResolver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public URL resolve(String domain) {
|
public HomeserverTarget resolve(String domain) {
|
||||||
Optional<URL> s1 = resolveOverwrite(domain);
|
Optional<URL> s1 = resolveOverwrite(domain);
|
||||||
if (s1.isPresent()) {
|
if (s1.isPresent()) {
|
||||||
URL dest = s1.get();
|
URL dest = s1.get();
|
||||||
log.info("Resolution of {} via DNS overwrite to {}", domain, dest);
|
log.info("Resolution of {} via DNS overwrite to {}", domain, dest);
|
||||||
return dest;
|
return new HomeserverTarget(dest.getHost(), dest);
|
||||||
}
|
}
|
||||||
|
|
||||||
Optional<URL> s2 = resolveLiteral(domain);
|
Optional<URL> s2 = resolveLiteral(domain);
|
||||||
if (s2.isPresent()) {
|
if (s2.isPresent()) {
|
||||||
URL dest = s2.get();
|
URL dest = s2.get();
|
||||||
log.info("Resolution of {} as IP literal or IP/hostname with explicit port to {}", domain, dest);
|
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);
|
Optional<URL> s3 = resolveWellKnown(domain);
|
||||||
if (s3.isPresent()) {
|
if (s3.isPresent()) {
|
||||||
URL dest = s3.get();
|
URL dest = s3.get();
|
||||||
log.info("Resolution of {} via well-known to {}", domain, dest);
|
log.info("Resolution of {} via well-known to {}", domain, dest);
|
||||||
return dest;
|
return new HomeserverTarget(dest.getHost(), dest);
|
||||||
}
|
}
|
||||||
// The domain needs to be resolved
|
// The domain needs to be resolved
|
||||||
|
|
||||||
@@ -205,12 +205,30 @@ public class HomeserverFederationResolver {
|
|||||||
if (s4.isPresent()) {
|
if (s4.isPresent()) {
|
||||||
URL dest = s4.get();
|
URL dest = s4.get();
|
||||||
log.info("Resolution of {} via DNS SRV record to {}", domain, dest);
|
log.info("Resolution of {} via DNS SRV record to {}", domain, dest);
|
||||||
return dest;
|
return new HomeserverTarget(domain, dest);
|
||||||
}
|
}
|
||||||
|
|
||||||
URL dest = build(domain + ":" + getDefaultPort());
|
URL dest = build(domain + ":" + getDefaultPort());
|
||||||
log.info("Resolution of {} to {}", domain, dest);
|
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.SingleLookupRequest;
|
||||||
import io.kamax.mxisd.lookup.ThreePidValidation;
|
import io.kamax.mxisd.lookup.ThreePidValidation;
|
||||||
import io.kamax.mxisd.matrix.HomeserverFederationResolver;
|
import io.kamax.mxisd.matrix.HomeserverFederationResolver;
|
||||||
|
import io.kamax.mxisd.matrix.HomeserverVerifier;
|
||||||
import io.kamax.mxisd.notification.NotificationManager;
|
import io.kamax.mxisd.notification.NotificationManager;
|
||||||
import io.kamax.mxisd.storage.IStorage;
|
import io.kamax.mxisd.storage.IStorage;
|
||||||
import io.kamax.mxisd.storage.dao.IThreePidSessionDao;
|
import io.kamax.mxisd.storage.dao.IThreePidSessionDao;
|
||||||
@@ -53,12 +54,14 @@ import org.apache.commons.lang3.StringUtils;
|
|||||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||||
import org.apache.http.client.methods.HttpGet;
|
import org.apache.http.client.methods.HttpGet;
|
||||||
import org.apache.http.impl.client.CloseableHttpClient;
|
import org.apache.http.impl.client.CloseableHttpClient;
|
||||||
|
import org.apache.http.impl.client.HttpClients;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.MalformedURLException;
|
import java.net.MalformedURLException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
import java.net.URI;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
@@ -72,7 +75,6 @@ public class SessionManager {
|
|||||||
private IStorage storage;
|
private IStorage storage;
|
||||||
private NotificationManager notifMgr;
|
private NotificationManager notifMgr;
|
||||||
private HomeserverFederationResolver resolver;
|
private HomeserverFederationResolver resolver;
|
||||||
private CloseableHttpClient client;
|
|
||||||
private SignatureManager signatureManager;
|
private SignatureManager signatureManager;
|
||||||
|
|
||||||
public SessionManager(
|
public SessionManager(
|
||||||
@@ -80,14 +82,12 @@ public class SessionManager {
|
|||||||
IStorage storage,
|
IStorage storage,
|
||||||
NotificationManager notifMgr,
|
NotificationManager notifMgr,
|
||||||
HomeserverFederationResolver resolver,
|
HomeserverFederationResolver resolver,
|
||||||
CloseableHttpClient client,
|
|
||||||
SignatureManager signatureManager
|
SignatureManager signatureManager
|
||||||
) {
|
) {
|
||||||
this.cfg = cfg;
|
this.cfg = cfg;
|
||||||
this.storage = storage;
|
this.storage = storage;
|
||||||
this.notifMgr = notifMgr;
|
this.notifMgr = notifMgr;
|
||||||
this.resolver = resolver;
|
this.resolver = resolver;
|
||||||
this.client = client;
|
|
||||||
this.signatureManager = signatureManager;
|
this.signatureManager = signatureManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,8 +236,10 @@ public class SessionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.info("Unbinding of {} {} to {} is accepted", tpid.getMedium(), tpid.getAddress(), mxid.getId());
|
log.info("Unbinding of {} {} to {} is accepted", tpid.getMedium(), tpid.getAddress(), mxid.getId());
|
||||||
|
if (cfg.getSession().getPolicy().getUnbind().shouldNotify()) {
|
||||||
notifMgr.sendForUnbind(tpid);
|
notifMgr.sendForUnbind(tpid);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private String getDomain(String publicUrl) {
|
private String getDomain(String publicUrl) {
|
||||||
URL url;
|
URL url;
|
||||||
@@ -305,19 +307,23 @@ public class SessionManager {
|
|||||||
|
|
||||||
String canonical = MatrixJson.encodeCanonical(jsonObject);
|
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) {
|
private String removeQuotes(String origin) {
|
||||||
return origin.startsWith("\"") && origin.endsWith("\"") ? origin.substring(1, origin.length() - 1) : 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");
|
HttpGet request = new HttpGet(originUrl + "/_matrix/key/v2/server");
|
||||||
log.info("Get keys from the server {}", request.getURI());
|
log.info("Get keys from the server {}", request.getURI());
|
||||||
try (CloseableHttpResponse response = client.execute(request)) {
|
try (CloseableHttpClient httpClient = HttpClients.custom()
|
||||||
|
.setSSLHostnameVerifier(new HomeserverVerifier(homeserverTarget.getDomain())).build()) {
|
||||||
|
try (CloseableHttpResponse response = httpClient.execute(request)) {
|
||||||
int statusCode = response.getStatusLine().getStatusCode();
|
int statusCode = response.getStatusLine().getStatusCode();
|
||||||
log.info("Answer code: {}", statusCode);
|
log.info("Answer code: {}", statusCode);
|
||||||
if (statusCode == 200) {
|
if (statusCode == 200) {
|
||||||
@@ -330,6 +336,11 @@ public class SessionManager {
|
|||||||
log.error(message, e);
|
log.error(message, e);
|
||||||
throw new IllegalArgumentException(message);
|
throw new IllegalArgumentException(message);
|
||||||
}
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
String message = "Unable to get server keys: " + originUrl;
|
||||||
|
log.error(message, e);
|
||||||
|
throw new IllegalArgumentException(message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void verifyKey(String key, String signature, String canonical, CloseableHttpResponse response) throws IOException {
|
private void verifyKey(String key, String signature, String canonical, CloseableHttpResponse response) throws IOException {
|
||||||
|
@@ -294,6 +294,13 @@ public class OrmLiteSqlStorage implements IStorage {
|
|||||||
public void acceptTerm(String token, String url) {
|
public void acceptTerm(String token, String url) {
|
||||||
withCatcher(() -> {
|
withCatcher(() -> {
|
||||||
AccountDao account = findAccount(token).orElseThrow(InvalidCredentialsException::new);
|
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()));
|
int created = acceptedDao.create(new AcceptedDao(url, account.getUserId(), System.currentTimeMillis()));
|
||||||
if (created != 1) {
|
if (created != 1) {
|
||||||
throw new RuntimeException("Unexpected row count after DB action: " + created);
|
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
|
@Test
|
||||||
public void hostnameWithoutPort() {
|
public void hostnameWithoutPort() {
|
||||||
URL url = resolver.resolve("example.org");
|
URL url = resolver.resolve("example.org").getUrl();
|
||||||
assertEquals("https://example.org:8448", url.toString());
|
assertEquals("https://example.org:8448", url.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void hostnameWithPort() {
|
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());
|
assertEquals("https://example.org:443", url.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user