Refactor after first tests against synapse

This commit is contained in:
Maxime Dor
2017-09-21 04:07:13 +02:00
parent 88cefeabbf
commit ace6019197
21 changed files with 544 additions and 144 deletions

View File

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

View File

@@ -23,7 +23,9 @@ package io.kamax.mxisd.controller.v1;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import io.kamax.mxisd.exception.BadRequestException;
import io.kamax.mxisd.exception.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.MissingServletRequestParameterException;
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 @@ public class DefaultExceptionHandler {
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 class DefaultExceptionHandler {
@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()
)
);
}
}

View File

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

View File

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

View File

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

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.exception;
public class MxisdException extends RuntimeException {
}

View File

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

View File

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

View File

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

View File

@@ -55,14 +55,14 @@ public class NotificationManager {
}
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");
}

View File

@@ -22,9 +22,11 @@ package io.kamax.mxisd.session;
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.beans.factory.annotation.Autowired;
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<IThreePidSessionDao> dao = storage.getThreePidSession(sid);
if (!dao.isPresent() || !StringUtils.equals(dao.get().getSecret(), secret)) {
@@ -71,12 +85,17 @@ public class SessionMananger {
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<IThreePidSessionDao> dao = storage.findThreePidSession(tpid, secret);
@@ -94,17 +113,46 @@ public class SessionMananger {
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 class SessionMananger {
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<SingleLookupReply> 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<SingleLookupReply> 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
}
}

View File

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

View File

@@ -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 class ThreePidSessionDao implements IThreePidSessionDao {
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 class ThreePidSessionDao implements IThreePidSessionDao {
this.id = id;
}
@Override
public long getCreationTime() {
return creationTime;
}
public void setCreationTime(long creationTime) {
this.creationTime = creationTime;
}
@Override
public String getServer() {
return server;
@@ -91,24 +113,6 @@ public class ThreePidSessionDao implements IThreePidSessionDao {
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;
@@ -127,6 +131,24 @@ public class ThreePidSessionDao implements IThreePidSessionDao {
this.address = address;
}
@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 getNextLink() {
return nextLink;
@@ -144,4 +166,22 @@ public class ThreePidSessionDao implements IThreePidSessionDao {
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;
}
}

View File

@@ -23,7 +23,9 @@ 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.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 class EmailSmtpConnector implements IEmailConnector {
@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));

View File

@@ -29,7 +29,7 @@ public interface INotificationGenerator {
String getMedium();
String get(IThreePidInviteReply invite);
String getForInvite(IThreePidInviteReply invite);
String getForValidation(IThreePidSession session);

View File

@@ -22,13 +22,19 @@ package io.kamax.mxisd.threepid.notification.email;
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 @@ import java.nio.charset.StandardCharsets;
@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 class EmailNotificationGenerator implements IEmailNotificationGenerator {
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 @@ public class EmailNotificationGenerator implements IEmailNotificationGenerator {
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);
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());
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);
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);
}
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("");
}
}

View File

@@ -42,6 +42,7 @@ public class EmailNotificationHandler implements INotificationHandler {
@Autowired
public EmailNotificationHandler(EmailConfig cfg, List<IEmailNotificationGenerator> generators, List<IEmailConnector> connectors) {
this.cfg = cfg;
generator = generators.stream()
.filter(o -> StringUtils.equals(cfg.getGenerator(), o.getId()))
.findFirst()
@@ -59,13 +60,23 @@ public class EmailNotificationHandler implements INotificationHandler {
}
@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)
);
}
}

View File

@@ -35,6 +35,8 @@ public interface IThreePidSession {
ThreePid getThreePid();
String getSecret();
int getAttempt();
void increaseAttempt();

View File

@@ -53,6 +53,11 @@ public class ThreePidSession implements IThreePidSession {
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 class ThreePidSession implements IThreePidSession {
return id;
}
@Override
public long getCreationTime() {
return timestamp.toEpochMilli();
}
@Override
public String getServer() {
return server;
@@ -189,6 +199,16 @@ public class ThreePidSession implements IThreePidSession {
return token;
}
@Override
public boolean getValidated() {
return isValidated;
}
@Override
public long getValidationTime() {
return isValidated ? validationTimestamp.toEpochMilli() : 0;
}
};
}

View File

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

View File

@@ -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:</p>
<p><a href=%VALIDATION_LINK%">Complete email verification</a></p>
<p><a href="%VALIDATION_LINK%">Complete email verification</a></p>
<p>...or copy this link into your web browser:</p>