Split template creation and 3PID connector to integrate bindings verification

This commit is contained in:
Maxime Dor
2017-09-19 03:46:31 +02:00
parent 5179c4dbb5
commit c1746697b9
8 changed files with 309 additions and 129 deletions

View File

@@ -0,0 +1,101 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.config.invite.medium;
import io.kamax.mxisd.config.MatrixConfig;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.WordUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
import java.io.File;
@Configuration
@ConfigurationProperties("invite.medium.email")
public class EmailInviteConfig {
private Logger log = LoggerFactory.getLogger(EmailInviteConfig.class);
private MatrixConfig mxCfg;
private String from;
private String name;
private String template;
@Autowired
public EmailInviteConfig(MatrixConfig mxCfg) {
this.mxCfg = mxCfg;
}
public String getFrom() {
return from;
}
public void setFrom(String from) {
this.from = from;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getTemplate() {
return template;
}
public void setTemplate(String template) {
this.template = template;
}
@PostConstruct
public void build() {
log.info("--- E-mail invites config ---");
log.info("From: {}", getFrom());
if (StringUtils.isBlank(getName())) {
setName(WordUtils.capitalize(mxCfg.getDomain()) + " Identity Server");
}
log.info("Name: {}", getName());
if (!StringUtils.startsWith(getTemplate(), "classpath:")) {
if (StringUtils.isBlank(getTemplate())) {
log.warn("invite.medium.email is empty! Will not send invites");
} else {
File cp = new File(getTemplate()).getAbsoluteFile();
log.info("Template: {}", cp.getAbsolutePath());
if (!cp.exists() || !cp.isFile() || !cp.canRead()) {
log.warn(getTemplate() + " does not exist, is not a file or cannot be read");
}
}
} else {
log.info("Template: Built-in: {}", getTemplate());
}
}
}

View File

@@ -18,7 +18,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package io.kamax.mxisd.config.invite.sender; package io.kamax.mxisd.config.threepid.connector;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -27,22 +27,18 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
import java.io.File;
@Configuration @Configuration
@ConfigurationProperties(prefix = "invite.sender.email") @ConfigurationProperties(prefix = "threepid.email.connector.provider.smtp")
public class EmailSenderConfig { public class EmailSmtpConfig {
private Logger log = LoggerFactory.getLogger(EmailSenderConfig.class); private Logger log = LoggerFactory.getLogger(EmailSmtpConfig.class);
private String host; private String host;
private int port; private int port;
private int tls; private int tls;
private String login; private String login;
private String password; private String password;
private String email;
private String name;
private String template;
public String getHost() { public String getHost() {
return host; return host;
@@ -84,52 +80,14 @@ public class EmailSenderConfig {
this.password = password; this.password = password;
} }
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getTemplate() {
return template;
}
public void setTemplate(String template) {
this.template = template;
}
@PostConstruct @PostConstruct
private void postConstruct() { public void build() {
log.info("--- E-mail Invite Sender config ---"); log.info("--- E-mail SMTP Connector config ---");
log.info("Host: {}", getHost()); log.info("Host: {}", getHost());
log.info("Port: {}", getPort()); log.info("Port: {}", getPort());
log.info("TLS Mode: {}", getTls()); log.info("TLS Mode: {}", getTls());
log.info("Login: {}", getLogin()); log.info("Login: {}", getLogin());
log.info("Has password: {}", !StringUtils.isBlank(getPassword())); log.info("Has password: {}", StringUtils.isNotBlank(getPassword()));
log.info("E-mail: {}", getEmail());
if (!StringUtils.startsWith(getTemplate(), "classpath:")) {
if (StringUtils.isBlank(getTemplate())) {
log.warn("invite.sender.template is empty! Will not send invites");
} else {
File cp = new File(getTemplate()).getAbsoluteFile();
log.info("Template: {}", cp.getAbsolutePath());
if (!cp.exists() || !cp.isFile() || !cp.canRead()) {
log.warn(getTemplate() + " does not exist, is not a file or cannot be read");
}
}
} else {
log.info("Template: Built-in");
}
} }
} }

View File

@@ -26,13 +26,14 @@ import io.kamax.mxisd.config.DnsOverwrite;
import io.kamax.mxisd.config.DnsOverwriteEntry; import io.kamax.mxisd.config.DnsOverwriteEntry;
import io.kamax.mxisd.exception.BadRequestException; import io.kamax.mxisd.exception.BadRequestException;
import io.kamax.mxisd.exception.MappingAlreadyExistsException; import io.kamax.mxisd.exception.MappingAlreadyExistsException;
import io.kamax.mxisd.invitation.sender.IInviteSender; import io.kamax.mxisd.invitation.generator.IInviteContentGenerator;
import io.kamax.mxisd.lookup.SingleLookupReply; import io.kamax.mxisd.lookup.SingleLookupReply;
import io.kamax.mxisd.lookup.ThreePidMapping; import io.kamax.mxisd.lookup.ThreePidMapping;
import io.kamax.mxisd.lookup.strategy.LookupStrategy; import io.kamax.mxisd.lookup.strategy.LookupStrategy;
import io.kamax.mxisd.signature.SignatureManager; import io.kamax.mxisd.signature.SignatureManager;
import io.kamax.mxisd.storage.IStorage; import io.kamax.mxisd.storage.IStorage;
import io.kamax.mxisd.storage.ormlite.ThreePidInviteIO; import io.kamax.mxisd.storage.ormlite.ThreePidInviteIO;
import io.kamax.mxisd.threepid.connector.IThreePidConnector;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.RandomStringUtils; import org.apache.commons.lang.RandomStringUtils;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
@@ -83,14 +84,35 @@ public class InvitationManager {
@Autowired @Autowired
private DnsOverwrite dns; private DnsOverwrite dns;
private Map<String, IInviteSender> senders; private Map<String, IInviteContentGenerator> generators;
private Map<String, IThreePidConnector> connectors;
private CloseableHttpClient client; private CloseableHttpClient client;
private Gson gson; private Gson gson;
private Timer refreshTimer; private Timer refreshTimer;
private String getId(IThreePidInvite invite) { @Autowired
return invite.getSender().getDomain().toLowerCase() + invite.getMedium().toLowerCase() + invite.getAddress().toLowerCase(); public InvitationManager(
List<IInviteContentGenerator> generatorList,
List<IThreePidConnector> connectorList
) {
generators = new HashMap<>();
generatorList.forEach(sender -> { // FIXME to support several possible implementations
if (generators.containsKey(sender.getMedium())) {
throw new RuntimeException("More than one " + sender.getMedium() + " content generator");
}
generators.put(sender.getMedium(), sender);
});
connectors = new HashMap<>();
connectorList.forEach(connector -> { // FIXME to support several possible implementations
if (connectors.containsKey(connector.getMedium())) {
throw new RuntimeException("More than one " + connector.getMedium() + " connector");
}
connectors.put(connector.getMedium(), connector);
});
} }
@PostConstruct @PostConstruct
@@ -140,9 +162,14 @@ public class InvitationManager {
@PreDestroy @PreDestroy
private void preDestroy() { private void preDestroy() {
refreshTimer.cancel();
ForkJoinPool.commonPool().awaitQuiescence(1, TimeUnit.MINUTES); ForkJoinPool.commonPool().awaitQuiescence(1, TimeUnit.MINUTES);
} }
private String getId(IThreePidInvite invite) {
return invite.getSender().getDomain().toLowerCase() + invite.getMedium().toLowerCase() + invite.getAddress().toLowerCase();
}
private String getIdForLog(IThreePidInviteReply reply) { private String getIdForLog(IThreePidInviteReply reply) {
return reply.getInvite().getSender().getId() + ":" + reply.getInvite().getRoomId() + ":" + reply.getInvite().getMedium() + ":" + reply.getInvite().getAddress(); return reply.getInvite().getSender().getId() + ":" + reply.getInvite().getRoomId() + ":" + reply.getInvite().getMedium() + ":" + reply.getInvite().getAddress();
} }
@@ -193,21 +220,16 @@ public class InvitationManager {
return "https://" + domain + ":8448"; return "https://" + domain + ":8448";
} }
@Autowired
public InvitationManager(List<IInviteSender> senderList) {
senders = new HashMap<>();
senderList.forEach(sender -> senders.put(sender.getMedium(), sender));
}
public synchronized IThreePidInviteReply storeInvite(IThreePidInvite invitation) { // TODO better sync public synchronized IThreePidInviteReply storeInvite(IThreePidInvite invitation) { // TODO better sync
IInviteSender sender = senders.get(invitation.getMedium()); IInviteContentGenerator generator = generators.get(invitation.getMedium());
if (sender == null) { IThreePidConnector connector = connectors.get(invitation.getMedium());
if (generator == null || connector == null) {
throw new BadRequestException("Medium type " + invitation.getMedium() + " is not supported"); throw new BadRequestException("Medium type " + invitation.getMedium() + " is not supported");
} }
String invId = getId(invitation); String invId = getId(invitation);
log.info("Handling invite for {}:{} from {} in room {}", invitation.getMedium(), invitation.getAddress(), invitation.getSender(), invitation.getRoomId()); log.info("Handling invite for {}:{} from {} in room {}", invitation.getMedium(), invitation.getAddress(), invitation.getSender(), invitation.getRoomId());
if (invitations.containsKey(invId)) { // FIXME we need to lookup using the HS domain too!! if (invitations.containsKey(invId)) {
log.info("Invite is already pending for {}:{}, returning data", invitation.getMedium(), invitation.getAddress()); log.info("Invite is already pending for {}:{}, returning data", invitation.getMedium(), invitation.getAddress());
return invitations.get(invId); return invitations.get(invId);
} }
@@ -224,7 +246,7 @@ public class InvitationManager {
IThreePidInviteReply reply = new ThreePidInviteReply(invId, invitation, token, displayName); IThreePidInviteReply reply = new ThreePidInviteReply(invId, invitation, token, displayName);
log.info("Performing invite to {}:{}", invitation.getMedium(), invitation.getAddress()); log.info("Performing invite to {}:{}", invitation.getMedium(), invitation.getAddress());
sender.send(reply); connector.send(reply, generator.generate(reply));
log.info("Storing invite under ID {}", invId); log.info("Storing invite under ID {}", invId);
storage.insertInvite(reply); storage.insertInvite(reply);

View File

@@ -18,61 +18,35 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package io.kamax.mxisd.invitation.sender; package io.kamax.mxisd.invitation.generator;
import com.sun.mail.smtp.SMTPTransport;
import io.kamax.matrix.ThreePidMedium; import io.kamax.matrix.ThreePidMedium;
import io.kamax.mxisd.config.MatrixConfig; import io.kamax.mxisd.config.MatrixConfig;
import io.kamax.mxisd.config.invite.sender.EmailSenderConfig; import io.kamax.mxisd.config.invite.medium.EmailInviteConfig;
import io.kamax.mxisd.exception.ConfigurationException;
import io.kamax.mxisd.invitation.IThreePidInviteReply; import io.kamax.mxisd.invitation.IThreePidInviteReply;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.WordUtils; import org.apache.commons.lang.WordUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component; 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.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Date;
@Component @Component
public class EmailInviteSender implements IInviteSender { public class EmailInviteContentGenerator implements IInviteContentGenerator {
private Logger log = LoggerFactory.getLogger(EmailInviteSender.class); private EmailInviteConfig cfg;
@Autowired
private EmailSenderConfig cfg;
@Autowired
private MatrixConfig mxCfg; private MatrixConfig mxCfg;
@Autowired
private ApplicationContext app; private ApplicationContext app;
private Session session; @Autowired
private InternetAddress sender; public EmailInviteContentGenerator(EmailInviteConfig cfg, MatrixConfig mxCfg, ApplicationContext app) {
this.cfg = cfg;
@PostConstruct this.mxCfg = mxCfg;
private void postConstruct() { this.app = app;
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 @Override
@@ -81,7 +55,7 @@ public class EmailInviteSender implements IInviteSender {
} }
@Override @Override
public void send(IThreePidInviteReply invite) { public String generate(IThreePidInviteReply invite) {
if (!ThreePidMedium.Email.is(invite.getInvite().getMedium())) { if (!ThreePidMedium.Email.is(invite.getInvite().getMedium())) {
throw new IllegalArgumentException(invite.getInvite().getMedium() + " is not a supported 3PID type"); throw new IllegalArgumentException(invite.getInvite().getMedium() + " is not a supported 3PID type");
} }
@@ -99,7 +73,7 @@ public class EmailInviteSender implements IInviteSender {
StandardCharsets.UTF_8); StandardCharsets.UTF_8);
templateBody = templateBody.replace("%DOMAIN%", mxCfg.getDomain()); templateBody = templateBody.replace("%DOMAIN%", mxCfg.getDomain());
templateBody = templateBody.replace("%DOMAIN_PRETTY%", domainPretty); templateBody = templateBody.replace("%DOMAIN_PRETTY%", domainPretty);
templateBody = templateBody.replace("%FROM_EMAIL%", cfg.getEmail()); templateBody = templateBody.replace("%FROM_EMAIL%", cfg.getFrom());
templateBody = templateBody.replace("%FROM_NAME%", cfg.getName()); templateBody = templateBody.replace("%FROM_NAME%", cfg.getName());
templateBody = templateBody.replace("%SENDER_ID%", invite.getInvite().getSender().getId()); templateBody = templateBody.replace("%SENDER_ID%", invite.getInvite().getSender().getId());
templateBody = templateBody.replace("%SENDER_NAME%", senderName); templateBody = templateBody.replace("%SENDER_NAME%", senderName);
@@ -110,28 +84,9 @@ public class EmailInviteSender implements IInviteSender {
templateBody = templateBody.replace("%ROOM_NAME%", roomName); templateBody = templateBody.replace("%ROOM_NAME%", roomName);
templateBody = templateBody.replace("%ROOM_NAME_OR_ID%", roomNameOrId); templateBody = templateBody.replace("%ROOM_NAME_OR_ID%", roomNameOrId);
MimeMessage msg = new MimeMessage(session, IOUtils.toInputStream(templateBody, StandardCharsets.UTF_8)); return templateBody;
msg.setHeader("X-Mailer", "mxisd"); // TODO set version } catch (IOException e) {
msg.setSentDate(new Date()); throw new RuntimeException("Unable to read template file", e);
msg.setFrom(sender);
msg.setRecipients(Message.RecipientType.TO, invite.getInvite().getAddress());
msg.saveChanges();
log.info("Sending invite to {} via SMTP using {}:{}", invite.getInvite().getAddress(), cfg.getHost(), cfg.getPort());
SMTPTransport transport = (SMTPTransport) session.getTransport("smtp");
transport.setStartTLS(cfg.getTls() > 0);
transport.setRequireStartTLS(cfg.getTls() > 1);
log.info("Connecting to {}:{}", cfg.getHost(), cfg.getPort());
transport.connect(cfg.getHost(), cfg.getPort(), cfg.getLogin(), cfg.getPassword());
try {
transport.sendMessage(msg, InternetAddress.parse(invite.getInvite().getAddress()));
log.info("Invite to {} was sent", invite.getInvite().getAddress());
} finally {
transport.close();
}
} catch (IOException | MessagingException e) {
throw new RuntimeException("Unable to send e-mail invite to " + invite.getInvite().getAddress(), e);
} }
} }

View File

@@ -18,14 +18,14 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package io.kamax.mxisd.invitation.sender; package io.kamax.mxisd.invitation.generator;
import io.kamax.mxisd.invitation.IThreePidInviteReply; import io.kamax.mxisd.invitation.IThreePidInviteReply;
public interface IInviteSender { public interface IInviteContentGenerator {
String getMedium(); String getMedium();
void send(IThreePidInviteReply invite); String generate(IThreePidInviteReply invite);
} }

View File

@@ -0,0 +1,106 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.threepid.connector;
import com.sun.mail.smtp.SMTPTransport;
import io.kamax.matrix.ThreePidMedium;
import io.kamax.mxisd.config.invite.medium.EmailInviteConfig;
import io.kamax.mxisd.config.threepid.connector.EmailSmtpConfig;
import io.kamax.mxisd.exception.ConfigurationException;
import io.kamax.mxisd.invitation.IThreePidInviteReply;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
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.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.util.Date;
@Component
public class EmailSmtpConnector implements IThreePidConnector {
private Logger log = LoggerFactory.getLogger(EmailSmtpConnector.class);
private EmailSmtpConfig cfg;
private EmailInviteConfig invCfg;
private Session session;
private InternetAddress sender;
@Autowired
public EmailSmtpConnector(EmailSmtpConfig cfg, EmailInviteConfig invCfg) {
try {
session = Session.getInstance(System.getProperties());
sender = new InternetAddress(invCfg.getFrom(), invCfg.getName());
} catch (UnsupportedEncodingException e) {
// What are we supposed to do with this?!
throw new ConfigurationException(e);
}
this.cfg = cfg;
this.invCfg = invCfg;
}
@Override
public String getMedium() {
return ThreePidMedium.Email.getId();
}
@Override
public void send(IThreePidInviteReply invite, String content) {
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, IOUtils.toInputStream(content, StandardCharsets.UTF_8));
msg.setHeader("X-Mailer", "mxisd"); // TODO set version
msg.setSentDate(new Date());
msg.setFrom(sender);
msg.setRecipients(Message.RecipientType.TO, invite.getInvite().getAddress());
msg.saveChanges();
log.info("Sending invite to {} via SMTP using {}:{}", invite.getInvite().getAddress(), cfg.getHost(), cfg.getPort());
SMTPTransport transport = (SMTPTransport) session.getTransport("smtp");
transport.setStartTLS(cfg.getTls() > 0);
transport.setRequireStartTLS(cfg.getTls() > 1);
log.info("Connecting to {}:{}", cfg.getHost(), cfg.getPort());
transport.connect(cfg.getHost(), cfg.getPort(), cfg.getLogin(), cfg.getPassword());
try {
transport.sendMessage(msg, InternetAddress.parse(invite.getInvite().getAddress()));
log.info("Invite to {} was sent", invite.getInvite().getAddress());
} finally {
transport.close();
}
} catch (MessagingException e) {
throw new RuntimeException("Unable to send e-mail invite to " + invite.getInvite().getAddress(), e);
}
}
}

View File

@@ -0,0 +1,31 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.threepid.connector;
import io.kamax.mxisd.invitation.IThreePidInviteReply;
public interface IThreePidConnector {
String getMedium();
void send(IThreePidInviteReply invite, String content);
}

View File

@@ -62,12 +62,19 @@ forward:
- "https://matrix.org" - "https://matrix.org"
- "https://vector.im" - "https://vector.im"
threepid:
email:
connector:
active: 'smtp'
provider:
smtp:
port: 587
tls: 1
invite: invite:
sender: medium:
email: email:
tls: 1 template: 'classpath:email/invite-template.eml'
name: "mxisd Identity Server"
template: "classpath:email/invite-template.eml"
storage: storage:
backend: 'sqlite' backend: 'sqlite'