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; package io.kamax.mxisd.config;
import com.google.gson.Gson;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
@Configuration @Configuration
@ConfigurationProperties("session") @ConfigurationProperties("session")
public class SessionConfig { public class SessionConfig {
private static Logger log = LoggerFactory.getLogger(SessionConfig.class); 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.Gson;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import io.kamax.mxisd.exception.BadRequestException; import io.kamax.mxisd.exception.BadRequestException;
import io.kamax.mxisd.exception.InternalServerError;
import io.kamax.mxisd.exception.MappingAlreadyExistsException; import io.kamax.mxisd.exception.MappingAlreadyExistsException;
import io.kamax.mxisd.exception.MatrixException;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -33,6 +35,8 @@ import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.time.Instant;
@ControllerAdvice @ControllerAdvice
@ResponseBody @ResponseBody
@@ -50,6 +54,23 @@ public class DefaultExceptionHandler {
return gson.toJson(obj); 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) @ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MissingServletRequestParameterException.class) @ExceptionHandler(MissingServletRequestParameterException.class)
public String handle(MissingServletRequestParameterException e) { public String handle(MissingServletRequestParameterException e) {
@@ -72,7 +93,14 @@ public class DefaultExceptionHandler {
@ExceptionHandler(RuntimeException.class) @ExceptionHandler(RuntimeException.class)
public String handle(HttpServletRequest req, RuntimeException e) { public String handle(HttpServletRequest req, RuntimeException e) {
log.error("Unknown error when handling {}", req.getRequestURL(), 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.SessionEmailTokenRequestJson
import io.kamax.mxisd.controller.v1.io.SessionPhoneTokenRequestJson import io.kamax.mxisd.controller.v1.io.SessionPhoneTokenRequestJson
import io.kamax.mxisd.exception.BadRequestException import io.kamax.mxisd.exception.BadRequestException
import io.kamax.mxisd.exception.SessionNotValidatedException
import io.kamax.mxisd.invitation.InvitationManager import io.kamax.mxisd.invitation.InvitationManager
import io.kamax.mxisd.lookup.ThreePidValidation import io.kamax.mxisd.lookup.ThreePidValidation
import io.kamax.mxisd.session.SessionMananger import io.kamax.mxisd.session.SessionMananger
@@ -105,12 +106,10 @@ class SessionController {
@RequestMapping(value = "/3pid/getValidated3pid") @RequestMapping(value = "/3pid/getValidated3pid")
String check(HttpServletRequest request, HttpServletResponse response, String check(HttpServletRequest request, HttpServletResponse response,
@RequestParam String sid, @RequestParam("client_secret") String secret) { @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) try {
if (result.isPresent()) { ThreePidValidation pid = mgr.getValidated(sid, secret)
log.info("requested session was validated")
ThreePidValidation pid = result.get()
JsonObject obj = new JsonObject() JsonObject obj = new JsonObject()
obj.addProperty("medium", pid.getMedium()) obj.addProperty("medium", pid.getMedium())
@@ -118,14 +117,9 @@ class SessionController {
obj.addProperty("validated_at", pid.getValidation().toEpochMilli()) obj.addProperty("validated_at", pid.getValidation().toEpochMilli())
return gson.toJson(obj); return gson.toJson(obj);
} else { } catch (SessionNotValidatedException e) {
log.info("requested session was not validated") log.info("Session {} was requested but has not yet been validated", sid);
throw e;
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)
} }
} }

View File

@@ -20,16 +20,35 @@
package io.kamax.mxisd.exception; package io.kamax.mxisd.exception;
import org.springframework.http.HttpStatus; import org.apache.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
import java.time.Instant; import java.time.Instant;
@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR) public class InternalServerError extends MatrixException {
public class InternalServerError extends RuntimeException {
private String reference = Long.toString(Instant.now().toEpochMilli());
private String internalReason;
public InternalServerError() { 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(); 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) { public void sendForInvite(IThreePidInviteReply invite) {
ensureMedium(invite.getInvite().getMedium()).notify(invite); ensureMedium(invite.getInvite().getMedium()).sendForInvite(invite);
} }
public void sendForValidation(IThreePidSession session) { 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"); 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.matrix.ThreePidMedium;
import io.kamax.mxisd.ThreePid; import io.kamax.mxisd.ThreePid;
import io.kamax.mxisd.config.MatrixConfig;
import io.kamax.mxisd.config.SessionConfig; import io.kamax.mxisd.config.SessionConfig;
import io.kamax.mxisd.exception.InvalidCredentialsException; 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.ThreePidValidation;
import io.kamax.mxisd.lookup.strategy.LookupStrategy; import io.kamax.mxisd.lookup.strategy.LookupStrategy;
import io.kamax.mxisd.notification.NotificationManager; import io.kamax.mxisd.notification.NotificationManager;
@@ -39,7 +41,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.Optional; import java.util.Optional;
import java.util.UUID;
@Component @Component
public class SessionMananger { public class SessionMananger {
@@ -52,13 +53,26 @@ public class SessionMananger {
private NotificationManager notifMgr; private NotificationManager notifMgr;
@Autowired @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.cfg = cfg;
this.storage = storage; this.storage = storage;
this.lookup = lookup; this.lookup = lookup;
this.notifMgr = notifMgr; 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) { private ThreePidSession getSession(String sid, String secret) {
Optional<IThreePidSessionDao> dao = storage.getThreePidSession(sid); Optional<IThreePidSessionDao> dao = storage.getThreePidSession(sid);
if (!dao.isPresent() || !StringUtils.equals(dao.get().getSecret(), secret)) { if (!dao.isPresent() || !StringUtils.equals(dao.get().getSecret(), secret)) {
@@ -71,12 +85,17 @@ public class SessionMananger {
private ThreePidSession getSessionIfValidated(String sid, String secret) { private ThreePidSession getSessionIfValidated(String sid, String secret) {
ThreePidSession session = getSession(sid, secret); ThreePidSession session = getSession(sid, secret);
if (!session.isValidated()) { if (!session.isValidated()) {
throw new IllegalStateException("Session " + sid + " has not been validated"); throw new SessionNotValidatedException();
} }
return session; return session;
} }
public String create(String server, ThreePid tpid, String secret, int attempt, String nextLink) { 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) { synchronized (this) {
log.info("Server {} is asking to create session for {} (Attempt #{}) - Next link: {}", server, tpid, attempt, nextLink); log.info("Server {} is asking to create session for {} (Attempt #{}) - Next link: {}", server, tpid, attempt, nextLink);
Optional<IThreePidSessionDao> dao = storage.findThreePidSession(tpid, secret); Optional<IThreePidSessionDao> dao = storage.findThreePidSession(tpid, secret);
@@ -94,17 +113,46 @@ public class SessionMananger {
return session.getId(); return session.getId();
} else { } else {
log.info("No existing session for {}", tpid); 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; String sessionId;
do { do {
sessionId = UUID.randomUUID().toString().replace("-", ""); sessionId = Long.toString(System.currentTimeMillis());
} while (storage.getThreePidSession(sessionId).isPresent()); } while (storage.getThreePidSession(sessionId).isPresent());
String token = RandomStringUtils.randomNumeric(6); String token = RandomStringUtils.randomNumeric(6);
ThreePidSession session = new ThreePidSession(sessionId, server, tpid, secret, attempt, nextLink, token); ThreePidSession session = new ThreePidSession(sessionId, server, tpid, secret, attempt, nextLink, token);
log.info("Generated new session {} to validate {} from server {}", sessionId, tpid, server); 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); notifMgr.sendForValidation(session);
log.info("Sent validation notification to {}", tpid); } else {
log.info("Session {} for {}: sending remote-only validation notification", sessionId, tpid);
notifMgr.sendforRemoteValidation(session);
}
}
storage.insertThreePidSession(session.getDao()); storage.insertThreePidSession(session.getDao());
log.info("Stored session {}", sessionId, tpid, server); log.info("Stored session {}", sessionId, tpid, server);
@@ -130,65 +178,8 @@ public class SessionMananger {
public void bind(String sid, String secret, String mxid) { public void bind(String sid, String secret, String mxid) {
ThreePidSession session = getSessionIfValidated(sid, secret); ThreePidSession session = getSessionIfValidated(sid, secret);
log.info("Attempting bind of {} on session {} from server {}", mxid, session.getId(), session.getServer()); log.info("Accepting bind of {} on session {} from server {}", mxid, session.getId(), session.getServer());
// TODO perform this if request was proxied
// 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);
}
}
} }
} }

View File

@@ -24,6 +24,8 @@ public interface IThreePidSessionDao {
String getId(); String getId();
long getCreationTime();
String getServer(); String getServer();
String getMedium(); String getMedium();
@@ -38,4 +40,8 @@ public interface IThreePidSessionDao {
String getToken(); String getToken();
boolean getValidated();
long getValidationTime();
} }

View File

@@ -31,6 +31,9 @@ public class ThreePidSessionDao implements IThreePidSessionDao {
@DatabaseField(id = true) @DatabaseField(id = true)
private String id; private String id;
@DatabaseField(canBeNull = false)
private long creationTime;
@DatabaseField(canBeNull = false) @DatabaseField(canBeNull = false)
private String server; private String server;
@@ -52,12 +55,19 @@ public class ThreePidSessionDao implements IThreePidSessionDao {
@DatabaseField(canBeNull = false) @DatabaseField(canBeNull = false)
private String token; private String token;
@DatabaseField
private boolean validated;
@DatabaseField
private long validationTime;
public ThreePidSessionDao() { public ThreePidSessionDao() {
// stub for ORMLite // stub for ORMLite
} }
public ThreePidSessionDao(IThreePidSessionDao session) { public ThreePidSessionDao(IThreePidSessionDao session) {
setId(session.getId()); setId(session.getId());
setCreationTime(session.getCreationTime());
setServer(session.getServer()); setServer(session.getServer());
setMedium(session.getMedium()); setMedium(session.getMedium());
setAddress(session.getAddress()); setAddress(session.getAddress());
@@ -65,6 +75,9 @@ public class ThreePidSessionDao implements IThreePidSessionDao {
setAttempt(session.getAttempt()); setAttempt(session.getAttempt());
setNextLink(session.getNextLink()); setNextLink(session.getNextLink());
setToken(session.getToken()); setToken(session.getToken());
setValidated(session.getValidated());
setValidationTime(session.getValidationTime());
} }
public ThreePidSessionDao(ThreePid tpid, String secret) { public ThreePidSessionDao(ThreePid tpid, String secret) {
@@ -82,6 +95,15 @@ public class ThreePidSessionDao implements IThreePidSessionDao {
this.id = id; this.id = id;
} }
@Override
public long getCreationTime() {
return creationTime;
}
public void setCreationTime(long creationTime) {
this.creationTime = creationTime;
}
@Override @Override
public String getServer() { public String getServer() {
return server; return server;
@@ -91,24 +113,6 @@ public class ThreePidSessionDao implements IThreePidSessionDao {
this.server = 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 @Override
public String getMedium() { public String getMedium() {
return medium; return medium;
@@ -127,6 +131,24 @@ public class ThreePidSessionDao implements IThreePidSessionDao {
this.address = address; 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 @Override
public String getNextLink() { public String getNextLink() {
return nextLink; return nextLink;
@@ -144,4 +166,22 @@ public class ThreePidSessionDao implements IThreePidSessionDao {
public void setToken(String token) { public void setToken(String token) {
this.token = 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 com.sun.mail.smtp.SMTPTransport;
import io.kamax.matrix.ThreePidMedium; import io.kamax.matrix.ThreePidMedium;
import io.kamax.mxisd.config.threepid.connector.EmailSmtpConfig; import io.kamax.mxisd.config.threepid.connector.EmailSmtpConfig;
import io.kamax.mxisd.exception.InternalServerError;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@@ -64,6 +66,10 @@ public class EmailSmtpConnector implements IEmailConnector {
@Override @Override
public void send(String senderAddress, String senderName, String recipient, String content) { public void send(String senderAddress, String senderName, String recipient, String content) {
if (StringUtils.isBlank(content)) {
throw new InternalServerError("Notification content is empty");
}
try { try {
InternetAddress sender = new InternetAddress(senderAddress, senderName); InternetAddress sender = new InternetAddress(senderAddress, senderName);
MimeMessage msg = new MimeMessage(session, IOUtils.toInputStream(content, StandardCharsets.UTF_8)); MimeMessage msg = new MimeMessage(session, IOUtils.toInputStream(content, StandardCharsets.UTF_8));

View File

@@ -29,7 +29,7 @@ public interface INotificationGenerator {
String getMedium(); String getMedium();
String get(IThreePidInviteReply invite); String getForInvite(IThreePidInviteReply invite);
String getForValidation(IThreePidSession session); 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.ThreePid;
import io.kamax.mxisd.config.MatrixConfig; 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.EmailConfig;
import io.kamax.mxisd.config.threepid.medium.EmailTemplateConfig; 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.invitation.IThreePidInviteReply;
import io.kamax.mxisd.threepid.session.IThreePidSession; import io.kamax.mxisd.threepid.session.IThreePidSession;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.WordUtils; import org.apache.commons.lang.WordUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -41,17 +47,22 @@ import java.nio.charset.StandardCharsets;
@Component @Component
public class EmailNotificationGenerator implements IEmailNotificationGenerator { public class EmailNotificationGenerator implements IEmailNotificationGenerator {
private Logger log = LoggerFactory.getLogger(EmailNotificationGenerator.class);
private EmailConfig cfg; private EmailConfig cfg;
private EmailTemplateConfig templateCfg; private EmailTemplateConfig templateCfg;
private MatrixConfig mxCfg; private MatrixConfig mxCfg;
private ServerConfig srvCfg;
@Autowired
private ApplicationContext app; private ApplicationContext app;
@Autowired // FIXME ApplicationContext shouldn't be injected, find another way from config (?) @Autowired
public EmailNotificationGenerator(EmailConfig cfg, EmailTemplateConfig templateCfg, MatrixConfig mxCfg, ApplicationContext app) { public EmailNotificationGenerator(EmailTemplateConfig templateCfg, EmailConfig cfg, MatrixConfig mxCfg, ServerConfig srvCfg) {
this.cfg = cfg; this.cfg = cfg;
this.templateCfg = templateCfg; this.templateCfg = templateCfg;
this.mxCfg = mxCfg; this.mxCfg = mxCfg;
this.app = app; this.srvCfg = srvCfg;
} }
@Override @Override
@@ -59,10 +70,14 @@ public class EmailNotificationGenerator implements IEmailNotificationGenerator {
return "template"; return "template";
} }
private String getTemplateContent(String location) throws IOException { private String getTemplateContent(String location) {
try {
InputStream is = StringUtils.startsWith(location, "classpath:") ? InputStream is = StringUtils.startsWith(location, "classpath:") ?
app.getResource(location).getInputStream() : new FileInputStream(location); app.getResource(location).getInputStream() : new FileInputStream(location);
return IOUtils.toString(is, StandardCharsets.UTF_8); 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) { private String populateCommon(String content, ThreePid recipient) {
@@ -77,13 +92,12 @@ public class EmailNotificationGenerator implements IEmailNotificationGenerator {
return content; return content;
} }
private String getTemplateAndPopulate(String location, ThreePid recipient) throws IOException { private String getTemplateAndPopulate(String location, ThreePid recipient) {
return populateCommon(getTemplateContent(location), recipient); return populateCommon(getTemplateContent(location), recipient);
} }
@Override @Override
public String get(IThreePidInviteReply invite) { public String getForInvite(IThreePidInviteReply invite) {
try {
ThreePid tpid = new ThreePid(invite.getInvite().getMedium(), invite.getInvite().getAddress()); ThreePid tpid = new ThreePid(invite.getInvite().getMedium(), invite.getInvite().getAddress());
String templateBody = getTemplateAndPopulate(templateCfg.getInvite(), tpid); String templateBody = getTemplateAndPopulate(templateCfg.getInvite(), tpid);
@@ -102,19 +116,27 @@ public class EmailNotificationGenerator implements IEmailNotificationGenerator {
templateBody = templateBody.replace("%ROOM_NAME_OR_ID%", roomNameOrId); templateBody = templateBody.replace("%ROOM_NAME_OR_ID%", roomNameOrId);
return templateBody; return templateBody;
} catch (IOException e) {
throw new RuntimeException("Unable to read template file", e);
}
} }
@Override @Override
public String getForValidation(IThreePidSession session) { 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 @Override
public String getForRemotePublishingValidation(IThreePidSession session) { public String getForRemotePublishingValidation(IThreePidSession session) {
return null; throw new NotImplementedException("");
} }
} }

View File

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

View File

@@ -53,6 +53,11 @@ public class ThreePidSession implements IThreePidSession {
dao.getNextLink(), dao.getNextLink(),
dao.getToken() 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) { 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; return id;
} }
@Override
public long getCreationTime() {
return timestamp.toEpochMilli();
}
@Override @Override
public String getServer() { public String getServer() {
return server; return server;
@@ -189,6 +199,16 @@ public class ThreePidSession implements IThreePidSession {
return token; 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: session:
validation: 'classpath:email/validate-template.eml' 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: storage:
backend: 'sqlite' backend: 'sqlite'

View File

@@ -45,7 +45,7 @@ body {
If this was you who made this request, you may use the following link to If this was you who made this request, you may use the following link to
complete the verification of your email address:</p> 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> <p>...or copy this link into your web browser:</p>