diff --git a/build.gradle b/build.gradle index 93b9b15..8a92acf 100644 --- a/build.gradle +++ b/build.gradle @@ -57,6 +57,9 @@ dependencies { // HTTP connections compile 'org.apache.httpcomponents:httpclient:4.5.3' + // JSON + compile 'com.google.code.gson:gson:2.8.1' + testCompile 'junit:junit:4.12' } 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 f65963d..a397e53 100644 --- a/src/main/groovy/io/kamax/mxisd/controller/v1/SessionController.groovy +++ b/src/main/groovy/io/kamax/mxisd/controller/v1/SessionController.groovy @@ -20,48 +20,132 @@ package io.kamax.mxisd.controller.v1 +import com.google.gson.Gson +import com.google.gson.JsonObject +import io.kamax.mxisd.controller.v1.io.SessionEmailTokenRequestJson +import io.kamax.mxisd.controller.v1.io.SessionPhoneTokenRequestJson +import io.kamax.mxisd.exception.BadRequestException import io.kamax.mxisd.exception.NotImplementedException +import io.kamax.mxisd.lookup.ThreePid +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 +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController import javax.servlet.http.HttpServletRequest - -import static org.springframework.web.bind.annotation.RequestMethod.GET -import static org.springframework.web.bind.annotation.RequestMethod.POST +import javax.servlet.http.HttpServletResponse +import java.nio.charset.StandardCharsets @RestController class SessionController { + @Autowired + private MappingManager mgr + + private Gson gson = new Gson() + private Logger log = LoggerFactory.getLogger(SessionController.class) - @RequestMapping(value = "/_matrix/identity/api/v1/validate/{medium}/requestToken", method = POST) - String init(HttpServletRequest request) { - log.error("{} was requested but not implemented", request.getRequestURL()) - - throw new NotImplementedException() + private T fromJson(HttpServletRequest req, Class obj) { + gson.fromJson(new InputStreamReader(req.getInputStream(), StandardCharsets.UTF_8), obj) } - @RequestMapping(value = "/_matrix/identity/api/v1/validate/{medium}/submitToken", method = [GET, POST]) + @RequestMapping(value = "/_matrix/identity/api/v1/validate/{medium}/requestToken") + String init(HttpServletRequest request, HttpServletResponse response, @PathVariable String medium) { + log.info("Requested: {}", request.getRequestURL(), request.getQueryString()) + + if (StringUtils.equals("email", medium)) { + SessionEmailTokenRequestJson req = fromJson(request, SessionEmailTokenRequestJson.class) + return gson.toJson(new Sid(mgr.create(req))) + } + + if (StringUtils.equals("msisdn", medium)) { + SessionPhoneTokenRequestJson req = fromJson(request, SessionPhoneTokenRequestJson.class) + return gson.toJson(new Sid(mgr.create(req))) + } + + JsonObject obj = new JsonObject(); + obj.addProperty("errcode", "M_INVALID_3PID_TYPE") + obj.addProperty("error", medium + " is not supported as a 3PID type") + response.setStatus(HttpStatus.SC_BAD_REQUEST) + return gson.toJson(obj) + } + + @RequestMapping(value = "/_matrix/identity/api/v1/validate/{medium}/submitToken") String validate(HttpServletRequest request) { - log.error("{} was requested but not implemented", request.getRequestURL()) + log.info("Requested: {}?{}", request.getRequestURL(), request.getQueryString()) throw new NotImplementedException() } - @RequestMapping(value = "/_matrix/identity/api/v1/3pid/getValidated3pid", method = POST) - String check(HttpServletRequest request) { - log.error("{} was requested but not implemented", request.getRequestURL()) + @RequestMapping(value = "/_matrix/identity/api/v1/3pid/getValidated3pid") + String check(HttpServletRequest request, HttpServletResponse response, + @RequestParam String sid, @RequestParam("client_secret") String secret) { + log.info("Requested: {}?{}", request.getRequestURL(), request.getQueryString()) - throw new NotImplementedException() + Optional result = mgr.getValidated(sid, secret) + if (result.isPresent()) { + log.info("requested session was validated") + ThreePid pid = result.get() + + JsonObject obj = new JsonObject() + obj.addProperty("medium", pid.getMedium()) + obj.addProperty("address", pid.getAddress()) + obj.addProperty("validated_at", pid.getValidation().toEpochMilli()) + + return gson.toJson(obj); + } else { + log.info("requested session was not validated") + + JsonObject obj = new JsonObject() + obj.addProperty("errcode", "M_SESSION_NOT_VALIDATED") + obj.addProperty("error", "sid, secret or session not valid") + response.setStatus(HttpStatus.SC_BAD_REQUEST) + return gson.toJson(obj) + } } - @RequestMapping(value = "/_matrix/identity/api/v1/3pid/bind", method = POST) - String bind(HttpServletRequest request) { - log.error("{} was requested but not implemented", request.getRequestURL()) + @RequestMapping(value = "/_matrix/identity/api/v1/3pid/bind") + String bind(HttpServletRequest request, HttpServletResponse response, + @RequestParam String sid, @RequestParam("client_secret") String secret, @RequestParam String mxid) { + String data = IOUtils.toString(request.getReader()) + log.info("Requested: {}", request.getRequestURL(), request.getQueryString()) + try { + mgr.bind(sid, secret, mxid) + return "{}" + } catch (BadRequestException e) { + log.info("requested session was not validated") - throw new NotImplementedException() + obj = new JsonObject() + obj.addProperty("errcode", "M_SESSION_NOT_VALIDATED") + obj.addProperty("error", e.getMessage()) + response.setStatus(HttpStatus.SC_BAD_REQUEST) + return gson.toJson(obj) + } + } + + private class Sid { + + private String sid; + + public Sid(String sid) { + setSid(sid); + } + + String getSid() { + return sid + } + + void setSid(String sid) { + this.sid = sid + } } } 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 new file mode 100644 index 0000000..4304460 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/controller/v1/io/GenericTokenRequestJson.java @@ -0,0 +1,23 @@ +package io.kamax.mxisd.controller.v1.io; + +import io.kamax.mxisd.mapping.MappingSession; + +public abstract class GenericTokenRequestJson implements MappingSession { + + private String client_secret; + private int send_attempt; + private String id_server; + + public String getSecret() { + return client_secret; + } + + public int getAttempt() { + return send_attempt; + } + + public String getServer() { + return id_server; + } + +} 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 new file mode 100644 index 0000000..e3c7f65 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/controller/v1/io/SessionEmailTokenRequestJson.java @@ -0,0 +1,17 @@ +package io.kamax.mxisd.controller.v1.io; + +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 new file mode 100644 index 0000000..116daab --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/controller/v1/io/SessionPhoneTokenRequestJson.java @@ -0,0 +1,22 @@ +package io.kamax.mxisd.controller.v1.io; + +public class SessionPhoneTokenRequestJson extends GenericTokenRequestJson { + + private String country; + private String phone_number; + + @Override + public String getMedium() { + return "email"; + } + + @Override + public String getValue() { + return phone_number; + } + + public String getCountry() { + return country; + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/lookup/ThreePid.java b/src/main/groovy/io/kamax/mxisd/lookup/ThreePid.java new file mode 100644 index 0000000..6ecf4ab --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/lookup/ThreePid.java @@ -0,0 +1,29 @@ +package io.kamax.mxisd.lookup; + +import java.time.Instant; + +public class ThreePid { + + private String medium; + private String address; + private Instant validation; + + public ThreePid(String medium, String address, Instant validation) { + this.medium = medium; + this.address = address; + this.validation = validation; + } + + public String getMedium() { + return medium; + } + + public String getAddress() { + return address; + } + + public Instant getValidation() { + return 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 cfd9e79..dee4646 100644 --- a/src/main/groovy/io/kamax/mxisd/lookup/strategy/LookupStrategy.groovy +++ b/src/main/groovy/io/kamax/mxisd/lookup/strategy/LookupStrategy.groovy @@ -23,9 +23,12 @@ package io.kamax.mxisd.lookup.strategy import io.kamax.mxisd.lookup.BulkLookupRequest import io.kamax.mxisd.lookup.SingleLookupRequest import io.kamax.mxisd.lookup.ThreePidMapping +import io.kamax.mxisd.lookup.provider.IThreePidProvider interface LookupStrategy { + List getLocalProviders() + Optional find(SingleLookupRequest request) List find(BulkLookupRequest requests) 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 49f2fff..4317adc 100644 --- a/src/main/groovy/io/kamax/mxisd/lookup/strategy/RecursivePriorityLookupStrategy.groovy +++ b/src/main/groovy/io/kamax/mxisd/lookup/strategy/RecursivePriorityLookupStrategy.groovy @@ -34,6 +34,9 @@ import org.springframework.beans.factory.InitializingBean import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Component +import java.util.function.Predicate +import java.util.stream.Collectors + @Component class RecursivePriorityLookupStrategy implements LookupStrategy, InitializingBean { @@ -104,6 +107,16 @@ class RecursivePriorityLookupStrategy implements LookupStrategy, InitializingBea return usableProviders } + @Override + List getLocalProviders() { + return providers.stream().filter(new Predicate() { + @Override + boolean test(IThreePidProvider iThreePidProvider) { + return iThreePidProvider.isEnabled() && iThreePidProvider.isLocal() + } + }).collect(Collectors.toList()) + } + @Override Optional find(SingleLookupRequest request) { for (IThreePidProvider provider : listUsableProviders(request)) { diff --git a/src/main/groovy/io/kamax/mxisd/mapping/MappingManager.java b/src/main/groovy/io/kamax/mxisd/mapping/MappingManager.java new file mode 100644 index 0000000..fe760e3 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/mapping/MappingManager.java @@ -0,0 +1,152 @@ +package io.kamax.mxisd.mapping; + +import io.kamax.mxisd.exception.BadRequestException; +import io.kamax.mxisd.lookup.ThreePid; +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.*; + +@Component +public class MappingManager { + + private Logger log = LoggerFactory.getLogger(MappingManager.class); + + private Map threePidLookups = new WeakHashMap<>(); + private Map sessions = new HashMap<>(); + private Timer cleaner; + + 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); + + sessions.remove(s.sid); + threePidLookups.remove(s.hash); + } + } + } + }, 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(); + Session session = threePidLookups.get(threePidHash); + if (session != null) { + sid = session.sid; + } else { + // TODO perform some kind of validation + + session = new Session(sid, threePidHash, data); + sessions.put(sid, session); + threePidLookups.put(threePidHash, session); + } + + log.info("Created new session {} to validate {} {}", sid, session.medium, session.address); + return sid; + } + + public Optional getValidated(String sid, String secret) { + Session s = sessions.get(sid); + if (s != null && StringUtils.equals(s.secret, secret)) { + return Optional.of(new ThreePid(s.medium, s.address, s.validationTimestamp)); + } + + return Optional.empty(); + } + + 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"); + } + + log.info("Performed bind for mxid {}", mxid); + // TODO perform bind, whatever it is + } + + 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(); + } + + 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; + } + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/mapping/MappingSession.java b/src/main/groovy/io/kamax/mxisd/mapping/MappingSession.java new file mode 100644 index 0000000..cf5d6a7 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/mapping/MappingSession.java @@ -0,0 +1,15 @@ +package io.kamax.mxisd.mapping; + +public interface MappingSession { + + String getServer(); + + String getSecret(); + + int getAttempt(); + + String getMedium(); + + String getValue(); + +}