Merge pull request #28 from kamax-io/invite-support

Invite support
This commit is contained in:
Max Dor
2017-09-14 02:49:24 +02:00
committed by GitHub
70 changed files with 2931 additions and 258 deletions

5
.gitignore vendored
View File

@@ -7,4 +7,7 @@ out/
.idea/
# Local dev config
application.yaml
/application.yaml
# Local dev storage
/mxisd.db

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

BIN
media/mx-id-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

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

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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();
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<AuthenticatorProvider> 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;
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<ThreePid> 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<ThreePid> getThreePids() {
return Collections.unmodifiableList(threePids);
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.auth.provider;
import io.kamax.mxisd.auth.UserAuthResult;

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<UserRecord> findInternal(String medium, String address) {
UserRecord r;
CountDownLatch l = new CountDownLatch(1);
OnSuccessListener<UserRecord> success = new OnSuccessListener<UserRecord>() {
@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<UserRecord> 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<ThreePidMapping> populate(List<ThreePidMapping> mappings) {
List<ThreePidMapping> results = new ArrayList<>();
mappings.parallelStream().forEach(new Consumer<ThreePidMapping>() {
@Override
void accept(ThreePidMapping o) {
Optional<UserRecord> 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<FirebaseToken>() {
@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<UserRecord>() {
@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;
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.auth.provider;
import io.kamax.matrix.MatrixID;

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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());
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.config
import org.slf4j.Logger

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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;
}
}

View File

@@ -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(), "<no reason provided>"))
}
log.info("--- Server config ---")
log.info("Name: {}", getName())
log.info("Port: {}", getPort())
log.info("Public URL: {}", getPublicUrl())
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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");
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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");
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.config.ldap;
import org.springframework.boot.context.properties.ConfigurationProperties;

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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());
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.config.ldap;
import org.springframework.boot.context.properties.ConfigurationProperties;

View File

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

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.config.ldap;
import org.springframework.boot.context.properties.ConfigurationProperties;

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.config.ldap;
import org.springframework.boot.context.properties.ConfigurationProperties;

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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."));
}
}

View File

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

View File

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

View File

@@ -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<SingleLookupReply> 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<ThreePidMapping> mappings = new ArrayList<>()

View File

@@ -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<ThreePid> result = mgr.getValidated(sid, secret)
Optional<ThreePidValidation> 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())

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.controller.v1;
import org.springframework.http.MediaType;

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.controller.v1.io;
import io.kamax.mxisd.mapping.MappingSession;

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.controller.v1.io;
public class SessionEmailTokenRequestJson extends GenericTokenRequestJson {

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.controller.v1.io;
import com.google.i18n.phonenumbers.NumberParseException;

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<String, Map<String, String>> 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();
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<Key> 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<Key> 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;
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<String> getDetailedMessage() {
return Optional.ofNullable(detailedMsg);
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.exception;
public class MappingAlreadyExistsException extends RuntimeException {
public MappingAlreadyExistsException() {
super("A mapping already exists for this 3PID");
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<String, String> getProperties();
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.invitation;
public interface IThreePidInviteReply {
String getId();
IThreePidInvite getInvite();
String getToken();
String getDisplayName();
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<String, IThreePidInviteReply> invitations = new ConcurrentHashMap<>();
@Autowired
private IStorage storage;
@Autowired
private LookupStrategy lookupMgr;
@Autowired
private SignatureManager signMgr;
private Map<String, IInviteSender> 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<ThreePidInviteIO> 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<SRVRecord> 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<IInviteSender> 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<SingleLookupReply> 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);
}
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<String, String> 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<String, String> 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<String, String> getProperties() {
return properties;
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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;
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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);
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.invitation.sender;
import io.kamax.mxisd.invitation.IThreePidInviteReply;
public interface IInviteSender {
String getMedium();
void send(IThreePidInviteReply invite);
}

View File

@@ -26,6 +26,7 @@ public abstract class ALookupRequest {
private String id;
private String requester;
private String userAgent;
private boolean isRecursive;
private List<String> 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;
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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;
}
}

View File

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

View File

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

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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;
}
}

View File

@@ -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<SingleLookupReply> find(SingleLookupRequest request)
List<ThreePidMapping> populate(List<ThreePidMapping> mappings)

View File

@@ -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<SingleLookupReply> find(String remote, SingleLookupRequest request)
List<ThreePidMapping> find(String remote, List<ThreePidMapping> mappings)

View File

