diff --git a/.gitignore b/.gitignore index 59aba00..93fc794 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,7 @@ out/ .idea/ # Local dev config -application.yaml +/application.yaml + +# Local dev storage +/mxisd.db diff --git a/application.example.yaml b/application.example.yaml index 2f54ce5..8b0a46c 100644 --- a/application.example.yaml +++ b/application.example.yaml @@ -12,6 +12,12 @@ server: # e.g. domain name in e-mails. name: 'example.org' + # Public URL to reach this identity server + # + # This is used with 3PID invites in room and other Homeserver key verification workflow. + # If left unconfigured, it will be generated from the server name + # publicUrl: 'https://example.org' + key: @@ -125,7 +131,7 @@ ldap: baseDn: 'CN=Users,DC=example,DC=org' # How to map Matrix attributes with LDAP attributes when performing lookup/auth - attributes: + attribute: # The username/login that will be looked up or used to build Matrix IDs uid: @@ -190,3 +196,86 @@ forward: servers: - "https://matrix.org" - "https://vector.im" + + + +# Configure the invite components +invite: + + # Configure invite senders for the various 3PID type + sender: + + # E-mail invite sender + email: + + # SMTP host + host: "smtp.example.org" + + # SMTP port + port: 587 + + # TLS mode for the connection. + # + # Possible values: + # 0 Disable TLS entirely + # 1 Enable TLS if supported by server + # 2 Force TLS and fail if not available + tls: 1 + + # Login for SMTP + login: "matrix-identity@example.org" + + # Password for the account + password: "ThePassword" + + # The e-mail to send as. If empty, will be the same as login + email: "matrix-identity@example.org" + + # The display name used in the e-mail + name: "Matrix Identity" + + # The E-mail template to use. + # + # The template is expected to be a full e-mail body, including client headers, using MIME and UTF-8 encoding. + # The following headers will be set by mxisd directly and should not be present in the template: + # - From + # - To + # - Date + # - Message-Id + # - X-Mailer + # + # The following placeholders are available: + # - %DOMAIN% Domain name as per server.name config item + # - %DOMAIN_PRETTY% Word capitalize version of the domain. e.g. example.org -> Example.org + # - %FROM_EMAIL% Value of this section's email config item + # - %FROM_NAME% Value of this section's name config item + # - %SENDER_ID% Matrix ID of the invitation sender + # - %SENDER_NAME% Display name of the invitation sender, empty if not available + # - %SENDER_NAME_OR_ID% Value of %SENDER_NAME% or, if empty, value of %SENDER_ID% + # - %INVITE_MEDIUM% Medium of the invite (e.g. email, msisdn) + # - %INVITE_ADDRESS% Address used to invite + # - %ROOM_ID% ID of the room where the invitation took place + # - %ROOM_NAME% Name of the room, empty if not available + # - %ROOM_NAME_OR_ID% Value of %ROOM_NAME% or, if empty, value of %ROOM_ID% + template: "/absolute/path/to/file" + + + +# Configure persistence settings +storage: + + # Configure the storage backend, usually a DB + # Possible built-in values: + # sqlite SQLite backend, default + # + #backend: 'sqlite' + + # Specific configuration for each provider, refer to their documentation for specifics. + provider: + + # Generic SQLite provider config + sqlite: + + # Path to the SQLite DB file, required + # + #database:'%SQLITE_DATABASE_PATH%' diff --git a/build.gradle b/build.gradle index df0d430..2a53882 100644 --- a/build.gradle +++ b/build.gradle @@ -97,9 +97,19 @@ dependencies { // Phone numbers validation compile 'com.googlecode.libphonenumber:libphonenumber:8.7.1' + // E-mail sending + compile 'com.sun.mail:javax.mail:1.5.6' + compile 'javax.mail:javax.mail-api:1.5.6' + // Google Firebase Authentication backend compile 'com.google.firebase:firebase-admin:5.3.0' + // ORMLite + compile 'com.j256.ormlite:ormlite-jdbc:5.0' + + // SQLite + compile 'org.xerial:sqlite-jdbc:3.20.0' + testCompile 'junit:junit:4.12' } @@ -111,6 +121,17 @@ springBoot { ] } +processResources { + doLast { + copy { + from('build/resources/main/application.yaml') { + rename 'application.yaml', 'mxisd.yaml' + } + into 'build/resources/main' + } + } +} + task buildDeb(dependsOn: build) { doLast { def v = gitVersion() @@ -146,6 +167,12 @@ task buildDeb(dependsOn: build) { value: "${debDataPath}/signing.key" ) + ant.replaceregexp( + file: "${debBuildConfPath}/${debConfFileName}", + match: "#?database:\\s*'%SQLITE_DATABASE_PATH%'", + replace: "database: '${debDataPath}/mxisd.db'" + ) + copy { from project.file('src/debian') into debBuildDebianPath diff --git a/media/mx-id-icon-reverse.png b/media/mx-id-icon-reverse.png new file mode 100644 index 0000000..79d6516 Binary files /dev/null and b/media/mx-id-icon-reverse.png differ diff --git a/media/mx-id-icon.png b/media/mx-id-icon.png new file mode 100644 index 0000000..b279610 Binary files /dev/null and b/media/mx-id-icon.png differ diff --git a/src/main/groovy/io/kamax/mxisd/GlobalProvider.java b/src/main/groovy/io/kamax/mxisd/GlobalProvider.java deleted file mode 100644 index 9e68b5d..0000000 --- a/src/main/groovy/io/kamax/mxisd/GlobalProvider.java +++ /dev/null @@ -1,7 +0,0 @@ -package io.kamax.mxisd; - -import io.kamax.mxisd.auth.provider.AuthenticatorProvider; -import io.kamax.mxisd.lookup.provider.IThreePidProvider; - -public interface GlobalProvider extends AuthenticatorProvider, IThreePidProvider { -} diff --git a/src/main/groovy/io/kamax/mxisd/ThreePid.java b/src/main/groovy/io/kamax/mxisd/ThreePid.java new file mode 100644 index 0000000..d51ecd6 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/ThreePid.java @@ -0,0 +1,47 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.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 . + */ + +package io.kamax.mxisd; + +// FIXME this should be in matrix-java-sdk +public class ThreePid { + + private String medium; + private String address; + + public ThreePid(String medium, String address) { + this.medium = medium; + this.address = address; + } + + public String getMedium() { + return medium; + } + + public String getAddress() { + return address; + } + + @Override + public String toString() { + return getMedium() + ":" + getAddress(); + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/auth/AuthManager.java b/src/main/groovy/io/kamax/mxisd/auth/AuthManager.java index bb6e367..6dbfff2 100644 --- a/src/main/groovy/io/kamax/mxisd/auth/AuthManager.java +++ b/src/main/groovy/io/kamax/mxisd/auth/AuthManager.java @@ -1,6 +1,29 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.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 . + */ + package io.kamax.mxisd.auth; +import io.kamax.mxisd.ThreePid; import io.kamax.mxisd.auth.provider.AuthenticatorProvider; +import io.kamax.mxisd.invitation.InvitationManager; +import io.kamax.mxisd.lookup.ThreePidMapping; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -17,6 +40,9 @@ public class AuthManager { @Autowired private List providers = new ArrayList<>(); + @Autowired + private InvitationManager invMgr; + public UserAuthResult authenticate(String id, String password) { for (AuthenticatorProvider provider : providers) { if (!provider.isEnabled()) { @@ -25,6 +51,14 @@ public class AuthManager { UserAuthResult result = provider.authenticate(id, password); if (result.isSuccess()) { + log.info("{} was authenticated by {}, publishing 3PID mappings, if any", id, provider.getClass().getSimpleName()); + for (ThreePid pid : result.getThreePids()) { + log.info("Processing {} for {}", pid, id); + invMgr.publishMappingIfInvited(new ThreePidMapping(pid, result.getMxid())); + } + + invMgr.lookupMappingsForInvites(); + return result; } } diff --git a/src/main/groovy/io/kamax/mxisd/auth/UserAuthResult.java b/src/main/groovy/io/kamax/mxisd/auth/UserAuthResult.java index 16e1cf6..f51f2bc 100644 --- a/src/main/groovy/io/kamax/mxisd/auth/UserAuthResult.java +++ b/src/main/groovy/io/kamax/mxisd/auth/UserAuthResult.java @@ -1,10 +1,38 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.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 . + */ + package io.kamax.mxisd.auth; +import io.kamax.matrix.ThreePidMedium; +import io.kamax.mxisd.ThreePid; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + public class UserAuthResult { private boolean success; private String mxid; private String displayName; + private List threePids = new ArrayList<>(); public UserAuthResult failure() { success = false; @@ -46,4 +74,18 @@ public class UserAuthResult { this.displayName = displayName; } + public UserAuthResult withThreePid(ThreePidMedium medium, String address) { + return withThreePid(medium.getId(), address); + } + + public UserAuthResult withThreePid(String medium, String address) { + threePids.add(new ThreePid(medium, address)); + + return this; + } + + public List getThreePids() { + return Collections.unmodifiableList(threePids); + } + } diff --git a/src/main/groovy/io/kamax/mxisd/auth/provider/AuthenticatorProvider.java b/src/main/groovy/io/kamax/mxisd/auth/provider/AuthenticatorProvider.java index bdcf72a..f9aefb6 100644 --- a/src/main/groovy/io/kamax/mxisd/auth/provider/AuthenticatorProvider.java +++ b/src/main/groovy/io/kamax/mxisd/auth/provider/AuthenticatorProvider.java @@ -1,3 +1,23 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.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 . + */ + package io.kamax.mxisd.auth.provider; import io.kamax.mxisd.auth.UserAuthResult; diff --git a/src/main/groovy/io/kamax/mxisd/auth/provider/GoogleFirebaseAuthenticator.groovy b/src/main/groovy/io/kamax/mxisd/auth/provider/GoogleFirebaseAuthenticator.groovy index 0d00c08..7d57cce 100644 --- a/src/main/groovy/io/kamax/mxisd/auth/provider/GoogleFirebaseAuthenticator.groovy +++ b/src/main/groovy/io/kamax/mxisd/auth/provider/GoogleFirebaseAuthenticator.groovy @@ -1,3 +1,23 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.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 . + */ + package io.kamax.mxisd.auth.provider import com.google.firebase.FirebaseApp @@ -7,31 +27,36 @@ import com.google.firebase.internal.NonNull import com.google.firebase.tasks.OnFailureListener import com.google.firebase.tasks.OnSuccessListener import io.kamax.matrix.ThreePidMedium -import io.kamax.mxisd.GlobalProvider import io.kamax.mxisd.auth.UserAuthResult -import io.kamax.mxisd.lookup.SingleLookupRequest -import io.kamax.mxisd.lookup.ThreePidMapping import org.apache.commons.lang.StringUtils import org.slf4j.Logger import org.slf4j.LoggerFactory import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit -import java.util.function.Consumer import java.util.regex.Matcher import java.util.regex.Pattern -public class GoogleFirebaseAuthenticator implements GlobalProvider { +public class GoogleFirebaseAuthenticator implements AuthenticatorProvider { private Logger log = LoggerFactory.getLogger(GoogleFirebaseAuthenticator.class); - private static final Pattern matrixIdLaxPattern = Pattern.compile("@(.*):(.+)"); + private static final Pattern matrixIdLaxPattern = Pattern.compile("@(.*):(.+)"); // FIXME use matrix-java-sdk private boolean isEnabled; private String domain; private FirebaseApp fbApp; private FirebaseAuth fbAuth; + private void waitOnLatch(UserAuthResult result, CountDownLatch l, long timeout, TimeUnit unit, String purpose) { + try { + l.await(timeout, unit); + } catch (InterruptedException e) { + log.warn("Interrupted while waiting for " + purpose); + result.failure(); + } + } + public GoogleFirebaseAuthenticator(boolean isEnabled) { this.isEnabled = isEnabled; } @@ -40,7 +65,7 @@ public class GoogleFirebaseAuthenticator implements GlobalProvider { this(true); this.domain = domain; try { - fbApp = FirebaseApp.initializeApp(getOpts(credsPath, db)); + fbApp = FirebaseApp.initializeApp(getOpts(credsPath, db), "AuthenticationProvider"); fbAuth = FirebaseAuth.getInstance(fbApp); log.info("Google Firebase Authentication is ready"); @@ -73,16 +98,6 @@ public class GoogleFirebaseAuthenticator implements GlobalProvider { return isEnabled; } - @Override - public boolean isLocal() { - return true; - } - - @Override - public int getPriority() { - return 25; - } - private void waitOnLatch(CountDownLatch l) { try { l.await(30, TimeUnit.SECONDS); @@ -91,84 +106,6 @@ public class GoogleFirebaseAuthenticator implements GlobalProvider { } } - private Optional findInternal(String medium, String address) { - UserRecord r; - CountDownLatch l = new CountDownLatch(1); - - OnSuccessListener success = new OnSuccessListener() { - @Override - void onSuccess(UserRecord result) { - log.info("Found 3PID match for {}:{} - UID is {}", medium, address, result.getUid()) - r = result; - l.countDown() - } - }; - - OnFailureListener failure = new OnFailureListener() { - @Override - void onFailure(@NonNull Exception e) { - log.info("No 3PID match for {}:{} - {}", medium, address, e.getMessage()) - r = null; - l.countDown() - } - }; - - if (ThreePidMedium.Email.is(medium)) { - log.info("Performing E-mail 3PID lookup for {}", address) - fbAuth.getUserByEmail(address) - .addOnSuccessListener(success) - .addOnFailureListener(failure); - waitOnLatch(l); - } else if (ThreePidMedium.PhoneNumber.is(medium)) { - log.info("Performing msisdn 3PID lookup for {}", address) - fbAuth.getUserByPhoneNumber(address) - .addOnSuccessListener(success) - .addOnFailureListener(failure); - waitOnLatch(l); - } else { - log.info("{} is not a supported 3PID medium", medium); - r = null; - } - - return Optional.ofNullable(r); - } - - @Override - public Optional find(SingleLookupRequest request) { - Optional urOpt = findInternal(request.getType(), request.getThreePid()) - if (urOpt.isPresent()) { - return [ - address : request.getThreePid(), - medium : request.getType(), - mxid : "@${urOpt.get().getUid()}:${domain}", - not_before: 0, - not_after : 9223372036854775807, - ts : 0 - ] - } else { - return Optional.empty(); - } - } - - @Override - public List populate(List mappings) { - List results = new ArrayList<>(); - mappings.parallelStream().forEach(new Consumer() { - @Override - void accept(ThreePidMapping o) { - Optional urOpt = findInternal(o.getMedium(), o.getValue()); - if (urOpt.isPresent()) { - ThreePidMapping result = new ThreePidMapping(); - result.setMedium(o.getMedium()) - result.setValue(o.getValue()) - result.setMxid("@${urOpt.get().getUid()}:${domain}") - results.add(result) - } - } - }); - return results; - } - @Override public UserAuthResult authenticate(String id, String password) { if (!isEnabled()) { @@ -190,37 +127,69 @@ public class GoogleFirebaseAuthenticator implements GlobalProvider { fbAuth.verifyIdToken(password).addOnSuccessListener(new OnSuccessListener() { @Override void onSuccess(FirebaseToken token) { - if (!StringUtils.equals(localpart, token.getUid())) { - log.info("Failture to authenticate {}: Matrix ID localpart '{}' does not match Firebase UID '{}'", id, localpart, token.getUid()); - result.failure(); - } + try { + if (!StringUtils.equals(localpart, token.getUid())) { + log.info("Failture to authenticate {}: Matrix ID localpart '{}' does not match Firebase UID '{}'", id, localpart, token.getUid()); + result.failure(); + return; + } - log.info("{} was successfully authenticated", id); - result.success(id, token.getName()); - l.countDown() + log.info("{} was successfully authenticated", id); + result.success(id, token.getName()); + + log.info("Fetching profile for {}", id); + CountDownLatch userRecordLatch = new CountDownLatch(1); + fbAuth.getUser(token.getUid()).addOnSuccessListener(new OnSuccessListener() { + @Override + void onSuccess(UserRecord user) { + try { + if (StringUtils.isNotBlank(user.getEmail())) { + result.withThreePid(ThreePidMedium.Email, user.getEmail()); + } + + if (StringUtils.isNotBlank(user.getPhoneNumber())) { + result.withThreePid(ThreePidMedium.PhoneNumber, user.getPhoneNumber()); + } + } finally { + userRecordLatch.countDown(); + } + } + }).addOnFailureListener(new OnFailureListener() { + @Override + void onFailure(@NonNull Exception e) { + try { + log.warn("Unable to fetch Firebase user profile for {}", id); + result.failure(); + } finally { + userRecordLatch.countDown(); + } + } + }); + + waitOnLatch(result, userRecordLatch, 30, TimeUnit.SECONDS, "Firebase user profile"); + } finally { + l.countDown() + } } }).addOnFailureListener(new OnFailureListener() { @Override void onFailure(@NonNull Exception e) { - if (e instanceof IllegalArgumentException) { - log.info("Failure to authenticate {}: invalid firebase token", id); - } else { - log.info("Failure to authenticate {}: {}", id, e.getMessage(), e); - log.info("Exception", e); - } + try { + if (e instanceof IllegalArgumentException) { + log.info("Failure to authenticate {}: invalid firebase token", id); + } else { + log.info("Failure to authenticate {}: {}", id, e.getMessage(), e); + log.info("Exception", e); + } - result.failure(); - l.countDown() + result.failure(); + } finally { + l.countDown() + } } }); - try { - l.await(30, TimeUnit.SECONDS); - } catch (InterruptedException e) { - log.warn("Interrupted while waiting for Firebase auth check"); - result.failure(); - } - + waitOnLatch(result, l, 30, TimeUnit.SECONDS, "Firebase auth check"); return result; } diff --git a/src/main/groovy/io/kamax/mxisd/auth/provider/LdapAuthProvider.java b/src/main/groovy/io/kamax/mxisd/auth/provider/LdapAuthProvider.java index ff2ddc4..6edeaa6 100644 --- a/src/main/groovy/io/kamax/mxisd/auth/provider/LdapAuthProvider.java +++ b/src/main/groovy/io/kamax/mxisd/auth/provider/LdapAuthProvider.java @@ -1,3 +1,23 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.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 . + */ + package io.kamax.mxisd.auth.provider; import io.kamax.matrix.MatrixID; diff --git a/src/main/groovy/io/kamax/mxisd/config/FirebaseConfig.java b/src/main/groovy/io/kamax/mxisd/config/FirebaseConfig.java index 6ad4791..b3799f8 100644 --- a/src/main/groovy/io/kamax/mxisd/config/FirebaseConfig.java +++ b/src/main/groovy/io/kamax/mxisd/config/FirebaseConfig.java @@ -1,7 +1,29 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.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 . + */ + package io.kamax.mxisd.config; -import io.kamax.mxisd.GlobalProvider; +import io.kamax.mxisd.auth.provider.AuthenticatorProvider; import io.kamax.mxisd.auth.provider.GoogleFirebaseAuthenticator; +import io.kamax.mxisd.lookup.provider.GoogleFirebaseProvider; +import io.kamax.mxisd.lookup.provider.IThreePidProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -56,16 +78,24 @@ public class FirebaseConfig { log.info("Credentials: {}", getCredentials()); log.info("Database: {}", getDatabase()); } - - } @Bean - public GlobalProvider getProvider() { + public AuthenticatorProvider getAuthProvider() { if (!enabled) { return new GoogleFirebaseAuthenticator(false); } else { return new GoogleFirebaseAuthenticator(credentials, database, srvCfg.getName()); } } + + @Bean + public IThreePidProvider getLookupProvider() { + if (!enabled) { + return new GoogleFirebaseProvider(false); + } else { + return new GoogleFirebaseProvider(credentials, database, srvCfg.getName()); + } + } + } diff --git a/src/main/groovy/io/kamax/mxisd/config/RecursiveLookupBridgeConfig.groovy b/src/main/groovy/io/kamax/mxisd/config/RecursiveLookupBridgeConfig.groovy index a51e12f..9830376 100644 --- a/src/main/groovy/io/kamax/mxisd/config/RecursiveLookupBridgeConfig.groovy +++ b/src/main/groovy/io/kamax/mxisd/config/RecursiveLookupBridgeConfig.groovy @@ -1,3 +1,23 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.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 . + */ + package io.kamax.mxisd.config import org.slf4j.Logger diff --git a/src/main/groovy/io/kamax/mxisd/config/SQLiteStorageConfig.java b/src/main/groovy/io/kamax/mxisd/config/SQLiteStorageConfig.java new file mode 100644 index 0000000..8ca0680 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/config/SQLiteStorageConfig.java @@ -0,0 +1,40 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.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 . + */ + +package io.kamax.mxisd.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties("storage.provider.sqlite") +public class SQLiteStorageConfig { + + private String database; + + public String getDatabase() { + return database; + } + + public void setDatabase(String database) { + this.database = database; + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/config/ServerConfig.groovy b/src/main/groovy/io/kamax/mxisd/config/ServerConfig.groovy index 3b14e96..d29b4ce 100644 --- a/src/main/groovy/io/kamax/mxisd/config/ServerConfig.groovy +++ b/src/main/groovy/io/kamax/mxisd/config/ServerConfig.groovy @@ -22,6 +22,8 @@ package io.kamax.mxisd.config import io.kamax.mxisd.exception.ConfigurationException import org.apache.commons.lang.StringUtils +import org.slf4j.Logger +import org.slf4j.LoggerFactory import org.springframework.beans.factory.InitializingBean import org.springframework.boot.context.properties.ConfigurationProperties import org.springframework.context.annotation.Configuration @@ -30,7 +32,11 @@ import org.springframework.context.annotation.Configuration @ConfigurationProperties(prefix = "server") class ServerConfig implements InitializingBean { + private Logger log = LoggerFactory.getLogger(ServerConfig.class); + private String name + private int port + private String publicUrl String getName() { return name @@ -40,11 +46,43 @@ class ServerConfig implements InitializingBean { this.name = name } + int getPort() { + return port + } + + void setPort(int port) { + this.port = port + } + + String getPublicUrl() { + return publicUrl + } + + void setPublicUrl(String publicUrl) { + this.publicUrl = publicUrl + } + @Override void afterPropertiesSet() throws Exception { if (StringUtils.isBlank(getName())) { throw new ConfigurationException("server.name") } + + if (StringUtils.isBlank(getPublicUrl())) { + log.warn("Public URL is empty, generating from name {}", getName()) + publicUrl = "https://${getName()}" + } + + try { + new URL(getPublicUrl()) + } catch (MalformedURLException e) { + log.warn("Public URL is not valid: {}", StringUtils.defaultIfBlank(e.getMessage(), "")) + } + + log.info("--- Server config ---") + log.info("Name: {}", getName()) + log.info("Port: {}", getPort()) + log.info("Public URL: {}", getPublicUrl()) } } diff --git a/src/main/groovy/io/kamax/mxisd/config/StorageConfig.java b/src/main/groovy/io/kamax/mxisd/config/StorageConfig.java new file mode 100644 index 0000000..247a424 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/config/StorageConfig.java @@ -0,0 +1,51 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.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 . + */ + +package io.kamax.mxisd.config; + +import io.kamax.mxisd.exception.ConfigurationException; +import org.apache.commons.lang.StringUtils; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import javax.annotation.PostConstruct; + +@Configuration +@ConfigurationProperties("storage") +public class StorageConfig { + + private String backend; + + public String getBackend() { + return backend; + } + + public void setBackend(String backend) { + this.backend = backend; + } + + @PostConstruct + private void postConstruct() { + if (StringUtils.isBlank(getBackend())) { + throw new ConfigurationException("storage.backend"); + } + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/config/invite/sender/EmailSenderConfig.java b/src/main/groovy/io/kamax/mxisd/config/invite/sender/EmailSenderConfig.java new file mode 100644 index 0000000..d6e12a8 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/config/invite/sender/EmailSenderConfig.java @@ -0,0 +1,135 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.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 . + */ + +package io.kamax.mxisd.config.invite.sender; + +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import javax.annotation.PostConstruct; +import java.io.File; + +@Configuration +@ConfigurationProperties(prefix = "invite.sender.email") +public class EmailSenderConfig { + + private Logger log = LoggerFactory.getLogger(EmailSenderConfig.class); + + private String host; + private int port; + private int tls; + private String login; + private String password; + private String email; + private String name; + private String template; + + public String getHost() { + return host; + } + + public void setHost(String host) { + this.host = host; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public int getTls() { + return tls; + } + + public void setTls(int tls) { + this.tls = tls; + } + + public String getLogin() { + return login; + } + + public void setLogin(String login) { + this.login = login; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getTemplate() { + return template; + } + + public void setTemplate(String template) { + this.template = template; + } + + @PostConstruct + private void postConstruct() { + log.info("--- E-mail Invite Sender config ---"); + log.info("Host: {}", getHost()); + log.info("Port: {}", getPort()); + log.info("TLS Mode: {}", getTls()); + log.info("Login: {}", getLogin()); + log.info("Has password: {}", StringUtils.isBlank(getPassword())); + log.info("E-mail: {}", getEmail()); + if (!StringUtils.startsWith(getTemplate(), "classpath:")) { + if (StringUtils.isBlank(getTemplate())) { + log.warn("invite.sender.template is empty! Will not send invites"); + } else { + File cp = new File(getTemplate()).getAbsoluteFile(); + log.info("Template: {}", cp.getAbsolutePath()); + if (!cp.exists() || !cp.isFile() || !cp.canRead()) { + log.warn(getTemplate() + " does not exist, is not a file or cannot be read"); + } + } + } else { + log.info("Template: Built-in"); + } + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/config/ldap/LdapAttributeConfig.java b/src/main/groovy/io/kamax/mxisd/config/ldap/LdapAttributeConfig.java index f8139ee..9ef4324 100644 --- a/src/main/groovy/io/kamax/mxisd/config/ldap/LdapAttributeConfig.java +++ b/src/main/groovy/io/kamax/mxisd/config/ldap/LdapAttributeConfig.java @@ -1,3 +1,23 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.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 . + */ + package io.kamax.mxisd.config.ldap; import org.springframework.boot.context.properties.ConfigurationProperties; diff --git a/src/main/groovy/io/kamax/mxisd/config/ldap/LdapAttributeUidConfig.java b/src/main/groovy/io/kamax/mxisd/config/ldap/LdapAttributeUidConfig.java index a56044a..57547b8 100644 --- a/src/main/groovy/io/kamax/mxisd/config/ldap/LdapAttributeUidConfig.java +++ b/src/main/groovy/io/kamax/mxisd/config/ldap/LdapAttributeUidConfig.java @@ -1,12 +1,28 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.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 . + */ + package io.kamax.mxisd.config.ldap; -import io.kamax.mxisd.lookup.provider.LdapProvider; -import org.apache.commons.lang.StringUtils; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; -import javax.annotation.PostConstruct; - @Configuration @ConfigurationProperties(prefix = "ldap.attribute.uid") public class LdapAttributeUidConfig { @@ -30,11 +46,4 @@ public class LdapAttributeUidConfig { this.value = value; } - @PostConstruct - public void postConstruct() { - if (!StringUtils.equals(LdapProvider.UID, getType()) && !StringUtils.equals(LdapProvider.MATRIX_ID, getType())) { - throw new IllegalArgumentException("Unsupported LDAP UID type: " + getType()); - } - } - } diff --git a/src/main/groovy/io/kamax/mxisd/config/ldap/LdapAuthConfig.java b/src/main/groovy/io/kamax/mxisd/config/ldap/LdapAuthConfig.java index 011500e..ffea425 100644 --- a/src/main/groovy/io/kamax/mxisd/config/ldap/LdapAuthConfig.java +++ b/src/main/groovy/io/kamax/mxisd/config/ldap/LdapAuthConfig.java @@ -1,3 +1,23 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.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 . + */ + package io.kamax.mxisd.config.ldap; import org.springframework.boot.context.properties.ConfigurationProperties; diff --git a/src/main/groovy/io/kamax/mxisd/config/ldap/LdapConfig.groovy b/src/main/groovy/io/kamax/mxisd/config/ldap/LdapConfig.groovy index 55fd3d7..66ca6e5 100644 --- a/src/main/groovy/io/kamax/mxisd/config/ldap/LdapConfig.groovy +++ b/src/main/groovy/io/kamax/mxisd/config/ldap/LdapConfig.groovy @@ -21,6 +21,7 @@ package io.kamax.mxisd.config.ldap import groovy.json.JsonOutput +import io.kamax.mxisd.lookup.provider.LdapProvider import org.apache.commons.lang.StringUtils import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -110,8 +111,11 @@ class LdapConfig { throw new IllegalStateException("Attribute UID value cannot be empty") } + String uidType = attribute.getUid().getType(); + if (!StringUtils.equals(LdapProvider.UID, uidType) && !StringUtils.equals(LdapProvider.MATRIX_ID, uidType)) { + throw new IllegalArgumentException("Unsupported LDAP UID type: " + uidType) + } - log.info("Conn: {}", JsonOutput.toJson(conn)) log.info("Host: {}", conn.getHost()) log.info("Port: {}", conn.getPort()) log.info("Bind DN: {}", conn.getBindDn()) diff --git a/src/main/groovy/io/kamax/mxisd/config/ldap/LdapConnectionConfig.java b/src/main/groovy/io/kamax/mxisd/config/ldap/LdapConnectionConfig.java index 7f85e5d..a4fa419 100644 --- a/src/main/groovy/io/kamax/mxisd/config/ldap/LdapConnectionConfig.java +++ b/src/main/groovy/io/kamax/mxisd/config/ldap/LdapConnectionConfig.java @@ -1,3 +1,23 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.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 . + */ + package io.kamax.mxisd.config.ldap; import org.springframework.boot.context.properties.ConfigurationProperties; diff --git a/src/main/groovy/io/kamax/mxisd/config/ldap/LdapIdentityConfig.java b/src/main/groovy/io/kamax/mxisd/config/ldap/LdapIdentityConfig.java index c6839e4..749530f 100644 --- a/src/main/groovy/io/kamax/mxisd/config/ldap/LdapIdentityConfig.java +++ b/src/main/groovy/io/kamax/mxisd/config/ldap/LdapIdentityConfig.java @@ -1,3 +1,23 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.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 . + */ + package io.kamax.mxisd.config.ldap; import org.springframework.boot.context.properties.ConfigurationProperties; diff --git a/src/main/groovy/io/kamax/mxisd/controller/v1/DefaultExceptionHandler.java b/src/main/groovy/io/kamax/mxisd/controller/v1/DefaultExceptionHandler.java new file mode 100644 index 0000000..1fb0f86 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/controller/v1/DefaultExceptionHandler.java @@ -0,0 +1,78 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.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 . + */ + +package io.kamax.mxisd.controller.v1; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import io.kamax.mxisd.exception.BadRequestException; +import io.kamax.mxisd.exception.MappingAlreadyExistsException; +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletRequest; + +@ControllerAdvice +@ResponseBody +@RequestMapping(produces = MediaType.APPLICATION_JSON_UTF8_VALUE) +public class DefaultExceptionHandler { + + private Logger log = LoggerFactory.getLogger(DefaultExceptionHandler.class); + + private static Gson gson = new Gson(); + + static String handle(String erroCode, String error) { + JsonObject obj = new JsonObject(); + obj.addProperty("errcode", erroCode); + obj.addProperty("error", error); + return gson.toJson(obj); + } + + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(MissingServletRequestParameterException.class) + public String handle(MissingServletRequestParameterException e) { + return handle("M_INVALID_BODY", e.getMessage()); + } + + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(MappingAlreadyExistsException.class) + public String handle(MappingAlreadyExistsException e) { + return handle("M_ALREADY_EXISTS", e.getMessage()); + } + + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(BadRequestException.class) + public String handle(BadRequestException e) { + return handle("M_BAD_REQUEST", e.getMessage()); + } + + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + @ExceptionHandler(RuntimeException.class) + public String handle(HttpServletRequest req, RuntimeException e) { + log.error("Unknown error when handling {}", req.getRequestURL(), e); + return handle("M_UNKNOWN", StringUtils.defaultIfBlank(e.getMessage(), "An uknown error occured. Contact the server administrator if this persists.")); + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/controller/v1/InvitationController.groovy b/src/main/groovy/io/kamax/mxisd/controller/v1/InvitationController.groovy index 46bd1fc..e83b00d 100644 --- a/src/main/groovy/io/kamax/mxisd/controller/v1/InvitationController.groovy +++ b/src/main/groovy/io/kamax/mxisd/controller/v1/InvitationController.groovy @@ -20,10 +20,22 @@ package io.kamax.mxisd.controller.v1 -import io.kamax.mxisd.exception.NotImplementedException +import com.google.gson.Gson +import io.kamax.matrix.MatrixID +import io.kamax.mxisd.config.ServerConfig +import io.kamax.mxisd.controller.v1.io.ThreePidInviteReplyIO +import io.kamax.mxisd.invitation.IThreePidInvite +import io.kamax.mxisd.invitation.IThreePidInviteReply +import io.kamax.mxisd.invitation.InvitationManager +import io.kamax.mxisd.invitation.ThreePidInvite +import io.kamax.mxisd.key.KeyManager import org.slf4j.Logger import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.http.MediaType +import org.springframework.web.bind.annotation.CrossOrigin import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController import javax.servlet.http.HttpServletRequest @@ -31,15 +43,38 @@ import javax.servlet.http.HttpServletRequest import static org.springframework.web.bind.annotation.RequestMethod.POST @RestController +@CrossOrigin +@RequestMapping(path = "/_matrix/identity/api/v1", produces = MediaType.APPLICATION_JSON_UTF8_VALUE) class InvitationController { private Logger log = LoggerFactory.getLogger(InvitationController.class) - @RequestMapping(value = "/_matrix/identity/api/v1/store-invite", method = POST) - String store(HttpServletRequest request) { - log.error("{} was requested but not implemented", request.getRequestURL()) + @Autowired + private InvitationManager mgr - throw new NotImplementedException() + @Autowired + private KeyManager keyMgr + + @Autowired + private ServerConfig srvCfg + + private Gson gson = new Gson() + + @RequestMapping(value = "/store-invite", method = POST) + String store( + HttpServletRequest request, + @RequestParam String sender, + @RequestParam String medium, + @RequestParam String address, + @RequestParam("room_id") String roomId) { + Map parameters = new HashMap<>() + for (String key : request.getParameterMap().keySet()) { + parameters.put(key, request.getParameter(key)); + } + IThreePidInvite invite = new ThreePidInvite(new MatrixID(sender), medium, address, roomId, parameters) + IThreePidInviteReply reply = mgr.storeInvite(invite) + + return gson.toJson(new ThreePidInviteReplyIO(reply, keyMgr.getPublicKeyBase64(keyMgr.getCurrentIndex()), srvCfg.getPublicUrl())) } } diff --git a/src/main/groovy/io/kamax/mxisd/controller/v1/KeyController.groovy b/src/main/groovy/io/kamax/mxisd/controller/v1/KeyController.groovy index f2e0a9e..7c583e7 100644 --- a/src/main/groovy/io/kamax/mxisd/controller/v1/KeyController.groovy +++ b/src/main/groovy/io/kamax/mxisd/controller/v1/KeyController.groovy @@ -24,18 +24,20 @@ import groovy.json.JsonOutput import io.kamax.mxisd.exception.BadRequestException import io.kamax.mxisd.exception.NotImplementedException import io.kamax.mxisd.key.KeyManager +import org.apache.commons.lang.StringUtils import org.slf4j.Logger import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Autowired -import org.springframework.web.bind.annotation.PathVariable -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RestController +import org.springframework.http.MediaType +import org.springframework.web.bind.annotation.* import javax.servlet.http.HttpServletRequest import static org.springframework.web.bind.annotation.RequestMethod.GET @RestController +@CrossOrigin +@RequestMapping(path = "/_matrix/identity/api/v1", produces = MediaType.APPLICATION_JSON_UTF8_VALUE) class KeyController { private Logger log = LoggerFactory.getLogger(KeyController.class) @@ -43,29 +45,34 @@ class KeyController { @Autowired private KeyManager keyMgr - @RequestMapping(value = "/_matrix/identity/api/v1/pubkey/{keyType}:{keyId}", method = GET) + @RequestMapping(value = "/pubkey/{keyType}:{keyId}", method = GET) String getKey(@PathVariable String keyType, @PathVariable int keyId) { if (!"ed25519".contentEquals(keyType)) { throw new BadRequestException("Invalid algorithm: " + keyType) } + log.info("Key {}:{} was requested", keyType, keyId) return JsonOutput.toJson([ public_key: keyMgr.getPublicKeyBase64(keyId) ]) } - @RequestMapping(value = "/_matrix/identity/api/v1/pubkey/ephemeral/isvalid", method = GET) + @RequestMapping(value = "/pubkey/ephemeral/isvalid", method = GET) String checkEphemeralKeyValidity(HttpServletRequest request) { log.error("{} was requested but not implemented", request.getRequestURL()) throw new NotImplementedException() } - @RequestMapping(value = "/_matrix/identity/api/v1/pubkey/isvalid", method = GET) - String checkKeyValidity(HttpServletRequest request) { - log.error("{} was requested but not implemented", request.getRequestURL()) + @RequestMapping(value = "/pubkey/isvalid", method = GET) + String checkKeyValidity(HttpServletRequest request, @RequestParam("public_key") String pubKey) { + log.info("Validating public key {}", pubKey) - throw new NotImplementedException() + // TODO do in manager + boolean valid = StringUtils.equals(pubKey, keyMgr.getPublicKeyBase64(keyMgr.getCurrentIndex())) + return JsonOutput.toJson( + valid: valid + ) } } diff --git a/src/main/groovy/io/kamax/mxisd/controller/v1/MappingController.groovy b/src/main/groovy/io/kamax/mxisd/controller/v1/MappingController.groovy index 050914b..faefd0f 100644 --- a/src/main/groovy/io/kamax/mxisd/controller/v1/MappingController.groovy +++ b/src/main/groovy/io/kamax/mxisd/controller/v1/MappingController.groovy @@ -20,18 +20,20 @@ package io.kamax.mxisd.controller.v1 +import com.google.gson.Gson +import com.google.gson.JsonObject import groovy.json.JsonOutput import groovy.json.JsonSlurper -import io.kamax.mxisd.lookup.ALookupRequest -import io.kamax.mxisd.lookup.BulkLookupRequest -import io.kamax.mxisd.lookup.SingleLookupRequest -import io.kamax.mxisd.lookup.ThreePidMapping +import io.kamax.mxisd.controller.v1.io.SingeLookupReplyJson +import io.kamax.mxisd.lookup.* import io.kamax.mxisd.lookup.strategy.LookupStrategy import io.kamax.mxisd.signature.SignatureManager import org.apache.commons.lang.StringUtils import org.slf4j.Logger import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Autowired +import org.springframework.http.MediaType +import org.springframework.web.bind.annotation.CrossOrigin import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController @@ -42,10 +44,13 @@ import static org.springframework.web.bind.annotation.RequestMethod.GET import static org.springframework.web.bind.annotation.RequestMethod.POST @RestController +@CrossOrigin +@RequestMapping(path = "/_matrix/identity/api/v1", produces = MediaType.APPLICATION_JSON_UTF8_VALUE) class MappingController { private Logger log = LoggerFactory.getLogger(MappingController.class) private JsonSlurper json = new JsonSlurper() + private Gson gson = new Gson() @Autowired private LookupStrategy strategy @@ -60,37 +65,43 @@ class MappingController { if (lookupReq.isRecursive()) { lookupReq.setRecurseHosts(Arrays.asList(xff.split(","))) } + + lookupReq.setUserAgent(req.getHeader("USER-AGENT")) } - @RequestMapping(value = "/_matrix/identity/api/v1/lookup", method = GET) + @RequestMapping(value = "/lookup", method = GET) String lookup(HttpServletRequest request, @RequestParam String medium, @RequestParam String address) { SingleLookupRequest lookupRequest = new SingleLookupRequest() setRequesterInfo(lookupRequest, request) lookupRequest.setType(medium) lookupRequest.setThreePid(address) - log.info("Got request from {} - Is recursive? {}", lookupRequest.getRequester(), lookupRequest.isRecursive()) + log.info("Got single lookup request from {} with client {} - Is recursive? {}", lookupRequest.getRequester(), lookupRequest.getUserAgent(), lookupRequest.isRecursive()) - Optional lookupOpt = strategy.find(lookupRequest) + Optional lookupOpt = strategy.find(lookupRequest) if (!lookupOpt.isPresent()) { log.info("No mapping was found, return empty JSON object") return JsonOutput.toJson([]) } - def lookup = lookupOpt.get() - if (lookup['signatures'] == null) { - log.info("lookup is not signed yet, we sign it") - lookup['signatures'] = signMgr.signMessage(JsonOutput.toJson(lookup)) - } + SingleLookupReply lookup = lookupOpt.get() + if (lookup.isSigned()) { + log.info("Lookup is already signed, sending as-is") + return lookup.getBody(); + } else { + log.info("Lookup is not signed, signing") + JsonObject obj = new Gson().toJsonTree(new SingeLookupReplyJson(lookup)).getAsJsonObject() + obj.add("signatures", signMgr.signMessageGson(gson.toJson(obj))) - return JsonOutput.toJson(lookup) + return gson.toJson(obj) + } } - @RequestMapping(value = "/_matrix/identity/api/v1/bulk_lookup", method = POST) + @RequestMapping(value = "/bulk_lookup", method = POST) String bulkLookup(HttpServletRequest request) { BulkLookupRequest lookupRequest = new BulkLookupRequest() setRequesterInfo(lookupRequest, request) - log.info("Got request from {} - Is recursive? {}", lookupRequest.getRequester(), lookupRequest.isRecursive()) + log.info("Got single lookup request from {} with client {} - Is recursive? {}", lookupRequest.getRequester(), lookupRequest.getUserAgent(), lookupRequest.isRecursive()) ClientBulkLookupRequest input = (ClientBulkLookupRequest) json.parseText(request.getInputStream().getText()) List mappings = new ArrayList<>() diff --git a/src/main/groovy/io/kamax/mxisd/controller/v1/SessionController.groovy b/src/main/groovy/io/kamax/mxisd/controller/v1/SessionController.groovy index c70d203..41aafcd 100644 --- a/src/main/groovy/io/kamax/mxisd/controller/v1/SessionController.groovy +++ b/src/main/groovy/io/kamax/mxisd/controller/v1/SessionController.groovy @@ -25,7 +25,7 @@ import com.google.gson.JsonObject import io.kamax.mxisd.controller.v1.io.SessionEmailTokenRequestJson import io.kamax.mxisd.controller.v1.io.SessionPhoneTokenRequestJson import io.kamax.mxisd.exception.BadRequestException -import io.kamax.mxisd.lookup.ThreePid +import io.kamax.mxisd.lookup.ThreePidValidation import io.kamax.mxisd.mapping.MappingManager import org.apache.commons.io.IOUtils import org.apache.commons.lang.StringUtils @@ -33,16 +33,16 @@ import org.apache.http.HttpStatus import org.slf4j.Logger import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Autowired -import org.springframework.web.bind.annotation.PathVariable -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RequestParam -import org.springframework.web.bind.annotation.RestController +import org.springframework.http.MediaType +import org.springframework.web.bind.annotation.* import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletResponse import java.nio.charset.StandardCharsets @RestController +@CrossOrigin +@RequestMapping(path = "/_matrix/identity/api/v1", produces = MediaType.APPLICATION_JSON_UTF8_VALUE) class SessionController { @Autowired @@ -56,7 +56,7 @@ class SessionController { gson.fromJson(new InputStreamReader(req.getInputStream(), StandardCharsets.UTF_8), obj) } - @RequestMapping(value = "/_matrix/identity/api/v1/validate/{medium}/requestToken") + @RequestMapping(value = "/validate/{medium}/requestToken") String init(HttpServletRequest request, HttpServletResponse response, @PathVariable String medium) { log.info("Requested: {}", request.getRequestURL(), request.getQueryString()) @@ -77,7 +77,7 @@ class SessionController { return gson.toJson(obj) } - @RequestMapping(value = "/_matrix/identity/api/v1/validate/{medium}/submitToken") + @RequestMapping(value = "/validate/{medium}/submitToken") String validate(HttpServletRequest request, @RequestParam String sid, @RequestParam("client_secret") String secret, @RequestParam String token) { @@ -88,15 +88,15 @@ class SessionController { return "{}" } - @RequestMapping(value = "/_matrix/identity/api/v1/3pid/getValidated3pid") + @RequestMapping(value = "/3pid/getValidated3pid") String check(HttpServletRequest request, HttpServletResponse response, @RequestParam String sid, @RequestParam("client_secret") String secret) { log.info("Requested: {}?{}", request.getRequestURL(), request.getQueryString()) - Optional result = mgr.getValidated(sid, secret) + Optional result = mgr.getValidated(sid, secret) if (result.isPresent()) { log.info("requested session was validated") - ThreePid pid = result.get() + ThreePidValidation pid = result.get() JsonObject obj = new JsonObject() obj.addProperty("medium", pid.getMedium()) @@ -115,7 +115,7 @@ class SessionController { } } - @RequestMapping(value = "/_matrix/identity/api/v1/3pid/bind") + @RequestMapping(value = "/3pid/bind") String bind(HttpServletRequest request, HttpServletResponse response, @RequestParam String sid, @RequestParam("client_secret") String secret, @RequestParam String mxid) { String data = IOUtils.toString(request.getReader()) diff --git a/src/main/groovy/io/kamax/mxisd/controller/v1/StatusController.java b/src/main/groovy/io/kamax/mxisd/controller/v1/StatusController.java index 516ff03..26ba03d 100644 --- a/src/main/groovy/io/kamax/mxisd/controller/v1/StatusController.java +++ b/src/main/groovy/io/kamax/mxisd/controller/v1/StatusController.java @@ -1,3 +1,23 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.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 . + */ + package io.kamax.mxisd.controller.v1; import org.springframework.http.MediaType; diff --git a/src/main/groovy/io/kamax/mxisd/controller/v1/io/GenericTokenRequestJson.java b/src/main/groovy/io/kamax/mxisd/controller/v1/io/GenericTokenRequestJson.java index 4304460..f7d1b35 100644 --- a/src/main/groovy/io/kamax/mxisd/controller/v1/io/GenericTokenRequestJson.java +++ b/src/main/groovy/io/kamax/mxisd/controller/v1/io/GenericTokenRequestJson.java @@ -1,3 +1,23 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.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 . + */ + package io.kamax.mxisd.controller.v1.io; import io.kamax.mxisd.mapping.MappingSession; diff --git a/src/main/groovy/io/kamax/mxisd/controller/v1/io/SessionEmailTokenRequestJson.java b/src/main/groovy/io/kamax/mxisd/controller/v1/io/SessionEmailTokenRequestJson.java index e3c7f65..3901045 100644 --- a/src/main/groovy/io/kamax/mxisd/controller/v1/io/SessionEmailTokenRequestJson.java +++ b/src/main/groovy/io/kamax/mxisd/controller/v1/io/SessionEmailTokenRequestJson.java @@ -1,3 +1,23 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.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 . + */ + package io.kamax.mxisd.controller.v1.io; public class SessionEmailTokenRequestJson extends GenericTokenRequestJson { diff --git a/src/main/groovy/io/kamax/mxisd/controller/v1/io/SessionPhoneTokenRequestJson.java b/src/main/groovy/io/kamax/mxisd/controller/v1/io/SessionPhoneTokenRequestJson.java index ebf728f..b66e881 100644 --- a/src/main/groovy/io/kamax/mxisd/controller/v1/io/SessionPhoneTokenRequestJson.java +++ b/src/main/groovy/io/kamax/mxisd/controller/v1/io/SessionPhoneTokenRequestJson.java @@ -1,3 +1,23 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.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 . + */ + package io.kamax.mxisd.controller.v1.io; import com.google.i18n.phonenumbers.NumberParseException; diff --git a/src/main/groovy/io/kamax/mxisd/controller/v1/io/SingeLookupReplyJson.java b/src/main/groovy/io/kamax/mxisd/controller/v1/io/SingeLookupReplyJson.java new file mode 100644 index 0000000..5627664 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/controller/v1/io/SingeLookupReplyJson.java @@ -0,0 +1,75 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.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 . + */ + +package io.kamax.mxisd.controller.v1.io; + +import io.kamax.mxisd.lookup.SingleLookupReply; + +import java.util.HashMap; +import java.util.Map; + +public class SingeLookupReplyJson { + + private String address; + private String medium; + private String mxid; + private long not_after; + private long not_before; + private long ts; + private Map> signatures = new HashMap<>(); + + public SingeLookupReplyJson(SingleLookupReply reply) { + this.address = reply.getRequest().getThreePid(); + this.medium = reply.getRequest().getType(); + this.mxid = reply.getMxid().getId(); + this.not_after = reply.getNotAfter().toEpochMilli(); + this.not_before = reply.getNotBefore().toEpochMilli(); + this.ts = reply.getTimestamp().toEpochMilli(); + } + + public String getAddress() { + return address; + } + + public String getMedium() { + return medium; + } + + public String getMxid() { + return mxid; + } + + public long getNot_after() { + return not_after; + } + + public long getNot_before() { + return not_before; + } + + public long getTs() { + return ts; + } + + public boolean isSigned() { + return signatures != null && !signatures.isEmpty(); + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/controller/v1/io/ThreePidInviteReplyIO.java b/src/main/groovy/io/kamax/mxisd/controller/v1/io/ThreePidInviteReplyIO.java new file mode 100644 index 0000000..7e88e2d --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/controller/v1/io/ThreePidInviteReplyIO.java @@ -0,0 +1,70 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.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 . + */ + +package io.kamax.mxisd.controller.v1.io; + +import io.kamax.mxisd.invitation.IThreePidInviteReply; + +import java.util.Collections; +import java.util.List; + +public class ThreePidInviteReplyIO { + + private String token; + private List public_keys; + private String display_name; + + public ThreePidInviteReplyIO(IThreePidInviteReply reply, String pubKey, String publicUrl) { + this.token = reply.getToken(); + this.public_keys = Collections.singletonList(new Key(pubKey, publicUrl)); + this.display_name = reply.getDisplayName(); + } + + public String getToken() { + return token; + } + + public List getPublic_keys() { + return public_keys; + } + + public String getDisplay_name() { + return display_name; + } + + public class Key { + private String key_validity_url; + private String public_key; + + public Key(String key, String publicUrl) { + this.key_validity_url = publicUrl + "/_matrix/identity/api/v1/pubkey/isvalid"; + this.public_key = key; + } + + public String getKey_validity_url() { + return key_validity_url; + } + + public String getPublic_key() { + return public_key; + } + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/exception/ConfigurationException.java b/src/main/groovy/io/kamax/mxisd/exception/ConfigurationException.java index 1481990..0e1b20b 100644 --- a/src/main/groovy/io/kamax/mxisd/exception/ConfigurationException.java +++ b/src/main/groovy/io/kamax/mxisd/exception/ConfigurationException.java @@ -1,11 +1,47 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.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 . + */ + package io.kamax.mxisd.exception; +import java.util.Optional; + public class ConfigurationException extends RuntimeException { private String key; + private String detailedMsg; public ConfigurationException(String key) { super("Invalid or empty value for configuration key " + key); } + public ConfigurationException(Throwable t) { + super(t.getMessage(), t); + } + + public ConfigurationException(String key, String detailedMsg) { + this(key); + this.detailedMsg = detailedMsg; + } + + public Optional getDetailedMessage() { + return Optional.ofNullable(detailedMsg); + } + } diff --git a/src/main/groovy/io/kamax/mxisd/exception/MappingAlreadyExistsException.java b/src/main/groovy/io/kamax/mxisd/exception/MappingAlreadyExistsException.java new file mode 100644 index 0000000..d722182 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/exception/MappingAlreadyExistsException.java @@ -0,0 +1,29 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.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 . + */ + +package io.kamax.mxisd.exception; + +public class MappingAlreadyExistsException extends RuntimeException { + + public MappingAlreadyExistsException() { + super("A mapping already exists for this 3PID"); + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/invitation/IThreePidInvite.java b/src/main/groovy/io/kamax/mxisd/invitation/IThreePidInvite.java new file mode 100644 index 0000000..62b43dd --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/invitation/IThreePidInvite.java @@ -0,0 +1,39 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.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 . + */ + +package io.kamax.mxisd.invitation; + +import io.kamax.matrix._MatrixID; + +import java.util.Map; + +public interface IThreePidInvite { + + _MatrixID getSender(); + + String getMedium(); + + String getAddress(); + + String getRoomId(); + + Map getProperties(); + +} diff --git a/src/main/groovy/io/kamax/mxisd/invitation/IThreePidInviteReply.java b/src/main/groovy/io/kamax/mxisd/invitation/IThreePidInviteReply.java new file mode 100644 index 0000000..cd80dba --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/invitation/IThreePidInviteReply.java @@ -0,0 +1,33 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.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 . + */ + +package io.kamax.mxisd.invitation; + +public interface IThreePidInviteReply { + + String getId(); + + IThreePidInvite getInvite(); + + String getToken(); + + String getDisplayName(); + +} diff --git a/src/main/groovy/io/kamax/mxisd/invitation/InvitationManager.java b/src/main/groovy/io/kamax/mxisd/invitation/InvitationManager.java new file mode 100644 index 0000000..fd9560a --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/invitation/InvitationManager.java @@ -0,0 +1,325 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.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 . + */ + +package io.kamax.mxisd.invitation; + +import com.google.gson.Gson; +import io.kamax.matrix.MatrixID; +import io.kamax.mxisd.exception.BadRequestException; +import io.kamax.mxisd.exception.MappingAlreadyExistsException; +import io.kamax.mxisd.invitation.sender.IInviteSender; +import io.kamax.mxisd.lookup.SingleLookupReply; +import io.kamax.mxisd.lookup.ThreePidMapping; +import io.kamax.mxisd.lookup.strategy.LookupStrategy; +import io.kamax.mxisd.signature.SignatureManager; +import io.kamax.mxisd.storage.IStorage; +import io.kamax.mxisd.storage.ormlite.ThreePidInviteIO; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.RandomStringUtils; +import org.apache.commons.lang.StringUtils; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.conn.ssl.TrustSelfSignedStrategy; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.ssl.SSLContextBuilder; +import org.json.JSONArray; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.xbill.DNS.*; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLContext; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.TimeUnit; + +@Component +public class InvitationManager { + + private Logger log = LoggerFactory.getLogger(InvitationManager.class); + + private Map invitations = new ConcurrentHashMap<>(); + + @Autowired + private IStorage storage; + + @Autowired + private LookupStrategy lookupMgr; + + @Autowired + private SignatureManager signMgr; + + private Map senders; + + private CloseableHttpClient client; + private Gson gson; + private Timer refreshTimer; + + private String getId(IThreePidInvite invite) { + return invite.getSender().getDomain() + invite.getMedium() + invite.getAddress(); + } + + @PostConstruct + private void postConstruct() { + gson = new Gson(); + + log.info("Loading saved invites"); + Collection ioList = storage.getInvites(); + ioList.forEach(io -> { + log.info("Processing invite {}", gson.toJson(io)); + ThreePidInvite invite = new ThreePidInvite( + new MatrixID(io.getSender()), + io.getMedium(), + io.getAddress(), + io.getRoomId(), + io.getProperties() + ); + + ThreePidInviteReply reply = new ThreePidInviteReply(getId(invite), invite, io.getToken(), ""); + invitations.put(reply.getId(), reply); + }); + + // FIXME export such madness into matrix-java-sdk with a nice wrapper to talk to a homeserver + try { + SSLContext sslContext = SSLContextBuilder.create().loadTrustMaterial(new TrustSelfSignedStrategy()).build(); + HostnameVerifier hostnameVerifier = new NoopHostnameVerifier(); + SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(sslContext, hostnameVerifier); + client = HttpClients.custom().setSSLSocketFactory(sslSocketFactory).build(); + } catch (Exception e) { + // FIXME do better... + throw new RuntimeException(e); + } + + log.info("Setting up invitation mapping refresh timer"); + refreshTimer = new Timer(); + refreshTimer.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + try { + lookupMappingsForInvites(); + } catch (Throwable t) { + log.error("Error when running background mapping refresh", t); + } + } + }, 5000L, TimeUnit.MILLISECONDS.convert(1, TimeUnit.MINUTES)); // FIXME make configurable + } + + @PreDestroy + private void preDestroy() { + ForkJoinPool.commonPool().awaitQuiescence(1, TimeUnit.MINUTES); + } + + private String getId(IThreePidInviteReply reply) { + return reply.getInvite().getSender().getId() + ":" + reply.getInvite().getRoomId() + ":" + reply.getInvite().getMedium() + ":" + reply.getInvite().getAddress(); + } + + String getSrvRecordName(String domain) { + return "_matrix._tcp." + domain; + } + + // TODO use caching mechanism + // TODO export in matrix-java-sdk + String findHomeserverForDomain(String domain) { + log.debug("Performing SRV lookup for {}", domain); + String lookupDns = getSrvRecordName(domain); + log.info("Lookup name: {}", lookupDns); + + try { + List srvRecords = new ArrayList<>(); + Record[] rawRecords = new Lookup(lookupDns, Type.SRV).run(); + if (rawRecords != null && rawRecords.length > 0) { + for (Record record : rawRecords) { + if (Type.SRV == record.getType()) { + srvRecords.add((SRVRecord) record); + } else { + log.info("Got non-SRV record: {}", record.toString()); + } + } + + srvRecords.sort(Comparator.comparingInt(SRVRecord::getPriority)); + for (SRVRecord record : srvRecords) { + log.info("Found SRV record: {}", record.toString()); + return "https://" + record.getTarget().toString(true) + ":" + record.getPort(); + } + } else { + log.info("No SRV record for {}", lookupDns); + } + } catch (TextParseException e) { + log.warn("Unable to perform DNS SRV query for {}: {}", lookupDns, e.getMessage()); + } + + log.info("Performing basic lookup using domain name {}", domain); + return "https://" + domain + ":8448"; + } + + @Autowired + public InvitationManager(List senderList) { + senders = new HashMap<>(); + senderList.forEach(sender -> senders.put(sender.getMedium(), sender)); + } + + public synchronized IThreePidInviteReply storeInvite(IThreePidInvite invitation) { // TODO better sync + IInviteSender sender = senders.get(invitation.getMedium()); + if (sender == null) { + throw new BadRequestException("Medium type " + invitation.getMedium() + " is not supported"); + } + + String invId = getId(invitation); + log.info("Handling invite for {}:{} from {} in room {}", invitation.getMedium(), invitation.getAddress(), invitation.getSender(), invitation.getRoomId()); + if (invitations.containsKey(invId)) { // FIXME we need to lookup using the HS domain too!! + log.info("Invite is already pending for {}:{}, returning data", invitation.getMedium(), invitation.getAddress()); + return invitations.get(invId); + } + + Optional result = lookupMgr.find(invitation.getMedium(), invitation.getAddress(), true); + if (result.isPresent()) { + log.info("Mapping for {}:{} already exists, refusing to store invite", invitation.getMedium(), invitation.getAddress()); + throw new MappingAlreadyExistsException(); + } + + String token = RandomStringUtils.randomAlphanumeric(64); + String displayName = invitation.getAddress().substring(0, 3) + "..."; + + IThreePidInviteReply reply = new ThreePidInviteReply(invId, invitation, token, displayName); + + log.info("Performing invite to {}:{}", invitation.getMedium(), invitation.getAddress()); + sender.send(reply); + + log.info("Storing invite under ID {}", invId); + storage.insertInvite(reply); + invitations.put(invId, reply); + log.info("A new invite has been created for {}:{} on HS {}", invitation.getMedium(), invitation.getAddress(), invitation.getSender().getDomain()); + + return reply; + } + + public void lookupMappingsForInvites() { + log.info("Checking for existing mapping for pending invites"); + for (IThreePidInviteReply reply : invitations.values()) { + log.info("Processing invite {}", getId(reply)); + ForkJoinPool.commonPool().submit(new MappingChecker(reply)); + } + } + + public void publishMappingIfInvited(ThreePidMapping threePid) { + log.info("Looking up possible pending invites for {}:{}", threePid.getMedium(), threePid.getValue()); + for (IThreePidInviteReply reply : invitations.values()) { + if (StringUtils.equals(reply.getInvite().getMedium(), threePid.getMedium()) && StringUtils.equals(reply.getInvite().getAddress(), threePid.getValue())) { + log.info("{}:{} has an invite pending on HS {}, publishing mapping", threePid.getMedium(), threePid.getValue(), reply.getInvite().getSender().getDomain()); + publishMapping(reply, threePid.getMxid()); + } + } + } + + private void publishMapping(IThreePidInviteReply reply, String mxid) { + String medium = reply.getInvite().getMedium(); + String address = reply.getInvite().getAddress(); + String domain = reply.getInvite().getSender().getDomain(); + log.info("Discovering HS for domain {}", domain); + String hsUrlOpt = findHomeserverForDomain(domain); + + // TODO this is needed as this will block if called during authentication cycle due to synapse implementation + new Thread(() -> { // FIXME need to make this retry-able and within a general background working pool + HttpPost req = new HttpPost(hsUrlOpt + "/_matrix/federation/v1/3pid/onbind"); + // Expected body: https://matrix.to/#/!HUeDbmFUsWAhxHHvFG:matrix.org/$150469846739DCLWc:matrix.trancendances.fr + JSONObject obj = new JSONObject(); // TODO use Gson instead + obj.put("mxid", mxid); + obj.put("token", reply.getToken()); + obj.put("signatures", signMgr.signMessageJson(obj.toString())); + + JSONObject objUp = new JSONObject(); + objUp.put("mxid", mxid); + objUp.put("medium", medium); + objUp.put("address", address); + objUp.put("sender", reply.getInvite().getSender().getId()); + objUp.put("room_id", reply.getInvite().getRoomId()); + objUp.put("signed", obj); + + JSONObject content = new JSONObject(); // TODO use Gson instead + JSONArray invites = new JSONArray(); + invites.put(objUp); + content.put("invites", invites); + content.put("medium", medium); + content.put("address", address); + content.put("mxid", mxid); + + content.put("signatures", signMgr.signMessageJson(content.toString())); + + StringEntity entity = new StringEntity(content.toString(), StandardCharsets.UTF_8); + entity.setContentType("application/json"); + req.setEntity(entity); + try { + log.info("Posting onBind event to {}", req.getURI()); + CloseableHttpResponse response = client.execute(req); + int statusCode = response.getStatusLine().getStatusCode(); + log.info("Answer code: {}", statusCode); + if (statusCode >= 300) { + log.warn("Answer body: {}", IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8)); + } else { + invitations.remove(getId(reply)); + storage.deleteInvite(reply.getId()); + log.info("Removed invite from internal store"); + } + response.close(); + } catch (IOException e) { + log.warn("Unable to tell HS {} about invite being mapped", domain, e); + } + }).start(); + } + + private class MappingChecker implements Runnable { + + private IThreePidInviteReply reply; + + public MappingChecker(IThreePidInviteReply reply) { + this.reply = reply; + } + + @Override + public void run() { + try { + log.info("Searching for mapping created since invite {} was created", getId(reply)); + Optional result = lookupMgr.find(reply.getInvite().getMedium(), reply.getInvite().getAddress(), true); + if (result.isPresent()) { + SingleLookupReply lookup = result.get(); + log.info("Found mapping for pending invite {}", getId(reply)); + publishMapping(reply, lookup.getMxid().getId()); + } else { + log.info("No mapping for pending invite {}", getId(reply)); + } + } catch (Throwable t) { + log.error("Unable to process invite", t); + } + } + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/invitation/ThreePidInvite.java b/src/main/groovy/io/kamax/mxisd/invitation/ThreePidInvite.java new file mode 100644 index 0000000..1294c87 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/invitation/ThreePidInvite.java @@ -0,0 +1,74 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.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 . + */ + +package io.kamax.mxisd.invitation; + +import io.kamax.matrix._MatrixID; + +import java.util.HashMap; +import java.util.Map; + +public class ThreePidInvite implements IThreePidInvite { + + private _MatrixID sender; + private String medium; + private String address; + private String roomId; + private Map properties; + + public ThreePidInvite(_MatrixID sender, String medium, String address, String roomId) { + this.sender = sender; + this.medium = medium; + this.address = address; + this.roomId = roomId; + this.properties = new HashMap<>(); + } + + public ThreePidInvite(_MatrixID sender, String medium, String address, String roomId, Map properties) { + this(sender, medium, address, roomId); + this.properties = properties; + } + + @Override + public _MatrixID getSender() { + return sender; + } + + @Override + public String getMedium() { + return medium; + } + + @Override + public String getAddress() { + return address; + } + + @Override + public String getRoomId() { + return roomId; + } + + @Override + public Map getProperties() { + return properties; + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/invitation/ThreePidInviteReply.java b/src/main/groovy/io/kamax/mxisd/invitation/ThreePidInviteReply.java new file mode 100644 index 0000000..c67139e --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/invitation/ThreePidInviteReply.java @@ -0,0 +1,57 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.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 . + */ + +package io.kamax.mxisd.invitation; + +public class ThreePidInviteReply implements IThreePidInviteReply { + + private String id; + private IThreePidInvite invite; + private String token; + private String displayName; + + public ThreePidInviteReply(String id, IThreePidInvite invite, String token, String displayName) { + this.id = id; + this.invite = invite; + this.token = token; + this.displayName = displayName; + } + + @Override + public String getId() { + return id; + } + + @Override + public IThreePidInvite getInvite() { + return invite; + } + + @Override + public String getToken() { + return token; + } + + @Override + public String getDisplayName() { + return displayName; + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/invitation/sender/EmailInviteSender.java b/src/main/groovy/io/kamax/mxisd/invitation/sender/EmailInviteSender.java new file mode 100644 index 0000000..854c783 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/invitation/sender/EmailInviteSender.java @@ -0,0 +1,138 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.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 . + */ + +package io.kamax.mxisd.invitation.sender; + +import com.sun.mail.smtp.SMTPTransport; +import io.kamax.matrix.ThreePidMedium; +import io.kamax.mxisd.config.ServerConfig; +import io.kamax.mxisd.config.invite.sender.EmailSenderConfig; +import io.kamax.mxisd.exception.ConfigurationException; +import io.kamax.mxisd.invitation.IThreePidInviteReply; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.WordUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import javax.mail.Message; +import javax.mail.MessagingException; +import javax.mail.Session; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeMessage; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; +import java.util.Date; + +@Component +public class EmailInviteSender implements IInviteSender { + + private Logger log = LoggerFactory.getLogger(EmailInviteSender.class); + + @Autowired + private EmailSenderConfig cfg; + + @Autowired + private ServerConfig srvCfg; + + @Autowired + private ApplicationContext app; + + private Session session; + private InternetAddress sender; + + @PostConstruct + private void postConstruct() { + try { + session = Session.getInstance(System.getProperties()); + sender = new InternetAddress(cfg.getEmail(), cfg.getName()); + } catch (UnsupportedEncodingException e) { + // What are we supposed to do with this?! + throw new ConfigurationException(e); + } + } + + @Override + public String getMedium() { + return ThreePidMedium.Email.getId(); + } + + @Override + public void send(IThreePidInviteReply invite) { + if (!ThreePidMedium.Email.is(invite.getInvite().getMedium())) { + throw new IllegalArgumentException(invite.getInvite().getMedium() + " is not a supported 3PID type"); + } + + try { + String domainPretty = WordUtils.capitalizeFully(srvCfg.getName()); + String senderName = invite.getInvite().getProperties().getOrDefault("sender_display_name", ""); + String senderNameOrId = StringUtils.defaultIfBlank(senderName, invite.getInvite().getSender().getId()); + String roomName = invite.getInvite().getProperties().getOrDefault("room_name", ""); + String roomNameOrId = StringUtils.defaultIfBlank(roomName, invite.getInvite().getRoomId()); + + String templateBody = IOUtils.toString( + StringUtils.startsWith(cfg.getTemplate(), "classpath:") ? + app.getResource(cfg.getTemplate()).getInputStream() : new FileInputStream(cfg.getTemplate()), + StandardCharsets.UTF_8); + templateBody = templateBody.replace("%DOMAIN%", srvCfg.getName()); + templateBody = templateBody.replace("%DOMAIN_PRETTY%", domainPretty); + templateBody = templateBody.replace("%FROM_EMAIL%", cfg.getEmail()); + templateBody = templateBody.replace("%FROM_NAME%", cfg.getName()); + templateBody = templateBody.replace("%SENDER_ID%", invite.getInvite().getSender().getId()); + templateBody = templateBody.replace("%SENDER_NAME%", senderName); + templateBody = templateBody.replace("%SENDER_NAME_OR_ID%", senderNameOrId); + templateBody = templateBody.replace("%INVITE_MEDIUM%", invite.getInvite().getMedium()); + templateBody = templateBody.replace("%INVITE_ADDRESS%", invite.getInvite().getAddress()); + templateBody = templateBody.replace("%ROOM_ID%", invite.getInvite().getRoomId()); + templateBody = templateBody.replace("%ROOM_NAME%", roomName); + templateBody = templateBody.replace("%ROOM_NAME_OR_ID%", roomNameOrId); + + MimeMessage msg = new MimeMessage(session, IOUtils.toInputStream(templateBody, StandardCharsets.UTF_8)); + msg.setHeader("X-Mailer", "mxisd"); // TODO set version + msg.setSentDate(new Date()); + msg.setFrom(sender); + msg.setRecipients(Message.RecipientType.TO, invite.getInvite().getAddress()); + msg.saveChanges(); + + log.info("Sending invite to {} via SMTP using {}:{}", invite.getInvite().getAddress(), cfg.getHost(), cfg.getPort()); + SMTPTransport transport = (SMTPTransport) session.getTransport("smtp"); + transport.setStartTLS(cfg.getTls() > 0); + transport.setRequireStartTLS(cfg.getTls() > 1); + + log.info("Connecting to {}:{}", cfg.getHost(), cfg.getPort()); + transport.connect(cfg.getHost(), cfg.getPort(), cfg.getLogin(), cfg.getPassword()); + try { + transport.sendMessage(msg, InternetAddress.parse(invite.getInvite().getAddress())); + log.info("Invite to {} was sent", invite.getInvite().getAddress()); + } finally { + transport.close(); + } + } catch (IOException | MessagingException e) { + throw new RuntimeException("Unable to send e-mail invite to " + invite.getInvite().getAddress(), e); + } + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/invitation/sender/IInviteSender.java b/src/main/groovy/io/kamax/mxisd/invitation/sender/IInviteSender.java new file mode 100644 index 0000000..14af7fd --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/invitation/sender/IInviteSender.java @@ -0,0 +1,31 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.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 . + */ + +package io.kamax.mxisd.invitation.sender; + +import io.kamax.mxisd.invitation.IThreePidInviteReply; + +public interface IInviteSender { + + String getMedium(); + + void send(IThreePidInviteReply invite); + +} diff --git a/src/main/groovy/io/kamax/mxisd/lookup/ALookupRequest.java b/src/main/groovy/io/kamax/mxisd/lookup/ALookupRequest.java index a668fd8..92bb392 100644 --- a/src/main/groovy/io/kamax/mxisd/lookup/ALookupRequest.java +++ b/src/main/groovy/io/kamax/mxisd/lookup/ALookupRequest.java @@ -26,6 +26,7 @@ public abstract class ALookupRequest { private String id; private String requester; + private String userAgent; private boolean isRecursive; private List recurseHosts; @@ -45,6 +46,14 @@ public abstract class ALookupRequest { this.requester = requester; } + public String getUserAgent() { + return userAgent; + } + + public void setUserAgent(String userAgent) { + this.userAgent = userAgent; + } + public boolean isRecursive() { return isRecursive; } diff --git a/src/main/groovy/io/kamax/mxisd/lookup/SingleLookupReply.java b/src/main/groovy/io/kamax/mxisd/lookup/SingleLookupReply.java new file mode 100644 index 0000000..d3a212c --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/lookup/SingleLookupReply.java @@ -0,0 +1,116 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.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 . + */ + +package io.kamax.mxisd.lookup; + +import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; +import io.kamax.matrix.MatrixID; +import io.kamax.matrix._MatrixID; +import io.kamax.mxisd.controller.v1.io.SingeLookupReplyJson; + +import java.time.Instant; + +public class SingleLookupReply { + + private static Gson gson = new Gson(); + + private boolean isRecursive; + private boolean isSigned; + private String body; + private SingleLookupRequest request; + private _MatrixID mxid; + private Instant notBefore; + private Instant notAfter; + private Instant timestamp; + + public static SingleLookupReply fromRecursive(SingleLookupRequest request, String body) { + SingleLookupReply reply = new SingleLookupReply(); + reply.isRecursive = true; + reply.request = request; + reply.body = body; + + try { + SingeLookupReplyJson json = gson.fromJson(body, SingeLookupReplyJson.class); + reply.mxid = new MatrixID(json.getMxid()); + reply.notAfter = Instant.ofEpochMilli(json.getNot_after()); + reply.notBefore = Instant.ofEpochMilli(json.getNot_before()); + reply.timestamp = Instant.ofEpochMilli(json.getTs()); + reply.isSigned = json.isSigned(); + } catch (JsonSyntaxException e) { + // stub - we only want to try, nothing more + } + + return reply; + } + + private SingleLookupReply() { + // stub + } + + public SingleLookupReply(SingleLookupRequest request, String mxid) { + this(request, new MatrixID(mxid)); + } + + public SingleLookupReply(SingleLookupRequest request, _MatrixID mxid) { + this(request, mxid, Instant.now(), Instant.ofEpochMilli(0), Instant.ofEpochMilli(253402300799000L)); + } + + public SingleLookupReply(SingleLookupRequest request, _MatrixID mxid, Instant timestamp, Instant notBefore, Instant notAfter) { + this.request = request; + this.mxid = mxid; + this.timestamp = timestamp; + this.notBefore = notBefore; + this.notAfter = notAfter; + } + + public boolean isRecursive() { + return isRecursive; + } + + public boolean isSigned() { + return isSigned; + } + + public String getBody() { + return body; + } + + public SingleLookupRequest getRequest() { + return request; + } + + public _MatrixID getMxid() { + return mxid; + } + + public Instant getNotBefore() { + return notBefore; + } + + public Instant getNotAfter() { + return notAfter; + } + + public Instant getTimestamp() { + return timestamp; + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/lookup/ThreePid.java b/src/main/groovy/io/kamax/mxisd/lookup/ThreePid.java deleted file mode 100644 index 6ecf4ab..0000000 --- a/src/main/groovy/io/kamax/mxisd/lookup/ThreePid.java +++ /dev/null @@ -1,29 +0,0 @@ -package io.kamax.mxisd.lookup; - -import java.time.Instant; - -public class ThreePid { - - private String medium; - private String address; - private Instant validation; - - public ThreePid(String medium, String address, Instant validation) { - this.medium = medium; - this.address = address; - this.validation = validation; - } - - public String getMedium() { - return medium; - } - - public String getAddress() { - return address; - } - - public Instant getValidation() { - return validation; - } - -} diff --git a/src/main/groovy/io/kamax/mxisd/lookup/ThreePidMapping.java b/src/main/groovy/io/kamax/mxisd/lookup/ThreePidMapping.java index 741e56b..5777ad7 100644 --- a/src/main/groovy/io/kamax/mxisd/lookup/ThreePidMapping.java +++ b/src/main/groovy/io/kamax/mxisd/lookup/ThreePidMapping.java @@ -21,6 +21,7 @@ package io.kamax.mxisd.lookup; import groovy.json.JsonOutput; +import io.kamax.mxisd.ThreePid; public class ThreePidMapping { @@ -28,6 +29,20 @@ public class ThreePidMapping { private String value; private String mxid; + public ThreePidMapping() { + // stub + } + + public ThreePidMapping(ThreePid threePid, String mxid) { + this(threePid.getMedium(), threePid.getAddress(), mxid); + } + + public ThreePidMapping(String medium, String value, String mxid) { + setMedium(medium); + setValue(value); + setMxid(mxid); + } + public String getMedium() { return medium; } diff --git a/src/main/groovy/io/kamax/mxisd/lookup/ThreePidValidation.java b/src/main/groovy/io/kamax/mxisd/lookup/ThreePidValidation.java new file mode 100644 index 0000000..8d3b4a0 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/lookup/ThreePidValidation.java @@ -0,0 +1,40 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.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 . + */ + +package io.kamax.mxisd.lookup; + +import io.kamax.mxisd.ThreePid; + +import java.time.Instant; + +public class ThreePidValidation extends ThreePid { + + private Instant validation; + + public ThreePidValidation(String medium, String address, Instant validation) { + super(medium, address); + this.validation = validation; + } + + public Instant getValidation() { + return validation; + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/lookup/fetcher/IBridgeFetcher.groovy b/src/main/groovy/io/kamax/mxisd/lookup/fetcher/IBridgeFetcher.groovy index a3d3624..bd445bb 100644 --- a/src/main/groovy/io/kamax/mxisd/lookup/fetcher/IBridgeFetcher.groovy +++ b/src/main/groovy/io/kamax/mxisd/lookup/fetcher/IBridgeFetcher.groovy @@ -20,12 +20,13 @@ package io.kamax.mxisd.lookup.fetcher +import io.kamax.mxisd.lookup.SingleLookupReply import io.kamax.mxisd.lookup.SingleLookupRequest import io.kamax.mxisd.lookup.ThreePidMapping interface IBridgeFetcher { - Optional find(SingleLookupRequest request) + Optional find(SingleLookupRequest request) List populate(List mappings) diff --git a/src/main/groovy/io/kamax/mxisd/lookup/fetcher/IRemoteIdentityServerFetcher.groovy b/src/main/groovy/io/kamax/mxisd/lookup/fetcher/IRemoteIdentityServerFetcher.groovy index 2b45959..aeb81d5 100644 --- a/src/main/groovy/io/kamax/mxisd/lookup/fetcher/IRemoteIdentityServerFetcher.groovy +++ b/src/main/groovy/io/kamax/mxisd/lookup/fetcher/IRemoteIdentityServerFetcher.groovy @@ -20,13 +20,15 @@ package io.kamax.mxisd.lookup.fetcher +import io.kamax.mxisd.lookup.SingleLookupReply +import io.kamax.mxisd.lookup.SingleLookupRequest import io.kamax.mxisd.lookup.ThreePidMapping interface IRemoteIdentityServerFetcher { boolean isUsable(String remote) - Optional find(String remote, String type, String threePid) + Optional find(String remote, SingleLookupRequest request) List find(String remote, List mappings) diff --git a/src/main/groovy/io/kamax/mxisd/lookup/provider/BridgeFetcher.java b/src/main/groovy/io/kamax/mxisd/lookup/provider/BridgeFetcher.java index ec1c8cd..bb8a85f 100644 --- a/src/main/groovy/io/kamax/mxisd/lookup/provider/BridgeFetcher.java +++ b/src/main/groovy/io/kamax/mxisd/lookup/provider/BridgeFetcher.java @@ -21,6 +21,7 @@ package io.kamax.mxisd.lookup.provider; import io.kamax.mxisd.config.RecursiveLookupBridgeConfig; +import io.kamax.mxisd.lookup.SingleLookupReply; import io.kamax.mxisd.lookup.SingleLookupRequest; import io.kamax.mxisd.lookup.ThreePidMapping; import io.kamax.mxisd.lookup.fetcher.IBridgeFetcher; @@ -46,16 +47,16 @@ public class BridgeFetcher implements IBridgeFetcher { private RemoteIdentityServerFetcher fetcher; @Override - public Optional find(SingleLookupRequest request) { + public Optional find(SingleLookupRequest request) { Optional mediumUrl = Optional.ofNullable(cfg.getMappings().get(request.getType())); if (mediumUrl.isPresent() && !StringUtils.isBlank(mediumUrl.get())) { log.info("Using specific medium bridge lookup URL {}", mediumUrl.get()); - return fetcher.find(mediumUrl.get(), request.getType(), request.getThreePid()); + return fetcher.find(mediumUrl.get(), request); } else if (!StringUtils.isBlank(cfg.getServer())) { log.info("Using generic bridge lookup URL {}", cfg.getServer()); - return fetcher.find(cfg.getServer(), request.getType(), request.getThreePid()); + return fetcher.find(cfg.getServer(), request); } else { log.info("No bridge lookup URL found/configured, skipping"); diff --git a/src/main/groovy/io/kamax/mxisd/lookup/provider/DnsLookupProvider.groovy b/src/main/groovy/io/kamax/mxisd/lookup/provider/DnsLookupProvider.groovy index a7076a1..40aec25 100644 --- a/src/main/groovy/io/kamax/mxisd/lookup/provider/DnsLookupProvider.groovy +++ b/src/main/groovy/io/kamax/mxisd/lookup/provider/DnsLookupProvider.groovy @@ -21,6 +21,7 @@ package io.kamax.mxisd.lookup.provider import io.kamax.mxisd.config.ServerConfig +import io.kamax.mxisd.lookup.SingleLookupReply import io.kamax.mxisd.lookup.SingleLookupRequest import io.kamax.mxisd.lookup.ThreePidMapping import io.kamax.mxisd.lookup.fetcher.IRemoteIdentityServerFetcher @@ -124,7 +125,7 @@ class DnsLookupProvider implements IThreePidProvider { } @Override - Optional find(SingleLookupRequest request) { + Optional find(SingleLookupRequest request) { if (!StringUtils.equals("email", request.getType())) { // TODO use enum log.info("Skipping unsupported type {} for {}", request.getType(), request.getThreePid()) return Optional.empty() @@ -137,7 +138,7 @@ class DnsLookupProvider implements IThreePidProvider { Optional baseUrl = findIdentityServerForDomain(domain) if (baseUrl.isPresent()) { - return fetcher.find(baseUrl.get(), request.getType().toString(), request.getThreePid()) + return fetcher.find(baseUrl.get(), request) } return Optional.empty() diff --git a/src/main/groovy/io/kamax/mxisd/lookup/provider/ForwarderProvider.groovy b/src/main/groovy/io/kamax/mxisd/lookup/provider/ForwarderProvider.groovy index f2253dc..1ef903a 100644 --- a/src/main/groovy/io/kamax/mxisd/lookup/provider/ForwarderProvider.groovy +++ b/src/main/groovy/io/kamax/mxisd/lookup/provider/ForwarderProvider.groovy @@ -21,6 +21,7 @@ package io.kamax.mxisd.lookup.provider import io.kamax.mxisd.config.ForwardConfig +import io.kamax.mxisd.lookup.SingleLookupReply import io.kamax.mxisd.lookup.SingleLookupRequest import io.kamax.mxisd.lookup.ThreePidMapping import io.kamax.mxisd.lookup.fetcher.IRemoteIdentityServerFetcher @@ -56,9 +57,9 @@ class ForwarderProvider implements IThreePidProvider { } @Override - Optional find(SingleLookupRequest request) { + Optional find(SingleLookupRequest request) { for (String root : cfg.getServers()) { - Optional answer = fetcher.find(root, request.getType(), request.getThreePid()) + Optional answer = fetcher.find(root, request) if (answer.isPresent()) { return answer } diff --git a/src/main/groovy/io/kamax/mxisd/lookup/provider/GoogleFirebaseProvider.groovy b/src/main/groovy/io/kamax/mxisd/lookup/provider/GoogleFirebaseProvider.groovy new file mode 100644 index 0000000..30d3c7b --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/lookup/provider/GoogleFirebaseProvider.groovy @@ -0,0 +1,190 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.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 . + */ + +package io.kamax.mxisd.lookup.provider + +import com.google.firebase.FirebaseApp +import com.google.firebase.FirebaseOptions +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.auth.FirebaseCredential +import com.google.firebase.auth.FirebaseCredentials +import com.google.firebase.auth.UserRecord +import com.google.firebase.internal.NonNull +import com.google.firebase.tasks.OnFailureListener +import com.google.firebase.tasks.OnSuccessListener +import io.kamax.matrix.ThreePidMedium +import io.kamax.mxisd.lookup.SingleLookupReply +import io.kamax.mxisd.lookup.SingleLookupRequest +import io.kamax.mxisd.lookup.ThreePidMapping +import org.apache.commons.lang.StringUtils +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.function.Consumer +import java.util.regex.Pattern + +public class GoogleFirebaseProvider implements IThreePidProvider { + + private Logger log = LoggerFactory.getLogger(GoogleFirebaseProvider.class); + + private static final Pattern matrixIdLaxPattern = Pattern.compile("@(.*):(.+)"); + + private boolean isEnabled; + private String domain; + private FirebaseApp fbApp; + private FirebaseAuth fbAuth; + + public GoogleFirebaseProvider(boolean isEnabled) { + this.isEnabled = isEnabled; + } + + public GoogleFirebaseProvider(String credsPath, String db, String domain) { + this(true); + this.domain = domain; + try { + fbApp = FirebaseApp.initializeApp(getOpts(credsPath, db), "ThreePidProvider"); + fbAuth = FirebaseAuth.getInstance(fbApp); + + log.info("Google Firebase Authentication is ready"); + } catch (IOException e) { + throw new RuntimeException("Error when initializing Firebase", e); + } + } + + private FirebaseCredential getCreds(String credsPath) throws IOException { + if (StringUtils.isNotBlank(credsPath)) { + return FirebaseCredentials.fromCertificate(new FileInputStream(credsPath)); + } else { + return FirebaseCredentials.applicationDefault(); + } + } + + private FirebaseOptions getOpts(String credsPath, String db) throws IOException { + if (StringUtils.isBlank(db)) { + throw new IllegalArgumentException("Firebase database is not configured"); + } + + return new FirebaseOptions.Builder() + .setCredential(getCreds(credsPath)) + .setDatabaseUrl(db) + .build(); + } + + private String getMxid(UserRecord record) { + return "@${record.getUid()}:${domain}"; + } + + @Override + public boolean isEnabled() { + return isEnabled; + } + + @Override + public boolean isLocal() { + return true; + } + + @Override + public int getPriority() { + return 25; + } + + private void waitOnLatch(CountDownLatch l) { + try { + l.await(30, TimeUnit.SECONDS); + } catch (InterruptedException e) { + log.warn("Interrupted while waiting for Firebase auth check"); + } + } + + private Optional findInternal(String medium, String address) { + UserRecord r; + CountDownLatch l = new CountDownLatch(1); + + OnSuccessListener success = new OnSuccessListener() { + @Override + void onSuccess(UserRecord result) { + log.info("Found 3PID match for {}:{} - UID is {}", medium, address, result.getUid()) + r = result; + l.countDown() + } + }; + + OnFailureListener failure = new OnFailureListener() { + @Override + void onFailure(@NonNull Exception e) { + log.info("No 3PID match for {}:{} - {}", medium, address, e.getMessage()) + r = null; + l.countDown() + } + }; + + if (ThreePidMedium.Email.is(medium)) { + log.info("Performing E-mail 3PID lookup for {}", address) + fbAuth.getUserByEmail(address) + .addOnSuccessListener(success) + .addOnFailureListener(failure); + waitOnLatch(l); + } else if (ThreePidMedium.PhoneNumber.is(medium)) { + log.info("Performing msisdn 3PID lookup for {}", address) + fbAuth.getUserByPhoneNumber(address) + .addOnSuccessListener(success) + .addOnFailureListener(failure); + waitOnLatch(l); + } else { + log.info("{} is not a supported 3PID medium", medium); + r = null; + } + + return Optional.ofNullable(r); + } + + @Override + public Optional find(SingleLookupRequest request) { + Optional urOpt = findInternal(request.getType(), request.getThreePid()) + if (urOpt.isPresent()) { + return Optional.of(new SingleLookupReply(request, getMxid(urOpt.get()))); + } + + return Optional.empty(); + } + + @Override + public List populate(List mappings) { + List results = new ArrayList<>(); + mappings.parallelStream().forEach(new Consumer() { + @Override + void accept(ThreePidMapping o) { + Optional urOpt = findInternal(o.getMedium(), o.getValue()); + if (urOpt.isPresent()) { + ThreePidMapping result = new ThreePidMapping(); + result.setMedium(o.getMedium()) + result.setValue(o.getValue()) + result.setMxid(getMxid(urOpt.get())) + results.add(result) + } + } + }); + return results; + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/lookup/provider/IThreePidProvider.groovy b/src/main/groovy/io/kamax/mxisd/lookup/provider/IThreePidProvider.groovy index 445afda..614cc04 100644 --- a/src/main/groovy/io/kamax/mxisd/lookup/provider/IThreePidProvider.groovy +++ b/src/main/groovy/io/kamax/mxisd/lookup/provider/IThreePidProvider.groovy @@ -20,6 +20,7 @@ package io.kamax.mxisd.lookup.provider +import io.kamax.mxisd.lookup.SingleLookupReply import io.kamax.mxisd.lookup.SingleLookupRequest import io.kamax.mxisd.lookup.ThreePidMapping @@ -34,7 +35,7 @@ interface IThreePidProvider { */ int getPriority() // Should not be here but let's KISS for now - Optional find(SingleLookupRequest request) + Optional find(SingleLookupRequest request) List populate(List mappings) diff --git a/src/main/groovy/io/kamax/mxisd/lookup/provider/LdapProvider.groovy b/src/main/groovy/io/kamax/mxisd/lookup/provider/LdapProvider.groovy index 1984f32..c2cc220 100644 --- a/src/main/groovy/io/kamax/mxisd/lookup/provider/LdapProvider.groovy +++ b/src/main/groovy/io/kamax/mxisd/lookup/provider/LdapProvider.groovy @@ -22,6 +22,7 @@ package io.kamax.mxisd.lookup.provider import io.kamax.mxisd.config.ServerConfig import io.kamax.mxisd.config.ldap.LdapConfig +import io.kamax.mxisd.lookup.SingleLookupReply import io.kamax.mxisd.lookup.SingleLookupRequest import io.kamax.mxisd.lookup.ThreePidMapping import org.apache.commons.lang.StringUtils @@ -131,7 +132,7 @@ class LdapProvider implements IThreePidProvider { } @Override - Optional find(SingleLookupRequest request) { + Optional find(SingleLookupRequest request) { log.info("Performing LDAP lookup ${request.getThreePid()} of type ${request.getType()}") LdapConnection conn = getConn() @@ -140,14 +141,7 @@ class LdapProvider implements IThreePidProvider { Optional mxid = lookup(conn, request.getType(), request.getThreePid()) if (mxid.isPresent()) { - return Optional.of([ - address : request.getThreePid(), - medium : request.getType(), - mxid : mxid.get(), - not_before: 0, - not_after : 9223372036854775807, - ts : 0 - ]) + return Optional.of(new SingleLookupReply(request, mxid.get())); } } finally { conn.close() diff --git a/src/main/groovy/io/kamax/mxisd/lookup/provider/RemoteIdentityServerFetcher.groovy b/src/main/groovy/io/kamax/mxisd/lookup/provider/RemoteIdentityServerFetcher.groovy index 36a38de..7435879 100644 --- a/src/main/groovy/io/kamax/mxisd/lookup/provider/RemoteIdentityServerFetcher.groovy +++ b/src/main/groovy/io/kamax/mxisd/lookup/provider/RemoteIdentityServerFetcher.groovy @@ -24,6 +24,8 @@ import groovy.json.JsonException import groovy.json.JsonOutput import groovy.json.JsonSlurper import io.kamax.mxisd.controller.v1.ClientBulkLookupRequest +import io.kamax.mxisd.lookup.SingleLookupReply +import io.kamax.mxisd.lookup.SingleLookupRequest import io.kamax.mxisd.lookup.ThreePidMapping import io.kamax.mxisd.lookup.fetcher.IRemoteIdentityServerFetcher import org.apache.http.HttpEntity @@ -77,24 +79,26 @@ public class RemoteIdentityServerFetcher implements IRemoteIdentityServerFetcher } @Override - Optional find(String remote, String type, String threePid) { - log.info("Looking up {} 3PID {} using {}", type, threePid, remote) + Optional find(String remote, SingleLookupRequest request) { + log.info("Looking up {} 3PID {} using {}", request.getType(), request.getThreePid(), remote) HttpURLConnection rootSrvConn = (HttpURLConnection) new URL( - "${remote}/_matrix/identity/api/v1/lookup?medium=${type}&address=${threePid}" + "${remote}/_matrix/identity/api/v1/lookup?medium=${request.getType()}&address=${request.getThreePid()}" ).openConnection() try { - def output = json.parseText(rootSrvConn.getInputStream().getText()) + String outputRaw = rootSrvConn.getInputStream().getText() + def output = json.parseText(outputRaw) if (output['address']) { log.info("Found 3PID mapping: {}", output) - return Optional.of(output) + + return Optional.of(SingleLookupReply.fromRecursive(request, outputRaw)) } log.info("Empty 3PID mapping from {}", remote) return Optional.empty() } catch (IOException e) { - log.warn("Error looking up 3PID mapping {}: {}", threePid, e.getMessage()) + log.warn("Error looking up 3PID mapping {}: {}", request.getThreePid(), e.getMessage()) return Optional.empty() } catch (JsonException e) { log.warn("Invalid JSON answer from {}", remote) diff --git a/src/main/groovy/io/kamax/mxisd/lookup/strategy/LookupStrategy.groovy b/src/main/groovy/io/kamax/mxisd/lookup/strategy/LookupStrategy.groovy index dee4646..37e4327 100644 --- a/src/main/groovy/io/kamax/mxisd/lookup/strategy/LookupStrategy.groovy +++ b/src/main/groovy/io/kamax/mxisd/lookup/strategy/LookupStrategy.groovy @@ -21,6 +21,7 @@ package io.kamax.mxisd.lookup.strategy import io.kamax.mxisd.lookup.BulkLookupRequest +import io.kamax.mxisd.lookup.SingleLookupReply import io.kamax.mxisd.lookup.SingleLookupRequest import io.kamax.mxisd.lookup.ThreePidMapping import io.kamax.mxisd.lookup.provider.IThreePidProvider @@ -29,7 +30,11 @@ interface LookupStrategy { List getLocalProviders() - Optional find(SingleLookupRequest request) + Optional find(String medium, String address, boolean recursive) + + Optional find(SingleLookupRequest request) + + Optional findRecursive(SingleLookupRequest request) List find(BulkLookupRequest requests) diff --git a/src/main/groovy/io/kamax/mxisd/lookup/strategy/RecursivePriorityLookupStrategy.groovy b/src/main/groovy/io/kamax/mxisd/lookup/strategy/RecursivePriorityLookupStrategy.groovy index 4317adc..e4ef776 100644 --- a/src/main/groovy/io/kamax/mxisd/lookup/strategy/RecursivePriorityLookupStrategy.groovy +++ b/src/main/groovy/io/kamax/mxisd/lookup/strategy/RecursivePriorityLookupStrategy.groovy @@ -22,10 +22,7 @@ package io.kamax.mxisd.lookup.strategy import edazdarevic.commons.net.CIDRUtils import io.kamax.mxisd.config.RecursiveLookupConfig -import io.kamax.mxisd.lookup.ALookupRequest -import io.kamax.mxisd.lookup.BulkLookupRequest -import io.kamax.mxisd.lookup.SingleLookupRequest -import io.kamax.mxisd.lookup.ThreePidMapping +import io.kamax.mxisd.lookup.* import io.kamax.mxisd.lookup.fetcher.IBridgeFetcher import io.kamax.mxisd.lookup.provider.IThreePidProvider import org.slf4j.Logger @@ -93,13 +90,17 @@ class RecursivePriorityLookupStrategy implements LookupStrategy, InitializingBea } List listUsableProviders(ALookupRequest request) { + return listUsableProviders(request, false); + } + + List listUsableProviders(ALookupRequest request, boolean forceRecursive) { List usableProviders = new ArrayList<>() - boolean canRecurse = isAllowedForRecursive(request.getRequester()) + boolean canRecurse = forceRecursive || isAllowedForRecursive(request.getRequester()) log.info("Host {} allowed for recursion: {}", request.getRequester(), canRecurse) for (IThreePidProvider provider : providers) { - if (provider.isEnabled() && (provider.isLocal() || canRecurse)) { + if (provider.isEnabled() && (provider.isLocal() || canRecurse || forceRecursive)) { usableProviders.add(provider) } } @@ -118,15 +119,27 @@ class RecursivePriorityLookupStrategy implements LookupStrategy, InitializingBea } @Override - Optional find(SingleLookupRequest request) { - for (IThreePidProvider provider : listUsableProviders(request)) { - Optional lookupDataOpt = provider.find(request) + Optional find(String medium, String address, boolean recursive) { + SingleLookupRequest req = new SingleLookupRequest(); + req.setType(medium) + req.setThreePid(address) + req.setRequester("Internal") + return find(req, recursive) + } + + Optional find(SingleLookupRequest request, boolean forceRecursive) { + for (IThreePidProvider provider : listUsableProviders(request, forceRecursive)) { + Optional lookupDataOpt = provider.find(request) if (lookupDataOpt.isPresent()) { return lookupDataOpt } } - if (recursiveCfg.getBridge().getEnabled() && (!recursiveCfg.getBridge().getRecursiveOnly() || isAllowedForRecursive(request.getRequester()))) { + if ( + recursiveCfg.getBridge() != null && + recursiveCfg.getBridge().getEnabled() && + (!recursiveCfg.getBridge().getRecursiveOnly() || isAllowedForRecursive(request.getRequester())) + ) { log.info("Using bridge failover for lookup") return bridge.find(request) } @@ -134,6 +147,16 @@ class RecursivePriorityLookupStrategy implements LookupStrategy, InitializingBea return Optional.empty() } + @Override + Optional find(SingleLookupRequest request) { + return find(request, false) + } + + @Override + Optional findRecursive(SingleLookupRequest request) { + return find(request, true) + } + @Override List find(BulkLookupRequest request) { List mapToDo = new ArrayList<>(request.getMappings()) diff --git a/src/main/groovy/io/kamax/mxisd/mapping/MappingManager.java b/src/main/groovy/io/kamax/mxisd/mapping/MappingManager.java index 1b929e9..2e7d879 100644 --- a/src/main/groovy/io/kamax/mxisd/mapping/MappingManager.java +++ b/src/main/groovy/io/kamax/mxisd/mapping/MappingManager.java @@ -1,7 +1,27 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.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 . + */ + package io.kamax.mxisd.mapping; import io.kamax.mxisd.exception.BadRequestException; -import io.kamax.mxisd.lookup.ThreePid; +import io.kamax.mxisd.lookup.ThreePidValidation; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -72,10 +92,10 @@ public class MappingManager { s.validationTimestamp = Instant.now(); } - public Optional getValidated(String sid, String secret) { + public Optional getValidated(String sid, String secret) { Session s = sessions.get(sid); if (s != null && StringUtils.equals(s.secret, secret)) { - return Optional.of(new ThreePid(s.medium, s.address, s.validationTimestamp)); + return Optional.of(new ThreePidValidation(s.medium, s.address, s.validationTimestamp)); } return Optional.empty(); diff --git a/src/main/groovy/io/kamax/mxisd/mapping/MappingSession.java b/src/main/groovy/io/kamax/mxisd/mapping/MappingSession.java index cf5d6a7..7d647d3 100644 --- a/src/main/groovy/io/kamax/mxisd/mapping/MappingSession.java +++ b/src/main/groovy/io/kamax/mxisd/mapping/MappingSession.java @@ -1,3 +1,23 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.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 . + */ + package io.kamax.mxisd.mapping; public interface MappingSession { diff --git a/src/main/groovy/io/kamax/mxisd/signature/SignatureManager.groovy b/src/main/groovy/io/kamax/mxisd/signature/SignatureManager.groovy index 2f4fb4e..84c4ad9 100644 --- a/src/main/groovy/io/kamax/mxisd/signature/SignatureManager.groovy +++ b/src/main/groovy/io/kamax/mxisd/signature/SignatureManager.groovy @@ -20,9 +20,11 @@ package io.kamax.mxisd.signature +import com.google.gson.JsonObject import io.kamax.mxisd.config.ServerConfig import io.kamax.mxisd.key.KeyManager import net.i2p.crypto.eddsa.EdDSAEngine +import org.json.JSONObject import org.springframework.beans.factory.InitializingBean import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Component @@ -40,14 +42,31 @@ class SignatureManager implements InitializingBean { private EdDSAEngine signEngine - Map signMessage(String message) { + private String sign(String message) { byte[] signRaw = signEngine.signOneShot(message.getBytes()) - String sign = Base64.getEncoder().encodeToString(signRaw) - return [ - "${srvCfg.getName()}": [ - "ed25519:${keyMgr.getCurrentIndex()}": sign - ] - ] + return Base64.getEncoder().encodeToString(signRaw) + } + + JSONObject signMessageJson(String message) { + String sign = sign(message) + + JSONObject keySignature = new JSONObject() + keySignature.put("ed25519:${keyMgr.getCurrentIndex()}", sign) + JSONObject signature = new JSONObject() + signature.put("${srvCfg.getName()}", keySignature) + + return signature + } + + JsonObject signMessageGson(String message) { + String sign = sign(message) + + JsonObject keySignature = new JsonObject() + keySignature.addProperty("ed25519:${keyMgr.getCurrentIndex()}", sign) + JsonObject signature = new JsonObject() + signature.add("${srvCfg.getName()}", keySignature); + + return signature } @Override diff --git a/src/main/groovy/io/kamax/mxisd/spring/ConfigurationFailureAnalyzer.java b/src/main/groovy/io/kamax/mxisd/spring/ConfigurationFailureAnalyzer.java index b6a0087..97fabf2 100644 --- a/src/main/groovy/io/kamax/mxisd/spring/ConfigurationFailureAnalyzer.java +++ b/src/main/groovy/io/kamax/mxisd/spring/ConfigurationFailureAnalyzer.java @@ -1,3 +1,23 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.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 . + */ + package io.kamax.mxisd.spring; import io.kamax.mxisd.exception.ConfigurationException; @@ -8,7 +28,11 @@ public class ConfigurationFailureAnalyzer extends AbstractFailureAnalyzer. + */ + +package io.kamax.mxisd.storage; + +import io.kamax.mxisd.invitation.IThreePidInviteReply; +import io.kamax.mxisd.storage.ormlite.ThreePidInviteIO; + +import java.util.Collection; + +public interface IStorage { + + Collection getInvites(); + + void insertInvite(IThreePidInviteReply data); + + void deleteInvite(String id); + +} diff --git a/src/main/groovy/io/kamax/mxisd/storage/ormlite/OrmLiteSqliteStorage.java b/src/main/groovy/io/kamax/mxisd/storage/ormlite/OrmLiteSqliteStorage.java new file mode 100644 index 0000000..1a37579 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/storage/ormlite/OrmLiteSqliteStorage.java @@ -0,0 +1,87 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.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 . + */ + +package io.kamax.mxisd.storage.ormlite; + +import com.j256.ormlite.dao.CloseableWrappedIterable; +import com.j256.ormlite.dao.Dao; +import com.j256.ormlite.dao.DaoManager; +import com.j256.ormlite.jdbc.JdbcConnectionSource; +import com.j256.ormlite.support.ConnectionSource; +import com.j256.ormlite.table.TableUtils; +import io.kamax.mxisd.invitation.IThreePidInviteReply; +import io.kamax.mxisd.storage.IStorage; + +import java.io.IOException; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +public class OrmLiteSqliteStorage implements IStorage { + + private Dao invDao; + + OrmLiteSqliteStorage(String path) { + try { + ConnectionSource connPool = new JdbcConnectionSource("jdbc:sqlite:" + path); + invDao = DaoManager.createDao(connPool, ThreePidInviteIO.class); + TableUtils.createTableIfNotExists(connPool, ThreePidInviteIO.class); + } catch (SQLException e) { + throw new RuntimeException(e); // FIXME do better + } + } + + @Override + public Collection getInvites() { + try (CloseableWrappedIterable t = invDao.getWrappedIterable()) { + List ioList = new ArrayList<>(); + t.forEach(ioList::add); + return ioList; + } catch (IOException e) { + throw new RuntimeException(e); // FIXME do better + } + } + + @Override + public void insertInvite(IThreePidInviteReply data) { + try { + int updated = invDao.create(new ThreePidInviteIO(data)); + if (updated != 1) { + throw new RuntimeException("Unexpected row count after DB action: " + updated); + } + } catch (SQLException e) { + throw new RuntimeException(e); // FIXME do better + } + } + + @Override + public void deleteInvite(String id) { + try { + int updated = invDao.deleteById(id); + if (updated != 1) { + throw new RuntimeException("Unexpected row count after DB action: " + updated); + } + } catch (SQLException e) { + throw new RuntimeException(e); // FIXME do better + } + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/storage/ormlite/OrmLiteSqliteStorageBeanFactory.java b/src/main/groovy/io/kamax/mxisd/storage/ormlite/OrmLiteSqliteStorageBeanFactory.java new file mode 100644 index 0000000..724dada --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/storage/ormlite/OrmLiteSqliteStorageBeanFactory.java @@ -0,0 +1,76 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.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 . + */ + +package io.kamax.mxisd.storage.ormlite; + +import io.kamax.mxisd.config.SQLiteStorageConfig; +import io.kamax.mxisd.config.StorageConfig; +import io.kamax.mxisd.exception.ConfigurationException; +import io.kamax.mxisd.storage.IStorage; +import org.apache.commons.lang.StringUtils; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.FactoryBeanNotInitializedException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; + +@Component +public class OrmLiteSqliteStorageBeanFactory implements FactoryBean { + + @Autowired + private StorageConfig storagecfg; + + @Autowired + private SQLiteStorageConfig cfg; + + private OrmLiteSqliteStorage storage; + + @PostConstruct + private void postConstruct() { + if (StringUtils.equals("sqlite", storagecfg.getBackend())) { + if (StringUtils.isBlank(cfg.getDatabase())) { + throw new ConfigurationException("storage.provider.sqlite.database"); + } + + storage = new OrmLiteSqliteStorage(cfg.getDatabase()); + } + } + + @Override + public IStorage getObject() throws Exception { + if (storage == null) { + throw new FactoryBeanNotInitializedException(); + } + + return storage; + } + + @Override + public Class getObjectType() { + return OrmLiteSqliteStorage.class; + } + + @Override + public boolean isSingleton() { + return true; + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/storage/ormlite/ThreePidInviteIO.java b/src/main/groovy/io/kamax/mxisd/storage/ormlite/ThreePidInviteIO.java new file mode 100644 index 0000000..91a9198 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/storage/ormlite/ThreePidInviteIO.java @@ -0,0 +1,106 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.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 . + */ + +package io.kamax.mxisd.storage.ormlite; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import com.j256.ormlite.field.DatabaseField; +import com.j256.ormlite.table.DatabaseTable; +import io.kamax.mxisd.invitation.IThreePidInviteReply; +import org.apache.commons.lang.StringUtils; + +import java.util.HashMap; +import java.util.Map; + +@DatabaseTable(tableName = "invite_3pid") +public class ThreePidInviteIO { + + private static Gson gson = new Gson(); + + @DatabaseField(id = true) + private String id; + + @DatabaseField(canBeNull = false) + private String token; + + @DatabaseField(canBeNull = false) + private String sender; + + @DatabaseField(canBeNull = false) + private String medium; + + @DatabaseField(canBeNull = false) + private String address; + + @DatabaseField(canBeNull = false) + private String roomId; + + @DatabaseField + private String properties; + + public ThreePidInviteIO() { + // needed for ORMlite + } + + public ThreePidInviteIO(IThreePidInviteReply data) { + this.id = data.getId(); + this.token = data.getToken(); + this.sender = data.getInvite().getSender().getId(); + this.medium = data.getInvite().getMedium(); + this.address = data.getInvite().getAddress(); + this.roomId = data.getInvite().getRoomId(); + this.properties = gson.toJson(data.getInvite().getProperties()); + } + + public String getId() { + return id; + } + + public String getToken() { + return token; + } + + public String getSender() { + return sender; + } + + public String getMedium() { + return medium; + } + + public String getAddress() { + return address; + } + + public String getRoomId() { + return roomId; + } + + public Map getProperties() { + if (StringUtils.isBlank(properties)) { + return new HashMap<>(); + } + + return gson.fromJson(properties, new TypeToken>() { + }.getType()); + } + +} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml new file mode 100644 index 0000000..3f11732 --- /dev/null +++ b/src/main/resources/application.yaml @@ -0,0 +1,45 @@ +logging: + level: + org: + springframework: "WARN" + apache: + catalina: "WARN" + directory: "WARN" + +server: + port: 8090 + +lookup: + recursive: + enabled: true + allowedCidr: + - '127.0.0.0/8' + - '10.0.0.0/8' + - '172.16.0.0/12' + - '192.168.0.0/16' + - '::1/128' + + bridge: + enabled: false + recursiveOnly: true + +ldap: + enabled: false + +firebase: + enabled: false + +forward: + servers: + - "https://matrix.org" + - "https://vector.im" + +invite: + sender: + email: + tls: 1 + name: "mxisd Identity Server" + template: "classpath:email/invite-template.eml" + +storage: + backend: 'sqlite' diff --git a/src/main/resources/email/invite-template.eml b/src/main/resources/email/invite-template.eml new file mode 100644 index 0000000..c422476 --- /dev/null +++ b/src/main/resources/email/invite-template.eml @@ -0,0 +1,97 @@ +Subject: You have been invited to %DOMAIN% +MIME-Version: 1.0 +Content-Type: multipart/alternative; + boundary="7REaIwWQCioQ6NaBlAQlg8ztbUQj6PKJ" + +--7REaIwWQCioQ6NaBlAQlg8ztbUQj6PKJ +Content-Type: text/plain; charset=UTF-8 +Content-Disposition: inline + +Hi, + +%SENDER_NAME_OR_ID% has invited you into a room [%ROOM_NAME_OR_ID%] on +Matrix. To join the conversation, register an account on http://%DOMAIN% + +You can also register an account on a public server, like Matrix.org, by going to +https://riot.im/app/#/register?%INVITE_MEDIUM%=%INVITE_ADDRESS% + + +About Matrix: + +Matrix is an open standard for interoperable, decentralised, real-time communication +over IP, supporting group chat, file transfer, voice and video calling, integrations to +other apps, bridges to other communication systems and much more. It can be used to power +Instant Messaging, VoIP/WebRTC signalling, Internet of Things communication. + +Thanks, + +%DOMAIN_PRETTY% Admins + +--7REaIwWQCioQ6NaBlAQlg8ztbUQj6PKJ +Content-Type: multipart/related; + boundary="M3yzHl5YZehm9v4bAM8sKEdcOoVnRnKR"; + type="text/html" + +--M3yzHl5YZehm9v4bAM8sKEdcOoVnRnKR +Content-Type: text/html; charset=UTF-8 +Content-Disposition: inline + + + + + + + + + + + + + +
+

Hi,

+ +

%SENDER_NAME_OR_ID% has invited you into a room [%ROOM_NAME_OR_ID%] on +Matrix. To join the conversation, register an account on %DOMAIN%.

+ +

You can also register an account on a public server, like Matrix.org, by following +this link.

+ +
+

About Matrix:

+ +

Matrix is an open standard for interoperable, decentralised, real-time communication + over IP, supporting group chat, file transfer, voice and video calling, integrations to + other apps, bridges to other communication systems and much more. It can be used to power + Instant Messaging, VoIP/WebRTC signalling, Internet of Things communication.

+ +

Thanks,

+ +

%DOMAIN_PRETTY% Admins

+
+ + +--M3yzHl5YZehm9v4bAM8sKEdcOoVnRnKR-- + +--7REaIwWQCioQ6NaBlAQlg8ztbUQj6PKJ--