Attempt to support invites, working in progress
This commit is contained in:
@@ -190,3 +190,43 @@ forward:
|
|||||||
servers:
|
servers:
|
||||||
- "https://matrix.org"
|
- "https://matrix.org"
|
||||||
- "https://vector.im"
|
- "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.kamax.io"
|
||||||
|
|
||||||
|
# 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 MIME content to send
|
||||||
|
contentPath: "/absolute/path/to/file"
|
||||||
|
@@ -97,6 +97,10 @@ dependencies {
|
|||||||
// Phone numbers validation
|
// Phone numbers validation
|
||||||
compile 'com.googlecode.libphonenumber:libphonenumber:8.7.1'
|
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
|
// Google Firebase Authentication backend
|
||||||
compile 'com.google.firebase:firebase-admin:5.3.0'
|
compile 'com.google.firebase:firebase-admin:5.3.0'
|
||||||
|
|
||||||
|
@@ -1,17 +1,14 @@
|
|||||||
package io.kamax.mxisd.lookup;
|
package io.kamax.mxisd;
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
|
|
||||||
|
// FIXME this should be in matrix-java-sdk
|
||||||
public class ThreePid {
|
public class ThreePid {
|
||||||
|
|
||||||
private String medium;
|
private String medium;
|
||||||
private String address;
|
private String address;
|
||||||
private Instant validation;
|
|
||||||
|
|
||||||
public ThreePid(String medium, String address, Instant validation) {
|
public ThreePid(String medium, String address) {
|
||||||
this.medium = medium;
|
this.medium = medium;
|
||||||
this.address = address;
|
this.address = address;
|
||||||
this.validation = validation;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getMedium() {
|
public String getMedium() {
|
||||||
@@ -22,8 +19,4 @@ public class ThreePid {
|
|||||||
return address;
|
return address;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Instant getValidation() {
|
|
||||||
return validation;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
@@ -2,14 +2,16 @@ package io.kamax.mxisd.auth.provider
|
|||||||
|
|
||||||
import com.google.firebase.FirebaseApp
|
import com.google.firebase.FirebaseApp
|
||||||
import com.google.firebase.FirebaseOptions
|
import com.google.firebase.FirebaseOptions
|
||||||
import com.google.firebase.auth.*
|
import com.google.firebase.auth.FirebaseAuth
|
||||||
|
import com.google.firebase.auth.FirebaseCredential
|
||||||
|
import com.google.firebase.auth.FirebaseCredentials
|
||||||
|
import com.google.firebase.auth.FirebaseToken
|
||||||
import com.google.firebase.internal.NonNull
|
import com.google.firebase.internal.NonNull
|
||||||
import com.google.firebase.tasks.OnFailureListener
|
import com.google.firebase.tasks.OnFailureListener
|
||||||
import com.google.firebase.tasks.OnSuccessListener
|
import com.google.firebase.tasks.OnSuccessListener
|
||||||
import io.kamax.matrix.ThreePidMedium
|
import io.kamax.matrix.ThreePidMedium
|
||||||
import io.kamax.mxisd.GlobalProvider
|
|
||||||
import io.kamax.mxisd.auth.UserAuthResult
|
import io.kamax.mxisd.auth.UserAuthResult
|
||||||
import io.kamax.mxisd.lookup.SingleLookupRequest
|
import io.kamax.mxisd.invitation.InvitationManager
|
||||||
import io.kamax.mxisd.lookup.ThreePidMapping
|
import io.kamax.mxisd.lookup.ThreePidMapping
|
||||||
import org.apache.commons.lang.StringUtils
|
import org.apache.commons.lang.StringUtils
|
||||||
import org.slf4j.Logger
|
import org.slf4j.Logger
|
||||||
@@ -17,11 +19,10 @@ import org.slf4j.LoggerFactory
|
|||||||
|
|
||||||
import java.util.concurrent.CountDownLatch
|
import java.util.concurrent.CountDownLatch
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import java.util.function.Consumer
|
|
||||||
import java.util.regex.Matcher
|
import java.util.regex.Matcher
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
|
|
||||||
public class GoogleFirebaseAuthenticator implements GlobalProvider {
|
public class GoogleFirebaseAuthenticator implements AuthenticatorProvider {
|
||||||
|
|
||||||
private Logger log = LoggerFactory.getLogger(GoogleFirebaseAuthenticator.class);
|
private Logger log = LoggerFactory.getLogger(GoogleFirebaseAuthenticator.class);
|
||||||
|
|
||||||
@@ -32,12 +33,15 @@ public class GoogleFirebaseAuthenticator implements GlobalProvider {
|
|||||||
private FirebaseApp fbApp;
|
private FirebaseApp fbApp;
|
||||||
private FirebaseAuth fbAuth;
|
private FirebaseAuth fbAuth;
|
||||||
|
|
||||||
public GoogleFirebaseAuthenticator(boolean isEnabled) {
|
private InvitationManager invMgr;
|
||||||
|
|
||||||
|
public GoogleFirebaseAuthenticator(InvitationManager invMgr, boolean isEnabled) {
|
||||||
this.isEnabled = isEnabled;
|
this.isEnabled = isEnabled;
|
||||||
|
this.invMgr = invMgr;
|
||||||
}
|
}
|
||||||
|
|
||||||
public GoogleFirebaseAuthenticator(String credsPath, String db, String domain) {
|
public GoogleFirebaseAuthenticator(InvitationManager invMgr, String credsPath, String db, String domain) {
|
||||||
this(true);
|
this(invMgr, true);
|
||||||
this.domain = domain;
|
this.domain = domain;
|
||||||
try {
|
try {
|
||||||
fbApp = FirebaseApp.initializeApp(getOpts(credsPath, db));
|
fbApp = FirebaseApp.initializeApp(getOpts(credsPath, db));
|
||||||
@@ -73,16 +77,6 @@ public class GoogleFirebaseAuthenticator implements GlobalProvider {
|
|||||||
return isEnabled;
|
return isEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isLocal() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getPriority() {
|
|
||||||
return 25;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void waitOnLatch(CountDownLatch l) {
|
private void waitOnLatch(CountDownLatch l) {
|
||||||
try {
|
try {
|
||||||
l.await(30, TimeUnit.SECONDS);
|
l.await(30, TimeUnit.SECONDS);
|
||||||
@@ -91,84 +85,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
|
@Override
|
||||||
public UserAuthResult authenticate(String id, String password) {
|
public UserAuthResult authenticate(String id, String password) {
|
||||||
if (!isEnabled()) {
|
if (!isEnabled()) {
|
||||||
@@ -190,27 +106,37 @@ public class GoogleFirebaseAuthenticator implements GlobalProvider {
|
|||||||
fbAuth.verifyIdToken(password).addOnSuccessListener(new OnSuccessListener<FirebaseToken>() {
|
fbAuth.verifyIdToken(password).addOnSuccessListener(new OnSuccessListener<FirebaseToken>() {
|
||||||
@Override
|
@Override
|
||||||
void onSuccess(FirebaseToken token) {
|
void onSuccess(FirebaseToken token) {
|
||||||
if (!StringUtils.equals(localpart, token.getUid())) {
|
try {
|
||||||
log.info("Failture to authenticate {}: Matrix ID localpart '{}' does not match Firebase UID '{}'", id, localpart, token.getUid());
|
if (!StringUtils.equals(localpart, token.getUid())) {
|
||||||
result.failure();
|
log.info("Failture to authenticate {}: Matrix ID localpart '{}' does not match Firebase UID '{}'", id, localpart, token.getUid());
|
||||||
}
|
result.failure();
|
||||||
|
}
|
||||||
|
|
||||||
log.info("{} was successfully authenticated", id);
|
log.info("{} was successfully authenticated", id);
|
||||||
result.success(id, token.getName());
|
result.success(id, token.getName());
|
||||||
l.countDown()
|
|
||||||
|
if (StringUtils.isNotBlank(token.getEmail())) {
|
||||||
|
invMgr.publishMappingIfInvited(new ThreePidMapping(ThreePidMedium.Email.getId(), token.getEmail(), id))
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
l.countDown()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}).addOnFailureListener(new OnFailureListener() {
|
}).addOnFailureListener(new OnFailureListener() {
|
||||||
@Override
|
@Override
|
||||||
void onFailure(@NonNull Exception e) {
|
void onFailure(@NonNull Exception e) {
|
||||||
if (e instanceof IllegalArgumentException) {
|
try {
|
||||||
log.info("Failure to authenticate {}: invalid firebase token", id);
|
if (e instanceof IllegalArgumentException) {
|
||||||
} else {
|
log.info("Failure to authenticate {}: invalid firebase token", id);
|
||||||
log.info("Failure to authenticate {}: {}", id, e.getMessage(), e);
|
} else {
|
||||||
log.info("Exception", e);
|
log.info("Failure to authenticate {}: {}", id, e.getMessage(), e);
|
||||||
}
|
log.info("Exception", e);
|
||||||
|
}
|
||||||
|
|
||||||
result.failure();
|
result.failure();
|
||||||
l.countDown()
|
} finally {
|
||||||
|
l.countDown()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -1,7 +1,10 @@
|
|||||||
package io.kamax.mxisd.config;
|
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.auth.provider.GoogleFirebaseAuthenticator;
|
||||||
|
import io.kamax.mxisd.invitation.InvitationManager;
|
||||||
|
import io.kamax.mxisd.lookup.provider.GoogleFirebaseProvider;
|
||||||
|
import io.kamax.mxisd.lookup.provider.IThreePidProvider;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
@@ -20,6 +23,9 @@ public class FirebaseConfig {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private ServerConfig srvCfg;
|
private ServerConfig srvCfg;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private InvitationManager invMgr;
|
||||||
|
|
||||||
private boolean enabled;
|
private boolean enabled;
|
||||||
private String credentials;
|
private String credentials;
|
||||||
private String database;
|
private String database;
|
||||||
@@ -56,16 +62,24 @@ public class FirebaseConfig {
|
|||||||
log.info("Credentials: {}", getCredentials());
|
log.info("Credentials: {}", getCredentials());
|
||||||
log.info("Database: {}", getDatabase());
|
log.info("Database: {}", getDatabase());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public GlobalProvider getProvider() {
|
public AuthenticatorProvider getAuthProvider() {
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
return new GoogleFirebaseAuthenticator(false);
|
return new GoogleFirebaseAuthenticator(invMgr, false);
|
||||||
} else {
|
} else {
|
||||||
return new GoogleFirebaseAuthenticator(credentials, database, srvCfg.getName());
|
return new GoogleFirebaseAuthenticator(invMgr, credentials, database, srvCfg.getName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public IThreePidProvider getLookupProvider() {
|
||||||
|
if (!enabled) {
|
||||||
|
return new GoogleFirebaseProvider(false);
|
||||||
|
} else {
|
||||||
|
return new GoogleFirebaseProvider(credentials, database, srvCfg.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,113 @@
|
|||||||
|
package io.kamax.mxisd.config.invite.sender;
|
||||||
|
|
||||||
|
import io.kamax.mxisd.exception.ConfigurationException;
|
||||||
|
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 contentPath;
|
||||||
|
|
||||||
|
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 getContentPath() {
|
||||||
|
return contentPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setContentPath(String contentPath) {
|
||||||
|
this.contentPath = contentPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
private void postConstruct() {
|
||||||
|
if (StringUtils.isBlank(getContentPath())) {
|
||||||
|
throw new ConfigurationException("invite.sender.email.contentPath");
|
||||||
|
}
|
||||||
|
|
||||||
|
File cp = new File(getContentPath()).getAbsoluteFile();
|
||||||
|
if (!cp.exists() || !cp.isFile() || !cp.canRead()) {
|
||||||
|
throw new ConfigurationException("invite.sender.email.contentPath", getContentPath() + " does not exist, is not a file or cannot be read");
|
||||||
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
log.info("Content path: {}", cp.getAbsolutePath());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,51 @@
|
|||||||
|
package io.kamax.mxisd.controller.v1;
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
static String handle(String erroCode, String error) {
|
||||||
|
return "{\"errcode\":\"" + erroCode + "\",\"error\":\"" + error + "\"}";
|
||||||
|
}
|
||||||
|
|
||||||
|
@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."));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -20,10 +20,19 @@
|
|||||||
|
|
||||||
package io.kamax.mxisd.controller.v1
|
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.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.Logger
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
import org.springframework.web.bind.annotation.RequestMapping
|
import org.springframework.web.bind.annotation.RequestMapping
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam
|
||||||
import org.springframework.web.bind.annotation.RestController
|
import org.springframework.web.bind.annotation.RestController
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest
|
import javax.servlet.http.HttpServletRequest
|
||||||
@@ -35,11 +44,25 @@ class InvitationController {
|
|||||||
|
|
||||||
private Logger log = LoggerFactory.getLogger(InvitationController.class)
|
private Logger log = LoggerFactory.getLogger(InvitationController.class)
|
||||||
|
|
||||||
@RequestMapping(value = "/_matrix/identity/api/v1/store-invite", method = POST)
|
@Autowired
|
||||||
String store(HttpServletRequest request) {
|
private InvitationManager mgr
|
||||||
log.error("{} was requested but not implemented", request.getRequestURL())
|
|
||||||
|
|
||||||
throw new NotImplementedException()
|
@Autowired
|
||||||
|
private KeyManager keyMgr
|
||||||
|
|
||||||
|
private Gson gson = new Gson()
|
||||||
|
|
||||||
|
@RequestMapping(value = "/_matrix/identity/api/v1/store-invite", method = POST)
|
||||||
|
String store(
|
||||||
|
HttpServletRequest request,
|
||||||
|
@RequestParam String sender,
|
||||||
|
@RequestParam String medium,
|
||||||
|
@RequestParam String address,
|
||||||
|
@RequestParam("room_id") String roomId) {
|
||||||
|
IThreePidInvite invite = new ThreePidInvite(new MatrixID(sender), medium, address, roomId)
|
||||||
|
IThreePidInviteReply reply = mgr.storeInvite(invite)
|
||||||
|
|
||||||
|
return gson.toJson(new ThreePidInviteReplyIO(reply, keyMgr.getPublicKeyBase64(keyMgr.getCurrentIndex())))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -25,7 +25,7 @@ import com.google.gson.JsonObject
|
|||||||
import io.kamax.mxisd.controller.v1.io.SessionEmailTokenRequestJson
|
import io.kamax.mxisd.controller.v1.io.SessionEmailTokenRequestJson
|
||||||
import io.kamax.mxisd.controller.v1.io.SessionPhoneTokenRequestJson
|
import io.kamax.mxisd.controller.v1.io.SessionPhoneTokenRequestJson
|
||||||
import io.kamax.mxisd.exception.BadRequestException
|
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 io.kamax.mxisd.mapping.MappingManager
|
||||||
import org.apache.commons.io.IOUtils
|
import org.apache.commons.io.IOUtils
|
||||||
import org.apache.commons.lang.StringUtils
|
import org.apache.commons.lang.StringUtils
|
||||||
@@ -93,10 +93,10 @@ class SessionController {
|
|||||||
@RequestParam String sid, @RequestParam("client_secret") String secret) {
|
@RequestParam String sid, @RequestParam("client_secret") String secret) {
|
||||||
log.info("Requested: {}?{}", request.getRequestURL(), request.getQueryString())
|
log.info("Requested: {}?{}", request.getRequestURL(), request.getQueryString())
|
||||||
|
|
||||||
Optional<ThreePid> result = mgr.getValidated(sid, secret)
|
Optional<ThreePidValidation> result = mgr.getValidated(sid, secret)
|
||||||
if (result.isPresent()) {
|
if (result.isPresent()) {
|
||||||
log.info("requested session was validated")
|
log.info("requested session was validated")
|
||||||
ThreePid pid = result.get()
|
ThreePidValidation pid = result.get()
|
||||||
|
|
||||||
JsonObject obj = new JsonObject()
|
JsonObject obj = new JsonObject()
|
||||||
obj.addProperty("medium", pid.getMedium())
|
obj.addProperty("medium", pid.getMedium())
|
||||||
|
@@ -0,0 +1,31 @@
|
|||||||
|
package io.kamax.mxisd.controller.v1.io;
|
||||||
|
|
||||||
|
import io.kamax.mxisd.invitation.IThreePidInviteReply;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
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) {
|
||||||
|
this.token = reply.getToken();
|
||||||
|
this.public_keys = new ArrayList<>(Arrays.asList(new Key(pubKey)));
|
||||||
|
this.display_name = reply.getDisplayName();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Key {
|
||||||
|
private String key_validity_url;
|
||||||
|
private String public_key;
|
||||||
|
|
||||||
|
public Key(String key) {
|
||||||
|
this.key_validity_url = "https://example.org/_matrix/fixme"; // FIXME have a proper URL even if synapse does not check
|
||||||
|
this.public_key = key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -1,11 +1,27 @@
|
|||||||
package io.kamax.mxisd.exception;
|
package io.kamax.mxisd.exception;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
public class ConfigurationException extends RuntimeException {
|
public class ConfigurationException extends RuntimeException {
|
||||||
|
|
||||||
private String key;
|
private String key;
|
||||||
|
private String detailedMsg;
|
||||||
|
|
||||||
public ConfigurationException(String key) {
|
public ConfigurationException(String key) {
|
||||||
super("Invalid or empty value for configuration key " + 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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,9 @@
|
|||||||
|
package io.kamax.mxisd.exception;
|
||||||
|
|
||||||
|
public class MappingAlreadyExistsException extends RuntimeException {
|
||||||
|
|
||||||
|
public MappingAlreadyExistsException() {
|
||||||
|
super("A mapping already exists for this 3PID");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,15 @@
|
|||||||
|
package io.kamax.mxisd.invitation;
|
||||||
|
|
||||||
|
import io.kamax.matrix._MatrixID;
|
||||||
|
|
||||||
|
public interface IThreePidInvite {
|
||||||
|
|
||||||
|
_MatrixID getSender();
|
||||||
|
|
||||||
|
String getMedium();
|
||||||
|
|
||||||
|
String getAddress();
|
||||||
|
|
||||||
|
String getRoomId();
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,11 @@
|
|||||||
|
package io.kamax.mxisd.invitation;
|
||||||
|
|
||||||
|
public interface IThreePidInviteReply {
|
||||||
|
|
||||||
|
IThreePidInvite getInvite();
|
||||||
|
|
||||||
|
String getToken();
|
||||||
|
|
||||||
|
String getDisplayName();
|
||||||
|
|
||||||
|
}
|
226
src/main/groovy/io/kamax/mxisd/invitation/InvitationManager.java
Normal file
226
src/main/groovy/io/kamax/mxisd/invitation/InvitationManager.java
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
package io.kamax.mxisd.invitation;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import io.kamax.matrix.ThreePid;
|
||||||
|
import io.kamax.mxisd.exception.BadRequestException;
|
||||||
|
import io.kamax.mxisd.exception.MappingAlreadyExistsException;
|
||||||
|
import io.kamax.mxisd.invitation.sender.IInviteSender;
|
||||||
|
import io.kamax.mxisd.lookup.SingleLookupRequest;
|
||||||
|
import io.kamax.mxisd.lookup.ThreePidMapping;
|
||||||
|
import io.kamax.mxisd.lookup.strategy.LookupStrategy;
|
||||||
|
import io.kamax.mxisd.signature.SignatureManager;
|
||||||
|
import org.apache.commons.lang.RandomStringUtils;
|
||||||
|
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||||
|
import org.apache.http.client.methods.HttpPost;
|
||||||
|
import org.apache.http.config.Registry;
|
||||||
|
import org.apache.http.config.RegistryBuilder;
|
||||||
|
import org.apache.http.conn.socket.ConnectionSocketFactory;
|
||||||
|
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
|
||||||
|
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
|
||||||
|
import org.apache.http.conn.ssl.TrustStrategy;
|
||||||
|
import org.apache.http.entity.StringEntity;
|
||||||
|
import org.apache.http.impl.client.CloseableHttpClient;
|
||||||
|
import org.apache.http.impl.client.HttpClientBuilder;
|
||||||
|
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
|
||||||
|
import org.apache.http.ssl.SSLContextBuilder;
|
||||||
|
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.Lookup;
|
||||||
|
import org.xbill.DNS.SRVRecord;
|
||||||
|
import org.xbill.DNS.TextParseException;
|
||||||
|
import org.xbill.DNS.Type;
|
||||||
|
|
||||||
|
import javax.annotation.PostConstruct;
|
||||||
|
import javax.net.ssl.HostnameVerifier;
|
||||||
|
import javax.net.ssl.SSLContext;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class InvitationManager {
|
||||||
|
|
||||||
|
private Logger log = LoggerFactory.getLogger(InvitationManager.class);
|
||||||
|
|
||||||
|
private Map<ThreePid, IThreePidInviteReply> invitations = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private LookupStrategy lookupMgr;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private SignatureManager signMgr;
|
||||||
|
|
||||||
|
private Map<String, IInviteSender> senders;
|
||||||
|
|
||||||
|
private CloseableHttpClient client;
|
||||||
|
private Gson gson;
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
private void postConstruct() {
|
||||||
|
gson = new Gson();
|
||||||
|
|
||||||
|
try {
|
||||||
|
HttpClientBuilder b = HttpClientBuilder.create();
|
||||||
|
|
||||||
|
// setup a Trust Strategy that allows all certificates.
|
||||||
|
//
|
||||||
|
SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustStrategy() {
|
||||||
|
public boolean isTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}).build();
|
||||||
|
b.setSslcontext(sslContext);
|
||||||
|
|
||||||
|
// don't check Hostnames, either.
|
||||||
|
// -- use SSLConnectionSocketFactory.getDefaultHostnameVerifier(), if you don't want to weaken
|
||||||
|
HostnameVerifier hostnameVerifier = SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER;
|
||||||
|
|
||||||
|
// here's the special part:
|
||||||
|
// -- need to create an SSL Socket Factory, to use our weakened "trust strategy";
|
||||||
|
// -- and create a Registry, to register it.
|
||||||
|
//
|
||||||
|
SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(sslContext, hostnameVerifier);
|
||||||
|
Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
|
||||||
|
.register("http", PlainConnectionSocketFactory.getSocketFactory())
|
||||||
|
.register("https", sslSocketFactory)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// now, we create connection-manager using our Registry.
|
||||||
|
// -- allows multi-threaded use
|
||||||
|
PoolingHttpClientConnectionManager connMgr = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
|
||||||
|
b.setConnectionManager(connMgr);
|
||||||
|
|
||||||
|
// finally, build the HttpClient;
|
||||||
|
// -- done!
|
||||||
|
client = b.build();
|
||||||
|
} catch (Exception e) {
|
||||||
|
// FIXME do better...
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String getSrvRecordName(String domain) {
|
||||||
|
return "_matrix._tcp." + domain;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO use caching mechanism
|
||||||
|
// TODO export in matrix-java-sdk
|
||||||
|
Optional<String> findHomeserverForDomain(String domain) {
|
||||||
|
log.debug("Performing SRV lookup for {}", domain);
|
||||||
|
String lookupDns = getSrvRecordName(domain);
|
||||||
|
log.info("Lookup name: {}", lookupDns);
|
||||||
|
|
||||||
|
try {
|
||||||
|
SRVRecord[] records = (SRVRecord[]) new Lookup(lookupDns, Type.SRV).run();
|
||||||
|
if (records != null) {
|
||||||
|
Arrays.sort(records, Comparator.comparingInt(SRVRecord::getPriority));
|
||||||
|
for (SRVRecord record : records) {
|
||||||
|
log.info("Found SRV record: {}", record.toString());
|
||||||
|
return Optional.of("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 Optional.of("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");
|
||||||
|
}
|
||||||
|
|
||||||
|
ThreePid pid = new ThreePid(invitation.getMedium(), invitation.getAddress());
|
||||||
|
|
||||||
|
log.info("Storing invite for {}:{} from {} in room {}", pid.getMedium(), pid.getAddress(), invitation.getSender(), invitation.getRoomId());
|
||||||
|
if (invitations.containsKey(pid)) {
|
||||||
|
log.info("Invite is already pending for {}:{}, returning data", pid.getMedium(), pid.getAddress());
|
||||||
|
return invitations.get(pid);
|
||||||
|
}
|
||||||
|
|
||||||
|
SingleLookupRequest request = new SingleLookupRequest();
|
||||||
|
request.setType(invitation.getMedium());
|
||||||
|
request.setThreePid(invitation.getAddress());
|
||||||
|
request.setRecursive(true);
|
||||||
|
request.setRequester("Internal");
|
||||||
|
|
||||||
|
Optional<?> result = lookupMgr.findRecursive(request);
|
||||||
|
if (result.isPresent()) {
|
||||||
|
log.info("Mapping for {}:{} already exists, refusing to store invite", pid.getMedium(), pid.getAddress());
|
||||||
|
throw new MappingAlreadyExistsException();
|
||||||
|
}
|
||||||
|
|
||||||
|
String token = RandomStringUtils.randomAlphanumeric(64);
|
||||||
|
String displayName = invitation.getAddress().substring(0, 3) + "...";
|
||||||
|
|
||||||
|
IThreePidInviteReply reply = new ThreePidInviteReply(invitation, token, displayName);
|
||||||
|
|
||||||
|
log.info("Performing invite to {}:{}", pid.getMedium(), pid.getAddress());
|
||||||
|
sender.send(reply);
|
||||||
|
|
||||||
|
invitations.put(pid, reply);
|
||||||
|
log.info("A new invite has been created for {}:{}", pid.getMedium(), pid.getAddress());
|
||||||
|
|
||||||
|
return reply;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void publishMappingIfInvited(ThreePidMapping threePid) {
|
||||||
|
ThreePid key = new ThreePid(threePid.getMedium(), threePid.getValue());
|
||||||
|
IThreePidInviteReply reply = invitations.get(key);
|
||||||
|
if (reply == null) {
|
||||||
|
log.info("{}:{} does not have a pending invite, no mapping to publish", threePid.getMedium(), threePid.getValue());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("{}:{} has an invite pending, publishing mapping", threePid.getMedium(), threePid.getValue());
|
||||||
|
String domain = reply.getInvite().getSender().getDomain();
|
||||||
|
log.info("Discovering HS for domain {}", domain);
|
||||||
|
Optional<String> hsUrlOpt = findHomeserverForDomain(domain);
|
||||||
|
if (!hsUrlOpt.isPresent()) {
|
||||||
|
log.warn("No HS found for domain {} - ignoring publishing", domain);
|
||||||
|
} else {
|
||||||
|
HttpPost req = new HttpPost(hsUrlOpt.get() + "/_matrix/federation/v1/3pid/onbind");
|
||||||
|
JSONObject obj = new JSONObject(); // TODO use Gson instead
|
||||||
|
obj.put("mxisd", threePid.getMxid());
|
||||||
|
obj.put("token", reply.getToken());
|
||||||
|
String mapping = gson.toJson(signMgr.signMessage(obj.toString())); // FIXME we shouldn't need to be doign this
|
||||||
|
|
||||||
|
JSONObject content = new JSONObject(); // TODO use Gson instead
|
||||||
|
content.put("invites", Collections.singletonList(mapping));
|
||||||
|
content.put("medium", threePid.getMedium());
|
||||||
|
content.put("address", threePid.getValue());
|
||||||
|
content.put("mxid", threePid.getMxid());
|
||||||
|
|
||||||
|
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);
|
||||||
|
response.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("Unable to tell HS {} about invite being mapped", domain, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
invitations.remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,39 @@
|
|||||||
|
package io.kamax.mxisd.invitation;
|
||||||
|
|
||||||
|
import io.kamax.matrix._MatrixID;
|
||||||
|
|
||||||
|
public class ThreePidInvite implements IThreePidInvite {
|
||||||
|
|
||||||
|
private _MatrixID sender;
|
||||||
|
private String medium;
|
||||||
|
private String address;
|
||||||
|
private String roomId;
|
||||||
|
|
||||||
|
public ThreePidInvite(_MatrixID sender, String medium, String address, String roomId) {
|
||||||
|
this.sender = sender;
|
||||||
|
this.medium = medium;
|
||||||
|
this.address = address;
|
||||||
|
this.roomId = roomId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public _MatrixID getSender() {
|
||||||
|
return sender;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getMedium() {
|
||||||
|
return medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getAddress() {
|
||||||
|
return address;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getRoomId() {
|
||||||
|
return roomId;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,30 @@
|
|||||||
|
package io.kamax.mxisd.invitation;
|
||||||
|
|
||||||
|
public class ThreePidInviteReply implements IThreePidInviteReply {
|
||||||
|
|
||||||
|
private IThreePidInvite invite;
|
||||||
|
private String token;
|
||||||
|
private String displayName;
|
||||||
|
|
||||||
|
public ThreePidInviteReply(IThreePidInvite invite, String token, String displayName) {
|
||||||
|
this.invite = invite;
|
||||||
|
this.token = token;
|
||||||
|
this.displayName = displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IThreePidInvite getInvite() {
|
||||||
|
return invite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getToken() {
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDisplayName() {
|
||||||
|
return displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,82 @@
|
|||||||
|
package io.kamax.mxisd.invitation.sender;
|
||||||
|
|
||||||
|
import com.sun.mail.smtp.SMTPTransport;
|
||||||
|
import io.kamax.matrix.ThreePidMedium;
|
||||||
|
import io.kamax.mxisd.config.invite.sender.EmailSenderConfig;
|
||||||
|
import io.kamax.mxisd.exception.ConfigurationException;
|
||||||
|
import io.kamax.mxisd.invitation.IThreePidInviteReply;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
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.util.Date;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class EmailInviteSender implements IInviteSender {
|
||||||
|
|
||||||
|
private Logger log = LoggerFactory.getLogger(EmailInviteSender.class);
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private EmailSenderConfig cfg;
|
||||||
|
|
||||||
|
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 {
|
||||||
|
MimeMessage msg = new MimeMessage(session, new FileInputStream(cfg.getContentPath()));
|
||||||
|
msg.setHeader("X-Mailer", "mxisd");
|
||||||
|
msg.setSentDate(new Date());
|
||||||
|
msg.setRecipients(Message.RecipientType.TO, invite.getInvite().getAddress());
|
||||||
|
msg.setFrom(sender);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,11 @@
|
|||||||
|
package io.kamax.mxisd.invitation.sender;
|
||||||
|
|
||||||
|
import io.kamax.mxisd.invitation.IThreePidInviteReply;
|
||||||
|
|
||||||
|
public interface IInviteSender {
|
||||||
|
|
||||||
|
String getMedium();
|
||||||
|
|
||||||
|
void send(IThreePidInviteReply invite);
|
||||||
|
|
||||||
|
}
|
@@ -28,6 +28,16 @@ public class ThreePidMapping {
|
|||||||
private String value;
|
private String value;
|
||||||
private String mxid;
|
private String mxid;
|
||||||
|
|
||||||
|
public ThreePidMapping() {
|
||||||
|
// stub
|
||||||
|
}
|
||||||
|
|
||||||
|
public ThreePidMapping(String medium, String value, String mxid) {
|
||||||
|
setMedium(medium);
|
||||||
|
setValue(value);
|
||||||
|
setMxid(mxid);
|
||||||
|
}
|
||||||
|
|
||||||
public String getMedium() {
|
public String getMedium() {
|
||||||
return medium;
|
return medium;
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,20 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,172 @@
|
|||||||
|
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.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));
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
@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<?> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -31,6 +31,8 @@ interface LookupStrategy {
|
|||||||
|
|
||||||
Optional<?> find(SingleLookupRequest request)
|
Optional<?> find(SingleLookupRequest request)
|
||||||
|
|
||||||
|
Optional<?> findRecursive(SingleLookupRequest request)
|
||||||
|
|
||||||
List<ThreePidMapping> find(BulkLookupRequest requests)
|
List<ThreePidMapping> find(BulkLookupRequest requests)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -93,13 +93,17 @@ class RecursivePriorityLookupStrategy implements LookupStrategy, InitializingBea
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<IThreePidProvider> listUsableProviders(ALookupRequest request) {
|
List<IThreePidProvider> listUsableProviders(ALookupRequest request) {
|
||||||
|
return listUsableProviders(request, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<IThreePidProvider> listUsableProviders(ALookupRequest request, boolean forceRecursive) {
|
||||||
List<IThreePidProvider> usableProviders = new ArrayList<>()
|
List<IThreePidProvider> usableProviders = new ArrayList<>()
|
||||||
|
|
||||||
boolean canRecurse = isAllowedForRecursive(request.getRequester())
|
boolean canRecurse = isAllowedForRecursive(request.getRequester())
|
||||||
|
|
||||||
log.info("Host {} allowed for recursion: {}", request.getRequester(), canRecurse)
|
log.info("Host {} allowed for recursion: {}", request.getRequester(), canRecurse)
|
||||||
for (IThreePidProvider provider : providers) {
|
for (IThreePidProvider provider : providers) {
|
||||||
if (provider.isEnabled() && (provider.isLocal() || canRecurse)) {
|
if (provider.isEnabled() && (provider.isLocal() || canRecurse || forceRecursive)) {
|
||||||
usableProviders.add(provider)
|
usableProviders.add(provider)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -117,9 +121,8 @@ class RecursivePriorityLookupStrategy implements LookupStrategy, InitializingBea
|
|||||||
}).collect(Collectors.toList())
|
}).collect(Collectors.toList())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
Optional<?> find(SingleLookupRequest request, boolean forceRecursive) {
|
||||||
Optional<?> find(SingleLookupRequest request) {
|
for (IThreePidProvider provider : listUsableProviders(request, forceRecursive)) {
|
||||||
for (IThreePidProvider provider : listUsableProviders(request)) {
|
|
||||||
Optional<?> lookupDataOpt = provider.find(request)
|
Optional<?> lookupDataOpt = provider.find(request)
|
||||||
if (lookupDataOpt.isPresent()) {
|
if (lookupDataOpt.isPresent()) {
|
||||||
return lookupDataOpt
|
return lookupDataOpt
|
||||||
@@ -134,6 +137,16 @@ class RecursivePriorityLookupStrategy implements LookupStrategy, InitializingBea
|
|||||||
return Optional.empty()
|
return Optional.empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
Optional<?> find(SingleLookupRequest request) {
|
||||||
|
return find(request, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
Optional<?> findRecursive(SingleLookupRequest request) {
|
||||||
|
return find(request, true)
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
List<ThreePidMapping> find(BulkLookupRequest request) {
|
List<ThreePidMapping> find(BulkLookupRequest request) {
|
||||||
List<ThreePidMapping> mapToDo = new ArrayList<>(request.getMappings())
|
List<ThreePidMapping> mapToDo = new ArrayList<>(request.getMappings())
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
package io.kamax.mxisd.mapping;
|
package io.kamax.mxisd.mapping;
|
||||||
|
|
||||||
import io.kamax.mxisd.exception.BadRequestException;
|
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.apache.commons.lang.StringUtils;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@@ -72,10 +72,10 @@ public class MappingManager {
|
|||||||
s.validationTimestamp = Instant.now();
|
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);
|
Session s = sessions.get(sid);
|
||||||
if (s != null && StringUtils.equals(s.secret, secret)) {
|
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();
|
return Optional.empty();
|
||||||
|
@@ -40,6 +40,7 @@ class SignatureManager implements InitializingBean {
|
|||||||
|
|
||||||
private EdDSAEngine signEngine
|
private EdDSAEngine signEngine
|
||||||
|
|
||||||
|
// FIXME we need to return a proper object, or a string with the signature and all?
|
||||||
Map<?, ?> signMessage(String message) {
|
Map<?, ?> signMessage(String message) {
|
||||||
byte[] signRaw = signEngine.signOneShot(message.getBytes())
|
byte[] signRaw = signEngine.signOneShot(message.getBytes())
|
||||||
String sign = Base64.getEncoder().encodeToString(signRaw)
|
String sign = Base64.getEncoder().encodeToString(signRaw)
|
||||||
|
@@ -8,7 +8,11 @@ public class ConfigurationFailureAnalyzer extends AbstractFailureAnalyzer<Config
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected FailureAnalysis analyze(Throwable rootFailure, ConfigurationException cause) {
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user