@@ -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<SingleLookupReply> find(SingleLookupRequest request) {
Optional<String> 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");

View File

@@ -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<SingleLookupReply> 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<String> 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()

View File

@@ -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<SingleLookupReply> find(SingleLookupRequest request) {
for (String root : cfg.getServers()) {
Optional<?> answer = fetcher.find(root, request.getType(), request.getThreePid())
Optional<SingleLookupReply> answer = fetcher.find(root, request)
if (answer.isPresent()) {
return answer
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<UserRecord> findInternal(String medium, String address) {
UserRecord r;
CountDownLatch l = new CountDownLatch(1);
OnSuccessListener<UserRecord> success = new OnSuccessListener<UserRecord>() {
@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<SingleLookupReply> find(SingleLookupRequest request) {
Optional<UserRecord> urOpt = findInternal(request.getType(), request.getThreePid())
if (urOpt.isPresent()) {
return Optional.of(new SingleLookupReply(request, getMxid(urOpt.get())));
}
return Optional.empty();
}
@Override
public List<ThreePidMapping> populate(List<ThreePidMapping> mappings) {
List<ThreePidMapping> results = new ArrayList<>();
mappings.parallelStream().forEach(new Consumer<ThreePidMapping>() {
@Override
void accept(ThreePidMapping o) {
Optional<UserRecord> 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;
}
}

View File

@@ -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<SingleLookupReply> find(SingleLookupRequest request)
List<ThreePidMapping> populate(List<ThreePidMapping> mappings)

View File

@@ -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<SingleLookupReply> 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<String> 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()

View File

@@ -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<SingleLookupReply> 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)

View File

@@ -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<IThreePidProvider> getLocalProviders()
Optional<?> find(SingleLookupRequest request)
Optional<SingleLookupReply> find(String medium, String address, boolean recursive)
Optional<SingleLookupReply> find(SingleLookupRequest request)
Optional<SingleLookupReply> findRecursive(SingleLookupRequest request)
List<ThreePidMapping> find(BulkLookupRequest requests)

View File

@@ -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<IThreePidProvider> listUsableProviders(ALookupRequest request) {
return listUsableProviders(request, false);
}
List<IThreePidProvider> listUsableProviders(ALookupRequest request, boolean forceRecursive) {
List<IThreePidProvider> 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<SingleLookupReply> 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<SingleLookupReply> find(SingleLookupRequest request, boolean forceRecursive) {
for (IThreePidProvider provider : listUsableProviders(request, forceRecursive)) {
Optional<SingleLookupReply> 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<SingleLookupReply> find(SingleLookupRequest request) {
return find(request, false)
}
@Override
Optional<SingleLookupReply> findRecursive(SingleLookupRequest request) {
return find(request, true)
}
@Override
List<ThreePidMapping> find(BulkLookupRequest request) {
List<ThreePidMapping> mapToDo = new ArrayList<>(request.getMappings())

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<ThreePid> getValidated(String sid, String secret) {
public Optional<ThreePidValidation> 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();

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.mapping;
public interface MappingSession {

View File

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

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.spring;
import io.kamax.mxisd.exception.ConfigurationException;
@@ -8,7 +28,11 @@ public class ConfigurationFailureAnalyzer extends AbstractFailureAnalyzer<Config
@Override
protected FailureAnalysis analyze(Throwable rootFailure, ConfigurationException cause) {
return new FailureAnalysis(cause.getMessage(), "Double check the key value", cause);
String message = cause.getMessage();
if (cause.getDetailedMessage().isPresent()) {
message += " - " + cause.getDetailedMessage().get();
}
return new FailureAnalysis(message, "Double check the key value", cause);
}
}

View File

@@ -0,0 +1,36 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
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<ThreePidInviteIO> getInvites();
void insertInvite(IThreePidInviteReply data);
void deleteInvite(String id);
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<ThreePidInviteIO, String> 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<ThreePidInviteIO> getInvites() {
try (CloseableWrappedIterable<ThreePidInviteIO> t = invDao.getWrappedIterable()) {
List<ThreePidInviteIO> 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
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<IStorage> {
@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;
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<String, String> getProperties() {
if (StringUtils.isBlank(properties)) {
return new HashMap<>();
}
return gson.fromJson(properties, new TypeToken<Map<String, String>>() {
}.getType());
}
}

View File

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

View File

@@ -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
<!doctype html>
<html lang="en">
<head>
<style type="text/css">
body {
margin: 0px;
}
pre, code {
word-break: break-word;
white-space: pre-wrap;
}
#page {
font-family: 'Open Sans', Helvetica, Arial, Sans-Serif;
font-color: #454545;
font-size: 12pt;
width: 100%%;
padding: 20px;
}
#inner {
width: 640px;
}
</style>
</head>
<body>
<table id="page">
<tr>
<td> </td>
<td id="inner">
<p>Hi,</p>
<p>%SENDER_NAME_OR_ID% has invited you into a room [%ROOM_NAME_OR_ID%] on
Matrix. To join the conversation, register an account on <a href="http://%DOMAIN%">%DOMAIN%</a>.</p>
<p>You can also register an account on a public server, like Matrix.org, by following
<a href="https://riot.im/app/#/register?%INVITE_MEDIUM%=%INVITE_ADDRESS%">this link</a>.</p>
<br>
<p>About Matrix:</p>
<p>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.</p>
<p>Thanks,</p>
<p>%DOMAIN_PRETTY% Admins</p>
</td>
<td> </td>
</tr>
</table>
</body>
</html>
--M3yzHl5YZehm9v4bAM8sKEdcOoVnRnKR--
--7REaIwWQCioQ6NaBlAQlg8ztbUQj6PKJ--