Compare commits
16 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
eb1326c56a | ||
|
10cdb4360e | ||
|
17ebc2a421 | ||
|
cbb9fced8d | ||
|
7509174611 | ||
|
51d9225dda | ||
|
6216113400 | ||
|
cb32441959 | ||
|
0ec4df2c06 | ||
|
86b880069b | ||
|
a97273fe77 | ||
|
f9daf4d58a | ||
|
9e4cabb69b | ||
|
0b81de3cd0 | ||
|
698a16ec17 | ||
|
619b70d860 |
140
docs/MSC2140_MSC2134.md
Normal file
140
docs/MSC2140_MSC2134.md
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
# 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: false
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
||||||
|
```
|
||||||
|
NOTE: Riot Web version 1.5.5 and below checks the v1 for backward compatibility.
|
||||||
|
|
||||||
|
NOTE: v2 disabled by default in order to preserve backward compatibility.
|
||||||
|
|
||||||
|
## Terms
|
||||||
|
|
||||||
|
###### Requires: No.
|
||||||
|
|
||||||
|
Administrator can omit terms configuration. In this case the terms checking will be disabled.
|
||||||
|
|
||||||
|
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_details'
|
||||||
|
- '/_matrix/identity/v2/lookup'
|
||||||
|
```
|
||||||
|
Where:
|
||||||
|
|
||||||
|
- `term_name` -- name of the terms.
|
||||||
|
- `version` -- the terms version.
|
||||||
|
- `lang` -- the term language.
|
||||||
|
- `name` -- the name of the term.
|
||||||
|
- `url` -- the url of the term. Might be any url (i.e. from another host) for a html page.
|
||||||
|
- `regexp` -- regexp patterns for API which should be available only after accepting the terms.
|
||||||
|
|
||||||
|
API will be checks for accepted terms only with authorization.
|
||||||
|
There are the next API:
|
||||||
|
- [`GET /_matrix/identity/v2/account`](https://matrix.org/docs/spec/identity_service/r0.3.0#get-matrix-identity-v2-account) - Gets information about what user owns the access token used in the request.
|
||||||
|
- [`POST /_matrix/identity/v2/account/logout`](https://matrix.org/docs/spec/identity_service/r0.3.0#post-matrix-identity-v2-account-logout) - Logs out the access token, preventing it from being used to authenticate future requests to the server.
|
||||||
|
- [`GET /_matrix/identity/v2/hash_details`](https://matrix.org/docs/spec/identity_service/r0.3.0#get-matrix-identity-v2-hash-details) - Gets parameters for hashing identifiers from the server. This can include any of the algorithms defined in this specification.
|
||||||
|
- [`POST /_matrix/identity/v2/lookup`](https://matrix.org/docs/spec/identity_service/r0.3.0#post-matrix-identity-v2-lookup) - Looks up the set of Matrix User IDs which have bound the 3PIDs given, if bindings are available. Note that the format of the addresses is defined later in this specification.
|
||||||
|
- [`POST /_matrix/identity/v2/validate/email/requestToken`](https://matrix.org/docs/spec/identity_service/r0.3.0#post-matrix-identity-v2-validate-email-requesttoken) - Create a session for validating an email address.
|
||||||
|
- [`POST /_matrix/identity/v2/validate/email/submitToken`](https://matrix.org/docs/spec/identity_service/r0.3.0#post-matrix-identity-v2-validate-email-submittoken) - Validate ownership of an email address.
|
||||||
|
- [`GET /_matrix/identity/v2/validate/email/submitToken`](https://matrix.org/docs/spec/identity_service/r0.3.0#get-matrix-identity-v2-validate-email-submittoken) - Validate ownership of an email address.
|
||||||
|
- [`POST /_matrix/identity/v2/validate/msisdn/requestToken`](https://matrix.org/docs/spec/identity_service/r0.3.0#post-matrix-identity-v2-validate-msisdn-requesttoken) - Create a session for validating a phone number.
|
||||||
|
- [`POST /_matrix/identity/v2/validate/msisdn/submitToken`](https://matrix.org/docs/spec/identity_service/r0.3.0#post-matrix-identity-v2-validate-msisdn-submittoken) - Validate ownership of a phone number.
|
||||||
|
- [`GET /_matrix/identity/v2/validate/msisdn/submitToken`](https://matrix.org/docs/spec/identity_service/r0.3.0#get-matrix-identity-v2-validate-msisdn-submittoken) - Validate ownership of a phone number.
|
||||||
|
- [`GET /_matrix/identity/v2/3pid/getValidated3pid`](https://matrix.org/docs/spec/identity_service/r0.3.0#get-matrix-identity-v2-3pid-getvalidated3pid) - Determines if a given 3pid has been validated by a user.
|
||||||
|
- [`POST /_matrix/identity/v2/3pid/bind`](https://matrix.org/docs/spec/identity_service/r0.3.0#post-matrix-identity-v2-3pid-bind) - Publish an association between a session and a Matrix user ID.
|
||||||
|
- [`POST /_matrix/identity/v2/3pid/unbind`](https://matrix.org/docs/spec/identity_service/r0.3.0#post-matrix-identity-v2-3pid-unbind) - Remove an association between a session and a Matrix user ID.
|
||||||
|
- [`POST /_matrix/identity/v2/store-invite`](https://matrix.org/docs/spec/identity_service/r0.3.0#post-matrix-identity-v2-store-invite) - Store pending invitations to a user's 3pid.
|
||||||
|
- [`POST /_matrix/identity/v2/sign-ed25519`](https://matrix.org/docs/spec/identity_service/r0.3.0#post-matrix-identity-v2-sign-ed25519) - Sign invitation details.
|
||||||
|
|
||||||
|
There is only one exception: [`POST /_matrix/identity/v2/terms`](https://matrix.org/docs/spec/identity_service/r0.3.0#post-matrix-identity-v2-terms) which uses for accepting the terms and requires the authorization.
|
||||||
|
|
||||||
|
## [Hash lookup](https://github.com/matrix-org/matrix-doc/blob/hs/hash-identity/proposals/2134-identity-hash-lookup.md)
|
||||||
|
|
||||||
|
Hashes and the pepper updates together according to the `rotationPolicy`.
|
||||||
|
|
||||||
|
###### Requires: No.
|
||||||
|
|
||||||
|
In case the `none` algorithms ma1sd will be lookup using the v1 bulk API.
|
||||||
|
|
||||||
|
```.yaml
|
||||||
|
hashing:
|
||||||
|
enabled: true # enable or disable the hash lookup MSC2140 (default is false)
|
||||||
|
pepperLength: 20 # length of the pepper value (default is 20)
|
||||||
|
rotationPolicy: per_requests # or `per_seconds` how often the hashes will be updating
|
||||||
|
hashStorageType: sql # or `in_memory` where the hashes will be stored
|
||||||
|
algorithms:
|
||||||
|
- none # the same as v1 bulk lookup
|
||||||
|
- sha256 # hash the 3PID and pepper.
|
||||||
|
delay: 2m # how often hashes will be updated if rotation policy = per_seconds (default is 10s)
|
||||||
|
requests: 10 # how many lookup requests will be performed before updating hashes if rotation policy = per_requests (default is 10)
|
||||||
|
```
|
||||||
|
|
||||||
|
When enabled and client requests the `none` algorithms then hash lookups works as v1 bulk lookup.
|
||||||
|
|
||||||
|
Delay specified in the format: `2d 4h 12m 34s` - this means 2 days 4 hours 12 minutes and 34 seconds. Zero units may be omitted. For example:
|
||||||
|
|
||||||
|
- 12s - 12 seconds
|
||||||
|
- 3m - 3 minutes
|
||||||
|
- 5m 6s - 5 minutes and 6 seconds
|
||||||
|
- 6h 3s - 6 hours and 3 seconds
|
||||||
|
|
||||||
|
|
||||||
|
Sha256 algorithm supports only sql, memory and exec 3PID providers.
|
||||||
|
For sql provider (i.e. for the `synapseSql`):
|
||||||
|
```.yaml
|
||||||
|
synapseSql:
|
||||||
|
lookup:
|
||||||
|
query: 'select user_id as mxid, medium, address from user_threepids' # query for retrive 3PIDs for hashes.
|
||||||
|
```
|
||||||
|
|
||||||
|
For general sql provider:
|
||||||
|
```.yaml
|
||||||
|
sql:
|
||||||
|
lookup:
|
||||||
|
query: 'select user as mxid, field1 as medium, field2 as address from some_table' # query for retrive 3PIDs for hashes.
|
||||||
|
```
|
||||||
|
|
||||||
|
Each query should return the `mxid`, `medium` and `address` fields.
|
||||||
|
|
||||||
|
|
||||||
|
For memory providers:
|
||||||
|
```.yaml
|
||||||
|
memory:
|
||||||
|
hashEnabled: true # enable the hash lookup (defaults is false)
|
||||||
|
```
|
||||||
|
|
||||||
|
For exec providers:
|
||||||
|
```.yaml
|
||||||
|
exec:
|
||||||
|
identity:
|
||||||
|
hashEnabled: true # enable the hash lookup (defaults is false)
|
||||||
|
```
|
||||||
|
|
||||||
|
NOTE: Federation requests work only with `none` algorithms.
|
||||||
|
|
@@ -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;
|
||||||
|
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,5 +1,5 @@
|
|||||||
#Thu Jul 04 22:47:59 MSK 2019
|
#Thu Dec 05 22:39:36 MSK 2019
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-6.0-all.zip
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStorePath=wrapper/dists
|
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,
|
||||||
|
@@ -21,6 +21,8 @@
|
|||||||
#
|
#
|
||||||
matrix:
|
matrix:
|
||||||
domain: ''
|
domain: ''
|
||||||
|
v1: true # deprecated
|
||||||
|
v2: false # MSC2140 API v2. Disabled by default in order to preserve backward compatibility.
|
||||||
|
|
||||||
|
|
||||||
################
|
################
|
||||||
@@ -109,3 +111,40 @@ threepid:
|
|||||||
|
|
||||||
# Password for the account
|
# Password for the account
|
||||||
password: "ThePassword"
|
password: "ThePassword"
|
||||||
|
|
||||||
|
|
||||||
|
#### MSC2134 (hash lookup)
|
||||||
|
|
||||||
|
#hashing:
|
||||||
|
# enabled: false # enable or disable the hash lookup MSC2140 (default is false)
|
||||||
|
# pepperLength: 20 # length of the pepper value (default is 20)
|
||||||
|
# rotationPolicy: per_requests # or `per_seconds` how often the hashes will be updating
|
||||||
|
# hashStorageType: sql # or `in_memory` where the hashes will be stored
|
||||||
|
# algorithms:
|
||||||
|
# - none # the same as v1 bulk lookup
|
||||||
|
# - sha256 # hash the 3PID and pepper.
|
||||||
|
# delay: 2m # how often hashes will be updated if rotation policy = per_seconds (default is 10s)
|
||||||
|
# requests: 10 # how many lookup requests will be performed before updating hashes if rotation policy = per_requests (default is 10)
|
||||||
|
|
||||||
|
### hash lookup for synapseSql provider.
|
||||||
|
# synapseSql:
|
||||||
|
# lookup:
|
||||||
|
# query: 'select user_id as mxid, medium, address from user_threepids' # query for retrive 3PIDs for hashes.
|
||||||
|
|
||||||
|
#### MSC2140 (Terms)
|
||||||
|
#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_details'
|
||||||
|
# - '/_matrix/identity/v2/lookup'
|
||||||
|
#
|
||||||
|
@@ -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,33 @@ public class HttpMxisd {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void accountEndpoints(RoutingHandler routingHandler) {
|
||||||
|
MatrixConfig matrixConfig = m.getConfig().getMatrix();
|
||||||
|
if (matrixConfig.isV2()) {
|
||||||
|
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()));
|
MatrixConfig matrixConfig = m.getConfig().getMatrix();
|
||||||
routingHandler
|
if (matrixConfig.isV2()) {
|
||||||
.post(AcceptTermsHandler.PATH, AuthorizationHandler.around(m.getAccMgr(), sane(new AcceptTermsHandler(m.getAccMgr()))));
|
routingHandler.get(GetTermsHandler.PATH, new GetTermsHandler(m.getConfig().getPolicy()));
|
||||||
|
routingHandler.post(AcceptTermsHandler.PATH, sane(new AcceptTermsHandler(m.getAccMgr())));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void hashEndpoints(RoutingHandler routingHandler) {
|
private void hashEndpoints(RoutingHandler routingHandler) {
|
||||||
routingHandler
|
MatrixConfig matrixConfig = m.getConfig().getMatrix();
|
||||||
.get(HashDetailsHandler.PATH, AuthorizationHandler.around(m.getAccMgr(), sane(new HashDetailsHandler(m.getHashManager()))));
|
if (matrixConfig.isV2()) {
|
||||||
routingHandler.post(HashLookupHandler.Path,
|
wrapWithTokenAndAuthorizationHandlers(routingHandler, Methods.GET, sane(new HashDetailsHandler(m.getHashManager())),
|
||||||
AuthorizationHandler.around(m.getAccMgr(), sane(new HashLookupHandler(m.getIdentity(), 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) {
|
private void addEndpoints(RoutingHandler routingHandler, HttpString method, boolean useAuthorization, ApiHandler... handlers) {
|
||||||
@@ -219,20 +230,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,14 +9,15 @@ 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.matrix.HomeserverVerifier;
|
||||||
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;
|
||||||
import org.apache.http.HttpStatus;
|
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;
|
||||||
@@ -33,15 +34,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 +55,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,24 +65,32 @@ 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 (CloseableHttpResponse response = httpClient.execute(getUserInfo)) {
|
try (CloseableHttpClient httpClient = HttpClients.custom()
|
||||||
int statusCode = response.getStatusLine().getStatusCode();
|
.setSSLHostnameVerifier(new HomeserverVerifier(homeserverTarget.getDomain())).build()) {
|
||||||
if (statusCode == HttpStatus.SC_OK) {
|
try (CloseableHttpResponse response = httpClient.execute(getUserInfo)) {
|
||||||
String content = EntityUtils.toString(response.getEntity());
|
int statusCode = response.getStatusLine().getStatusCode();
|
||||||
LOGGER.trace("Response: {}", content);
|
if (statusCode == HttpStatus.SC_OK) {
|
||||||
JsonObject body = GsonUtil.parseObj(content);
|
String content = EntityUtils.toString(response.getEntity());
|
||||||
userId = GsonUtil.getStringOrThrow(body, "sub");
|
LOGGER.trace("Response: {}", content);
|
||||||
} else {
|
JsonObject body = GsonUtil.parseObj(content);
|
||||||
LOGGER.error("Wrong response status: {}", statusCode);
|
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();
|
throw new InvalidCredentialsException();
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} 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();
|
throw new InvalidCredentialsException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -173,6 +173,10 @@ public class ExecIdentityStore extends ExecStore implements IThreePidProvider {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Iterable<ThreePidMapping> populateHashes() {
|
public Iterable<ThreePidMapping> populateHashes() {
|
||||||
|
if (!cfg.isHashLookup()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
Processor<List<ThreePidMapping>> p = new Processor<>();
|
Processor<List<ThreePidMapping>> p = new Processor<>();
|
||||||
p.withConfig(cfg.getLookup().getBulk());
|
p.withConfig(cfg.getLookup().getBulk());
|
||||||
|
|
||||||
|
@@ -174,6 +174,10 @@ public class MemoryIdentityStore implements AuthenticatorProvider, DirectoryProv
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Iterable<ThreePidMapping> populateHashes() {
|
public Iterable<ThreePidMapping> populateHashes() {
|
||||||
|
if (!cfg.isHashEnabled()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
return cfg.getIdentities().stream()
|
return cfg.getIdentities().stream()
|
||||||
.map(mic -> mic.getThreepids().stream().map(mtp -> new ThreePidMapping(mtp.getMedium(), mtp.getAddress(), mic.getUsername())))
|
.map(mic -> mic.getThreepids().stream().map(mtp -> new ThreePidMapping(mtp.getMedium(), mtp.getAddress(), mic.getUsername())))
|
||||||
.flatMap(s -> s).collect(
|
.flatMap(s -> s).collect(
|
||||||
|
@@ -0,0 +1,30 @@
|
|||||||
|
package io.kamax.mxisd.config;
|
||||||
|
|
||||||
|
public class DurationDeserializer {
|
||||||
|
|
||||||
|
public long deserialize(String argument) {
|
||||||
|
long duration = 0L;
|
||||||
|
for (String part : argument.split(" ")) {
|
||||||
|
String unit = part.substring(part.length() - 1);
|
||||||
|
long value = Long.parseLong(part.substring(0, part.length() - 1));
|
||||||
|
switch (unit) {
|
||||||
|
case "s":
|
||||||
|
duration += value;
|
||||||
|
break;
|
||||||
|
case "m":
|
||||||
|
duration += value * 60;
|
||||||
|
break;
|
||||||
|
case "h":
|
||||||
|
duration += value * 60 * 60;
|
||||||
|
break;
|
||||||
|
case "d":
|
||||||
|
duration += value * 60 * 60 * 24;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException(String.format("Unknown duration unit: %s", unit));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return duration;
|
||||||
|
}
|
||||||
|
}
|
@@ -309,6 +309,7 @@ public class ExecConfig {
|
|||||||
private Boolean enabled;
|
private Boolean enabled;
|
||||||
private int priority;
|
private int priority;
|
||||||
private Lookup lookup = new Lookup();
|
private Lookup lookup = new Lookup();
|
||||||
|
private boolean hashLookup = false;
|
||||||
|
|
||||||
public Boolean isEnabled() {
|
public Boolean isEnabled() {
|
||||||
return enabled;
|
return enabled;
|
||||||
@@ -334,6 +335,13 @@ public class ExecConfig {
|
|||||||
this.lookup = lookup;
|
this.lookup = lookup;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isHashLookup() {
|
||||||
|
return hashLookup;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHashLookup(boolean hashLookup) {
|
||||||
|
this.hashLookup = hashLookup;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Profile {
|
public static class Profile {
|
||||||
|
@@ -11,10 +11,12 @@ 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 String delay = "10s";
|
||||||
|
private transient long delayInSeconds = 10;
|
||||||
|
private int requests = 10;
|
||||||
private List<Algorithm> algorithms = new ArrayList<>();
|
private List<Algorithm> algorithms = new ArrayList<>();
|
||||||
|
|
||||||
public void build() {
|
public void build() {
|
||||||
@@ -23,27 +25,33 @@ 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 == getRotationPolicy()) {
|
||||||
LOGGER.info(" Rotation delay: {}", delay);
|
setDelayInSeconds(new DurationDeserializer().deserialize(getDelay()));
|
||||||
|
LOGGER.info(" Rotation delay: {}", getDelay());
|
||||||
|
LOGGER.info(" Rotation delay in seconds: {}", getDelayInSeconds());
|
||||||
}
|
}
|
||||||
|
if (RotationPolicyEnum.per_requests == getRotationPolicy()) {
|
||||||
|
LOGGER.info(" Rotation after requests: {}", getRequests());
|
||||||
|
}
|
||||||
|
LOGGER.info(" Algorithms: {}", getAlgorithms());
|
||||||
} 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() {
|
||||||
@@ -78,14 +86,30 @@ public class HashingConfig {
|
|||||||
this.hashStorageType = hashStorageType;
|
this.hashStorageType = hashStorageType;
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getDelay() {
|
public String getDelay() {
|
||||||
return delay;
|
return delay;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDelay(long delay) {
|
public void setDelay(String delay) {
|
||||||
this.delay = delay;
|
this.delay = delay;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long getDelayInSeconds() {
|
||||||
|
return delayInSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDelayInSeconds(long delayInSeconds) {
|
||||||
|
this.delayInSeconds = delayInSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getRequests() {
|
||||||
|
return requests;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRequests(int requests) {
|
||||||
|
this.requests = requests;
|
||||||
|
}
|
||||||
|
|
||||||
public List<Algorithm> getAlgorithms() {
|
public List<Algorithm> getAlgorithms() {
|
||||||
return algorithms;
|
return algorithms;
|
||||||
}
|
}
|
||||||
|
@@ -64,7 +64,7 @@ public class MatrixConfig {
|
|||||||
private String domain;
|
private String domain;
|
||||||
private Identity identity = new Identity();
|
private Identity identity = new Identity();
|
||||||
private boolean v1 = true;
|
private boolean v1 = true;
|
||||||
private boolean v2 = true;
|
private boolean v2 = false;
|
||||||
|
|
||||||
public String getDomain() {
|
public String getDomain() {
|
||||||
return domain;
|
return domain;
|
||||||
|
@@ -100,10 +100,12 @@ 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");
|
||||||
for (Map.Entry<String, TermObject> termItem : policyObject.getTerms().entrySet()) {
|
if (policyObject.getTerms() != null) {
|
||||||
sb.append(" - lang: ").append(termItem.getKey()).append("\n");
|
for (Map.Entry<String, TermObject> termItem : policyObject.getTerms().entrySet()) {
|
||||||
sb.append(" name: ").append(termItem.getValue().getName()).append("\n");
|
sb.append(" - lang: ").append(termItem.getKey()).append("\n");
|
||||||
sb.append(" url: ").append(termItem.getValue().getUrl()).append("\n");
|
sb.append(" name: ").append(termItem.getValue().getName()).append("\n");
|
||||||
|
sb.append(" url: ").append(termItem.getValue().getUrl()).append("\n");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
LOGGER.info(sb.toString());
|
LOGGER.info(sb.toString());
|
||||||
}
|
}
|
||||||
|
@@ -27,6 +27,7 @@ public class MemoryStoreConfig {
|
|||||||
|
|
||||||
private boolean enabled;
|
private boolean enabled;
|
||||||
private List<MemoryIdentityConfig> identities = new ArrayList<>();
|
private List<MemoryIdentityConfig> identities = new ArrayList<>();
|
||||||
|
private boolean hashEnabled = false;
|
||||||
|
|
||||||
public boolean isEnabled() {
|
public boolean isEnabled() {
|
||||||
return enabled;
|
return enabled;
|
||||||
@@ -44,6 +45,14 @@ public class MemoryStoreConfig {
|
|||||||
this.identities = identities;
|
this.identities = identities;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isHashEnabled() {
|
||||||
|
return hashEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHashEnabled(boolean hashEnabled) {
|
||||||
|
this.hashEnabled = hashEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
public void build() {
|
public void build() {
|
||||||
// no-op
|
// no-op
|
||||||
}
|
}
|
||||||
|
@@ -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,20 +1,25 @@
|
|||||||
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.Base64;
|
||||||
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 final Base64.Encoder base64 = Base64.getUrlEncoder().withoutPadding();
|
||||||
private String pepper;
|
private String pepper;
|
||||||
|
|
||||||
public HashEngine(List<? extends IThreePidProvider> providers, HashStorage hashStorage, HashingConfig config) {
|
public HashEngine(List<? extends IThreePidProvider> providers, HashStorage hashStorage, HashingConfig config) {
|
||||||
@@ -24,15 +29,21 @@ 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) {
|
||||||
for (ThreePidMapping pidMapping : provider.populateHashes()) {
|
try {
|
||||||
hashStorage.add(pidMapping, hash(pidMapping));
|
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() {
|
public String getPepper() {
|
||||||
@@ -42,10 +53,10 @@ public class HashEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected String hash(ThreePidMapping pidMapping) {
|
protected String hash(ThreePidMapping pidMapping) {
|
||||||
return sha256.hash(pidMapping.getMedium() + " " + pidMapping.getValue() + " " + getPepper());
|
return base64.encodeToString(DigestUtils.sha256(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,11 +57,11 @@ 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(config.getRequests());
|
||||||
break;
|
break;
|
||||||
case PER_SECONDS:
|
case per_seconds:
|
||||||
this.rotationStrategy = new TimeBasedRotation(config.getDelay());
|
this.rotationStrategy = new TimeBasedRotation(config.getDelayInSeconds());
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new IllegalArgumentException("Unknown rotation type: " + config.getHashStorageType());
|
throw new IllegalArgumentException("Unknown rotation type: " + config.getHashStorageType());
|
||||||
|
@@ -8,10 +8,16 @@ public class RotationPerRequests implements HashRotationStrategy {
|
|||||||
|
|
||||||
private HashEngine hashEngine;
|
private HashEngine hashEngine;
|
||||||
private final AtomicInteger counter = new AtomicInteger(0);
|
private final AtomicInteger counter = new AtomicInteger(0);
|
||||||
|
private final int barrier;
|
||||||
|
|
||||||
|
public RotationPerRequests(int barrier) {
|
||||||
|
this.barrier = barrier;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void register(HashEngine hashEngine) {
|
public void register(HashEngine hashEngine) {
|
||||||
this.hashEngine = hashEngine;
|
this.hashEngine = hashEngine;
|
||||||
|
trigger();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -22,7 +28,7 @@ public class RotationPerRequests implements HashRotationStrategy {
|
|||||||
@Override
|
@Override
|
||||||
public synchronized void newRequest() {
|
public synchronized void newRequest() {
|
||||||
int newValue = counter.incrementAndGet();
|
int newValue = counter.incrementAndGet();
|
||||||
if (newValue >= 10) {
|
if (newValue >= barrier) {
|
||||||
counter.set(0);
|
counter.set(0);
|
||||||
trigger();
|
trigger();
|
||||||
}
|
}
|
||||||
|
@@ -12,6 +12,7 @@ public class SqlHashStorage implements HashStorage {
|
|||||||
|
|
||||||
public SqlHashStorage(IStorage storage) {
|
public SqlHashStorage(IStorage storage) {
|
||||||
this.storage = storage;
|
this.storage = storage;
|
||||||
|
Runtime.getRuntime().addShutdownHook(new Thread(storage::clearHashes));
|
||||||
}
|
}
|
||||||
|
|
||||||
@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(System.currentTimeMillis() + openIdToken.getExpiresIn()));
|
||||||
}
|
}
|
||||||
|
|
||||||
String token = accountManager.register(openIdToken);
|
String token = accountManager.register(openIdToken);
|
||||||
|
@@ -31,6 +31,8 @@ import io.kamax.mxisd.http.undertow.handler.ApiHandler;
|
|||||||
import io.kamax.mxisd.http.undertow.handler.identity.share.LookupHandler;
|
import io.kamax.mxisd.http.undertow.handler.identity.share.LookupHandler;
|
||||||
import io.kamax.mxisd.lookup.BulkLookupRequest;
|
import io.kamax.mxisd.lookup.BulkLookupRequest;
|
||||||
import io.kamax.mxisd.lookup.HashLookupRequest;
|
import io.kamax.mxisd.lookup.HashLookupRequest;
|
||||||
|
import io.kamax.mxisd.lookup.SingleLookupReply;
|
||||||
|
import io.kamax.mxisd.lookup.SingleLookupRequest;
|
||||||
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.undertow.server.HttpServerExchange;
|
import io.undertow.server.HttpServerExchange;
|
||||||
@@ -40,6 +42,7 @@ import org.slf4j.LoggerFactory;
|
|||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
public class HashLookupHandler extends LookupHandler implements ApiHandler {
|
public class HashLookupHandler extends LookupHandler implements ApiHandler {
|
||||||
|
|
||||||
@@ -60,6 +63,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,17 +88,29 @@ 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ClientHashLookupAnswer answer = null;
|
||||||
|
if (input.getAddresses() != null && input.getAddresses().size() > 0) {
|
||||||
|
if (input.getAddresses().size() == 1) {
|
||||||
|
answer = noneSingleLookup(request, input);
|
||||||
|
} else {
|
||||||
|
answer = noneBulkLookup(request, input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
respondJson(exchange, answer != null ? answer : new ClientHashLookupAnswer());
|
||||||
|
}
|
||||||
|
|
||||||
|
private ClientHashLookupAnswer noneBulkLookup(HashLookupRequest request, ClientHashLookupRequest input) throws Exception {
|
||||||
BulkLookupRequest bulkLookupRequest = new BulkLookupRequest();
|
BulkLookupRequest bulkLookupRequest = new BulkLookupRequest();
|
||||||
List<ThreePidMapping> mappings = new ArrayList<>();
|
List<ThreePidMapping> mappings = new ArrayList<>();
|
||||||
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);
|
||||||
@@ -106,19 +122,42 @@ public class HashLookupHandler extends LookupHandler implements ApiHandler {
|
|||||||
}
|
}
|
||||||
log.info("Finished bulk lookup request from {}", request.getRequester());
|
log.info("Finished bulk lookup request from {}", request.getRequester());
|
||||||
|
|
||||||
respondJson(exchange, answer);
|
return answer;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ClientHashLookupAnswer noneSingleLookup(HashLookupRequest request, ClientHashLookupRequest input) {
|
||||||
|
SingleLookupRequest singleLookupRequest = new SingleLookupRequest();
|
||||||
|
String address = input.getAddresses().get(0);
|
||||||
|
String[] parts = address.split(" ");
|
||||||
|
singleLookupRequest.setThreePid(parts[0]);
|
||||||
|
singleLookupRequest.setType(parts[1]);
|
||||||
|
|
||||||
|
ClientHashLookupAnswer answer = new ClientHashLookupAnswer();
|
||||||
|
|
||||||
|
Optional<SingleLookupReply> singleLookupReply = strategy.find(singleLookupRequest);
|
||||||
|
if (singleLookupReply.isPresent()) {
|
||||||
|
SingleLookupReply reply = singleLookupReply.get();
|
||||||
|
answer.getMappings().put(address, reply.getMxid().toString());
|
||||||
|
}
|
||||||
|
log.info("Finished single lookup request from {}", request.getRequester());
|
||||||
|
|
||||||
|
return answer;
|
||||||
}
|
}
|
||||||
|
|
||||||
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();
|
||||||
for (Pair<String, ThreePidMapping> pair : hashManager.getHashStorage().find(request.getHashes())) {
|
if (request.getHashes() != null && !request.getHashes().isEmpty()) {
|
||||||
answer.getMappings().put(pair.getKey(), pair.getValue().getMxid());
|
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);
|
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,36 +455,41 @@ public class InvitationManager {
|
|||||||
Instant resolvedAt = Instant.now();
|
Instant resolvedAt = Instant.now();
|
||||||
boolean couldPublish = false;
|
boolean couldPublish = false;
|
||||||
boolean shouldArchive = true;
|
boolean shouldArchive = true;
|
||||||
try {
|
try (CloseableHttpClient httpClient = HttpClients.custom().setSSLHostnameVerifier(new HomeserverVerifier(hsUrlOpt.getDomain()))
|
||||||
log.info("Posting onBind event to {}", req.getURI());
|
.build()) {
|
||||||
CloseableHttpResponse response = client.execute(req);
|
try {
|
||||||
int statusCode = response.getStatusLine().getStatusCode();
|
log.info("Posting onBind event to {}", req.getURI());
|
||||||
log.info("Answer code: {}", statusCode);
|
CloseableHttpResponse response = httpClient.execute(req);
|
||||||
if (statusCode >= 300 && statusCode != 403) {
|
int statusCode = response.getStatusLine().getStatusCode();
|
||||||
log.info("Answer body: {}", IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8));
|
log.info("Answer code: {}", statusCode);
|
||||||
log.warn("HS returned an error.");
|
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) {
|
if (shouldArchive) {
|
||||||
log.info("Invite can be found in historical storage for manual re-processing");
|
synchronized (this) {
|
||||||
}
|
storage.insertHistoricalInvite(reply, mxid, resolvedAt, couldPublish);
|
||||||
} else {
|
removeInvite(reply);
|
||||||
couldPublish = true;
|
log.info("Moved invite {} to historical table", reply.getId());
|
||||||
if (statusCode == 403) {
|
}
|
||||||
log.info("Invite is obsolete or no longer under our control");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
response.close();
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.warn("Unable to tell HS {} about invite being mapped", domain, e);
|
log.error("Unable to create client to the " + hsUrlOpt.getUrl().toString(), e);
|
||||||
} finally {
|
|
||||||
if (shouldArchive) {
|
|
||||||
synchronized (this) {
|
|
||||||
storage.insertHistoricalInvite(reply, mxid, resolvedAt, couldPublish);
|
|
||||||
removeInvite(reply);
|
|
||||||
log.info("Moved invite {} to historical table", reply.getId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}).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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
86
src/main/java/io/kamax/mxisd/matrix/HomeserverVerifier.java
Normal file
86
src/main/java/io/kamax/mxisd/matrix/HomeserverVerifier.java
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
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("*.")) {
|
||||||
|
String subjectNameWithoutMask = altSubjectName.substring(1); // remove wildcard
|
||||||
|
return matrixHostname.toLowerCase().endsWith(subjectNameWithoutMask.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,6 +54,7 @@ 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;
|
||||||
|
|
||||||
@@ -73,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(
|
||||||
@@ -81,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,25 +307,34 @@ 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()
|
||||||
int statusCode = response.getStatusLine().getStatusCode();
|
.setSSLHostnameVerifier(new HomeserverVerifier(homeserverTarget.getDomain())).build()) {
|
||||||
log.info("Answer code: {}", statusCode);
|
try (CloseableHttpResponse response = httpClient.execute(request)) {
|
||||||
if (statusCode == 200) {
|
int statusCode = response.getStatusLine().getStatusCode();
|
||||||
verifyKey(key, signature, canonical, response);
|
log.info("Answer code: {}", statusCode);
|
||||||
} else {
|
if (statusCode == 200) {
|
||||||
throw new RemoteHomeServerException("Unable to fetch server keys.");
|
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) {
|
} catch (IOException e) {
|
||||||
String message = "Unable to get server keys: " + originUrl;
|
String message = "Unable to get server keys: " + originUrl;
|
||||||
|
@@ -39,6 +39,7 @@ import io.kamax.mxisd.storage.IStorage;
|
|||||||
import io.kamax.mxisd.storage.dao.IThreePidSessionDao;
|
import io.kamax.mxisd.storage.dao.IThreePidSessionDao;
|
||||||
import io.kamax.mxisd.storage.ormlite.dao.ASTransactionDao;
|
import io.kamax.mxisd.storage.ormlite.dao.ASTransactionDao;
|
||||||
import io.kamax.mxisd.storage.ormlite.dao.AccountDao;
|
import io.kamax.mxisd.storage.ormlite.dao.AccountDao;
|
||||||
|
import io.kamax.mxisd.storage.ormlite.dao.ChangelogDao;
|
||||||
import io.kamax.mxisd.storage.ormlite.dao.HashDao;
|
import io.kamax.mxisd.storage.ormlite.dao.HashDao;
|
||||||
import io.kamax.mxisd.storage.ormlite.dao.HistoricalThreePidInviteIO;
|
import io.kamax.mxisd.storage.ormlite.dao.HistoricalThreePidInviteIO;
|
||||||
import io.kamax.mxisd.storage.ormlite.dao.AcceptedDao;
|
import io.kamax.mxisd.storage.ormlite.dao.AcceptedDao;
|
||||||
@@ -46,12 +47,15 @@ import io.kamax.mxisd.storage.ormlite.dao.ThreePidInviteIO;
|
|||||||
import io.kamax.mxisd.storage.ormlite.dao.ThreePidSessionDao;
|
import io.kamax.mxisd.storage.ormlite.dao.ThreePidSessionDao;
|
||||||
import org.apache.commons.lang.StringUtils;
|
import org.apache.commons.lang.StringUtils;
|
||||||
import org.apache.commons.lang3.tuple.Pair;
|
import org.apache.commons.lang3.tuple.Pair;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@@ -59,6 +63,8 @@ import java.util.stream.Collectors;
|
|||||||
|
|
||||||
public class OrmLiteSqlStorage implements IStorage {
|
public class OrmLiteSqlStorage implements IStorage {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(OrmLiteSqlStorage.class);
|
||||||
|
|
||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
private interface Getter<T> {
|
private interface Getter<T> {
|
||||||
|
|
||||||
@@ -73,13 +79,18 @@ public class OrmLiteSqlStorage implements IStorage {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class Migrations {
|
||||||
|
public static final String FIX_ACCEPTED_DAO = "2019_12_09__2254__fix_accepted_dao";
|
||||||
|
}
|
||||||
|
|
||||||
private Dao<ThreePidInviteIO, String> invDao;
|
private Dao<ThreePidInviteIO, String> invDao;
|
||||||
private Dao<HistoricalThreePidInviteIO, String> expInvDao;
|
private Dao<HistoricalThreePidInviteIO, String> expInvDao;
|
||||||
private Dao<ThreePidSessionDao, String> sessionDao;
|
private Dao<ThreePidSessionDao, String> sessionDao;
|
||||||
private Dao<ASTransactionDao, String> asTxnDao;
|
private Dao<ASTransactionDao, String> asTxnDao;
|
||||||
private Dao<AccountDao, String> accountDao;
|
private Dao<AccountDao, String> accountDao;
|
||||||
private Dao<AcceptedDao, String> acceptedDao;
|
private Dao<AcceptedDao, Long> acceptedDao;
|
||||||
private Dao<HashDao, String> hashDao;
|
private Dao<HashDao, String> hashDao;
|
||||||
|
private Dao<ChangelogDao, String> changelogDao;
|
||||||
|
|
||||||
public OrmLiteSqlStorage(MxisdConfig cfg) {
|
public OrmLiteSqlStorage(MxisdConfig cfg) {
|
||||||
this(cfg.getStorage().getBackend(), cfg.getStorage().getProvider().getSqlite().getDatabase());
|
this(cfg.getStorage().getBackend(), cfg.getStorage().getProvider().getSqlite().getDatabase());
|
||||||
@@ -96,6 +107,7 @@ public class OrmLiteSqlStorage implements IStorage {
|
|||||||
|
|
||||||
withCatcher(() -> {
|
withCatcher(() -> {
|
||||||
ConnectionSource connPool = new JdbcConnectionSource("jdbc:" + backend + ":" + path);
|
ConnectionSource connPool = new JdbcConnectionSource("jdbc:" + backend + ":" + path);
|
||||||
|
changelogDao = createDaoAndTable(connPool, ChangelogDao.class);
|
||||||
invDao = createDaoAndTable(connPool, ThreePidInviteIO.class);
|
invDao = createDaoAndTable(connPool, ThreePidInviteIO.class);
|
||||||
expInvDao = createDaoAndTable(connPool, HistoricalThreePidInviteIO.class);
|
expInvDao = createDaoAndTable(connPool, HistoricalThreePidInviteIO.class);
|
||||||
sessionDao = createDaoAndTable(connPool, ThreePidSessionDao.class);
|
sessionDao = createDaoAndTable(connPool, ThreePidSessionDao.class);
|
||||||
@@ -103,10 +115,26 @@ public class OrmLiteSqlStorage implements IStorage {
|
|||||||
accountDao = createDaoAndTable(connPool, AccountDao.class);
|
accountDao = createDaoAndTable(connPool, AccountDao.class);
|
||||||
acceptedDao = createDaoAndTable(connPool, AcceptedDao.class);
|
acceptedDao = createDaoAndTable(connPool, AcceptedDao.class);
|
||||||
hashDao = createDaoAndTable(connPool, HashDao.class);
|
hashDao = createDaoAndTable(connPool, HashDao.class);
|
||||||
|
runMigration(connPool);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void runMigration(ConnectionSource connPol) throws SQLException {
|
||||||
|
ChangelogDao fixAcceptedDao = changelogDao.queryForId(Migrations.FIX_ACCEPTED_DAO);
|
||||||
|
if (fixAcceptedDao == null) {
|
||||||
|
fixAcceptedDao(connPol);
|
||||||
|
changelogDao.create(new ChangelogDao(Migrations.FIX_ACCEPTED_DAO, new Date(), "Recreate the accepted table."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fixAcceptedDao(ConnectionSource connPool) throws SQLException {
|
||||||
|
LOGGER.info("Migration: {}", Migrations.FIX_ACCEPTED_DAO);
|
||||||
|
TableUtils.dropTable(acceptedDao, true);
|
||||||
|
TableUtils.createTableIfNotExists(connPool, AcceptedDao.class);
|
||||||
|
}
|
||||||
|
|
||||||
private <V, K> Dao<V, K> createDaoAndTable(ConnectionSource connPool, Class<V> c) throws SQLException {
|
private <V, K> Dao<V, K> createDaoAndTable(ConnectionSource connPool, Class<V> c) throws SQLException {
|
||||||
|
LOGGER.info("Create the dao: {}", c.getSimpleName());
|
||||||
Dao<V, K> dao = DaoManager.createDao(connPool, c);
|
Dao<V, K> dao = DaoManager.createDao(connPool, c);
|
||||||
TableUtils.createTableIfNotExists(connPool, c);
|
TableUtils.createTableIfNotExists(connPool, c);
|
||||||
return dao;
|
return dao;
|
||||||
@@ -294,6 +322,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);
|
||||||
|
@@ -26,7 +26,10 @@ import com.j256.ormlite.table.DatabaseTable;
|
|||||||
@DatabaseTable(tableName = "accepted")
|
@DatabaseTable(tableName = "accepted")
|
||||||
public class AcceptedDao {
|
public class AcceptedDao {
|
||||||
|
|
||||||
@DatabaseField(canBeNull = false, id = true)
|
@DatabaseField(generatedId = true)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@DatabaseField(canBeNull = false)
|
||||||
private String url;
|
private String url;
|
||||||
|
|
||||||
@DatabaseField(canBeNull = false)
|
@DatabaseField(canBeNull = false)
|
||||||
@@ -45,6 +48,14 @@ public class AcceptedDao {
|
|||||||
this.acceptedAt = acceptedAt;
|
this.acceptedAt = acceptedAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
public String getUrl() {
|
public String getUrl() {
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,52 @@
|
|||||||
|
package io.kamax.mxisd.storage.ormlite.dao;
|
||||||
|
|
||||||
|
import com.j256.ormlite.field.DatabaseField;
|
||||||
|
import com.j256.ormlite.table.DatabaseTable;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
@DatabaseTable(tableName = "changelog")
|
||||||
|
public class ChangelogDao {
|
||||||
|
|
||||||
|
@DatabaseField(id = true)
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
@DatabaseField
|
||||||
|
private Date createdAt;
|
||||||
|
|
||||||
|
@DatabaseField
|
||||||
|
private String comment;
|
||||||
|
|
||||||
|
public ChangelogDao() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChangelogDao(String id, Date createdAt, String comment) {
|
||||||
|
this.id = id;
|
||||||
|
this.createdAt = createdAt;
|
||||||
|
this.comment = comment;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(String id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Date getCreatedAt() {
|
||||||
|
return createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreatedAt(Date createdAt) {
|
||||||
|
this.createdAt = createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getComment() {
|
||||||
|
return comment;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setComment(String comment) {
|
||||||
|
this.comment = comment;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,20 @@
|
|||||||
|
package io.kamax.mxisd.test.config;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
import io.kamax.mxisd.config.DurationDeserializer;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
public class DurationDeserializerTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void durationLoadTest() {
|
||||||
|
DurationDeserializer deserializer = new DurationDeserializer();
|
||||||
|
|
||||||
|
assertEquals(4, deserializer.deserialize("4s"));
|
||||||
|
assertEquals((60 * 60) + 4, deserializer.deserialize("1h 4s"));
|
||||||
|
assertEquals((2 * 60) + 4, deserializer.deserialize("2m 4s"));
|
||||||
|
assertEquals((2 * 60 * 60) + (7 * 60) + 4, deserializer.deserialize("2h 7m 4s"));
|
||||||
|
assertEquals((60 * 60 * 24) + (2 * 60 * 60) + (7 * 60) + 4, deserializer.deserialize("1d 2h 7m 4s"));
|
||||||
|
}
|
||||||
|
}
|
18
src/test/java/io/kamax/mxisd/test/hash/HashEngineTest.java
Normal file
18
src/test/java/io/kamax/mxisd/test/hash/HashEngineTest.java
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package io.kamax.mxisd.test.hash;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
import org.apache.commons.codec.digest.DigestUtils;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.Base64;
|
||||||
|
|
||||||
|
public class HashEngineTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void sha256test() {
|
||||||
|
Base64.Encoder encoder = Base64.getUrlEncoder().withoutPadding();
|
||||||
|
assertEquals("rujYzy1w0JxulN_rVlErGUmkdXT5znL0sjSF_IWreko",
|
||||||
|
encoder.encodeToString(DigestUtils.sha256("user@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