Compare commits

...

38 Commits
2.1.2 ... 2.2.1

Author SHA1 Message Date
Anatoly Sablin
eb1326c56a Add unique id for the accepted table.
Add a little more logs.
2019-12-10 22:29:00 +03:00
Anatoly Sablin
10cdb4360e Fix homeserver verification with wildcards certificates.
Disable v2 by default.
Add migration to fix the accepted table (due to sqlite unable to change constraint, drop table and create again).
Fix displaying the expiration period of the new token.
Remove duplicated code.
Use v1 single lookup when receive the request with `none` algorithm and the only one argument.
Hide v2 endpoint if v2 API disabled.
2019-12-10 00:10:13 +03:00
Anatoly Sablin
17ebc2a421 Fix hash generation. 2019-12-06 23:15:00 +03:00
Anatoly Sablin
cbb9fced8d Clarify the documentation. Add the hash config to the example config. Uses duration in the delay field instead of the seconds. 2019-12-05 23:27:13 +03:00
Anatoly Sablin
7509174611 Add documentation. Add options to enable/disable the hash providers. Add the option for setup barrier for rotation per requests strategy. 2019-12-02 23:23:17 +03:00
Anatoly Sablin
51d9225dda Don't wrap the AcceptTermsHandler with terms checking. Clear the sql hash storage on shutdown. 2019-12-02 22:31:36 +03:00
Anatoly Sablin
6216113400 FIx terms. 2019-11-29 23:38:52 +03:00
Anatoly Sablin
cb32441959 Fix sha256 hashing. Fix v2 lookup. 2019-11-29 00:26:08 +03:00
Anatoly Sablin
0ec4df2c06 Fix bug with token expiration. Increase the default length of the pepper. Update hashes on startup with RotationPerRequest strategy. Don't check for existing pepper on the none hash algorithm. 2019-11-28 00:28:11 +03:00
Anatoly Sablin
86b880069b Wrap with the CheckTermsHandler handlers only with authorization. 2019-11-27 22:55:34 +03:00
Anatoly Sablin
a97273fe77 Wrap with the CheckTermsHandler is necessary. 2019-11-25 23:35:56 +03:00
Anatoly Sablin
f9daf4d58a Make configuration enums in lowercase. Wrap create hashes by try-catch. Add initial part of the documentation. 2019-11-15 23:39:45 +03:00
Anatoly Sablin
9e4cabb69b Fix the token expiration period. 2019-11-15 22:50:08 +03:00
Anatoly Sablin
0b81de3cd0 Make the federation homeserver resolve more accurate (on resolve via DNS record check that the certificate present for the original host). 2019-11-13 23:08:34 +03:00
Anatoly Sablin
698a16ec17 Fix matrix server hostname verification. 2019-11-11 23:48:49 +03:00
Anatoly Sablin
619b70d860 Bump gradle to 6.0. 2019-11-11 23:48:49 +03:00
Anatoly Sablin
494c9e3941 Merge branch 'MSC2140'
# Conflicts:
#	src/main/java/io/kamax/mxisd/session/SessionManager.java
2019-11-07 22:29:25 +03:00
Anatoly Sablin
0786a6520f Bump gradle version. 2019-11-07 00:16:18 +03:00
Anatoly Sablin
430136c391 Bump dependency verions. 2019-11-06 23:26:56 +03:00
Anatoly Sablin
eda4404335 MSC2140 Add populating hashes via exec identity store. 2019-11-06 23:16:27 +03:00
Anatoly Sablin
c52034b18a MSC2140 Add populating hashes via sql and memory stores. 2019-11-06 23:07:42 +03:00
Anatoly Sablin
8d346037b7 MSC2140 Add hash configuration. 2019-11-06 00:20:39 +03:00
Anatoly Sablin
14ad4435bc MSC2140 Add SQL storage for hashes and the time-based rotation policy. 2019-11-05 23:18:11 +03:00
Anatoly Sablin
8ba8756871 Fix account registration. 2019-10-21 23:48:47 +03:00
Anatoly Sablin
43fe8b1aec Add the hash lookup handler. 2019-10-18 22:52:13 +03:00
Anatoly Sablin
a0270c7d01 Add NoOp configuration. Split classes into packages. 2019-10-16 23:07:14 +03:00
Anatoly Sablin
703044d06a Add initial Hash configuration. Add the HashDetailsHandler. 2019-10-15 23:38:32 +03:00
Anatoly Sablin
add6ed8fd9 Add the TOS API. 2019-10-09 23:12:23 +03:00
Anatoly Sablin
baed894ff8 Update policy configuration. Add Handler to check that user accepts terms. 2019-10-08 00:13:40 +03:00
Anatoly Sablin
14e095a147 Add policy configuration. 2019-10-04 00:03:51 +03:00
Anatoly Sablin
bc8795e940 Add authorization handler. 2019-10-01 23:52:01 +03:00
Anatoly Sablin
5521c0c338 Add account handlers. 2019-09-30 23:53:38 +03:00
Anatoly Sablin
614b3440e2 Registration API. Add DAO, Manager. 2019-09-30 23:16:58 +03:00
Anatoly Sablin
d0fd9fb9b0 Get domain from public url. 2019-09-17 23:17:30 +03:00
Anatoly Sablin
1232e9ce79 MSC2140 MSC2134 Remove the unused path. 2019-09-01 22:38:59 +03:00
Anatoly Sablin
a47a983c10 MSC2140 MSC2134 Refactoring. Move common classes to the share package. 2019-09-01 22:33:03 +03:00
Anatoly Sablin
fbb0d7c7ba MSC2140 Mirroring V1 to V2. 2019-08-31 23:35:58 +03:00
Anatoly Sablin
f1dd309551 MSC2140 Add option to enable/disable v1 and v2 api. 2019-08-31 23:09:20 +03:00
90 changed files with 3138 additions and 296 deletions

View File

