From c1746697b94f1f2f454afc769647ef282b3b0ebb Mon Sep 17 00:00:00 2001 From: Maxime Dor Date: Tue, 19 Sep 2017 03:46:31 +0200 Subject: [PATCH 01/14] Split template creation and 3PID connector to integrate bindings verification --- .../invite/medium/EmailInviteConfig.java | 101 +++++++++++++++++ .../connector/EmailSmtpConfig.java} | 56 ++------- .../mxisd/invitation/InvitationManager.java | 50 ++++++--- .../EmailInviteContentGenerator.java} | 73 +++--------- .../IInviteContentGenerator.java} | 6 +- .../connector/EmailSmtpConnector.java | 106 ++++++++++++++++++ .../connector/IThreePidConnector.java | 31 +++++ src/main/resources/application.yaml | 15 ++- 8 files changed, 309 insertions(+), 129 deletions(-) create mode 100644 src/main/groovy/io/kamax/mxisd/config/invite/medium/EmailInviteConfig.java rename src/main/groovy/io/kamax/mxisd/config/{invite/sender/EmailSenderConfig.java => threepid/connector/EmailSmtpConfig.java} (56%) rename src/main/groovy/io/kamax/mxisd/invitation/{sender/EmailInviteSender.java => generator/EmailInviteContentGenerator.java} (58%) rename src/main/groovy/io/kamax/mxisd/invitation/{sender/IInviteSender.java => generator/IInviteContentGenerator.java} (86%) create mode 100644 src/main/groovy/io/kamax/mxisd/threepid/connector/EmailSmtpConnector.java create mode 100644 src/main/groovy/io/kamax/mxisd/threepid/connector/IThreePidConnector.java diff --git a/src/main/groovy/io/kamax/mxisd/config/invite/medium/EmailInviteConfig.java b/src/main/groovy/io/kamax/mxisd/config/invite/medium/EmailInviteConfig.java new file mode 100644 index 00000000..76b79c28 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/config/invite/medium/EmailInviteConfig.java @@ -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 . + */ + +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()); + } + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/config/invite/sender/EmailSenderConfig.java b/src/main/groovy/io/kamax/mxisd/config/threepid/connector/EmailSmtpConfig.java similarity index 56% rename from src/main/groovy/io/kamax/mxisd/config/invite/sender/EmailSenderConfig.java rename to src/main/groovy/io/kamax/mxisd/config/threepid/connector/EmailSmtpConfig.java index 8a0fa67b..24a5e49e 100644 --- a/src/main/groovy/io/kamax/mxisd/config/invite/sender/EmailSenderConfig.java +++ b/src/main/groovy/io/kamax/mxisd/config/threepid/connector/EmailSmtpConfig.java @@ -18,7 +18,7 @@ * along with this program. If not, see . */ -package io.kamax.mxisd.config.invite.sender; +package io.kamax.mxisd.config.threepid.connector; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; @@ -27,22 +27,18 @@ import org.springframework.context.annotation.Configuration; import javax.annotation.PostConstruct; -import java.io.File; @Configuration -@ConfigurationProperties(prefix = "invite.sender.email") -public class EmailSenderConfig { +@ConfigurationProperties(prefix = "threepid.email.connector.provider.smtp") +public class EmailSmtpConfig { - private Logger log = LoggerFactory.getLogger(EmailSenderConfig.class); + private Logger log = LoggerFactory.getLogger(EmailSmtpConfig.class); private String host; private int port; private int tls; private String login; private String password; - private String email; - private String name; - private String template; public String getHost() { return host; @@ -84,52 +80,14 @@ public void setPassword(String password) { this.password = password; } - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getTemplate() { - return template; - } - - public void setTemplate(String template) { - this.template = template; - } - @PostConstruct - private void postConstruct() { - log.info("--- E-mail Invite Sender config ---"); + public void build() { + log.info("--- E-mail SMTP Connector config ---"); log.info("Host: {}", getHost()); log.info("Port: {}", getPort()); log.info("TLS Mode: {}", getTls()); log.info("Login: {}", getLogin()); - log.info("Has password: {}", !StringUtils.isBlank(getPassword())); - log.info("E-mail: {}", getEmail()); - if (!StringUtils.startsWith(getTemplate(), "classpath:")) { - if (StringUtils.isBlank(getTemplate())) { - log.warn("invite.sender.template is empty! Will not send invites"); - } else { - File cp = new File(getTemplate()).getAbsoluteFile(); - log.info("Template: {}", cp.getAbsolutePath()); - if (!cp.exists() || !cp.isFile() || !cp.canRead()) { - log.warn(getTemplate() + " does not exist, is not a file or cannot be read"); - } - } - } else { - log.info("Template: Built-in"); - } + log.info("Has password: {}", StringUtils.isNotBlank(getPassword())); } } diff --git a/src/main/groovy/io/kamax/mxisd/invitation/InvitationManager.java b/src/main/groovy/io/kamax/mxisd/invitation/InvitationManager.java index f9bab89e..d5502d3f 100644 --- a/src/main/groovy/io/kamax/mxisd/invitation/InvitationManager.java +++ b/src/main/groovy/io/kamax/mxisd/invitation/InvitationManager.java @@ -26,13 +26,14 @@ import io.kamax.mxisd.config.DnsOverwriteEntry; import io.kamax.mxisd.exception.BadRequestException; 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.ThreePidMapping; import io.kamax.mxisd.lookup.strategy.LookupStrategy; import io.kamax.mxisd.signature.SignatureManager; import io.kamax.mxisd.storage.IStorage; import io.kamax.mxisd.storage.ormlite.ThreePidInviteIO; +import io.kamax.mxisd.threepid.connector.IThreePidConnector; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.RandomStringUtils; import org.apache.commons.lang.StringUtils; @@ -83,14 +84,35 @@ public class InvitationManager { @Autowired private DnsOverwrite dns; - private Map senders; + private Map generators; + private Map connectors; private CloseableHttpClient client; private Gson gson; private Timer refreshTimer; - private String getId(IThreePidInvite invite) { - return invite.getSender().getDomain().toLowerCase() + invite.getMedium().toLowerCase() + invite.getAddress().toLowerCase(); + @Autowired + public InvitationManager( + List generatorList, + List 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 @@ -140,9 +162,14 @@ public void run() { @PreDestroy private void preDestroy() { + refreshTimer.cancel(); 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) { return reply.getInvite().getSender().getId() + ":" + reply.getInvite().getRoomId() + ":" + reply.getInvite().getMedium() + ":" + reply.getInvite().getAddress(); } @@ -193,21 +220,16 @@ String findHomeserverForDomain(String domain) { return "https://" + domain + ":8448"; } - @Autowired - public InvitationManager(List senderList) { - senders = new HashMap<>(); - senderList.forEach(sender -> senders.put(sender.getMedium(), sender)); - } - public synchronized IThreePidInviteReply storeInvite(IThreePidInvite invitation) { // TODO better sync - IInviteSender sender = senders.get(invitation.getMedium()); - if (sender == null) { + IInviteContentGenerator generator = generators.get(invitation.getMedium()); + IThreePidConnector connector = connectors.get(invitation.getMedium()); + if (generator == null || connector == null) { throw new BadRequestException("Medium type " + invitation.getMedium() + " is not supported"); } String invId = getId(invitation); log.info("Handling invite for {}:{} from {} in room {}", invitation.getMedium(), invitation.getAddress(), invitation.getSender(), invitation.getRoomId()); - if (invitations.containsKey(invId)) { // FIXME we need to lookup using the HS domain too!! + if (invitations.containsKey(invId)) { log.info("Invite is already pending for {}:{}, returning data", invitation.getMedium(), invitation.getAddress()); return invitations.get(invId); } @@ -224,7 +246,7 @@ public synchronized IThreePidInviteReply storeInvite(IThreePidInvite invitation) IThreePidInviteReply reply = new ThreePidInviteReply(invId, invitation, token, displayName); 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); storage.insertInvite(reply); diff --git a/src/main/groovy/io/kamax/mxisd/invitation/sender/EmailInviteSender.java b/src/main/groovy/io/kamax/mxisd/invitation/generator/EmailInviteContentGenerator.java similarity index 58% rename from src/main/groovy/io/kamax/mxisd/invitation/sender/EmailInviteSender.java rename to src/main/groovy/io/kamax/mxisd/invitation/generator/EmailInviteContentGenerator.java index 8a67ac2f..bcd1e448 100644 --- a/src/main/groovy/io/kamax/mxisd/invitation/sender/EmailInviteSender.java +++ b/src/main/groovy/io/kamax/mxisd/invitation/generator/EmailInviteContentGenerator.java @@ -18,61 +18,35 @@ * along with this program. If not, see . */ -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.mxisd.config.MatrixConfig; -import io.kamax.mxisd.config.invite.sender.EmailSenderConfig; -import io.kamax.mxisd.exception.ConfigurationException; +import io.kamax.mxisd.config.invite.medium.EmailInviteConfig; import io.kamax.mxisd.invitation.IThreePidInviteReply; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.WordUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Component; -import javax.annotation.PostConstruct; -import javax.mail.Message; -import javax.mail.MessagingException; -import javax.mail.Session; -import javax.mail.internet.InternetAddress; -import javax.mail.internet.MimeMessage; import java.io.FileInputStream; import java.io.IOException; -import java.io.UnsupportedEncodingException; import java.nio.charset.StandardCharsets; -import java.util.Date; @Component -public class EmailInviteSender implements IInviteSender { +public class EmailInviteContentGenerator implements IInviteContentGenerator { - private Logger log = LoggerFactory.getLogger(EmailInviteSender.class); - - @Autowired - private EmailSenderConfig cfg; - - @Autowired + private EmailInviteConfig cfg; private MatrixConfig mxCfg; - - @Autowired private ApplicationContext app; - private Session session; - private InternetAddress sender; - - @PostConstruct - private void postConstruct() { - try { - session = Session.getInstance(System.getProperties()); - sender = new InternetAddress(cfg.getEmail(), cfg.getName()); - } catch (UnsupportedEncodingException e) { - // What are we supposed to do with this?! - throw new ConfigurationException(e); - } + @Autowired + public EmailInviteContentGenerator(EmailInviteConfig cfg, MatrixConfig mxCfg, ApplicationContext app) { + this.cfg = cfg; + this.mxCfg = mxCfg; + this.app = app; } @Override @@ -81,7 +55,7 @@ public String getMedium() { } @Override - public void send(IThreePidInviteReply invite) { + public String generate(IThreePidInviteReply invite) { if (!ThreePidMedium.Email.is(invite.getInvite().getMedium())) { throw new IllegalArgumentException(invite.getInvite().getMedium() + " is not a supported 3PID type"); } @@ -99,7 +73,7 @@ public void send(IThreePidInviteReply invite) { StandardCharsets.UTF_8); templateBody = templateBody.replace("%DOMAIN%", mxCfg.getDomain()); 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("%SENDER_ID%", invite.getInvite().getSender().getId()); templateBody = templateBody.replace("%SENDER_NAME%", senderName); @@ -110,28 +84,9 @@ public void send(IThreePidInviteReply invite) { templateBody = templateBody.replace("%ROOM_NAME%", roomName); templateBody = templateBody.replace("%ROOM_NAME_OR_ID%", roomNameOrId); - MimeMessage msg = new MimeMessage(session, IOUtils.toInputStream(templateBody, StandardCharsets.UTF_8)); - msg.setHeader("X-Mailer", "mxisd"); // TODO set version - msg.setSentDate(new Date()); - msg.setFrom(sender); - msg.setRecipients(Message.RecipientType.TO, invite.getInvite().getAddress()); - msg.saveChanges(); - - log.info("Sending invite to {} via SMTP using {}:{}", invite.getInvite().getAddress(), cfg.getHost(), cfg.getPort()); - SMTPTransport transport = (SMTPTransport) session.getTransport("smtp"); - transport.setStartTLS(cfg.getTls() > 0); - transport.setRequireStartTLS(cfg.getTls() > 1); - - log.info("Connecting to {}:{}", cfg.getHost(), cfg.getPort()); - transport.connect(cfg.getHost(), cfg.getPort(), cfg.getLogin(), cfg.getPassword()); - try { - transport.sendMessage(msg, InternetAddress.parse(invite.getInvite().getAddress())); - log.info("Invite to {} was sent", invite.getInvite().getAddress()); - } finally { - transport.close(); - } - } catch (IOException | MessagingException e) { - throw new RuntimeException("Unable to send e-mail invite to " + invite.getInvite().getAddress(), e); + return templateBody; + } catch (IOException e) { + throw new RuntimeException("Unable to read template file", e); } } diff --git a/src/main/groovy/io/kamax/mxisd/invitation/sender/IInviteSender.java b/src/main/groovy/io/kamax/mxisd/invitation/generator/IInviteContentGenerator.java similarity index 86% rename from src/main/groovy/io/kamax/mxisd/invitation/sender/IInviteSender.java rename to src/main/groovy/io/kamax/mxisd/invitation/generator/IInviteContentGenerator.java index 14af7fdf..a05b3813 100644 --- a/src/main/groovy/io/kamax/mxisd/invitation/sender/IInviteSender.java +++ b/src/main/groovy/io/kamax/mxisd/invitation/generator/IInviteContentGenerator.java @@ -18,14 +18,14 @@ * along with this program. If not, see . */ -package io.kamax.mxisd.invitation.sender; +package io.kamax.mxisd.invitation.generator; import io.kamax.mxisd.invitation.IThreePidInviteReply; -public interface IInviteSender { +public interface IInviteContentGenerator { String getMedium(); - void send(IThreePidInviteReply invite); + String generate(IThreePidInviteReply invite); } diff --git a/src/main/groovy/io/kamax/mxisd/threepid/connector/EmailSmtpConnector.java b/src/main/groovy/io/kamax/mxisd/threepid/connector/EmailSmtpConnector.java new file mode 100644 index 00000000..b9d68e0f --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/threepid/connector/EmailSmtpConnector.java @@ -0,0 +1,106 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.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); + } + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/threepid/connector/IThreePidConnector.java b/src/main/groovy/io/kamax/mxisd/threepid/connector/IThreePidConnector.java new file mode 100644 index 00000000..07bdc1a7 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/threepid/connector/IThreePidConnector.java @@ -0,0 +1,31 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.threepid.connector; + +import io.kamax.mxisd.invitation.IThreePidInviteReply; + +public interface IThreePidConnector { + + String getMedium(); + + void send(IThreePidInviteReply invite, String content); + +} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 87d88f1e..8dc80b60 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -62,12 +62,19 @@ forward: - "https://matrix.org" - "https://vector.im" +threepid: + email: + connector: + active: 'smtp' + provider: + smtp: + port: 587 + tls: 1 + invite: - sender: + medium: email: - tls: 1 - name: "mxisd Identity Server" - template: "classpath:email/invite-template.eml" + template: 'classpath:email/invite-template.eml' storage: backend: 'sqlite' From 0b087ee08c6c7d6fcab19ff4beb825c7c63d442e Mon Sep 17 00:00:00 2001 From: Maxime Dor Date: Wed, 20 Sep 2017 04:35:34 +0200 Subject: [PATCH 02/14] Prepare structure to handle 3PID sessions and bindings validation/proxy --- src/main/groovy/io/kamax/mxisd/ThreePid.java | 4 + .../invite/medium/EmailInviteConfig.java | 34 --- .../config/threepid/medium/EmailConfig.java | 77 ++++++ .../controller/v1/SessionController.groovy | 24 +- .../v1/io/GenericTokenRequestJson.java | 10 +- .../v1/io/SessionEmailTokenRequestJson.java | 2 - .../v1/io/SessionPhoneTokenRequestJson.java | 2 - .../mxisd/exception/InternalServerError.java | 35 +++ .../InvalidCredentialsException.java | 33 +++ .../exception/ObjectNotFoundException.java | 33 +++ .../EmailInviteContentGenerator.java | 13 +- .../mxisd/lookup/ThreePidValidation.java | 4 +- .../lookup/strategy/LookupStrategy.groovy | 4 + .../RecursivePriorityLookupStrategy.groovy | 35 ++- .../kamax/mxisd/mapping/MappingManager.java | 238 +++++++++--------- .../io/kamax/mxisd/storage/IStorage.java | 11 + .../dao/IThreePidSessionDao.java} | 14 +- .../storage/ormlite/OrmLiteSqliteStorage.java | 126 ++++++++-- .../ormlite/dao/ThreePidSessionDao.java | 147 +++++++++++ .../connector/EmailSmtpConnector.java | 10 +- .../threepid/session/IThreePidSession.java | 52 ++++ .../threepid/session/ThreePidSession.java | 197 +++++++++++++++ 22 files changed, 901 insertions(+), 204 deletions(-) create mode 100644 src/main/groovy/io/kamax/mxisd/config/threepid/medium/EmailConfig.java create mode 100644 src/main/groovy/io/kamax/mxisd/exception/InternalServerError.java create mode 100644 src/main/groovy/io/kamax/mxisd/exception/InvalidCredentialsException.java create mode 100644 src/main/groovy/io/kamax/mxisd/exception/ObjectNotFoundException.java rename src/main/groovy/io/kamax/mxisd/{mapping/MappingSession.java => storage/dao/IThreePidSessionDao.java} (83%) create mode 100644 src/main/groovy/io/kamax/mxisd/storage/ormlite/dao/ThreePidSessionDao.java create mode 100644 src/main/groovy/io/kamax/mxisd/threepid/session/IThreePidSession.java create mode 100644 src/main/groovy/io/kamax/mxisd/threepid/session/ThreePidSession.java diff --git a/src/main/groovy/io/kamax/mxisd/ThreePid.java b/src/main/groovy/io/kamax/mxisd/ThreePid.java index d51ecd65..3a0b6bff 100644 --- a/src/main/groovy/io/kamax/mxisd/ThreePid.java +++ b/src/main/groovy/io/kamax/mxisd/ThreePid.java @@ -26,6 +26,10 @@ public class ThreePid { private String medium; private String address; + public ThreePid(ThreePid tpid) { + this(tpid.getMedium(), tpid.getAddress()); + } + public ThreePid(String medium, String address) { this.medium = medium; this.address = address; diff --git a/src/main/groovy/io/kamax/mxisd/config/invite/medium/EmailInviteConfig.java b/src/main/groovy/io/kamax/mxisd/config/invite/medium/EmailInviteConfig.java index 76b79c28..a244aff0 100644 --- a/src/main/groovy/io/kamax/mxisd/config/invite/medium/EmailInviteConfig.java +++ b/src/main/groovy/io/kamax/mxisd/config/invite/medium/EmailInviteConfig.java @@ -20,12 +20,9 @@ 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; @@ -38,33 +35,8 @@ 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; } @@ -76,12 +48,6 @@ public void setTemplate(String 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())) { diff --git a/src/main/groovy/io/kamax/mxisd/config/threepid/medium/EmailConfig.java b/src/main/groovy/io/kamax/mxisd/config/threepid/medium/EmailConfig.java new file mode 100644 index 00000000..2d47bf3b --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/config/threepid/medium/EmailConfig.java @@ -0,0 +1,77 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.config.threepid.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; + +@Configuration +@ConfigurationProperties("threepid.medium.email") +public class EmailConfig { + + private Logger log = LoggerFactory.getLogger(EmailConfig.class); + + private MatrixConfig mxCfg; + + private String from; + private String name; + + @Autowired + public EmailConfig(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; + } + + @PostConstruct + public void build() { + log.info("--- E-mail config ---"); + log.info("From: {}", getFrom()); + + if (StringUtils.isBlank(getName())) { + setName(WordUtils.capitalize(mxCfg.getDomain()) + " Identity Server"); + } + log.info("Name: {}", getName()); + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/controller/v1/SessionController.groovy b/src/main/groovy/io/kamax/mxisd/controller/v1/SessionController.groovy index 2df1de06..93181b4d 100644 --- a/src/main/groovy/io/kamax/mxisd/controller/v1/SessionController.groovy +++ b/src/main/groovy/io/kamax/mxisd/controller/v1/SessionController.groovy @@ -22,6 +22,8 @@ package io.kamax.mxisd.controller.v1 import com.google.gson.Gson import com.google.gson.JsonObject +import io.kamax.matrix.ThreePidMedium +import io.kamax.mxisd.ThreePid import io.kamax.mxisd.controller.v1.io.SessionEmailTokenRequestJson import io.kamax.mxisd.controller.v1.io.SessionPhoneTokenRequestJson import io.kamax.mxisd.exception.BadRequestException @@ -29,7 +31,6 @@ import io.kamax.mxisd.invitation.InvitationManager import io.kamax.mxisd.lookup.ThreePidValidation import io.kamax.mxisd.mapping.MappingManager import org.apache.commons.io.IOUtils -import org.apache.commons.lang.StringUtils import org.apache.http.HttpStatus import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -62,16 +63,25 @@ class SessionController { @RequestMapping(value = "/validate/{medium}/requestToken") String init(HttpServletRequest request, HttpServletResponse response, @PathVariable String medium) { - log.info("Requested: {}", request.getRequestURL(), request.getQueryString()) - - if (StringUtils.equals("email", medium)) { + log.info("Request {}: {}", request.getMethod(), request.getRequestURL(), request.getQueryString()) + if (ThreePidMedium.Email.is(medium)) { SessionEmailTokenRequestJson req = fromJson(request, SessionEmailTokenRequestJson.class) - return gson.toJson(new Sid(mgr.create(req))) + return gson.toJson(new Sid(mgr.create( + request.getRemoteHost(), + new ThreePid(req.getMedium(), req.getValue()), + req.getSecret(), + req.getAttempt(), + req.getNextLink()))); } - if (StringUtils.equals("msisdn", medium)) { + if (ThreePidMedium.PhoneNumber) { SessionPhoneTokenRequestJson req = fromJson(request, SessionPhoneTokenRequestJson.class) - return gson.toJson(new Sid(mgr.create(req))) + return gson.toJson(new Sid(mgr.create( + request.getRemoteHost(), + new ThreePid(req.getMedium(), req.getValue()), + req.getSecret(), + req.getAttempt(), + req.getNextLink()))); } JsonObject obj = new JsonObject(); diff --git a/src/main/groovy/io/kamax/mxisd/controller/v1/io/GenericTokenRequestJson.java b/src/main/groovy/io/kamax/mxisd/controller/v1/io/GenericTokenRequestJson.java index f7d1b35e..0812505f 100644 --- a/src/main/groovy/io/kamax/mxisd/controller/v1/io/GenericTokenRequestJson.java +++ b/src/main/groovy/io/kamax/mxisd/controller/v1/io/GenericTokenRequestJson.java @@ -20,13 +20,11 @@ package io.kamax.mxisd.controller.v1.io; -import io.kamax.mxisd.mapping.MappingSession; - -public abstract class GenericTokenRequestJson implements MappingSession { +public abstract class GenericTokenRequestJson { private String client_secret; private int send_attempt; - private String id_server; + private String next_link; public String getSecret() { return client_secret; @@ -36,8 +34,8 @@ public int getAttempt() { return send_attempt; } - public String getServer() { - return id_server; + public String getNextLink() { + return next_link; } } diff --git a/src/main/groovy/io/kamax/mxisd/controller/v1/io/SessionEmailTokenRequestJson.java b/src/main/groovy/io/kamax/mxisd/controller/v1/io/SessionEmailTokenRequestJson.java index 39010459..527f1e74 100644 --- a/src/main/groovy/io/kamax/mxisd/controller/v1/io/SessionEmailTokenRequestJson.java +++ b/src/main/groovy/io/kamax/mxisd/controller/v1/io/SessionEmailTokenRequestJson.java @@ -24,12 +24,10 @@ public class SessionEmailTokenRequestJson extends GenericTokenRequestJson { private String email; - @Override public String getMedium() { return "email"; } - @Override public String getValue() { return email; } diff --git a/src/main/groovy/io/kamax/mxisd/controller/v1/io/SessionPhoneTokenRequestJson.java b/src/main/groovy/io/kamax/mxisd/controller/v1/io/SessionPhoneTokenRequestJson.java index b66e8810..e2e82c2a 100644 --- a/src/main/groovy/io/kamax/mxisd/controller/v1/io/SessionPhoneTokenRequestJson.java +++ b/src/main/groovy/io/kamax/mxisd/controller/v1/io/SessionPhoneTokenRequestJson.java @@ -31,12 +31,10 @@ public class SessionPhoneTokenRequestJson extends GenericTokenRequestJson { private String country; private String phone_number; - @Override public String getMedium() { return "msisdn"; } - @Override public String getValue() { try { Phonenumber.PhoneNumber num = phoneUtil.parse(phone_number, country); diff --git a/src/main/groovy/io/kamax/mxisd/exception/InternalServerError.java b/src/main/groovy/io/kamax/mxisd/exception/InternalServerError.java new file mode 100644 index 00000000..d3ca848b --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/exception/InternalServerError.java @@ -0,0 +1,35 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +import java.time.Instant; + +@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR) +public class InternalServerError extends RuntimeException { + + public InternalServerError() { + super("An internal server error occured. If this error persists, please contact support with reference #" + Instant.now().toEpochMilli()); + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/exception/InvalidCredentialsException.java b/src/main/groovy/io/kamax/mxisd/exception/InvalidCredentialsException.java new file mode 100644 index 00000000..bbd66df0 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/exception/InvalidCredentialsException.java @@ -0,0 +1,33 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(value = HttpStatus.FORBIDDEN) +public class InvalidCredentialsException extends RuntimeException { + + public InvalidCredentialsException() { + super("Supplied credentials are invalid"); + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/exception/ObjectNotFoundException.java b/src/main/groovy/io/kamax/mxisd/exception/ObjectNotFoundException.java new file mode 100644 index 00000000..acfb0c37 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/exception/ObjectNotFoundException.java @@ -0,0 +1,33 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(value = HttpStatus.NOT_FOUND) +public class ObjectNotFoundException extends RuntimeException { + + public ObjectNotFoundException(String type, String id) { + super(type + " with ID " + id + " does not exist"); + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/invitation/generator/EmailInviteContentGenerator.java b/src/main/groovy/io/kamax/mxisd/invitation/generator/EmailInviteContentGenerator.java index bcd1e448..0bce14d7 100644 --- a/src/main/groovy/io/kamax/mxisd/invitation/generator/EmailInviteContentGenerator.java +++ b/src/main/groovy/io/kamax/mxisd/invitation/generator/EmailInviteContentGenerator.java @@ -23,6 +23,7 @@ import io.kamax.matrix.ThreePidMedium; import io.kamax.mxisd.config.MatrixConfig; import io.kamax.mxisd.config.invite.medium.EmailInviteConfig; +import io.kamax.mxisd.config.threepid.medium.EmailConfig; import io.kamax.mxisd.invitation.IThreePidInviteReply; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; @@ -38,13 +39,15 @@ @Component public class EmailInviteContentGenerator implements IInviteContentGenerator { - private EmailInviteConfig cfg; + private EmailConfig cfg; + private EmailInviteConfig invCfg; private MatrixConfig mxCfg; private ApplicationContext app; - @Autowired - public EmailInviteContentGenerator(EmailInviteConfig cfg, MatrixConfig mxCfg, ApplicationContext app) { + @Autowired // FIXME ApplicationContext shouldn't be injected, find another way from config (?) + public EmailInviteContentGenerator(EmailConfig cfg, EmailInviteConfig invCfg, MatrixConfig mxCfg, ApplicationContext app) { this.cfg = cfg; + this.invCfg = invCfg; this.mxCfg = mxCfg; this.app = app; } @@ -68,8 +71,8 @@ public String generate(IThreePidInviteReply invite) { String roomNameOrId = StringUtils.defaultIfBlank(roomName, invite.getInvite().getRoomId()); String templateBody = IOUtils.toString( - StringUtils.startsWith(cfg.getTemplate(), "classpath:") ? - app.getResource(cfg.getTemplate()).getInputStream() : new FileInputStream(cfg.getTemplate()), + StringUtils.startsWith(invCfg.getTemplate(), "classpath:") ? + app.getResource(invCfg.getTemplate()).getInputStream() : new FileInputStream(invCfg.getTemplate()), StandardCharsets.UTF_8); templateBody = templateBody.replace("%DOMAIN%", mxCfg.getDomain()); templateBody = templateBody.replace("%DOMAIN_PRETTY%", domainPretty); diff --git a/src/main/groovy/io/kamax/mxisd/lookup/ThreePidValidation.java b/src/main/groovy/io/kamax/mxisd/lookup/ThreePidValidation.java index 8d3b4a0e..206d9d4e 100644 --- a/src/main/groovy/io/kamax/mxisd/lookup/ThreePidValidation.java +++ b/src/main/groovy/io/kamax/mxisd/lookup/ThreePidValidation.java @@ -28,8 +28,8 @@ public class ThreePidValidation extends ThreePid { private Instant validation; - public ThreePidValidation(String medium, String address, Instant validation) { - super(medium, address); + public ThreePidValidation(ThreePid tpid, Instant validation) { + super(tpid); this.validation = validation; } diff --git a/src/main/groovy/io/kamax/mxisd/lookup/strategy/LookupStrategy.groovy b/src/main/groovy/io/kamax/mxisd/lookup/strategy/LookupStrategy.groovy index 37e43278..44b67f0e 100644 --- a/src/main/groovy/io/kamax/mxisd/lookup/strategy/LookupStrategy.groovy +++ b/src/main/groovy/io/kamax/mxisd/lookup/strategy/LookupStrategy.groovy @@ -32,6 +32,10 @@ interface LookupStrategy { Optional find(String medium, String address, boolean recursive) + Optional findLocal(String medium, String address); + + Optional findRemote(String medium, String address); + Optional find(SingleLookupRequest request) Optional findRecursive(SingleLookupRequest request) diff --git a/src/main/groovy/io/kamax/mxisd/lookup/strategy/RecursivePriorityLookupStrategy.groovy b/src/main/groovy/io/kamax/mxisd/lookup/strategy/RecursivePriorityLookupStrategy.groovy index e4ef7761..048dd219 100644 --- a/src/main/groovy/io/kamax/mxisd/lookup/strategy/RecursivePriorityLookupStrategy.groovy +++ b/src/main/groovy/io/kamax/mxisd/lookup/strategy/RecursivePriorityLookupStrategy.groovy @@ -118,17 +118,44 @@ class RecursivePriorityLookupStrategy implements LookupStrategy, InitializingBea }).collect(Collectors.toList()) } - @Override - Optional find(String medium, String address, boolean recursive) { + List getRemoteProviders() { + return providers.stream().filter(new Predicate() { + @Override + boolean test(IThreePidProvider iThreePidProvider) { + return iThreePidProvider.isEnabled() && !iThreePidProvider.isLocal() + } + }).collect(Collectors.toList()) + } + + private static SingleLookupRequest build(String medium, String address) { SingleLookupRequest req = new SingleLookupRequest(); req.setType(medium) req.setThreePid(address) req.setRequester("Internal") - return find(req, recursive) + return req; + } + + @Override + Optional find(String medium, String address, boolean recursive) { + return find(build(medium, address), recursive) + } + + @Override + Optional findLocal(String medium, String address) { + return find(build(medium, address), getLocalProviders()) + } + + @Override + Optional findRemote(String medium, String address) { + return find(build(medium, address), getRemoteProviders()) } Optional find(SingleLookupRequest request, boolean forceRecursive) { - for (IThreePidProvider provider : listUsableProviders(request, forceRecursive)) { + return find(request, listUsableProviders(request, forceRecursive)); + } + + Optional find(SingleLookupRequest request, List providers) { + for (IThreePidProvider provider : providers) { Optional lookupDataOpt = provider.find(request) if (lookupDataOpt.isPresent()) { return lookupDataOpt diff --git a/src/main/groovy/io/kamax/mxisd/mapping/MappingManager.java b/src/main/groovy/io/kamax/mxisd/mapping/MappingManager.java index 1f8c3ecb..42e53b0c 100644 --- a/src/main/groovy/io/kamax/mxisd/mapping/MappingManager.java +++ b/src/main/groovy/io/kamax/mxisd/mapping/MappingManager.java @@ -20,155 +20,167 @@ package io.kamax.mxisd.mapping; -import io.kamax.mxisd.exception.BadRequestException; +import io.kamax.matrix.ThreePidMedium; +import io.kamax.mxisd.ThreePid; +import io.kamax.mxisd.exception.InvalidCredentialsException; +import io.kamax.mxisd.lookup.SingleLookupReply; import io.kamax.mxisd.lookup.ThreePidValidation; +import io.kamax.mxisd.lookup.strategy.LookupStrategy; +import io.kamax.mxisd.storage.IStorage; +import io.kamax.mxisd.storage.dao.IThreePidSessionDao; +import io.kamax.mxisd.threepid.session.ThreePidSession; +import org.apache.commons.lang.RandomStringUtils; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.*; +import java.util.Optional; +import java.util.UUID; @Component public class MappingManager { private Logger log = LoggerFactory.getLogger(MappingManager.class); - private Map sessions = new HashMap<>(); - private Timer cleaner; - - MappingManager() { - cleaner = new Timer(); - cleaner.schedule(new TimerTask() { - @Override - public void run() { - List sList = new ArrayList<>(sessions.values()); - for (Session s : sList) { - if (s.timestamp.plus(24, ChronoUnit.HOURS).isBefore(Instant.now())) { // TODO config timeout - log.info("Session {} is obsolete, removing", s.sid); - - sessions.remove(s.sid); - } - } - } - }, 0, 10 * 1000); // TODO config delay - } - - public String create(MappingSession data) { - String sid; - do { - sid = Long.toString(System.currentTimeMillis()); - } while (sessions.containsKey(sid)); - - String threePidHash = data.getMedium() + data.getValue(); - // TODO think how to handle different requests for the same e-mail - Session session = new Session(sid, threePidHash, data); - sessions.put(sid, session); - - log.info("Created new session {} to validate {} {}", sid, session.medium, session.address); - return sid; - } - - public void validate(String sid, String secret, String token) { - Session s = sessions.get(sid); - if (s == null || !StringUtils.equals(s.secret, secret)) { - throw new BadRequestException("sid or secret are not valid"); - } - - // TODO actually check token + private IStorage storage; + private LookupStrategy lookup; - s.isValidated = true; - s.validationTimestamp = Instant.now(); + @Autowired + public MappingManager(IStorage storage, LookupStrategy lookup) { + this.storage = storage; } - public Optional getValidated(String sid, String secret) { - Session s = sessions.get(sid); - if (s != null && StringUtils.equals(s.secret, secret)) { - return Optional.of(new ThreePidValidation(s.medium, s.address, s.validationTimestamp)); + private ThreePidSession getSession(String sid, String secret) { + Optional dao = storage.getThreePidSession(sid); + if (!dao.isPresent() || !StringUtils.equals(dao.get().getSecret(), secret)) { + throw new InvalidCredentialsException(); } - return Optional.empty(); + return new ThreePidSession(dao.get()); } - public void bind(String sid, String secret, String mxid) { - Session s = sessions.get(sid); - if (s == null || !StringUtils.equals(s.secret, secret)) { - throw new BadRequestException("sid or secret are not valid"); + private ThreePidSession getSessionIfValidated(String sid, String secret) { + ThreePidSession session = getSession(sid, secret); + if (!session.isValidated()) { + throw new IllegalStateException("Session " + sid + " has not been validated"); } - - log.info("Performed bind for mxid {}", mxid); - // TODO perform bind, whatever it is + return session; } - private class Session { - - private String sid; - private String hash; - private Instant timestamp; - private Instant validationTimestamp; - private boolean isValidated; - private String secret; - private String medium; - private String address; - - public Session(String sid, String hash, MappingSession data) { - this.sid = sid; - this.hash = hash; - timestamp = Instant.now(); - validationTimestamp = Instant.now(); - secret = data.getSecret(); - medium = data.getMedium(); - address = data.getValue(); - } + public String create(String server, ThreePid tpid, String secret, int attempt, String nextLink) { + synchronized (this) { + log.info("Server {} is asking to create session for {} (Attempt #{}) - Next link: {}", server, tpid, attempt, nextLink); + Optional dao = storage.findThreePidSession(tpid, secret); + if (dao.isPresent()) { + ThreePidSession session = new ThreePidSession(dao.get()); + log.info("We already have a session for {}: {}", tpid, session.getId()); + if (session.getAttempt() < attempt) { + log.info("Received attempt {} is greater than stored attempt {}, sending validation communication", attempt, session.getAttempt()); + // TODO send via connector + session.increaseAttempt(); + storage.updateThreePidSession(session.getDao()); + } - public Instant getTimestamp() { - return timestamp; - } + return session.getId(); + } else { + log.info("No existing session for {}", tpid); + String sessionId; + do { + sessionId = UUID.randomUUID().toString().replace("-", ""); + } while (storage.getThreePidSession(sessionId).isPresent()); - public void setTimestamp(Instant timestamp) { - this.timestamp = timestamp; - } + String token = RandomStringUtils.randomNumeric(6); + ThreePidSession session = new ThreePidSession(sessionId, server, tpid, secret, attempt, nextLink, token); + log.info("Generated new session {} to validate {} from server {}", sessionId, tpid, server); - public Instant getValidationTimestamp() { - return validationTimestamp; - } + // TODO send via connector + // log.info("Sent validation notification to {}", tpid); - public void setValidationTimestamp(Instant validationTimestamp) { - this.validationTimestamp = validationTimestamp; - } + storage.insertThreePidSession(session.getDao()); + log.info("Stored session {}", sessionId, tpid, server); - public boolean isValidated() { - return isValidated; + return sessionId; + } } + } - public void setValidated(boolean validated) { - isValidated = validated; - } + public Optional validate(String sid, String secret, String token) { + ThreePidSession session = getSession(sid, secret); + log.info("Attempting validation for session {} from {}", session.getId(), session.getServer()); + session.validate(token); + storage.updateThreePidSession(session.getDao()); + log.info("Session {} has been validated", session.getId()); + return session.getNextLink(); + } - public String getSecret() { - return secret; - } + public ThreePidValidation getValidated(String sid, String secret) { + ThreePidSession session = getSessionIfValidated(sid, secret); + return new ThreePidValidation(session.getThreePid(), session.getValidationTime()); + } - public void setSecret(String secret) { - this.secret = secret; - } + public void bind(String sid, String secret, String mxid) { + ThreePidSession session = getSessionIfValidated(sid, secret); + log.info("Attempting bind of {} on session {} from server {}", mxid, session.getId(), session.getServer()); + + // We lookup if the 3PID is already known locally. + // If it is, we do not need to process any further as it is already bound. + Optional rLocal = lookup.findLocal(session.getThreePid().getMedium(), session.getThreePid().getAddress()); + boolean knownLocal = rLocal.isPresent() && StringUtils.equals(rLocal.get().getMxid().getId(), mxid); + log.info("Mapping {} -> {} is " + (knownLocal ? "already" : "not") + " known locally", mxid, session.getThreePid()); + + // MXID is not known locally, checking remotely + Optional rRemote = lookup.findRemote(session.getThreePid().getMedium(), session.getThreePid().getAddress()); + boolean knownRemote = rRemote.isPresent() && StringUtils.equals(rRemote.get().getMxid().getId(), mxid); + log.info("Mapping {} -> {} is " + (knownRemote ? "already" : "not") + " known remotely", mxid, session.getThreePid()); + + boolean isLocalDomain = false; + if (ThreePidMedium.Email.is(session.getThreePid().getMedium())) { + // TODO + // 1. Extract domain from email + // 2. set isLocalDomain + isLocalDomain = session.getThreePid().getAddress().isEmpty(); // FIXME only for testing + } + if (knownRemote) { + if (isLocalDomain && !knownLocal) { + log.warn("Mapping {} -> {} is not known locally but is about a local domain!"); + } - public String getMedium() { - return medium; + log.info("No further action needed for Mapping {} -> {}"); + return; } - public void setMedium(String medium) { - this.medium = medium; - } + // This might need a configuration by medium type? + if (knownLocal) { // 3PID is ony known local + if (isLocalDomain) { + // TODO + // 1. Check if global publishing is enabled, allowed and offered. If one is no, return. + // 2. Publish globally + } - public String getAddress() { - return address; - } + if (System.currentTimeMillis() % 2 == 0) { + // TODO + // 1. Check if configured to publish globally non-local domain. If no, return + } - public void setAddress(String address) { - this.address = address; + // TODO + // Proxy to configurable IS, by default Matrix.org + // + // Separate workflow, if user accepts to publish globally + // 1. display page to the user that it is waiting for the confirmation + // 2. call mxisd-specific endpoint to publish globally + // 3. check regularly on client page for a binding + // 4. when found, show page "Done globally!" + } else { + if (isLocalDomain) { // 3PID is not known anywhere but is a local domain + // TODO + // check if config says this should fail or silently accept. + // Required to silently accept if the backend is synapse itself. + } else { // 3PID is not known anywhere and is remote + // TODO + // Proxy to configurable IS, by default Matrix.org + } } } diff --git a/src/main/groovy/io/kamax/mxisd/storage/IStorage.java b/src/main/groovy/io/kamax/mxisd/storage/IStorage.java index b77c50b7..329dfd05 100644 --- a/src/main/groovy/io/kamax/mxisd/storage/IStorage.java +++ b/src/main/groovy/io/kamax/mxisd/storage/IStorage.java @@ -20,10 +20,13 @@ package io.kamax.mxisd.storage; +import io.kamax.mxisd.ThreePid; import io.kamax.mxisd.invitation.IThreePidInviteReply; +import io.kamax.mxisd.storage.dao.IThreePidSessionDao; import io.kamax.mxisd.storage.ormlite.ThreePidInviteIO; import java.util.Collection; +import java.util.Optional; public interface IStorage { @@ -33,4 +36,12 @@ public interface IStorage { void deleteInvite(String id); + Optional getThreePidSession(String sid); + + Optional findThreePidSession(ThreePid tpid, String secret); + + void insertThreePidSession(IThreePidSessionDao session); + + void updateThreePidSession(IThreePidSessionDao session); + } diff --git a/src/main/groovy/io/kamax/mxisd/mapping/MappingSession.java b/src/main/groovy/io/kamax/mxisd/storage/dao/IThreePidSessionDao.java similarity index 83% rename from src/main/groovy/io/kamax/mxisd/mapping/MappingSession.java rename to src/main/groovy/io/kamax/mxisd/storage/dao/IThreePidSessionDao.java index 7d647d38..dd5085e5 100644 --- a/src/main/groovy/io/kamax/mxisd/mapping/MappingSession.java +++ b/src/main/groovy/io/kamax/mxisd/storage/dao/IThreePidSessionDao.java @@ -18,18 +18,24 @@ * along with this program. If not, see . */ -package io.kamax.mxisd.mapping; +package io.kamax.mxisd.storage.dao; -public interface MappingSession { +public interface IThreePidSessionDao { + + String getId(); String getServer(); + String getMedium(); + + String getAddress(); + String getSecret(); int getAttempt(); - String getMedium(); + String getNextLink(); - String getValue(); + String getToken(); } diff --git a/src/main/groovy/io/kamax/mxisd/storage/ormlite/OrmLiteSqliteStorage.java b/src/main/groovy/io/kamax/mxisd/storage/ormlite/OrmLiteSqliteStorage.java index e76596ab..0aac625e 100644 --- a/src/main/groovy/io/kamax/mxisd/storage/ormlite/OrmLiteSqliteStorage.java +++ b/src/main/groovy/io/kamax/mxisd/storage/ormlite/OrmLiteSqliteStorage.java @@ -26,8 +26,14 @@ import com.j256.ormlite.jdbc.JdbcConnectionSource; import com.j256.ormlite.support.ConnectionSource; import com.j256.ormlite.table.TableUtils; +import io.kamax.mxisd.ThreePid; +import io.kamax.mxisd.exception.InternalServerError; import io.kamax.mxisd.invitation.IThreePidInviteReply; import io.kamax.mxisd.storage.IStorage; +import io.kamax.mxisd.storage.dao.IThreePidSessionDao; +import io.kamax.mxisd.storage.ormlite.dao.ThreePidSessionDao; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; @@ -35,59 +41,141 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Optional; public class OrmLiteSqliteStorage implements IStorage { + private Logger log = LoggerFactory.getLogger(OrmLiteSqliteStorage.class); + + @FunctionalInterface + private interface Getter { + + T get() throws SQLException, IOException; + + } + + @FunctionalInterface + private interface Doer { + + void run() throws SQLException, IOException; + + } + private Dao invDao; + private Dao sessionDao; OrmLiteSqliteStorage(String path) { - try { + withCatcher(() -> { File parent = new File(path).getParentFile(); if (!parent.mkdirs() && !parent.isDirectory()) { throw new RuntimeException("Unable to create DB parent directory: " + parent); } ConnectionSource connPool = new JdbcConnectionSource("jdbc:sqlite:" + path); - invDao = DaoManager.createDao(connPool, ThreePidInviteIO.class); - TableUtils.createTableIfNotExists(connPool, ThreePidInviteIO.class); - } catch (SQLException e) { + invDao = createDaoAndTable(connPool, ThreePidInviteIO.class); + sessionDao = createDaoAndTable(connPool, ThreePidSessionDao.class); + }); + } + + private Dao createDaoAndTable(ConnectionSource connPool, Class c) throws SQLException { + Dao dao = DaoManager.createDao(connPool, c); + TableUtils.createTableIfNotExists(connPool, c); + return dao; + } + + private T withCatcher(Getter g) { + try { + return g.get(); + } catch (SQLException | IOException e) { throw new RuntimeException(e); // FIXME do better } } - @Override - public Collection getInvites() { - try (CloseableWrappedIterable t = invDao.getWrappedIterable()) { - List ioList = new ArrayList<>(); - t.forEach(ioList::add); - return ioList; - } catch (IOException e) { + private void withCatcher(Doer d) { + try { + d.run(); + } catch (SQLException | IOException e) { throw new RuntimeException(e); // FIXME do better } } + private List forIterable(CloseableWrappedIterable t) { + return withCatcher(() -> { + try { + List ioList = new ArrayList<>(); + t.forEach(ioList::add); + return ioList; + } finally { + t.close(); + } + }); + } + + @Override + public Collection getInvites() { + return forIterable(invDao.getWrappedIterable()); + } + @Override public void insertInvite(IThreePidInviteReply data) { - try { + withCatcher(() -> { int updated = invDao.create(new ThreePidInviteIO(data)); if (updated != 1) { throw new RuntimeException("Unexpected row count after DB action: " + updated); } - } catch (SQLException e) { - throw new RuntimeException(e); // FIXME do better - } + }); } @Override public void deleteInvite(String id) { - try { + withCatcher(() -> { int updated = invDao.deleteById(id); if (updated != 1) { throw new RuntimeException("Unexpected row count after DB action: " + updated); } - } catch (SQLException e) { - throw new RuntimeException(e); // FIXME do better - } + }); + } + + @Override + public Optional getThreePidSession(String sid) { + return withCatcher(() -> Optional.ofNullable(sessionDao.queryForId(sid))); + } + + @Override + public Optional findThreePidSession(ThreePid tpid, String secret) { + return withCatcher(() -> { + List daoList = sessionDao.queryForMatchingArgs(new ThreePidSessionDao(tpid, secret)); + if (daoList.size() > 1) { + log.error("Lookup for 3PID Session {}:{} returned more than one result"); + throw new InternalServerError(); + } + + if (daoList.isEmpty()) { + return Optional.empty(); + } + + return Optional.of(daoList.get(0)); + }); + } + + @Override + public void insertThreePidSession(IThreePidSessionDao session) { + withCatcher(() -> { + int updated = sessionDao.create(new ThreePidSessionDao(session)); + if (updated != 1) { + throw new RuntimeException("Unexpected row count after DB action: " + updated); + } + }); + } + + @Override + public void updateThreePidSession(IThreePidSessionDao session) { + withCatcher(() -> { + int updated = sessionDao.update(new ThreePidSessionDao(session)); + if (updated != 1) { + throw new RuntimeException("Unexpected row count after DB action: " + updated); + } + }); } } diff --git a/src/main/groovy/io/kamax/mxisd/storage/ormlite/dao/ThreePidSessionDao.java b/src/main/groovy/io/kamax/mxisd/storage/ormlite/dao/ThreePidSessionDao.java new file mode 100644 index 00000000..0b557e12 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/storage/ormlite/dao/ThreePidSessionDao.java @@ -0,0 +1,147 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.storage.ormlite.dao; + +import com.j256.ormlite.field.DatabaseField; +import com.j256.ormlite.table.DatabaseTable; +import io.kamax.mxisd.ThreePid; +import io.kamax.mxisd.storage.dao.IThreePidSessionDao; + +@DatabaseTable(tableName = "session_3pid") +public class ThreePidSessionDao implements IThreePidSessionDao { + + @DatabaseField(id = true) + private String id; + + @DatabaseField(canBeNull = false) + private String server; + + @DatabaseField(canBeNull = false) + private String medium; + + @DatabaseField(canBeNull = false) + private String address; + + @DatabaseField(canBeNull = false) + private String secret; + + @DatabaseField(canBeNull = false) + private int attempt; + + @DatabaseField + private String nextLink; + + @DatabaseField(canBeNull = false) + private String token; + + public ThreePidSessionDao() { + // stub for ORMLite + } + + public ThreePidSessionDao(IThreePidSessionDao session) { + setId(session.getId()); + setServer(session.getServer()); + setMedium(session.getMedium()); + setAddress(session.getAddress()); + setSecret(session.getSecret()); + setAttempt(session.getAttempt()); + setNextLink(session.getNextLink()); + setToken(session.getToken()); + } + + public ThreePidSessionDao(ThreePid tpid, String secret) { + setMedium(tpid.getMedium()); + setAddress(tpid.getAddress()); + setSecret(secret); + } + + @Override + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @Override + public String getServer() { + return server; + } + + public void setServer(String server) { + this.server = server; + } + + @Override + public String getSecret() { + return secret; + } + + public void setSecret(String secret) { + this.secret = secret; + } + + @Override + public int getAttempt() { + return attempt; + } + + public void setAttempt(int attempt) { + this.attempt = attempt; + } + + @Override + public String getMedium() { + return medium; + } + + public void setMedium(String medium) { + this.medium = medium; + } + + @Override + public String getAddress() { + return address; + } + + public void setAddress(String address) { + this.address = address; + } + + @Override + public String getNextLink() { + return nextLink; + } + + public void setNextLink(String nextLink) { + this.nextLink = nextLink; + } + + @Override + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } +} diff --git a/src/main/groovy/io/kamax/mxisd/threepid/connector/EmailSmtpConnector.java b/src/main/groovy/io/kamax/mxisd/threepid/connector/EmailSmtpConnector.java index b9d68e0f..7bc3ae39 100644 --- a/src/main/groovy/io/kamax/mxisd/threepid/connector/EmailSmtpConnector.java +++ b/src/main/groovy/io/kamax/mxisd/threepid/connector/EmailSmtpConnector.java @@ -22,8 +22,8 @@ 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.config.threepid.medium.EmailConfig; import io.kamax.mxisd.exception.ConfigurationException; import io.kamax.mxisd.invitation.IThreePidInviteReply; import org.apache.commons.io.IOUtils; @@ -47,23 +47,21 @@ 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) { + public EmailSmtpConnector(EmailConfig cfg, EmailSmtpConfig smtpCfg) { try { session = Session.getInstance(System.getProperties()); - sender = new InternetAddress(invCfg.getFrom(), invCfg.getName()); + sender = new InternetAddress(cfg.getFrom(), cfg.getName()); } catch (UnsupportedEncodingException e) { // What are we supposed to do with this?! throw new ConfigurationException(e); } - this.cfg = cfg; - this.invCfg = invCfg; + this.cfg = smtpCfg; } @Override diff --git a/src/main/groovy/io/kamax/mxisd/threepid/session/IThreePidSession.java b/src/main/groovy/io/kamax/mxisd/threepid/session/IThreePidSession.java new file mode 100644 index 00000000..28af4305 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/threepid/session/IThreePidSession.java @@ -0,0 +1,52 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.threepid.session; + +import io.kamax.mxisd.ThreePid; + +import java.time.Instant; +import java.util.Optional; + +public interface IThreePidSession { + + String getId(); + + String getHash(); + + Instant getCreationTime(); + + String getServer(); + + ThreePid getThreePid(); + + int getAttempt(); + + void increaseAttempt(); + + Optional getNextLink(); + + void validate(String token); + + boolean isValidated(); + + Instant getValidationTime(); + +} diff --git a/src/main/groovy/io/kamax/mxisd/threepid/session/ThreePidSession.java b/src/main/groovy/io/kamax/mxisd/threepid/session/ThreePidSession.java new file mode 100644 index 00000000..8d0fcbcc --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/threepid/session/ThreePidSession.java @@ -0,0 +1,197 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.threepid.session; + +import io.kamax.mxisd.ThreePid; +import io.kamax.mxisd.exception.BadRequestException; +import io.kamax.mxisd.exception.InvalidCredentialsException; +import io.kamax.mxisd.storage.dao.IThreePidSessionDao; +import org.apache.commons.lang.StringUtils; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Optional; + +public class ThreePidSession implements IThreePidSession { + + private String id; + private Instant timestamp; + private String hash; + private String server; + private ThreePid tPid; + private String secret; + private String nextLink; + private String token; + private int attempt; + private Instant validationTimestamp; + private boolean isValidated; + + public ThreePidSession(IThreePidSessionDao dao) { + this( + dao.getId(), + dao.getServer(), + new ThreePid(dao.getMedium(), dao.getAddress()), + dao.getSecret(), + dao.getAttempt(), + dao.getNextLink(), + dao.getToken() + ); + } + + public ThreePidSession(String id, String server, ThreePid tPid, String secret, int attempt, String nextLink, String token) { + this.id = id; + this.server = server; + this.tPid = new ThreePid(tPid); + this.secret = secret; + this.attempt = attempt; + this.nextLink = nextLink; + this.token = token; + + this.timestamp = Instant.now(); + this.hash = server.toLowerCase() + tPid.getMedium().toLowerCase() + tPid.getAddress().toLowerCase() + secret; + } + + @Override + public String getId() { + return id; + } + + @Override + public String getHash() { + return hash; + } + + @Override + public Instant getCreationTime() { + return timestamp; + } + + @Override + public String getServer() { + return server; + } + + @Override + public ThreePid getThreePid() { + return tPid; + } + + public String getSecret() { + return secret; + } + + @Override + public int getAttempt() { + return attempt; + } + + @Override + public void increaseAttempt() { + attempt++; + } + + @Override + public Optional getNextLink() { + return Optional.ofNullable(nextLink); + } + + public synchronized void setAttempt(int attempt) { + if (isValidated()) { + throw new IllegalStateException(); + } + + this.attempt = attempt; + } + + @Override + public Instant getValidationTime() { + return validationTimestamp; + } + + @Override + public boolean isValidated() { + return isValidated; + } + + public synchronized void validate(String token) { + if (Instant.now().minus(24, ChronoUnit.HOURS).isAfter(getCreationTime())) { + throw new BadRequestException("Session " + getId() + " has expired"); + } + + if (!StringUtils.equals(this.token, token)) { + throw new InvalidCredentialsException(); + } + + if (isValidated()) { + return; + } + + validationTimestamp = Instant.now(); + isValidated = true; + } + + public IThreePidSessionDao getDao() { + return new IThreePidSessionDao() { + + @Override + public String getId() { + return id; + } + + @Override + public String getServer() { + return server; + } + + @Override + public String getMedium() { + return tPid.getMedium(); + } + + @Override + public String getAddress() { + return tPid.getAddress(); + } + + @Override + public String getSecret() { + return secret; + } + + @Override + public int getAttempt() { + return attempt; + } + + @Override + public String getNextLink() { + return nextLink; + } + + @Override + public String getToken() { + return token; + } + + }; + } + +} From bf2afd873946f9637d79d3867c8c6c59aec264e5 Mon Sep 17 00:00:00 2001 From: Maxime Dor Date: Wed, 20 Sep 2017 17:22:51 +0200 Subject: [PATCH 03/14] Further work --- .../io/kamax/mxisd/config/SessionConfig.java | 35 ++++++++ .../invite/medium/EmailInviteConfig.java | 67 --------------- .../threepid/connector/EmailSmtpConfig.java | 2 +- .../config/threepid/medium/EmailConfig.java | 69 ++++++++++++---- .../threepid/medium/EmailTemplateConfig.java | 82 +++++++++++++++++++ .../controller/v1/SessionController.groovy | 4 +- .../exception/NotImplementedException.groovy | 7 +- .../mxisd/invitation/InvitationManager.java | 35 ++------ .../INotificationHandler.java} | 9 +- .../notification/NotificationManager.java | 69 ++++++++++++++++ .../SessionMananger.java} | 45 +++++----- .../connector/IThreePidConnector.java | 6 +- .../{ => email}/EmailSmtpConnector.java | 46 ++++------- .../connector/email/IEmailConnector.java | 35 ++++++++ .../notification/INotificationGenerator.java | 38 +++++++++ .../email/EmailNotificationGenerator.java} | 74 +++++++++++------ .../email/EmailNotificationHandler.java | 71 ++++++++++++++++ .../email/IEmailNotificationGenerator.java | 33 ++++++++ .../threepid/session/IThreePidSession.java | 4 +- .../threepid/session/ThreePidSession.java | 5 ++ src/main/resources/application.yaml | 25 ++++-- .../resources/email/validate-template.eml | 66 +++++++++++++++ 22 files changed, 622 insertions(+), 205 deletions(-) create mode 100644 src/main/groovy/io/kamax/mxisd/config/SessionConfig.java delete mode 100644 src/main/groovy/io/kamax/mxisd/config/invite/medium/EmailInviteConfig.java create mode 100644 src/main/groovy/io/kamax/mxisd/config/threepid/medium/EmailTemplateConfig.java rename src/main/groovy/io/kamax/mxisd/{invitation/generator/IInviteContentGenerator.java => notification/INotificationHandler.java} (79%) create mode 100644 src/main/groovy/io/kamax/mxisd/notification/NotificationManager.java rename src/main/groovy/io/kamax/mxisd/{mapping/MappingManager.java => session/SessionMananger.java} (87%) rename src/main/groovy/io/kamax/mxisd/threepid/connector/{ => email}/EmailSmtpConnector.java (64%) create mode 100644 src/main/groovy/io/kamax/mxisd/threepid/connector/email/IEmailConnector.java create mode 100644 src/main/groovy/io/kamax/mxisd/threepid/notification/INotificationGenerator.java rename src/main/groovy/io/kamax/mxisd/{invitation/generator/EmailInviteContentGenerator.java => threepid/notification/email/EmailNotificationGenerator.java} (56%) create mode 100644 src/main/groovy/io/kamax/mxisd/threepid/notification/email/EmailNotificationHandler.java create mode 100644 src/main/groovy/io/kamax/mxisd/threepid/notification/email/IEmailNotificationGenerator.java create mode 100644 src/main/resources/email/validate-template.eml diff --git a/src/main/groovy/io/kamax/mxisd/config/SessionConfig.java b/src/main/groovy/io/kamax/mxisd/config/SessionConfig.java new file mode 100644 index 00000000..fb515375 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/config/SessionConfig.java @@ -0,0 +1,35 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.config; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties("session") +public class SessionConfig { + + private static Logger log = LoggerFactory.getLogger(SessionConfig.class); + + +} diff --git a/src/main/groovy/io/kamax/mxisd/config/invite/medium/EmailInviteConfig.java b/src/main/groovy/io/kamax/mxisd/config/invite/medium/EmailInviteConfig.java deleted file mode 100644 index a244aff0..00000000 --- a/src/main/groovy/io/kamax/mxisd/config/invite/medium/EmailInviteConfig.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * mxisd - Matrix Identity Server Daemon - * Copyright (C) 2017 Maxime Dor - * - * https://max.kamax.io/ - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package io.kamax.mxisd.config.invite.medium; - -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("invite.medium.email") -public class EmailInviteConfig { - - private Logger log = LoggerFactory.getLogger(EmailInviteConfig.class); - - private String template; - - public String getTemplate() { - return template; - } - - public void setTemplate(String template) { - this.template = template; - } - - @PostConstruct - public void build() { - log.info("--- E-mail invites config ---"); - - 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()); - } - } - -} diff --git a/src/main/groovy/io/kamax/mxisd/config/threepid/connector/EmailSmtpConfig.java b/src/main/groovy/io/kamax/mxisd/config/threepid/connector/EmailSmtpConfig.java index 24a5e49e..49848b8b 100644 --- a/src/main/groovy/io/kamax/mxisd/config/threepid/connector/EmailSmtpConfig.java +++ b/src/main/groovy/io/kamax/mxisd/config/threepid/connector/EmailSmtpConfig.java @@ -29,7 +29,7 @@ import javax.annotation.PostConstruct; @Configuration -@ConfigurationProperties(prefix = "threepid.email.connector.provider.smtp") +@ConfigurationProperties(prefix = "threepid.medium.email.connectors.smtp") public class EmailSmtpConfig { private Logger log = LoggerFactory.getLogger(EmailSmtpConfig.class); diff --git a/src/main/groovy/io/kamax/mxisd/config/threepid/medium/EmailConfig.java b/src/main/groovy/io/kamax/mxisd/config/threepid/medium/EmailConfig.java index 2d47bf3b..29ac7c06 100644 --- a/src/main/groovy/io/kamax/mxisd/config/threepid/medium/EmailConfig.java +++ b/src/main/groovy/io/kamax/mxisd/config/threepid/medium/EmailConfig.java @@ -21,6 +21,7 @@ package io.kamax.mxisd.config.threepid.medium; import io.kamax.mxisd.config.MatrixConfig; +import io.kamax.mxisd.exception.ConfigurationException; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.WordUtils; import org.slf4j.Logger; @@ -35,43 +36,81 @@ @ConfigurationProperties("threepid.medium.email") public class EmailConfig { + public static class Identity { + private String from; + private String name; + + 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; + } + + } + + private String generator; + private String connector; + private Logger log = LoggerFactory.getLogger(EmailConfig.class); private MatrixConfig mxCfg; - - private String from; - private String name; + private Identity identity = new Identity(); @Autowired public EmailConfig(MatrixConfig mxCfg) { this.mxCfg = mxCfg; } - public String getFrom() { - return from; + public Identity getIdentity() { + return identity; + } + + public String getGenerator() { + return generator; } - public void setFrom(String from) { - this.from = from; + public void setGenerator(String generator) { + this.generator = generator; } - public String getName() { - return name; + public String getConnector() { + return connector; } - public void setName(String name) { - this.name = name; + public void setConnector(String connector) { + this.connector = connector; } @PostConstruct public void build() { log.info("--- E-mail config ---"); - log.info("From: {}", getFrom()); - if (StringUtils.isBlank(getName())) { - setName(WordUtils.capitalize(mxCfg.getDomain()) + " Identity Server"); + if (StringUtils.isBlank(getGenerator())) { + throw new ConfigurationException("generator"); + } + + if (StringUtils.isBlank(getConnector())) { + throw new ConfigurationException("connector"); + } + + log.info("From: {}", identity.getFrom()); + + if (StringUtils.isBlank(identity.getName())) { + identity.setName(WordUtils.capitalize(mxCfg.getDomain()) + " Identity Server"); } - log.info("Name: {}", getName()); + log.info("Name: {}", identity.getName()); + log.info("Generator: {}", getGenerator()); + log.info("Connector: {}", getConnector()); } } diff --git a/src/main/groovy/io/kamax/mxisd/config/threepid/medium/EmailTemplateConfig.java b/src/main/groovy/io/kamax/mxisd/config/threepid/medium/EmailTemplateConfig.java new file mode 100644 index 00000000..4054dd7c --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/config/threepid/medium/EmailTemplateConfig.java @@ -0,0 +1,82 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.config.threepid.medium; + +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; + +@Configuration +@ConfigurationProperties("threepid.medium.email.generators.template") +public class EmailTemplateConfig { + + private static Logger log = LoggerFactory.getLogger(EmailTemplateConfig.class); + private static final String classpathPrefix = "classpath:"; + + private static String getName(String path) { + if (StringUtils.startsWith(path, classpathPrefix)) { + return "Built-in (" + path.substring(classpathPrefix.length()) + ")"; + } + + return path; + } + + public static class Session { + + private String validation; + + public String getValidation() { + return validation; + } + + public void setValidation(String validation) { + this.validation = validation; + } + + } + + private String invite; + private Session session = new Session(); + + public String getInvite() { + return invite; + } + + public void setInvite(String invite) { + this.invite = invite; + } + + public Session getSession() { + return session; + } + + @PostConstruct + public void build() { + log.info("--- E-mail Generator templates config ---"); + log.info("Invite: {}", getName(getInvite())); + log.info("Session validation: {}", getName(getSession().getValidation())); + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/controller/v1/SessionController.groovy b/src/main/groovy/io/kamax/mxisd/controller/v1/SessionController.groovy index 93181b4d..1e107d7b 100644 --- a/src/main/groovy/io/kamax/mxisd/controller/v1/SessionController.groovy +++ b/src/main/groovy/io/kamax/mxisd/controller/v1/SessionController.groovy @@ -29,7 +29,7 @@ import io.kamax.mxisd.controller.v1.io.SessionPhoneTokenRequestJson import io.kamax.mxisd.exception.BadRequestException import io.kamax.mxisd.invitation.InvitationManager import io.kamax.mxisd.lookup.ThreePidValidation -import io.kamax.mxisd.mapping.MappingManager +import io.kamax.mxisd.session.SessionMananger import org.apache.commons.io.IOUtils import org.apache.http.HttpStatus import org.slf4j.Logger @@ -48,7 +48,7 @@ import java.nio.charset.StandardCharsets class SessionController { @Autowired - private MappingManager mgr + private SessionMananger mgr @Autowired private InvitationManager invMgr; diff --git a/src/main/groovy/io/kamax/mxisd/exception/NotImplementedException.groovy b/src/main/groovy/io/kamax/mxisd/exception/NotImplementedException.groovy index 57ab376e..59121365 100644 --- a/src/main/groovy/io/kamax/mxisd/exception/NotImplementedException.groovy +++ b/src/main/groovy/io/kamax/mxisd/exception/NotImplementedException.groovy @@ -24,5 +24,10 @@ import org.springframework.http.HttpStatus import org.springframework.web.bind.annotation.ResponseStatus @ResponseStatus(value = HttpStatus.NOT_IMPLEMENTED) -class NotImplementedException extends RuntimeException { +public class NotImplementedException extends RuntimeException { + + public NotImplementedException(String s) { + super(s); + } + } diff --git a/src/main/groovy/io/kamax/mxisd/invitation/InvitationManager.java b/src/main/groovy/io/kamax/mxisd/invitation/InvitationManager.java index d5502d3f..f0acdd5b 100644 --- a/src/main/groovy/io/kamax/mxisd/invitation/InvitationManager.java +++ b/src/main/groovy/io/kamax/mxisd/invitation/InvitationManager.java @@ -26,14 +26,13 @@ import io.kamax.mxisd.config.DnsOverwriteEntry; import io.kamax.mxisd.exception.BadRequestException; import io.kamax.mxisd.exception.MappingAlreadyExistsException; -import io.kamax.mxisd.invitation.generator.IInviteContentGenerator; import io.kamax.mxisd.lookup.SingleLookupReply; import io.kamax.mxisd.lookup.ThreePidMapping; import io.kamax.mxisd.lookup.strategy.LookupStrategy; +import io.kamax.mxisd.notification.NotificationManager; import io.kamax.mxisd.signature.SignatureManager; import io.kamax.mxisd.storage.IStorage; import io.kamax.mxisd.storage.ormlite.ThreePidInviteIO; -import io.kamax.mxisd.threepid.connector.IThreePidConnector; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.RandomStringUtils; import org.apache.commons.lang.StringUtils; @@ -84,35 +83,15 @@ public class InvitationManager { @Autowired private DnsOverwrite dns; - private Map generators; - private Map connectors; + private NotificationManager notifMgr; private CloseableHttpClient client; private Gson gson; private Timer refreshTimer; @Autowired - public InvitationManager( - List generatorList, - List 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); - }); + public InvitationManager(NotificationManager notifMgr) { + this.notifMgr = notifMgr; } @PostConstruct @@ -221,9 +200,7 @@ String findHomeserverForDomain(String domain) { } public synchronized IThreePidInviteReply storeInvite(IThreePidInvite invitation) { // TODO better sync - IInviteContentGenerator generator = generators.get(invitation.getMedium()); - IThreePidConnector connector = connectors.get(invitation.getMedium()); - if (generator == null || connector == null) { + if (!notifMgr.isMediumSupported(invitation.getMedium())) { throw new BadRequestException("Medium type " + invitation.getMedium() + " is not supported"); } @@ -246,7 +223,7 @@ public synchronized IThreePidInviteReply storeInvite(IThreePidInvite invitation) IThreePidInviteReply reply = new ThreePidInviteReply(invId, invitation, token, displayName); log.info("Performing invite to {}:{}", invitation.getMedium(), invitation.getAddress()); - connector.send(reply, generator.generate(reply)); + notifMgr.send(reply); log.info("Storing invite under ID {}", invId); storage.insertInvite(reply); diff --git a/src/main/groovy/io/kamax/mxisd/invitation/generator/IInviteContentGenerator.java b/src/main/groovy/io/kamax/mxisd/notification/INotificationHandler.java similarity index 79% rename from src/main/groovy/io/kamax/mxisd/invitation/generator/IInviteContentGenerator.java rename to src/main/groovy/io/kamax/mxisd/notification/INotificationHandler.java index a05b3813..0b622dff 100644 --- a/src/main/groovy/io/kamax/mxisd/invitation/generator/IInviteContentGenerator.java +++ b/src/main/groovy/io/kamax/mxisd/notification/INotificationHandler.java @@ -18,14 +18,17 @@ * along with this program. If not, see . */ -package io.kamax.mxisd.invitation.generator; +package io.kamax.mxisd.notification; import io.kamax.mxisd.invitation.IThreePidInviteReply; +import io.kamax.mxisd.threepid.session.IThreePidSession; -public interface IInviteContentGenerator { +public interface INotificationHandler { String getMedium(); - String generate(IThreePidInviteReply invite); + void notify(IThreePidInviteReply invite); + + void notify(IThreePidSession session); } diff --git a/src/main/groovy/io/kamax/mxisd/notification/NotificationManager.java b/src/main/groovy/io/kamax/mxisd/notification/NotificationManager.java new file mode 100644 index 00000000..63d72e43 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/notification/NotificationManager.java @@ -0,0 +1,69 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.notification; + +import io.kamax.mxisd.exception.NotImplementedException; +import io.kamax.mxisd.invitation.IThreePidInviteReply; +import io.kamax.mxisd.threepid.session.IThreePidSession; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Component +public class NotificationManager { + + private Map handlers; + + @Autowired + public NotificationManager(List handlers) { + this.handlers = new HashMap<>(); + handlers.forEach(h -> this.handlers.put(h.getMedium(), h)); + } + + private INotificationHandler ensureMedium(String medium) { + INotificationHandler handler = handlers.get(medium); + if (handler == null) { + throw new NotImplementedException(medium + " is not a supported 3PID medium type"); + } + + return handler; + } + + public boolean isMediumSupported(String medium) { + return handlers.containsKey(medium); + } + + public void sendForInvite(IThreePidInviteReply invite) { + ensureMedium(invite.getInvite().getMedium()).notify(invite); + } + + public void sendForValidation(IThreePidSession session) { + ensureMedium(session.getThreePid().getMedium()).notify(session); + } + + public void sendforRemotePublish(IThreePidSession session) { + throw new NotImplementedException("Remote publish of 3PID bind"); + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/mapping/MappingManager.java b/src/main/groovy/io/kamax/mxisd/session/SessionMananger.java similarity index 87% rename from src/main/groovy/io/kamax/mxisd/mapping/MappingManager.java rename to src/main/groovy/io/kamax/mxisd/session/SessionMananger.java index 42e53b0c..d6072d7b 100644 --- a/src/main/groovy/io/kamax/mxisd/mapping/MappingManager.java +++ b/src/main/groovy/io/kamax/mxisd/session/SessionMananger.java @@ -18,14 +18,16 @@ * along with this program. If not, see . */ -package io.kamax.mxisd.mapping; +package io.kamax.mxisd.session; import io.kamax.matrix.ThreePidMedium; import io.kamax.mxisd.ThreePid; +import io.kamax.mxisd.config.SessionConfig; import io.kamax.mxisd.exception.InvalidCredentialsException; import io.kamax.mxisd.lookup.SingleLookupReply; import io.kamax.mxisd.lookup.ThreePidValidation; import io.kamax.mxisd.lookup.strategy.LookupStrategy; +import io.kamax.mxisd.notification.NotificationManager; import io.kamax.mxisd.storage.IStorage; import io.kamax.mxisd.storage.dao.IThreePidSessionDao; import io.kamax.mxisd.threepid.session.ThreePidSession; @@ -40,16 +42,21 @@ import java.util.UUID; @Component -public class MappingManager { +public class SessionMananger { - private Logger log = LoggerFactory.getLogger(MappingManager.class); + private Logger log = LoggerFactory.getLogger(SessionMananger.class); + private SessionConfig cfg; private IStorage storage; private LookupStrategy lookup; + private NotificationManager notifMgr; @Autowired - public MappingManager(IStorage storage, LookupStrategy lookup) { + public SessionMananger(SessionConfig cfg, IStorage storage, LookupStrategy lookup, NotificationManager notifMgr) { + this.cfg = cfg; this.storage = storage; + this.lookup = lookup; + this.notifMgr = notifMgr; } private ThreePidSession getSession(String sid, String secret) { @@ -78,7 +85,8 @@ public String create(String server, ThreePid tpid, String secret, int attempt, S log.info("We already have a session for {}: {}", tpid, session.getId()); if (session.getAttempt() < attempt) { log.info("Received attempt {} is greater than stored attempt {}, sending validation communication", attempt, session.getAttempt()); - // TODO send via connector + notifMgr.sendForValidation(session); + log.info("Sent validation notification to {}", tpid); session.increaseAttempt(); storage.updateThreePidSession(session.getDao()); } @@ -95,8 +103,8 @@ public String create(String server, ThreePid tpid, String secret, int attempt, S ThreePidSession session = new ThreePidSession(sessionId, server, tpid, secret, attempt, nextLink, token); log.info("Generated new session {} to validate {} from server {}", sessionId, tpid, server); - // TODO send via connector - // log.info("Sent validation notification to {}", tpid); + notifMgr.sendForValidation(session); + log.info("Sent validation notification to {}", tpid); storage.insertThreePidSession(session.getDao()); log.info("Stored session {}", sessionId, tpid, server); @@ -124,13 +132,7 @@ public void bind(String sid, String secret, String mxid) { ThreePidSession session = getSessionIfValidated(sid, secret); log.info("Attempting bind of {} on session {} from server {}", mxid, session.getId(), session.getServer()); - // We lookup if the 3PID is already known locally. - // If it is, we do not need to process any further as it is already bound. - Optional rLocal = lookup.findLocal(session.getThreePid().getMedium(), session.getThreePid().getAddress()); - boolean knownLocal = rLocal.isPresent() && StringUtils.equals(rLocal.get().getMxid().getId(), mxid); - log.info("Mapping {} -> {} is " + (knownLocal ? "already" : "not") + " known locally", mxid, session.getThreePid()); - - // MXID is not known locally, checking remotely + // We lookup if the 3PID is already known remotely. Optional rRemote = lookup.findRemote(session.getThreePid().getMedium(), session.getThreePid().getAddress()); boolean knownRemote = rRemote.isPresent() && StringUtils.equals(rRemote.get().getMxid().getId(), mxid); log.info("Mapping {} -> {} is " + (knownRemote ? "already" : "not") + " known remotely", mxid, session.getThreePid()); @@ -143,25 +145,28 @@ public void bind(String sid, String secret, String mxid) { isLocalDomain = session.getThreePid().getAddress().isEmpty(); // FIXME only for testing } if (knownRemote) { - if (isLocalDomain && !knownLocal) { - log.warn("Mapping {} -> {} is not known locally but is about a local domain!"); - } - log.info("No further action needed for Mapping {} -> {}"); return; } + // We lookup if the 3PID is already known locally. + Optional rLocal = lookup.findLocal(session.getThreePid().getMedium(), session.getThreePid().getAddress()); + boolean knownLocal = rLocal.isPresent() && StringUtils.equals(rLocal.get().getMxid().getId(), mxid); + log.info("Mapping {} -> {} is " + (knownLocal ? "already" : "not") + " known locally", mxid, session.getThreePid()); + // This might need a configuration by medium type? if (knownLocal) { // 3PID is ony known local if (isLocalDomain) { // TODO // 1. Check if global publishing is enabled, allowed and offered. If one is no, return. // 2. Publish globally + notifMgr.sendforRemotePublish(session); } - if (System.currentTimeMillis() % 2 == 0) { + if (System.currentTimeMillis() % 2 == 0) { // FIXME only for testing // TODO // 1. Check if configured to publish globally non-local domain. If no, return + notifMgr.sendforRemotePublish(session); } // TODO @@ -172,6 +177,7 @@ public void bind(String sid, String secret, String mxid) { // 2. call mxisd-specific endpoint to publish globally // 3. check regularly on client page for a binding // 4. when found, show page "Done globally!" + notifMgr.sendforRemotePublish(session); } else { if (isLocalDomain) { // 3PID is not known anywhere but is a local domain // TODO @@ -180,6 +186,7 @@ public void bind(String sid, String secret, String mxid) { } else { // 3PID is not known anywhere and is remote // TODO // Proxy to configurable IS, by default Matrix.org + notifMgr.sendforRemotePublish(session); } } } diff --git a/src/main/groovy/io/kamax/mxisd/threepid/connector/IThreePidConnector.java b/src/main/groovy/io/kamax/mxisd/threepid/connector/IThreePidConnector.java index 07bdc1a7..c8fc23f1 100644 --- a/src/main/groovy/io/kamax/mxisd/threepid/connector/IThreePidConnector.java +++ b/src/main/groovy/io/kamax/mxisd/threepid/connector/IThreePidConnector.java @@ -20,12 +20,10 @@ package io.kamax.mxisd.threepid.connector; -import io.kamax.mxisd.invitation.IThreePidInviteReply; - public interface IThreePidConnector { - String getMedium(); + String getId(); - void send(IThreePidInviteReply invite, String content); + String getMedium(); } diff --git a/src/main/groovy/io/kamax/mxisd/threepid/connector/EmailSmtpConnector.java b/src/main/groovy/io/kamax/mxisd/threepid/connector/email/EmailSmtpConnector.java similarity index 64% rename from src/main/groovy/io/kamax/mxisd/threepid/connector/EmailSmtpConnector.java rename to src/main/groovy/io/kamax/mxisd/threepid/connector/email/EmailSmtpConnector.java index 7bc3ae39..8124bf71 100644 --- a/src/main/groovy/io/kamax/mxisd/threepid/connector/EmailSmtpConnector.java +++ b/src/main/groovy/io/kamax/mxisd/threepid/connector/email/EmailSmtpConnector.java @@ -18,14 +18,11 @@ * along with this program. If not, see . */ -package io.kamax.mxisd.threepid.connector; +package io.kamax.mxisd.threepid.connector.email; import com.sun.mail.smtp.SMTPTransport; import io.kamax.matrix.ThreePidMedium; import io.kamax.mxisd.config.threepid.connector.EmailSmtpConfig; -import io.kamax.mxisd.config.threepid.medium.EmailConfig; -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; @@ -42,26 +39,22 @@ import java.util.Date; @Component -public class EmailSmtpConnector implements IThreePidConnector { +public class EmailSmtpConnector implements IEmailConnector { private Logger log = LoggerFactory.getLogger(EmailSmtpConnector.class); private EmailSmtpConfig cfg; - private Session session; - private InternetAddress sender; @Autowired - public EmailSmtpConnector(EmailConfig cfg, EmailSmtpConfig smtpCfg) { - try { - session = Session.getInstance(System.getProperties()); - sender = new InternetAddress(cfg.getFrom(), cfg.getName()); - } catch (UnsupportedEncodingException e) { - // What are we supposed to do with this?! - throw new ConfigurationException(e); - } + public EmailSmtpConnector(EmailSmtpConfig cfg) { + this.cfg = cfg; + session = Session.getInstance(System.getProperties()); + } - this.cfg = smtpCfg; + @Override + public String getId() { + return "smtp"; } @Override @@ -70,20 +63,17 @@ public String getMedium() { } @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"); - } - + public void send(String senderAddress, String senderName, String recipient, String content) { try { + InternetAddress sender = new InternetAddress(senderAddress, senderName); MimeMessage msg = new MimeMessage(session, IOUtils.toInputStream(content, StandardCharsets.UTF_8)); - msg.setHeader("X-Mailer", "mxisd"); // TODO set version + msg.setHeader("X-Mailer", "mxisd"); // FIXME set version msg.setSentDate(new Date()); msg.setFrom(sender); - msg.setRecipients(Message.RecipientType.TO, invite.getInvite().getAddress()); + msg.setRecipients(Message.RecipientType.TO, recipient); msg.saveChanges(); - log.info("Sending invite to {} via SMTP using {}:{}", invite.getInvite().getAddress(), cfg.getHost(), cfg.getPort()); + log.info("Sending invite to {} via SMTP using {}:{}", recipient, cfg.getHost(), cfg.getPort()); SMTPTransport transport = (SMTPTransport) session.getTransport("smtp"); transport.setStartTLS(cfg.getTls() > 0); transport.setRequireStartTLS(cfg.getTls() > 1); @@ -91,13 +81,13 @@ public void send(IThreePidInviteReply invite, String content) { 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()); + transport.sendMessage(msg, InternetAddress.parse(recipient)); + log.info("Invite to {} was sent", recipient); } finally { transport.close(); } - } catch (MessagingException e) { - throw new RuntimeException("Unable to send e-mail invite to " + invite.getInvite().getAddress(), e); + } catch (UnsupportedEncodingException | MessagingException e) { + throw new RuntimeException("Unable to send e-mail invite to " + recipient, e); } } diff --git a/src/main/groovy/io/kamax/mxisd/threepid/connector/email/IEmailConnector.java b/src/main/groovy/io/kamax/mxisd/threepid/connector/email/IEmailConnector.java new file mode 100644 index 00000000..e2b7bd1d --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/threepid/connector/email/IEmailConnector.java @@ -0,0 +1,35 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.threepid.connector.email; + +import io.kamax.matrix.ThreePidMedium; +import io.kamax.mxisd.threepid.connector.IThreePidConnector; + +public interface IEmailConnector extends IThreePidConnector { + + @Override + default String getMedium() { + return ThreePidMedium.Email.getId(); + } + + void send(String senderAddress, String senderName, String recipient, String content); + +} diff --git a/src/main/groovy/io/kamax/mxisd/threepid/notification/INotificationGenerator.java b/src/main/groovy/io/kamax/mxisd/threepid/notification/INotificationGenerator.java new file mode 100644 index 00000000..969a87b6 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/threepid/notification/INotificationGenerator.java @@ -0,0 +1,38 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.threepid.notification; + +import io.kamax.mxisd.invitation.IThreePidInviteReply; +import io.kamax.mxisd.threepid.session.IThreePidSession; + +public interface INotificationGenerator { + + String getId(); + + String getMedium(); + + String get(IThreePidInviteReply invite); + + String getForValidation(IThreePidSession session); + + String getForRemotePublishingValidation(IThreePidSession session); + +} diff --git a/src/main/groovy/io/kamax/mxisd/invitation/generator/EmailInviteContentGenerator.java b/src/main/groovy/io/kamax/mxisd/threepid/notification/email/EmailNotificationGenerator.java similarity index 56% rename from src/main/groovy/io/kamax/mxisd/invitation/generator/EmailInviteContentGenerator.java rename to src/main/groovy/io/kamax/mxisd/threepid/notification/email/EmailNotificationGenerator.java index 0bce14d7..b327bea5 100644 --- a/src/main/groovy/io/kamax/mxisd/invitation/generator/EmailInviteContentGenerator.java +++ b/src/main/groovy/io/kamax/mxisd/threepid/notification/email/EmailNotificationGenerator.java @@ -18,13 +18,14 @@ * along with this program. If not, see . */ -package io.kamax.mxisd.invitation.generator; +package io.kamax.mxisd.threepid.notification.email; -import io.kamax.matrix.ThreePidMedium; +import io.kamax.mxisd.ThreePid; import io.kamax.mxisd.config.MatrixConfig; -import io.kamax.mxisd.config.invite.medium.EmailInviteConfig; import io.kamax.mxisd.config.threepid.medium.EmailConfig; +import io.kamax.mxisd.config.threepid.medium.EmailTemplateConfig; import io.kamax.mxisd.invitation.IThreePidInviteReply; +import io.kamax.mxisd.threepid.session.IThreePidSession; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.WordUtils; @@ -34,55 +35,68 @@ import java.io.FileInputStream; import java.io.IOException; +import java.io.InputStream; import java.nio.charset.StandardCharsets; @Component -public class EmailInviteContentGenerator implements IInviteContentGenerator { +public class EmailNotificationGenerator implements IEmailNotificationGenerator { private EmailConfig cfg; - private EmailInviteConfig invCfg; + private EmailTemplateConfig templateCfg; private MatrixConfig mxCfg; private ApplicationContext app; @Autowired // FIXME ApplicationContext shouldn't be injected, find another way from config (?) - public EmailInviteContentGenerator(EmailConfig cfg, EmailInviteConfig invCfg, MatrixConfig mxCfg, ApplicationContext app) { + public EmailNotificationGenerator(EmailConfig cfg, EmailTemplateConfig templateCfg, MatrixConfig mxCfg, ApplicationContext app) { this.cfg = cfg; - this.invCfg = invCfg; + this.templateCfg = templateCfg; this.mxCfg = mxCfg; this.app = app; } @Override - public String getMedium() { - return ThreePidMedium.Email.getId(); + public String getId() { + return "template"; } - @Override - public String generate(IThreePidInviteReply invite) { - if (!ThreePidMedium.Email.is(invite.getInvite().getMedium())) { - throw new IllegalArgumentException(invite.getInvite().getMedium() + " is not a supported 3PID type"); - } + private String getTemplateContent(String location) throws IOException { + InputStream is = StringUtils.startsWith(location, "classpath:") ? + app.getResource(location).getInputStream() : new FileInputStream(location); + return IOUtils.toString(is, StandardCharsets.UTF_8); + } + + private String populateCommon(String content, ThreePid recipient) { + String domainPretty = WordUtils.capitalizeFully(mxCfg.getDomain()); + + content = content.replace("%DOMAIN%", mxCfg.getDomain()); + content = content.replace("%DOMAIN_PRETTY%", domainPretty); + content = content.replace("%FROM_EMAIL%", cfg.getIdentity().getFrom()); + content = content.replace("%FROM_NAME%", cfg.getIdentity().getName()); + content = content.replace("%RECIPIENT_MEDIUM%", recipient.getMedium()); + content = content.replace("%RECIPIENT_ADDRESS%", recipient.getAddress()); + return content; + } + + private String getTemplateAndPopulate(String location, ThreePid recipient) throws IOException { + return populateCommon(getTemplateContent(location), recipient); + } + @Override + public String get(IThreePidInviteReply invite) { try { - String domainPretty = WordUtils.capitalizeFully(mxCfg.getDomain()); + ThreePid tpid = new ThreePid(invite.getInvite().getMedium(), invite.getInvite().getAddress()); + String templateBody = getTemplateAndPopulate(templateCfg.getInvite(), tpid); + String senderName = invite.getInvite().getProperties().getOrDefault("sender_display_name", ""); String senderNameOrId = StringUtils.defaultIfBlank(senderName, invite.getInvite().getSender().getId()); String roomName = invite.getInvite().getProperties().getOrDefault("room_name", ""); String roomNameOrId = StringUtils.defaultIfBlank(roomName, invite.getInvite().getRoomId()); - String templateBody = IOUtils.toString( - StringUtils.startsWith(invCfg.getTemplate(), "classpath:") ? - app.getResource(invCfg.getTemplate()).getInputStream() : new FileInputStream(invCfg.getTemplate()), - StandardCharsets.UTF_8); - templateBody = templateBody.replace("%DOMAIN%", mxCfg.getDomain()); - templateBody = templateBody.replace("%DOMAIN_PRETTY%", domainPretty); - templateBody = templateBody.replace("%FROM_EMAIL%", cfg.getFrom()); - templateBody = templateBody.replace("%FROM_NAME%", cfg.getName()); templateBody = templateBody.replace("%SENDER_ID%", invite.getInvite().getSender().getId()); templateBody = templateBody.replace("%SENDER_NAME%", senderName); templateBody = templateBody.replace("%SENDER_NAME_OR_ID%", senderNameOrId); - templateBody = templateBody.replace("%INVITE_MEDIUM%", invite.getInvite().getMedium()); - templateBody = templateBody.replace("%INVITE_ADDRESS%", invite.getInvite().getAddress()); + templateBody = templateBody.replace("%INVITE_MEDIUM%", tpid.getMedium()); + templateBody = templateBody.replace("%INVITE_ADDRESS%", tpid.getAddress()); templateBody = templateBody.replace("%ROOM_ID%", invite.getInvite().getRoomId()); templateBody = templateBody.replace("%ROOM_NAME%", roomName); templateBody = templateBody.replace("%ROOM_NAME_OR_ID%", roomNameOrId); @@ -93,4 +107,14 @@ public String generate(IThreePidInviteReply invite) { } } + @Override + public String getForValidation(IThreePidSession session) { + return null; + } + + @Override + public String getForRemotePublishingValidation(IThreePidSession session) { + return null; + } + } diff --git a/src/main/groovy/io/kamax/mxisd/threepid/notification/email/EmailNotificationHandler.java b/src/main/groovy/io/kamax/mxisd/threepid/notification/email/EmailNotificationHandler.java new file mode 100644 index 00000000..e0d9cfeb --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/threepid/notification/email/EmailNotificationHandler.java @@ -0,0 +1,71 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.threepid.notification.email; + +import io.kamax.matrix.ThreePidMedium; +import io.kamax.mxisd.config.threepid.medium.EmailConfig; +import io.kamax.mxisd.exception.ConfigurationException; +import io.kamax.mxisd.invitation.IThreePidInviteReply; +import io.kamax.mxisd.notification.INotificationHandler; +import io.kamax.mxisd.threepid.connector.email.IEmailConnector; +import io.kamax.mxisd.threepid.session.IThreePidSession; +import org.apache.commons.lang.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +public class EmailNotificationHandler implements INotificationHandler { + + private EmailConfig cfg; + private IEmailNotificationGenerator generator; + private IEmailConnector connector; + + @Autowired + public EmailNotificationHandler(EmailConfig cfg, List generators, List connectors) { + generator = generators.stream() + .filter(o -> StringUtils.equals(cfg.getGenerator(), o.getId())) + .findFirst() + .orElseThrow(() -> new ConfigurationException("Email notification generator [" + cfg.getGenerator() + "] could not be found")); + + connector = connectors.stream() + .filter(o -> StringUtils.equals(cfg.getConnector(), o.getId())) + .findFirst() + .orElseThrow(() -> new ConfigurationException("Email sender connector [" + cfg.getConnector() + "] could not be found")); + } + + @Override + public String getMedium() { + return ThreePidMedium.Email.getId(); + } + + @Override + public void notify(IThreePidInviteReply invite) { + + } + + @Override + public void notify(IThreePidSession session) { + + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/threepid/notification/email/IEmailNotificationGenerator.java b/src/main/groovy/io/kamax/mxisd/threepid/notification/email/IEmailNotificationGenerator.java new file mode 100644 index 00000000..a7401ab4 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/threepid/notification/email/IEmailNotificationGenerator.java @@ -0,0 +1,33 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.threepid.notification.email; + +import io.kamax.matrix.ThreePidMedium; +import io.kamax.mxisd.threepid.notification.INotificationGenerator; + +public interface IEmailNotificationGenerator extends INotificationGenerator { + + @Override + default String getMedium() { + return ThreePidMedium.Email.getId(); + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/threepid/session/IThreePidSession.java b/src/main/groovy/io/kamax/mxisd/threepid/session/IThreePidSession.java index 28af4305..d5c7dfcd 100644 --- a/src/main/groovy/io/kamax/mxisd/threepid/session/IThreePidSession.java +++ b/src/main/groovy/io/kamax/mxisd/threepid/session/IThreePidSession.java @@ -29,8 +29,6 @@ public interface IThreePidSession { String getId(); - String getHash(); - Instant getCreationTime(); String getServer(); @@ -43,6 +41,8 @@ public interface IThreePidSession { Optional getNextLink(); + String getToken(); + void validate(String token); boolean isValidated(); diff --git a/src/main/groovy/io/kamax/mxisd/threepid/session/ThreePidSession.java b/src/main/groovy/io/kamax/mxisd/threepid/session/ThreePidSession.java index 8d0fcbcc..4b37358e 100644 --- a/src/main/groovy/io/kamax/mxisd/threepid/session/ThreePidSession.java +++ b/src/main/groovy/io/kamax/mxisd/threepid/session/ThreePidSession.java @@ -113,6 +113,11 @@ public Optional getNextLink() { return Optional.ofNullable(nextLink); } + @Override + public String getToken() { + return token; + } + public synchronized void setAttempt(int attempt) { if (isValidated()) { throw new IllegalStateException(); diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 8dc80b60..82c1feca 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -63,18 +63,25 @@ forward: - "https://vector.im" threepid: - email: - connector: - active: 'smtp' - provider: + medium: + email: + identity: + from: '' + name: '' + connector: 'smtp' + generator: 'template' + connectors: smtp: + host: '' port: 587 tls: 1 - -invite: - medium: - email: - template: 'classpath:email/invite-template.eml' + login: '' + password: '' + generators: + template: + invite: 'classpath:email/invite-template.eml' + session: + validation: 'classpath:email/validate-template.eml' storage: backend: 'sqlite' diff --git a/src/main/resources/email/validate-template.eml b/src/main/resources/email/validate-template.eml new file mode 100644 index 00000000..07617127 --- /dev/null +++ b/src/main/resources/email/validate-template.eml @@ -0,0 +1,66 @@ +Subject: Your Matrix Validation Token +MIME-Version: 1.0 +Content-Type: multipart/alternative; + boundary="7REaIwWQCioQ6NaBlAQlg8ztbUQj6PKJ" + +--7REaIwWQCioQ6NaBlAQlg8ztbUQj6PKJ +Content-Type: text/plain; charset=UTF-8 +Content-Disposition: inline + +Hello, + +We have received a request to link this email address with a Matrix account. +If this was you who made this request, you may use the following link to complete the verification of your email address: + + %VALIDATION_LINK% + +If your client requires a code, the code is %VALIDATION_TOKEN% + +If you aren't aware of making such a request, please disregard this email. + +Regards, +%DOMAIN_PRETTY% Admins + +--7REaIwWQCioQ6NaBlAQlg8ztbUQj6PKJ +Content-Type: text/html; charset=UTF-8 +Content-Disposition: inline + + + + + + + + + +

Hello,

+ +

We have received a request to link this email address with a Matrix account. + If this was you who made this request, you may use the following link to + complete the verification of your email address:

+ +

Complete email verification

+ +

...or copy this link into your web browser:

+ +

%VALIDATION_LINK%

+ +

If your client requires a code, the code is %VALIDATION_TOKEN%

+ +

If you aren't aware of making such a request, please disregard this +email.

+ +
+

Regards,
+ %DOMAIN_PRETTY% Admins

+ + + + +--7REaIwWQCioQ6NaBlAQlg8ztbUQj6PKJ-- From 88cefeabbf0dfe7ea89574d3c7bb11894e2f6855 Mon Sep 17 00:00:00 2001 From: Maxime Dor Date: Wed, 20 Sep 2017 17:24:21 +0200 Subject: [PATCH 04/14] Fix refactored calls --- .../io/kamax/mxisd/invitation/InvitationManager.java | 2 +- .../io/kamax/mxisd/threepid/session/ThreePidSession.java | 7 ------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/main/groovy/io/kamax/mxisd/invitation/InvitationManager.java b/src/main/groovy/io/kamax/mxisd/invitation/InvitationManager.java index f0acdd5b..06a0d39d 100644 --- a/src/main/groovy/io/kamax/mxisd/invitation/InvitationManager.java +++ b/src/main/groovy/io/kamax/mxisd/invitation/InvitationManager.java @@ -223,7 +223,7 @@ public synchronized IThreePidInviteReply storeInvite(IThreePidInvite invitation) IThreePidInviteReply reply = new ThreePidInviteReply(invId, invitation, token, displayName); log.info("Performing invite to {}:{}", invitation.getMedium(), invitation.getAddress()); - notifMgr.send(reply); + notifMgr.sendForInvite(reply); log.info("Storing invite under ID {}", invId); storage.insertInvite(reply); diff --git a/src/main/groovy/io/kamax/mxisd/threepid/session/ThreePidSession.java b/src/main/groovy/io/kamax/mxisd/threepid/session/ThreePidSession.java index 4b37358e..83802dac 100644 --- a/src/main/groovy/io/kamax/mxisd/threepid/session/ThreePidSession.java +++ b/src/main/groovy/io/kamax/mxisd/threepid/session/ThreePidSession.java @@ -34,7 +34,6 @@ public class ThreePidSession implements IThreePidSession { private String id; private Instant timestamp; - private String hash; private String server; private ThreePid tPid; private String secret; @@ -66,7 +65,6 @@ public ThreePidSession(String id, String server, ThreePid tPid, String secret, i this.token = token; this.timestamp = Instant.now(); - this.hash = server.toLowerCase() + tPid.getMedium().toLowerCase() + tPid.getAddress().toLowerCase() + secret; } @Override @@ -74,11 +72,6 @@ public String getId() { return id; } - @Override - public String getHash() { - return hash; - } - @Override public Instant getCreationTime() { return timestamp; From ace6019197bb6909fee7298d62ecf6a7adbc10ae Mon Sep 17 00:00:00 2001 From: Maxime Dor Date: Thu, 21 Sep 2017 04:07:13 +0200 Subject: [PATCH 05/14] Refactor after first tests against synapse --- .../io/kamax/mxisd/config/SessionConfig.java | 115 ++++++++++++++++ .../v1/DefaultExceptionHandler.java | 30 ++++- .../controller/v1/SessionController.groovy | 20 +-- .../mxisd/exception/InternalServerError.java | 29 ++++- .../mxisd/exception/MatrixException.java | 47 +++++++ .../kamax/mxisd/exception/MxisdException.java | 24 ++++ .../mxisd/exception/NotAllowedException.java | 33 +++++ .../SessionNotValidatedException.java | 31 +++++ .../notification/INotificationHandler.java | 4 +- .../notification/NotificationManager.java | 6 +- .../kamax/mxisd/session/SessionMananger.java | 123 ++++++++---------- .../storage/dao/IThreePidSessionDao.java | 6 + .../ormlite/dao/ThreePidSessionDao.java | 72 +++++++--- .../connector/email/EmailSmtpConnector.java | 6 + .../notification/INotificationGenerator.java | 2 +- .../email/EmailNotificationGenerator.java | 88 ++++++++----- .../email/EmailNotificationHandler.java | 19 ++- .../threepid/session/IThreePidSession.java | 2 + .../threepid/session/ThreePidSession.java | 20 +++ src/main/resources/application.yaml | 11 ++ .../resources/email/validate-template.eml | 2 +- 21 files changed, 545 insertions(+), 145 deletions(-) create mode 100644 src/main/groovy/io/kamax/mxisd/exception/MatrixException.java create mode 100644 src/main/groovy/io/kamax/mxisd/exception/MxisdException.java create mode 100644 src/main/groovy/io/kamax/mxisd/exception/NotAllowedException.java create mode 100644 src/main/groovy/io/kamax/mxisd/exception/SessionNotValidatedException.java diff --git a/src/main/groovy/io/kamax/mxisd/config/SessionConfig.java b/src/main/groovy/io/kamax/mxisd/config/SessionConfig.java index fb515375..fd20b1ee 100644 --- a/src/main/groovy/io/kamax/mxisd/config/SessionConfig.java +++ b/src/main/groovy/io/kamax/mxisd/config/SessionConfig.java @@ -20,16 +20,131 @@ package io.kamax.mxisd.config; +import com.google.gson.Gson; 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; + @Configuration @ConfigurationProperties("session") public class SessionConfig { private static Logger log = LoggerFactory.getLogger(SessionConfig.class); + public static class Policy { + + public static class PolicyTemplate { + + public static class PolicySource { + + private boolean enabled; + private boolean toLocal; + private boolean toRemote; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public boolean toLocal() { + return toLocal; + } + + public void setToLocal(boolean toLocal) { + this.toLocal = toLocal; + } + + public boolean toRemote() { + return toRemote; + } + + public void setToRemote(boolean toRemote) { + this.toRemote = toRemote; + } + + } + + private boolean enabled; + private PolicySource forLocal = new PolicySource(); + private PolicySource forRemote = new PolicySource(); + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public PolicySource getForLocal() { + return forLocal; + } + + public PolicySource forLocal() { + return forLocal; + } + + public PolicySource getForRemote() { + return forRemote; + } + + public PolicySource forRemote() { + return forRemote; + } + } + + private PolicyTemplate bind = new PolicyTemplate(); + private PolicyTemplate validation = new PolicyTemplate(); + + public PolicyTemplate getBind() { + return bind; + } + + public void setBind(PolicyTemplate bind) { + this.bind = bind; + } + + public PolicyTemplate getValidation() { + return validation; + } + + public void setValidation(PolicyTemplate validation) { + this.validation = validation; + } + + } + + private MatrixConfig mxCfg; + private Policy policy = new Policy(); + + @Autowired + public SessionConfig(MatrixConfig mxCfg) { + this.mxCfg = mxCfg; + } + + public MatrixConfig getMatrixCfg() { + return mxCfg; + } + + public Policy getPolicy() { + return policy; + } + + public void setPolicy(Policy policy) { + this.policy = policy; + } + + @PostConstruct + public void build() { + log.info("--- Session config ---"); + log.info("Global Policy: {}", new Gson().toJson(policy)); + } } diff --git a/src/main/groovy/io/kamax/mxisd/controller/v1/DefaultExceptionHandler.java b/src/main/groovy/io/kamax/mxisd/controller/v1/DefaultExceptionHandler.java index 1fb0f86b..8d03874f 100644 --- a/src/main/groovy/io/kamax/mxisd/controller/v1/DefaultExceptionHandler.java +++ b/src/main/groovy/io/kamax/mxisd/controller/v1/DefaultExceptionHandler.java @@ -23,7 +23,9 @@ import com.google.gson.Gson; import com.google.gson.JsonObject; import io.kamax.mxisd.exception.BadRequestException; +import io.kamax.mxisd.exception.InternalServerError; import io.kamax.mxisd.exception.MappingAlreadyExistsException; +import io.kamax.mxisd.exception.MatrixException; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,6 +35,8 @@ import org.springframework.web.bind.annotation.*; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.time.Instant; @ControllerAdvice @ResponseBody @@ -50,6 +54,23 @@ static String handle(String erroCode, String error) { return gson.toJson(obj); } + @ExceptionHandler(InternalServerError.class) + public String handle(InternalServerError e, HttpServletResponse response) { + if (StringUtils.isNotBlank(e.getInternalReason())) { + log.error("Reference #{} - {}", e.getReference(), e.getInternalReason()); + } else { + log.error("Reference #{}", e); + } + + return handleGeneric(e, response); + } + + @ExceptionHandler(MatrixException.class) + public String handleGeneric(MatrixException e, HttpServletResponse response) { + response.setStatus(e.getStatus()); + return handle(e.getErrorCode(), e.getError()); + } + @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(MissingServletRequestParameterException.class) public String handle(MissingServletRequestParameterException e) { @@ -72,7 +93,14 @@ public String handle(BadRequestException e) { @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.")); + return handle( + "M_UNKNOWN", + StringUtils.defaultIfBlank( + e.getMessage(), + "An internal server error occured. If this error persists, please contact support with reference #" + + Instant.now().toEpochMilli() + ) + ); } } diff --git a/src/main/groovy/io/kamax/mxisd/controller/v1/SessionController.groovy b/src/main/groovy/io/kamax/mxisd/controller/v1/SessionController.groovy index 1e107d7b..f785e5ad 100644 --- a/src/main/groovy/io/kamax/mxisd/controller/v1/SessionController.groovy +++ b/src/main/groovy/io/kamax/mxisd/controller/v1/SessionController.groovy @@ -27,6 +27,7 @@ import io.kamax.mxisd.ThreePid 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.exception.SessionNotValidatedException import io.kamax.mxisd.invitation.InvitationManager import io.kamax.mxisd.lookup.ThreePidValidation import io.kamax.mxisd.session.SessionMananger @@ -105,12 +106,10 @@ class SessionController { @RequestMapping(value = "/3pid/getValidated3pid") String check(HttpServletRequest request, HttpServletResponse response, @RequestParam String sid, @RequestParam("client_secret") String secret) { - log.info("Requested: {}?{}", request.getRequestURL(), request.getQueryString()) + log.info("Requested: {}", request.getRequestURL(), request.getQueryString()) - Optional result = mgr.getValidated(sid, secret) - if (result.isPresent()) { - log.info("requested session was validated") - ThreePidValidation pid = result.get() + try { + ThreePidValidation pid = mgr.getValidated(sid, secret) JsonObject obj = new JsonObject() obj.addProperty("medium", pid.getMedium()) @@ -118,14 +117,9 @@ class SessionController { obj.addProperty("validated_at", pid.getValidation().toEpochMilli()) return gson.toJson(obj); - } else { - log.info("requested session was not validated") - - JsonObject obj = new JsonObject() - obj.addProperty("errcode", "M_SESSION_NOT_VALIDATED") - obj.addProperty("error", "sid, secret or session not valid") - response.setStatus(HttpStatus.SC_BAD_REQUEST) - return gson.toJson(obj) + } catch (SessionNotValidatedException e) { + log.info("Session {} was requested but has not yet been validated", sid); + throw e; } } diff --git a/src/main/groovy/io/kamax/mxisd/exception/InternalServerError.java b/src/main/groovy/io/kamax/mxisd/exception/InternalServerError.java index d3ca848b..56800750 100644 --- a/src/main/groovy/io/kamax/mxisd/exception/InternalServerError.java +++ b/src/main/groovy/io/kamax/mxisd/exception/InternalServerError.java @@ -20,16 +20,35 @@ package io.kamax.mxisd.exception; -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; +import org.apache.http.HttpStatus; import java.time.Instant; -@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR) -public class InternalServerError extends RuntimeException { +public class InternalServerError extends MatrixException { + + private String reference = Long.toString(Instant.now().toEpochMilli()); + private String internalReason; public InternalServerError() { - super("An internal server error occured. If this error persists, please contact support with reference #" + Instant.now().toEpochMilli()); + super( + HttpStatus.SC_INTERNAL_SERVER_ERROR, + "M_UNKNOWN", + "An internal server error occured. If this error persists, please contact support with reference #" + + Instant.now().toEpochMilli() + ); + } + + public InternalServerError(String internalReason) { + this(); + this.internalReason = internalReason; + } + + public String getReference() { + return reference; + } + + public String getInternalReason() { + return internalReason; } } diff --git a/src/main/groovy/io/kamax/mxisd/exception/MatrixException.java b/src/main/groovy/io/kamax/mxisd/exception/MatrixException.java new file mode 100644 index 00000000..7565b150 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/exception/MatrixException.java @@ -0,0 +1,47 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.exception; + +public abstract class MatrixException extends MxisdException { + + private int status; + private String errorCode; + private String error; + + public MatrixException(int status, String errorCode, String error) { + this.status = status; + this.errorCode = errorCode; + this.error = error; + } + + public int getStatus() { + return status; + } + + public String getErrorCode() { + return errorCode; + } + + public String getError() { + return error; + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/exception/MxisdException.java b/src/main/groovy/io/kamax/mxisd/exception/MxisdException.java new file mode 100644 index 00000000..e6088f39 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/exception/MxisdException.java @@ -0,0 +1,24 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.exception; + +public class MxisdException extends RuntimeException { +} diff --git a/src/main/groovy/io/kamax/mxisd/exception/NotAllowedException.java b/src/main/groovy/io/kamax/mxisd/exception/NotAllowedException.java new file mode 100644 index 00000000..b3a1f775 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/exception/NotAllowedException.java @@ -0,0 +1,33 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(value = HttpStatus.FORBIDDEN) +public class NotAllowedException extends RuntimeException { + + public NotAllowedException(String s) { + super(s); + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/exception/SessionNotValidatedException.java b/src/main/groovy/io/kamax/mxisd/exception/SessionNotValidatedException.java new file mode 100644 index 00000000..6524ba69 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/exception/SessionNotValidatedException.java @@ -0,0 +1,31 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.exception; + +import org.apache.http.HttpStatus; + +public class SessionNotValidatedException extends MatrixException { + + public SessionNotValidatedException() { + super(HttpStatus.SC_OK, "M_SESSION_NOT_VALIDATED", "This validation session has not yet been completed"); + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/notification/INotificationHandler.java b/src/main/groovy/io/kamax/mxisd/notification/INotificationHandler.java index 0b622dff..61387849 100644 --- a/src/main/groovy/io/kamax/mxisd/notification/INotificationHandler.java +++ b/src/main/groovy/io/kamax/mxisd/notification/INotificationHandler.java @@ -27,8 +27,8 @@ public interface INotificationHandler { String getMedium(); - void notify(IThreePidInviteReply invite); + void sendForInvite(IThreePidInviteReply invite); - void notify(IThreePidSession session); + void sendForValidation(IThreePidSession session); } diff --git a/src/main/groovy/io/kamax/mxisd/notification/NotificationManager.java b/src/main/groovy/io/kamax/mxisd/notification/NotificationManager.java index 63d72e43..21785302 100644 --- a/src/main/groovy/io/kamax/mxisd/notification/NotificationManager.java +++ b/src/main/groovy/io/kamax/mxisd/notification/NotificationManager.java @@ -55,14 +55,14 @@ public boolean isMediumSupported(String medium) { } public void sendForInvite(IThreePidInviteReply invite) { - ensureMedium(invite.getInvite().getMedium()).notify(invite); + ensureMedium(invite.getInvite().getMedium()).sendForInvite(invite); } public void sendForValidation(IThreePidSession session) { - ensureMedium(session.getThreePid().getMedium()).notify(session); + ensureMedium(session.getThreePid().getMedium()).sendForValidation(session); } - public void sendforRemotePublish(IThreePidSession session) { + public void sendforRemoteValidation(IThreePidSession session) { throw new NotImplementedException("Remote publish of 3PID bind"); } diff --git a/src/main/groovy/io/kamax/mxisd/session/SessionMananger.java b/src/main/groovy/io/kamax/mxisd/session/SessionMananger.java index d6072d7b..02730628 100644 --- a/src/main/groovy/io/kamax/mxisd/session/SessionMananger.java +++ b/src/main/groovy/io/kamax/mxisd/session/SessionMananger.java @@ -22,9 +22,11 @@ import io.kamax.matrix.ThreePidMedium; import io.kamax.mxisd.ThreePid; +import io.kamax.mxisd.config.MatrixConfig; import io.kamax.mxisd.config.SessionConfig; import io.kamax.mxisd.exception.InvalidCredentialsException; -import io.kamax.mxisd.lookup.SingleLookupReply; +import io.kamax.mxisd.exception.NotAllowedException; +import io.kamax.mxisd.exception.SessionNotValidatedException; import io.kamax.mxisd.lookup.ThreePidValidation; import io.kamax.mxisd.lookup.strategy.LookupStrategy; import io.kamax.mxisd.notification.NotificationManager; @@ -39,7 +41,6 @@ import org.springframework.stereotype.Component; import java.util.Optional; -import java.util.UUID; @Component public class SessionMananger { @@ -52,13 +53,26 @@ public class SessionMananger { private NotificationManager notifMgr; @Autowired - public SessionMananger(SessionConfig cfg, IStorage storage, LookupStrategy lookup, NotificationManager notifMgr) { + public SessionMananger(SessionConfig cfg, MatrixConfig mxCfg, IStorage storage, LookupStrategy lookup, NotificationManager notifMgr) { this.cfg = cfg; this.storage = storage; this.lookup = lookup; this.notifMgr = notifMgr; } + private boolean isLocal(ThreePid tpid) { + if (!ThreePidMedium.Email.is(tpid.getMedium())) { // We can only handle E-mails for now + return false; + } + + String domain = tpid.getAddress().split("@")[1]; + return StringUtils.equalsIgnoreCase(cfg.getMatrixCfg().getDomain(), domain); + } + + private boolean isKnownLocal(ThreePid tpid) { + return lookup.findLocal(tpid.getMedium(), tpid.getAddress()).isPresent(); + } + private ThreePidSession getSession(String sid, String secret) { Optional dao = storage.getThreePidSession(sid); if (!dao.isPresent() || !StringUtils.equals(dao.get().getSecret(), secret)) { @@ -71,12 +85,17 @@ private ThreePidSession getSession(String sid, String secret) { private ThreePidSession getSessionIfValidated(String sid, String secret) { ThreePidSession session = getSession(sid, secret); if (!session.isValidated()) { - throw new IllegalStateException("Session " + sid + " has not been validated"); + throw new SessionNotValidatedException(); } return session; } public String create(String server, ThreePid tpid, String secret, int attempt, String nextLink) { + SessionConfig.Policy.PolicyTemplate policy = cfg.getPolicy().getValidation(); + if (!policy.isEnabled()) { + throw new NotAllowedException("Validating 3PID is disabled globally"); + } + synchronized (this) { log.info("Server {} is asking to create session for {} (Attempt #{}) - Next link: {}", server, tpid, attempt, nextLink); Optional dao = storage.findThreePidSession(tpid, secret); @@ -94,17 +113,46 @@ public String create(String server, ThreePid tpid, String secret, int attempt, S return session.getId(); } else { log.info("No existing session for {}", tpid); + + boolean isLocalDomain = isLocal(tpid); + log.info("Is 3PID bound to local domain? {}", isLocalDomain); + + if (isLocalDomain && (!policy.forLocal().isEnabled() || !policy.forLocal().toLocal())) { + throw new NotAllowedException("Validating local 3PID is not allowed"); + } + + // We lookup if the 3PID is already known locally. + boolean knownLocal = isKnownLocal(tpid); + log.info("Mapping with {} is " + (knownLocal ? "already" : "not") + " known locally", tpid); + + if (!isLocalDomain && ( + !policy.forRemote().isEnabled() || ( + !policy.forRemote().toLocal() && + !policy.forRemote().toRemote() + ) + )) { + throw new NotAllowedException("Validating unknown remote 3PID is not allowed"); + } + String sessionId; do { - sessionId = UUID.randomUUID().toString().replace("-", ""); + sessionId = Long.toString(System.currentTimeMillis()); } while (storage.getThreePidSession(sessionId).isPresent()); String token = RandomStringUtils.randomNumeric(6); ThreePidSession session = new ThreePidSession(sessionId, server, tpid, secret, attempt, nextLink, token); log.info("Generated new session {} to validate {} from server {}", sessionId, tpid, server); - notifMgr.sendForValidation(session); - log.info("Sent validation notification to {}", tpid); + // This might need a configuration by medium type? + if (!isLocalDomain) { + if (policy.forRemote().toLocal() && policy.forRemote().toRemote()) { + log.info("Session {} for {}: sending local validation notification", sessionId, tpid); + notifMgr.sendForValidation(session); + } else { + log.info("Session {} for {}: sending remote-only validation notification", sessionId, tpid); + notifMgr.sendforRemoteValidation(session); + } + } storage.insertThreePidSession(session.getDao()); log.info("Stored session {}", sessionId, tpid, server); @@ -130,65 +178,8 @@ public ThreePidValidation getValidated(String sid, String secret) { public void bind(String sid, String secret, String mxid) { ThreePidSession session = getSessionIfValidated(sid, secret); - log.info("Attempting bind of {} on session {} from server {}", mxid, session.getId(), session.getServer()); - - // We lookup if the 3PID is already known remotely. - Optional rRemote = lookup.findRemote(session.getThreePid().getMedium(), session.getThreePid().getAddress()); - boolean knownRemote = rRemote.isPresent() && StringUtils.equals(rRemote.get().getMxid().getId(), mxid); - log.info("Mapping {} -> {} is " + (knownRemote ? "already" : "not") + " known remotely", mxid, session.getThreePid()); - - boolean isLocalDomain = false; - if (ThreePidMedium.Email.is(session.getThreePid().getMedium())) { - // TODO - // 1. Extract domain from email - // 2. set isLocalDomain - isLocalDomain = session.getThreePid().getAddress().isEmpty(); // FIXME only for testing - } - if (knownRemote) { - log.info("No further action needed for Mapping {} -> {}"); - return; - } - - // We lookup if the 3PID is already known locally. - Optional rLocal = lookup.findLocal(session.getThreePid().getMedium(), session.getThreePid().getAddress()); - boolean knownLocal = rLocal.isPresent() && StringUtils.equals(rLocal.get().getMxid().getId(), mxid); - log.info("Mapping {} -> {} is " + (knownLocal ? "already" : "not") + " known locally", mxid, session.getThreePid()); - - // This might need a configuration by medium type? - if (knownLocal) { // 3PID is ony known local - if (isLocalDomain) { - // TODO - // 1. Check if global publishing is enabled, allowed and offered. If one is no, return. - // 2. Publish globally - notifMgr.sendforRemotePublish(session); - } - - if (System.currentTimeMillis() % 2 == 0) { // FIXME only for testing - // TODO - // 1. Check if configured to publish globally non-local domain. If no, return - notifMgr.sendforRemotePublish(session); - } - - // TODO - // Proxy to configurable IS, by default Matrix.org - // - // Separate workflow, if user accepts to publish globally - // 1. display page to the user that it is waiting for the confirmation - // 2. call mxisd-specific endpoint to publish globally - // 3. check regularly on client page for a binding - // 4. when found, show page "Done globally!" - notifMgr.sendforRemotePublish(session); - } else { - if (isLocalDomain) { // 3PID is not known anywhere but is a local domain - // TODO - // check if config says this should fail or silently accept. - // Required to silently accept if the backend is synapse itself. - } else { // 3PID is not known anywhere and is remote - // TODO - // Proxy to configurable IS, by default Matrix.org - notifMgr.sendforRemotePublish(session); - } - } + log.info("Accepting bind of {} on session {} from server {}", mxid, session.getId(), session.getServer()); + // TODO perform this if request was proxied } } diff --git a/src/main/groovy/io/kamax/mxisd/storage/dao/IThreePidSessionDao.java b/src/main/groovy/io/kamax/mxisd/storage/dao/IThreePidSessionDao.java index dd5085e5..34da70dd 100644 --- a/src/main/groovy/io/kamax/mxisd/storage/dao/IThreePidSessionDao.java +++ b/src/main/groovy/io/kamax/mxisd/storage/dao/IThreePidSessionDao.java @@ -24,6 +24,8 @@ public interface IThreePidSessionDao { String getId(); + long getCreationTime(); + String getServer(); String getMedium(); @@ -38,4 +40,8 @@ public interface IThreePidSessionDao { String getToken(); + boolean getValidated(); + + long getValidationTime(); + } diff --git a/src/main/groovy/io/kamax/mxisd/storage/ormlite/dao/ThreePidSessionDao.java b/src/main/groovy/io/kamax/mxisd/storage/ormlite/dao/ThreePidSessionDao.java index 0b557e12..c2b18c08 100644 --- a/src/main/groovy/io/kamax/mxisd/storage/ormlite/dao/ThreePidSessionDao.java +++ b/src/main/groovy/io/kamax/mxisd/storage/ormlite/dao/ThreePidSessionDao.java @@ -31,6 +31,9 @@ public class ThreePidSessionDao implements IThreePidSessionDao { @DatabaseField(id = true) private String id; + @DatabaseField(canBeNull = false) + private long creationTime; + @DatabaseField(canBeNull = false) private String server; @@ -52,12 +55,19 @@ public class ThreePidSessionDao implements IThreePidSessionDao { @DatabaseField(canBeNull = false) private String token; + @DatabaseField + private boolean validated; + + @DatabaseField + private long validationTime; + public ThreePidSessionDao() { // stub for ORMLite } public ThreePidSessionDao(IThreePidSessionDao session) { setId(session.getId()); + setCreationTime(session.getCreationTime()); setServer(session.getServer()); setMedium(session.getMedium()); setAddress(session.getAddress()); @@ -65,6 +75,9 @@ public ThreePidSessionDao(IThreePidSessionDao session) { setAttempt(session.getAttempt()); setNextLink(session.getNextLink()); setToken(session.getToken()); + setValidated(session.getValidated()); + setValidationTime(session.getValidationTime()); + } public ThreePidSessionDao(ThreePid tpid, String secret) { @@ -82,6 +95,15 @@ public void setId(String id) { this.id = id; } + @Override + public long getCreationTime() { + return creationTime; + } + + public void setCreationTime(long creationTime) { + this.creationTime = creationTime; + } + @Override public String getServer() { return server; @@ -92,39 +114,39 @@ public void setServer(String server) { } @Override - public String getSecret() { - return secret; + public String getMedium() { + return medium; } - public void setSecret(String secret) { - this.secret = secret; + public void setMedium(String medium) { + this.medium = medium; } @Override - public int getAttempt() { - return attempt; + public String getAddress() { + return address; } - public void setAttempt(int attempt) { - this.attempt = attempt; + public void setAddress(String address) { + this.address = address; } @Override - public String getMedium() { - return medium; + public String getSecret() { + return secret; } - public void setMedium(String medium) { - this.medium = medium; + public void setSecret(String secret) { + this.secret = secret; } @Override - public String getAddress() { - return address; + public int getAttempt() { + return attempt; } - public void setAddress(String address) { - this.address = address; + public void setAttempt(int attempt) { + this.attempt = attempt; } @Override @@ -144,4 +166,22 @@ public String getToken() { public void setToken(String token) { this.token = token; } + + public boolean getValidated() { + return validated; + } + + public void setValidated(boolean validated) { + this.validated = validated; + } + + @Override + public long getValidationTime() { + return validationTime; + } + + public void setValidationTime(long validationTime) { + this.validationTime = validationTime; + } + } diff --git a/src/main/groovy/io/kamax/mxisd/threepid/connector/email/EmailSmtpConnector.java b/src/main/groovy/io/kamax/mxisd/threepid/connector/email/EmailSmtpConnector.java index 8124bf71..18e0faeb 100644 --- a/src/main/groovy/io/kamax/mxisd/threepid/connector/email/EmailSmtpConnector.java +++ b/src/main/groovy/io/kamax/mxisd/threepid/connector/email/EmailSmtpConnector.java @@ -23,7 +23,9 @@ import com.sun.mail.smtp.SMTPTransport; import io.kamax.matrix.ThreePidMedium; import io.kamax.mxisd.config.threepid.connector.EmailSmtpConfig; +import io.kamax.mxisd.exception.InternalServerError; import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -64,6 +66,10 @@ public String getMedium() { @Override public void send(String senderAddress, String senderName, String recipient, String content) { + if (StringUtils.isBlank(content)) { + throw new InternalServerError("Notification content is empty"); + } + try { InternetAddress sender = new InternetAddress(senderAddress, senderName); MimeMessage msg = new MimeMessage(session, IOUtils.toInputStream(content, StandardCharsets.UTF_8)); diff --git a/src/main/groovy/io/kamax/mxisd/threepid/notification/INotificationGenerator.java b/src/main/groovy/io/kamax/mxisd/threepid/notification/INotificationGenerator.java index 969a87b6..c61dba81 100644 --- a/src/main/groovy/io/kamax/mxisd/threepid/notification/INotificationGenerator.java +++ b/src/main/groovy/io/kamax/mxisd/threepid/notification/INotificationGenerator.java @@ -29,7 +29,7 @@ public interface INotificationGenerator { String getMedium(); - String get(IThreePidInviteReply invite); + String getForInvite(IThreePidInviteReply invite); String getForValidation(IThreePidSession session); diff --git a/src/main/groovy/io/kamax/mxisd/threepid/notification/email/EmailNotificationGenerator.java b/src/main/groovy/io/kamax/mxisd/threepid/notification/email/EmailNotificationGenerator.java index b327bea5..6ed62c34 100644 --- a/src/main/groovy/io/kamax/mxisd/threepid/notification/email/EmailNotificationGenerator.java +++ b/src/main/groovy/io/kamax/mxisd/threepid/notification/email/EmailNotificationGenerator.java @@ -22,13 +22,19 @@ import io.kamax.mxisd.ThreePid; import io.kamax.mxisd.config.MatrixConfig; +import io.kamax.mxisd.config.ServerConfig; import io.kamax.mxisd.config.threepid.medium.EmailConfig; import io.kamax.mxisd.config.threepid.medium.EmailTemplateConfig; +import io.kamax.mxisd.controller.v1.IdentityAPIv1; +import io.kamax.mxisd.exception.InternalServerError; +import io.kamax.mxisd.exception.NotImplementedException; import io.kamax.mxisd.invitation.IThreePidInviteReply; import io.kamax.mxisd.threepid.session.IThreePidSession; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.WordUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Component; @@ -41,17 +47,22 @@ @Component public class EmailNotificationGenerator implements IEmailNotificationGenerator { + private Logger log = LoggerFactory.getLogger(EmailNotificationGenerator.class); + private EmailConfig cfg; private EmailTemplateConfig templateCfg; private MatrixConfig mxCfg; + private ServerConfig srvCfg; + + @Autowired private ApplicationContext app; - @Autowired // FIXME ApplicationContext shouldn't be injected, find another way from config (?) - public EmailNotificationGenerator(EmailConfig cfg, EmailTemplateConfig templateCfg, MatrixConfig mxCfg, ApplicationContext app) { + @Autowired + public EmailNotificationGenerator(EmailTemplateConfig templateCfg, EmailConfig cfg, MatrixConfig mxCfg, ServerConfig srvCfg) { this.cfg = cfg; this.templateCfg = templateCfg; this.mxCfg = mxCfg; - this.app = app; + this.srvCfg = srvCfg; } @Override @@ -59,10 +70,14 @@ public String getId() { return "template"; } - private String getTemplateContent(String location) throws IOException { - InputStream is = StringUtils.startsWith(location, "classpath:") ? - app.getResource(location).getInputStream() : new FileInputStream(location); - return IOUtils.toString(is, StandardCharsets.UTF_8); + private String getTemplateContent(String location) { + try { + InputStream is = StringUtils.startsWith(location, "classpath:") ? + app.getResource(location).getInputStream() : new FileInputStream(location); + return IOUtils.toString(is, StandardCharsets.UTF_8); + } catch (IOException e) { + throw new InternalServerError("Unable to read template content at " + location + ": " + e.getMessage()); + } } private String populateCommon(String content, ThreePid recipient) { @@ -77,44 +92,51 @@ private String populateCommon(String content, ThreePid recipient) { return content; } - private String getTemplateAndPopulate(String location, ThreePid recipient) throws IOException { + private String getTemplateAndPopulate(String location, ThreePid recipient) { return populateCommon(getTemplateContent(location), recipient); } @Override - public String get(IThreePidInviteReply invite) { - try { - ThreePid tpid = new ThreePid(invite.getInvite().getMedium(), invite.getInvite().getAddress()); - String templateBody = getTemplateAndPopulate(templateCfg.getInvite(), tpid); - - String senderName = invite.getInvite().getProperties().getOrDefault("sender_display_name", ""); - String senderNameOrId = StringUtils.defaultIfBlank(senderName, invite.getInvite().getSender().getId()); - String roomName = invite.getInvite().getProperties().getOrDefault("room_name", ""); - String roomNameOrId = StringUtils.defaultIfBlank(roomName, invite.getInvite().getRoomId()); - - templateBody = templateBody.replace("%SENDER_ID%", invite.getInvite().getSender().getId()); - templateBody = templateBody.replace("%SENDER_NAME%", senderName); - templateBody = templateBody.replace("%SENDER_NAME_OR_ID%", senderNameOrId); - templateBody = templateBody.replace("%INVITE_MEDIUM%", tpid.getMedium()); - templateBody = templateBody.replace("%INVITE_ADDRESS%", tpid.getAddress()); - templateBody = templateBody.replace("%ROOM_ID%", invite.getInvite().getRoomId()); - templateBody = templateBody.replace("%ROOM_NAME%", roomName); - templateBody = templateBody.replace("%ROOM_NAME_OR_ID%", roomNameOrId); - - return templateBody; - } catch (IOException e) { - throw new RuntimeException("Unable to read template file", e); - } + public String getForInvite(IThreePidInviteReply invite) { + ThreePid tpid = new ThreePid(invite.getInvite().getMedium(), invite.getInvite().getAddress()); + String templateBody = getTemplateAndPopulate(templateCfg.getInvite(), tpid); + + String senderName = invite.getInvite().getProperties().getOrDefault("sender_display_name", ""); + String senderNameOrId = StringUtils.defaultIfBlank(senderName, invite.getInvite().getSender().getId()); + String roomName = invite.getInvite().getProperties().getOrDefault("room_name", ""); + String roomNameOrId = StringUtils.defaultIfBlank(roomName, invite.getInvite().getRoomId()); + + templateBody = templateBody.replace("%SENDER_ID%", invite.getInvite().getSender().getId()); + templateBody = templateBody.replace("%SENDER_NAME%", senderName); + templateBody = templateBody.replace("%SENDER_NAME_OR_ID%", senderNameOrId); + templateBody = templateBody.replace("%INVITE_MEDIUM%", tpid.getMedium()); + templateBody = templateBody.replace("%INVITE_ADDRESS%", tpid.getAddress()); + templateBody = templateBody.replace("%ROOM_ID%", invite.getInvite().getRoomId()); + templateBody = templateBody.replace("%ROOM_NAME%", roomName); + templateBody = templateBody.replace("%ROOM_NAME_OR_ID%", roomNameOrId); + + return templateBody; } @Override public String getForValidation(IThreePidSession session) { - return null; + log.info("Generating notification content for 3PID Session validation"); + String templateBody = getTemplateAndPopulate(templateCfg.getSession().getValidation(), session.getThreePid()); + + String validationLink = srvCfg.getPublicUrl() + IdentityAPIv1.BASE + + "/validate/" + session.getThreePid().getMedium() + + "/submitToken?sid=" + session.getId() + "&client_secret=" + session.getSecret() + + "&token=" + session.getToken(); + + templateBody = templateBody.replace("%VALIDATION_LINK%", validationLink); + templateBody = templateBody.replace("%VALIDATION_TOKEN%", session.getToken()); + + return templateBody; } @Override public String getForRemotePublishingValidation(IThreePidSession session) { - return null; + throw new NotImplementedException(""); } } diff --git a/src/main/groovy/io/kamax/mxisd/threepid/notification/email/EmailNotificationHandler.java b/src/main/groovy/io/kamax/mxisd/threepid/notification/email/EmailNotificationHandler.java index e0d9cfeb..024c6ec2 100644 --- a/src/main/groovy/io/kamax/mxisd/threepid/notification/email/EmailNotificationHandler.java +++ b/src/main/groovy/io/kamax/mxisd/threepid/notification/email/EmailNotificationHandler.java @@ -42,6 +42,7 @@ public class EmailNotificationHandler implements INotificationHandler { @Autowired public EmailNotificationHandler(EmailConfig cfg, List generators, List connectors) { + this.cfg = cfg; generator = generators.stream() .filter(o -> StringUtils.equals(cfg.getGenerator(), o.getId())) .findFirst() @@ -59,13 +60,23 @@ public String getMedium() { } @Override - public void notify(IThreePidInviteReply invite) { - + public void sendForInvite(IThreePidInviteReply invite) { + connector.send( + cfg.getIdentity().getFrom(), + cfg.getIdentity().getName(), + invite.getInvite().getAddress(), + generator.getForInvite(invite) + ); } @Override - public void notify(IThreePidSession session) { - + public void sendForValidation(IThreePidSession session) { + connector.send( + cfg.getIdentity().getFrom(), + cfg.getIdentity().getName(), + session.getThreePid().getAddress(), + generator.getForValidation(session) + ); } } diff --git a/src/main/groovy/io/kamax/mxisd/threepid/session/IThreePidSession.java b/src/main/groovy/io/kamax/mxisd/threepid/session/IThreePidSession.java index d5c7dfcd..446d1486 100644 --- a/src/main/groovy/io/kamax/mxisd/threepid/session/IThreePidSession.java +++ b/src/main/groovy/io/kamax/mxisd/threepid/session/IThreePidSession.java @@ -35,6 +35,8 @@ public interface IThreePidSession { ThreePid getThreePid(); + String getSecret(); + int getAttempt(); void increaseAttempt(); diff --git a/src/main/groovy/io/kamax/mxisd/threepid/session/ThreePidSession.java b/src/main/groovy/io/kamax/mxisd/threepid/session/ThreePidSession.java index 83802dac..dee93546 100644 --- a/src/main/groovy/io/kamax/mxisd/threepid/session/ThreePidSession.java +++ b/src/main/groovy/io/kamax/mxisd/threepid/session/ThreePidSession.java @@ -53,6 +53,11 @@ public ThreePidSession(IThreePidSessionDao dao) { dao.getNextLink(), dao.getToken() ); + timestamp = Instant.ofEpochMilli(dao.getCreationTime()); + isValidated = dao.getValidated(); + if (isValidated) { + validationTimestamp = Instant.ofEpochMilli(dao.getValidationTime()); + } } public ThreePidSession(String id, String server, ThreePid tPid, String secret, int attempt, String nextLink, String token) { @@ -154,6 +159,11 @@ public String getId() { return id; } + @Override + public long getCreationTime() { + return timestamp.toEpochMilli(); + } + @Override public String getServer() { return server; @@ -189,6 +199,16 @@ public String getToken() { return token; } + @Override + public boolean getValidated() { + return isValidated; + } + + @Override + public long getValidationTime() { + return isValidated ? validationTimestamp.toEpochMilli() : 0; + } + }; } diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 82c1feca..31e8c594 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -83,6 +83,17 @@ threepid: session: validation: 'classpath:email/validate-template.eml' +session.policy.validation: + enabled: true + forLocal: + enabled: true + toLocal: true + toRemote: true + forRemote: + enabled: true + toLocal: true # This should not be changed unless you know exactly the implications! + toRemote: true + storage: backend: 'sqlite' diff --git a/src/main/resources/email/validate-template.eml b/src/main/resources/email/validate-template.eml index 07617127..e5b13462 100644 --- a/src/main/resources/email/validate-template.eml +++ b/src/main/resources/email/validate-template.eml @@ -45,7 +45,7 @@ body { If this was you who made this request, you may use the following link to complete the verification of your email address:

-

Complete email verification

+

Complete email verification

...or copy this link into your web browser:

From a4b4a3f24c2603e43da2f09bf1ad0d99a47f8178 Mon Sep 17 00:00:00 2001 From: Maxime Dor Date: Thu, 21 Sep 2017 07:26:33 +0200 Subject: [PATCH 06/14] Polishing, prepare for proxying 3PID sessions --- .../io/kamax/mxisd/config/SessionConfig.java | 13 +++ .../threepid/medium/EmailTemplateConfig.java | 33 ++++++- .../v1/remote/RemoteIdentityAPIv1.java | 27 ++++++ .../mxisd/exception/NotAllowedException.java | 9 +- .../notification/INotificationHandler.java | 2 + .../notification/NotificationManager.java | 2 +- .../kamax/mxisd/session/SessionMananger.java | 38 +++----- .../notification/INotificationGenerator.java | 2 +- .../email/EmailNotificationGenerator.java | 20 +++- .../email/EmailNotificationHandler.java | 25 +++-- src/main/resources/application.yaml | 8 +- .../email/validate-local-template.eml | 91 +++++++++++++++++++ .../resources/email/validate-template.eml | 66 -------------- 13 files changed, 217 insertions(+), 119 deletions(-) create mode 100644 src/main/groovy/io/kamax/mxisd/controller/v1/remote/RemoteIdentityAPIv1.java create mode 100644 src/main/resources/email/validate-local-template.eml delete mode 100644 src/main/resources/email/validate-template.eml diff --git a/src/main/groovy/io/kamax/mxisd/config/SessionConfig.java b/src/main/groovy/io/kamax/mxisd/config/SessionConfig.java index fd20b1ee..e72a5fcd 100644 --- a/src/main/groovy/io/kamax/mxisd/config/SessionConfig.java +++ b/src/main/groovy/io/kamax/mxisd/config/SessionConfig.java @@ -42,6 +42,7 @@ public static class PolicyTemplate { public static class PolicySource { private boolean enabled; + private boolean alwaysValidate; private boolean toLocal; private boolean toRemote; @@ -53,6 +54,14 @@ public void setEnabled(boolean enabled) { this.enabled = enabled; } + public boolean isAlwaysValidate() { + return alwaysValidate; + } + + public void setAlwaysValidate(boolean alwaysValidate) { + this.alwaysValidate = alwaysValidate; + } + public boolean toLocal() { return toLocal; } @@ -98,6 +107,10 @@ public PolicySource getForRemote() { public PolicySource forRemote() { return forRemote; } + + public PolicySource forIf(boolean isLocal) { + return isLocal ? forLocal : forRemote; + } } private PolicyTemplate bind = new PolicyTemplate(); diff --git a/src/main/groovy/io/kamax/mxisd/config/threepid/medium/EmailTemplateConfig.java b/src/main/groovy/io/kamax/mxisd/config/threepid/medium/EmailTemplateConfig.java index 4054dd7c..a426dc44 100644 --- a/src/main/groovy/io/kamax/mxisd/config/threepid/medium/EmailTemplateConfig.java +++ b/src/main/groovy/io/kamax/mxisd/config/threepid/medium/EmailTemplateConfig.java @@ -45,13 +45,36 @@ private static String getName(String path) { public static class Session { - private String validation; + public static class SessionValidation { - public String getValidation() { + private String local; + private String remote; + + public String getLocal() { + return local; + } + + public void setLocal(String local) { + this.local = local; + } + + public String getRemote() { + return remote; + } + + public void setRemote(String remote) { + this.remote = remote; + } + + } + + private SessionValidation validation; + + public SessionValidation getValidation() { return validation; } - public void setValidation(String validation) { + public void setValidation(SessionValidation validation) { this.validation = validation; } @@ -76,7 +99,9 @@ public Session getSession() { public void build() { log.info("--- E-mail Generator templates config ---"); log.info("Invite: {}", getName(getInvite())); - log.info("Session validation: {}", getName(getSession().getValidation())); + log.info("Session validation:"); + log.info("\tLocal: {}", getName(getSession().getValidation().getLocal())); + log.info("\tRemote: {}", getName(getSession().getValidation().getRemote())); } } diff --git a/src/main/groovy/io/kamax/mxisd/controller/v1/remote/RemoteIdentityAPIv1.java b/src/main/groovy/io/kamax/mxisd/controller/v1/remote/RemoteIdentityAPIv1.java new file mode 100644 index 00000000..d4517368 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/controller/v1/remote/RemoteIdentityAPIv1.java @@ -0,0 +1,27 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.controller.v1.remote; + +public class RemoteIdentityAPIv1 { + + public static final String BASE = "/_matrix/identity-remote/api/v1"; + +} diff --git a/src/main/groovy/io/kamax/mxisd/exception/NotAllowedException.java b/src/main/groovy/io/kamax/mxisd/exception/NotAllowedException.java index b3a1f775..068127da 100644 --- a/src/main/groovy/io/kamax/mxisd/exception/NotAllowedException.java +++ b/src/main/groovy/io/kamax/mxisd/exception/NotAllowedException.java @@ -20,14 +20,13 @@ package io.kamax.mxisd.exception; -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; -@ResponseStatus(value = HttpStatus.FORBIDDEN) -public class NotAllowedException extends RuntimeException { +import org.apache.http.HttpStatus; + +public class NotAllowedException extends MatrixException { public NotAllowedException(String s) { - super(s); + super(HttpStatus.SC_FORBIDDEN, "M_FORBIDDEN", s); } } diff --git a/src/main/groovy/io/kamax/mxisd/notification/INotificationHandler.java b/src/main/groovy/io/kamax/mxisd/notification/INotificationHandler.java index 61387849..e42735ff 100644 --- a/src/main/groovy/io/kamax/mxisd/notification/INotificationHandler.java +++ b/src/main/groovy/io/kamax/mxisd/notification/INotificationHandler.java @@ -31,4 +31,6 @@ public interface INotificationHandler { void sendForValidation(IThreePidSession session); + void sendForRemoteValidation(IThreePidSession session); + } diff --git a/src/main/groovy/io/kamax/mxisd/notification/NotificationManager.java b/src/main/groovy/io/kamax/mxisd/notification/NotificationManager.java index 21785302..78a38a2b 100644 --- a/src/main/groovy/io/kamax/mxisd/notification/NotificationManager.java +++ b/src/main/groovy/io/kamax/mxisd/notification/NotificationManager.java @@ -63,7 +63,7 @@ public void sendForValidation(IThreePidSession session) { } public void sendforRemoteValidation(IThreePidSession session) { - throw new NotImplementedException("Remote publish of 3PID bind"); + ensureMedium(session.getThreePid().getMedium()).sendForRemoteValidation(session); } } diff --git a/src/main/groovy/io/kamax/mxisd/session/SessionMananger.java b/src/main/groovy/io/kamax/mxisd/session/SessionMananger.java index 02730628..47c58b75 100644 --- a/src/main/groovy/io/kamax/mxisd/session/SessionMananger.java +++ b/src/main/groovy/io/kamax/mxisd/session/SessionMananger.java @@ -114,24 +114,14 @@ public String create(String server, ThreePid tpid, String secret, int attempt, S } else { log.info("No existing session for {}", tpid); - boolean isLocalDomain = isLocal(tpid); - log.info("Is 3PID bound to local domain? {}", isLocalDomain); + boolean isLocal = isLocal(tpid); + log.info("Is 3PID bound to local domain? {}", isLocal); - if (isLocalDomain && (!policy.forLocal().isEnabled() || !policy.forLocal().toLocal())) { - throw new NotAllowedException("Validating local 3PID is not allowed"); - } - - // We lookup if the 3PID is already known locally. - boolean knownLocal = isKnownLocal(tpid); - log.info("Mapping with {} is " + (knownLocal ? "already" : "not") + " known locally", tpid); - - if (!isLocalDomain && ( - !policy.forRemote().isEnabled() || ( - !policy.forRemote().toLocal() && - !policy.forRemote().toRemote() - ) - )) { - throw new NotAllowedException("Validating unknown remote 3PID is not allowed"); + // This might need a configuration by medium type? + SessionConfig.Policy.PolicyTemplate.PolicySource policySource = policy.forIf(isLocal); + if (!policySource.isEnabled() || (!policySource.toLocal() && !policySource.toRemote())) { + log.info("Session for {}: cancelled due to policy", tpid); + throw new NotAllowedException("Validating " + (isLocal ? "local" : "remote") + " 3PID is not allowed"); } String sessionId; @@ -144,14 +134,12 @@ public String create(String server, ThreePid tpid, String secret, int attempt, S log.info("Generated new session {} to validate {} from server {}", sessionId, tpid, server); // This might need a configuration by medium type? - if (!isLocalDomain) { - if (policy.forRemote().toLocal() && policy.forRemote().toRemote()) { - log.info("Session {} for {}: sending local validation notification", sessionId, tpid); - notifMgr.sendForValidation(session); - } else { - log.info("Session {} for {}: sending remote-only validation notification", sessionId, tpid); - notifMgr.sendforRemoteValidation(session); - } + if (policySource.toLocal()) { + log.info("Session {} for {}: sending local validation notification", sessionId, tpid); + notifMgr.sendForValidation(session); + } else { + log.info("Session {} for {}: sending remote-only validation notification", sessionId, tpid); + notifMgr.sendforRemoteValidation(session); } storage.insertThreePidSession(session.getDao()); diff --git a/src/main/groovy/io/kamax/mxisd/threepid/notification/INotificationGenerator.java b/src/main/groovy/io/kamax/mxisd/threepid/notification/INotificationGenerator.java index c61dba81..ad429ed8 100644 --- a/src/main/groovy/io/kamax/mxisd/threepid/notification/INotificationGenerator.java +++ b/src/main/groovy/io/kamax/mxisd/threepid/notification/INotificationGenerator.java @@ -33,6 +33,6 @@ public interface INotificationGenerator { String getForValidation(IThreePidSession session); - String getForRemotePublishingValidation(IThreePidSession session); + String getForRemoteValidation(IThreePidSession session); } diff --git a/src/main/groovy/io/kamax/mxisd/threepid/notification/email/EmailNotificationGenerator.java b/src/main/groovy/io/kamax/mxisd/threepid/notification/email/EmailNotificationGenerator.java index 6ed62c34..57977a24 100644 --- a/src/main/groovy/io/kamax/mxisd/threepid/notification/email/EmailNotificationGenerator.java +++ b/src/main/groovy/io/kamax/mxisd/threepid/notification/email/EmailNotificationGenerator.java @@ -26,8 +26,8 @@ import io.kamax.mxisd.config.threepid.medium.EmailConfig; import io.kamax.mxisd.config.threepid.medium.EmailTemplateConfig; import io.kamax.mxisd.controller.v1.IdentityAPIv1; +import io.kamax.mxisd.controller.v1.remote.RemoteIdentityAPIv1; import io.kamax.mxisd.exception.InternalServerError; -import io.kamax.mxisd.exception.NotImplementedException; import io.kamax.mxisd.invitation.IThreePidInviteReply; import io.kamax.mxisd.threepid.session.IThreePidSession; import org.apache.commons.io.IOUtils; @@ -121,8 +121,9 @@ public String getForInvite(IThreePidInviteReply invite) { @Override public String getForValidation(IThreePidSession session) { log.info("Generating notification content for 3PID Session validation"); - String templateBody = getTemplateAndPopulate(templateCfg.getSession().getValidation(), session.getThreePid()); + String templateBody = getTemplateAndPopulate(templateCfg.getSession().getValidation().getLocal(), session.getThreePid()); + // FIXME should have a global link builder, most likely in the SDK? String validationLink = srvCfg.getPublicUrl() + IdentityAPIv1.BASE + "/validate/" + session.getThreePid().getMedium() + "/submitToken?sid=" + session.getId() + "&client_secret=" + session.getSecret() + @@ -135,8 +136,19 @@ public String getForValidation(IThreePidSession session) { } @Override - public String getForRemotePublishingValidation(IThreePidSession session) { - throw new NotImplementedException(""); + public String getForRemoteValidation(IThreePidSession session) { + log.info("Generating notification content for 3PID Session validation"); + String templateBody = getTemplateAndPopulate(templateCfg.getSession().getValidation().getLocal(), session.getThreePid()); + + // FIXME should have a global link builder, specific to mxisd + String nextStepLink = srvCfg.getPublicUrl() + RemoteIdentityAPIv1.BASE + + "/validate/requestToken?sid=" + session.getId() + "&client_secret=" + session.getSecret(); + + templateBody = templateBody.replace("%SESSION_ID%", session.getId()); + templateBody = templateBody.replace("%SESSION_SECRET%", session.getSecret()); + templateBody = templateBody.replace("%NEXT_STEP_LINK%", nextStepLink); + + return templateBody; } } diff --git a/src/main/groovy/io/kamax/mxisd/threepid/notification/email/EmailNotificationHandler.java b/src/main/groovy/io/kamax/mxisd/threepid/notification/email/EmailNotificationHandler.java index 024c6ec2..46c4b2e1 100644 --- a/src/main/groovy/io/kamax/mxisd/threepid/notification/email/EmailNotificationHandler.java +++ b/src/main/groovy/io/kamax/mxisd/threepid/notification/email/EmailNotificationHandler.java @@ -43,6 +43,7 @@ public class EmailNotificationHandler implements INotificationHandler { @Autowired public EmailNotificationHandler(EmailConfig cfg, List generators, List connectors) { this.cfg = cfg; + generator = generators.stream() .filter(o -> StringUtils.equals(cfg.getGenerator(), o.getId())) .findFirst() @@ -59,24 +60,28 @@ public String getMedium() { return ThreePidMedium.Email.getId(); } - @Override - public void sendForInvite(IThreePidInviteReply invite) { + private void send(String recipient, String content) { connector.send( cfg.getIdentity().getFrom(), cfg.getIdentity().getName(), - invite.getInvite().getAddress(), - generator.getForInvite(invite) + recipient, + content ); } + @Override + public void sendForInvite(IThreePidInviteReply invite) { + send(invite.getInvite().getAddress(), generator.getForInvite(invite)); + } + @Override public void sendForValidation(IThreePidSession session) { - connector.send( - cfg.getIdentity().getFrom(), - cfg.getIdentity().getName(), - session.getThreePid().getAddress(), - generator.getForValidation(session) - ); + send(session.getThreePid().getAddress(), generator.getForValidation(session)); + } + + @Override + public void sendForRemoteValidation(IThreePidSession session) { + send(session.getThreePid().getAddress(), generator.getForRemoteValidation(session)); } } diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 31e8c594..d7172626 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -81,17 +81,19 @@ threepid: template: invite: 'classpath:email/invite-template.eml' session: - validation: 'classpath:email/validate-template.eml' + validation: + local: 'classpath:email/validate-local-template.eml' + remote: 'classpath:email/validate-remote-template.eml' session.policy.validation: enabled: true forLocal: enabled: true - toLocal: true + toLocal: true # This should not be changed unless you know exactly the implications! toRemote: true forRemote: enabled: true - toLocal: true # This should not be changed unless you know exactly the implications! + toLocal: true toRemote: true storage: diff --git a/src/main/resources/email/validate-local-template.eml b/src/main/resources/email/validate-local-template.eml new file mode 100644 index 00000000..08b4f60b --- /dev/null +++ b/src/main/resources/email/validate-local-template.eml @@ -0,0 +1,91 @@ +Subject: Your Matrix Validation Token +MIME-Version: 1.0 +Content-Type: multipart/alternative; + boundary="7REaIwWQCioQ6NaBlAQlg8ztbUQj6PKJ" + +--7REaIwWQCioQ6NaBlAQlg8ztbUQj6PKJ +Content-Type: text/plain; charset=UTF-8 +Content-Disposition: inline + +Hello there! + +We have received a request to link this email address with your Matrix account. + +If it was really you who made this request, you can click on the following link to +complete the verification of your email address: + + %VALIDATION_LINK% + +If you didn't make this request, you can safely disregard this email. + +Thanks! + +%DOMAIN_PRETTY% Admins + +--7REaIwWQCioQ6NaBlAQlg8ztbUQj6PKJ +Content-Type: multipart/related; + boundary="M3yzHl5YZehm9v4bAM8sKEdcOoVnRnKR"; + type="text/html" + +--M3yzHl5YZehm9v4bAM8sKEdcOoVnRnKR +Content-Type: text/html; charset=UTF-8 +Content-Disposition: inline + + + + + + + + + + + + + +
+

Hello there!

+ +

We have received a request to link this email address with your Matrix account.

+ +

If it was really you who made this request, you can click on the following link to + complete the verification of your email address:

+ +

Complete email verification

+ +

If you didn't make this request, you can safely disregard this email.

+ +

Thanks!

+ +

%DOMAIN_PRETTY% Admins

+
+ + +--M3yzHl5YZehm9v4bAM8sKEdcOoVnRnKR-- + +--7REaIwWQCioQ6NaBlAQlg8ztbUQj6PKJ-- diff --git a/src/main/resources/email/validate-template.eml b/src/main/resources/email/validate-template.eml deleted file mode 100644 index e5b13462..00000000 --- a/src/main/resources/email/validate-template.eml +++ /dev/null @@ -1,66 +0,0 @@ -Subject: Your Matrix Validation Token -MIME-Version: 1.0 -Content-Type: multipart/alternative; - boundary="7REaIwWQCioQ6NaBlAQlg8ztbUQj6PKJ" - ---7REaIwWQCioQ6NaBlAQlg8ztbUQj6PKJ -Content-Type: text/plain; charset=UTF-8 -Content-Disposition: inline - -Hello, - -We have received a request to link this email address with a Matrix account. -If this was you who made this request, you may use the following link to complete the verification of your email address: - - %VALIDATION_LINK% - -If your client requires a code, the code is %VALIDATION_TOKEN% - -If you aren't aware of making such a request, please disregard this email. - -Regards, -%DOMAIN_PRETTY% Admins - ---7REaIwWQCioQ6NaBlAQlg8ztbUQj6PKJ -Content-Type: text/html; charset=UTF-8 -Content-Disposition: inline - - - - - - - - - -

Hello,

- -

We have received a request to link this email address with a Matrix account. - If this was you who made this request, you may use the following link to - complete the verification of your email address:

- -

Complete email verification

- -

...or copy this link into your web browser:

- -

%VALIDATION_LINK%

- -

If your client requires a code, the code is %VALIDATION_TOKEN%

- -

If you aren't aware of making such a request, please disregard this -email.

- -
-

Regards,
- %DOMAIN_PRETTY% Admins

- - - - ---7REaIwWQCioQ6NaBlAQlg8ztbUQj6PKJ-- From 58d80b8eb3f79b7604645ab5f1245ed98430c1af Mon Sep 17 00:00:00 2001 From: Maxime Dor Date: Fri, 22 Sep 2017 00:00:25 +0200 Subject: [PATCH 07/14] Notification for proxying 3PID, remote 3PID are proxied by default --- .../email/EmailNotificationGenerator.java | 9 +- src/main/resources/application.yaml | 2 +- .../email/validate-local-template.eml | 4 - .../email/validate-remote-template.eml | 102 ++++++++++++++++++ 4 files changed, 109 insertions(+), 8 deletions(-) create mode 100644 src/main/resources/email/validate-remote-template.eml diff --git a/src/main/groovy/io/kamax/mxisd/threepid/notification/email/EmailNotificationGenerator.java b/src/main/groovy/io/kamax/mxisd/threepid/notification/email/EmailNotificationGenerator.java index 57977a24..78100e31 100644 --- a/src/main/groovy/io/kamax/mxisd/threepid/notification/email/EmailNotificationGenerator.java +++ b/src/main/groovy/io/kamax/mxisd/threepid/notification/email/EmailNotificationGenerator.java @@ -137,15 +137,18 @@ public String getForValidation(IThreePidSession session) { @Override public String getForRemoteValidation(IThreePidSession session) { - log.info("Generating notification content for 3PID Session validation"); - String templateBody = getTemplateAndPopulate(templateCfg.getSession().getValidation().getLocal(), session.getThreePid()); + log.info("Generating notification content for remote-only 3PID session"); + String templateBody = getTemplateAndPopulate(templateCfg.getSession().getValidation().getRemote(), session.getThreePid()); // FIXME should have a global link builder, specific to mxisd String nextStepLink = srvCfg.getPublicUrl() + RemoteIdentityAPIv1.BASE + - "/validate/requestToken?sid=" + session.getId() + "&client_secret=" + session.getSecret(); + "/validate/requestToken?sid=" + session.getId() + + "&client_secret=" + session.getSecret() + + "&token=" + session.getToken(); templateBody = templateBody.replace("%SESSION_ID%", session.getId()); templateBody = templateBody.replace("%SESSION_SECRET%", session.getSecret()); + templateBody = templateBody.replace("%SESSION_TOKEN%", session.getToken()); templateBody = templateBody.replace("%NEXT_STEP_LINK%", nextStepLink); return templateBody; diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index d7172626..18656d2f 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -93,7 +93,7 @@ session.policy.validation: toRemote: true forRemote: enabled: true - toLocal: true + toLocal: false toRemote: true storage: diff --git a/src/main/resources/email/validate-local-template.eml b/src/main/resources/email/validate-local-template.eml index 08b4f60b..78df56e8 100644 --- a/src/main/resources/email/validate-local-template.eml +++ b/src/main/resources/email/validate-local-template.eml @@ -18,8 +18,6 @@ complete the verification of your email address: If you didn't make this request, you can safely disregard this email. -Thanks! - %DOMAIN_PRETTY% Admins --7REaIwWQCioQ6NaBlAQlg8ztbUQj6PKJ @@ -77,8 +75,6 @@ pre, code {

If you didn't make this request, you can safely disregard this email.

-

Thanks!

-

%DOMAIN_PRETTY% Admins

diff --git a/src/main/resources/email/validate-remote-template.eml b/src/main/resources/email/validate-remote-template.eml new file mode 100644 index 00000000..76c64fc4 --- /dev/null +++ b/src/main/resources/email/validate-remote-template.eml @@ -0,0 +1,102 @@ +Subject: Linking your Email address to your Matrix account +MIME-Version: 1.0 +Content-Type: multipart/alternative; + boundary="7REaIwWQCioQ6NaBlAQlg8ztbUQj6PKJ" + +--7REaIwWQCioQ6NaBlAQlg8ztbUQj6PKJ +Content-Type: text/plain; charset=UTF-8 +Content-Disposition: inline + +Hello there! + +We have received a request to link this email address with your Matrix account. + +Due to the security policy in place, this email address can only be stored in the central Matrix Identity Server. +If you continue, your e-mail address and Matrix ID association will be made public without any current mean to be removed. + +If you would still like to continue, you will need to: +1. Go to your private Public registration process page: + + %NEXT_STEP_LINK% + +2. Follow the registration process of the central Identity Server, usually another email with similar content +3. Once your email address validated with the central Identity Server, click on "Continue" on page of step #1 +4. If your public association is found by our Identity server, the next step will be given to you. + + +If you didn't make this request, or do not want to make your address public, you can safely disregard this email. + +%DOMAIN_PRETTY% Admins + +--7REaIwWQCioQ6NaBlAQlg8ztbUQj6PKJ +Content-Type: multipart/related; + boundary="M3yzHl5YZehm9v4bAM8sKEdcOoVnRnKR"; + type="text/html" + +--M3yzHl5YZehm9v4bAM8sKEdcOoVnRnKR +Content-Type: text/html; charset=UTF-8 +Content-Disposition: inline + + + + + + + + + + + + + +
+

Hello there!

+ +

We have received a request to link this email address with your Matrix account.

+ +

Due to the security policy in place, this email address can only be stored in the central Matrix Identity Server. + If you continue, your e-mail address and Matrix ID association will be made public without any current mean to be removed.

+ +

If you would still like to continue, you will need to: +

    +
  1. Go to your private Public registration process page
  2. +
  3. Follow the registration process of the central Identity Server, usually another email with similar content
  4. +
  5. Once your email address validated with the central Identity Server, click on "Continue" on page of step #1
  6. +
  7. If your public association is found by our Identity server, the next step will be given to you.
  8. +
+

+ +

If you didn't make this request, or do not want to make your address public, you can safely disregard this email.

+ +

%DOMAIN_PRETTY% Admins

+
+ + +--M3yzHl5YZehm9v4bAM8sKEdcOoVnRnKR-- + +--7REaIwWQCioQ6NaBlAQlg8ztbUQj6PKJ-- From 5836965a1ebad06cfb0544824a689ed21dd7f81a Mon Sep 17 00:00:00 2001 From: Maxime Dor Date: Fri, 22 Sep 2017 01:49:27 +0200 Subject: [PATCH 08/14] Saving current work --- .../io/kamax/mxisd/config/MatrixConfig.java | 35 ++++++ .../io/kamax/mxisd/config/SessionConfig.java | 50 +++++--- .../v1/remote/RemoteSessionController.java | 37 ++++++ .../RemoteIdentityServerException.java | 31 +++++ .../lookup/provider/DnsLookupProvider.groovy | 47 +------- .../RemoteIdentityServerFetcher.groovy | 26 +--- .../mxisd/matrix/IdentityServerUtils.java | 114 ++++++++++++++++++ .../kamax/mxisd/session/SessionMananger.java | 63 +++++++++- .../io/kamax/mxisd/util/RestClientUtils.java | 8 ++ src/main/resources/application.yaml | 14 ++- 10 files changed, 329 insertions(+), 96 deletions(-) create mode 100644 src/main/groovy/io/kamax/mxisd/controller/v1/remote/RemoteSessionController.java create mode 100644 src/main/groovy/io/kamax/mxisd/exception/RemoteIdentityServerException.java create mode 100644 src/main/groovy/io/kamax/mxisd/matrix/IdentityServerUtils.java diff --git a/src/main/groovy/io/kamax/mxisd/config/MatrixConfig.java b/src/main/groovy/io/kamax/mxisd/config/MatrixConfig.java index f5acfb99..d3f4048b 100644 --- a/src/main/groovy/io/kamax/mxisd/config/MatrixConfig.java +++ b/src/main/groovy/io/kamax/mxisd/config/MatrixConfig.java @@ -20,6 +20,7 @@ package io.kamax.mxisd.config; +import com.google.gson.Gson; import io.kamax.mxisd.exception.ConfigurationException; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; @@ -28,14 +29,38 @@ import org.springframework.context.annotation.Configuration; import javax.annotation.PostConstruct; +import java.util.HashMap; +import java.util.List; +import java.util.Map; @Configuration @ConfigurationProperties("matrix") public class MatrixConfig { + public static class Identity { + private Map> servers = new HashMap<>(); + + public Map> getServers() { + return servers; + } + + public void setServers(Map> servers) { + this.servers = servers; + } + + public List getServers(String label) { + if (!servers.containsKey(label)) { + throw new RuntimeException("No Identity server list with label '" + label + "'"); + } + + return servers.get(label); + } + } + private Logger log = LoggerFactory.getLogger(MatrixConfig.class); private String domain; + private Identity identity = new Identity(); public String getDomain() { return domain; @@ -45,6 +70,14 @@ public void setDomain(String domain) { this.domain = domain; } + public Identity getIdentity() { + return identity; + } + + public void setIdentity(Identity identity) { + this.identity = identity; + } + @PostConstruct public void build() { log.info("--- Matrix config ---"); @@ -54,6 +87,8 @@ public void build() { } log.info("Domain: {}", getDomain()); + log.info("Identity:"); + log.info("\tServers: {}", new Gson().toJson(identity.getServers())); } } diff --git a/src/main/groovy/io/kamax/mxisd/config/SessionConfig.java b/src/main/groovy/io/kamax/mxisd/config/SessionConfig.java index e72a5fcd..fab29ae2 100644 --- a/src/main/groovy/io/kamax/mxisd/config/SessionConfig.java +++ b/src/main/groovy/io/kamax/mxisd/config/SessionConfig.java @@ -41,10 +41,32 @@ public static class PolicyTemplate { public static class PolicySource { + public static class PolicySourceRemote { + + private boolean enabled; + private String server; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public String getServer() { + return server; + } + + public void setServer(String server) { + this.server = server; + } + + } + private boolean enabled; - private boolean alwaysValidate; private boolean toLocal; - private boolean toRemote; + private PolicySourceRemote toRemote = new PolicySourceRemote(); public boolean isEnabled() { return enabled; @@ -54,14 +76,6 @@ public void setEnabled(boolean enabled) { this.enabled = enabled; } - public boolean isAlwaysValidate() { - return alwaysValidate; - } - - public void setAlwaysValidate(boolean alwaysValidate) { - this.alwaysValidate = alwaysValidate; - } - public boolean toLocal() { return toLocal; } @@ -71,10 +85,14 @@ public void setToLocal(boolean toLocal) { } public boolean toRemote() { + return toRemote.isEnabled(); + } + + public PolicySourceRemote getToRemote() { return toRemote; } - public void setToRemote(boolean toRemote) { + public void setToRemote(PolicySourceRemote toRemote) { this.toRemote = toRemote; } @@ -111,18 +129,10 @@ public PolicySource forRemote() { public PolicySource forIf(boolean isLocal) { return isLocal ? forLocal : forRemote; } - } - private PolicyTemplate bind = new PolicyTemplate(); - private PolicyTemplate validation = new PolicyTemplate(); - - public PolicyTemplate getBind() { - return bind; } - public void setBind(PolicyTemplate bind) { - this.bind = bind; - } + private PolicyTemplate validation = new PolicyTemplate(); public PolicyTemplate getValidation() { return validation; diff --git a/src/main/groovy/io/kamax/mxisd/controller/v1/remote/RemoteSessionController.java b/src/main/groovy/io/kamax/mxisd/controller/v1/remote/RemoteSessionController.java new file mode 100644 index 00000000..ed06f493 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/controller/v1/remote/RemoteSessionController.java @@ -0,0 +1,37 @@ +package io.kamax.mxisd.controller.v1.remote; + +import io.kamax.mxisd.session.SessionMananger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; + +@RestController +@CrossOrigin +@RequestMapping(path = RemoteIdentityAPIv1.BASE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) +public class RemoteSessionController { + + private Logger log = LoggerFactory.getLogger(RemoteSessionController.class); + + @Autowired + private SessionMananger mgr; + + @RequestMapping(path = "/validate/requestToken") + public String requestToken( + HttpServletRequest request, + @RequestParam String sid, + @RequestParam("client_secret") String secret, + @RequestParam String token) { + log.info("Request {}: {}", request.getMethod(), request.getRequestURL(), request.getQueryString()); + mgr.createRemote(sid, secret, token); + + return "{}"; + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/exception/RemoteIdentityServerException.java b/src/main/groovy/io/kamax/mxisd/exception/RemoteIdentityServerException.java new file mode 100644 index 00000000..1e65fc40 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/exception/RemoteIdentityServerException.java @@ -0,0 +1,31 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.exception; + +import org.apache.http.HttpStatus; + +public class RemoteIdentityServerException extends MatrixException { + + public RemoteIdentityServerException(String error) { + super(HttpStatus.SC_SERVICE_UNAVAILABLE, "M_REMOTE_IS_ERROR", error); + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/lookup/provider/DnsLookupProvider.groovy b/src/main/groovy/io/kamax/mxisd/lookup/provider/DnsLookupProvider.groovy index e63e030c..28ef1a8f 100644 --- a/src/main/groovy/io/kamax/mxisd/lookup/provider/DnsLookupProvider.groovy +++ b/src/main/groovy/io/kamax/mxisd/lookup/provider/DnsLookupProvider.groovy @@ -25,14 +25,12 @@ import io.kamax.mxisd.lookup.SingleLookupReply import io.kamax.mxisd.lookup.SingleLookupRequest import io.kamax.mxisd.lookup.ThreePidMapping import io.kamax.mxisd.lookup.fetcher.IRemoteIdentityServerFetcher +import io.kamax.mxisd.matrix.IdentityServerUtils import org.apache.commons.lang.StringUtils 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.Type import java.util.concurrent.ForkJoinPool import java.util.concurrent.RecursiveTask @@ -64,10 +62,6 @@ class DnsLookupProvider implements IThreePidProvider { return 10 } - String getSrvRecordName(String domain) { - return "_matrix-identity._tcp." + domain - } - Optional getDomain(String email) { int atIndex = email.lastIndexOf("@") if (atIndex == -1) { @@ -84,44 +78,7 @@ class DnsLookupProvider implements IThreePidProvider { return Optional.empty() } - log.info("Performing SRV lookup") - String lookupDns = getSrvRecordName(domain) - log.info("Lookup name: {}", lookupDns) - - SRVRecord[] records = (SRVRecord[]) new Lookup(lookupDns, Type.SRV).run() - if (records != null) { - Arrays.sort(records, new Comparator() { - - @Override - int compare(SRVRecord o1, SRVRecord o2) { - return Integer.compare(o1.getPriority(), o2.getPriority()) - } - - }) - - for (SRVRecord record : records) { - log.info("Found SRV record: {}", record.toString()) - String baseUrl = "https://${record.getTarget().toString(true)}:${record.getPort()}" - if (fetcher.isUsable(baseUrl)) { - log.info("Found Identity Server for domain {} at {}", domain, baseUrl) - return Optional.of(baseUrl) - } else { - log.info("{} is not a usable Identity Server", baseUrl) - } - } - } else { - log.info("No SRV record for {}", lookupDns) - } - - log.info("Performing basic lookup using domain name {}", domain) - String baseUrl = "https://" + domain - if (fetcher.isUsable(baseUrl)) { - log.info("Found Identity Server for domain {} at {}", domain, baseUrl) - return Optional.of(baseUrl) - } else { - log.info("{} is not a usable Identity Server", baseUrl) - return Optional.empty() - } + return IdentityServerUtils.findIsUrlForDomain(domain) } @Override diff --git a/src/main/groovy/io/kamax/mxisd/lookup/provider/RemoteIdentityServerFetcher.groovy b/src/main/groovy/io/kamax/mxisd/lookup/provider/RemoteIdentityServerFetcher.groovy index 7435879c..5844d9cb 100644 --- a/src/main/groovy/io/kamax/mxisd/lookup/provider/RemoteIdentityServerFetcher.groovy +++ b/src/main/groovy/io/kamax/mxisd/lookup/provider/RemoteIdentityServerFetcher.groovy @@ -28,6 +28,7 @@ import io.kamax.mxisd.lookup.SingleLookupReply import io.kamax.mxisd.lookup.SingleLookupRequest import io.kamax.mxisd.lookup.ThreePidMapping import io.kamax.mxisd.lookup.fetcher.IRemoteIdentityServerFetcher +import io.kamax.mxisd.matrix.IdentityServerUtils import org.apache.http.HttpEntity import org.apache.http.HttpResponse import org.apache.http.client.HttpClient @@ -46,36 +47,13 @@ import org.springframework.stereotype.Component @Lazy public class RemoteIdentityServerFetcher implements IRemoteIdentityServerFetcher { - public static final String THREEPID_TEST_MEDIUM = "email" - public static final String THREEPID_TEST_ADDRESS = "john.doe@example.org" - private Logger log = LoggerFactory.getLogger(RemoteIdentityServerFetcher.class) private JsonSlurper json = new JsonSlurper() @Override boolean isUsable(String remote) { - try { - HttpURLConnection rootSrvConn = (HttpURLConnection) new URL( - "${remote}/_matrix/identity/api/v1/lookup?medium=${THREEPID_TEST_MEDIUM}&address=${THREEPID_TEST_ADDRESS}" - ).openConnection() - // TODO turn this into a configuration property - rootSrvConn.setConnectTimeout(2000) - - if (rootSrvConn.getResponseCode() != 200) { - return false - } - - def output = json.parseText(rootSrvConn.getInputStream().getText()) - if (output['address']) { - return false - } - - return true - } catch (IOException | JsonException e) { - log.info("{} is not a usable Identity Server: {}", remote, e.getMessage()) - return false - } + return IdentityServerUtils.isUsable(remote) } @Override diff --git a/src/main/groovy/io/kamax/mxisd/matrix/IdentityServerUtils.java b/src/main/groovy/io/kamax/mxisd/matrix/IdentityServerUtils.java new file mode 100644 index 00000000..f6071068 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/matrix/IdentityServerUtils.java @@ -0,0 +1,114 @@ +package io.kamax.mxisd.matrix; + +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.JsonParser; +import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.xbill.DNS.Lookup; +import org.xbill.DNS.SRVRecord; +import org.xbill.DNS.TextParseException; +import org.xbill.DNS.Type; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Optional; + +// FIXME placeholder, this must go in matrix-java-sdk for 1.0 +public class IdentityServerUtils { + + public static final String THREEPID_TEST_MEDIUM = "email"; + public static final String THREEPID_TEST_ADDRESS = "mxisd-email-forever-unknown@forever-invalid.kamax.io"; + + private static Logger log = LoggerFactory.getLogger(IdentityServerUtils.class); + private static JsonParser parser = new JsonParser(); + + public static boolean isUsable(String remote) { + try { + // FIXME use Apache HTTP client + HttpURLConnection rootSrvConn = (HttpURLConnection) new URL( + remote + "/_matrix/identity/api/v1/lookup?medium=" + THREEPID_TEST_MEDIUM + "&address=" + THREEPID_TEST_ADDRESS + ).openConnection(); + // TODO turn this into a configuration property + rootSrvConn.setConnectTimeout(2000); + + if (rootSrvConn.getResponseCode() != 200) { + return false; + } + + JsonElement el = parser.parse(IOUtils.toString(rootSrvConn.getInputStream(), StandardCharsets.UTF_8)); + if (!el.isJsonObject()) { + log.debug("IS {} did not send back a JSON object for single 3PID lookup"); + return false; + } + + if (el.getAsJsonObject().has("address")) { + log.debug("IS {} did not send back a JSON object for single 3PID lookup"); + return false; + } + + return true; + } catch (IOException | JsonParseException e) { + log.info("{} is not a usable Identity Server: {}", remote, e.getMessage()); + return false; + } + } + + public static String getSrvRecordName(String domain) { + return "_matrix-identity._tcp." + domain; + } + + public static Optional findIsUrlForDomain(String domainOrUrl) { + try { + try { + domainOrUrl = new URL(domainOrUrl).getHost(); + } catch (MalformedURLException e) { + log.info("{} is not an URL, using as-is", domainOrUrl); + } + + log.info("Discovery Identity Server for {}", domainOrUrl); + log.info("Performing SRV lookup"); + String lookupDns = getSrvRecordName(domainOrUrl); + log.info("Lookup name: {}", lookupDns); + + 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()); + String baseUrl = "https://${record.getTarget().toString(true)}:${record.getPort()}"; + if (isUsable(baseUrl)) { + log.info("Found Identity Server for domain {} at {}", domainOrUrl, baseUrl); + return Optional.of(baseUrl); + } else { + log.info("{} is not a usable Identity Server", baseUrl); + return Optional.empty(); + } + } + } else { + log.info("No SRV record for {}", lookupDns); + } + + log.info("Performing basic lookup using domain name {}", domainOrUrl); + String baseUrl = "https://" + domainOrUrl; + if (isUsable(baseUrl)) { + log.info("Found Identity Server for domain {} at {}", domainOrUrl, baseUrl); + return Optional.of(baseUrl); + } else { + log.info("{} is not a usable Identity Server", baseUrl); + return Optional.empty(); + } + } catch (TextParseException e) { + log.warn(domainOrUrl + " is not a valid domain name"); + return Optional.empty(); + } + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/session/SessionMananger.java b/src/main/groovy/io/kamax/mxisd/session/SessionMananger.java index 47c58b75..484abbff 100644 --- a/src/main/groovy/io/kamax/mxisd/session/SessionMananger.java +++ b/src/main/groovy/io/kamax/mxisd/session/SessionMananger.java @@ -20,41 +20,56 @@ package io.kamax.mxisd.session; +import com.google.gson.JsonObject; import io.kamax.matrix.ThreePidMedium; import io.kamax.mxisd.ThreePid; import io.kamax.mxisd.config.MatrixConfig; import io.kamax.mxisd.config.SessionConfig; -import io.kamax.mxisd.exception.InvalidCredentialsException; -import io.kamax.mxisd.exception.NotAllowedException; -import io.kamax.mxisd.exception.SessionNotValidatedException; +import io.kamax.mxisd.exception.*; import io.kamax.mxisd.lookup.ThreePidValidation; import io.kamax.mxisd.lookup.strategy.LookupStrategy; +import io.kamax.mxisd.matrix.IdentityServerUtils; import io.kamax.mxisd.notification.NotificationManager; import io.kamax.mxisd.storage.IStorage; import io.kamax.mxisd.storage.dao.IThreePidSessionDao; import io.kamax.mxisd.threepid.session.ThreePidSession; +import io.kamax.mxisd.util.RestClientUtils; import org.apache.commons.lang.RandomStringUtils; import org.apache.commons.lang.StringUtils; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import java.io.IOException; +import java.util.List; import java.util.Optional; +import static io.kamax.mxisd.config.SessionConfig.Policy.PolicyTemplate; +import static io.kamax.mxisd.config.SessionConfig.Policy.PolicyTemplate.PolicySource; + @Component public class SessionMananger { private Logger log = LoggerFactory.getLogger(SessionMananger.class); private SessionConfig cfg; + private MatrixConfig mxCfg; private IStorage storage; private LookupStrategy lookup; private NotificationManager notifMgr; + // FIXME export into central class, set version + private CloseableHttpClient client = HttpClients.custom().setUserAgent("mxisd").build(); + @Autowired public SessionMananger(SessionConfig cfg, MatrixConfig mxCfg, IStorage storage, LookupStrategy lookup, NotificationManager notifMgr) { this.cfg = cfg; + this.mxCfg = mxCfg; this.storage = storage; this.lookup = lookup; this.notifMgr = notifMgr; @@ -91,7 +106,7 @@ private ThreePidSession getSessionIfValidated(String sid, String secret) { } public String create(String server, ThreePid tpid, String secret, int attempt, String nextLink) { - SessionConfig.Policy.PolicyTemplate policy = cfg.getPolicy().getValidation(); + PolicyTemplate policy = cfg.getPolicy().getValidation(); if (!policy.isEnabled()) { throw new NotAllowedException("Validating 3PID is disabled globally"); } @@ -118,7 +133,7 @@ public String create(String server, ThreePid tpid, String secret, int attempt, S log.info("Is 3PID bound to local domain? {}", isLocal); // This might need a configuration by medium type? - SessionConfig.Policy.PolicyTemplate.PolicySource policySource = policy.forIf(isLocal); + PolicySource policySource = policy.forIf(isLocal); if (!policySource.isEnabled() || (!policySource.toLocal() && !policySource.toRemote())) { log.info("Session for {}: cancelled due to policy", tpid); throw new NotAllowedException("Validating " + (isLocal ? "local" : "remote") + " 3PID is not allowed"); @@ -170,4 +185,42 @@ public void bind(String sid, String secret, String mxid) { // TODO perform this if request was proxied } + public void createRemote(String sid, String secret, String token) { + ThreePidSession session = getSessionIfValidated(sid, secret); + + boolean isLocal = isLocal(session.getThreePid()); + PolicySource policy = cfg.getPolicy().getValidation().forIf(isLocal); + if (!policy.isEnabled() || !policy.toRemote()) { + throw new NotAllowedException("Validating " + (isLocal ? "local" : "remote") + " 3PID is not allowed"); + } + + List servers = mxCfg.getIdentity().getServers(policy.getToRemote().getServer()); + if (servers.isEmpty()) { + throw new InternalServerError(); + } + + String url = IdentityServerUtils.findIsUrlForDomain(servers.get(0)).orElseThrow(InternalServerError::new); + + JsonObject body = new JsonObject(); + body.addProperty("client_secret", RandomStringUtils.randomAlphanumeric(16)); + body.addProperty(session.getThreePid().getMedium(), session.getThreePid().getAddress()); + body.addProperty("send_attempt", 1); + + log.info("Creating remote 3PID session for {} with local session [{}] to {}", session.getThreePid(), sid); + HttpPost tokenReq = RestClientUtils.post(url + "/_matrix/identity/api/v1/validate/" + session.getThreePid().getMedium() + "/requestToken", body); + try (CloseableHttpResponse response = client.execute(tokenReq)) { + int status = response.getStatusLine().getStatusCode(); + if (status < 200 || status >= 300) { + throw new RemoteIdentityServerException("Remote identity server returned with status " + status); + } + + // TODO finish + } catch (IOException e) { + log.warn("Failed to create remote session with {} for {}: {}", url, session.getThreePid(), e.getMessage()); + throw new RemoteIdentityServerException(e.getMessage()); + } + + + } + } diff --git a/src/main/groovy/io/kamax/mxisd/util/RestClientUtils.java b/src/main/groovy/io/kamax/mxisd/util/RestClientUtils.java index c5210088..1c23a83b 100644 --- a/src/main/groovy/io/kamax/mxisd/util/RestClientUtils.java +++ b/src/main/groovy/io/kamax/mxisd/util/RestClientUtils.java @@ -20,7 +20,9 @@ package io.kamax.mxisd.util; +import com.google.gson.FieldNamingPolicy; import com.google.gson.Gson; +import com.google.gson.GsonBuilder; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.ContentType; import org.apache.http.entity.StringEntity; @@ -29,6 +31,8 @@ public class RestClientUtils { + private static Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create(); + public static HttpPost post(String url, String body) { StringEntity entity = new StringEntity(body, StandardCharsets.UTF_8); entity.setContentType(ContentType.APPLICATION_JSON.toString()); @@ -45,4 +49,8 @@ public static HttpPost post(String url, Gson gson, Object o) { return post(url, gson.toJson(o)); } + public static HttpPost post(String url, Object o) { + return post(url, gson.toJson(o)); + } + } diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 18656d2f..839b3789 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -15,6 +15,12 @@ logging: server: port: 8090 +matrix: + identity: + servers: + root: + - 'https://matrix.org' + lookup: recursive: enabled: true @@ -90,11 +96,15 @@ session.policy.validation: forLocal: enabled: true toLocal: true # This should not be changed unless you know exactly the implications! - toRemote: true + toRemote: + enabled: true + server: 'root' forRemote: enabled: true toLocal: false - toRemote: true + toRemote: + enabled: true + server: 'root' storage: backend: 'sqlite' From df81dda22d2726469776c35c144dfe9364734ff6 Mon Sep 17 00:00:00 2001 From: Maxime Dor Date: Sat, 23 Sep 2017 04:27:14 +0200 Subject: [PATCH 09/14] First working prototype to proxy 3PID binds to central Matrix.org IS --- build.gradle | 3 + .../kamax/mxisd/config/ThymeleafConfig.java | 42 +++++ .../io/kamax/mxisd/config/ViewConfig.java | 144 ++++++++++++++++ .../controller/v1/SessionController.groovy | 160 +++++------------- .../controller/v1/SessionRestController.java | 159 +++++++++++++++++ .../v1/io/RequestTokenResponse.java | 31 ++++ .../v1/remote/RemoteIdentityAPIv1.java | 12 +- .../v1/remote/RemoteSessionController.java | 44 +++-- .../exception/SessionUnknownException.java | 33 ++++ .../kamax/mxisd/session/SessionMananger.java | 144 ++++++++++++++-- .../kamax/mxisd/session/ValidationResult.java | 54 ++++++ .../storage/dao/IThreePidSessionDao.java | 12 ++ .../ormlite/dao/ThreePidSessionDao.java | 79 ++++++++- .../email/EmailNotificationGenerator.java | 15 +- .../threepid/session/IThreePidSession.java | 12 ++ .../threepid/session/ThreePidSession.java | 89 ++++++++++ .../io/kamax/mxisd/util/GsonParser.java | 5 + src/main/resources/application.yaml | 18 ++ .../email/validate-remote-template.eml | 4 +- .../session/local/tokenSubmitFailure.html | 40 +++++ .../session/local/tokenSubmitSuccess.html | 40 +++++ .../localRemote/tokenSubmitSuccess.html | 47 +++++ .../session/remote/checkFailure.html | 43 +++++ .../session/remote/checkSuccess.html | 41 +++++ .../session/remote/requestFailure.html | 42 +++++ .../session/remote/requestSuccess.html | 45 +++++ 26 files changed, 1200 insertions(+), 158 deletions(-) create mode 100644 src/main/groovy/io/kamax/mxisd/config/ThymeleafConfig.java create mode 100644 src/main/groovy/io/kamax/mxisd/config/ViewConfig.java create mode 100644 src/main/groovy/io/kamax/mxisd/controller/v1/SessionRestController.java create mode 100644 src/main/groovy/io/kamax/mxisd/controller/v1/io/RequestTokenResponse.java create mode 100644 src/main/groovy/io/kamax/mxisd/exception/SessionUnknownException.java create mode 100644 src/main/groovy/io/kamax/mxisd/session/ValidationResult.java create mode 100644 src/main/resources/templates/session/local/tokenSubmitFailure.html create mode 100644 src/main/resources/templates/session/local/tokenSubmitSuccess.html create mode 100644 src/main/resources/templates/session/localRemote/tokenSubmitSuccess.html create mode 100644 src/main/resources/templates/session/remote/checkFailure.html create mode 100644 src/main/resources/templates/session/remote/checkSuccess.html create mode 100644 src/main/resources/templates/session/remote/requestFailure.html create mode 100644 src/main/resources/templates/session/remote/requestSuccess.html diff --git a/build.gradle b/build.gradle index 59e6b9d8..053837f4 100644 --- a/build.gradle +++ b/build.gradle @@ -79,6 +79,9 @@ dependencies { // Spring Boot - standalone app compile 'org.springframework.boot:spring-boot-starter-web:1.5.3.RELEASE' + // Thymeleaf for HTML templates + compile "org.springframework.boot:spring-boot-starter-thymeleaf:1.5.3.RELEASE" + // Matrix Java SDK compile 'io.kamax:matrix-java-sdk:0.0.2' diff --git a/src/main/groovy/io/kamax/mxisd/config/ThymeleafConfig.java b/src/main/groovy/io/kamax/mxisd/config/ThymeleafConfig.java new file mode 100644 index 00000000..ecb967b7 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/config/ThymeleafConfig.java @@ -0,0 +1,42 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.thymeleaf.resourceresolver.FileResourceResolver; +import org.thymeleaf.templateresolver.TemplateResolver; + +@Configuration +public class ThymeleafConfig { + + @Bean + public TemplateResolver getFileSystemResolver() { + TemplateResolver resolver = new TemplateResolver(); + resolver.setPrefix(""); + resolver.setSuffix(""); + resolver.setCacheable(false); + resolver.setOrder(1); + resolver.setResourceResolver(new FileResourceResolver()); + return resolver; + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/config/ViewConfig.java b/src/main/groovy/io/kamax/mxisd/config/ViewConfig.java new file mode 100644 index 00000000..2a6fa7c4 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/config/ViewConfig.java @@ -0,0 +1,144 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.config; + +import com.google.gson.Gson; +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; + +@Configuration +@ConfigurationProperties("view") +public class ViewConfig { + + private Logger log = LoggerFactory.getLogger(ViewConfig.class); + + public static class Session { + + public static class Paths { + + private String failure; + private String success; + + public String getFailure() { + return failure; + } + + public void setFailure(String failure) { + this.failure = failure; + } + + public String getSuccess() { + return success; + } + + public void setSuccess(String success) { + this.success = success; + } + + } + + public static class Local { + + private Paths onTokenSubmit = new Paths(); + + public Paths getOnTokenSubmit() { + return onTokenSubmit; + } + + public void setOnTokenSubmit(Paths onTokenSubmit) { + this.onTokenSubmit = onTokenSubmit; + } + + } + + public static class Remote { + + private Paths onRequest = new Paths(); + private Paths onCheck = new Paths(); + + public Paths getOnRequest() { + return onRequest; + } + + public void setOnRequest(Paths onRequest) { + this.onRequest = onRequest; + } + + public Paths getOnCheck() { + return onCheck; + } + + public void setOnCheck(Paths onCheck) { + this.onCheck = onCheck; + } + + } + + private Local local = new Local(); + private Local localRemote = new Local(); + private Remote remote = new Remote(); + + public Local getLocal() { + return local; + } + + public void setLocal(Local local) { + this.local = local; + } + + public Local getLocalRemote() { + return localRemote; + } + + public void setLocalRemote(Local localRemote) { + this.localRemote = localRemote; + } + + public Remote getRemote() { + return remote; + } + + public void setRemote(Remote remote) { + this.remote = remote; + } + } + + private Session session = new Session(); + + public Session getSession() { + return session; + } + + public void setSession(Session session) { + this.session = session; + } + + @PostConstruct + public void build() { + log.info("--- View config ---"); + log.info("Session: {}", new Gson().toJson(session)); + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/controller/v1/SessionController.groovy b/src/main/groovy/io/kamax/mxisd/controller/v1/SessionController.groovy index f785e5ad..718d6714 100644 --- a/src/main/groovy/io/kamax/mxisd/controller/v1/SessionController.groovy +++ b/src/main/groovy/io/kamax/mxisd/controller/v1/SessionController.groovy @@ -20,146 +20,62 @@ package io.kamax.mxisd.controller.v1 -import com.google.gson.Gson -import com.google.gson.JsonObject -import io.kamax.matrix.ThreePidMedium -import io.kamax.mxisd.ThreePid -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.exception.SessionNotValidatedException -import io.kamax.mxisd.invitation.InvitationManager -import io.kamax.mxisd.lookup.ThreePidValidation +import io.kamax.mxisd.config.ServerConfig +import io.kamax.mxisd.config.ViewConfig +import io.kamax.mxisd.controller.v1.remote.RemoteIdentityAPIv1 import io.kamax.mxisd.session.SessionMananger -import org.apache.commons.io.IOUtils -import org.apache.http.HttpStatus +import io.kamax.mxisd.session.ValidationResult import org.slf4j.Logger import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Autowired -import org.springframework.http.MediaType -import org.springframework.web.bind.annotation.* +import org.springframework.stereotype.Controller +import org.springframework.ui.Model +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletResponse -import java.nio.charset.StandardCharsets -@RestController -@CrossOrigin -@RequestMapping(path = IdentityAPIv1.BASE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) +@Controller +@RequestMapping(path = IdentityAPIv1.BASE) class SessionController { - @Autowired - private SessionMananger mgr - - @Autowired - private InvitationManager invMgr; - - private Gson gson = new Gson() - private Logger log = LoggerFactory.getLogger(SessionController.class) - private T fromJson(HttpServletRequest req, Class obj) { - gson.fromJson(new InputStreamReader(req.getInputStream(), StandardCharsets.UTF_8), obj) - } - - @RequestMapping(value = "/validate/{medium}/requestToken") - String init(HttpServletRequest request, HttpServletResponse response, @PathVariable String medium) { - log.info("Request {}: {}", request.getMethod(), request.getRequestURL(), request.getQueryString()) - if (ThreePidMedium.Email.is(medium)) { - SessionEmailTokenRequestJson req = fromJson(request, SessionEmailTokenRequestJson.class) - return gson.toJson(new Sid(mgr.create( - request.getRemoteHost(), - new ThreePid(req.getMedium(), req.getValue()), - req.getSecret(), - req.getAttempt(), - req.getNextLink()))); - } + @Autowired + private ServerConfig srvCfg; - if (ThreePidMedium.PhoneNumber) { - SessionPhoneTokenRequestJson req = fromJson(request, SessionPhoneTokenRequestJson.class) - return gson.toJson(new Sid(mgr.create( - request.getRemoteHost(), - new ThreePid(req.getMedium(), req.getValue()), - req.getSecret(), - req.getAttempt(), - req.getNextLink()))); - } + @Autowired + private SessionMananger mgr - JsonObject obj = new JsonObject(); - obj.addProperty("errcode", "M_INVALID_3PID_TYPE") - obj.addProperty("error", medium + " is not supported as a 3PID type") - response.setStatus(HttpStatus.SC_BAD_REQUEST) - return gson.toJson(obj) - } + @Autowired + private ViewConfig viewCfg; @RequestMapping(value = "/validate/{medium}/submitToken") - String validate(HttpServletRequest request, - @RequestParam String sid, - @RequestParam("client_secret") String secret, @RequestParam String token) { + String validate( + HttpServletRequest request, + HttpServletResponse response, + @RequestParam String sid, + @RequestParam("client_secret") String secret, + @RequestParam String token, + Model model + ) { log.info("Requested: {}?{}", request.getRequestURL(), request.getQueryString()) - mgr.validate(sid, secret, token) - - return "{}" - } - - @RequestMapping(value = "/3pid/getValidated3pid") - String check(HttpServletRequest request, HttpServletResponse response, - @RequestParam String sid, @RequestParam("client_secret") String secret) { - log.info("Requested: {}", request.getRequestURL(), request.getQueryString()) - - try { - ThreePidValidation pid = mgr.getValidated(sid, secret) - - JsonObject obj = new JsonObject() - obj.addProperty("medium", pid.getMedium()) - obj.addProperty("address", pid.getAddress()) - obj.addProperty("validated_at", pid.getValidation().toEpochMilli()) - - return gson.toJson(obj); - } catch (SessionNotValidatedException e) { - log.info("Session {} was requested but has not yet been validated", sid); - throw e; - } - } - - @RequestMapping(value = "/3pid/bind") - String bind(HttpServletRequest request, HttpServletResponse response, - @RequestParam String sid, @RequestParam("client_secret") String secret, @RequestParam String mxid) { - String data = IOUtils.toString(request.getReader()) - log.info("Requested: {}", request.getRequestURL(), request.getQueryString()) - try { - mgr.bind(sid, secret, mxid) - return "{}" - } catch (BadRequestException e) { - log.info("requested session was not validated") - - JsonObject obj = new JsonObject() - obj.addProperty("errcode", "M_SESSION_NOT_VALIDATED") - obj.addProperty("error", e.getMessage()) - response.setStatus(HttpStatus.SC_BAD_REQUEST) - return gson.toJson(obj) - } finally { - // If a user registers, there is no standard login event. Instead, this is the only way to trigger - // resolution at an appropriate time. Meh at synapse/Riot! - invMgr.lookupMappingsForInvites() - } - } - - private class Sid { - - private String sid; - - public Sid(String sid) { - setSid(sid); - } - - String getSid() { - return sid - } - - void setSid(String sid) { - this.sid = sid + ValidationResult r = mgr.validate(sid, secret, token) + log.info("Session {} was validated", sid) + if (r.getNextUrl().isPresent()) { + String url = srvCfg.getPublicUrl() + r.getNextUrl().get() + log.info("Session {} validation: next URL is present, redirecting to {}", sid, url) + response.sendRedirect(url) + } else { + if (r.isCanRemote()) { + String url = srvCfg.getPublicUrl() + RemoteIdentityAPIv1.getRequestToken(r.getSession().getId(), r.getSession().getSecret()); + model.addAttribute("remoteSessionLink", url) + return viewCfg.getSession().getLocalRemote().getOnTokenSubmit().getSuccess() + } else { + return viewCfg.getSession().getLocal().getOnTokenSubmit().getSuccess() + } } } diff --git a/src/main/groovy/io/kamax/mxisd/controller/v1/SessionRestController.java b/src/main/groovy/io/kamax/mxisd/controller/v1/SessionRestController.java new file mode 100644 index 00000000..aa7061cd --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/controller/v1/SessionRestController.java @@ -0,0 +1,159 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.controller.v1; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import io.kamax.matrix.ThreePidMedium; +import io.kamax.mxisd.ThreePid; +import io.kamax.mxisd.config.ServerConfig; +import io.kamax.mxisd.config.ViewConfig; +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.exception.SessionNotValidatedException; +import io.kamax.mxisd.invitation.InvitationManager; +import io.kamax.mxisd.lookup.ThreePidValidation; +import io.kamax.mxisd.session.SessionMananger; +import io.kamax.mxisd.util.GsonParser; +import org.apache.http.HttpStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +@RestController +@CrossOrigin +@RequestMapping(path = IdentityAPIv1.BASE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) +public class SessionRestController { + + private Logger log = LoggerFactory.getLogger(SessionRestController.class); + + private class Sid { // FIXME replace with RequestTokenResponse + + private String sid; + + public Sid(String sid) { + setSid(sid); + } + + String getSid() { + return sid; + } + + void setSid(String sid) { + this.sid = sid; + } + } + + @Autowired + private ServerConfig srvCfg; + + @Autowired + private SessionMananger mgr; + + @Autowired + private InvitationManager invMgr; + + @Autowired + private ViewConfig viewCfg; + + private Gson gson = new Gson(); + private GsonParser parser = new GsonParser(gson); + + @RequestMapping(value = "/validate/{medium}/requestToken") + String init(HttpServletRequest request, HttpServletResponse response, @PathVariable String medium) throws IOException { + log.info("Request {}: {}", request.getMethod(), request.getRequestURL(), request.getQueryString()); + if (ThreePidMedium.Email.is(medium)) { + SessionEmailTokenRequestJson req = parser.parse(request, SessionEmailTokenRequestJson.class); + return gson.toJson(new Sid(mgr.create( + request.getRemoteHost(), + new ThreePid(req.getMedium(), req.getValue()), + req.getSecret(), + req.getAttempt(), + req.getNextLink()))); + } + + if (ThreePidMedium.PhoneNumber.is(medium)) { + SessionPhoneTokenRequestJson req = parser.parse(request, SessionPhoneTokenRequestJson.class); + return gson.toJson(new Sid(mgr.create( + request.getRemoteHost(), + new ThreePid(req.getMedium(), req.getValue()), + req.getSecret(), + req.getAttempt(), + req.getNextLink()))); + } + + JsonObject obj = new JsonObject(); + obj.addProperty("errcode", "M_INVALID_3PID_TYPE"); + obj.addProperty("error", medium + " is not supported as a 3PID type"); + response.setStatus(HttpStatus.SC_BAD_REQUEST); + return gson.toJson(obj); + } + + @RequestMapping(value = "/3pid/getValidated3pid") + String check(HttpServletRequest request, HttpServletResponse response, + @RequestParam String sid, @RequestParam("client_secret") String secret) { + log.info("Requested: {}", request.getRequestURL(), request.getQueryString()); + + try { + ThreePidValidation pid = mgr.getValidated(sid, secret); + + JsonObject obj = new JsonObject(); + obj.addProperty("medium", pid.getMedium()); + obj.addProperty("address", pid.getAddress()); + obj.addProperty("validated_at", pid.getValidation().toEpochMilli()); + + return gson.toJson(obj); + } catch (SessionNotValidatedException e) { + log.info("Session {} was requested but has not yet been validated", sid); + throw e; + } + } + + @RequestMapping(value = "/3pid/bind") + String bind(HttpServletRequest request, HttpServletResponse response, + @RequestParam String sid, @RequestParam("client_secret") String secret, @RequestParam String mxid) { + log.info("Requested: {}", request.getRequestURL(), request.getQueryString()); + try { + mgr.bind(sid, secret, mxid); + return "{}"; + } catch (BadRequestException e) { + log.info("requested session was not validated"); + + JsonObject obj = new JsonObject(); + obj.addProperty("errcode", "M_SESSION_NOT_VALIDATED"); + obj.addProperty("error", e.getMessage()); + response.setStatus(HttpStatus.SC_BAD_REQUEST); + return gson.toJson(obj); + } finally { + // If a user registers, there is no standard login event. Instead, this is the only way to trigger + // resolution at an appropriate time. Meh at synapse/Riot! + invMgr.lookupMappingsForInvites(); + } + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/controller/v1/io/RequestTokenResponse.java b/src/main/groovy/io/kamax/mxisd/controller/v1/io/RequestTokenResponse.java new file mode 100644 index 00000000..1946cd09 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/controller/v1/io/RequestTokenResponse.java @@ -0,0 +1,31 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.controller.v1.io; + +public class RequestTokenResponse { + + private String sid; + + public String getSid() { + return sid; + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/controller/v1/remote/RemoteIdentityAPIv1.java b/src/main/groovy/io/kamax/mxisd/controller/v1/remote/RemoteIdentityAPIv1.java index d4517368..11d51924 100644 --- a/src/main/groovy/io/kamax/mxisd/controller/v1/remote/RemoteIdentityAPIv1.java +++ b/src/main/groovy/io/kamax/mxisd/controller/v1/remote/RemoteIdentityAPIv1.java @@ -22,6 +22,16 @@ public class RemoteIdentityAPIv1 { - public static final String BASE = "/_matrix/identity-remote/api/v1"; + public static final String BASE = "/_matrix/identity/remote/api/v1"; + public static final String SESSION_REQUEST_TOKEN = BASE + "/validate/requestToken"; + public static final String SESSION_CHECK = BASE + "/validate/check"; + + public static String getRequestToken(String id, String secret) { + return SESSION_REQUEST_TOKEN + "?sid=" + id + "&client_secret=" + secret; + } + + public static String getSessionCheck(String id, String secret) { + return SESSION_CHECK + "?sid=" + id + "&client_secret=" + secret; + } } diff --git a/src/main/groovy/io/kamax/mxisd/controller/v1/remote/RemoteSessionController.java b/src/main/groovy/io/kamax/mxisd/controller/v1/remote/RemoteSessionController.java index ed06f493..25c7517c 100644 --- a/src/main/groovy/io/kamax/mxisd/controller/v1/remote/RemoteSessionController.java +++ b/src/main/groovy/io/kamax/mxisd/controller/v1/remote/RemoteSessionController.java @@ -1,37 +1,59 @@ package io.kamax.mxisd.controller.v1.remote; +import io.kamax.mxisd.config.ViewConfig; +import io.kamax.mxisd.exception.SessionNotValidatedException; import io.kamax.mxisd.session.SessionMananger; +import io.kamax.mxisd.threepid.session.IThreePidSession; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.MediaType; -import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; 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; -@RestController -@CrossOrigin -@RequestMapping(path = RemoteIdentityAPIv1.BASE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) +import static io.kamax.mxisd.controller.v1.remote.RemoteIdentityAPIv1.SESSION_CHECK; +import static io.kamax.mxisd.controller.v1.remote.RemoteIdentityAPIv1.SESSION_REQUEST_TOKEN; + +@Controller public class RemoteSessionController { private Logger log = LoggerFactory.getLogger(RemoteSessionController.class); + @Autowired + private ViewConfig viewCfg; + @Autowired private SessionMananger mgr; - @RequestMapping(path = "/validate/requestToken") + @RequestMapping(path = SESSION_REQUEST_TOKEN) public String requestToken( HttpServletRequest request, @RequestParam String sid, @RequestParam("client_secret") String secret, - @RequestParam String token) { - log.info("Request {}: {}", request.getMethod(), request.getRequestURL(), request.getQueryString()); - mgr.createRemote(sid, secret, token); + Model model + ) { + log.info("Request {}: {}", request.getMethod(), request.getRequestURL()); + IThreePidSession session = mgr.createRemote(sid, secret); + model.addAttribute("checkLink", RemoteIdentityAPIv1.getSessionCheck(session.getId(), session.getSecret())); + return viewCfg.getSession().getRemote().getOnRequest().getSuccess(); + } - return "{}"; + @RequestMapping(path = SESSION_CHECK) + public String check( + HttpServletRequest request, + @RequestParam String sid, + @RequestParam("client_secret") String secret) { + log.info("Request {}: {}", request.getMethod(), request.getRequestURL()); + + try { + mgr.validateRemote(sid, secret); + return viewCfg.getSession().getRemote().getOnCheck().getSuccess(); + } catch (SessionNotValidatedException e) { + return viewCfg.getSession().getRemote().getOnCheck().getFailure(); + } } } diff --git a/src/main/groovy/io/kamax/mxisd/exception/SessionUnknownException.java b/src/main/groovy/io/kamax/mxisd/exception/SessionUnknownException.java new file mode 100644 index 00000000..14cd5fa8 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/exception/SessionUnknownException.java @@ -0,0 +1,33 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.exception; + +public class SessionUnknownException extends MatrixException { + + public SessionUnknownException() { + this("No valid session was found matching that sid and client secret"); + } + + public SessionUnknownException(String error) { + super(200, "M_NO_VALID_SESSION", error); + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/session/SessionMananger.java b/src/main/groovy/io/kamax/mxisd/session/SessionMananger.java index 484abbff..9d0cd524 100644 --- a/src/main/groovy/io/kamax/mxisd/session/SessionMananger.java +++ b/src/main/groovy/io/kamax/mxisd/session/SessionMananger.java @@ -21,10 +21,14 @@ package io.kamax.mxisd.session; import com.google.gson.JsonObject; +import io.kamax.matrix.MatrixID; import io.kamax.matrix.ThreePidMedium; +import io.kamax.matrix._MatrixID; import io.kamax.mxisd.ThreePid; import io.kamax.mxisd.config.MatrixConfig; import io.kamax.mxisd.config.SessionConfig; +import io.kamax.mxisd.controller.v1.io.RequestTokenResponse; +import io.kamax.mxisd.controller.v1.remote.RemoteIdentityAPIv1; import io.kamax.mxisd.exception.*; import io.kamax.mxisd.lookup.ThreePidValidation; import io.kamax.mxisd.lookup.strategy.LookupStrategy; @@ -32,20 +36,28 @@ import io.kamax.mxisd.notification.NotificationManager; import io.kamax.mxisd.storage.IStorage; import io.kamax.mxisd.storage.dao.IThreePidSessionDao; +import io.kamax.mxisd.threepid.session.IThreePidSession; import io.kamax.mxisd.threepid.session.ThreePidSession; +import io.kamax.mxisd.util.GsonParser; import io.kamax.mxisd.util.RestClientUtils; +import org.apache.commons.io.IOUtils; import org.apache.commons.lang.RandomStringUtils; import org.apache.commons.lang.StringUtils; +import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; +import org.apache.http.message.BasicNameValuePair; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; import java.util.List; import java.util.Optional; @@ -91,7 +103,7 @@ private boolean isKnownLocal(ThreePid tpid) { private ThreePidSession getSession(String sid, String secret) { Optional dao = storage.getThreePidSession(sid); if (!dao.isPresent() || !StringUtils.equals(dao.get().getSecret(), secret)) { - throw new InvalidCredentialsException(); + throw new SessionUnknownException(); } return new ThreePidSession(dao.get()); @@ -165,13 +177,28 @@ public String create(String server, ThreePid tpid, String secret, int attempt, S } } - public Optional validate(String sid, String secret, String token) { + public ValidationResult validate(String sid, String secret, String token) { ThreePidSession session = getSession(sid, secret); log.info("Attempting validation for session {} from {}", session.getId(), session.getServer()); + + boolean isLocal = isLocal(session.getThreePid()); + PolicySource policy = cfg.getPolicy().getValidation().forIf(isLocal); + if (!policy.isEnabled()) { + throw new NotAllowedException("Validating " + (isLocal ? "local" : "remote") + " 3PID is not allowed"); + } + session.validate(token); storage.updateThreePidSession(session.getDao()); log.info("Session {} has been validated", session.getId()); - return session.getNextLink(); + + // FIXME definitely doable in a nicer way + ValidationResult r = new ValidationResult(session, policy.toRemote()); + if (!policy.toLocal()) { + r.setNextUrl(RemoteIdentityAPIv1.getRequestToken(sid, secret)); + } else { + session.getNextLink().ifPresent(r::setNextUrl); + } + return r; } public ThreePidValidation getValidated(String sid, String secret) { @@ -179,34 +206,74 @@ public ThreePidValidation getValidated(String sid, String secret) { return new ThreePidValidation(session.getThreePid(), session.getValidationTime()); } - public void bind(String sid, String secret, String mxid) { + public void bind(String sid, String secret, String mxidRaw) { + _MatrixID mxid = new MatrixID(mxidRaw); ThreePidSession session = getSessionIfValidated(sid, secret); - log.info("Accepting bind of {} on session {} from server {}", mxid, session.getId(), session.getServer()); - // TODO perform this if request was proxied + + if (!session.isRemote()) { + log.info("Session {} for {}: MXID {} was bound locally", sid, session.getThreePid(), mxid); + return; + } + + log.info("Session {} for {}: MXID {} bind is remote", sid, session.getThreePid(), mxid); + if (!session.isRemoteValidated()) { + log.error("Session {} for {}: Not validated remotely", sid, session.getThreePid()); + throw new SessionNotValidatedException(); + } + + log.info("Session {} for {}: Performing remote bind", sid, session.getThreePid()); + + UrlEncodedFormEntity entity = new UrlEncodedFormEntity( + Arrays.asList( + new BasicNameValuePair("sid", session.getRemoteId()), + new BasicNameValuePair("client_secret", session.getRemoteSecret()), + new BasicNameValuePair("mxid", mxid.getId()) + ), StandardCharsets.UTF_8); + HttpPost bindReq = new HttpPost(session.getRemoteServer() + "/_matrix/identity/api/v1/3pid/bind"); + bindReq.setEntity(entity); + + try (CloseableHttpResponse response = client.execute(bindReq)) { + int status = response.getStatusLine().getStatusCode(); + if (status < 200 || status >= 300) { + String body = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); + log.error("Session {} for {}: Remote IS {} failed when trying to bind {} for remote session {}\n{}", + sid, session.getThreePid(), session.getRemoteServer(), mxid, session.getRemoteId(), body); + throw new RemoteIdentityServerException(body); + } + + log.error("Session {} for {}: MXID {} was bound remotely", sid, session.getThreePid(), mxid); + } catch (IOException e) { + log.error("Session {} for {}: I/O Error when trying to bind mxid {}", sid, session.getThreePid(), mxid); + throw new RemoteIdentityServerException(e.getMessage()); + } } - public void createRemote(String sid, String secret, String token) { + public IThreePidSession createRemote(String sid, String secret) { ThreePidSession session = getSessionIfValidated(sid, secret); + log.info("Creating remote 3PID session for {} with local session [{}] to {}", session.getThreePid(), sid); boolean isLocal = isLocal(session.getThreePid()); PolicySource policy = cfg.getPolicy().getValidation().forIf(isLocal); if (!policy.isEnabled() || !policy.toRemote()) { throw new NotAllowedException("Validating " + (isLocal ? "local" : "remote") + " 3PID is not allowed"); } + log.info("Remote 3PID is allowed by policy"); List servers = mxCfg.getIdentity().getServers(policy.getToRemote().getServer()); if (servers.isEmpty()) { throw new InternalServerError(); } - String url = IdentityServerUtils.findIsUrlForDomain(servers.get(0)).orElseThrow(InternalServerError::new); + log.info("Will use IS endpoint {}", url); + + String remoteSecret = session.isRemote() ? session.getRemoteSecret() : RandomStringUtils.randomAlphanumeric(16); JsonObject body = new JsonObject(); - body.addProperty("client_secret", RandomStringUtils.randomAlphanumeric(16)); + body.addProperty("client_secret", remoteSecret); body.addProperty(session.getThreePid().getMedium(), session.getThreePid().getAddress()); - body.addProperty("send_attempt", 1); + body.addProperty("send_attempt", session.increaseAndGetRemoteAttempt()); - log.info("Creating remote 3PID session for {} with local session [{}] to {}", session.getThreePid(), sid); + log.info("Requesting remote session with attempt {}", session.getRemoteAttempt()); HttpPost tokenReq = RestClientUtils.post(url + "/_matrix/identity/api/v1/validate/" + session.getThreePid().getMedium() + "/requestToken", body); try (CloseableHttpResponse response = client.execute(tokenReq)) { int status = response.getStatusLine().getStatusCode(); @@ -214,13 +281,66 @@ public void createRemote(String sid, String secret, String token) { throw new RemoteIdentityServerException("Remote identity server returned with status " + status); } - // TODO finish + RequestTokenResponse data = new GsonParser().parse(response, RequestTokenResponse.class); + log.info("Remote Session ID: {}", data.getSid()); + + session.setRemoteData(url, data.getSid(), remoteSecret, 1); + storage.updateThreePidSession(session.getDao()); + log.info("Updated Session {} with remote data", sid); + + return session; } catch (IOException e) { log.warn("Failed to create remote session with {} for {}: {}", url, session.getThreePid(), e.getMessage()); throw new RemoteIdentityServerException(e.getMessage()); } + } + public void validateRemote(String sid, String secret) { + ThreePidSession session = getSessionIfValidated(sid, secret); + if (!session.isRemote()) { + throw new NotAllowedException("Cannot remotely validate a local session"); + } + + log.info("Session {} for {}: Validating remote 3PID session {} on {}", sid, session.getThreePid(), session.getRemoteId(), session.getRemoteServer()); + if (session.isRemoteValidated()) { + log.info("Session {} for {}: Already remotely validated", sid, session.getThreePid()); + return; + } + HttpGet validateReq = new HttpGet(session.getRemoteServer() + "/_matrix/identity/api/v1/3pid/getValidated3pid?sid=" + session.getRemoteId() + "&client_secret=" + session.getRemoteSecret()); + try (CloseableHttpResponse response = client.execute(validateReq)) { + int status = response.getStatusLine().getStatusCode(); + if (status < 200 || status >= 300) { + throw new RemoteIdentityServerException("Remote identity server returned with status " + status); + } + + JsonObject o = new GsonParser().parse(response.getEntity().getContent()); + if (o.has("errcode")) { + String errcode = o.get("errcode").getAsString(); + if (StringUtils.equals("M_SESSION_NOT_VALIDATED", errcode)) { + throw new SessionNotValidatedException(); + } else if (StringUtils.equals("M_NO_VALID_SESSION", errcode)) { + throw new SessionUnknownException(); + } else { + throw new RemoteIdentityServerException("Unknown error while validating Remote 3PID session: " + errcode + " - " + o.get("error").getAsString()); + } + } + + if (o.has("validated_at")) { + ThreePid remoteThreePid = new ThreePid(o.get("medium").getAsString(), o.get("address").getAsString()); + if (session.getThreePid().equals(remoteThreePid)) { // sanity check + throw new InternalServerError("Local 3PID " + session.getThreePid() + " and remote 3PID " + remoteThreePid + " do not match for session " + session.getId()); + } + + log.info("Session {} for {}: Remotely validated successfully", sid, session.getThreePid()); + session.validateRemote(); + storage.updateThreePidSession(session.getDao()); + log.info("Session {} was updated in storage", sid); + } + } catch (IOException e) { + log.warn("Session {} for {}: Failed to validated remotely on {}: {}", sid, session.getThreePid(), session.getRemoteServer(), e.getMessage()); + throw new RemoteIdentityServerException(e.getMessage()); + } } } diff --git a/src/main/groovy/io/kamax/mxisd/session/ValidationResult.java b/src/main/groovy/io/kamax/mxisd/session/ValidationResult.java new file mode 100644 index 00000000..72c987fb --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/session/ValidationResult.java @@ -0,0 +1,54 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.session; + +import io.kamax.mxisd.threepid.session.IThreePidSession; + +import java.util.Optional; + +public class ValidationResult { + + private IThreePidSession session; + private boolean canRemote; + private String nextUrl; + + public ValidationResult(IThreePidSession session, boolean canRemote) { + this.session = session; + this.canRemote = canRemote; + } + + public IThreePidSession getSession() { + return session; + } + + public boolean isCanRemote() { + return canRemote; + } + + public Optional getNextUrl() { + return Optional.ofNullable(nextUrl); + } + + public void setNextUrl(String nextUrl) { + this.nextUrl = nextUrl; + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/storage/dao/IThreePidSessionDao.java b/src/main/groovy/io/kamax/mxisd/storage/dao/IThreePidSessionDao.java index 34da70dd..a907e59a 100644 --- a/src/main/groovy/io/kamax/mxisd/storage/dao/IThreePidSessionDao.java +++ b/src/main/groovy/io/kamax/mxisd/storage/dao/IThreePidSessionDao.java @@ -44,4 +44,16 @@ public interface IThreePidSessionDao { long getValidationTime(); + boolean isRemote(); + + String getRemoteServer(); + + String getRemoteId(); + + String getRemoteSecret(); + + int getRemoteAttempt(); + + boolean isRemoteValidated(); + } diff --git a/src/main/groovy/io/kamax/mxisd/storage/ormlite/dao/ThreePidSessionDao.java b/src/main/groovy/io/kamax/mxisd/storage/ormlite/dao/ThreePidSessionDao.java index c2b18c08..fc74bf69 100644 --- a/src/main/groovy/io/kamax/mxisd/storage/ormlite/dao/ThreePidSessionDao.java +++ b/src/main/groovy/io/kamax/mxisd/storage/ormlite/dao/ThreePidSessionDao.java @@ -61,6 +61,24 @@ public class ThreePidSessionDao implements IThreePidSessionDao { @DatabaseField private long validationTime; + @DatabaseField(canBeNull = false) + private boolean isRemote; + + @DatabaseField + private String remoteServer; + + @DatabaseField + private String remoteId; + + @DatabaseField + private String remoteSecret; + + @DatabaseField + private Integer remoteAttempt; + + @DatabaseField(canBeNull = false) + private boolean isRemoteValidated; + public ThreePidSessionDao() { // stub for ORMLite } @@ -77,7 +95,12 @@ public ThreePidSessionDao(IThreePidSessionDao session) { setToken(session.getToken()); setValidated(session.getValidated()); setValidationTime(session.getValidationTime()); - + setRemote(session.isRemote()); + setRemoteServer(session.getRemoteServer()); + setRemoteId(session.getRemoteId()); + setRemoteSecret(session.getRemoteSecret()); + setRemoteAttempt(session.getRemoteAttempt()); + setRemoteValidated(session.isRemoteValidated()); } public ThreePidSessionDao(ThreePid tpid, String secret) { @@ -180,6 +203,60 @@ public long getValidationTime() { return validationTime; } + @Override + public boolean isRemote() { + return isRemote; + } + + public void setRemote(boolean remote) { + isRemote = remote; + } + + @Override + public String getRemoteServer() { + return remoteServer; + } + + public void setRemoteServer(String remoteServer) { + this.remoteServer = remoteServer; + } + + @Override + public String getRemoteId() { + return remoteId; + } + + public void setRemoteId(String remoteId) { + this.remoteId = remoteId; + } + + @Override + public String getRemoteSecret() { + return remoteSecret; + } + + public void setRemoteSecret(String remoteSecret) { + this.remoteSecret = remoteSecret; + } + + @Override + public int getRemoteAttempt() { + return remoteAttempt; + } + + @Override + public boolean isRemoteValidated() { + return isRemoteValidated; + } + + public void setRemoteValidated(boolean remoteValidated) { + isRemoteValidated = remoteValidated; + } + + public void setRemoteAttempt(int remoteAttempt) { + this.remoteAttempt = remoteAttempt; + } + public void setValidationTime(long validationTime) { this.validationTime = validationTime; } diff --git a/src/main/groovy/io/kamax/mxisd/threepid/notification/email/EmailNotificationGenerator.java b/src/main/groovy/io/kamax/mxisd/threepid/notification/email/EmailNotificationGenerator.java index 78100e31..85739db6 100644 --- a/src/main/groovy/io/kamax/mxisd/threepid/notification/email/EmailNotificationGenerator.java +++ b/src/main/groovy/io/kamax/mxisd/threepid/notification/email/EmailNotificationGenerator.java @@ -26,7 +26,6 @@ import io.kamax.mxisd.config.threepid.medium.EmailConfig; import io.kamax.mxisd.config.threepid.medium.EmailTemplateConfig; import io.kamax.mxisd.controller.v1.IdentityAPIv1; -import io.kamax.mxisd.controller.v1.remote.RemoteIdentityAPIv1; import io.kamax.mxisd.exception.InternalServerError; import io.kamax.mxisd.invitation.IThreePidInviteReply; import io.kamax.mxisd.threepid.session.IThreePidSession; @@ -140,16 +139,14 @@ public String getForRemoteValidation(IThreePidSession session) { log.info("Generating notification content for remote-only 3PID session"); String templateBody = getTemplateAndPopulate(templateCfg.getSession().getValidation().getRemote(), session.getThreePid()); - // FIXME should have a global link builder, specific to mxisd - String nextStepLink = srvCfg.getPublicUrl() + RemoteIdentityAPIv1.BASE + - "/validate/requestToken?sid=" + session.getId() + - "&client_secret=" + session.getSecret() + + // FIXME should have a global link builder, most likely in the SDK? + String validationLink = srvCfg.getPublicUrl() + IdentityAPIv1.BASE + + "/validate/" + session.getThreePid().getMedium() + + "/submitToken?sid=" + session.getId() + "&client_secret=" + session.getSecret() + "&token=" + session.getToken(); - templateBody = templateBody.replace("%SESSION_ID%", session.getId()); - templateBody = templateBody.replace("%SESSION_SECRET%", session.getSecret()); - templateBody = templateBody.replace("%SESSION_TOKEN%", session.getToken()); - templateBody = templateBody.replace("%NEXT_STEP_LINK%", nextStepLink); + templateBody = templateBody.replace("%VALIDATION_LINK%", validationLink); + templateBody = templateBody.replace("%VALIDATION_TOKEN%", session.getToken()); return templateBody; } diff --git a/src/main/groovy/io/kamax/mxisd/threepid/session/IThreePidSession.java b/src/main/groovy/io/kamax/mxisd/threepid/session/IThreePidSession.java index 446d1486..86118556 100644 --- a/src/main/groovy/io/kamax/mxisd/threepid/session/IThreePidSession.java +++ b/src/main/groovy/io/kamax/mxisd/threepid/session/IThreePidSession.java @@ -51,4 +51,16 @@ public interface IThreePidSession { Instant getValidationTime(); + boolean isRemote(); + + String getRemoteServer(); + + String getRemoteId(); + + String getRemoteSecret(); + + int getRemoteAttempt(); + + void setRemoteData(String server, String id, String secret, int attempt); + } diff --git a/src/main/groovy/io/kamax/mxisd/threepid/session/ThreePidSession.java b/src/main/groovy/io/kamax/mxisd/threepid/session/ThreePidSession.java index dee93546..035a60a2 100644 --- a/src/main/groovy/io/kamax/mxisd/threepid/session/ThreePidSession.java +++ b/src/main/groovy/io/kamax/mxisd/threepid/session/ThreePidSession.java @@ -42,6 +42,12 @@ public class ThreePidSession implements IThreePidSession { private int attempt; private Instant validationTimestamp; private boolean isValidated; + private boolean isRemote; + private String remoteServer; + private String remoteId; + private String remoteSecret; + private int remoteAttempt; + private boolean isRemoteValidated; public ThreePidSession(IThreePidSessionDao dao) { this( @@ -58,6 +64,13 @@ public ThreePidSession(IThreePidSessionDao dao) { if (isValidated) { validationTimestamp = Instant.ofEpochMilli(dao.getValidationTime()); } + + isRemote = dao.isRemote(); + remoteServer = dao.getRemoteServer(); + remoteId = dao.getRemoteId(); + remoteSecret = dao.getRemoteSecret(); + remoteAttempt = dao.getRemoteAttempt(); + isRemoteValidated = dao.isRemoteValidated(); } public ThreePidSession(String id, String server, ThreePid tPid, String secret, int attempt, String nextLink, String token) { @@ -129,6 +142,44 @@ public Instant getValidationTime() { return validationTimestamp; } + @Override + public boolean isRemote() { + return isRemote; + } + + @Override + public String getRemoteServer() { + return remoteServer; + } + + @Override + public String getRemoteId() { + return remoteId; + } + + @Override + public String getRemoteSecret() { + return remoteSecret; + } + + @Override + public int getRemoteAttempt() { + return remoteAttempt; + } + + public int increaseAndGetRemoteAttempt() { + return ++remoteAttempt; + } + + @Override + public void setRemoteData(String server, String id, String secret, int attempt) { + this.remoteServer = server; + this.remoteId = id; + this.remoteSecret = secret; + this.attempt = attempt; + this.isRemote = true; + } + @Override public boolean isValidated() { return isValidated; @@ -151,6 +202,14 @@ public synchronized void validate(String token) { isValidated = true; } + public boolean isRemoteValidated() { + return isRemoteValidated; + } + + public void validateRemote() { + this.isRemoteValidated = true; + } + public IThreePidSessionDao getDao() { return new IThreePidSessionDao() { @@ -209,6 +268,36 @@ public long getValidationTime() { return isValidated ? validationTimestamp.toEpochMilli() : 0; } + @Override + public boolean isRemote() { + return isRemote; + } + + @Override + public String getRemoteServer() { + return remoteServer; + } + + @Override + public String getRemoteId() { + return remoteId; + } + + @Override + public String getRemoteSecret() { + return remoteSecret; + } + + @Override + public int getRemoteAttempt() { + return remoteAttempt; + } + + @Override + public boolean isRemoteValidated() { + return isRemoteValidated; + } + }; } diff --git a/src/main/groovy/io/kamax/mxisd/util/GsonParser.java b/src/main/groovy/io/kamax/mxisd/util/GsonParser.java index ec311c80..a83ebaf6 100644 --- a/src/main/groovy/io/kamax/mxisd/util/GsonParser.java +++ b/src/main/groovy/io/kamax/mxisd/util/GsonParser.java @@ -26,6 +26,7 @@ import org.apache.commons.io.IOUtils; import org.apache.http.HttpResponse; +import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; @@ -53,6 +54,10 @@ public JsonObject parse(InputStream stream) throws IOException { return el.getAsJsonObject(); } + public T parse(HttpServletRequest req, Class type) throws IOException { + return gson.fromJson(parse(req.getInputStream()), type); + } + public T parse(HttpResponse res, Class type) throws IOException { return gson.fromJson(parse(res.getEntity().getContent()), type); } diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 839b3789..35c99f9b 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -106,6 +106,24 @@ session.policy.validation: enabled: true server: 'root' +view: + session: + local: + onTokenSubmit: + success: 'session/local/tokenSubmitSuccess' + failure: 'session/local/tokenSubmitFailure' + localRemote: + onTokenSubmit: + success: 'session/localRemote/tokenSubmitSuccess' + failure: 'session/local/tokenSubmitFailure' + remote: + onRequest: + success: 'session/remote/requestSuccess' + failure: 'session/remote/requestFailure' + onCheck: + success: 'session/remote/checkSuccess' + failure: 'session/remote/checkFailure' + storage: backend: 'sqlite' diff --git a/src/main/resources/email/validate-remote-template.eml b/src/main/resources/email/validate-remote-template.eml index 76c64fc4..b4888c62 100644 --- a/src/main/resources/email/validate-remote-template.eml +++ b/src/main/resources/email/validate-remote-template.eml @@ -17,7 +17,7 @@ If you continue, your e-mail address and Matrix ID association will be made publ If you would still like to continue, you will need to: 1. Go to your private Public registration process page: - %NEXT_STEP_LINK% + %VALIDATION_LINK% 2. Follow the registration process of the central Identity Server, usually another email with similar content 3. Once your email address validated with the central Identity Server, click on "Continue" on page of step #1 @@ -81,7 +81,7 @@ pre, code {

If you would still like to continue, you will need to:

    -
  1. Go to your private Public registration process page
  2. +
  3. Go to your private Public registration process page
  4. Follow the registration process of the central Identity Server, usually another email with similar content
  5. Once your email address validated with the central Identity Server, click on "Continue" on page of step #1
  6. If your public association is found by our Identity server, the next step will be given to you.
  7. diff --git a/src/main/resources/templates/session/local/tokenSubmitFailure.html b/src/main/resources/templates/session/local/tokenSubmitFailure.html new file mode 100644 index 00000000..bdeed88c --- /dev/null +++ b/src/main/resources/templates/session/local/tokenSubmitFailure.html @@ -0,0 +1,40 @@ + + + + + Matrix Token Verification + + + +
    +

    Verification failed: you may need to request another verification email

    +
    + + diff --git a/src/main/resources/templates/session/local/tokenSubmitSuccess.html b/src/main/resources/templates/session/local/tokenSubmitSuccess.html new file mode 100644 index 00000000..2bcd7c60 --- /dev/null +++ b/src/main/resources/templates/session/local/tokenSubmitSuccess.html @@ -0,0 +1,40 @@ + + + + + Matrix Token Verification + + + +
    +

    Verification successful: return to your Matrix client to complete the process.

    +
    + + diff --git a/src/main/resources/templates/session/localRemote/tokenSubmitSuccess.html b/src/main/resources/templates/session/localRemote/tokenSubmitSuccess.html new file mode 100644 index 00000000..2dc81a1a --- /dev/null +++ b/src/main/resources/templates/session/localRemote/tokenSubmitSuccess.html @@ -0,0 +1,47 @@ + + + + + Matrix Token Verification + + + +
    +

    Verification successful!

    +

    Your email will remain private and you will only be discoverable with it on your own server, or any related + servers configured by your system admin.
    + If you would like to be globally discoverable, start the process here. +
    If you chose to start the global publication process, wait until it is done before returning to your + client.

    +

    If the remote process is finished, or if you do not wish to start it at this time, you can now return to your + Matrix client to complete the process.

    +
    + + diff --git a/src/main/resources/templates/session/remote/checkFailure.html b/src/main/resources/templates/session/remote/checkFailure.html new file mode 100644 index 00000000..95965fc4 --- /dev/null +++ b/src/main/resources/templates/session/remote/checkFailure.html @@ -0,0 +1,43 @@ + + + + + Matrix global token verification + + + +
    +

    You do not seem to have validated your session with the global server. Please check your messages for one similar + to the one you received initially.
    + Once this is done, click here to continue

    +

    If this problem persists, contact your system administrator with the following info: Reference #ABC

    +
    + + diff --git a/src/main/resources/templates/session/remote/checkSuccess.html b/src/main/resources/templates/session/remote/checkSuccess.html new file mode 100644 index 00000000..13697631 --- /dev/null +++ b/src/main/resources/templates/session/remote/checkSuccess.html @@ -0,0 +1,41 @@ + + + + + Matrix global token verification + + + +
    +

    Verification successful!

    +

    Return to your Matrix client to complete the process and make yourself globally discoverable.

    +
    + + diff --git a/src/main/resources/templates/session/remote/requestFailure.html b/src/main/resources/templates/session/remote/requestFailure.html new file mode 100644 index 00000000..d5463079 --- /dev/null +++ b/src/main/resources/templates/session/remote/requestFailure.html @@ -0,0 +1,42 @@ + + + + + Matrix global token verification + + + +
    +

    The process to be globally discoverable has failed!
    You can try to refresh this page in a few seconds or + minutes.

    +

    If this problem persists, contact your system administrator with the following info: Reference #ABC

    +
    + + diff --git a/src/main/resources/templates/session/remote/requestSuccess.html b/src/main/resources/templates/session/remote/requestSuccess.html new file mode 100644 index 00000000..fbf678b7 --- /dev/null +++ b/src/main/resources/templates/session/remote/requestSuccess.html @@ -0,0 +1,45 @@ + + + + + Matrix global token verification + + + +
    +

    The process to be globally discoverable has started. A verification token has been requested on your behalf.

    +

    You will receive a similar communication as the first verification message.
    + Follow the instructions and come back to this page once you are told to return to your Matrix client or that the + verification was successful.

    +

    Once the validation was successful with the global server, please follow this link + to validate it with us.

    +
    + + From 597fc95cef4f19f8572e25e2f31bc709adf2f74b Mon Sep 17 00:00:00 2001 From: Maxime Dor Date: Sun, 24 Sep 2017 01:49:56 +0200 Subject: [PATCH 10/14] Clean-up --- .../kamax/mxisd/session/SessionMananger.java | 9 +----- src/main/resources/application.yaml | 28 ++++++++++--------- 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/src/main/groovy/io/kamax/mxisd/session/SessionMananger.java b/src/main/groovy/io/kamax/mxisd/session/SessionMananger.java index 9d0cd524..1f410298 100644 --- a/src/main/groovy/io/kamax/mxisd/session/SessionMananger.java +++ b/src/main/groovy/io/kamax/mxisd/session/SessionMananger.java @@ -31,7 +31,6 @@ import io.kamax.mxisd.controller.v1.remote.RemoteIdentityAPIv1; import io.kamax.mxisd.exception.*; import io.kamax.mxisd.lookup.ThreePidValidation; -import io.kamax.mxisd.lookup.strategy.LookupStrategy; import io.kamax.mxisd.matrix.IdentityServerUtils; import io.kamax.mxisd.notification.NotificationManager; import io.kamax.mxisd.storage.IStorage; @@ -72,18 +71,16 @@ public class SessionMananger { private SessionConfig cfg; private MatrixConfig mxCfg; private IStorage storage; - private LookupStrategy lookup; private NotificationManager notifMgr; // FIXME export into central class, set version private CloseableHttpClient client = HttpClients.custom().setUserAgent("mxisd").build(); @Autowired - public SessionMananger(SessionConfig cfg, MatrixConfig mxCfg, IStorage storage, LookupStrategy lookup, NotificationManager notifMgr) { + public SessionMananger(SessionConfig cfg, MatrixConfig mxCfg, IStorage storage, NotificationManager notifMgr) { this.cfg = cfg; this.mxCfg = mxCfg; this.storage = storage; - this.lookup = lookup; this.notifMgr = notifMgr; } @@ -96,10 +93,6 @@ private boolean isLocal(ThreePid tpid) { return StringUtils.equalsIgnoreCase(cfg.getMatrixCfg().getDomain(), domain); } - private boolean isKnownLocal(ThreePid tpid) { - return lookup.findLocal(tpid.getMedium(), tpid.getAddress()).isPresent(); - } - private ThreePidSession getSession(String sid, String secret) { Optional dao = storage.getThreePidSession(sid); if (!dao.isPresent() || !StringUtils.equals(dao.get().getSecret(), secret)) { diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 35c99f9b..3a147f43 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -91,20 +91,22 @@ threepid: local: 'classpath:email/validate-local-template.eml' remote: 'classpath:email/validate-remote-template.eml' -session.policy.validation: - enabled: true - forLocal: - enabled: true - toLocal: true # This should not be changed unless you know exactly the implications! - toRemote: - enabled: true - server: 'root' - forRemote: - enabled: true - toLocal: false - toRemote: +session: + policy: + validation: enabled: true - server: 'root' + forLocal: + enabled: true + toLocal: true + toRemote: + enabled: true + server: 'root' + forRemote: + enabled: true + toLocal: false + toRemote: + enabled: true + server: 'root' view: session: From f93a94ddf109d32589b430799b2e5aa47bb33a39 Mon Sep 17 00:00:00 2001 From: Maxime Dor Date: Sun, 24 Sep 2017 04:47:16 +0200 Subject: [PATCH 11/14] First draft of 3PID sessions/binds manual --- docs/sessions/3pid.md | 323 ++++++++++++++++++++++++++++ src/main/resources/application.yaml | 7 + 2 files changed, 330 insertions(+) create mode 100644 docs/sessions/3pid.md diff --git a/docs/sessions/3pid.md b/docs/sessions/3pid.md new file mode 100644 index 00000000..528e83cf --- /dev/null +++ b/docs/sessions/3pid.md @@ -0,0 +1,323 @@ +# 3PID Sessions +- [Overview](#overview) +- [Purpose](#purpose) +- [Federation](#federation) + - [3PID scope](#3pid-scope) + - [Session scope](#session-scope) +- [Notifications](#notifications) +- [Usage](#usage) + - [Configuration](#configuration) + - [Scenarios](#scenarios) + - [Recommendations](#recommendations) + +## Overview +When adding an email, a phone number or any other kind of 3PID (Third-Party Identifier), +the identity server is called to validate the 3PID. + +Once this 3PID is validated, the Homeserver will publish the user Matrix ID on the Identity Server and +add this 3PID to the Matrix account which initiated the request. + +## Purpose +This serves two purposes: +- Add the 3PID as an administrative/login info for the Homeserver directly +- Publish, or *Bind*, the 3PID so it can be queried from Homeservers and clients when inviting someone in a room +by a 3PID, allowing it to be resolved to a Matrix ID. + +## Federation +Federation is based on the principle that one can get a domain name and serve services and information within that +domain namespace in a way which can be discovered following a specific protocol or specification. + +In the Matrix eco-system, some 3PID can be federated (e.g. emails) while some others cannot (phone numbers). +Also, Matrix users might add 3PIDs that would not point to the Identity server that actually holds the 3PID binding. + +Example: a user from Homeserver `example.org` adds an email `john@gmail.com`. +If a federated lookup was performed, Identity servers would try to find the 3PID bind at the `gmail.com` server, and +not `example.org`. + +To allow global publishing of 3PID bindings to be found anywhere within the current protocol specification, one would +perform a *Remote session* and *Remote bind*, effectively starting a new 3PID session with another Identity server on +behalf of the user. +To ensure lookup works consistency within the current Matrix network, the central Matrix.org Identity Server should be +used to store *remote* sessions and binds. + +On the flip side, at the time of writing, the Matrix specification and the central Matrix.org servers do not allow to +remote a 3PID bind. This means that once a 3PID is published (email, phone number, etc.), it cannot be easily remove +and would require contacting the Matrix.org administrators for each bind individually. +This poses a privacy, control and security concern, especially for groups/corporations that want to keep a tight control +on where such identifiers can be made publicly visible. + +To ensure full control, validation management rely on two concepts: +- The scope of 3PID being validated +- The scope of 3PID sessions that should be possible/offered + +### 3PID scope +3PID can either be scoped as local or remote. + +Local means that they can looked up using federation and that such federation call would end up on the local +Identity Server. +Remote means that they cannot be lookup using federation or that a federation call would not end up on the local +Identity Server. + +Email addresses can either be local or remote 3PID, depending on the domain. If the address is one from the configured +domain in the Identity server, it will be scoped as local. If it is from another domain, it will be as remote. + +Phone number can only be scoped as remote, since there is currently no way to perform DNS queries that would lead back +to the Identity server who validated the phone number. + +### Session scope +Sessions can be scoped as: +- Local only - validate 3PIDs directly, do not allow the creation of 3PID sessions on a remote Identity server. +- Local and Remote - validate 3PIDs directly, offer users to option to also validate and bind 3PID on another server. +- Remote only - validate and bind 3PIDs on another server, no validation or bind done locally. + +--- + +**IMPORTANT NOTE:** mxisd does not store bindings directly. While a user can see its email, phone number or any other +3PID in its settings/profile, it does **NOT** mean it is published anywhere and can be used to invite/search the user. +Identity backends (LDAP, REST, SQL) are the ones holding such data. +If you still want added arbitrary 3PIDs to be discoverable on your local server, you will need to link mxisd to your +synapse DB to make it an Identity backend. + +See the [Scenarios](#scenarios) for more info on how and why. + +## Notifications +3PIDs are validated by sending a pre-formatted message containing a token to that 3PID address, which must be given to the +Identity server that received the request. This is usually done by means of a URL to visit for email or a short number +received by SMS for phone numbers. + +mxisd use two components for this: +- Generator which produces the message to be sent with the necessary information the user needs to validate their session. +- Connector which actually send the notification (e.g. SMTP for email). + +Built-in generators and connectors for supported 3PID types: + +**Email** +Generators: +- Template + +Connectors: +- SMTP + + +## Usage +### Configuration +The following example of configuration (incomplete extract) shows which items are relevant for 3PID sessions. + +**IMPORTANT:** Most configuration items shown have default values and should not be included in your own configuration +file unless you want to specifically overwrite them. +Please refer to the full example config file to see which keys are mandatory and to be included in your configuration. +``` +matrix: + identity: + servers: + root: # Not to be included in config! Already present in default config! + - 'https://matrix.org' + + +threepid: + medium: + email: + connector: 'smtp' + generator: 'template' + connectors: + smtp: + host: '' + port: 587 + tls: 1 + login: '' + password: '' + generators: + template: # Not to be included in config! Already present in default config! + invite: 'classpath:email/invite-template.eml' + session: + validation: + local: 'classpath:email/validate-local-template.eml' + remote: 'classpath:email/validate-remote-template.eml' + +session: + policy: + validation: + enabled: true + forLocal: + enabled: true + toLocal: true + toRemote: + enabled: true + server: 'configExample' # Not to be included in config! Already present in default config! + forRemote: + enabled: true + toLocal: false + toRemote: + enabled: true + server: 'configExample' # Not to be included in config! Already present in default config! +``` + +`matrix.identity.servers` is the namespace to configure arbitrary list of Identity servers with a label as parent key. +In the above example, the list with label `configExample` contains a single server entry pointing to `https://matrix.org`. + +**NOTE:** The server list is set to `root` by default and should typically NOT be included in your config. + +Identity server entry can be of two format: +- URL, bypassing any kind of domain and port discovery +- Domain name as `string`, allowing federated discovery to take place. + +The label can be used in other places of the configuration, allowing you to only declare Identity servers once. + +--- + +`threepid.medium.<3PID>` is the namespace to configure 3PID specific items, not directly tied to any other component of +mxisd. +In the above example, only `email` is defined as 3PID type. + +Each 3PID namespace comes with 4 configuration key allowing you to configure generators and connectors for notifications: +- `connectors` is a configuration namespace to be used for any connector configuration. Child keys represent the unique +ID for each connector. +- `generators` is a configuration namespace to be used for any generator configuration. Child keys represent the unique +ID for each generator. +- `connector` is given the ID of the connector to be used at runtime. +- `generator` is given the ID of the generator to be used at runtime. + +In the above example, emails notifications are generated by the `template` module and sent with the `smtp` module. + +mxisd comes with the following IDs built-in: +**Connectors** +- `smtp` for a basic SMTP connector, attempting STARTLS by default. + +**Generators** +- `template`, loading content from template files, using built-in mxisd templates by default. + +--- + +`session.policy.validation` is the core configuration to control what users configured to use your Identity server +are allowed to do in terms of 3PID sessions. + +The policy is divided contains a global on/off switch for 3PID sessions using `.enabled` +It is also divided into two sections: `forLocal` and `forRemote` which refers to the 3PID scopes. + +Each scope is divided into three parts: +- global on/off switch for 3PID sessions using `.enabled` +- `toLocal` allowing or not local 3PID session validations +- `toRemote` allowing or not remote 3PID session validations and to which server such sessions should be sent. +`.server` takes a Matrix Identity server list label. Only the first server in the list is currently used. + +If both `toLocal` and `toRemote` are enabled, the user will be offered to initiate a remote session once their 3PID +locally validated. + +### Scenarios +It is important to keep in mind that mxisd does not create bindings, irrelevant if a user added a 3PID to their profile. +Instead, when queried for bindings, mxisd will query Identity backends which are responsible to store this kind of information. + +This has the side effect that any 3PID added to a user profile which is NOT within a configured and enabled Identity backend +will simply not be usable for search or invites, **even on the same Homeserver!** +mxisd does not store binds on purpose, as one of its primary goal is to ensure maximum compatibility with federation +and the rest of the Matrix ecosystem is preserved. + +Nonetheless, because mxisd also aims at offering support for tight control over identity data, it is possible to have +such 3PID bindings available for search and invite queries on the local Homeserver by using the `SQL` backend and +configuring it to use the synapse database. Support for `SQLite` and `PostgreSQL` is available. + +See the *Local only* use case for more information on how to configure. + +#### Default +By default, mxisd allows the following: + +| | Local Session | Remote Session | +|----------------|-------|--------| +| **Local 3PID** | Yes | Yes, offered | +| **Remote 3PID** | No, Remote forced | Yes | + +This is usually what people typically expect and will feel natural to users and does not involve further integration. + +This allows to stay in control for e-mail addresses which domain matches your Matrix environment, still making them +discoverable with federation but not recorded in a 3rd party Identity server which is not under your control. +Users still get the possibility to publish globally their address if needed. + +Other e-mail addresses and phone number will be redirected to remote sessions to ensure full compatibility with the Matrix +ecosystem and other federated servers. + +#### Local sessions only +**NOTE:** This does not affect 3PID lookups (queries to find Matrix IDs) which will remain public due to limitation +in the Matrix protocol. + +This configuration ensures maximum confidentiality and privacy. +Typical use cases: +- Private Homeserver, not federated +- Internal Homeserver without direct Internet access +- Custom product based on Matrix which does not federate + +No 3PID will be sent to a remote Identity server and all validation will be performed locally. +On the flip side, people with *Remote* 3PID scopes will not be found from other servers. + +Use the following values: +``` +session: + policy: + validation: + enabled: true + forLocal: + enabled: true + toLocal: true + toRemote: + enabled: false + forRemote: + enabled: true + toLocal: true + toRemote: + enabled: false +``` + +**IMPORTANT**: When using local-only mode, you will also need to link mxisd to synapse if you want user searches and invites to work. +To do so, add/edit the following configuration keys: +``` +sql: + enabled: true + type: 'postgresql' + connection: '' +``` +- `sql.enabled` set to `true` to activate the SQL backend. +- `sql.type` can be set to `sqlite` or `postgresql`, depending on your synapse setup. +- `sql.connection` use a JDBC format which is appened after the `jdbc:type:` connection URI. +Example values for each type: + - `sqlite`: `/path/to/homeserver.db` + - `postgresql`: `//localhost/database?user=synapse&password=synapse` + +#### Remote sessions only +This configuration ensures all 3PID are made public for maximum compatibility and reach within the Matrix ecosystem, at +the cost of confidentiality and privacy. + +Typical use cases: +- Public Homeserver +- Homeserver with registration enabled + +Use the following values: +``` +session: + policy: + validation: + enabled: true + forLocal: + enabled: true + toLocal: false + toRemote: + enabled: false + forRemote: + enabled: true + toLocal: true + toRemote: + enabled: true +``` + +#### Sessions disabled +This configuration would disable 3PID session altogether, preventing users from adding emails and/or phone numbers to +their profiles. +This would be used if mxisd is also performing authentication for the Homeserver, typically with synapse and the +[REST Auth module](https://github.com/kamax-io/matrix-synapse-rest-auth). + +While this feature is not yet ready in the REST auth module, you would use this configuration mode to auto-populate 3PID +at user login and prevent any further add. + +**This mode comes with several important restrictions:** +- This does not prevent users from removing 3PID from their profile. They would be unable to add them back! +- This prevents users from initiating remote session to make their 3PID binds globally visible + +It is therefore recommended to not fully disable sessions but instead restrict specific set of 3PID and Session scopes. diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 3a147f43..b5ebaa65 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -61,7 +61,14 @@ firebase: enabled: false sql: + enabled: false type: 'sqlite' + connection: '' + auth: + enabled: false + identity: + type: 'mxid' + query: "SELECT user_id AS uid FROM user_threepids WHERE medium = ? AND address = ?" forward: servers: From ebb9a6daa093440ece054cbc81caee757fa03da2 Mon Sep 17 00:00:00 2001 From: Maxime Dor Date: Sun, 24 Sep 2017 04:52:24 +0200 Subject: [PATCH 12/14] Fix links and better formatting --- docs/sessions/3pid.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/sessions/3pid.md b/docs/sessions/3pid.md index 528e83cf..71597ed8 100644 --- a/docs/sessions/3pid.md +++ b/docs/sessions/3pid.md @@ -5,10 +5,14 @@ - [3PID scope](#3pid-scope) - [Session scope](#session-scope) - [Notifications](#notifications) + - [Email](#email) - [Usage](#usage) - [Configuration](#configuration) - [Scenarios](#scenarios) - - [Recommendations](#recommendations) + - [Default](#default) + - [Local sessions only](#local-sessions-only) + - [Remote sessions only](#remote-sessions-only) + - [Sessions disabled](#sessions-disabled) ## Overview When adding an email, a phone number or any other kind of 3PID (Third-Party Identifier), @@ -91,7 +95,7 @@ mxisd use two components for this: Built-in generators and connectors for supported 3PID types: -**Email** +### Email Generators: - Template @@ -204,11 +208,11 @@ If both `toLocal` and `toRemote` are enabled, the user will be offered to initia locally validated. ### Scenarios -It is important to keep in mind that mxisd does not create bindings, irrelevant if a user added a 3PID to their profile. +It is important to keep in mind that mxisd does not create bindings, irrelevant if a user added a 3PID to their profile. Instead, when queried for bindings, mxisd will query Identity backends which are responsible to store this kind of information. This has the side effect that any 3PID added to a user profile which is NOT within a configured and enabled Identity backend -will simply not be usable for search or invites, **even on the same Homeserver!** +will simply not be usable for search or invites, **even on the same Homeserver!** mxisd does not store binds on purpose, as one of its primary goal is to ensure maximum compatibility with federation and the rest of the Matrix ecosystem is preserved. @@ -216,7 +220,7 @@ Nonetheless, because mxisd also aims at offering support for tight control over such 3PID bindings available for search and invite queries on the local Homeserver by using the `SQL` backend and configuring it to use the synapse database. Support for `SQLite` and `PostgreSQL` is available. -See the *Local only* use case for more information on how to configure. +See the [Local sessions only](#local-sessions-only) use case for more information on how to configure. #### Default By default, mxisd allows the following: @@ -226,7 +230,7 @@ By default, mxisd allows the following: | **Local 3PID** | Yes | Yes, offered | | **Remote 3PID** | No, Remote forced | Yes | -This is usually what people typically expect and will feel natural to users and does not involve further integration. +This is usually what people expect and will feel natural to users and does not involve further integration. This allows to stay in control for e-mail addresses which domain matches your Matrix environment, still making them discoverable with federation but not recorded in a 3rd party Identity server which is not under your control. From 542c549e4eec4645b1edd978d9f80f7b7913ab8c Mon Sep 17 00:00:00 2001 From: Maxime Dor Date: Sun, 24 Sep 2017 04:53:37 +0200 Subject: [PATCH 13/14] Fix config for remote-only sessions mode --- docs/sessions/3pid.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sessions/3pid.md b/docs/sessions/3pid.md index 71597ed8..2b1d2760 100644 --- a/docs/sessions/3pid.md +++ b/docs/sessions/3pid.md @@ -303,10 +303,10 @@ session: enabled: true toLocal: false toRemote: - enabled: false + enabled: true forRemote: enabled: true - toLocal: true + toLocal: false toRemote: enabled: true ``` From b46d0474117f1dc07a2e6dcabe9b3c4c394be37f Mon Sep 17 00:00:00 2001 From: Maxime Dor Date: Sun, 24 Sep 2017 04:54:48 +0200 Subject: [PATCH 14/14] Add session disabled config --- docs/sessions/3pid.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/sessions/3pid.md b/docs/sessions/3pid.md index 2b1d2760..0f3a0ba6 100644 --- a/docs/sessions/3pid.md +++ b/docs/sessions/3pid.md @@ -325,3 +325,11 @@ at user login and prevent any further add. - This prevents users from initiating remote session to make their 3PID binds globally visible It is therefore recommended to not fully disable sessions but instead restrict specific set of 3PID and Session scopes. + +Use the following values to enable this mode: +``` +session: + policy: + validation: + enabled: false +```