diff --git a/build.gradle b/build.gradle index 0387e8a..d12e571 100644 --- a/build.gradle +++ b/build.gradle @@ -116,6 +116,9 @@ dependencies { // PostgreSQL compile 'org.postgresql:postgresql:42.1.4' + // Twilio SDK for SMS + compile 'com.twilio.sdk:twilio:7.14.5' + testCompile 'junit:junit:4.12' testCompile 'com.github.tomakehurst:wiremock:2.8.0' } diff --git a/docs/sessions/3pid.md b/docs/sessions/3pid.md index 1e6293c..59299bd 100644 --- a/docs/sessions/3pid.md +++ b/docs/sessions/3pid.md @@ -6,6 +6,7 @@ - [Session scope](#session-scope) - [Notifications](#notifications) - [Email](#email) + - [Phone numbers](#msisdn-phone-numbers) - [Usage](#usage) - [Configuration](#configuration) - [Web views](#web-views) @@ -98,11 +99,17 @@ Built-in generators and connectors for supported 3PID types: ### Email Generators: -- [Template](https://github.com/kamax-io/mxisd/blob/master/docs/threepids/email/notifications/template-generator.md) +- [Template](../threepids/notifications/template-generator.md) Connectors: -- [SMTP](https://github.com/kamax-io/mxisd/blob/master/docs/threepids/email/notifications/smtp-connector.md) +- [SMTP](../threepids/medium/email/smtp-connector.md) +#### MSISDN (Phone numbers) +Generators: +- [Template](../threepids/notifications/template-generator.md) + +Connectors: + - [Twilio](../threepids/medium/msisdn/twilio-connector.md) with SMS ## Usage ### Configuration @@ -178,16 +185,6 @@ ID for each generator. In the above example, emails notifications are generated by the `example2` module and sent with the `example1` module. By default, `template` is used as generator and `smtp` as connector. -mxisd comes with the following IDs built-in: -**Connectors** -- `smtp` for a basic SMTP connector, attempting STARTLS by default. -See [the dedicated document](https://github.com/kamax-io/mxisd/tree/master/docs/threepids/email/notifications/smtp-connector.md) - -**Generators** -- `template`, loading content from template files, using built-in mxisd templates by default. -See [the dedicated document](https://github.com/kamax-io/mxisd/tree/master/docs/threepids/email/notifications/template-generator.md) -for further configuration and customization options. - --- `session.policy.validation` is the core configuration to control what users configured to use your Identity server @@ -210,7 +207,7 @@ Once a user click on a validation link, it is taken to the Identity Server valid If the session or token is invalid, an error page is displayed. Workflow pages are also available for the remote 3PID session process. -See [the dedicated document](https://github.com/kamax-io/mxisd/tree/master/docs/sessions/3pid-views.md) +See [the dedicated document](3pid-views.md) on how to configure/customize/brand those pages to your liking. ### Scenarios diff --git a/docs/threepids/email/notifications/smtp-connector.md b/docs/threepids/medium/email/smtp-connector.md similarity index 88% rename from docs/threepids/email/notifications/smtp-connector.md rename to docs/threepids/medium/email/smtp-connector.md index 8bff396..6aa686d 100644 --- a/docs/threepids/email/notifications/smtp-connector.md +++ b/docs/threepids/medium/email/smtp-connector.md @@ -1,5 +1,7 @@ # Email notifications - SMTP connector -The following configuration items are available: +Connector ID: `smtp` + +Example configuration: ``` threepid: medium: diff --git a/docs/threepids/medium/msisdn/twilio-connector.md b/docs/threepids/medium/msisdn/twilio-connector.md new file mode 100644 index 0000000..3840ffc --- /dev/null +++ b/docs/threepids/medium/msisdn/twilio-connector.md @@ -0,0 +1,15 @@ +# SMS notifications - Twilio connector +Connector ID: `twilio` + +Example configuration: +``` +threepid: + medium: + msisdn: + connectors: + twilio: + accountSid: 'myAccountSid' + authToken: 'myAuthToken' + number: '+123456789' + +``` diff --git a/docs/threepids/email/notifications/template-generator.md b/docs/threepids/notifications/template-generator.md similarity index 72% rename from docs/threepids/email/notifications/template-generator.md rename to docs/threepids/notifications/template-generator.md index 04b6548..e2acc0a 100644 --- a/docs/threepids/email/notifications/template-generator.md +++ b/docs/threepids/notifications/template-generator.md @@ -1,6 +1,6 @@ -# Email notifications: Generate from templates -To create notification content, you can use the `template` generator which will read MIME email body, including headers, -encoded as UTF-8. +# Notifications: Generate from templates +To create notification content, you can use the `template` generator if supported for the 3PID medium which will read +content from configured files. Placeholders can be integrated into the templates to dynamically populate such content with relevant information like the 3PID that was requested, the domain of your Identity server, etc. @@ -13,7 +13,7 @@ To configure paths to the various templates: ``` threepid: medium: - email: + : generators: template: invite: '/path/to/invite-template.eml' @@ -22,16 +22,16 @@ threepid: local: '/path/to/validate-local-template.eml' remote: 'path/to/validate-remote-template.eml' ``` -The `template` generator is the default, so no further configuration is needed. +The `template` generator is usually the default, so no further configuration is needed. ## Global placeholders | Placeholder | Purpose | |-----------------------|------------------------------------------------------------------------------| | `%DOMAIN%` | Identity server authoritative domain, as configured in `matrix.domain` | | `%DOMAIN_PRETTY%` | Same as `%DOMAIN%` with the first letter upper case and all other lower case | -| `%FROM_EMAIL%` | Email address configured in `threepid.medium.email.identity.from` | -| `%FROM_NAME%` | Name configured in `threepid.medium.email.identity.name` | -| `%RECIPIENT_MEDIUM%` | Set as `email` | +| `%FROM_EMAIL%` | Email address configured in `threepid.medium.<3PID medium>.identity.from` | +| `%FROM_NAME%` | Name configured in `threepid.medium.<3PID medium>.identity.name` | +| `%RECIPIENT_MEDIUM%` | The 3PID medium, like `email` or `msisdn` | | `%RECIPIENT_ADDRESS%` | The address to which the notification is sent | ## Events @@ -43,14 +43,14 @@ This template is used when someone is invited into a room using an email address | `%SENDER_ID%` | Matrix ID of the user who made the invite | | `%SENDER_NAME%` | Display name of the user who made the invite, if not available/set, empty | | `%SENDER_NAME_OR_ID%` | Display name of the user who made the invite. If not available/set, its Matrix ID | -| `%INVITE_MEDIUM%` | The 3PID medium for the invite. Always set to `email` | +| `%INVITE_MEDIUM%` | The 3PID medium for the invite. | | `%INVITE_ADDRESS%` | The 3PID address for the invite. | | `%ROOM_ID%` | The Matrix ID of the Room in which the invite took place | | `%ROOM_NAME%` | The Name of the room in which the invite took place. If not available/set, empty | | `%ROOM_NAME_OR_ID%` | The Name of the room in which the invite took place. If not available/set, its Matrix ID | ### Local validation of 3PID Session -This template is used when to user which added their email address to their profile/settings and the session policy +This template is used when to user which added their 3PID address to their profile/settings and the session policy allows at least local sessions. #### Placeholders @@ -60,13 +60,14 @@ allows at least local sessions. | `%VALIDATION_TOKEN%` | The token needed to validate the local session, in case the user cannot use the link | ### Remote validation of 3PID Session -This template is used when to user which added their email address to their profile/settings and the session policy only +This template is used when to user which added their 3PID address to their profile/settings and the session policy only allows remote sessions. **NOTE:** 3PID session always require local validation of a token, even if a remote session is enforced. -One cannot bind a MXID to the session until the remote +One cannot bind a MXID to the session until both local and remote sessions have been validated. #### Placeholders -| Placeholder | Purpose | -|--------------|--------------------------------------------------------| -| `%NEXT_URL%` | URL to continue with remote validation of the session. | +| Placeholder | Purpose | +|----------------------|--------------------------------------------------------| +| `%VALIDATION_TOKEN%` | The token needed to validate the session | +| `%NEXT_URL%` | URL to continue with remote validation of the session. | diff --git a/src/main/java/io/kamax/mxisd/UserIdType.java b/src/main/java/io/kamax/mxisd/UserIdType.java index e0625bf..5b94ec2 100644 --- a/src/main/java/io/kamax/mxisd/UserIdType.java +++ b/src/main/java/io/kamax/mxisd/UserIdType.java @@ -28,7 +28,7 @@ public enum UserIdType { Localpart("localpart"), MatrixID("mxid"), EmailLocalpart("email_localpart"), - Email("email"); + Email("threepids/email"); private String id; diff --git a/src/main/java/io/kamax/mxisd/config/threepid/connector/PhoneTwilioConfig.java b/src/main/java/io/kamax/mxisd/config/threepid/connector/PhoneTwilioConfig.java new file mode 100644 index 0000000..53bfe1e --- /dev/null +++ b/src/main/java/io/kamax/mxisd/config/threepid/connector/PhoneTwilioConfig.java @@ -0,0 +1,88 @@ +/* + * 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.connector; + +import io.kamax.mxisd.exception.ConfigurationException; +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import javax.annotation.PostConstruct; + +@Configuration +@ConfigurationProperties(prefix = PhoneTwilioConfig.NAMESPACE) +public class PhoneTwilioConfig { + + static final String NAMESPACE = "threepid.medium.msisdn.connectors.twilio"; + + private Logger log = LoggerFactory.getLogger(PhoneTwilioConfig.class); + + private String accountSid; + private String authToken; + private String number; + + public String getAccountSid() { + return accountSid; + } + + public void setAccountSid(String accountSid) { + this.accountSid = accountSid; + } + + public String getAuthToken() { + return authToken; + } + + public void setAuthToken(String authToken) { + this.authToken = authToken; + } + + public String getNumber() { + return number; + } + + public void setNumber(String number) { + this.number = number; + } + + @PostConstruct + public void build() { + log.info("--- Phone SMS Twilio connector config ---"); + + if (StringUtils.isBlank(getAccountSid())) { + throw new ConfigurationException(NAMESPACE + ".accountSid"); + } + + if (StringUtils.isBlank(getAuthToken())) { + throw new ConfigurationException(NAMESPACE + ".authToken"); + } + + if (StringUtils.isBlank(getNumber())) { + throw new ConfigurationException(NAMESPACE + ".number"); + } + + log.info("Account SID: {}", getAccountSid()); + log.info("Sender number: {}", getNumber()); + } + +} diff --git a/src/main/java/io/kamax/mxisd/config/threepid/medium/EmailConfig.java b/src/main/java/io/kamax/mxisd/config/threepid/medium/EmailConfig.java index 29ac7c0..416ba65 100644 --- a/src/main/java/io/kamax/mxisd/config/threepid/medium/EmailConfig.java +++ b/src/main/java/io/kamax/mxisd/config/threepid/medium/EmailConfig.java @@ -36,6 +36,8 @@ import javax.annotation.PostConstruct; @ConfigurationProperties("threepid.medium.email") public class EmailConfig { + private Logger log = LoggerFactory.getLogger(EmailConfig.class); + public static class Identity { private String from; private String name; @@ -61,8 +63,6 @@ public class EmailConfig { private String generator; private String connector; - private Logger log = LoggerFactory.getLogger(EmailConfig.class); - private MatrixConfig mxCfg; private Identity identity = new Identity(); diff --git a/src/main/java/io/kamax/mxisd/config/threepid/medium/EmailTemplateConfig.java b/src/main/java/io/kamax/mxisd/config/threepid/medium/EmailTemplateConfig.java index a426dc4..00a5a3d 100644 --- a/src/main/java/io/kamax/mxisd/config/threepid/medium/EmailTemplateConfig.java +++ b/src/main/java/io/kamax/mxisd/config/threepid/medium/EmailTemplateConfig.java @@ -20,7 +20,6 @@ package io.kamax.mxisd.config.threepid.medium; -import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -30,70 +29,9 @@ import javax.annotation.PostConstruct; @Configuration @ConfigurationProperties("threepid.medium.email.generators.template") -public class EmailTemplateConfig { +public class EmailTemplateConfig extends GenericTemplateConfig { private static Logger log = LoggerFactory.getLogger(EmailTemplateConfig.class); - private static final String classpathPrefix = "classpath:"; - - private static String getName(String path) { - if (StringUtils.startsWith(path, classpathPrefix)) { - return "Built-in (" + path.substring(classpathPrefix.length()) + ")"; - } - - return path; - } - - public static class Session { - - public static class SessionValidation { - - private String local; - private String remote; - - public String getLocal() { - return local; - } - - public void setLocal(String local) { - this.local = local; - } - - public String getRemote() { - return remote; - } - - public void setRemote(String remote) { - this.remote = remote; - } - - } - - private SessionValidation validation; - - public SessionValidation getValidation() { - return validation; - } - - public void setValidation(SessionValidation validation) { - this.validation = validation; - } - - } - - private String invite; - private Session session = new Session(); - - public String getInvite() { - return invite; - } - - public void setInvite(String invite) { - this.invite = invite; - } - - public Session getSession() { - return session; - } @PostConstruct public void build() { diff --git a/src/main/java/io/kamax/mxisd/config/threepid/medium/GenericTemplateConfig.java b/src/main/java/io/kamax/mxisd/config/threepid/medium/GenericTemplateConfig.java new file mode 100644 index 0000000..cc848c7 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/config/threepid/medium/GenericTemplateConfig.java @@ -0,0 +1,89 @@ +/* + * 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 org.apache.commons.lang.StringUtils; + +public class GenericTemplateConfig { + + private static final String classpathPrefix = "classpath:"; + + protected static String getName(String path) { + if (StringUtils.startsWith(path, classpathPrefix)) { + return "Built-in (" + path.substring(classpathPrefix.length()) + ")"; + } + + return path; + } + + public static class Session { + + public static class SessionValidation { + + private String local; + private String remote; + + public String getLocal() { + return local; + } + + public void setLocal(String local) { + this.local = local; + } + + public String getRemote() { + return remote; + } + + public void setRemote(String remote) { + this.remote = remote; + } + + } + + private SessionValidation validation; + + public SessionValidation getValidation() { + return validation; + } + + public void setValidation(SessionValidation validation) { + this.validation = validation; + } + + } + + private String invite; + private Session session = new Session(); + + public String getInvite() { + return invite; + } + + public void setInvite(String invite) { + this.invite = invite; + } + + public Session getSession() { + return session; + } + +} diff --git a/src/main/java/io/kamax/mxisd/config/threepid/medium/PhoneConfig.java b/src/main/java/io/kamax/mxisd/config/threepid/medium/PhoneConfig.java new file mode 100644 index 0000000..91dfbd2 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/config/threepid/medium/PhoneConfig.java @@ -0,0 +1,73 @@ +/* + * 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.exception.ConfigurationException; +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import javax.annotation.PostConstruct; + +@Configuration +@ConfigurationProperties("threepid.medium.msisdn") +public class PhoneConfig { + + private Logger log = LoggerFactory.getLogger(PhoneConfig.class); + + private String generator; + private String connector; + + public String getGenerator() { + return generator; + } + + public void setGenerator(String generator) { + this.generator = generator; + } + + public String getConnector() { + return connector; + } + + public void setConnector(String connector) { + this.connector = connector; + } + + @PostConstruct + public void build() { + log.info("--- Phone config ---"); + + if (StringUtils.isBlank(getGenerator())) { + throw new ConfigurationException("generator"); + } + + if (StringUtils.isBlank(getConnector())) { + throw new ConfigurationException("connector"); + } + + log.info("Generator: {}", getGenerator()); + log.info("Connector: {}", getConnector()); + } + +} diff --git a/src/main/java/io/kamax/mxisd/config/threepid/medium/PhoneSmsTemplateConfig.java b/src/main/java/io/kamax/mxisd/config/threepid/medium/PhoneSmsTemplateConfig.java new file mode 100644 index 0000000..5331470 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/config/threepid/medium/PhoneSmsTemplateConfig.java @@ -0,0 +1,45 @@ +/* + * 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 org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import javax.annotation.PostConstruct; + +@Configuration +@ConfigurationProperties("threepid.medium.msisdn.generators.template") +public class PhoneSmsTemplateConfig extends GenericTemplateConfig { + + private static Logger log = LoggerFactory.getLogger(EmailTemplateConfig.class); + + @PostConstruct + public void build() { + log.info("--- SMS Generator templates config ---"); + log.info("Invite: {}", getName(getInvite())); + log.info("Session validation:"); + log.info("\tLocal: {}", getName(getSession().getValidation().getLocal())); + log.info("\tRemote: {}", getName(getSession().getValidation().getRemote())); + } + +} diff --git a/src/main/java/io/kamax/mxisd/controller/v1/DefaultExceptionHandler.java b/src/main/java/io/kamax/mxisd/controller/v1/DefaultExceptionHandler.java index 8d03874..19a3407 100644 --- a/src/main/java/io/kamax/mxisd/controller/v1/DefaultExceptionHandler.java +++ b/src/main/java/io/kamax/mxisd/controller/v1/DefaultExceptionHandler.java @@ -51,6 +51,7 @@ public class DefaultExceptionHandler { JsonObject obj = new JsonObject(); obj.addProperty("errcode", erroCode); obj.addProperty("error", error); + obj.addProperty("success", false); return gson.toJson(obj); } diff --git a/src/main/java/io/kamax/mxisd/controller/v1/IdentityAPIv1.java b/src/main/java/io/kamax/mxisd/controller/v1/IdentityAPIv1.java index f2b425f..a069ff8 100644 --- a/src/main/java/io/kamax/mxisd/controller/v1/IdentityAPIv1.java +++ b/src/main/java/io/kamax/mxisd/controller/v1/IdentityAPIv1.java @@ -24,4 +24,9 @@ public class IdentityAPIv1 { public static final String BASE = "/_matrix/identity/api/v1"; + public static String getValidate(String medium, String sid, String secret, String token) { + // FIXME use some kind of URLBuilder + return BASE + "/validate/" + medium + "/submitToken?sid=" + sid + "&client_secret=" + secret + "&token=" + token; + } + } diff --git a/src/main/java/io/kamax/mxisd/controller/v1/SessionController.java b/src/main/java/io/kamax/mxisd/controller/v1/SessionController.java index 03ca111..319f441 100644 --- a/src/main/java/io/kamax/mxisd/controller/v1/SessionController.java +++ b/src/main/java/io/kamax/mxisd/controller/v1/SessionController.java @@ -38,6 +38,8 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import static org.springframework.web.bind.annotation.RequestMethod.GET; + @Controller @RequestMapping(path = IdentityAPIv1.BASE) class SessionController { @@ -52,9 +54,8 @@ class SessionController { @Autowired private ViewConfig viewCfg; - ; - @RequestMapping(value = "/validate/{medium}/submitToken") + @RequestMapping(value = "/validate/{medium}/submitToken", method = GET) public String validate( HttpServletRequest request, HttpServletResponse response, diff --git a/src/main/java/io/kamax/mxisd/controller/v1/SessionRestController.java b/src/main/java/io/kamax/mxisd/controller/v1/SessionRestController.java index aa7061c..163dc88 100644 --- a/src/main/java/io/kamax/mxisd/controller/v1/SessionRestController.java +++ b/src/main/java/io/kamax/mxisd/controller/v1/SessionRestController.java @@ -28,23 +28,28 @@ import io.kamax.mxisd.config.ServerConfig; import io.kamax.mxisd.config.ViewConfig; import io.kamax.mxisd.controller.v1.io.SessionEmailTokenRequestJson; import io.kamax.mxisd.controller.v1.io.SessionPhoneTokenRequestJson; +import io.kamax.mxisd.controller.v1.io.SuccessStatusJson; import io.kamax.mxisd.exception.BadRequestException; import io.kamax.mxisd.exception.SessionNotValidatedException; import io.kamax.mxisd.invitation.InvitationManager; import io.kamax.mxisd.lookup.ThreePidValidation; import io.kamax.mxisd.session.SessionMananger; +import io.kamax.mxisd.session.ValidationResult; import io.kamax.mxisd.util.GsonParser; import org.apache.http.HttpStatus; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; +import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import static org.springframework.web.bind.annotation.RequestMethod.POST; + @RestController @CrossOrigin @RequestMapping(path = IdentityAPIv1.BASE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) @@ -114,6 +119,23 @@ public class SessionRestController { return gson.toJson(obj); } + @RequestMapping(value = "/validate/{medium}/submitToken", method = POST) + public String validate( + HttpServletRequest request, + HttpServletResponse response, + @RequestParam String sid, + @RequestParam("client_secret") String secret, + @RequestParam String token, + Model model + ) { + log.info("Requested: {}", request.getRequestURL()); + + ValidationResult r = mgr.validate(sid, secret, token); + log.info("Session {} was validated", sid); + + return gson.toJson(new SuccessStatusJson(true)); + } + @RequestMapping(value = "/3pid/getValidated3pid") String check(HttpServletRequest request, HttpServletResponse response, @RequestParam String sid, @RequestParam("client_secret") String secret) { diff --git a/src/main/java/io/kamax/mxisd/controller/v1/io/SessionEmailTokenRequestJson.java b/src/main/java/io/kamax/mxisd/controller/v1/io/SessionEmailTokenRequestJson.java index 527f1e7..84de8f4 100644 --- a/src/main/java/io/kamax/mxisd/controller/v1/io/SessionEmailTokenRequestJson.java +++ b/src/main/java/io/kamax/mxisd/controller/v1/io/SessionEmailTokenRequestJson.java @@ -25,7 +25,7 @@ public class SessionEmailTokenRequestJson extends GenericTokenRequestJson { private String email; public String getMedium() { - return "email"; + return "threepids/email"; } public String getValue() { diff --git a/src/main/java/io/kamax/mxisd/controller/v1/io/SuccessStatusJson.java b/src/main/java/io/kamax/mxisd/controller/v1/io/SuccessStatusJson.java new file mode 100644 index 0000000..e7f43b1 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/controller/v1/io/SuccessStatusJson.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.controller.v1.io; + +public class SuccessStatusJson { + + private boolean success; + + public SuccessStatusJson(boolean success) { + this.success = success; + } + + public boolean isSuccess() { + return success; + } + +} diff --git a/src/main/java/io/kamax/mxisd/exception/MessageForClientException.java b/src/main/java/io/kamax/mxisd/exception/MessageForClientException.java new file mode 100644 index 0000000..bf81e21 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/exception/MessageForClientException.java @@ -0,0 +1,31 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.exception; + +import org.apache.http.HttpStatus; + +public class MessageForClientException extends MatrixException { + + public MessageForClientException(String error) { + super(HttpStatus.SC_OK, "M_MESSAGE_FOR_CLIENT", error); + } + +} diff --git a/src/main/java/io/kamax/mxisd/exception/RemoteIdentityServerException.java b/src/main/java/io/kamax/mxisd/exception/RemoteIdentityServerException.java index 1e65fc4..7077fe2 100644 --- a/src/main/java/io/kamax/mxisd/exception/RemoteIdentityServerException.java +++ b/src/main/java/io/kamax/mxisd/exception/RemoteIdentityServerException.java @@ -25,7 +25,7 @@ import org.apache.http.HttpStatus; public class RemoteIdentityServerException extends MatrixException { public RemoteIdentityServerException(String error) { - super(HttpStatus.SC_SERVICE_UNAVAILABLE, "M_REMOTE_IS_ERROR", error); + super(HttpStatus.SC_SERVICE_UNAVAILABLE, "M_REMOTE_IS_ERROR", "Error from remote server: " + error); } } diff --git a/src/main/java/io/kamax/mxisd/lookup/provider/DnsLookupProvider.java b/src/main/java/io/kamax/mxisd/lookup/provider/DnsLookupProvider.java index 89e1f77..5564e54 100644 --- a/src/main/java/io/kamax/mxisd/lookup/provider/DnsLookupProvider.java +++ b/src/main/java/io/kamax/mxisd/lookup/provider/DnsLookupProvider.java @@ -83,7 +83,7 @@ class DnsLookupProvider implements IThreePidProvider { @Override public Optional find(SingleLookupRequest request) { - if (!StringUtils.equals("email", request.getType())) { // TODO use enum + if (!StringUtils.equals("threepids/email", request.getType())) { // TODO use enum log.info("Skipping unsupported type {} for {}", request.getType(), request.getThreePid()); return Optional.empty(); } @@ -106,7 +106,7 @@ class DnsLookupProvider implements IThreePidProvider { Map> domains = new HashMap<>(); for (ThreePidMapping mapping : mappings) { - if (!StringUtils.equals("email", mapping.getMedium())) { + if (!StringUtils.equals("threepids/email", mapping.getMedium())) { log.info("Skipping unsupported type {} for {}", mapping.getMedium(), mapping.getValue()); continue; } diff --git a/src/main/java/io/kamax/mxisd/matrix/IdentityServerUtils.java b/src/main/java/io/kamax/mxisd/matrix/IdentityServerUtils.java index 34b1a6a..5441a3f 100644 --- a/src/main/java/io/kamax/mxisd/matrix/IdentityServerUtils.java +++ b/src/main/java/io/kamax/mxisd/matrix/IdentityServerUtils.java @@ -21,7 +21,7 @@ import java.util.Optional; // FIXME placeholder, this must go in matrix-java-sdk for 1.0 public class IdentityServerUtils { - public static final String THREEPID_TEST_MEDIUM = "email"; + public static final String THREEPID_TEST_MEDIUM = "threepids/email"; public static final String THREEPID_TEST_ADDRESS = "mxisd-email-forever-unknown@forever-invalid.kamax.io"; private static Logger log = LoggerFactory.getLogger(IdentityServerUtils.class); diff --git a/src/main/java/io/kamax/mxisd/session/SessionMananger.java b/src/main/java/io/kamax/mxisd/session/SessionMananger.java index 1f41029..f50fa6d 100644 --- a/src/main/java/io/kamax/mxisd/session/SessionMananger.java +++ b/src/main/java/io/kamax/mxisd/session/SessionMananger.java @@ -21,6 +21,9 @@ package io.kamax.mxisd.session; import com.google.gson.JsonObject; +import com.google.i18n.phonenumbers.NumberParseException; +import com.google.i18n.phonenumbers.PhoneNumberUtil; +import com.google.i18n.phonenumbers.Phonenumber; import io.kamax.matrix.MatrixID; import io.kamax.matrix.ThreePidMedium; import io.kamax.matrix._MatrixID; @@ -73,6 +76,9 @@ public class SessionMananger { private IStorage storage; private NotificationManager notifMgr; + private GsonParser parser = new GsonParser(); + private PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance(); // FIXME refactor for sessions handling their own stuff + // FIXME export into central class, set version private CloseableHttpClient client = HttpClients.custom().setUserAgent("mxisd").build(); @@ -180,9 +186,21 @@ public class SessionMananger { throw new NotAllowedException("Validating " + (isLocal ? "local" : "remote") + " 3PID is not allowed"); } + if (ThreePidMedium.PhoneNumber.is(session.getThreePid().getMedium()) && session.isValidated() && session.isRemote()) { + submitRemote(session, token); + session.validateRemote(); + return new ValidationResult(session, false); + } + session.validate(token); storage.updateThreePidSession(session.getDao()); - log.info("Session {} has been validated", session.getId()); + log.info("Session {} has been validated locally", session.getId()); + + if (ThreePidMedium.PhoneNumber.is(session.getThreePid().getMedium()) && session.isValidated()) { + createRemote(sid, secret); + // FIXME make the message configurable/customizable (templates?) + throw new MessageForClientException("You will receive a NEW code from another number. Enter it below"); + } // FIXME definitely doable in a nicer way ValidationResult r = new ValidationResult(session, policy.toRemote()); @@ -265,13 +283,22 @@ public class SessionMananger { body.addProperty("client_secret", remoteSecret); body.addProperty(session.getThreePid().getMedium(), session.getThreePid().getAddress()); body.addProperty("send_attempt", session.increaseAndGetRemoteAttempt()); + try { + Phonenumber.PhoneNumber msisdn = phoneUtil.parse("+" + session.getThreePid().getAddress(), null); + String country = phoneUtil.getRegionCodeForNumber(msisdn).toUpperCase(); + body.addProperty("phone_number", phoneUtil.format(msisdn, PhoneNumberUtil.PhoneNumberFormat.NATIONAL)); + body.addProperty("country", country); + } catch (NumberParseException e) { + throw new InternalServerError(e); + } log.info("Requesting remote session with attempt {}", session.getRemoteAttempt()); HttpPost tokenReq = RestClientUtils.post(url + "/_matrix/identity/api/v1/validate/" + session.getThreePid().getMedium() + "/requestToken", body); try (CloseableHttpResponse response = client.execute(tokenReq)) { int status = response.getStatusLine().getStatusCode(); if (status < 200 || status >= 300) { - throw new RemoteIdentityServerException("Remote identity server returned with status " + status); + JsonObject obj = parser.parseOptional(response).orElseThrow(() -> new RemoteIdentityServerException("Status " + status)); + throw new RemoteIdentityServerException(obj.get("errcode").getAsString() + ": " + obj.get("error").getAsString()); } RequestTokenResponse data = new GsonParser().parse(response, RequestTokenResponse.class); @@ -288,6 +315,29 @@ public class SessionMananger { } } + private void submitRemote(ThreePidSession session, String token) { + UrlEncodedFormEntity entity = new UrlEncodedFormEntity( + Arrays.asList( + new BasicNameValuePair("sid", session.getRemoteId()), + new BasicNameValuePair("client_secret", session.getRemoteSecret()), + new BasicNameValuePair("token", token) + ), StandardCharsets.UTF_8); + HttpPost submitReq = new HttpPost(session.getRemoteServer() + "/_matrix/identity/api/v1/submitToken"); + submitReq.setEntity(entity); + + try (CloseableHttpResponse response = client.execute(submitReq)) { + JsonObject o = new GsonParser().parse(response.getEntity().getContent()); + if (!o.has("success") || !o.get("success").getAsBoolean()) { + String errcode = o.get("errcode").getAsString(); + throw new RemoteIdentityServerException(errcode + ": " + o.get("error").getAsString()); + } + + log.info("Successfully submitted validation token for {} to {}", session.getThreePid(), session.getRemoteServer()); + } catch (IOException e) { + throw new RemoteIdentityServerException(e.getMessage()); + } + } + public void validateRemote(String sid, String secret) { ThreePidSession session = getSessionIfValidated(sid, secret); if (!session.isRemote()) { diff --git a/src/main/java/io/kamax/mxisd/threepid/connector/phone/IPhoneConnector.java b/src/main/java/io/kamax/mxisd/threepid/connector/phone/IPhoneConnector.java new file mode 100644 index 0000000..4b1df97 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/threepid/connector/phone/IPhoneConnector.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.threepid.connector.phone; + +import io.kamax.matrix.ThreePidMedium; +import io.kamax.mxisd.threepid.connector.IThreePidConnector; + +public interface IPhoneConnector extends IThreePidConnector { + + @Override + default String getMedium() { + return ThreePidMedium.PhoneNumber.getId(); + } + + void send(String recipient, String content); + +} diff --git a/src/main/java/io/kamax/mxisd/threepid/connector/phone/PhoneSmsTwilioConnector.java b/src/main/java/io/kamax/mxisd/threepid/connector/phone/PhoneSmsTwilioConnector.java new file mode 100644 index 0000000..f46bd2b --- /dev/null +++ b/src/main/java/io/kamax/mxisd/threepid/connector/phone/PhoneSmsTwilioConnector.java @@ -0,0 +1,59 @@ +/* + * 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.connector.phone; + +import com.twilio.Twilio; +import com.twilio.rest.api.v2010.account.Message; +import com.twilio.type.PhoneNumber; +import io.kamax.mxisd.config.threepid.connector.PhoneTwilioConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class PhoneSmsTwilioConnector implements IPhoneConnector { + + private Logger log = LoggerFactory.getLogger(PhoneSmsTwilioConnector.class); + + private PhoneTwilioConfig cfg; + + @Autowired + public PhoneSmsTwilioConnector(PhoneTwilioConfig cfg) { + this.cfg = cfg; + + Twilio.init(cfg.getAccountSid(), cfg.getAuthToken()); + log.info("Twilio API has been initiated"); + } + + @Override + public String getId() { + return "twilio"; + } + + @Override + public void send(String recipient, String content) { + recipient = "+" + recipient; + log.info("Sending SMS notification from {} to {} with {} characters", cfg.getNumber(), recipient, content.length()); + Message.creator(new PhoneNumber("+" + recipient), new PhoneNumber(cfg.getNumber()), content).create(); + } + +} diff --git a/src/main/java/io/kamax/mxisd/threepid/notification/GenericNotificationHandler.java b/src/main/java/io/kamax/mxisd/threepid/notification/GenericNotificationHandler.java new file mode 100644 index 0000000..49a4c06 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/threepid/notification/GenericNotificationHandler.java @@ -0,0 +1,72 @@ +/* + * 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.notification; + +import io.kamax.mxisd.exception.ConfigurationException; +import io.kamax.mxisd.invitation.IThreePidInviteReply; +import io.kamax.mxisd.notification.INotificationHandler; +import io.kamax.mxisd.threepid.connector.IThreePidConnector; +import io.kamax.mxisd.threepid.session.IThreePidSession; +import org.apache.commons.lang.StringUtils; + +import java.util.List; + +public abstract class GenericNotificationHandler implements INotificationHandler { + + private A connector; + private B generator; + + protected abstract String getConnectorId(); + + protected abstract String getGeneratorId(); + + protected abstract void send(A connector, String recipient, String content); + + protected void process(List connectors, List generators) { + generator = generators.stream() + .filter(o -> StringUtils.equals(getGeneratorId(), o.getId())) + .findFirst() + .orElseThrow(() -> new ConfigurationException(getMedium() + " notification generator [" + + getGeneratorId() + "] could not be found")); + + connector = connectors.stream() + .filter(o -> StringUtils.equals(getConnectorId(), o.getId())) + .findFirst() + .orElseThrow(() -> new ConfigurationException(getMedium() + " sender connector [" + + getConnectorId() + "] could not be found")); + } + + @Override + public void sendForInvite(IThreePidInviteReply invite) { + send(connector, invite.getInvite().getAddress(), generator.getForInvite(invite)); + } + + @Override + public void sendForValidation(IThreePidSession session) { + send(connector, session.getThreePid().getAddress(), generator.getForValidation(session)); + } + + @Override + public void sendForRemoteValidation(IThreePidSession session) { + send(connector, session.getThreePid().getAddress(), generator.getForRemoteValidation(session)); + } + +} diff --git a/src/main/java/io/kamax/mxisd/threepid/notification/GenericTemplateNotificationGenerator.java b/src/main/java/io/kamax/mxisd/threepid/notification/GenericTemplateNotificationGenerator.java new file mode 100644 index 0000000..bf2ef50 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/threepid/notification/GenericTemplateNotificationGenerator.java @@ -0,0 +1,150 @@ +/* + * 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.notification; + +import io.kamax.mxisd.ThreePid; +import io.kamax.mxisd.config.MatrixConfig; +import io.kamax.mxisd.config.ServerConfig; +import io.kamax.mxisd.config.threepid.medium.GenericTemplateConfig; +import io.kamax.mxisd.controller.v1.IdentityAPIv1; +import io.kamax.mxisd.exception.InternalServerError; +import io.kamax.mxisd.invitation.IThreePidInviteReply; +import io.kamax.mxisd.threepid.session.IThreePidSession; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.WordUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +@Component +public abstract class GenericTemplateNotificationGenerator implements INotificationGenerator { + + private Logger log = LoggerFactory.getLogger(GenericTemplateNotificationGenerator.class); + + private MatrixConfig mxCfg; + private ServerConfig srvCfg; + private GenericTemplateConfig cfg; + + @Autowired + private ApplicationContext app; + + public GenericTemplateNotificationGenerator(MatrixConfig mxCfg, ServerConfig srvCfg, GenericTemplateConfig cfg) { + this.mxCfg = mxCfg; + this.srvCfg = srvCfg; + this.cfg = cfg; + } + + protected String populateForCommon(String body, ThreePid recipient) { + return body; + } + + private String populateCommon(String body, ThreePid recipient) { + body = populateForCommon(body, recipient); + + String domainPretty = WordUtils.capitalizeFully(mxCfg.getDomain()); + body = body.replace("%DOMAIN%", mxCfg.getDomain()); + body = body.replace("%DOMAIN_PRETTY%", domainPretty); + body = body.replace("%RECIPIENT_MEDIUM%", recipient.getMedium()); + body = body.replace("%RECIPIENT_ADDRESS%", recipient.getAddress()); + + return body; + } + + private String getTemplateContent(String location) { + try { + InputStream is = StringUtils.startsWith(location, "classpath:") ? + app.getResource(location).getInputStream() : new FileInputStream(location); + return IOUtils.toString(is, StandardCharsets.UTF_8); + } catch (IOException e) { + throw new InternalServerError("Unable to read template content at " + location + ": " + e.getMessage()); + } + } + + private String getTemplateAndPopulate(String location, ThreePid recipient) { + return populateCommon(getTemplateContent(location), recipient); + } + + @Override + public String getForInvite(IThreePidInviteReply invite) { + ThreePid tpid = new ThreePid(invite.getInvite().getMedium(), invite.getInvite().getAddress()); + String templateBody = getTemplateAndPopulate(cfg.getInvite(), tpid); + + String senderName = invite.getInvite().getProperties().getOrDefault("sender_display_name", ""); + String senderNameOrId = StringUtils.defaultIfBlank(senderName, invite.getInvite().getSender().getId()); + String roomName = invite.getInvite().getProperties().getOrDefault("room_name", ""); + String roomNameOrId = StringUtils.defaultIfBlank(roomName, invite.getInvite().getRoomId()); + + templateBody = templateBody.replace("%SENDER_ID%", invite.getInvite().getSender().getId()); + templateBody = templateBody.replace("%SENDER_NAME%", senderName); + templateBody = templateBody.replace("%SENDER_NAME_OR_ID%", senderNameOrId); + templateBody = templateBody.replace("%INVITE_MEDIUM%", tpid.getMedium()); + templateBody = templateBody.replace("%INVITE_ADDRESS%", tpid.getAddress()); + templateBody = templateBody.replace("%ROOM_ID%", invite.getInvite().getRoomId()); + templateBody = templateBody.replace("%ROOM_NAME%", roomName); + templateBody = templateBody.replace("%ROOM_NAME_OR_ID%", roomNameOrId); + + return templateBody; + } + + @Override + public String getForValidation(IThreePidSession session) { + log.info("Generating notification content for 3PID Session validation"); + String templateBody = getTemplateAndPopulate(cfg.getSession().getValidation().getLocal(), session.getThreePid()); + + String validationLink = srvCfg.getPublicUrl() + IdentityAPIv1.getValidate( + session.getThreePid().getMedium(), + session.getId(), + session.getSecret(), + session.getToken()); + + templateBody = templateBody.replace("%VALIDATION_LINK%", validationLink); + templateBody = templateBody.replace("%VALIDATION_TOKEN%", session.getToken()); + + return templateBody; + } + + @Override + public String getForRemoteValidation(IThreePidSession session) { + log.info("Generating notification content for remote-only 3PID session"); + String templateBody = getTemplateAndPopulate(cfg.getSession().getValidation().getRemote(), session.getThreePid()); + + String validationLink = srvCfg.getPublicUrl() + IdentityAPIv1.getValidate( + session.getThreePid().getMedium(), + session.getId(), + session.getSecret(), + session.getToken()); + + templateBody = templateBody.replace("%VALIDATION_LINK%", validationLink); + templateBody = templateBody.replace("%VALIDATION_TOKEN%", session.getToken()); + templateBody = templateBody.replace("%NEXT_URL%", validationLink); + + return templateBody; + } + +} diff --git a/src/main/java/io/kamax/mxisd/threepid/notification/email/EmailNotificationGenerator.java b/src/main/java/io/kamax/mxisd/threepid/notification/email/EmailNotificationGenerator.java index e39afc0..263679c 100644 --- a/src/main/java/io/kamax/mxisd/threepid/notification/email/EmailNotificationGenerator.java +++ b/src/main/java/io/kamax/mxisd/threepid/notification/email/EmailNotificationGenerator.java @@ -25,43 +25,19 @@ import io.kamax.mxisd.config.MatrixConfig; import io.kamax.mxisd.config.ServerConfig; import io.kamax.mxisd.config.threepid.medium.EmailConfig; import io.kamax.mxisd.config.threepid.medium.EmailTemplateConfig; -import io.kamax.mxisd.controller.v1.IdentityAPIv1; -import io.kamax.mxisd.exception.InternalServerError; -import io.kamax.mxisd.invitation.IThreePidInviteReply; -import io.kamax.mxisd.threepid.session.IThreePidSession; -import org.apache.commons.io.IOUtils; -import org.apache.commons.lang.StringUtils; -import org.apache.commons.lang.WordUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import io.kamax.mxisd.threepid.notification.GenericTemplateNotificationGenerator; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Component; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; - @Component -public class EmailNotificationGenerator implements IEmailNotificationGenerator { - - private Logger log = LoggerFactory.getLogger(EmailNotificationGenerator.class); +public class EmailNotificationGenerator extends GenericTemplateNotificationGenerator implements IEmailNotificationGenerator { private EmailConfig cfg; - private EmailTemplateConfig templateCfg; - private MatrixConfig mxCfg; - private ServerConfig srvCfg; - - @Autowired - private ApplicationContext app; @Autowired public EmailNotificationGenerator(EmailTemplateConfig templateCfg, EmailConfig cfg, MatrixConfig mxCfg, ServerConfig srvCfg) { + super(mxCfg, srvCfg, templateCfg); this.cfg = cfg; - this.templateCfg = templateCfg; - this.mxCfg = mxCfg; - this.srvCfg = srvCfg; } @Override @@ -69,85 +45,11 @@ public class EmailNotificationGenerator implements IEmailNotificationGenerator { return "template"; } - private String getTemplateContent(String location) { - try { - InputStream is = StringUtils.startsWith(location, "classpath:") ? - app.getResource(location).getInputStream() : new FileInputStream(location); - return IOUtils.toString(is, StandardCharsets.UTF_8); - } catch (IOException e) { - throw new InternalServerError("Unable to read template content at " + location + ": " + e.getMessage()); - } - } - - private String populateCommon(String content, ThreePid recipient) { - String domainPretty = WordUtils.capitalizeFully(mxCfg.getDomain()); - - content = content.replace("%DOMAIN%", mxCfg.getDomain()); - content = content.replace("%DOMAIN_PRETTY%", domainPretty); - content = content.replace("%FROM_EMAIL%", cfg.getIdentity().getFrom()); - content = content.replace("%FROM_NAME%", cfg.getIdentity().getName()); - content = content.replace("%RECIPIENT_MEDIUM%", recipient.getMedium()); - content = content.replace("%RECIPIENT_ADDRESS%", recipient.getAddress()); - return content; - } - - private String getTemplateAndPopulate(String location, ThreePid recipient) { - return populateCommon(getTemplateContent(location), recipient); - } - @Override - public String getForInvite(IThreePidInviteReply invite) { - ThreePid tpid = new ThreePid(invite.getInvite().getMedium(), invite.getInvite().getAddress()); - String templateBody = getTemplateAndPopulate(templateCfg.getInvite(), tpid); - - String senderName = invite.getInvite().getProperties().getOrDefault("sender_display_name", ""); - String senderNameOrId = StringUtils.defaultIfBlank(senderName, invite.getInvite().getSender().getId()); - String roomName = invite.getInvite().getProperties().getOrDefault("room_name", ""); - String roomNameOrId = StringUtils.defaultIfBlank(roomName, invite.getInvite().getRoomId()); - - templateBody = templateBody.replace("%SENDER_ID%", invite.getInvite().getSender().getId()); - templateBody = templateBody.replace("%SENDER_NAME%", senderName); - templateBody = templateBody.replace("%SENDER_NAME_OR_ID%", senderNameOrId); - templateBody = templateBody.replace("%INVITE_MEDIUM%", tpid.getMedium()); - templateBody = templateBody.replace("%INVITE_ADDRESS%", tpid.getAddress()); - templateBody = templateBody.replace("%ROOM_ID%", invite.getInvite().getRoomId()); - templateBody = templateBody.replace("%ROOM_NAME%", roomName); - templateBody = templateBody.replace("%ROOM_NAME_OR_ID%", roomNameOrId); - - return templateBody; - } - - @Override - public String getForValidation(IThreePidSession session) { - log.info("Generating notification content for 3PID Session validation"); - String templateBody = getTemplateAndPopulate(templateCfg.getSession().getValidation().getLocal(), session.getThreePid()); - - // FIXME should have a global link builder, most likely in the SDK? - String validationLink = srvCfg.getPublicUrl() + IdentityAPIv1.BASE + - "/validate/" + session.getThreePid().getMedium() + - "/submitToken?sid=" + session.getId() + "&client_secret=" + session.getSecret() + - "&token=" + session.getToken(); - - templateBody = templateBody.replace("%VALIDATION_LINK%", validationLink); - templateBody = templateBody.replace("%VALIDATION_TOKEN%", session.getToken()); - - return templateBody; - } - - @Override - public String getForRemoteValidation(IThreePidSession session) { - log.info("Generating notification content for remote-only 3PID session"); - String templateBody = getTemplateAndPopulate(templateCfg.getSession().getValidation().getRemote(), session.getThreePid()); - - // FIXME should have a global link builder, most likely in the SDK? - String validationLink = srvCfg.getPublicUrl() + IdentityAPIv1.BASE + - "/validate/" + session.getThreePid().getMedium() + - "/submitToken?sid=" + session.getId() + "&client_secret=" + session.getSecret() + - "&token=" + session.getToken(); - - templateBody = templateBody.replace("%NEXT_URL%", validationLink); - - return templateBody; + protected String populateForCommon(String body, ThreePid recipient) { + body = body.replace("%FROM_EMAIL%", cfg.getIdentity().getFrom()); + body = body.replace("%FROM_NAME%", cfg.getIdentity().getName()); + return body; } } diff --git a/src/main/java/io/kamax/mxisd/threepid/notification/email/EmailNotificationHandler.java b/src/main/java/io/kamax/mxisd/threepid/notification/email/EmailNotificationHandler.java index 46c4b2e..8991610 100644 --- a/src/main/java/io/kamax/mxisd/threepid/notification/email/EmailNotificationHandler.java +++ b/src/main/java/io/kamax/mxisd/threepid/notification/email/EmailNotificationHandler.java @@ -22,37 +22,22 @@ package io.kamax.mxisd.threepid.notification.email; import io.kamax.matrix.ThreePidMedium; import io.kamax.mxisd.config.threepid.medium.EmailConfig; -import io.kamax.mxisd.exception.ConfigurationException; -import io.kamax.mxisd.invitation.IThreePidInviteReply; -import io.kamax.mxisd.notification.INotificationHandler; import io.kamax.mxisd.threepid.connector.email.IEmailConnector; -import io.kamax.mxisd.threepid.session.IThreePidSession; -import org.apache.commons.lang.StringUtils; +import io.kamax.mxisd.threepid.notification.GenericNotificationHandler; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.util.List; @Component -public class EmailNotificationHandler implements INotificationHandler { +public class EmailNotificationHandler extends GenericNotificationHandler { private EmailConfig cfg; - private IEmailNotificationGenerator generator; - private IEmailConnector connector; @Autowired public EmailNotificationHandler(EmailConfig cfg, List generators, List connectors) { this.cfg = cfg; - - generator = generators.stream() - .filter(o -> StringUtils.equals(cfg.getGenerator(), o.getId())) - .findFirst() - .orElseThrow(() -> new ConfigurationException("Email notification generator [" + cfg.getGenerator() + "] could not be found")); - - connector = connectors.stream() - .filter(o -> StringUtils.equals(cfg.getConnector(), o.getId())) - .findFirst() - .orElseThrow(() -> new ConfigurationException("Email sender connector [" + cfg.getConnector() + "] could not be found")); + process(connectors, generators); } @Override @@ -60,7 +45,18 @@ public class EmailNotificationHandler implements INotificationHandler { return ThreePidMedium.Email.getId(); } - private void send(String recipient, String content) { + @Override + protected String getConnectorId() { + return cfg.getConnector(); + } + + @Override + protected String getGeneratorId() { + return cfg.getGenerator(); + } + + @Override + protected void send(IEmailConnector connector, String recipient, String content) { connector.send( cfg.getIdentity().getFrom(), cfg.getIdentity().getName(), @@ -69,19 +65,4 @@ public class EmailNotificationHandler implements INotificationHandler { ); } - @Override - public void sendForInvite(IThreePidInviteReply invite) { - send(invite.getInvite().getAddress(), generator.getForInvite(invite)); - } - - @Override - public void sendForValidation(IThreePidSession session) { - send(session.getThreePid().getAddress(), generator.getForValidation(session)); - } - - @Override - public void sendForRemoteValidation(IThreePidSession session) { - send(session.getThreePid().getAddress(), generator.getForRemoteValidation(session)); - } - } diff --git a/src/main/java/io/kamax/mxisd/threepid/notification/phone/IPhoneNotificationGenerator.java b/src/main/java/io/kamax/mxisd/threepid/notification/phone/IPhoneNotificationGenerator.java new file mode 100644 index 0000000..dbab934 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/threepid/notification/phone/IPhoneNotificationGenerator.java @@ -0,0 +1,31 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.threepid.notification.phone; + +import io.kamax.mxisd.threepid.notification.INotificationGenerator; + +public interface IPhoneNotificationGenerator extends INotificationGenerator { + + default String getMedium() { + return "msisdn"; + } + +} diff --git a/src/main/java/io/kamax/mxisd/threepid/notification/phone/PhoneNotificationHandler.java b/src/main/java/io/kamax/mxisd/threepid/notification/phone/PhoneNotificationHandler.java new file mode 100644 index 0000000..ff651b6 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/threepid/notification/phone/PhoneNotificationHandler.java @@ -0,0 +1,63 @@ +/* + * 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.notification.phone; + +import io.kamax.matrix.ThreePidMedium; +import io.kamax.mxisd.config.threepid.medium.PhoneConfig; +import io.kamax.mxisd.threepid.connector.phone.IPhoneConnector; +import io.kamax.mxisd.threepid.notification.GenericNotificationHandler; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +public class PhoneNotificationHandler extends GenericNotificationHandler { + + private PhoneConfig cfg; + + @Autowired + public PhoneNotificationHandler(PhoneConfig cfg, List connectors, List generators) { + this.cfg = cfg; + process(connectors, generators); + } + + @Override + public String getMedium() { + return ThreePidMedium.PhoneNumber.getId(); + } + + @Override + protected String getConnectorId() { + return cfg.getConnector(); + } + + @Override + protected String getGeneratorId() { + return cfg.getGenerator(); + } + + @Override + protected void send(IPhoneConnector connector, String recipient, String content) { + connector.send(recipient, content); + } + +} diff --git a/src/main/java/io/kamax/mxisd/threepid/notification/phone/SmsNotificationGenerator.java b/src/main/java/io/kamax/mxisd/threepid/notification/phone/SmsNotificationGenerator.java new file mode 100644 index 0000000..b8c34c2 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/threepid/notification/phone/SmsNotificationGenerator.java @@ -0,0 +1,41 @@ +/* + * 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.notification.phone; + +import io.kamax.mxisd.config.MatrixConfig; +import io.kamax.mxisd.config.ServerConfig; +import io.kamax.mxisd.config.threepid.medium.PhoneSmsTemplateConfig; +import io.kamax.mxisd.threepid.notification.GenericTemplateNotificationGenerator; +import org.springframework.stereotype.Component; + +@Component +public class SmsNotificationGenerator extends GenericTemplateNotificationGenerator implements IPhoneNotificationGenerator { + + public SmsNotificationGenerator(MatrixConfig mxCfg, ServerConfig srvCfg, PhoneSmsTemplateConfig cfg) { + super(mxCfg, srvCfg, cfg); + } + + @Override + public String getId() { + return "template"; + } + +} diff --git a/src/main/java/io/kamax/mxisd/util/GsonParser.java b/src/main/java/io/kamax/mxisd/util/GsonParser.java index a83ebaf..5e10904 100644 --- a/src/main/java/io/kamax/mxisd/util/GsonParser.java +++ b/src/main/java/io/kamax/mxisd/util/GsonParser.java @@ -62,6 +62,14 @@ public class GsonParser { return gson.fromJson(parse(res.getEntity().getContent()), type); } + public Optional parseOptional(HttpResponse res) { + try { + return Optional.of(parse(res.getEntity().getContent())); + } catch (IOException e) { + return Optional.empty(); + } + } + public JsonObject parse(InputStream stream, String property) throws IOException { JsonObject obj = parse(stream); if (!obj.has(property)) { diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index b5ebaa6..7d5f010 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -92,11 +92,26 @@ threepid: password: '' generators: template: - invite: 'classpath:email/invite-template.eml' + invite: 'classpath:threepids/email/invite-template.eml' session: validation: - local: 'classpath:email/validate-local-template.eml' - remote: 'classpath:email/validate-remote-template.eml' + local: 'classpath:threepids/email/validate-local-template.eml' + remote: 'classpath:threepids/email/validate-remote-template.eml' + msisdn: + connector: 'twilio' + generator: 'template' + connectors: + twilio: + accountSid: '' + authToken: '' + number: '' + generators: + template: + invite: 'classpath:threepids/sms/invite-template.txt' + session: + validation: + local: 'classpath:threepids/sms/validate-local-template.txt' + remote: 'classpath:threepids/sms/validate-remote-template.txt' session: policy: diff --git a/src/main/resources/email/invite-template.eml b/src/main/resources/threepids/email/invite-template.eml similarity index 100% rename from src/main/resources/email/invite-template.eml rename to src/main/resources/threepids/email/invite-template.eml diff --git a/src/main/resources/email/validate-local-template.eml b/src/main/resources/threepids/email/validate-local-template.eml similarity index 100% rename from src/main/resources/email/validate-local-template.eml rename to src/main/resources/threepids/email/validate-local-template.eml diff --git a/src/main/resources/email/validate-remote-template.eml b/src/main/resources/threepids/email/validate-remote-template.eml similarity index 100% rename from src/main/resources/email/validate-remote-template.eml rename to src/main/resources/threepids/email/validate-remote-template.eml diff --git a/src/main/resources/threepids/sms/invite-template.txt b/src/main/resources/threepids/sms/invite-template.txt new file mode 100644 index 0000000..ff6125a --- /dev/null +++ b/src/main/resources/threepids/sms/invite-template.txt @@ -0,0 +1 @@ +You have been invited to a Matrix room by %SENDER_NAME_OR_ID%. Visit https://riot.im/ or any public server to join and start chatting! \ No newline at end of file diff --git a/src/main/resources/threepids/sms/validate-local-template.txt b/src/main/resources/threepids/sms/validate-local-template.txt new file mode 100644 index 0000000..a66d6c5 --- /dev/null +++ b/src/main/resources/threepids/sms/validate-local-template.txt @@ -0,0 +1 @@ +Your Matrix token is %VALIDATION_TOKEN% \ No newline at end of file diff --git a/src/main/resources/threepids/sms/validate-remote-template.txt b/src/main/resources/threepids/sms/validate-remote-template.txt new file mode 100644 index 0000000..e36ea5b --- /dev/null +++ b/src/main/resources/threepids/sms/validate-remote-template.txt @@ -0,0 +1 @@ +Your phone number will be made publicly searchable. To continue, you will need to enter two codes. Your first code is %VALIDATION_TOKEN% \ No newline at end of file