@@ -78,7 +78,7 @@ buildscript {
dependencies { dependencies {
classpath 'com.github.jengelman.gradle.plugins:shadow:5.1.0' classpath 'com.github.jengelman.gradle.plugins:shadow:5.1.0'
classpath 'com.github.ben-manes:gradle-versions-plugin:0.21.0' classpath 'com.github.ben-manes:gradle-versions-plugin:0.27.0'
} }
} }
@@ -94,12 +94,12 @@ dependencies {
compile 'commons-io:commons-io:2.6' compile 'commons-io:commons-io:2.6'
// Config management // Config management
compile 'org.yaml:snakeyaml:1.24' compile 'org.yaml:snakeyaml:1.25'
// Dependencies from old Matrix-java-sdk // Dependencies from old Matrix-java-sdk
compile 'org.apache.commons:commons-lang3:3.9' compile 'org.apache.commons:commons-lang3:3.9'
compile 'com.squareup.okhttp3:okhttp:4.0.1' compile 'com.squareup.okhttp3:okhttp:4.2.2'
compile 'commons-codec:commons-codec:1.12' compile 'commons-codec:commons-codec:1.13'
// ORMLite // ORMLite
compile 'com.j256.ormlite:ormlite-jdbc:5.1' compile 'com.j256.ormlite:ormlite-jdbc:5.1'
@@ -114,10 +114,10 @@ dependencies {
compile 'dnsjava:dnsjava:2.1.9' compile 'dnsjava:dnsjava:2.1.9'
// HTTP connections // HTTP connections
compile 'org.apache.httpcomponents:httpclient:4.5.9' compile 'org.apache.httpcomponents:httpclient:4.5.10'
// Phone numbers validation // Phone numbers validation
compile 'com.googlecode.libphonenumber:libphonenumber:8.10.15' compile 'com.googlecode.libphonenumber:libphonenumber:8.10.22'
// E-mail sending // E-mail sending
compile 'javax.mail:javax.mail-api:1.6.2' compile 'javax.mail:javax.mail-api:1.6.2'
@@ -133,13 +133,13 @@ dependencies {
compile 'org.xerial:sqlite-jdbc:3.28.0' compile 'org.xerial:sqlite-jdbc:3.28.0'
// PostgreSQL // PostgreSQL
compile 'org.postgresql:postgresql:42.2.6' compile 'org.postgresql:postgresql:42.2.8'
// MariaDB/MySQL // MariaDB/MySQL
compile 'org.mariadb.jdbc:mariadb-java-client:2.4.2' compile 'org.mariadb.jdbc:mariadb-java-client:2.5.1'
// Twilio SDK for SMS // Twilio SDK for SMS
compile 'com.twilio.sdk:twilio:7.40.1' compile 'com.twilio.sdk:twilio:7.45.0'
// SendGrid SDK to send emails from GCE // SendGrid SDK to send emails from GCE
compile 'com.sendgrid:sendgrid-java:2.2.2' compile 'com.sendgrid:sendgrid-java:2.2.2'
@@ -148,15 +148,15 @@ dependencies {
compile 'org.zeroturnaround:zt-exec:1.11' compile 'org.zeroturnaround:zt-exec:1.11'
// HTTP server // HTTP server
compile 'io.undertow:undertow-core:2.0.22.Final' compile 'io.undertow:undertow-core:2.0.27.Final'
// Command parser for AS interface // Command parser for AS interface
implementation 'commons-cli:commons-cli:1.4' implementation 'commons-cli:commons-cli:1.4'
testCompile 'junit:junit:4.13-beta-3' testCompile 'junit:junit:4.13-rc-1'
testCompile 'com.github.tomakehurst:wiremock:2.24.0' testCompile 'com.github.tomakehurst:wiremock:2.25.1'
testCompile 'com.unboundid:unboundid-ldapsdk:4.0.11' testCompile 'com.unboundid:unboundid-ldapsdk:4.0.12'
testCompile 'com.icegreen:greenmail:1.5.10' testCompile 'com.icegreen:greenmail:1.5.11'
} }
jar { jar {

140
docs/MSC2140_MSC2134.md Normal file
View 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.

View File

@@ -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;

View File

@@ -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.5.1-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
View File

@@ -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
View File

@@ -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,

View File

@@ -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'
#

View File

@@ -1,47 +0,0 @@
/*
* matrix-java-sdk - Matrix Client SDK for Java
* Copyright (C) 2017 Kamax Sarl
*
* https://www.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.matrix.codec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class MxSha256 {
private MessageDigest md;
public MxSha256() {
try {
md = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
public String hash(byte[] data) {
return MxBase64.encode(md.digest(data));
}
public String hash(String data) {
return hash(data.getBytes(StandardCharsets.UTF_8));
}
}

View File

@@ -20,7 +20,12 @@
package io.kamax.mxisd; package io.kamax.mxisd;
import io.kamax.mxisd.config.MatrixConfig;
import io.kamax.mxisd.config.MxisdConfig; import io.kamax.mxisd.config.MxisdConfig;
import io.kamax.mxisd.config.PolicyConfig;
import io.kamax.mxisd.http.undertow.handler.ApiHandler;
import io.kamax.mxisd.http.undertow.handler.AuthorizationHandler;
import io.kamax.mxisd.http.undertow.handler.CheckTermsHandler;
import io.kamax.mxisd.http.undertow.handler.InternalInfoHandler; import io.kamax.mxisd.http.undertow.handler.InternalInfoHandler;
import io.kamax.mxisd.http.undertow.handler.OptionsHandler; import io.kamax.mxisd.http.undertow.handler.OptionsHandler;
import io.kamax.mxisd.http.undertow.handler.SaneHandler; import io.kamax.mxisd.http.undertow.handler.SaneHandler;
@@ -31,19 +36,47 @@ import io.kamax.mxisd.http.undertow.handler.auth.RestAuthHandler;
import io.kamax.mxisd.http.undertow.handler.auth.v1.LoginGetHandler; import io.kamax.mxisd.http.undertow.handler.auth.v1.LoginGetHandler;
import io.kamax.mxisd.http.undertow.handler.auth.v1.LoginHandler; import io.kamax.mxisd.http.undertow.handler.auth.v1.LoginHandler;
import io.kamax.mxisd.http.undertow.handler.auth.v1.LoginPostHandler; import io.kamax.mxisd.http.undertow.handler.auth.v1.LoginPostHandler;
import io.kamax.mxisd.http.undertow.handler.auth.v2.AccountGetUserInfoHandler;
import io.kamax.mxisd.http.undertow.handler.auth.v2.AccountLogoutHandler;
import io.kamax.mxisd.http.undertow.handler.auth.v2.AccountRegisterHandler;
import io.kamax.mxisd.http.undertow.handler.directory.v1.UserDirectorySearchHandler; import io.kamax.mxisd.http.undertow.handler.directory.v1.UserDirectorySearchHandler;
import io.kamax.mxisd.http.undertow.handler.identity.v1.*; import io.kamax.mxisd.http.undertow.handler.identity.share.EphemeralKeyIsValidHandler;
import io.kamax.mxisd.http.undertow.handler.identity.share.HelloHandler;
import io.kamax.mxisd.http.undertow.handler.identity.share.KeyGetHandler;
import io.kamax.mxisd.http.undertow.handler.identity.share.RegularKeyIsValidHandler;
import io.kamax.mxisd.http.undertow.handler.identity.share.SessionStartHandler;
import io.kamax.mxisd.http.undertow.handler.identity.share.SessionTpidBindHandler;
import io.kamax.mxisd.http.undertow.handler.identity.share.SessionTpidGetValidatedHandler;
import io.kamax.mxisd.http.undertow.handler.identity.share.SessionTpidUnbindHandler;
import io.kamax.mxisd.http.undertow.handler.identity.share.SessionValidationGetHandler;
import io.kamax.mxisd.http.undertow.handler.identity.share.SessionValidationPostHandler;
import io.kamax.mxisd.http.undertow.handler.identity.share.SignEd25519Handler;
import io.kamax.mxisd.http.undertow.handler.identity.share.StoreInviteHandler;
import io.kamax.mxisd.http.undertow.handler.identity.v1.BulkLookupHandler;
import io.kamax.mxisd.http.undertow.handler.identity.v1.SingleLookupHandler;
import io.kamax.mxisd.http.undertow.handler.identity.v2.HashDetailsHandler;
import io.kamax.mxisd.http.undertow.handler.identity.v2.HashLookupHandler;
import io.kamax.mxisd.http.undertow.handler.invite.v1.RoomInviteHandler; import io.kamax.mxisd.http.undertow.handler.invite.v1.RoomInviteHandler;
import io.kamax.mxisd.http.undertow.handler.profile.v1.InternalProfileHandler; import io.kamax.mxisd.http.undertow.handler.profile.v1.InternalProfileHandler;
import io.kamax.mxisd.http.undertow.handler.profile.v1.ProfileHandler; import io.kamax.mxisd.http.undertow.handler.profile.v1.ProfileHandler;
import io.kamax.mxisd.http.undertow.handler.register.v1.Register3pidRequestTokenHandler; import io.kamax.mxisd.http.undertow.handler.register.v1.Register3pidRequestTokenHandler;
import io.kamax.mxisd.http.undertow.handler.status.StatusHandler; import io.kamax.mxisd.http.undertow.handler.status.StatusHandler;
import io.kamax.mxisd.http.undertow.handler.status.VersionHandler; import io.kamax.mxisd.http.undertow.handler.status.VersionHandler;
import io.kamax.mxisd.http.undertow.handler.term.v2.AcceptTermsHandler;
import io.kamax.mxisd.http.undertow.handler.term.v2.GetTermsHandler;
import io.kamax.mxisd.matrix.IdentityServiceAPI;
import io.undertow.Handlers; import io.undertow.Handlers;
import io.undertow.Undertow; import io.undertow.Undertow;
import io.undertow.server.HttpHandler; import io.undertow.server.HttpHandler;
import io.undertow.server.RoutingHandler;
import io.undertow.util.HttpString;
import io.undertow.util.Methods;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.regex.Pattern;
public class HttpMxisd { public class HttpMxisd {
@@ -66,81 +99,172 @@ public class HttpMxisd {
public void start() { public void start() {
m.start(); m.start();
HttpHandler helloHandler = SaneHandler.around(new HelloHandler());
HttpHandler asUserHandler = SaneHandler.around(new AsUserHandler(m.getAs())); HttpHandler asUserHandler = SaneHandler.around(new AsUserHandler(m.getAs()));
HttpHandler asTxnHandler = SaneHandler.around(new AsTransactionHandler(m.getAs())); HttpHandler asTxnHandler = SaneHandler.around(new AsTransactionHandler(m.getAs()));
HttpHandler asNotFoundHandler = SaneHandler.around(new AsNotFoundHandler(m.getAs())); HttpHandler asNotFoundHandler = SaneHandler.around(new AsNotFoundHandler(m.getAs()));
HttpHandler storeInvHandler = SaneHandler.around(new StoreInviteHandler(m.getConfig().getServer(), m.getInvite(), m.getKeyManager())); final RoutingHandler handler = Handlers.routing()
.add("OPTIONS", "/**", SaneHandler.around(new OptionsHandler()))
httpSrv = Undertow.builder().addHttpListener(m.getConfig().getServer().getPort(), "0.0.0.0").setHandler(Handlers.routing() // Status endpoints
.get(StatusHandler.Path, SaneHandler.around(new StatusHandler()))
.get(VersionHandler.Path, SaneHandler.around(new VersionHandler()))
.add("OPTIONS", "/**", SaneHandler.around(new OptionsHandler())) // Authentication endpoints
.get(LoginHandler.Path, SaneHandler.around(new LoginGetHandler(m.getAuth(), m.getHttpClient())))
.post(LoginHandler.Path, SaneHandler.around(new LoginPostHandler(m.getAuth())))
.post(RestAuthHandler.Path, SaneHandler.around(new RestAuthHandler(m.getAuth())))
// Status endpoints // Directory endpoints
.get(StatusHandler.Path, SaneHandler.around(new StatusHandler())) .post(UserDirectorySearchHandler.Path, SaneHandler.around(new UserDirectorySearchHandler(m.getDirectory())))
.get(VersionHandler.Path, SaneHandler.around(new VersionHandler()))
// Authentication endpoints // Profile endpoints
.get(LoginHandler.Path, SaneHandler.around(new LoginGetHandler(m.getAuth(), m.getHttpClient()))) .get(ProfileHandler.Path, SaneHandler.around(new ProfileHandler(m.getProfile())))
.post(LoginHandler.Path, SaneHandler.around(new LoginPostHandler(m.getAuth()))) .get(InternalProfileHandler.Path, SaneHandler.around(new InternalProfileHandler(m.getProfile())))
.post(RestAuthHandler.Path, SaneHandler.around(new RestAuthHandler(m.getAuth())))
// Directory endpoints // Registration endpoints
.post(UserDirectorySearchHandler.Path, SaneHandler.around(new UserDirectorySearchHandler(m.getDirectory()))) .post(Register3pidRequestTokenHandler.Path,
SaneHandler.around(new Register3pidRequestTokenHandler(m.getReg(), m.getClientDns(), m.getHttpClient())))
// Key endpoints // Invite endpoints
.get(KeyGetHandler.Path, SaneHandler.around(new KeyGetHandler(m.getKeyManager()))) .post(RoomInviteHandler.Path, SaneHandler.around(new RoomInviteHandler(m.getHttpClient(), m.getClientDns(), m.getInvite())))
.get(RegularKeyIsValidHandler.Path, SaneHandler.around(new RegularKeyIsValidHandler(m.getKeyManager())))
.get(EphemeralKeyIsValidHandler.Path, SaneHandler.around(new EphemeralKeyIsValidHandler(m.getKeyManager())))
// Identity endpoints // Application Service endpoints
.get(HelloHandler.Path, helloHandler) .get(AsUserHandler.Path, asUserHandler)
.get(HelloHandler.Path + "/", helloHandler) // Be lax with possibly trailing slash .get("/_matrix/app/v1/rooms/**", asNotFoundHandler)
.get(SingleLookupHandler.Path, SaneHandler.around(new SingleLookupHandler(m.getConfig(), m.getIdentity(), m.getSign()))) .put(AsTransactionHandler.Path, asTxnHandler)
.post(BulkLookupHandler.Path, SaneHandler.around(new BulkLookupHandler(m.getIdentity())))
.post(StoreInviteHandler.Path, storeInvHandler)
.post(SessionStartHandler.Path, SaneHandler.around(new SessionStartHandler(m.getSession())))
.get(SessionValidateHandler.Path, SaneHandler.around(new SessionValidationGetHandler(m.getSession(), m.getConfig())))
.post(SessionValidateHandler.Path, SaneHandler.around(new SessionValidationPostHandler(m.getSession())))
.get(SessionTpidGetValidatedHandler.Path, SaneHandler.around(new SessionTpidGetValidatedHandler(m.getSession())))
.post(SessionTpidBindHandler.Path, SaneHandler.around(new SessionTpidBindHandler(m.getSession(), m.getInvite(), m.getSign())))
.post(SessionTpidUnbindHandler.Path, SaneHandler.around(new SessionTpidUnbindHandler(m.getSession())))
.post(SignEd25519Handler.Path, SaneHandler.around(new SignEd25519Handler(m.getConfig(), m.getInvite(), m.getSign())))
// Profile endpoints .get("/users/{" + AsUserHandler.ID + "}", asUserHandler) // Legacy endpoint
.get(ProfileHandler.Path, SaneHandler.around(new ProfileHandler(m.getProfile()))) .get("/rooms/**", asNotFoundHandler) // Legacy endpoint
.get(InternalProfileHandler.Path, SaneHandler.around(new InternalProfileHandler(m.getProfile()))) .put("/transactions/{" + AsTransactionHandler.ID + "}", asTxnHandler) // Legacy endpoint
// Registration endpoints // Banned endpoints
.post(Register3pidRequestTokenHandler.Path, SaneHandler.around(new Register3pidRequestTokenHandler(m.getReg(), m.getClientDns(), m.getHttpClient()))) .get(InternalInfoHandler.Path, SaneHandler.around(new InternalInfoHandler()));
keyEndpoints(handler);
// Invite endpoints identityEndpoints(handler);
.post(RoomInviteHandler.Path, SaneHandler.around(new RoomInviteHandler(m.getHttpClient(), m.getClientDns(), m.getInvite()))) termsEndpoints(handler);
hashEndpoints(handler);
// Application Service endpoints accountEndpoints(handler);
.get(AsUserHandler.Path, asUserHandler) httpSrv = Undertow.builder().addHttpListener(m.getConfig().getServer().getPort(), "0.0.0.0").setHandler(handler).build();
.get("/_matrix/app/v1/rooms/**", asNotFoundHandler)
.put(AsTransactionHandler.Path, asTxnHandler)
.get("/users/{" + AsUserHandler.ID + "}", asUserHandler) // Legacy endpoint
.get("/rooms/**", asNotFoundHandler) // Legacy endpoint
.put("/transactions/{" + AsTransactionHandler.ID + "}", asTxnHandler) // Legacy endpoint
// Banned endpoints
.get(InternalInfoHandler.Path, SaneHandler.around(new InternalInfoHandler()))
).build();
httpSrv.start(); httpSrv.start();
} }
public void stop() { public void stop() {
// Because it might have never been initialized if an exception is thrown early // Because it might have never been initialized if an exception is thrown early
if (Objects.nonNull(httpSrv)) httpSrv.stop(); if (Objects.nonNull(httpSrv)) {
httpSrv.stop();
}
m.stop(); m.stop();
} }
private void keyEndpoints(RoutingHandler routingHandler) {
addEndpoints(routingHandler, Methods.GET, false,
new KeyGetHandler(m.getKeyManager()),
new RegularKeyIsValidHandler(m.getKeyManager()),
new EphemeralKeyIsValidHandler(m.getKeyManager())
);
}
private void identityEndpoints(RoutingHandler routingHandler) {
// Legacy v1
routingHandler.get(SingleLookupHandler.Path, sane(new SingleLookupHandler(m.getConfig(), m.getIdentity(), m.getSign())));
routingHandler.post(BulkLookupHandler.Path, sane(new BulkLookupHandler(m.getIdentity())));
addEndpoints(routingHandler, Methods.GET, false, new HelloHandler());
addEndpoints(routingHandler, Methods.GET, true,
new SessionValidationGetHandler(m.getSession(), m.getConfig()),
new SessionTpidGetValidatedHandler(m.getSession())
);
addEndpoints(routingHandler, Methods.POST, true,
new StoreInviteHandler(m.getConfig().getServer(), m.getInvite(), m.getKeyManager()),
new SessionStartHandler(m.getSession()),
new SessionValidationPostHandler(m.getSession()),
new SessionTpidBindHandler(m.getSession(), m.getInvite(), m.getSign()),
new SessionTpidUnbindHandler(m.getSession()),
new SignEd25519Handler(m.getConfig(), m.getInvite(), m.getSign())
);
}
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) {
MatrixConfig matrixConfig = m.getConfig().getMatrix();
if (matrixConfig.isV2()) {
routingHandler.get(GetTermsHandler.PATH, new GetTermsHandler(m.getConfig().getPolicy()));
routingHandler.post(AcceptTermsHandler.PATH, sane(new AcceptTermsHandler(m.getAccMgr())));
}
}
private void hashEndpoints(RoutingHandler routingHandler) {
MatrixConfig matrixConfig = m.getConfig().getMatrix();
if (matrixConfig.isV2()) {
wrapWithTokenAndAuthorizationHandlers(routingHandler, Methods.GET, sane(new HashDetailsHandler(m.getHashManager())),
HashDetailsHandler.PATH, true);
wrapWithTokenAndAuthorizationHandlers(routingHandler, Methods.POST,
sane(new HashLookupHandler(m.getIdentity(), m.getHashManager())), HashLookupHandler.Path, true);
}
}
private void addEndpoints(RoutingHandler routingHandler, HttpString method, boolean useAuthorization, ApiHandler... handlers) {
for (ApiHandler handler : handlers) {
attachHandler(routingHandler, method, handler, useAuthorization, sane(handler));
}
}
private void attachHandler(RoutingHandler routingHandler, HttpString method, ApiHandler apiHandler, boolean useAuthorization,
HttpHandler httpHandler) {
MatrixConfig matrixConfig = m.getConfig().getMatrix();
if (matrixConfig.isV1()) {
routingHandler.add(method, apiHandler.getPath(IdentityServiceAPI.V1), httpHandler);
}
if (matrixConfig.isV2()) {
String path = apiHandler.getPath(IdentityServiceAPI.V2);
wrapWithTokenAndAuthorizationHandlers(routingHandler, method, httpHandler, path, useAuthorization);
}
}
private void wrapWithTokenAndAuthorizationHandlers(RoutingHandler routingHandler, HttpString method, HttpHandler httpHandler,
String url, boolean useAuthorization) {
List<PolicyConfig.PolicyObject> policyObjects = getPolicyObjects(url);
HttpHandler wrappedHandler;
if (useAuthorization) {
wrappedHandler = policyObjects.isEmpty() ? httpHandler : CheckTermsHandler.around(m.getAccMgr(), httpHandler, policyObjects);
wrappedHandler = AuthorizationHandler.around(m.getAccMgr(), wrappedHandler);
} else {
wrappedHandler = httpHandler;
}
routingHandler.add(method, url, wrappedHandler);
}
@NotNull
private List<PolicyConfig.PolicyObject> getPolicyObjects(String url) {
PolicyConfig policyConfig = m.getConfig().getPolicy();
List<PolicyConfig.PolicyObject> policies = new ArrayList<>();
if (!policyConfig.getPolicies().isEmpty()) {
for (PolicyConfig.PolicyObject policy : policyConfig.getPolicies().values()) {
for (Pattern pattern : policy.getPatterns()) {
if (pattern.matcher(url).matches()) {
policies.add(policy);
}
}
}
}
return policies;
}
private HttpHandler sane(HttpHandler httpHandler) {
return SaneHandler.around(httpHandler);
}
} }

View File

@@ -21,6 +21,7 @@
package io.kamax.mxisd; package io.kamax.mxisd;
import io.kamax.mxisd.as.AppSvcManager; import io.kamax.mxisd.as.AppSvcManager;
import io.kamax.mxisd.auth.AccountManager;
import io.kamax.mxisd.auth.AuthManager; import io.kamax.mxisd.auth.AuthManager;
import io.kamax.mxisd.auth.AuthProviders; import io.kamax.mxisd.auth.AuthProviders;
import io.kamax.mxisd.backend.IdentityStoreSupplier; import io.kamax.mxisd.backend.IdentityStoreSupplier;
@@ -34,6 +35,7 @@ import io.kamax.mxisd.directory.DirectoryManager;
import io.kamax.mxisd.directory.DirectoryProviders; import io.kamax.mxisd.directory.DirectoryProviders;
import io.kamax.mxisd.dns.ClientDnsOverwrite; import io.kamax.mxisd.dns.ClientDnsOverwrite;
import io.kamax.mxisd.dns.FederationDnsOverwrite; import io.kamax.mxisd.dns.FederationDnsOverwrite;
import io.kamax.mxisd.hash.HashManager;
import io.kamax.mxisd.invitation.InvitationManager; import io.kamax.mxisd.invitation.InvitationManager;
import io.kamax.mxisd.lookup.ThreePidProviders; import io.kamax.mxisd.lookup.ThreePidProviders;
import io.kamax.mxisd.lookup.fetcher.IRemoteIdentityServerFetcher; import io.kamax.mxisd.lookup.fetcher.IRemoteIdentityServerFetcher;
@@ -85,6 +87,8 @@ public class Mxisd {
private SessionManager sessMgr; private SessionManager sessMgr;
private NotificationManager notifMgr; private NotificationManager notifMgr;
private RegistrationManager regMgr; private RegistrationManager regMgr;
private AccountManager accMgr;
private HashManager hashManager;
// HS-specific classes // HS-specific classes
private Synapse synapse; private Synapse synapse;
@@ -115,15 +119,19 @@ public class Mxisd {
ServiceLoader.load(IdentityStoreSupplier.class).iterator().forEachRemaining(p -> p.accept(this)); ServiceLoader.load(IdentityStoreSupplier.class).iterator().forEachRemaining(p -> p.accept(this));
ServiceLoader.load(NotificationHandlerSupplier.class).iterator().forEachRemaining(p -> p.accept(this)); ServiceLoader.load(NotificationHandlerSupplier.class).iterator().forEachRemaining(p -> p.accept(this));
idStrategy = new RecursivePriorityLookupStrategy(cfg.getLookup(), ThreePidProviders.get(), bridgeFetcher); hashManager = new HashManager();
hashManager.init(cfg.getHashing(), ThreePidProviders.get(), store);
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, cfg.getAccountConfig(), cfg.getMatrix());
} }
public MxisdConfig getConfig() { public MxisdConfig getConfig() {
@@ -194,6 +202,14 @@ public class Mxisd {
return synapse; return synapse;
} }
public AccountManager getAccMgr() {
return accMgr;
}
public HashManager getHashManager() {
return hashManager;
}
public void start() { public void start() {
build(); build();
} }

View File

@@ -0,0 +1,166 @@
package io.kamax.mxisd.auth;
import com.google.gson.JsonObject;
import io.kamax.matrix.MatrixID;
import io.kamax.matrix.json.GsonUtil;
import io.kamax.mxisd.config.AccountConfig;
import io.kamax.mxisd.config.MatrixConfig;
import io.kamax.mxisd.config.PolicyConfig;
import io.kamax.mxisd.exception.BadRequestException;
import io.kamax.mxisd.exception.InvalidCredentialsException;
import io.kamax.mxisd.exception.NotFoundException;
import io.kamax.mxisd.matrix.HomeserverFederationResolver;
import io.kamax.mxisd.matrix.HomeserverVerifier;
import io.kamax.mxisd.storage.IStorage;
import io.kamax.mxisd.storage.ormlite.dao.AccountDao;
import org.apache.http.HttpStatus;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.time.Instant;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
public class AccountManager {
private static final Logger LOGGER = LoggerFactory.getLogger(AccountManager.class);
private final IStorage storage;
private final HomeserverFederationResolver resolver;
private final AccountConfig accountConfig;
private final MatrixConfig matrixConfig;
public AccountManager(IStorage storage, HomeserverFederationResolver resolver, AccountConfig accountConfig, MatrixConfig matrixConfig) {
this.storage = storage;
this.resolver = resolver;
this.accountConfig = accountConfig;
this.matrixConfig = matrixConfig;
}
public String register(OpenIdToken openIdToken) {
Objects.requireNonNull(openIdToken.getAccessToken(), "Missing required access_token");
Objects.requireNonNull(openIdToken.getTokenType(), "Missing required token type");
Objects.requireNonNull(openIdToken.getMatrixServerName(), "Missing required matrix domain");
LOGGER.info("Registration from the server: {}", openIdToken.getMatrixServerName());
String userId = getUserId(openIdToken);
LOGGER.info("UserId: {}", userId);
String token = UUID.randomUUID().toString();
AccountDao account = new AccountDao(openIdToken.getAccessToken(), openIdToken.getTokenType(),
openIdToken.getMatrixServerName(), openIdToken.getExpiresIn(),
Instant.now().getEpochSecond(), userId, token);
storage.insertToken(account);
LOGGER.info("User {} registered", userId);
return token;
}
private String getUserId(OpenIdToken openIdToken) {
String matrixServerName = openIdToken.getMatrixServerName();
HomeserverFederationResolver.HomeserverTarget homeserverTarget = resolver.resolve(matrixServerName);
String homeserverURL = homeserverTarget.getUrl().toString();
LOGGER.info("Domain resolved: {} => {}", matrixServerName, homeserverURL);
HttpGet getUserInfo = new HttpGet(
homeserverURL + "/_matrix/federation/v1/openid/userinfo?access_token=" + openIdToken.getAccessToken());
String userId;
try (CloseableHttpClient httpClient = HttpClients.custom()
.setSSLHostnameVerifier(new HomeserverVerifier(homeserverTarget.getDomain())).build()) {
try (CloseableHttpResponse response = httpClient.execute(getUserInfo)) {
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == HttpStatus.SC_OK) {
String content = EntityUtils.toString(response.getEntity());
LOGGER.trace("Response: {}", content);
JsonObject body = GsonUtil.parseObj(content);
userId = GsonUtil.getStringOrThrow(body, "sub");
} else {
LOGGER.error("Wrong response status: {}", statusCode);
throw new InvalidCredentialsException();
}
} catch (IOException e) {
LOGGER.error("Unable to get user info.", e);
throw new InvalidCredentialsException();
}
} catch (IOException e) {
LOGGER.error("Unable to create a connection to host: " + homeserverURL, e);
throw new InvalidCredentialsException();
}
checkMXID(userId);
return userId;
}
private void checkMXID(String userId) {
MatrixID mxid;
try {
mxid = MatrixID.asValid(userId);
} catch (IllegalArgumentException e) {
LOGGER.error("Wrong MXID: " + userId, e);
throw new BadRequestException("Wrong MXID");
}
if (getAccountConfig().isAllowOnlyTrustDomains()) {
LOGGER.info("Allow registration only for trust domain.");
if (getMatrixConfig().getDomain().equals(mxid.getDomain())) {
LOGGER.info("Allow user {} to registration", userId);
} else {
LOGGER.error("Deny user {} to registration", userId);
throw new InvalidCredentialsException();
}
} else {
LOGGER.info("Allow registration from any server.");
}
}
public String getUserId(String token) {
return storage.findAccount(token).orElseThrow(NotFoundException::new).getUserId();
}
public AccountDao findAccount(String token) {
AccountDao accountDao = storage.findAccount(token).orElse(null);
if (LOGGER.isInfoEnabled()) {
if (accountDao != null) {
LOGGER.info("Found account for user: {}", accountDao.getUserId());
} else {
LOGGER.warn("Account not found.");
}
}
return accountDao;
}
public void logout(String token) {
String userId = storage.findAccount(token).orElseThrow(InvalidCredentialsException::new).getUserId();
LOGGER.info("Logout: {}", userId);
deleteAccount(token);
}
public void deleteAccount(String token) {
storage.deleteAccepts(token);
storage.deleteToken(token);
}
public void acceptTerm(String token, String url) {
storage.acceptTerm(token, url);
}
public boolean isTermAccepted(String token, List<PolicyConfig.PolicyObject> policies) {
return policies.isEmpty() || storage.isTermAccepted(token, policies);
}
public AccountConfig getAccountConfig() {
return accountConfig;
}
public MatrixConfig getMatrixConfig() {
return matrixConfig;
}
}

View File

@@ -0,0 +1,50 @@
package io.kamax.mxisd.auth;
import com.google.gson.annotations.SerializedName;
public class OpenIdToken {
@SerializedName("access_token")
private String accessToken;
@SerializedName("token_type")
private String tokenType;
@SerializedName("matrix_server_name")
private String matrixServerName;
@SerializedName("expires_in")
private long expiresIn;
public String getAccessToken() {
return accessToken;
}
public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}
public String getTokenType() {
return tokenType;
}
public void setTokenType(String tokenType) {
this.tokenType = tokenType;
}
public String getMatrixServerName() {
return matrixServerName;
}
public void setMatrixServerName(String matrixServerName) {
this.matrixServerName = matrixServerName;
}
public long getExpiresIn() {
return expiresIn;
}
public void setExpiresIn(long expiresIn) {
this.expiresIn = expiresIn;
}
}

View File

@@ -164,6 +164,30 @@ public class ExecIdentityStore extends ExecStore implements IThreePidProvider {
return input.toString(); return input.toString();
}); });
addBulkSuccessMapper(p);
p.withFailureDefault(output -> Collections.emptyList());
return p.execute();
}
@Override
public Iterable<ThreePidMapping> populateHashes() {
if (!cfg.isHashLookup()) {
return Collections.emptyList();
}
Processor<List<ThreePidMapping>> p = new Processor<>();
p.withConfig(cfg.getLookup().getBulk());
addBulkSuccessMapper(p);
p.withFailureDefault(output -> Collections.emptyList());
return p.execute();
}
private void addBulkSuccessMapper(Processor<List<ThreePidMapping>> p) {
p.addSuccessMapper(JsonType, output -> { p.addSuccessMapper(JsonType, output -> {
if (StringUtils.isBlank(output)) { if (StringUtils.isBlank(output)) {
return Collections.emptyList(); return Collections.emptyList();
@@ -188,10 +212,5 @@ public class ExecIdentityStore extends ExecStore implements IThreePidProvider {
throw new InternalServerError("Invalid user type: " + item.getId().getType()); throw new InternalServerError("Invalid user type: " + item.getId().getType());
}).collect(Collectors.toList()); }).collect(Collectors.toList());
}); });
p.withFailureDefault(output -> Collections.emptyList());
return p.execute();
} }
} }

View File

@@ -48,6 +48,7 @@ import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.stream.Collectors;
public class MemoryIdentityStore implements AuthenticatorProvider, DirectoryProvider, IThreePidProvider, ProfileProvider { public class MemoryIdentityStore implements AuthenticatorProvider, DirectoryProvider, IThreePidProvider, ProfileProvider {
@@ -171,4 +172,15 @@ public class MemoryIdentityStore implements AuthenticatorProvider, DirectoryProv
}).orElseGet(BackendAuthResult::failure); }).orElseGet(BackendAuthResult::failure);
} }
@Override
public Iterable<ThreePidMapping> populateHashes() {
if (!cfg.isHashEnabled()) {
return Collections.emptyList();
}
return cfg.getIdentities().stream()
.map(mic -> mic.getThreepids().stream().map(mtp -> new ThreePidMapping(mtp.getMedium(), mtp.getAddress(), mic.getUsername())))
.flatMap(s -> s).collect(
Collectors.toList());
}
} }

View File

@@ -36,6 +36,7 @@ import java.sql.PreparedStatement;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@@ -104,4 +105,27 @@ public abstract class SqlThreePidProvider implements IThreePidProvider {
return new ArrayList<>(); return new ArrayList<>();
} }
@Override
public Iterable<ThreePidMapping> populateHashes() {
if (StringUtils.isBlank(cfg.getLookup().getQuery())) {
log.warn("Lookup query not configured, skip.");
return Collections.emptyList();
}
List<ThreePidMapping> result = new ArrayList<>();
try (Connection connection = pool.get()) {
PreparedStatement statement = connection.prepareStatement(cfg.getLookup().getQuery());
try (ResultSet resultSet = statement.executeQuery()) {
while (resultSet.next()) {
String mxid = resultSet.getString("mxid");
String medium = resultSet.getString("medium");
String address = resultSet.getString("address");
result.add(new ThreePidMapping(medium, address, mxid));
}
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
return result;
}
} }

View File

@@ -0,0 +1,8 @@
package io.kamax.mxisd.config;
public enum AcceptingPolicy {
ALL,
ANY
}

View File

@@ -0,0 +1,24 @@
package io.kamax.mxisd.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class AccountConfig {
private final static Logger log = LoggerFactory.getLogger(DirectoryConfig.class);
private boolean allowOnlyTrustDomains = true;
public boolean isAllowOnlyTrustDomains() {
return allowOnlyTrustDomains;
}
public void setAllowOnlyTrustDomains(boolean allowOnlyTrustDomains) {
this.allowOnlyTrustDomains = allowOnlyTrustDomains;
}
public void build() {
log.info("--- Account config ---");
log.info("Allow registration only for trust domain: {}", isAllowOnlyTrustDomains());
}
}

View File

@@ -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;
}
}

View File

@@ -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 {

View File

@@ -0,0 +1,120 @@
package io.kamax.mxisd.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
public class HashingConfig {
private static final Logger LOGGER = LoggerFactory.getLogger(HashingConfig.class);
private boolean enabled = false;
private int pepperLength = 20;
private RotationPolicyEnum rotationPolicy;
private HashStorageEnum hashStorageType;
private String delay = "10s";
private transient long delayInSeconds = 10;
private int requests = 10;
private List<Algorithm> algorithms = new ArrayList<>();
public void build() {
if (isEnabled()) {
LOGGER.info("--- Hash configuration ---");
LOGGER.info(" Pepper length: {}", getPepperLength());
LOGGER.info(" Rotation policy: {}", getRotationPolicy());
LOGGER.info(" Hash storage type: {}", getHashStorageType());
if (RotationPolicyEnum.per_seconds == getRotationPolicy()) {
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 {
LOGGER.info("Hash configuration disabled, used only `none` pepper.");
}
}
public enum Algorithm {
none,
sha256
}
public enum RotationPolicyEnum {
per_requests,
per_seconds
}
public enum HashStorageEnum {
in_memory,
sql
}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public int getPepperLength() {
return pepperLength;
}
public void setPepperLength(int pepperLength) {
this.pepperLength = pepperLength;
}
public RotationPolicyEnum getRotationPolicy() {
return rotationPolicy;
}
public void setRotationPolicy(RotationPolicyEnum rotationPolicy) {
this.rotationPolicy = rotationPolicy;
}
public HashStorageEnum getHashStorageType() {
return hashStorageType;
}
public void setHashStorageType(HashStorageEnum hashStorageType) {
this.hashStorageType = hashStorageType;
}
public String getDelay() {
return delay;
}
public void setDelay(String 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() {
return algorithms;
}
public void setAlgorithms(List<Algorithm> algorithms) {
this.algorithms = algorithms;
}
}

View File

@@ -63,6 +63,8 @@ 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 v2 = false;
public String getDomain() { public String getDomain() {
return domain; return domain;
@@ -80,6 +82,22 @@ public class MatrixConfig {
this.identity = identity; this.identity = identity;
} }
public boolean isV1() {
return v1;
}
public void setV1(boolean v1) {
this.v1 = v1;
}
public boolean isV2() {
return v2;
}
public void setV2(boolean v2) {
this.v2 = v2;
}
public void build() { public void build() {
log.info("--- Matrix config ---"); log.info("--- Matrix config ---");
@@ -90,6 +108,11 @@ public class MatrixConfig {
log.info("Domain: {}", getDomain()); log.info("Domain: {}", getDomain());
log.info("Identity:"); log.info("Identity:");
log.info("\tServers: {}", GsonUtil.get().toJson(identity.getServers())); log.info("\tServers: {}", GsonUtil.get().toJson(identity.getServers()));
log.info("API v1: {}", v1);
log.info("API v2: {}", v2);
if (v1) {
log.warn("API v1 is deprecated via MSC2140: https://github.com/matrix-org/matrix-doc/pull/2140 and will be deleted in future releases.");
log.warn("Please upgrade your homeserver and enable only API v2.");
}
} }
} }

View File

@@ -92,6 +92,7 @@ public class MxisdConfig {
private AppServiceConfig appsvc = new AppServiceConfig(); private AppServiceConfig appsvc = new AppServiceConfig();
private AuthenticationConfig auth = new AuthenticationConfig(); private AuthenticationConfig auth = new AuthenticationConfig();
private DirectoryConfig directory = new DirectoryConfig(); private DirectoryConfig directory = new DirectoryConfig();
private AccountConfig accountConfig = new AccountConfig();
private Dns dns = new Dns(); private Dns dns = new Dns();
private ExecConfig exec = new ExecConfig(); private ExecConfig exec = new ExecConfig();
private FirebaseConfig firebase = new FirebaseConfig(); private FirebaseConfig firebase = new FirebaseConfig();
@@ -114,6 +115,8 @@ public class MxisdConfig {
private ThreePidConfig threepid = new ThreePidConfig(); private ThreePidConfig threepid = new ThreePidConfig();
private ViewConfig view = new ViewConfig(); private ViewConfig view = new ViewConfig();
private WordpressConfig wordpress = new WordpressConfig(); private WordpressConfig wordpress = new WordpressConfig();
private PolicyConfig policy = new PolicyConfig();
private HashingConfig hashing = new HashingConfig();
public AppServiceConfig getAppsvc() { public AppServiceConfig getAppsvc() {
return appsvc; return appsvc;
@@ -131,6 +134,14 @@ public class MxisdConfig {
this.auth = auth; this.auth = auth;
} }
public AccountConfig getAccountConfig() {
return accountConfig;
}
public void setAccountConfig(AccountConfig accountConfig) {
this.accountConfig = accountConfig;
}
public DirectoryConfig getDirectory() { public DirectoryConfig getDirectory() {
return directory; return directory;
} }
@@ -315,6 +326,22 @@ public class MxisdConfig {
this.wordpress = wordpress; this.wordpress = wordpress;
} }
public PolicyConfig getPolicy() {
return policy;
}
public void setPolicy(PolicyConfig policy) {
this.policy = policy;
}
public HashingConfig getHashing() {
return hashing;
}
public void setHashing(HashingConfig hashing) {
this.hashing = hashing;
}
public MxisdConfig inMemory() { public MxisdConfig inMemory() {
getKey().setPath(":memory:"); getKey().setPath(":memory:");
getStorage().getProvider().getSqlite().setDatabase(":memory:"); getStorage().getProvider().getSqlite().setDatabase(":memory:");
@@ -330,6 +357,7 @@ public class MxisdConfig {
getAppsvc().build(); getAppsvc().build();
getAuth().build(); getAuth().build();
getAccountConfig().build();
getDirectory().build(); getDirectory().build();
getExec().build(); getExec().build();
getFirebase().build(); getFirebase().build();
@@ -352,6 +380,8 @@ public class MxisdConfig {
getThreepid().build(); getThreepid().build();
getView().build(); getView().build();
getWordpress().build(); getWordpress().build();
getPolicy().build();
getHashing().build();
return this; return this;
} }

View File

@@ -0,0 +1,114 @@
package io.kamax.mxisd.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
public class PolicyConfig {
private static final Logger LOGGER = LoggerFactory.getLogger(PolicyConfig.class);
private Map<String, PolicyObject> policies = new HashMap<>();
public static class TermObject {
private String name;
private String url;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
}
public static class PolicyObject {
private String version;
private Map<String, TermObject> terms;
private List<String> regexp = new ArrayList<>();
private transient List<Pattern> patterns = new ArrayList<>();
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
public Map<String, TermObject> getTerms() {
return terms;
}
public void setTerms(Map<String, TermObject> terms) {
this.terms = terms;
}
public List<String> getRegexp() {
return regexp;
}
public void setRegexp(List<String> regexp) {
this.regexp = regexp;
}
public List<Pattern> getPatterns() {
return patterns;
}
}
public Map<String, PolicyObject> getPolicies() {
return policies;
}
public void setPolicies(Map<String, PolicyObject> policies) {
this.policies = policies;
}
public void build() {
LOGGER.info("--- Policy Config ---");
if (getPolicies().isEmpty()) {
LOGGER.info("Empty");
} else {
for (Map.Entry<String, PolicyObject> policyObjectItem : getPolicies().entrySet()) {
PolicyObject policyObject = policyObjectItem.getValue();
StringBuilder sb = new StringBuilder();
sb.append("Policy \"").append(policyObjectItem.getKey()).append("\"\n");
sb.append(" version: ").append(policyObject.getVersion()).append("\n");
for (String regexp : policyObjectItem.getValue().getRegexp()) {
sb.append(" - ").append(regexp).append("\n");
policyObjectItem.getValue().getPatterns().add(Pattern.compile(regexp));
}
sb.append(" terms:\n");
if (policyObject.getTerms() != null) {
for (Map.Entry<String, TermObject> termItem : policyObject.getTerms().entrySet()) {
sb.append(" - lang: ").append(termItem.getKey()).append("\n");
sb.append(" name: ").append(termItem.getValue().getName()).append("\n");
sb.append(" url: ").append(termItem.getValue().getUrl()).append("\n");
}
}
LOGGER.info(sb.toString());
}
}
}
}

View File

@@ -79,5 +79,4 @@ public class ServerConfig {
log.info("Port: {}", getPort()); log.info("Port: {}", getPort());
log.info("Public URL: {}", getPublicUrl()); log.info("Public URL: {}", getPublicUrl());
} }
} }

View File

@@ -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
} }

View File

@@ -124,6 +124,18 @@ public abstract class SqlConfig {
} }
public static class Lookup {
private String query = "SELECT user_id AS mxid, medium, address from user_threepids";
public String getQuery() {
return query;
}
public void setQuery(String query) {
this.query = query;
}
}
public static class Identity { public static class Identity {
private Boolean enabled; private Boolean enabled;
@@ -264,6 +276,7 @@ public abstract class SqlConfig {
private Directory directory = new Directory(); private Directory directory = new Directory();
private Identity identity = new Identity(); private Identity identity = new Identity();
private Profile profile = new Profile(); private Profile profile = new Profile();
private Lookup lookup = new Lookup();
public boolean isEnabled() { public boolean isEnabled() {
return enabled; return enabled;
@@ -321,6 +334,14 @@ public abstract class SqlConfig {
this.profile = profile; this.profile = profile;
} }
public Lookup getLookup() {
return lookup;
}
public void setLookup(Lookup lookup) {
this.lookup = lookup;
}
protected abstract String getProviderName(); protected abstract String getProviderName();
public void build() { public void build() {
@@ -354,6 +375,7 @@ public abstract class SqlConfig {
log.info("Identity type: {}", getIdentity().getType()); log.info("Identity type: {}", getIdentity().getType());
log.info("3PID mapping query: {}", getIdentity().getQuery()); log.info("3PID mapping query: {}", getIdentity().getQuery());
log.info("Identity medium queries: {}", GsonUtil.build().toJson(getIdentity().getMedium())); log.info("Identity medium queries: {}", GsonUtil.build().toJson(getIdentity().getMedium()));
log.info("Lookup query: {}", getLookup().getQuery());
log.info("Profile:"); log.info("Profile:");
log.info(" Enabled: {}", getProfile().isEnabled()); log.info(" Enabled: {}", getProfile().isEnabled());
if (getProfile().isEnabled()) { if (getProfile().isEnabled()) {

View File

@@ -0,0 +1,27 @@
/*
* mxisd - Matrix Identity Server Daemon
* 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.mxisd.exception;
public class InvalidParamException extends RuntimeException {
public InvalidParamException() {
}
}

View File

@@ -0,0 +1,27 @@
/*
* mxisd - Matrix Identity Server Daemon
* 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.mxisd.exception;
public class InvalidPepperException extends RuntimeException {
public InvalidPepperException() {
}
}

View File

@@ -0,0 +1,62 @@
package io.kamax.mxisd.hash;
import io.kamax.mxisd.config.HashingConfig;
import io.kamax.mxisd.hash.storage.HashStorage;
import io.kamax.mxisd.lookup.ThreePidMapping;
import io.kamax.mxisd.lookup.provider.IThreePidProvider;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.RandomStringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Base64;
import java.util.List;
public class HashEngine {
private static final Logger LOGGER = LoggerFactory.getLogger(HashEngine.class);
private final List<? extends IThreePidProvider> providers;
private final HashStorage hashStorage;
private final HashingConfig config;
private final Base64.Encoder base64 = Base64.getUrlEncoder().withoutPadding();
private String pepper;
public HashEngine(List<? extends IThreePidProvider> providers, HashStorage hashStorage, HashingConfig config) {
this.providers = providers;
this.hashStorage = hashStorage;
this.config = config;
}
public void updateHashes() {
LOGGER.info("Start update hashes.");
synchronized (hashStorage) {
this.pepper = newPepper();
hashStorage.clear();
for (IThreePidProvider provider : providers) {
try {
for (ThreePidMapping pidMapping : provider.populateHashes()) {
hashStorage.add(pidMapping, hash(pidMapping));
}
} catch (Exception e) {
LOGGER.error("Unable to update hashes of the provider: " + provider.toString(), e);
}
}
}
LOGGER.info("Finish update hashes.");
}
public String getPepper() {
synchronized (hashStorage) {
return pepper;
}
}
protected String hash(ThreePidMapping pidMapping) {
return base64.encodeToString(DigestUtils.sha256(pidMapping.getValue() + " " + pidMapping.getMedium() + " " + getPepper()));
}
protected String newPepper() {
return RandomStringUtils.random(config.getPepperLength(), true, true);
}
}

View File

@@ -0,0 +1,91 @@
package io.kamax.mxisd.hash;
import io.kamax.mxisd.config.HashingConfig;
import io.kamax.mxisd.hash.rotation.HashRotationStrategy;
import io.kamax.mxisd.hash.rotation.NoOpRotationStrategy;
import io.kamax.mxisd.hash.rotation.RotationPerRequests;
import io.kamax.mxisd.hash.rotation.TimeBasedRotation;
import io.kamax.mxisd.hash.storage.EmptyStorage;
import io.kamax.mxisd.hash.storage.HashStorage;
import io.kamax.mxisd.hash.storage.InMemoryHashStorage;
import io.kamax.mxisd.hash.storage.SqlHashStorage;
import io.kamax.mxisd.lookup.provider.IThreePidProvider;
import io.kamax.mxisd.storage.IStorage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
public class HashManager {
private static final Logger LOGGER = LoggerFactory.getLogger(HashManager.class);
private HashEngine hashEngine;
private HashRotationStrategy rotationStrategy;
private HashStorage hashStorage;
private HashingConfig config;
private IStorage storage;
private AtomicBoolean configured = new AtomicBoolean(false);
public void init(HashingConfig config, List<? extends IThreePidProvider> providers, IStorage storage) {
this.config = config;
this.storage = storage;
initStorage();
hashEngine = new HashEngine(providers, getHashStorage(), config);
initRotationStrategy();
configured.set(true);
}
private void initStorage() {
if (config.isEnabled()) {
switch (config.getHashStorageType()) {
case in_memory:
this.hashStorage = new InMemoryHashStorage();
break;
case sql:
this.hashStorage = new SqlHashStorage(storage);
break;
default:
throw new IllegalArgumentException("Unknown storage type: " + config.getHashStorageType());
}
} else {
this.hashStorage = new EmptyStorage();
}
}
private void initRotationStrategy() {
if (config.isEnabled()) {
switch (config.getRotationPolicy()) {
case per_requests:
this.rotationStrategy = new RotationPerRequests(config.getRequests());
break;
case per_seconds:
this.rotationStrategy = new TimeBasedRotation(config.getDelayInSeconds());
break;
default:
throw new IllegalArgumentException("Unknown rotation type: " + config.getHashStorageType());
}
} else {
this.rotationStrategy = new NoOpRotationStrategy();
}
this.rotationStrategy.register(getHashEngine());
}
public HashEngine getHashEngine() {
return hashEngine;
}
public HashRotationStrategy getRotationStrategy() {
return rotationStrategy;
}
public HashStorage getHashStorage() {
return hashStorage;
}
public HashingConfig getConfig() {
return config;
}
}

View File

@@ -0,0 +1,16 @@
package io.kamax.mxisd.hash.rotation;
import io.kamax.mxisd.hash.HashEngine;
public interface HashRotationStrategy {
void register(HashEngine hashEngine);
HashEngine getHashEngine();
void newRequest();
default void trigger() {
getHashEngine().updateHashes();
}
}

View File

@@ -0,0 +1,23 @@
package io.kamax.mxisd.hash.rotation;
import io.kamax.mxisd.hash.HashEngine;
public class NoOpRotationStrategy implements HashRotationStrategy {
private HashEngine hashEngine;
@Override
public void register(HashEngine hashEngine) {
this.hashEngine = hashEngine;
}
@Override
public HashEngine getHashEngine() {
return hashEngine;
}
@Override
public void newRequest() {
// nothing to do
}
}

View File

@@ -0,0 +1,36 @@
package io.kamax.mxisd.hash.rotation;
import io.kamax.mxisd.hash.HashEngine;
import java.util.concurrent.atomic.AtomicInteger;
public class RotationPerRequests implements HashRotationStrategy {
private HashEngine hashEngine;
private final AtomicInteger counter = new AtomicInteger(0);
private final int barrier;
public RotationPerRequests(int barrier) {
this.barrier = barrier;
}
@Override
public void register(HashEngine hashEngine) {
this.hashEngine = hashEngine;
trigger();
}
@Override
public HashEngine getHashEngine() {
return hashEngine;
}
@Override
public synchronized void newRequest() {
int newValue = counter.incrementAndGet();
if (newValue >= barrier) {
counter.set(0);
trigger();
}
}
}

View File

@@ -0,0 +1,34 @@
package io.kamax.mxisd.hash.rotation;
import io.kamax.mxisd.hash.HashEngine;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class TimeBasedRotation implements HashRotationStrategy {
private final long delay;
private HashEngine hashEngine;
private final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
public TimeBasedRotation(long delay) {
this.delay = delay;
}
@Override
public void register(HashEngine hashEngine) {
this.hashEngine = hashEngine;
Runtime.getRuntime().addShutdownHook(new Thread(executorService::shutdown));
executorService.scheduleWithFixedDelay(this::trigger, 0, delay, TimeUnit.SECONDS);
}
@Override
public HashEngine getHashEngine() {
return hashEngine;
}
@Override
public void newRequest() {
}
}

View File

@@ -0,0 +1,25 @@
package io.kamax.mxisd.hash.storage;
import io.kamax.mxisd.lookup.ThreePidMapping;
import org.apache.commons.lang3.tuple.Pair;
import java.util.Collection;
import java.util.Collections;
public class EmptyStorage implements HashStorage {
@Override
public Collection<Pair<String, ThreePidMapping>> find(Iterable<String> hashes) {
return Collections.emptyList();
}
@Override
public void add(ThreePidMapping pidMapping, String hash) {
}
@Override
public void clear() {
}
}

View File

@@ -0,0 +1,15 @@
package io.kamax.mxisd.hash.storage;
import io.kamax.mxisd.lookup.ThreePidMapping;
import org.apache.commons.lang3.tuple.Pair;
import java.util.Collection;
public interface HashStorage {
Collection<Pair<String, ThreePidMapping>> find(Iterable<String> hashes);
void add(ThreePidMapping pidMapping, String hash);
void clear();
}

View File

@@ -0,0 +1,37 @@
package io.kamax.mxisd.hash.storage;
import io.kamax.mxisd.lookup.ThreePidMapping;
import org.apache.commons.lang3.tuple.Pair;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class InMemoryHashStorage implements HashStorage {
private final Map<String, ThreePidMapping> mapping = new ConcurrentHashMap<>();
@Override
public Collection<Pair<String, ThreePidMapping>> find(Iterable<String> hashes) {
List<Pair<String, ThreePidMapping>> result = new ArrayList<>();
for (String hash : hashes) {
ThreePidMapping pidMapping = mapping.get(hash);
if (pidMapping != null) {
result.add(Pair.of(hash, pidMapping));
}
}
return result;
}
@Override
public void add(ThreePidMapping pidMapping, String hash) {
mapping.put(hash, pidMapping);
}
@Override
public void clear() {
mapping.clear();
}
}

View File

@@ -0,0 +1,32 @@
package io.kamax.mxisd.hash.storage;
import io.kamax.mxisd.lookup.ThreePidMapping;
import io.kamax.mxisd.storage.IStorage;
import org.apache.commons.lang3.tuple.Pair;
import java.util.Collection;
public class SqlHashStorage implements HashStorage {
private final IStorage storage;
public SqlHashStorage(IStorage storage) {
this.storage = storage;
Runtime.getRuntime().addShutdownHook(new Thread(storage::clearHashes));
}
@Override
public Collection<Pair<String, ThreePidMapping>> find(Iterable<String> hashes) {
return storage.findHashes(hashes);
}
@Override
public void add(ThreePidMapping pidMapping, String hash) {
storage.addHash(pidMapping.getMxid(), pidMapping.getMedium(), pidMapping.getValue(), hash);
}
@Override
public void clear() {
storage.clearHashes();
}
}

View File

@@ -25,8 +25,6 @@ public class IsAPIv1 {
public static final String Base = "/_matrix/identity/api/v1"; public static final String Base = "/_matrix/identity/api/v1";
public static String getValidate(String medium, String sid, String secret, String token) { public static String getValidate(String medium, String sid, String secret, String token) {
// FIXME use some kind of URLBuilder return String.format("%s/validate/%s/submitToken?sid=%s&client_secret=%s&token=%s", Base, medium, sid, secret, token);
return Base + "/validate/" + medium + "/submitToken?sid=" + sid + "&client_secret=" + secret + "&token=" + token;
} }
} }

View File

@@ -0,0 +1,31 @@
/*
* mxisd - Matrix Identity Server Daemon
* 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.mxisd.http;
public class IsAPIv2 {
public static final String Base = "/_matrix/identity/v2";
public static String getValidate(String medium, String sid, String secret, String token) {
return String.format("%s/validate/%s/submitToken?sid=%s&client_secret=%s&token=%s", Base, medium, sid, secret, token);
}
}

View File

@@ -0,0 +1,33 @@
/*
* mxisd - Matrix Identity Server Daemon
* 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.mxisd.http.io.identity;
import java.util.HashMap;
import java.util.Map;
public class ClientHashLookupAnswer {
private Map<String, String> mappings = new HashMap<>();
public Map<String, String> getMappings() {
return mappings;
}
}

View File

@@ -0,0 +1,55 @@
/*
* mxisd - Matrix Identity Server Daemon
* 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.mxisd.http.io.identity;
import java.util.ArrayList;
import java.util.List;
public class ClientHashLookupRequest {
private String algorithm;
private String pepper;
private List<String> addresses = new ArrayList<>();
public String getAlgorithm() {
return algorithm;
}
public void setAlgorithm(String algorithm) {
this.algorithm = algorithm;
}
public String getPepper() {
return pepper;
}
public void setPepper(String pepper) {
this.pepper = pepper;
}
public List<String> getAddresses() {
return addresses;
}
public void setAddresses(List<String> addresses) {
this.addresses = addresses;
}
}

View File

@@ -0,0 +1,22 @@
package io.kamax.mxisd.http.undertow.handler;
import io.kamax.mxisd.http.IsAPIv1;
import io.kamax.mxisd.http.IsAPIv2;
import io.kamax.mxisd.matrix.IdentityServiceAPI;
import io.undertow.server.HttpHandler;
public interface ApiHandler extends HttpHandler {
default String getPath(IdentityServiceAPI api) {
switch (api) {
case V2:
return IsAPIv2.Base + getHandlerPath();
case V1:
return IsAPIv1.Base + getHandlerPath();
default:
throw new IllegalArgumentException("Unknown api version: " + api);
}
}
String getHandlerPath();
}

View File

@@ -0,0 +1,70 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2018 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.mxisd.http.undertow.handler;
import io.kamax.mxisd.auth.AccountManager;
import io.kamax.mxisd.exception.InvalidCredentialsException;
import io.kamax.mxisd.storage.ormlite.dao.AccountDao;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class AuthorizationHandler extends BasicHttpHandler {
private static final Logger log = LoggerFactory.getLogger(AuthorizationHandler.class);
private final AccountManager accountManager;
private final HttpHandler child;
public static AuthorizationHandler around(AccountManager accountManager, HttpHandler child) {
return new AuthorizationHandler(accountManager, child);
}
private AuthorizationHandler(AccountManager accountManager, HttpHandler child) {
this.accountManager = accountManager;
this.child = child;
}
@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
String token = findAccessToken(exchange).orElse(null);
if (token == null) {
log.error("Unauthorized request from: {}", exchange.getHostAndPort());
throw new InvalidCredentialsException();
}
AccountDao account = accountManager.findAccount(token);
if (account == null) {
log.error("Account not found from request from: {}", exchange.getHostAndPort());
throw new InvalidCredentialsException();
}
long expiredAt = (account.getCreatedAt() + account.getExpiresIn()) * 1000; // expired in milliseconds
if (expiredAt < System.currentTimeMillis()) {
log.error("Account for '{}' from: {}", account.getUserId(), exchange.getHostAndPort());
accountManager.deleteAccount(token);
throw new InvalidCredentialsException();
}
log.trace("Access for '{}' allowed", account.getUserId());
child.handleRequest(exchange);
}
}

View File

@@ -28,6 +28,7 @@ import io.kamax.mxisd.exception.AccessTokenNotFoundException;
import io.kamax.mxisd.exception.HttpMatrixException; import io.kamax.mxisd.exception.HttpMatrixException;
import io.kamax.mxisd.exception.InternalServerError; import io.kamax.mxisd.exception.InternalServerError;
import io.kamax.mxisd.proxy.Response; import io.kamax.mxisd.proxy.Response;
import io.kamax.mxisd.util.OptionalUtil;
import io.kamax.mxisd.util.RestClientUtils; import io.kamax.mxisd.util.RestClientUtils;
import io.undertow.server.HttpHandler; import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange; import io.undertow.server.HttpServerExchange;
@@ -55,6 +56,24 @@ public abstract class BasicHttpHandler implements HttpHandler {
private static final Logger log = LoggerFactory.getLogger(BasicHttpHandler.class); private static final Logger log = LoggerFactory.getLogger(BasicHttpHandler.class);
protected final static String headerName = "Authorization";
protected final static String headerValuePrefix = "Bearer ";
private final static String parameterName = "access_token";
Optional<String> findAccessTokenInHeaders(HttpServerExchange exchange) {
return Optional.ofNullable(exchange.getRequestHeaders().getFirst(headerName))
.filter(header -> StringUtils.startsWith(header, headerValuePrefix))
.map(header -> header.substring(headerValuePrefix.length()));
}
Optional<String> findAccessTokenInQuery(HttpServerExchange exchange) {
return Optional.ofNullable(exchange.getQueryParameters().getOrDefault(parameterName, new LinkedList<>()).peekFirst());
}
public Optional<String> findAccessToken(HttpServerExchange exchange) {
return OptionalUtil.findFirst(() -> findAccessTokenInHeaders(exchange), () -> findAccessTokenInQuery(exchange));
}
protected String getAccessToken(HttpServerExchange exchange) { protected String getAccessToken(HttpServerExchange exchange) {
return Optional.ofNullable(exchange.getRequestHeaders().getFirst("Authorization")) return Optional.ofNullable(exchange.getRequestHeaders().getFirst("Authorization"))
.flatMap(v -> { .flatMap(v -> {

View File

@@ -0,0 +1,74 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2018 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.mxisd.http.undertow.handler;
import io.kamax.mxisd.auth.AccountManager;
import io.kamax.mxisd.config.PolicyConfig;
import io.kamax.mxisd.exception.InvalidCredentialsException;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
public class CheckTermsHandler extends BasicHttpHandler {
private static final Logger log = LoggerFactory.getLogger(CheckTermsHandler.class);
private final AccountManager accountManager;
private final HttpHandler child;
private final List<PolicyConfig.PolicyObject> policies;
public static CheckTermsHandler around(AccountManager accountManager, HttpHandler child, List<PolicyConfig.PolicyObject> policies) {
return new CheckTermsHandler(accountManager, child, policies);
}
private CheckTermsHandler(AccountManager accountManager, HttpHandler child,
List<PolicyConfig.PolicyObject> policies) {
this.accountManager = accountManager;
this.child = child;
this.policies = policies;
}
@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
if (policies == null || policies.isEmpty()) {
child.handleRequest(exchange);
return;
}
String token = findAccessToken(exchange).orElse(null);
if (token == null) {
log.error("Unauthorized request from: {}", exchange.getHostAndPort());
throw new InvalidCredentialsException();
}
if (!accountManager.isTermAccepted(token, policies)) {
log.error("Non accepting request from: {}", exchange.getHostAndPort());
throw new InvalidCredentialsException();
}
log.trace("Access granted");
child.handleRequest(exchange);
}
}

View File

@@ -21,35 +21,11 @@
package io.kamax.mxisd.http.undertow.handler; package io.kamax.mxisd.http.undertow.handler;
import io.kamax.mxisd.exception.AccessTokenNotFoundException; import io.kamax.mxisd.exception.AccessTokenNotFoundException;
import io.kamax.mxisd.util.OptionalUtil;
import io.undertow.server.HttpServerExchange; import io.undertow.server.HttpServerExchange;
import org.apache.commons.lang3.StringUtils;
import java.util.LinkedList;
import java.util.Optional;
public abstract class HomeserverProxyHandler extends BasicHttpHandler { public abstract class HomeserverProxyHandler extends BasicHttpHandler {
protected final static String headerName = "Authorization";
protected final static String headerValuePrefix = "Bearer ";
private final static String parameterName = "access_token";
Optional<String> findAccessTokenInHeaders(HttpServerExchange exchange) {
return Optional.ofNullable(exchange.getRequestHeaders().getFirst(headerName))
.filter(header -> StringUtils.startsWith(header, headerValuePrefix))
.map(header -> header.substring(headerValuePrefix.length()));
}
Optional<String> findAccessTokenInQuery(HttpServerExchange exchange) {
return Optional.ofNullable(exchange.getQueryParameters().getOrDefault(parameterName, new LinkedList<>()).peekFirst());
}
public Optional<String> findAccessToken(HttpServerExchange exchange) {
return OptionalUtil.findFirst(() -> findAccessTokenInHeaders(exchange), () -> findAccessTokenInQuery(exchange));
}
public String getAccessToken(HttpServerExchange exchange) { public String getAccessToken(HttpServerExchange exchange) {
return findAccessToken(exchange).orElseThrow(AccessTokenNotFoundException::new); return findAccessToken(exchange).orElseThrow(AccessTokenNotFoundException::new);
} }
} }

View File

@@ -87,6 +87,10 @@ public class SaneHandler extends BasicHttpHandler {
respond(exchange, HttpStatus.SC_NOT_FOUND, "M_NOT_FOUND", e.getMessage()); respond(exchange, HttpStatus.SC_NOT_FOUND, "M_NOT_FOUND", e.getMessage());
} catch (NotImplementedException e) { } catch (NotImplementedException e) {
respond(exchange, HttpStatus.SC_NOT_IMPLEMENTED, "M_NOT_IMPLEMENTED", e.getMessage()); respond(exchange, HttpStatus.SC_NOT_IMPLEMENTED, "M_NOT_IMPLEMENTED", e.getMessage());
} catch (InvalidPepperException e) {
respond(exchange, HttpStatus.SC_BAD_REQUEST, "M_INVALID_PEPPER", e.getMessage());
} catch (InvalidParamException e) {
respond(exchange, HttpStatus.SC_BAD_REQUEST, "M_INVALID_PARAM", e.getMessage());
} catch (FeatureNotAvailable e) { } catch (FeatureNotAvailable e) {
if (StringUtils.isNotBlank(e.getInternalReason())) { if (StringUtils.isNotBlank(e.getInternalReason())) {
log.error("Feature not available: {}", e.getInternalReason()); log.error("Feature not available: {}", e.getInternalReason());

View File

@@ -0,0 +1,52 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2018 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.mxisd.http.undertow.handler.auth.v2;
import io.kamax.matrix.json.GsonUtil;
import io.kamax.mxisd.auth.AccountManager;
import io.kamax.mxisd.exception.InvalidCredentialsException;
import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler;
import io.undertow.server.HttpServerExchange;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class AccountGetUserInfoHandler extends BasicHttpHandler {
public static final String Path = "/_matrix/identity/v2/account";
private static final Logger LOGGER = LoggerFactory.getLogger(AccountGetUserInfoHandler.class);
private final AccountManager accountManager;
public AccountGetUserInfoHandler(AccountManager accountManager) {
this.accountManager = accountManager;
}
@Override
public void handleRequest(HttpServerExchange exchange) {
LOGGER.info("Get User Info.");
String token = findAccessToken(exchange).orElseThrow(InvalidCredentialsException::new);
String userId = accountManager.getUserId(token);
LOGGER.info("Account found: {}", userId);
respond(exchange, GsonUtil.makeObj("user_id", userId));
}
}

View File

@@ -0,0 +1,51 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2018 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.mxisd.http.undertow.handler.auth.v2;
import io.kamax.mxisd.auth.AccountManager;
import io.kamax.mxisd.exception.InvalidCredentialsException;
import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler;
import io.undertow.server.HttpServerExchange;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class AccountLogoutHandler extends BasicHttpHandler {
public static final String Path = "/_matrix/identity/v2/account/logout";
private static final Logger LOGGER = LoggerFactory.getLogger(AccountLogoutHandler.class);
private final AccountManager accountManager;
public AccountLogoutHandler(AccountManager accountManager) {
this.accountManager = accountManager;
}
@Override
public void handleRequest(HttpServerExchange exchange) {
LOGGER.info("Logout.");
String token = findAccessToken(exchange).orElseThrow(InvalidCredentialsException::new);
accountManager.logout(token);
respondJson(exchange, "{}");
}
}

View File

@@ -0,0 +1,57 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2018 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.mxisd.http.undertow.handler.auth.v2;
import io.kamax.matrix.json.GsonUtil;
import io.kamax.mxisd.auth.AccountManager;
import io.kamax.mxisd.auth.OpenIdToken;
import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler;
import io.undertow.server.HttpServerExchange;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Date;
public class AccountRegisterHandler extends BasicHttpHandler {
public static final String Path = "/_matrix/identity/v2/account/register";
private static final Logger LOGGER = LoggerFactory.getLogger(AccountRegisterHandler.class);
private final AccountManager accountManager;
public AccountRegisterHandler(AccountManager accountManager) {
this.accountManager = accountManager;
}
@Override
public void handleRequest(HttpServerExchange exchange) {
OpenIdToken openIdToken = parseJsonTo(exchange, OpenIdToken.class);
if (LOGGER.isInfoEnabled()) {
LOGGER.info("Registration from domain: {}, expired at {}", openIdToken.getMatrixServerName(),
new Date(System.currentTimeMillis() + openIdToken.getExpiresIn()));
}
String token = accountManager.register(openIdToken);
respond(exchange, GsonUtil.makeObj("token", token));
}
}

View File

@@ -18,18 +18,16 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package io.kamax.mxisd.http.undertow.handler.identity.v1; package io.kamax.mxisd.http.undertow.handler.identity.share;
import io.kamax.mxisd.crypto.KeyManager; import io.kamax.mxisd.crypto.KeyManager;
import io.kamax.mxisd.crypto.KeyType; import io.kamax.mxisd.crypto.KeyType;
import io.kamax.mxisd.http.IsAPIv1; import io.kamax.mxisd.http.undertow.handler.ApiHandler;
import io.undertow.server.HttpServerExchange; import io.undertow.server.HttpServerExchange;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
public class EphemeralKeyIsValidHandler extends KeyIsValidHandler { public class EphemeralKeyIsValidHandler extends KeyIsValidHandler implements ApiHandler {
public static final String Path = IsAPIv1.Base + "/pubkey/ephemeral/isvalid";
private static final Logger log = LoggerFactory.getLogger(EphemeralKeyIsValidHandler.class); private static final Logger log = LoggerFactory.getLogger(EphemeralKeyIsValidHandler.class);
@@ -48,4 +46,8 @@ public class EphemeralKeyIsValidHandler extends KeyIsValidHandler {
respondJson(exchange, mgr.isValid(KeyType.Ephemeral, pubKey) ? validKey : invalidKey); respondJson(exchange, mgr.isValid(KeyType.Ephemeral, pubKey) ? validKey : invalidKey);
} }
@Override
public String getHandlerPath() {
return "/pubkey/ephemeral/isvalid";
}
} }

View File

@@ -18,19 +18,21 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package io.kamax.mxisd.http.undertow.handler.identity.v1; package io.kamax.mxisd.http.undertow.handler.identity.share;
import io.kamax.mxisd.http.IsAPIv1;
import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler; import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler;
import io.kamax.mxisd.http.undertow.handler.ApiHandler;
import io.undertow.server.HttpServerExchange; import io.undertow.server.HttpServerExchange;
public class HelloHandler extends BasicHttpHandler { public class HelloHandler extends BasicHttpHandler implements ApiHandler {
public static final String Path = IsAPIv1.Base;
@Override @Override
public void handleRequest(HttpServerExchange exchange) { public void handleRequest(HttpServerExchange exchange) {
respondJson(exchange, "{}"); respondJson(exchange, "{}");
} }
@Override
public String getHandlerPath() {
return "";
}
} }

View File

@@ -18,23 +18,22 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package io.kamax.mxisd.http.undertow.handler.identity.v1; package io.kamax.mxisd.http.undertow.handler.identity.share;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import io.kamax.mxisd.crypto.GenericKeyIdentifier; import io.kamax.mxisd.crypto.GenericKeyIdentifier;
import io.kamax.mxisd.crypto.KeyManager; import io.kamax.mxisd.crypto.KeyManager;
import io.kamax.mxisd.crypto.KeyType; import io.kamax.mxisd.crypto.KeyType;
import io.kamax.mxisd.http.IsAPIv1;
import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler; import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler;
import io.kamax.mxisd.http.undertow.handler.ApiHandler;
import io.undertow.server.HttpServerExchange; import io.undertow.server.HttpServerExchange;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
public class KeyGetHandler extends BasicHttpHandler { public class KeyGetHandler extends BasicHttpHandler implements ApiHandler {
public static final String Key = "key"; public static final String Key = "key";
public static final String Path = IsAPIv1.Base + "/pubkey/{" + Key + "}";
private transient final Logger log = LoggerFactory.getLogger(KeyGetHandler.class); private transient final Logger log = LoggerFactory.getLogger(KeyGetHandler.class);
@@ -61,4 +60,8 @@ public class KeyGetHandler extends BasicHttpHandler {
respond(exchange, obj); respond(exchange, obj);
} }
@Override
public String getHandlerPath() {
return "/pubkey/{" + Key + "}";
}
} }

View File

@@ -18,7 +18,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package io.kamax.mxisd.http.undertow.handler.identity.v1; package io.kamax.mxisd.http.undertow.handler.identity.share;
import io.kamax.matrix.json.GsonUtil; import io.kamax.matrix.json.GsonUtil;
import io.kamax.mxisd.http.io.identity.KeyValidityJson; import io.kamax.mxisd.http.io.identity.KeyValidityJson;

View File

@@ -18,7 +18,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package io.kamax.mxisd.http.undertow.handler.identity.v1; package io.kamax.mxisd.http.undertow.handler.identity.share;
import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler; import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler;
import io.kamax.mxisd.lookup.ALookupRequest; import io.kamax.mxisd.lookup.ALookupRequest;

View File

@@ -18,18 +18,16 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package io.kamax.mxisd.http.undertow.handler.identity.v1; package io.kamax.mxisd.http.undertow.handler.identity.share;
import io.kamax.mxisd.crypto.KeyManager; import io.kamax.mxisd.crypto.KeyManager;
import io.kamax.mxisd.crypto.KeyType; import io.kamax.mxisd.crypto.KeyType;
import io.kamax.mxisd.http.IsAPIv1; import io.kamax.mxisd.http.undertow.handler.ApiHandler;
import io.undertow.server.HttpServerExchange; import io.undertow.server.HttpServerExchange;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
public class RegularKeyIsValidHandler extends KeyIsValidHandler { public class RegularKeyIsValidHandler extends KeyIsValidHandler implements ApiHandler {
public static final String Path = IsAPIv1.Base + "/pubkey/isvalid";
private transient final Logger log = LoggerFactory.getLogger(RegularKeyIsValidHandler.class); private transient final Logger log = LoggerFactory.getLogger(RegularKeyIsValidHandler.class);
@@ -48,4 +46,8 @@ public class RegularKeyIsValidHandler extends KeyIsValidHandler {
respondJson(exchange, mgr.isValid(KeyType.Regular, pubKey) ? validKey : invalidKey); respondJson(exchange, mgr.isValid(KeyType.Regular, pubKey) ? validKey : invalidKey);
} }
@Override
public String getHandlerPath() {
return "/pubkey/isvalid";
}
} }

View File

@@ -18,26 +18,25 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package io.kamax.mxisd.http.undertow.handler.identity.v1; package io.kamax.mxisd.http.undertow.handler.identity.share;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import io.kamax.matrix.ThreePid; import io.kamax.matrix.ThreePid;
import io.kamax.matrix.ThreePidMedium; import io.kamax.matrix.ThreePidMedium;
import io.kamax.mxisd.http.IsAPIv1;
import io.kamax.mxisd.http.io.identity.RequestTokenResponse; import io.kamax.mxisd.http.io.identity.RequestTokenResponse;
import io.kamax.mxisd.http.io.identity.SessionEmailTokenRequestJson; import io.kamax.mxisd.http.io.identity.SessionEmailTokenRequestJson;
import io.kamax.mxisd.http.io.identity.SessionPhoneTokenRequestJson; import io.kamax.mxisd.http.io.identity.SessionPhoneTokenRequestJson;
import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler; import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler;
import io.kamax.mxisd.http.undertow.handler.ApiHandler;
import io.kamax.mxisd.session.SessionManager; import io.kamax.mxisd.session.SessionManager;
import io.undertow.server.HttpServerExchange; import io.undertow.server.HttpServerExchange;
import org.apache.http.HttpStatus; import org.apache.http.HttpStatus;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
public class SessionStartHandler extends BasicHttpHandler { public class SessionStartHandler extends BasicHttpHandler implements ApiHandler {
public static final String Medium = "medium"; public static final String Medium = "medium";
public static final String Path = IsAPIv1.Base + "/validate/{" + Medium + "}/requestToken";
private transient final Logger log = LoggerFactory.getLogger(SessionStartHandler.class); private transient final Logger log = LoggerFactory.getLogger(SessionStartHandler.class);
@@ -84,4 +83,8 @@ public class SessionStartHandler extends BasicHttpHandler {
} }
} }
@Override
public String getHandlerPath() {
return "/validate/{" + Medium + "}/requestToken";
}
} }

View File

@@ -18,16 +18,16 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package io.kamax.mxisd.http.undertow.handler.identity.v1; package io.kamax.mxisd.http.undertow.handler.identity.share;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import io.kamax.matrix.json.GsonUtil; import io.kamax.matrix.json.GsonUtil;
import io.kamax.mxisd.crypto.SignatureManager; import io.kamax.mxisd.crypto.SignatureManager;
import io.kamax.mxisd.exception.BadRequestException; import io.kamax.mxisd.exception.BadRequestException;
import io.kamax.mxisd.http.IsAPIv1;
import io.kamax.mxisd.http.io.identity.BindRequest; import io.kamax.mxisd.http.io.identity.BindRequest;
import io.kamax.mxisd.http.io.identity.SingeLookupReplyJson; import io.kamax.mxisd.http.io.identity.SingeLookupReplyJson;
import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler; import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler;
import io.kamax.mxisd.http.undertow.handler.ApiHandler;
import io.kamax.mxisd.invitation.InvitationManager; import io.kamax.mxisd.invitation.InvitationManager;
import io.kamax.mxisd.lookup.SingleLookupReply; import io.kamax.mxisd.lookup.SingleLookupReply;
import io.kamax.mxisd.session.SessionManager; import io.kamax.mxisd.session.SessionManager;
@@ -42,9 +42,7 @@ import java.nio.charset.StandardCharsets;
import java.util.Deque; import java.util.Deque;
import java.util.Map; import java.util.Map;
public class SessionTpidBindHandler extends BasicHttpHandler { public class SessionTpidBindHandler extends BasicHttpHandler implements ApiHandler {
public static final String Path = IsAPIv1.Base + "/3pid/bind";
private static final Logger log = LoggerFactory.getLogger(SessionTpidBindHandler.class); private static final Logger log = LoggerFactory.getLogger(SessionTpidBindHandler.class);
@@ -97,4 +95,8 @@ public class SessionTpidBindHandler extends BasicHttpHandler {
} }
} }
@Override
public String getHandlerPath() {
return "/3pid/bind";
}
} }

View File

@@ -18,21 +18,19 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package io.kamax.mxisd.http.undertow.handler.identity.v1; package io.kamax.mxisd.http.undertow.handler.identity.share;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import io.kamax.mxisd.exception.SessionNotValidatedException; import io.kamax.mxisd.exception.SessionNotValidatedException;
import io.kamax.mxisd.http.IsAPIv1;
import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler; import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler;
import io.kamax.mxisd.http.undertow.handler.ApiHandler;
import io.kamax.mxisd.lookup.ThreePidValidation; import io.kamax.mxisd.lookup.ThreePidValidation;
import io.kamax.mxisd.session.SessionManager; import io.kamax.mxisd.session.SessionManager;
import io.undertow.server.HttpServerExchange; import io.undertow.server.HttpServerExchange;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
public class SessionTpidGetValidatedHandler extends BasicHttpHandler { public class SessionTpidGetValidatedHandler extends BasicHttpHandler implements ApiHandler {
public static final String Path = IsAPIv1.Base + "/3pid/getValidated3pid";
private static final Logger log = LoggerFactory.getLogger(SessionTpidGetValidatedHandler.class); private static final Logger log = LoggerFactory.getLogger(SessionTpidGetValidatedHandler.class);
@@ -62,4 +60,8 @@ public class SessionTpidGetValidatedHandler extends BasicHttpHandler {
} }
} }
@Override
public String getHandlerPath() {
return "/3pid/getValidated3pid";
}
} }

View File

@@ -18,19 +18,17 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package io.kamax.mxisd.http.undertow.handler.identity.v1; package io.kamax.mxisd.http.undertow.handler.identity.share;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import io.kamax.mxisd.http.IsAPIv1;
import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler; import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler;
import io.kamax.mxisd.http.undertow.handler.ApiHandler;
import io.kamax.mxisd.session.SessionManager; import io.kamax.mxisd.session.SessionManager;
import io.undertow.server.HttpServerExchange; import io.undertow.server.HttpServerExchange;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
public class SessionTpidUnbindHandler extends BasicHttpHandler { public class SessionTpidUnbindHandler extends BasicHttpHandler implements ApiHandler {
public static final String Path = IsAPIv1.Base + "/3pid/unbind";
private static final Logger log = LoggerFactory.getLogger(SessionTpidUnbindHandler.class); private static final Logger log = LoggerFactory.getLogger(SessionTpidUnbindHandler.class);
@@ -48,4 +46,9 @@ public class SessionTpidUnbindHandler extends BasicHttpHandler {
sessionMgr.unbind(auth, body); sessionMgr.unbind(auth, body);
writeBodyAsUtf8(exchange, "{}"); writeBodyAsUtf8(exchange, "{}");
} }
@Override
public String getHandlerPath() {
return "/3pid/unbind";
}
} }

View File

@@ -18,19 +18,17 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package io.kamax.mxisd.http.undertow.handler.identity.v1; package io.kamax.mxisd.http.undertow.handler.identity.share;
import io.kamax.mxisd.http.IsAPIv1;
import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler; import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler;
import io.kamax.mxisd.http.undertow.handler.ApiHandler;
import io.kamax.mxisd.session.SessionManager; import io.kamax.mxisd.session.SessionManager;
import io.kamax.mxisd.session.ValidationResult; import io.kamax.mxisd.session.ValidationResult;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
public abstract class SessionValidateHandler extends BasicHttpHandler { public abstract class SessionValidateHandler extends BasicHttpHandler implements ApiHandler {
public static final String Path = IsAPIv1.Base + "/validate/{medium}/submitToken";
private transient final Logger log = LoggerFactory.getLogger(SessionValidateHandler.class); private transient final Logger log = LoggerFactory.getLogger(SessionValidateHandler.class);
@@ -52,4 +50,8 @@ public abstract class SessionValidateHandler extends BasicHttpHandler {
return mgr.validate(sid, secret, token); return mgr.validate(sid, secret, token);
} }
@Override
public String getHandlerPath() {
return "/validate/{medium}/submitToken";
}
} }

View File

@@ -18,7 +18,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package io.kamax.mxisd.http.undertow.handler.identity.v1; package io.kamax.mxisd.http.undertow.handler.identity.share;
import io.kamax.mxisd.config.MxisdConfig; import io.kamax.mxisd.config.MxisdConfig;
import io.kamax.mxisd.config.ServerConfig; import io.kamax.mxisd.config.ServerConfig;

View File

@@ -18,7 +18,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package io.kamax.mxisd.http.undertow.handler.identity.v1; package io.kamax.mxisd.http.undertow.handler.identity.share;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import io.kamax.matrix.json.GsonUtil; import io.kamax.matrix.json.GsonUtil;

View File

@@ -18,7 +18,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package io.kamax.mxisd.http.undertow.handler.identity.v1; package io.kamax.mxisd.http.undertow.handler.identity.share;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import io.kamax.matrix.MatrixID; import io.kamax.matrix.MatrixID;
@@ -27,17 +27,15 @@ import io.kamax.matrix.json.GsonUtil;
import io.kamax.matrix.json.MatrixJson; import io.kamax.matrix.json.MatrixJson;
import io.kamax.mxisd.config.MxisdConfig; import io.kamax.mxisd.config.MxisdConfig;
import io.kamax.mxisd.crypto.SignatureManager; import io.kamax.mxisd.crypto.SignatureManager;
import io.kamax.mxisd.http.IsAPIv1;
import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler; import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler;
import io.kamax.mxisd.http.undertow.handler.ApiHandler;
import io.kamax.mxisd.invitation.IThreePidInviteReply; import io.kamax.mxisd.invitation.IThreePidInviteReply;
import io.kamax.mxisd.invitation.InvitationManager; import io.kamax.mxisd.invitation.InvitationManager;
import io.undertow.server.HttpServerExchange; import io.undertow.server.HttpServerExchange;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
public class SignEd25519Handler extends BasicHttpHandler { public class SignEd25519Handler extends BasicHttpHandler implements ApiHandler {
public static final String Path = IsAPIv1.Base + "/sign-ed25519";
private static final Logger log = LoggerFactory.getLogger(SignEd25519Handler.class); private static final Logger log = LoggerFactory.getLogger(SignEd25519Handler.class);
@@ -72,4 +70,8 @@ public class SignEd25519Handler extends BasicHttpHandler {
respondJson(exchange, res); respondJson(exchange, res);
} }
@Override
public String getHandlerPath() {
return "/sign-ed25519";
}
} }

View File

@@ -18,7 +18,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package io.kamax.mxisd.http.undertow.handler.identity.v1; package io.kamax.mxisd.http.undertow.handler.identity.share;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
@@ -28,10 +28,10 @@ import io.kamax.matrix.json.GsonUtil;
import io.kamax.mxisd.config.ServerConfig; import io.kamax.mxisd.config.ServerConfig;
import io.kamax.mxisd.crypto.KeyManager; import io.kamax.mxisd.crypto.KeyManager;
import io.kamax.mxisd.exception.BadRequestException; import io.kamax.mxisd.exception.BadRequestException;
import io.kamax.mxisd.http.IsAPIv1;
import io.kamax.mxisd.http.io.identity.StoreInviteRequest; import io.kamax.mxisd.http.io.identity.StoreInviteRequest;
import io.kamax.mxisd.http.io.identity.ThreePidInviteReplyIO; import io.kamax.mxisd.http.io.identity.ThreePidInviteReplyIO;
import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler; import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler;
import io.kamax.mxisd.http.undertow.handler.ApiHandler;
import io.kamax.mxisd.invitation.IThreePidInvite; import io.kamax.mxisd.invitation.IThreePidInvite;
import io.kamax.mxisd.invitation.IThreePidInviteReply; import io.kamax.mxisd.invitation.IThreePidInviteReply;
import io.kamax.mxisd.invitation.InvitationManager; import io.kamax.mxisd.invitation.InvitationManager;
@@ -45,9 +45,7 @@ import java.nio.charset.StandardCharsets;
import java.util.Deque; import java.util.Deque;
import java.util.Map; import java.util.Map;
public class StoreInviteHandler extends BasicHttpHandler { public class StoreInviteHandler extends BasicHttpHandler implements ApiHandler {
public static final String Path = IsAPIv1.Base + "/store-invite";
private ServerConfig cfg; private ServerConfig cfg;
private InvitationManager invMgr; private InvitationManager invMgr;
@@ -100,4 +98,8 @@ public class StoreInviteHandler extends BasicHttpHandler {
respondJson(exchange, new ThreePidInviteReplyIO(reply, keyMgr.getPublicKeyBase64(keyMgr.getServerSigningKey().getId()), cfg.getPublicUrl())); respondJson(exchange, new ThreePidInviteReplyIO(reply, keyMgr.getPublicKeyBase64(keyMgr.getServerSigningKey().getId()), cfg.getPublicUrl()));
} }
@Override
public String getHandlerPath() {
return "/store-invite";
}
} }

View File

@@ -23,6 +23,8 @@ package io.kamax.mxisd.http.undertow.handler.identity.v1;
import io.kamax.mxisd.http.IsAPIv1; import io.kamax.mxisd.http.IsAPIv1;
import io.kamax.mxisd.http.io.identity.ClientBulkLookupAnswer; import io.kamax.mxisd.http.io.identity.ClientBulkLookupAnswer;
import io.kamax.mxisd.http.io.identity.ClientBulkLookupRequest; import io.kamax.mxisd.http.io.identity.ClientBulkLookupRequest;
import io.kamax.mxisd.http.undertow.handler.ApiHandler;
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.ThreePidMapping; import io.kamax.mxisd.lookup.ThreePidMapping;
import io.kamax.mxisd.lookup.strategy.LookupStrategy; import io.kamax.mxisd.lookup.strategy.LookupStrategy;
@@ -33,7 +35,7 @@ import org.slf4j.LoggerFactory;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
public class BulkLookupHandler extends LookupHandler { public class BulkLookupHandler extends LookupHandler implements ApiHandler {
public static final String Path = IsAPIv1.Base + "/bulk_lookup"; public static final String Path = IsAPIv1.Base + "/bulk_lookup";
@@ -69,4 +71,8 @@ public class BulkLookupHandler extends LookupHandler {
respondJson(exchange, answer); respondJson(exchange, answer);
} }
@Override
public String getHandlerPath() {
return "/bulk_lookup";
}
} }

View File

@@ -27,6 +27,8 @@ import io.kamax.mxisd.config.ServerConfig;
import io.kamax.mxisd.crypto.SignatureManager; import io.kamax.mxisd.crypto.SignatureManager;
import io.kamax.mxisd.http.IsAPIv1; import io.kamax.mxisd.http.IsAPIv1;
import io.kamax.mxisd.http.io.identity.SingeLookupReplyJson; import io.kamax.mxisd.http.io.identity.SingeLookupReplyJson;
import io.kamax.mxisd.http.undertow.handler.ApiHandler;
import io.kamax.mxisd.http.undertow.handler.identity.share.LookupHandler;
import io.kamax.mxisd.lookup.SingleLookupReply; import io.kamax.mxisd.lookup.SingleLookupReply;
import io.kamax.mxisd.lookup.SingleLookupRequest; import io.kamax.mxisd.lookup.SingleLookupRequest;
import io.kamax.mxisd.lookup.strategy.LookupStrategy; import io.kamax.mxisd.lookup.strategy.LookupStrategy;
@@ -36,7 +38,7 @@ import org.slf4j.LoggerFactory;
import java.util.Optional; import java.util.Optional;
public class SingleLookupHandler extends LookupHandler { public class SingleLookupHandler extends LookupHandler implements ApiHandler {
public static final String Path = IsAPIv1.Base + "/lookup"; public static final String Path = IsAPIv1.Base + "/lookup";
@@ -77,4 +79,8 @@ public class SingleLookupHandler extends LookupHandler {
} }
} }
@Override
public String getHandlerPath() {
return "/lookup";
}
} }

View File

@@ -0,0 +1,38 @@
package io.kamax.mxisd.http.undertow.handler.identity.v2;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import io.kamax.mxisd.config.HashingConfig;
import io.kamax.mxisd.hash.HashManager;
import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler;
import io.undertow.server.HttpServerExchange;
public class HashDetailsHandler extends BasicHttpHandler {
public static final String PATH = "/_matrix/identity/v2/hash_details";
private final HashManager hashManager;
public HashDetailsHandler(HashManager hashManager) {
this.hashManager = hashManager;
}
@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
respond(exchange, getResponse());
}
private JsonObject getResponse() {
JsonObject response = new JsonObject();
response.addProperty("lookup_pepper", hashManager.getHashEngine().getPepper());
JsonArray algorithms = new JsonArray();
HashingConfig config = hashManager.getConfig();
if (config.isEnabled()) {
for (HashingConfig.Algorithm algorithm : config.getAlgorithms()) {
algorithms.add(algorithm.name().toLowerCase());
}
}
response.add("algorithms", algorithms);
return response;
}
}

View File

@@ -0,0 +1,169 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2018 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.mxisd.http.undertow.handler.identity.v2;
import io.kamax.mxisd.config.HashingConfig;
import io.kamax.mxisd.exception.InvalidParamException;
import io.kamax.mxisd.exception.InvalidPepperException;
import io.kamax.mxisd.hash.HashManager;
import io.kamax.mxisd.http.IsAPIv2;
import io.kamax.mxisd.http.io.identity.ClientHashLookupAnswer;
import io.kamax.mxisd.http.io.identity.ClientHashLookupRequest;
import io.kamax.mxisd.http.undertow.handler.ApiHandler;
import io.kamax.mxisd.http.undertow.handler.identity.share.LookupHandler;
import io.kamax.mxisd.lookup.BulkLookupRequest;
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.strategy.LookupStrategy;
import io.undertow.server.HttpServerExchange;
import org.apache.commons.lang3.tuple.Pair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
public class HashLookupHandler extends LookupHandler implements ApiHandler {
public static final String Path = IsAPIv2.Base + "/lookup";
private static final Logger log = LoggerFactory.getLogger(HashLookupHandler.class);
private LookupStrategy strategy;
private HashManager hashManager;
public HashLookupHandler(LookupStrategy strategy, HashManager hashManager) {
this.strategy = strategy;
this.hashManager = hashManager;
}
@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
ClientHashLookupRequest input = parseJsonTo(exchange, ClientHashLookupRequest.class);
HashLookupRequest lookupRequest = new HashLookupRequest();
setRequesterInfo(lookupRequest, exchange);
lookupRequest.setHashes(input.getAddresses());
log.info("Got bulk lookup request from {} with client {} - Is recursive? {}",
lookupRequest.getRequester(), lookupRequest.getUserAgent(), lookupRequest.isRecursive());
if (!hashManager.getConfig().isEnabled()) {
throw new InvalidParamException();
}
if (!hashManager.getHashEngine().getPepper().equals(input.getPepper())) {
throw new InvalidPepperException();
}
switch (input.getAlgorithm()) {
case "none":
noneAlgorithm(exchange, lookupRequest, input);
break;
case "sha256":
sha256Algorithm(exchange, lookupRequest, input);
break;
default:
throw new InvalidParamException();
}
}
private void noneAlgorithm(HttpServerExchange exchange, HashLookupRequest request, ClientHashLookupRequest input) throws Exception {
if (!hashManager.getConfig().getAlgorithms().contains(HashingConfig.Algorithm.none)) {
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();
List<ThreePidMapping> mappings = new ArrayList<>();
for (String address : input.getAddresses()) {
String[] parts = address.split(" ");
ThreePidMapping mapping = new ThreePidMapping();
mapping.setMedium(parts[1]);
mapping.setValue(parts[0]);
mappings.add(mapping);
}
bulkLookupRequest.setMappings(mappings);
ClientHashLookupAnswer answer = new ClientHashLookupAnswer();
for (ThreePidMapping mapping : strategy.find(bulkLookupRequest).get()) {
answer.getMappings().put(mapping.getMedium() + " " + mapping.getValue(), mapping.getMxid());
}
log.info("Finished bulk lookup request from {}", request.getRequester());
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) {
if (!hashManager.getConfig().getAlgorithms().contains(HashingConfig.Algorithm.sha256)) {
throw new InvalidParamException();
}
ClientHashLookupAnswer answer = new ClientHashLookupAnswer();
if (request.getHashes() != null && !request.getHashes().isEmpty()) {
for (Pair<String, ThreePidMapping> pair : hashManager.getHashStorage().find(request.getHashes())) {
answer.getMappings().put(pair.getKey(), pair.getValue().getMxid());
}
log.info("Finished bulk lookup request from {}", request.getRequester());
} else {
log.warn("Empty request");
}
respondJson(exchange, answer);
}
@Override
public String getHandlerPath() {
return "/bulk_lookup";
}
}

View File

@@ -0,0 +1,56 @@
package io.kamax.mxisd.http.undertow.handler.term.v2;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import io.kamax.mxisd.auth.AccountManager;
import io.kamax.mxisd.exception.InvalidCredentialsException;
import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler;
import io.kamax.mxisd.storage.ormlite.dao.AccountDao;
import io.undertow.server.HttpServerExchange;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class AcceptTermsHandler extends BasicHttpHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(AcceptTermsHandler.class);
public static final String PATH = "/_matrix/identity/v2/terms";
private final AccountManager accountManager;
public AcceptTermsHandler(AccountManager accountManager) {
this.accountManager = accountManager;
}
@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
String token = getAccessToken(exchange);
JsonObject request = parseJsonObject(exchange);
JsonElement accepts = request.get("user_accepts");
AccountDao account = accountManager.findAccount(token);
if (account == null) {
throw new InvalidCredentialsException();
}
if (accepts == null || accepts.isJsonNull()) {
respondJson(exchange, "{}");
return;
}
if (accepts.isJsonArray()) {
for (JsonElement acceptItem : accepts.getAsJsonArray()) {
String termUrl = acceptItem.getAsString();
LOGGER.info("User {} accepts the term: {}", account.getUserId(), termUrl);
accountManager.acceptTerm(token, termUrl);
}
} else {
String termUrl = accepts.getAsString();
LOGGER.info("User {} accepts the term: {}", account.getUserId(), termUrl);
accountManager.acceptTerm(token, termUrl);
}
respondJson(exchange, "{}");
}
}

View File

@@ -0,0 +1,37 @@
package io.kamax.mxisd.http.undertow.handler.term.v2;
import com.google.gson.JsonObject;
import io.kamax.mxisd.config.PolicyConfig;
import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler;
import io.undertow.server.HttpServerExchange;
import java.util.Map;
public class GetTermsHandler extends BasicHttpHandler {
public static final String PATH = "/_matrix/identity/v2/terms";
private final JsonObject policyResponse;
public GetTermsHandler(PolicyConfig config) {
policyResponse = new JsonObject();
JsonObject policies = new JsonObject();
for (Map.Entry<String, PolicyConfig.PolicyObject> policyItem : config.getPolicies().entrySet()) {
JsonObject policy = new JsonObject();
policy.addProperty("version", policyItem.getValue().getVersion());
for (Map.Entry<String, PolicyConfig.TermObject> termEntry : policyItem.getValue().getTerms().entrySet()) {
JsonObject term = new JsonObject();
term.addProperty("name", termEntry.getValue().getName());
term.addProperty("url", termEntry.getValue().getUrl());
policy.add(termEntry.getKey(), term);
}
policies.add(policyItem.getKey(), policy);
}
policyResponse.add("policies", policies);
}
@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
respond(exchange, policyResponse);
}
}

View File

@@ -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();
} }

View File

@@ -0,0 +1,16 @@
package io.kamax.mxisd.lookup;
import java.util.List;
public class HashLookupRequest extends ALookupRequest {
private List<String> hashes;
public List<String> getHashes() {
return hashes;
}
public void setHashes(List<String> hashes) {
this.hashes = hashes;
}
}

View File

@@ -24,6 +24,7 @@ import io.kamax.mxisd.lookup.SingleLookupReply;
import io.kamax.mxisd.lookup.SingleLookupRequest; import io.kamax.mxisd.lookup.SingleLookupRequest;
import io.kamax.mxisd.lookup.ThreePidMapping; import io.kamax.mxisd.lookup.ThreePidMapping;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@@ -40,4 +41,7 @@ public interface IThreePidProvider {
List<ThreePidMapping> populate(List<ThreePidMapping> mappings); List<ThreePidMapping> populate(List<ThreePidMapping> mappings);
default Iterable<ThreePidMapping> populateHashes() {
return Collections.emptyList();
}
} }

View File

@@ -21,6 +21,7 @@
package io.kamax.mxisd.lookup.strategy; package io.kamax.mxisd.lookup.strategy;
import io.kamax.mxisd.lookup.BulkLookupRequest; import io.kamax.mxisd.lookup.BulkLookupRequest;
import io.kamax.mxisd.lookup.HashLookupRequest;
import io.kamax.mxisd.lookup.SingleLookupReply; import io.kamax.mxisd.lookup.SingleLookupReply;
import io.kamax.mxisd.lookup.SingleLookupRequest; import io.kamax.mxisd.lookup.SingleLookupRequest;
import io.kamax.mxisd.lookup.ThreePidMapping; import io.kamax.mxisd.lookup.ThreePidMapping;
@@ -46,4 +47,5 @@ public interface LookupStrategy {
CompletableFuture<List<ThreePidMapping>> find(BulkLookupRequest requests); CompletableFuture<List<ThreePidMapping>> find(BulkLookupRequest requests);
CompletableFuture<List<ThreePidMapping>> find(HashLookupRequest request);
} }

View File

@@ -25,10 +25,13 @@ import io.kamax.matrix.json.GsonUtil;
import io.kamax.matrix.json.MatrixJson; import io.kamax.matrix.json.MatrixJson;
import io.kamax.mxisd.config.MxisdConfig; import io.kamax.mxisd.config.MxisdConfig;
import io.kamax.mxisd.exception.ConfigurationException; import io.kamax.mxisd.exception.ConfigurationException;
import io.kamax.mxisd.hash.HashManager;
import io.kamax.mxisd.hash.storage.HashStorage;
import io.kamax.mxisd.lookup.*; import io.kamax.mxisd.lookup.*;
import io.kamax.mxisd.lookup.fetcher.IBridgeFetcher; import io.kamax.mxisd.lookup.fetcher.IBridgeFetcher;
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.codec.digest.DigestUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -50,10 +53,14 @@ public class RecursivePriorityLookupStrategy implements LookupStrategy {
private List<CIDRUtils> allowedCidr = new ArrayList<>(); private List<CIDRUtils> allowedCidr = new ArrayList<>();
public RecursivePriorityLookupStrategy(MxisdConfig.Lookup cfg, List<? extends IThreePidProvider> providers, IBridgeFetcher bridge) { private HashManager hashManager;
public RecursivePriorityLookupStrategy(MxisdConfig.Lookup cfg, List<? extends IThreePidProvider> providers, IBridgeFetcher bridge,
HashManager hashManager) {
this.cfg = cfg; this.cfg = cfg;
this.bridge = bridge; this.bridge = bridge;
this.providers = new ArrayList<>(providers); this.providers = new ArrayList<>(providers);
this.hashManager = hashManager;
try { try {
log.info("Found {} providers", providers.size()); log.info("Found {} providers", providers.size());
@@ -65,6 +72,8 @@ public class RecursivePriorityLookupStrategy implements LookupStrategy {
log.info("{} is allowed for recursion", cidr); log.info("{} is allowed for recursion", cidr);
allowedCidr.add(new CIDRUtils(cidr)); allowedCidr.add(new CIDRUtils(cidr));
} }
log.info("Hash lookups enabled: {}", hashManager.getConfig().isEnabled());
} catch (UnknownHostException e) { } catch (UnknownHostException e) {
throw new ConfigurationException("lookup.recursive.allowedCidrs", "Allowed CIDRs"); throw new ConfigurationException("lookup.recursive.allowedCidrs", "Allowed CIDRs");
} }
@@ -154,20 +163,20 @@ public class RecursivePriorityLookupStrategy implements LookupStrategy {
Optional<SingleLookupReply> lookupDataOpt = provider.find(request); Optional<SingleLookupReply> lookupDataOpt = provider.find(request);
if (lookupDataOpt.isPresent()) { if (lookupDataOpt.isPresent()) {
log.info("Found 3PID mapping: {medium: '{}', address: '{}', mxid: '{}'}", log.info("Found 3PID mapping: {medium: '{}', address: '{}', mxid: '{}'}",
request.getType(), request.getThreePid(), lookupDataOpt.get().getMxid().getId()); request.getType(), request.getThreePid(), lookupDataOpt.get().getMxid().getId());
return lookupDataOpt; return lookupDataOpt;
} }
} }
if ( if (
cfg.getRecursive().getBridge() != null && cfg.getRecursive().getBridge() != null &&
cfg.getRecursive().getBridge().getEnabled() && cfg.getRecursive().getBridge().getEnabled() &&
(!cfg.getRecursive().getBridge().getRecursiveOnly() || isAllowedForRecursive(request.getRequester())) (!cfg.getRecursive().getBridge().getRecursiveOnly() || isAllowedForRecursive(request.getRequester()))
) { ) {
log.info("Using bridge failover for lookup"); log.info("Using bridge failover for lookup");
Optional<SingleLookupReply> lookupDataOpt = bridge.find(request); Optional<SingleLookupReply> lookupDataOpt = bridge.find(request);
log.info("Found 3PID mapping: {medium: '{}', address: '{}', mxid: '{}'}", log.info("Found 3PID mapping: {medium: '{}', address: '{}', mxid: '{}'}",
request.getThreePid(), request.getId(), lookupDataOpt.get().getMxid().getId()); request.getThreePid(), request.getId(), lookupDataOpt.get().getMxid().getId());
return lookupDataOpt; return lookupDataOpt;
} }
@@ -230,4 +239,12 @@ public class RecursivePriorityLookupStrategy implements LookupStrategy {
return bulkLookupInProgress.remove(payloadId); return bulkLookupInProgress.remove(payloadId);
} }
@Override
public CompletableFuture<List<ThreePidMapping>> find(HashLookupRequest request) {
HashStorage hashStorage = hashManager.getHashStorage();
CompletableFuture<List<ThreePidMapping>> result = new CompletableFuture<>();
result.complete(hashStorage.find(request.getHashes()).stream().map(Pair::getValue).collect(Collectors.toList()));
hashManager.getRotationStrategy().newRequest();
return result;
}
} }

View File

@@ -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;
}
}
} }

View 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);
}
}
}

View File

@@ -0,0 +1,9 @@
package io.kamax.mxisd.matrix;
public enum IdentityServiceAPI {
@Deprecated
V1,
V2
}

View File

@@ -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,10 +54,13 @@ import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.IOException; import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URI; import java.net.URI;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Base64; import java.util.Base64;
@@ -71,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(
@@ -79,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;
} }
@@ -240,12 +241,26 @@ public class SessionManager {
} }
} }
private String getDomain(String publicUrl) {
URL url;
try {
url = new URL(publicUrl);
} catch (MalformedURLException e) {
log.error("Malformed public url, use as is");
return publicUrl;
}
int port = url.getPort();
return url.getHost() + (port != -1 ? ":" + url.getPort() : "");
}
private void checkAuthorization(String auth, JsonObject reqData) { private void checkAuthorization(String auth, JsonObject reqData) {
if (!auth.startsWith("X-Matrix ")) { if (!auth.startsWith("X-Matrix ")) {
throw new NotAllowedException("Wrong authorization header"); throw new NotAllowedException("Wrong authorization header");
} }
if (StringUtils.isBlank(cfg.getServer().getPublicUrl())) { String domain = getDomain(cfg.getServer().getPublicUrl());
if (StringUtils.isBlank(domain)) {
throw new NotAllowedException("Unable to verify request, missing `server.publicUrl` property"); throw new NotAllowedException("Unable to verify request, missing `server.publicUrl` property");
} }
@@ -287,30 +302,39 @@ public class SessionManager {
jsonObject.addProperty("method", "POST"); jsonObject.addProperty("method", "POST");
jsonObject.addProperty("uri", "/_matrix/identity/api/v1/3pid/unbind"); jsonObject.addProperty("uri", "/_matrix/identity/api/v1/3pid/unbind");
jsonObject.addProperty("origin", origin); jsonObject.addProperty("origin", origin);
jsonObject.addProperty("destination_is", URI.create(cfg.getServer().getPublicUrl()).getHost()); jsonObject.addProperty("destination_is", domain);
jsonObject.add("content", reqData); jsonObject.add("content", reqData);
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;

View File

@@ -21,13 +21,18 @@
package io.kamax.mxisd.storage; package io.kamax.mxisd.storage;
import io.kamax.matrix.ThreePid; import io.kamax.matrix.ThreePid;
import io.kamax.mxisd.config.PolicyConfig;
import io.kamax.mxisd.invitation.IThreePidInviteReply; import io.kamax.mxisd.invitation.IThreePidInviteReply;
import io.kamax.mxisd.lookup.ThreePidMapping;
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.ThreePidInviteIO; import io.kamax.mxisd.storage.ormlite.dao.ThreePidInviteIO;
import org.apache.commons.lang3.tuple.Pair;
import java.time.Instant; import java.time.Instant;
import java.util.Collection; import java.util.Collection;
import java.util.List;
import java.util.Optional; import java.util.Optional;
public interface IStorage { public interface IStorage {
@@ -52,4 +57,21 @@ public interface IStorage {
Optional<ASTransactionDao> getTransactionResult(String localpart, String txnId); Optional<ASTransactionDao> getTransactionResult(String localpart, String txnId);
void insertToken(AccountDao accountDao);
Optional<AccountDao> findAccount(String token);
void deleteToken(String token);
void acceptTerm(String token, String url);
void deleteAccepts(String token);
boolean isTermAccepted(String token, List<PolicyConfig.PolicyObject> policies);
void clearHashes();
void addHash(String mxid, String medium, String address, String hash);
Collection<Pair<String, ThreePidMapping>> findHashes(Iterable<String> hashes);
} }

View File

@@ -24,28 +24,47 @@ import com.j256.ormlite.dao.CloseableWrappedIterable;
import com.j256.ormlite.dao.Dao; import com.j256.ormlite.dao.Dao;
import com.j256.ormlite.dao.DaoManager; import com.j256.ormlite.dao.DaoManager;
import com.j256.ormlite.jdbc.JdbcConnectionSource; import com.j256.ormlite.jdbc.JdbcConnectionSource;
import com.j256.ormlite.stmt.QueryBuilder;
import com.j256.ormlite.support.ConnectionSource; import com.j256.ormlite.support.ConnectionSource;
import com.j256.ormlite.table.TableUtils; import com.j256.ormlite.table.TableUtils;
import io.kamax.matrix.ThreePid; import io.kamax.matrix.ThreePid;
import io.kamax.mxisd.config.MxisdConfig; import io.kamax.mxisd.config.MxisdConfig;
import io.kamax.mxisd.config.PolicyConfig;
import io.kamax.mxisd.exception.ConfigurationException; import io.kamax.mxisd.exception.ConfigurationException;
import io.kamax.mxisd.exception.InternalServerError; import io.kamax.mxisd.exception.InternalServerError;
import io.kamax.mxisd.exception.InvalidCredentialsException;
import io.kamax.mxisd.invitation.IThreePidInviteReply; import io.kamax.mxisd.invitation.IThreePidInviteReply;
import io.kamax.mxisd.lookup.ThreePidMapping;
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;
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.ChangelogDao;
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.ThreePidInviteIO; 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.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.*; import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
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> {
@@ -60,10 +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<AcceptedDao, Long> acceptedDao;
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());
@@ -80,14 +107,34 @@ 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);
asTxnDao = createDaoAndTable(connPool, ASTransactionDao.class); asTxnDao = createDaoAndTable(connPool, ASTransactionDao.class);
accountDao = createDaoAndTable(connPool, AccountDao.class);
acceptedDao = createDaoAndTable(connPool, AcceptedDao.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;
@@ -175,7 +222,7 @@ public class OrmLiteSqlStorage implements IStorage {
List<ThreePidSessionDao> daoList = sessionDao.queryForMatchingArgs(new ThreePidSessionDao(tpid, secret)); List<ThreePidSessionDao> daoList = sessionDao.queryForMatchingArgs(new ThreePidSessionDao(tpid, secret));
if (daoList.size() > 1) { if (daoList.size() > 1) {
throw new InternalServerError("Lookup for 3PID Session " + throw new InternalServerError("Lookup for 3PID Session " +
tpid + " returned more than one result"); tpid + " returned more than one result");
} }
if (daoList.isEmpty()) { if (daoList.isEmpty()) {
@@ -226,7 +273,7 @@ public class OrmLiteSqlStorage implements IStorage {
if (daoList.size() > 1) { if (daoList.size() > 1) {
throw new InternalServerError("Lookup for Transaction " + throw new InternalServerError("Lookup for Transaction " +
txnId + " for localpart " + localpart + " returned more than one result"); txnId + " for localpart " + localpart + " returned more than one result");
} }
if (daoList.isEmpty()) { if (daoList.isEmpty()) {
@@ -237,4 +284,110 @@ public class OrmLiteSqlStorage implements IStorage {
}); });
} }
@Override
public void insertToken(AccountDao account) {
withCatcher(() -> {
int created = accountDao.create(account);
if (created != 1) {
throw new RuntimeException("Unexpected row count after DB action: " + created);
}
});
}
@Override
public Optional<AccountDao> findAccount(String token) {
return withCatcher(() -> {
List<AccountDao> accounts = accountDao.queryForEq("token", token);
if (accounts.isEmpty()) {
return Optional.empty();
}
if (accounts.size() != 1) {
throw new RuntimeException("Unexpected rows for access token: " + accounts.size());
}
return Optional.of(accounts.get(0));
});
}
@Override
public void deleteToken(String token) {
withCatcher(() -> {
int updated = accountDao.deleteById(token);
if (updated != 1) {
throw new RuntimeException("Unexpected row count after DB action: " + updated);
}
});
}
@Override
public void acceptTerm(String token, String url) {
withCatcher(() -> {
AccountDao account = findAccount(token).orElseThrow(InvalidCredentialsException::new);
List<AcceptedDao> acceptedTerms = acceptedDao.queryForEq("userId", account.getUserId());
for (AcceptedDao acceptedTerm : acceptedTerms) {
if (acceptedTerm.getUrl().equalsIgnoreCase(url)) {
// already accepted
return;
}
}
int created = acceptedDao.create(new AcceptedDao(url, account.getUserId(), System.currentTimeMillis()));
if (created != 1) {
throw new RuntimeException("Unexpected row count after DB action: " + created);
}
});
}
@Override
public void deleteAccepts(String token) {
withCatcher(() -> {
AccountDao account = findAccount(token).orElseThrow(InvalidCredentialsException::new);
acceptedDao.delete(acceptedDao.queryForEq("userId", account.getUserId()));
});
}
@Override
public boolean isTermAccepted(String token, List<PolicyConfig.PolicyObject> policies) {
return withCatcher(() -> {
AccountDao account = findAccount(token).orElseThrow(InvalidCredentialsException::new);
List<AcceptedDao> acceptedTerms = acceptedDao.queryForEq("userId", account.getUserId());
for (AcceptedDao acceptedTerm : acceptedTerms) {
for (PolicyConfig.PolicyObject policy : policies) {
for (PolicyConfig.TermObject termObject : policy.getTerms().values()) {
if (termObject.getUrl().equalsIgnoreCase(acceptedTerm.getUrl())) {
return true;
}
}
}
}
return false;
});
}
@Override
public void clearHashes() {
withCatcher(() -> {
List<HashDao> allHashes = hashDao.queryForAll();
int deleted = hashDao.delete(allHashes);
if (deleted != allHashes.size()) {
throw new RuntimeException("Not all hashes deleted: " + deleted);
}
});
}
@Override
public void addHash(String mxid, String medium, String address, String hash) {
withCatcher(() -> {
hashDao.create(new HashDao(mxid, medium, address, hash));
});
}
@Override
public Collection<Pair<String, ThreePidMapping>> findHashes(Iterable<String> hashes) {
return withCatcher(() -> {
QueryBuilder<HashDao, String> builder = hashDao.queryBuilder();
builder.where().in("hash", hashes);
return hashDao.query(builder.prepare()).stream()
.map(dao -> Pair.of(dao.getHash(), new ThreePidMapping(dao.getMedium(), dao.getAddress(), dao.getMxid()))).collect(
Collectors.toList());
});
}
} }

View File

@@ -0,0 +1,82 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2018 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.mxisd.storage.ormlite.dao;
import com.j256.ormlite.field.DatabaseField;
import com.j256.ormlite.table.DatabaseTable;
@DatabaseTable(tableName = "accepted")
public class AcceptedDao {
@DatabaseField(generatedId = true)
private Long id;
@DatabaseField(canBeNull = false)
private String url;
@DatabaseField(canBeNull = false)
private String userId;
@DatabaseField(canBeNull = false)
private long acceptedAt;
public AcceptedDao() {
// Needed for ORMLite
}
public AcceptedDao(String url, String userId, long acceptedAt) {
this.url = url;
this.userId = userId;
this.acceptedAt = acceptedAt;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public long getAcceptedAt() {
return acceptedAt;
}
public void setAcceptedAt(long acceptedAt) {
this.acceptedAt = acceptedAt;
}
}

View File

@@ -0,0 +1,119 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2018 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.mxisd.storage.ormlite.dao;
import com.j256.ormlite.field.DatabaseField;
import com.j256.ormlite.table.DatabaseTable;
@DatabaseTable(tableName = "account")
public class AccountDao {
@DatabaseField(canBeNull = false, id = true)
private String token;
@DatabaseField(canBeNull = false)
private String accessToken;
@DatabaseField(canBeNull = false)
private String tokenType;
@DatabaseField(canBeNull = false)
private String matrixServerName;
@DatabaseField(canBeNull = false)
private long expiresIn;
@DatabaseField(canBeNull = false)
private long createdAt;
@DatabaseField(canBeNull = false)
private String userId;
public AccountDao() {
// Needed for ORMLite
}
public AccountDao(String accessToken, String tokenType, String matrixServerName, long expiresIn, long createdAt, String userId, String token) {
this.accessToken = accessToken;
this.tokenType = tokenType;
this.matrixServerName = matrixServerName;
this.expiresIn = expiresIn;
this.createdAt = createdAt;
this.userId = userId;
this.token = token;
}
public String getAccessToken() {
return accessToken;
}
public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}
public String getTokenType() {
return tokenType;
}
public void setTokenType(String tokenType) {
this.tokenType = tokenType;
}
public String getMatrixServerName() {
return matrixServerName;
}
public void setMatrixServerName(String matrixServerName) {
this.matrixServerName = matrixServerName;
}
public long getExpiresIn() {
return expiresIn;
}
public void setExpiresIn(long expiresIn) {
this.expiresIn = expiresIn;
}
public long getCreatedAt() {
return createdAt;
}
public void setCreatedAt(long createdAt) {
this.createdAt = createdAt;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,62 @@
package io.kamax.mxisd.storage.ormlite.dao;
import com.j256.ormlite.field.DatabaseField;
import com.j256.ormlite.table.DatabaseTable;
@DatabaseTable(tableName = "hashes")
public class HashDao {
@DatabaseField(canBeNull = false, id = true)
private String mxid;
@DatabaseField(canBeNull = false)
private String medium;
@DatabaseField(canBeNull = false)
private String address;
@DatabaseField(canBeNull = false)
private String hash;
public HashDao() {
}
public HashDao(String mxid, String medium, String address, String hash) {
this.mxid = mxid;
this.medium = medium;
this.address = address;
this.hash = hash;
}
public String getMxid() {
return mxid;
}
public void setMxid(String mxid) {
this.mxid = mxid;
}
public String getMedium() {
return medium;
}
public void setMedium(String medium) {
this.medium = medium;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String getHash() {
return hash;
}
public void setHash(String hash) {
this.hash = hash;
}
}

View File

@@ -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"));
}
}

View 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")));
}
}

View File

@@ -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());
} }