Attempt to support invites, working in progress

This commit is contained in:
Maxime Dor
2017-09-06 04:17:46 +02:00
parent c05ca68de4
commit cd6960fa80
27 changed files with 999 additions and 143 deletions

View File

@@ -190,3 +190,43 @@ 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.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"

View File

@@ -97,6 +97,10 @@ 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'

View File

@@ -1,17 +1,14 @@
package io.kamax.mxisd.lookup;
import java.time.Instant;
package io.kamax.mxisd;
// FIXME this should be in matrix-java-sdk
public class ThreePid {
private String medium;
private String address;
private Instant validation;
public ThreePid(String medium, String address, Instant validation) {
public ThreePid(String medium, String address) {
this.medium = medium;
this.address = address;
this.validation = validation;
}
public String getMedium() {
@@ -22,8 +19,4 @@ public class ThreePid {
return address;
}
public Instant getValidation() {
return validation;
}
}

View File

@@ -2,14 +2,16 @@ package io.kamax.mxisd.auth.provider
import com.google.firebase.FirebaseApp
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.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.invitation.InvitationManager
import io.kamax.mxisd.lookup.ThreePidMapping
import org.apache.commons.lang.StringUtils
import org.slf4j.Logger
@@ -17,11 +19,10 @@ 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);
@@ -32,12 +33,15 @@ public class GoogleFirebaseAuthenticator implements GlobalProvider {
private FirebaseApp fbApp;
private FirebaseAuth fbAuth;
public GoogleFirebaseAuthenticator(boolean isEnabled) {
private InvitationManager invMgr;
public GoogleFirebaseAuthenticator(InvitationManager invMgr, boolean isEnabled) {
this.isEnabled = isEnabled;
this.invMgr = invMgr;
}
public GoogleFirebaseAuthenticator(String credsPath, String db, String domain) {
this(true);
public GoogleFirebaseAuthenticator(InvitationManager invMgr, String credsPath, String db, String domain) {
this(invMgr, true);
this.domain = domain;
try {
fbApp = FirebaseApp.initializeApp(getOpts(credsPath, db));
@@ -73,16 +77,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 +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
public UserAuthResult authenticate(String id, String password) {
if (!isEnabled()) {
@@ -190,27 +106,37 @@ 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();
}
log.info("{} was successfully authenticated", id);
result.success(id, token.getName());
l.countDown()
log.info("{} was successfully authenticated", id);
result.success(id, token.getName());
if (StringUtils.isNotBlank(token.getEmail())) {
invMgr.publishMappingIfInvited(new ThreePidMapping(ThreePidMedium.Email.getId(), token.getEmail(), id))
}
} 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()
}
}
});

View File

@@ -1,7 +1,10 @@
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.invitation.InvitationManager;
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;
@@ -20,6 +23,9 @@ public class FirebaseConfig {
@Autowired
private ServerConfig srvCfg;
@Autowired
private InvitationManager invMgr;
private boolean enabled;
private String credentials;
private String database;
@@ -56,16 +62,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);
return new GoogleFirebaseAuthenticator(invMgr, false);
} 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());
}
}
}

View File

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

View File

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

View File

@@ -20,10 +20,19 @@
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.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
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
@@ -35,11 +44,25 @@ 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
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())))
}
}

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
@@ -93,10 +93,10 @@ class SessionController {
@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())

View File

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

View File

@@ -1,11 +1,27 @@
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,9 @@
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,15 @@
package io.kamax.mxisd.invitation;
import io.kamax.matrix._MatrixID;
public interface IThreePidInvite {
_MatrixID getSender();
String getMedium();
String getAddress();
String getRoomId();
}

View File

@@ -0,0 +1,11 @@
package io.kamax.mxisd.invitation;
public interface IThreePidInviteReply {
IThreePidInvite getInvite();
String getToken();
String getDisplayName();
}

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -28,6 +28,16 @@ public class ThreePidMapping {
private String value;
private String mxid;
public ThreePidMapping() {
// stub
}
public ThreePidMapping(String medium, String value, String mxid) {
setMedium(medium);
setValue(value);
setMxid(mxid);
}
public String getMedium() {
return medium;
}

View File

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

View File

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

View File

@@ -31,6 +31,8 @@ interface LookupStrategy {
Optional<?> find(SingleLookupRequest request)
Optional<?> findRecursive(SingleLookupRequest request)
List<ThreePidMapping> find(BulkLookupRequest requests)
}

View File

@@ -93,13 +93,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())
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)
}
}
@@ -117,9 +121,8 @@ class RecursivePriorityLookupStrategy implements LookupStrategy, InitializingBea
}).collect(Collectors.toList())
}
@Override
Optional<?> find(SingleLookupRequest request) {
for (IThreePidProvider provider : listUsableProviders(request)) {
Optional<?> find(SingleLookupRequest request, boolean forceRecursive) {
for (IThreePidProvider provider : listUsableProviders(request, forceRecursive)) {
Optional<?> lookupDataOpt = provider.find(request)
if (lookupDataOpt.isPresent()) {
return lookupDataOpt
@@ -134,6 +137,16 @@ class RecursivePriorityLookupStrategy implements LookupStrategy, InitializingBea
return Optional.empty()
}
@Override
Optional<?> find(SingleLookupRequest request) {
return find(request, false)
}
@Override
Optional<?> findRecursive(SingleLookupRequest request) {
return find(request, true)
}
@Override
List<ThreePidMapping> find(BulkLookupRequest request) {
List<ThreePidMapping> mapToDo = new ArrayList<>(request.getMappings())

View File

@@ -1,7 +1,7 @@
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 +72,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

@@ -40,6 +40,7 @@ class SignatureManager implements InitializingBean {
private EdDSAEngine signEngine
// FIXME we need to return a proper object, or a string with the signature and all?
Map<?, ?> signMessage(String message) {
byte[] signRaw = signEngine.signOneShot(message.getBytes())
String sign = Base64.getEncoder().encodeToString(signRaw)

View File

@@ -8,7 +8,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);
}
}