diff --git a/src/main/groovy/io/kamax/mxisd/ThreePid.java b/src/main/groovy/io/kamax/mxisd/ThreePid.java index d51ecd6..3a0b6bf 100644 --- a/src/main/groovy/io/kamax/mxisd/ThreePid.java +++ b/src/main/groovy/io/kamax/mxisd/ThreePid.java @@ -26,6 +26,10 @@ public class ThreePid { private String medium; private String address; + public ThreePid(ThreePid tpid) { + this(tpid.getMedium(), tpid.getAddress()); + } + public ThreePid(String medium, String address) { this.medium = medium; this.address = address; diff --git a/src/main/groovy/io/kamax/mxisd/config/invite/medium/EmailInviteConfig.java b/src/main/groovy/io/kamax/mxisd/config/invite/medium/EmailInviteConfig.java index 76b79c2..a244aff 100644 --- a/src/main/groovy/io/kamax/mxisd/config/invite/medium/EmailInviteConfig.java +++ b/src/main/groovy/io/kamax/mxisd/config/invite/medium/EmailInviteConfig.java @@ -20,12 +20,9 @@ package io.kamax.mxisd.config.invite.medium; -import io.kamax.mxisd.config.MatrixConfig; import org.apache.commons.lang.StringUtils; -import org.apache.commons.lang.WordUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; @@ -38,33 +35,8 @@ public class EmailInviteConfig { private Logger log = LoggerFactory.getLogger(EmailInviteConfig.class); - private MatrixConfig mxCfg; - - private String from; - private String name; private String template; - @Autowired - public EmailInviteConfig(MatrixConfig mxCfg) { - this.mxCfg = mxCfg; - } - - public String getFrom() { - return from; - } - - public void setFrom(String from) { - this.from = from; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - public String getTemplate() { return template; } @@ -76,12 +48,6 @@ public class EmailInviteConfig { @PostConstruct public void build() { log.info("--- E-mail invites config ---"); - log.info("From: {}", getFrom()); - - if (StringUtils.isBlank(getName())) { - setName(WordUtils.capitalize(mxCfg.getDomain()) + " Identity Server"); - } - log.info("Name: {}", getName()); if (!StringUtils.startsWith(getTemplate(), "classpath:")) { if (StringUtils.isBlank(getTemplate())) { diff --git a/src/main/groovy/io/kamax/mxisd/config/threepid/medium/EmailConfig.java b/src/main/groovy/io/kamax/mxisd/config/threepid/medium/EmailConfig.java new file mode 100644 index 0000000..2d47bf3 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/config/threepid/medium/EmailConfig.java @@ -0,0 +1,77 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.config.threepid.medium; + +import io.kamax.mxisd.config.MatrixConfig; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.WordUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import javax.annotation.PostConstruct; + +@Configuration +@ConfigurationProperties("threepid.medium.email") +public class EmailConfig { + + private Logger log = LoggerFactory.getLogger(EmailConfig.class); + + private MatrixConfig mxCfg; + + private String from; + private String name; + + @Autowired + public EmailConfig(MatrixConfig mxCfg) { + this.mxCfg = mxCfg; + } + + public String getFrom() { + return from; + } + + public void setFrom(String from) { + this.from = from; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @PostConstruct + public void build() { + log.info("--- E-mail config ---"); + log.info("From: {}", getFrom()); + + if (StringUtils.isBlank(getName())) { + setName(WordUtils.capitalize(mxCfg.getDomain()) + " Identity Server"); + } + log.info("Name: {}", getName()); + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/controller/v1/SessionController.groovy b/src/main/groovy/io/kamax/mxisd/controller/v1/SessionController.groovy index 2df1de0..93181b4 100644 --- a/src/main/groovy/io/kamax/mxisd/controller/v1/SessionController.groovy +++ b/src/main/groovy/io/kamax/mxisd/controller/v1/SessionController.groovy @@ -22,6 +22,8 @@ package io.kamax.mxisd.controller.v1 import com.google.gson.Gson import com.google.gson.JsonObject +import io.kamax.matrix.ThreePidMedium +import io.kamax.mxisd.ThreePid import io.kamax.mxisd.controller.v1.io.SessionEmailTokenRequestJson import io.kamax.mxisd.controller.v1.io.SessionPhoneTokenRequestJson import io.kamax.mxisd.exception.BadRequestException @@ -29,7 +31,6 @@ import io.kamax.mxisd.invitation.InvitationManager import io.kamax.mxisd.lookup.ThreePidValidation import io.kamax.mxisd.mapping.MappingManager import org.apache.commons.io.IOUtils -import org.apache.commons.lang.StringUtils import org.apache.http.HttpStatus import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -62,16 +63,25 @@ class SessionController { @RequestMapping(value = "/validate/{medium}/requestToken") String init(HttpServletRequest request, HttpServletResponse response, @PathVariable String medium) { - log.info("Requested: {}", request.getRequestURL(), request.getQueryString()) - - if (StringUtils.equals("email", medium)) { + log.info("Request {}: {}", request.getMethod(), request.getRequestURL(), request.getQueryString()) + if (ThreePidMedium.Email.is(medium)) { SessionEmailTokenRequestJson req = fromJson(request, SessionEmailTokenRequestJson.class) - return gson.toJson(new Sid(mgr.create(req))) + return gson.toJson(new Sid(mgr.create( + request.getRemoteHost(), + new ThreePid(req.getMedium(), req.getValue()), + req.getSecret(), + req.getAttempt(), + req.getNextLink()))); } - if (StringUtils.equals("msisdn", medium)) { + if (ThreePidMedium.PhoneNumber) { SessionPhoneTokenRequestJson req = fromJson(request, SessionPhoneTokenRequestJson.class) - return gson.toJson(new Sid(mgr.create(req))) + return gson.toJson(new Sid(mgr.create( + request.getRemoteHost(), + new ThreePid(req.getMedium(), req.getValue()), + req.getSecret(), + req.getAttempt(), + req.getNextLink()))); } JsonObject obj = new JsonObject(); diff --git a/src/main/groovy/io/kamax/mxisd/controller/v1/io/GenericTokenRequestJson.java b/src/main/groovy/io/kamax/mxisd/controller/v1/io/GenericTokenRequestJson.java index f7d1b35..0812505 100644 --- a/src/main/groovy/io/kamax/mxisd/controller/v1/io/GenericTokenRequestJson.java +++ b/src/main/groovy/io/kamax/mxisd/controller/v1/io/GenericTokenRequestJson.java @@ -20,13 +20,11 @@ package io.kamax.mxisd.controller.v1.io; -import io.kamax.mxisd.mapping.MappingSession; - -public abstract class GenericTokenRequestJson implements MappingSession { +public abstract class GenericTokenRequestJson { private String client_secret; private int send_attempt; - private String id_server; + private String next_link; public String getSecret() { return client_secret; @@ -36,8 +34,8 @@ public abstract class GenericTokenRequestJson implements MappingSession { return send_attempt; } - public String getServer() { - return id_server; + public String getNextLink() { + return next_link; } } diff --git a/src/main/groovy/io/kamax/mxisd/controller/v1/io/SessionEmailTokenRequestJson.java b/src/main/groovy/io/kamax/mxisd/controller/v1/io/SessionEmailTokenRequestJson.java index 3901045..527f1e7 100644 --- a/src/main/groovy/io/kamax/mxisd/controller/v1/io/SessionEmailTokenRequestJson.java +++ b/src/main/groovy/io/kamax/mxisd/controller/v1/io/SessionEmailTokenRequestJson.java @@ -24,12 +24,10 @@ public class SessionEmailTokenRequestJson extends GenericTokenRequestJson { private String email; - @Override public String getMedium() { return "email"; } - @Override public String getValue() { return email; } diff --git a/src/main/groovy/io/kamax/mxisd/controller/v1/io/SessionPhoneTokenRequestJson.java b/src/main/groovy/io/kamax/mxisd/controller/v1/io/SessionPhoneTokenRequestJson.java index b66e881..e2e82c2 100644 --- a/src/main/groovy/io/kamax/mxisd/controller/v1/io/SessionPhoneTokenRequestJson.java +++ b/src/main/groovy/io/kamax/mxisd/controller/v1/io/SessionPhoneTokenRequestJson.java @@ -31,12 +31,10 @@ public class SessionPhoneTokenRequestJson extends GenericTokenRequestJson { private String country; private String phone_number; - @Override public String getMedium() { return "msisdn"; } - @Override public String getValue() { try { Phonenumber.PhoneNumber num = phoneUtil.parse(phone_number, country); diff --git a/src/main/groovy/io/kamax/mxisd/exception/InternalServerError.java b/src/main/groovy/io/kamax/mxisd/exception/InternalServerError.java new file mode 100644 index 0000000..d3ca848 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/exception/InternalServerError.java @@ -0,0 +1,35 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +import java.time.Instant; + +@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR) +public class InternalServerError extends RuntimeException { + + public InternalServerError() { + super("An internal server error occured. If this error persists, please contact support with reference #" + Instant.now().toEpochMilli()); + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/exception/InvalidCredentialsException.java b/src/main/groovy/io/kamax/mxisd/exception/InvalidCredentialsException.java new file mode 100644 index 0000000..bbd66df --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/exception/InvalidCredentialsException.java @@ -0,0 +1,33 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(value = HttpStatus.FORBIDDEN) +public class InvalidCredentialsException extends RuntimeException { + + public InvalidCredentialsException() { + super("Supplied credentials are invalid"); + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/exception/ObjectNotFoundException.java b/src/main/groovy/io/kamax/mxisd/exception/ObjectNotFoundException.java new file mode 100644 index 0000000..acfb0c3 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/exception/ObjectNotFoundException.java @@ -0,0 +1,33 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(value = HttpStatus.NOT_FOUND) +public class ObjectNotFoundException extends RuntimeException { + + public ObjectNotFoundException(String type, String id) { + super(type + " with ID " + id + " does not exist"); + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/invitation/generator/EmailInviteContentGenerator.java b/src/main/groovy/io/kamax/mxisd/invitation/generator/EmailInviteContentGenerator.java index bcd1e44..0bce14d 100644 --- a/src/main/groovy/io/kamax/mxisd/invitation/generator/EmailInviteContentGenerator.java +++ b/src/main/groovy/io/kamax/mxisd/invitation/generator/EmailInviteContentGenerator.java @@ -23,6 +23,7 @@ package io.kamax.mxisd.invitation.generator; import io.kamax.matrix.ThreePidMedium; import io.kamax.mxisd.config.MatrixConfig; import io.kamax.mxisd.config.invite.medium.EmailInviteConfig; +import io.kamax.mxisd.config.threepid.medium.EmailConfig; import io.kamax.mxisd.invitation.IThreePidInviteReply; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; @@ -38,13 +39,15 @@ import java.nio.charset.StandardCharsets; @Component public class EmailInviteContentGenerator implements IInviteContentGenerator { - private EmailInviteConfig cfg; + private EmailConfig cfg; + private EmailInviteConfig invCfg; private MatrixConfig mxCfg; private ApplicationContext app; - @Autowired - public EmailInviteContentGenerator(EmailInviteConfig cfg, MatrixConfig mxCfg, ApplicationContext app) { + @Autowired // FIXME ApplicationContext shouldn't be injected, find another way from config (?) + public EmailInviteContentGenerator(EmailConfig cfg, EmailInviteConfig invCfg, MatrixConfig mxCfg, ApplicationContext app) { this.cfg = cfg; + this.invCfg = invCfg; this.mxCfg = mxCfg; this.app = app; } @@ -68,8 +71,8 @@ public class EmailInviteContentGenerator implements IInviteContentGenerator { String roomNameOrId = StringUtils.defaultIfBlank(roomName, invite.getInvite().getRoomId()); String templateBody = IOUtils.toString( - StringUtils.startsWith(cfg.getTemplate(), "classpath:") ? - app.getResource(cfg.getTemplate()).getInputStream() : new FileInputStream(cfg.getTemplate()), + StringUtils.startsWith(invCfg.getTemplate(), "classpath:") ? + app.getResource(invCfg.getTemplate()).getInputStream() : new FileInputStream(invCfg.getTemplate()), StandardCharsets.UTF_8); templateBody = templateBody.replace("%DOMAIN%", mxCfg.getDomain()); templateBody = templateBody.replace("%DOMAIN_PRETTY%", domainPretty); diff --git a/src/main/groovy/io/kamax/mxisd/lookup/ThreePidValidation.java b/src/main/groovy/io/kamax/mxisd/lookup/ThreePidValidation.java index 8d3b4a0..206d9d4 100644 --- a/src/main/groovy/io/kamax/mxisd/lookup/ThreePidValidation.java +++ b/src/main/groovy/io/kamax/mxisd/lookup/ThreePidValidation.java @@ -28,8 +28,8 @@ public class ThreePidValidation extends ThreePid { private Instant validation; - public ThreePidValidation(String medium, String address, Instant validation) { - super(medium, address); + public ThreePidValidation(ThreePid tpid, Instant validation) { + super(tpid); this.validation = validation; } diff --git a/src/main/groovy/io/kamax/mxisd/lookup/strategy/LookupStrategy.groovy b/src/main/groovy/io/kamax/mxisd/lookup/strategy/LookupStrategy.groovy index 37e4327..44b67f0 100644 --- a/src/main/groovy/io/kamax/mxisd/lookup/strategy/LookupStrategy.groovy +++ b/src/main/groovy/io/kamax/mxisd/lookup/strategy/LookupStrategy.groovy @@ -32,6 +32,10 @@ interface LookupStrategy { Optional find(String medium, String address, boolean recursive) + Optional findLocal(String medium, String address); + + Optional findRemote(String medium, String address); + Optional find(SingleLookupRequest request) Optional findRecursive(SingleLookupRequest request) diff --git a/src/main/groovy/io/kamax/mxisd/lookup/strategy/RecursivePriorityLookupStrategy.groovy b/src/main/groovy/io/kamax/mxisd/lookup/strategy/RecursivePriorityLookupStrategy.groovy index e4ef776..048dd21 100644 --- a/src/main/groovy/io/kamax/mxisd/lookup/strategy/RecursivePriorityLookupStrategy.groovy +++ b/src/main/groovy/io/kamax/mxisd/lookup/strategy/RecursivePriorityLookupStrategy.groovy @@ -118,17 +118,44 @@ class RecursivePriorityLookupStrategy implements LookupStrategy, InitializingBea }).collect(Collectors.toList()) } - @Override - Optional find(String medium, String address, boolean recursive) { + List getRemoteProviders() { + return providers.stream().filter(new Predicate() { + @Override + boolean test(IThreePidProvider iThreePidProvider) { + return iThreePidProvider.isEnabled() && !iThreePidProvider.isLocal() + } + }).collect(Collectors.toList()) + } + + private static SingleLookupRequest build(String medium, String address) { SingleLookupRequest req = new SingleLookupRequest(); req.setType(medium) req.setThreePid(address) req.setRequester("Internal") - return find(req, recursive) + return req; + } + + @Override + Optional find(String medium, String address, boolean recursive) { + return find(build(medium, address), recursive) + } + + @Override + Optional findLocal(String medium, String address) { + return find(build(medium, address), getLocalProviders()) + } + + @Override + Optional findRemote(String medium, String address) { + return find(build(medium, address), getRemoteProviders()) } Optional find(SingleLookupRequest request, boolean forceRecursive) { - for (IThreePidProvider provider : listUsableProviders(request, forceRecursive)) { + return find(request, listUsableProviders(request, forceRecursive)); + } + + Optional find(SingleLookupRequest request, List providers) { + for (IThreePidProvider provider : providers) { Optional lookupDataOpt = provider.find(request) if (lookupDataOpt.isPresent()) { return lookupDataOpt diff --git a/src/main/groovy/io/kamax/mxisd/mapping/MappingManager.java b/src/main/groovy/io/kamax/mxisd/mapping/MappingManager.java index 1f8c3ec..42e53b0 100644 --- a/src/main/groovy/io/kamax/mxisd/mapping/MappingManager.java +++ b/src/main/groovy/io/kamax/mxisd/mapping/MappingManager.java @@ -20,155 +20,167 @@ package io.kamax.mxisd.mapping; -import io.kamax.mxisd.exception.BadRequestException; +import io.kamax.matrix.ThreePidMedium; +import io.kamax.mxisd.ThreePid; +import io.kamax.mxisd.exception.InvalidCredentialsException; +import io.kamax.mxisd.lookup.SingleLookupReply; import io.kamax.mxisd.lookup.ThreePidValidation; +import io.kamax.mxisd.lookup.strategy.LookupStrategy; +import io.kamax.mxisd.storage.IStorage; +import io.kamax.mxisd.storage.dao.IThreePidSessionDao; +import io.kamax.mxisd.threepid.session.ThreePidSession; +import org.apache.commons.lang.RandomStringUtils; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.*; +import java.util.Optional; +import java.util.UUID; @Component public class MappingManager { private Logger log = LoggerFactory.getLogger(MappingManager.class); - private Map sessions = new HashMap<>(); - private Timer cleaner; + private IStorage storage; + private LookupStrategy lookup; - MappingManager() { - cleaner = new Timer(); - cleaner.schedule(new TimerTask() { - @Override - public void run() { - List sList = new ArrayList<>(sessions.values()); - for (Session s : sList) { - if (s.timestamp.plus(24, ChronoUnit.HOURS).isBefore(Instant.now())) { // TODO config timeout - log.info("Session {} is obsolete, removing", s.sid); + @Autowired + public MappingManager(IStorage storage, LookupStrategy lookup) { + this.storage = storage; + } - sessions.remove(s.sid); - } + private ThreePidSession getSession(String sid, String secret) { + Optional dao = storage.getThreePidSession(sid); + if (!dao.isPresent() || !StringUtils.equals(dao.get().getSecret(), secret)) { + throw new InvalidCredentialsException(); + } + + return new ThreePidSession(dao.get()); + } + + private ThreePidSession getSessionIfValidated(String sid, String secret) { + ThreePidSession session = getSession(sid, secret); + if (!session.isValidated()) { + throw new IllegalStateException("Session " + sid + " has not been validated"); + } + return session; + } + + public String create(String server, ThreePid tpid, String secret, int attempt, String nextLink) { + synchronized (this) { + log.info("Server {} is asking to create session for {} (Attempt #{}) - Next link: {}", server, tpid, attempt, nextLink); + Optional dao = storage.findThreePidSession(tpid, secret); + if (dao.isPresent()) { + ThreePidSession session = new ThreePidSession(dao.get()); + log.info("We already have a session for {}: {}", tpid, session.getId()); + if (session.getAttempt() < attempt) { + log.info("Received attempt {} is greater than stored attempt {}, sending validation communication", attempt, session.getAttempt()); + // TODO send via connector + session.increaseAttempt(); + storage.updateThreePidSession(session.getDao()); } + + return session.getId(); + } else { + log.info("No existing session for {}", tpid); + String sessionId; + do { + sessionId = UUID.randomUUID().toString().replace("-", ""); + } 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); + + // TODO send via connector + // log.info("Sent validation notification to {}", tpid); + + storage.insertThreePidSession(session.getDao()); + log.info("Stored session {}", sessionId, tpid, server); + + return sessionId; } - }, 0, 10 * 1000); // TODO config delay - } - - public String create(MappingSession data) { - String sid; - do { - sid = Long.toString(System.currentTimeMillis()); - } while (sessions.containsKey(sid)); - - String threePidHash = data.getMedium() + data.getValue(); - // TODO think how to handle different requests for the same e-mail - Session session = new Session(sid, threePidHash, data); - sessions.put(sid, session); - - log.info("Created new session {} to validate {} {}", sid, session.medium, session.address); - return sid; - } - - public void validate(String sid, String secret, String token) { - Session s = sessions.get(sid); - if (s == null || !StringUtils.equals(s.secret, secret)) { - throw new BadRequestException("sid or secret are not valid"); } - - // TODO actually check token - - s.isValidated = true; - s.validationTimestamp = Instant.now(); } - public Optional getValidated(String sid, String secret) { - Session s = sessions.get(sid); - if (s != null && StringUtils.equals(s.secret, secret)) { - return Optional.of(new ThreePidValidation(s.medium, s.address, s.validationTimestamp)); - } + public Optional validate(String sid, String secret, String token) { + ThreePidSession session = getSession(sid, secret); + log.info("Attempting validation for session {} from {}", session.getId(), session.getServer()); + session.validate(token); + storage.updateThreePidSession(session.getDao()); + log.info("Session {} has been validated", session.getId()); + return session.getNextLink(); + } - return Optional.empty(); + public ThreePidValidation getValidated(String sid, String secret) { + ThreePidSession session = getSessionIfValidated(sid, secret); + return new ThreePidValidation(session.getThreePid(), session.getValidationTime()); } public void bind(String sid, String secret, String mxid) { - Session s = sessions.get(sid); - if (s == null || !StringUtils.equals(s.secret, secret)) { - throw new BadRequestException("sid or secret are not valid"); + ThreePidSession session = getSessionIfValidated(sid, secret); + log.info("Attempting bind of {} on session {} from server {}", mxid, session.getId(), session.getServer()); + + // We lookup if the 3PID is already known locally. + // If it is, we do not need to process any further as it is already bound. + Optional rLocal = lookup.findLocal(session.getThreePid().getMedium(), session.getThreePid().getAddress()); + boolean knownLocal = rLocal.isPresent() && StringUtils.equals(rLocal.get().getMxid().getId(), mxid); + log.info("Mapping {} -> {} is " + (knownLocal ? "already" : "not") + " known locally", mxid, session.getThreePid()); + + // MXID is not known locally, checking remotely + Optional rRemote = lookup.findRemote(session.getThreePid().getMedium(), session.getThreePid().getAddress()); + boolean knownRemote = rRemote.isPresent() && StringUtils.equals(rRemote.get().getMxid().getId(), mxid); + log.info("Mapping {} -> {} is " + (knownRemote ? "already" : "not") + " known remotely", mxid, session.getThreePid()); + + boolean isLocalDomain = false; + if (ThreePidMedium.Email.is(session.getThreePid().getMedium())) { + // TODO + // 1. Extract domain from email + // 2. set isLocalDomain + isLocalDomain = session.getThreePid().getAddress().isEmpty(); // FIXME only for testing + } + if (knownRemote) { + if (isLocalDomain && !knownLocal) { + log.warn("Mapping {} -> {} is not known locally but is about a local domain!"); + } + + log.info("No further action needed for Mapping {} -> {}"); + return; } - log.info("Performed bind for mxid {}", mxid); - // TODO perform bind, whatever it is - } + // 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 + } - private class Session { + if (System.currentTimeMillis() % 2 == 0) { + // TODO + // 1. Check if configured to publish globally non-local domain. If no, return + } - private String sid; - private String hash; - private Instant timestamp; - private Instant validationTimestamp; - private boolean isValidated; - private String secret; - private String medium; - private String address; - - public Session(String sid, String hash, MappingSession data) { - this.sid = sid; - this.hash = hash; - timestamp = Instant.now(); - validationTimestamp = Instant.now(); - secret = data.getSecret(); - medium = data.getMedium(); - address = data.getValue(); - } - - public Instant getTimestamp() { - return timestamp; - } - - public void setTimestamp(Instant timestamp) { - this.timestamp = timestamp; - } - - public Instant getValidationTimestamp() { - return validationTimestamp; - } - - public void setValidationTimestamp(Instant validationTimestamp) { - this.validationTimestamp = validationTimestamp; - } - - public boolean isValidated() { - return isValidated; - } - - public void setValidated(boolean validated) { - isValidated = validated; - } - - public String getSecret() { - return secret; - } - - public void setSecret(String secret) { - this.secret = secret; - } - - public String getMedium() { - return medium; - } - - public void setMedium(String medium) { - this.medium = medium; - } - - public String getAddress() { - return address; - } - - public void setAddress(String address) { - this.address = address; + // TODO + // Proxy to configurable IS, by default Matrix.org + // + // Separate workflow, if user accepts to publish globally + // 1. display page to the user that it is waiting for the confirmation + // 2. call mxisd-specific endpoint to publish globally + // 3. check regularly on client page for a binding + // 4. when found, show page "Done globally!" + } else { + if (isLocalDomain) { // 3PID is not known anywhere but is a local domain + // TODO + // check if config says this should fail or silently accept. + // Required to silently accept if the backend is synapse itself. + } else { // 3PID is not known anywhere and is remote + // TODO + // Proxy to configurable IS, by default Matrix.org + } } } diff --git a/src/main/groovy/io/kamax/mxisd/storage/IStorage.java b/src/main/groovy/io/kamax/mxisd/storage/IStorage.java index b77c50b..329dfd0 100644 --- a/src/main/groovy/io/kamax/mxisd/storage/IStorage.java +++ b/src/main/groovy/io/kamax/mxisd/storage/IStorage.java @@ -20,10 +20,13 @@ package io.kamax.mxisd.storage; +import io.kamax.mxisd.ThreePid; import io.kamax.mxisd.invitation.IThreePidInviteReply; +import io.kamax.mxisd.storage.dao.IThreePidSessionDao; import io.kamax.mxisd.storage.ormlite.ThreePidInviteIO; import java.util.Collection; +import java.util.Optional; public interface IStorage { @@ -33,4 +36,12 @@ public interface IStorage { void deleteInvite(String id); + Optional getThreePidSession(String sid); + + Optional findThreePidSession(ThreePid tpid, String secret); + + void insertThreePidSession(IThreePidSessionDao session); + + void updateThreePidSession(IThreePidSessionDao session); + } diff --git a/src/main/groovy/io/kamax/mxisd/mapping/MappingSession.java b/src/main/groovy/io/kamax/mxisd/storage/dao/IThreePidSessionDao.java similarity index 83% rename from src/main/groovy/io/kamax/mxisd/mapping/MappingSession.java rename to src/main/groovy/io/kamax/mxisd/storage/dao/IThreePidSessionDao.java index 7d647d3..dd5085e 100644 --- a/src/main/groovy/io/kamax/mxisd/mapping/MappingSession.java +++ b/src/main/groovy/io/kamax/mxisd/storage/dao/IThreePidSessionDao.java @@ -18,18 +18,24 @@ * along with this program. If not, see . */ -package io.kamax.mxisd.mapping; +package io.kamax.mxisd.storage.dao; -public interface MappingSession { +public interface IThreePidSessionDao { + + String getId(); String getServer(); + String getMedium(); + + String getAddress(); + String getSecret(); int getAttempt(); - String getMedium(); + String getNextLink(); - String getValue(); + String getToken(); } diff --git a/src/main/groovy/io/kamax/mxisd/storage/ormlite/OrmLiteSqliteStorage.java b/src/main/groovy/io/kamax/mxisd/storage/ormlite/OrmLiteSqliteStorage.java index e76596a..0aac625 100644 --- a/src/main/groovy/io/kamax/mxisd/storage/ormlite/OrmLiteSqliteStorage.java +++ b/src/main/groovy/io/kamax/mxisd/storage/ormlite/OrmLiteSqliteStorage.java @@ -26,8 +26,14 @@ import com.j256.ormlite.dao.DaoManager; import com.j256.ormlite.jdbc.JdbcConnectionSource; import com.j256.ormlite.support.ConnectionSource; import com.j256.ormlite.table.TableUtils; +import io.kamax.mxisd.ThreePid; +import io.kamax.mxisd.exception.InternalServerError; import io.kamax.mxisd.invitation.IThreePidInviteReply; import io.kamax.mxisd.storage.IStorage; +import io.kamax.mxisd.storage.dao.IThreePidSessionDao; +import io.kamax.mxisd.storage.ormlite.dao.ThreePidSessionDao; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; @@ -35,59 +41,141 @@ import java.sql.SQLException; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Optional; public class OrmLiteSqliteStorage implements IStorage { + private Logger log = LoggerFactory.getLogger(OrmLiteSqliteStorage.class); + + @FunctionalInterface + private interface Getter { + + T get() throws SQLException, IOException; + + } + + @FunctionalInterface + private interface Doer { + + void run() throws SQLException, IOException; + + } + private Dao invDao; + private Dao sessionDao; OrmLiteSqliteStorage(String path) { - try { + withCatcher(() -> { File parent = new File(path).getParentFile(); if (!parent.mkdirs() && !parent.isDirectory()) { throw new RuntimeException("Unable to create DB parent directory: " + parent); } ConnectionSource connPool = new JdbcConnectionSource("jdbc:sqlite:" + path); - invDao = DaoManager.createDao(connPool, ThreePidInviteIO.class); - TableUtils.createTableIfNotExists(connPool, ThreePidInviteIO.class); - } catch (SQLException e) { + invDao = createDaoAndTable(connPool, ThreePidInviteIO.class); + sessionDao = createDaoAndTable(connPool, ThreePidSessionDao.class); + }); + } + + private Dao createDaoAndTable(ConnectionSource connPool, Class c) throws SQLException { + Dao dao = DaoManager.createDao(connPool, c); + TableUtils.createTableIfNotExists(connPool, c); + return dao; + } + + private T withCatcher(Getter g) { + try { + return g.get(); + } catch (SQLException | IOException e) { throw new RuntimeException(e); // FIXME do better } } + private void withCatcher(Doer d) { + try { + d.run(); + } catch (SQLException | IOException e) { + throw new RuntimeException(e); // FIXME do better + } + } + + private List forIterable(CloseableWrappedIterable t) { + return withCatcher(() -> { + try { + List ioList = new ArrayList<>(); + t.forEach(ioList::add); + return ioList; + } finally { + t.close(); + } + }); + } + @Override public Collection getInvites() { - try (CloseableWrappedIterable t = invDao.getWrappedIterable()) { - List ioList = new ArrayList<>(); - t.forEach(ioList::add); - return ioList; - } catch (IOException e) { - throw new RuntimeException(e); // FIXME do better - } + return forIterable(invDao.getWrappedIterable()); } @Override public void insertInvite(IThreePidInviteReply data) { - try { + withCatcher(() -> { int updated = invDao.create(new ThreePidInviteIO(data)); if (updated != 1) { throw new RuntimeException("Unexpected row count after DB action: " + updated); } - } catch (SQLException e) { - throw new RuntimeException(e); // FIXME do better - } + }); } @Override public void deleteInvite(String id) { - try { + withCatcher(() -> { int updated = invDao.deleteById(id); if (updated != 1) { throw new RuntimeException("Unexpected row count after DB action: " + updated); } - } catch (SQLException e) { - throw new RuntimeException(e); // FIXME do better - } + }); + } + + @Override + public Optional getThreePidSession(String sid) { + return withCatcher(() -> Optional.ofNullable(sessionDao.queryForId(sid))); + } + + @Override + public Optional findThreePidSession(ThreePid tpid, String secret) { + return withCatcher(() -> { + List daoList = sessionDao.queryForMatchingArgs(new ThreePidSessionDao(tpid, secret)); + if (daoList.size() > 1) { + log.error("Lookup for 3PID Session {}:{} returned more than one result"); + throw new InternalServerError(); + } + + if (daoList.isEmpty()) { + return Optional.empty(); + } + + return Optional.of(daoList.get(0)); + }); + } + + @Override + public void insertThreePidSession(IThreePidSessionDao session) { + withCatcher(() -> { + int updated = sessionDao.create(new ThreePidSessionDao(session)); + if (updated != 1) { + throw new RuntimeException("Unexpected row count after DB action: " + updated); + } + }); + } + + @Override + public void updateThreePidSession(IThreePidSessionDao session) { + withCatcher(() -> { + int updated = sessionDao.update(new ThreePidSessionDao(session)); + if (updated != 1) { + throw new RuntimeException("Unexpected row count after DB action: " + updated); + } + }); } } diff --git a/src/main/groovy/io/kamax/mxisd/storage/ormlite/dao/ThreePidSessionDao.java b/src/main/groovy/io/kamax/mxisd/storage/ormlite/dao/ThreePidSessionDao.java new file mode 100644 index 0000000..0b557e1 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/storage/ormlite/dao/ThreePidSessionDao.java @@ -0,0 +1,147 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.storage.ormlite.dao; + +import com.j256.ormlite.field.DatabaseField; +import com.j256.ormlite.table.DatabaseTable; +import io.kamax.mxisd.ThreePid; +import io.kamax.mxisd.storage.dao.IThreePidSessionDao; + +@DatabaseTable(tableName = "session_3pid") +public class ThreePidSessionDao implements IThreePidSessionDao { + + @DatabaseField(id = true) + private String id; + + @DatabaseField(canBeNull = false) + private String server; + + @DatabaseField(canBeNull = false) + private String medium; + + @DatabaseField(canBeNull = false) + private String address; + + @DatabaseField(canBeNull = false) + private String secret; + + @DatabaseField(canBeNull = false) + private int attempt; + + @DatabaseField + private String nextLink; + + @DatabaseField(canBeNull = false) + private String token; + + public ThreePidSessionDao() { + // stub for ORMLite + } + + public ThreePidSessionDao(IThreePidSessionDao session) { + setId(session.getId()); + setServer(session.getServer()); + setMedium(session.getMedium()); + setAddress(session.getAddress()); + setSecret(session.getSecret()); + setAttempt(session.getAttempt()); + setNextLink(session.getNextLink()); + setToken(session.getToken()); + } + + public ThreePidSessionDao(ThreePid tpid, String secret) { + setMedium(tpid.getMedium()); + setAddress(tpid.getAddress()); + setSecret(secret); + } + + @Override + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @Override + public String getServer() { + return server; + } + + public void setServer(String server) { + this.server = server; + } + + @Override + public String getSecret() { + return secret; + } + + public void setSecret(String secret) { + this.secret = secret; + } + + @Override + public int getAttempt() { + return attempt; + } + + public void setAttempt(int attempt) { + this.attempt = attempt; + } + + @Override + public String getMedium() { + return medium; + } + + public void setMedium(String medium) { + this.medium = medium; + } + + @Override + public String getAddress() { + return address; + } + + public void setAddress(String address) { + this.address = address; + } + + @Override + public String getNextLink() { + return nextLink; + } + + public void setNextLink(String nextLink) { + this.nextLink = nextLink; + } + + @Override + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } +} diff --git a/src/main/groovy/io/kamax/mxisd/threepid/connector/EmailSmtpConnector.java b/src/main/groovy/io/kamax/mxisd/threepid/connector/EmailSmtpConnector.java index b9d68e0..7bc3ae3 100644 --- a/src/main/groovy/io/kamax/mxisd/threepid/connector/EmailSmtpConnector.java +++ b/src/main/groovy/io/kamax/mxisd/threepid/connector/EmailSmtpConnector.java @@ -22,8 +22,8 @@ package io.kamax.mxisd.threepid.connector; import com.sun.mail.smtp.SMTPTransport; import io.kamax.matrix.ThreePidMedium; -import io.kamax.mxisd.config.invite.medium.EmailInviteConfig; import io.kamax.mxisd.config.threepid.connector.EmailSmtpConfig; +import io.kamax.mxisd.config.threepid.medium.EmailConfig; import io.kamax.mxisd.exception.ConfigurationException; import io.kamax.mxisd.invitation.IThreePidInviteReply; import org.apache.commons.io.IOUtils; @@ -47,23 +47,21 @@ public class EmailSmtpConnector implements IThreePidConnector { private Logger log = LoggerFactory.getLogger(EmailSmtpConnector.class); private EmailSmtpConfig cfg; - private EmailInviteConfig invCfg; private Session session; private InternetAddress sender; @Autowired - public EmailSmtpConnector(EmailSmtpConfig cfg, EmailInviteConfig invCfg) { + public EmailSmtpConnector(EmailConfig cfg, EmailSmtpConfig smtpCfg) { try { session = Session.getInstance(System.getProperties()); - sender = new InternetAddress(invCfg.getFrom(), invCfg.getName()); + sender = new InternetAddress(cfg.getFrom(), cfg.getName()); } catch (UnsupportedEncodingException e) { // What are we supposed to do with this?! throw new ConfigurationException(e); } - this.cfg = cfg; - this.invCfg = invCfg; + this.cfg = smtpCfg; } @Override diff --git a/src/main/groovy/io/kamax/mxisd/threepid/session/IThreePidSession.java b/src/main/groovy/io/kamax/mxisd/threepid/session/IThreePidSession.java new file mode 100644 index 0000000..28af430 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/threepid/session/IThreePidSession.java @@ -0,0 +1,52 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.threepid.session; + +import io.kamax.mxisd.ThreePid; + +import java.time.Instant; +import java.util.Optional; + +public interface IThreePidSession { + + String getId(); + + String getHash(); + + Instant getCreationTime(); + + String getServer(); + + ThreePid getThreePid(); + + int getAttempt(); + + void increaseAttempt(); + + Optional getNextLink(); + + void validate(String token); + + boolean isValidated(); + + Instant getValidationTime(); + +} diff --git a/src/main/groovy/io/kamax/mxisd/threepid/session/ThreePidSession.java b/src/main/groovy/io/kamax/mxisd/threepid/session/ThreePidSession.java new file mode 100644 index 0000000..8d0fcbc --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/threepid/session/ThreePidSession.java @@ -0,0 +1,197 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.threepid.session; + +import io.kamax.mxisd.ThreePid; +import io.kamax.mxisd.exception.BadRequestException; +import io.kamax.mxisd.exception.InvalidCredentialsException; +import io.kamax.mxisd.storage.dao.IThreePidSessionDao; +import org.apache.commons.lang.StringUtils; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Optional; + +public class ThreePidSession implements IThreePidSession { + + private String id; + private Instant timestamp; + private String hash; + private String server; + private ThreePid tPid; + private String secret; + private String nextLink; + private String token; + private int attempt; + private Instant validationTimestamp; + private boolean isValidated; + + public ThreePidSession(IThreePidSessionDao dao) { + this( + dao.getId(), + dao.getServer(), + new ThreePid(dao.getMedium(), dao.getAddress()), + dao.getSecret(), + dao.getAttempt(), + dao.getNextLink(), + dao.getToken() + ); + } + + public ThreePidSession(String id, String server, ThreePid tPid, String secret, int attempt, String nextLink, String token) { + this.id = id; + this.server = server; + this.tPid = new ThreePid(tPid); + this.secret = secret; + this.attempt = attempt; + this.nextLink = nextLink; + this.token = token; + + this.timestamp = Instant.now(); + this.hash = server.toLowerCase() + tPid.getMedium().toLowerCase() + tPid.getAddress().toLowerCase() + secret; + } + + @Override + public String getId() { + return id; + } + + @Override + public String getHash() { + return hash; + } + + @Override + public Instant getCreationTime() { + return timestamp; + } + + @Override + public String getServer() { + return server; + } + + @Override + public ThreePid getThreePid() { + return tPid; + } + + public String getSecret() { + return secret; + } + + @Override + public int getAttempt() { + return attempt; + } + + @Override + public void increaseAttempt() { + attempt++; + } + + @Override + public Optional getNextLink() { + return Optional.ofNullable(nextLink); + } + + public synchronized void setAttempt(int attempt) { + if (isValidated()) { + throw new IllegalStateException(); + } + + this.attempt = attempt; + } + + @Override + public Instant getValidationTime() { + return validationTimestamp; + } + + @Override + public boolean isValidated() { + return isValidated; + } + + public synchronized void validate(String token) { + if (Instant.now().minus(24, ChronoUnit.HOURS).isAfter(getCreationTime())) { + throw new BadRequestException("Session " + getId() + " has expired"); + } + + if (!StringUtils.equals(this.token, token)) { + throw new InvalidCredentialsException(); + } + + if (isValidated()) { + return; + } + + validationTimestamp = Instant.now(); + isValidated = true; + } + + public IThreePidSessionDao getDao() { + return new IThreePidSessionDao() { + + @Override + public String getId() { + return id; + } + + @Override + public String getServer() { + return server; + } + + @Override + public String getMedium() { + return tPid.getMedium(); + } + + @Override + public String getAddress() { + return tPid.getAddress(); + } + + @Override + public String getSecret() { + return secret; + } + + @Override + public int getAttempt() { + return attempt; + } + + @Override + public String getNextLink() { + return nextLink; + } + + @Override + public String getToken() { + return token; + } + + }; + } + +}