diff --git a/src/main/groovy/io/kamax/mxisd/config/SessionConfig.java b/src/main/groovy/io/kamax/mxisd/config/SessionConfig.java index fb51537..fd20b1e 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 1fb0f86..8d03874 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 @@ 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() + ) + ); } } 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 1e107d7..f785e5a 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 d3ca848..5680075 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 0000000..7565b15 --- /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 0000000..e6088f3 --- /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 0000000..b3a1f77 --- /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 0000000..6524ba6 --- /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 0b622df..6138784 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 63d72e4..2178530 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 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"); } diff --git a/src/main/groovy/io/kamax/mxisd/session/SessionMananger.java b/src/main/groovy/io/kamax/mxisd/session/SessionMananger.java index d6072d7..0273062 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 @@ 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 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 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 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 dd5085e..34da70d 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 0b557e1..c2b18c0 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 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; + } + } 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 8124bf7..18e0fae 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 @@ 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)); 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 969a87b..c61dba8 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 b327bea..6ed62c3 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 @@ 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(""); } } 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 e0d9cfe..024c6ec 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 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) + ); } } 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 d5c7dfc..446d148 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 83802da..dee9354 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 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; + } + }; } diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 82c1fec..31e8c59 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 0761712..e5b1346 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: