Prepare structure to handle 3PID sessions and bindings validation/proxy

This commit is contained in:
Maxime Dor
2017-09-20 04:35:34 +02:00
parent c1746697b9
commit 0b087ee08c
22 changed files with 910 additions and 213 deletions

View File

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

View File

@@ -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())) {

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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());
}
}

View File

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

View File

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

View File

@@ -24,12 +24,10 @@ public class SessionEmailTokenRequestJson extends GenericTokenRequestJson {
private String email;
@Override
public String getMedium() {
return "email";
}
@Override
public String getValue() {
return email;
}

View File

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

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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());
}
}

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 InvalidCredentialsException extends RuntimeException {
public InvalidCredentialsException() {
super("Supplied credentials are invalid");
}
}

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.NOT_FOUND)
public class ObjectNotFoundException extends RuntimeException {
public ObjectNotFoundException(String type, String id) {
super(type + " with ID " + id + " does not exist");
}
}

View File

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

View File

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

View File

@@ -32,6 +32,10 @@ interface LookupStrategy {
Optional<SingleLookupReply> find(String medium, String address, boolean recursive)
Optional<SingleLookupReply> findLocal(String medium, String address);
Optional<SingleLookupReply> findRemote(String medium, String address);
Optional<SingleLookupReply> find(SingleLookupRequest request)
Optional<SingleLookupReply> findRecursive(SingleLookupRequest request)

View File

@@ -118,17 +118,44 @@ class RecursivePriorityLookupStrategy implements LookupStrategy, InitializingBea
}).collect(Collectors.toList())
}
List<IThreePidProvider> getRemoteProviders() {
return providers.stream().filter(new Predicate<IThreePidProvider>() {
@Override
Optional<SingleLookupReply> find(String medium, String address, boolean recursive) {
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<SingleLookupReply> find(String medium, String address, boolean recursive) {
return find(build(medium, address), recursive)
}
@Override
Optional<SingleLookupReply> findLocal(String medium, String address) {
return find(build(medium, address), getLocalProviders())
}
@Override
Optional<SingleLookupReply> findRemote(String medium, String address) {
return find(build(medium, address), getRemoteProviders())
}
Optional<SingleLookupReply> find(SingleLookupRequest request, boolean forceRecursive) {
for (IThreePidProvider provider : listUsableProviders(request, forceRecursive)) {
return find(request, listUsableProviders(request, forceRecursive));
}
Optional<SingleLookupReply> find(SingleLookupRequest request, List<IThreePidProvider> providers) {
for (IThreePidProvider provider : providers) {
Optional<SingleLookupReply> lookupDataOpt = provider.find(request)
if (lookupDataOpt.isPresent()) {
return lookupDataOpt

View File

@@ -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<String, Session> 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<Session> sList = new ArrayList<>(sessions.values());
for (Session s : sList) {
if (s.timestamp.plus(24, ChronoUnit.HOURS).isBefore(Instant.now())) { // TODO config timeout
log.info("Session {} is obsolete, removing", s.sid);
sessions.remove(s.sid);
}
}
}
}, 0, 10 * 1000); // TODO config delay
@Autowired
public MappingManager(IStorage storage, LookupStrategy lookup) {
this.storage = storage;
}
public String create(MappingSession data) {
String sid;
private ThreePidSession getSession(String sid, String secret) {
Optional<IThreePidSessionDao> 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<IThreePidSessionDao> 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 {
sid = Long.toString(System.currentTimeMillis());
} while (sessions.containsKey(sid));
sessionId = UUID.randomUUID().toString().replace("-", "");
} while (storage.getThreePidSession(sessionId).isPresent());
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);
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);
log.info("Created new session {} to validate {} {}", sid, session.medium, session.address);
return sid;
// TODO send via connector
// log.info("Sent validation notification to {}", tpid);
storage.insertThreePidSession(session.getDao());
log.info("Stored session {}", sessionId, tpid, server);
return sessionId;
}
}
}
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");
public Optional<String> 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();
}
// TODO actually check token
s.isValidated = true;
s.validationTimestamp = Instant.now();
}
public Optional<ThreePidValidation> 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));
}
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<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());
// MXID is not known locally, checking 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) {
if (isLocalDomain && !knownLocal) {
log.warn("Mapping {} -> {} is not known locally but is about a local domain!");
}
log.info("Performed bind for mxid {}", mxid);
// TODO perform bind, whatever it is
log.info("No further action needed for Mapping {} -> {}");
return;
}
private class Session {
private String sid;
private String hash;
private Instant timestamp;
private Instant validationTimestamp;
private boolean isValidated;
private String secret;
private String medium;
private String address;
public Session(String sid, String hash, MappingSession data) {
this.sid = sid;
this.hash = hash;
timestamp = Instant.now();
validationTimestamp = Instant.now();
secret = data.getSecret();
medium = data.getMedium();
address = data.getValue();
// This might need a configuration by medium type?
if (knownLocal) { // 3PID is ony known local
if (isLocalDomain) {
// TODO
// 1. Check if global publishing is enabled, allowed and offered. If one is no, return.
// 2. Publish globally
}
public Instant getTimestamp() {
return timestamp;
if (System.currentTimeMillis() % 2 == 0) {
// TODO
// 1. Check if configured to publish globally non-local domain. If no, return
}
public void setTimestamp(Instant timestamp) {
this.timestamp = timestamp;
// 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
}
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;
}
}

View File

@@ -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<IThreePidSessionDao> getThreePidSession(String sid);
Optional<IThreePidSessionDao> findThreePidSession(ThreePid tpid, String secret);
void insertThreePidSession(IThreePidSessionDao session);
void updateThreePidSession(IThreePidSessionDao session);
}

View File

@@ -18,18 +18,24 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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();
}

View File

@@ -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> {
T get() throws SQLException, IOException;
}
@FunctionalInterface
private interface Doer {
void run() throws SQLException, IOException;
}
private Dao<ThreePidInviteIO, String> invDao;
private Dao<ThreePidSessionDao, String> 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 <V, K> Dao<V, K> createDaoAndTable(ConnectionSource connPool, Class<V> c) throws SQLException {
Dao<V, K> dao = DaoManager.createDao(connPool, c);
TableUtils.createTableIfNotExists(connPool, c);
return dao;
}
private <T> T withCatcher(Getter<T> 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 <T> List<T> forIterable(CloseableWrappedIterable<? extends T> t) {
return withCatcher(() -> {
try {
List<T> ioList = new ArrayList<>();
t.forEach(ioList::add);
return ioList;
} finally {
t.close();
}
});
}
@Override
public Collection<ThreePidInviteIO> getInvites() {
try (CloseableWrappedIterable<ThreePidInviteIO> t = invDao.getWrappedIterable()) {
List<ThreePidInviteIO> 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<IThreePidSessionDao> getThreePidSession(String sid) {
return withCatcher(() -> Optional.ofNullable(sessionDao.queryForId(sid)));
}
@Override
public Optional<IThreePidSessionDao> findThreePidSession(ThreePid tpid, String secret) {
return withCatcher(() -> {
List<ThreePidSessionDao> 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);
}
});
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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;
}
}

View File

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

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<String> getNextLink();
void validate(String token);
boolean isValidated();
Instant getValidationTime();
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<String> 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;
}
};
}
}