From 6618fcfd0755d535e292f616cf7f100ecb73544d Mon Sep 17 00:00:00 2001 From: Daniel Gultsch <daniel@gultsch.de> Date: Wed, 27 Jan 2021 21:05:57 +0100 Subject: [PATCH] implement MetaVerificationProvider to choose verification provider based on country code --- .../server/configuration/Configuration.java | 42 ++++----- .../server/controller/BaseController.java | 3 +- .../AbstractVerificationProvider.java | 11 +++ .../MetaVerificationProvider.java | 92 +++++++++++++++++++ .../MockVerificationProvider.java | 8 +- .../NexmoVerificationProvider.java | 21 ++++- .../TwilioVerificationProvider.java | 26 +++++- 7 files changed, 170 insertions(+), 33 deletions(-) create mode 100644 src/main/java/im/quicksy/server/verification/AbstractVerificationProvider.java create mode 100644 src/main/java/im/quicksy/server/verification/MetaVerificationProvider.java diff --git a/src/main/java/im/quicksy/server/configuration/Configuration.java b/src/main/java/im/quicksy/server/configuration/Configuration.java index f3a6e3d..857f4db 100644 --- a/src/main/java/im/quicksy/server/configuration/Configuration.java +++ b/src/main/java/im/quicksy/server/configuration/Configuration.java @@ -31,8 +31,7 @@ import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.time.Duration; -import java.util.HashMap; -import java.util.Optional; +import java.util.*; public class Configuration { @@ -43,12 +42,9 @@ public class Configuration { private Web web = new Web(); private HashMap<String, DatabaseConfiguration> db; private PayPal payPal = new PayPal(); - private String twilioAuthToken; - private String nexmoApiKey; - private String nexmoPhoneNumber; + private TreeMap<String, ProviderConfiguration> provider; - private String nexmoApiSecret; private String cimAuthToken; private Version minVersion; private Duration accountInactivity = Duration.ofDays(28); @@ -135,22 +131,6 @@ public class Configuration { return new DatabaseConfigurationBundle.Builder().setEjabberdConfiguration(db.get("ejabberd")).setQuicksyConfiguration(db.get("quicksy")).build(); } - public String getTwilioAuthToken() { - return twilioAuthToken; - } - - public String getNexmoApiKey() { - return nexmoApiKey; - } - - public String getNexmoApiSecret() { - return nexmoApiSecret; - } - - public String getNexmoPhoneNumber() { - return nexmoPhoneNumber; - } - public Optional<String> getCimAuthToken() { return Optional.ofNullable(cimAuthToken); } @@ -171,6 +151,10 @@ public class Configuration { return minVersion; } + public TreeMap<String, Configuration.ProviderConfiguration> getProvider() { + return this.provider; + } + public static class XMPP { private String host = "localhost"; private int port = 5347; @@ -232,4 +216,18 @@ public class Configuration { return username != null && password != null && signature != null; } } + + + public static class ProviderConfiguration { + private Map<String, String> parameter; + private List<Integer> deny; + + public Map<String, String> getParameter() { + return parameter; + } + + public List<Integer> getDeny() { + return deny; + } + } } diff --git a/src/main/java/im/quicksy/server/controller/BaseController.java b/src/main/java/im/quicksy/server/controller/BaseController.java index d037d27..1e2719b 100644 --- a/src/main/java/im/quicksy/server/controller/BaseController.java +++ b/src/main/java/im/quicksy/server/controller/BaseController.java @@ -21,6 +21,7 @@ import com.github.zafarkhaja.semver.Version; import com.google.common.base.Splitter; import com.google.common.net.InetAddresses; import im.quicksy.server.configuration.Configuration; +import im.quicksy.server.verification.MetaVerificationProvider; import im.quicksy.server.verification.NexmoVerificationProvider; import im.quicksy.server.verification.TwilioVerificationProvider; import im.quicksy.server.verification.VerificationProvider; @@ -52,7 +53,7 @@ public class BaseController { protected static Pattern PIN_PATTERN = Pattern.compile("^[0-9]{6}$"); protected static Pattern UUID_PATTERN = Pattern.compile("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"); - protected static final VerificationProvider VERIFICATION_PROVIDER = new NexmoVerificationProvider(); + protected static final VerificationProvider VERIFICATION_PROVIDER = new MetaVerificationProvider(); protected static InetAddress getClientIp(Request request) { final InetAddress remote = InetAddresses.forString(request.ip()); diff --git a/src/main/java/im/quicksy/server/verification/AbstractVerificationProvider.java b/src/main/java/im/quicksy/server/verification/AbstractVerificationProvider.java new file mode 100644 index 0000000..0f82e9f --- /dev/null +++ b/src/main/java/im/quicksy/server/verification/AbstractVerificationProvider.java @@ -0,0 +1,11 @@ +package im.quicksy.server.verification; + +import java.util.Map; + +public abstract class AbstractVerificationProvider implements VerificationProvider { + + public AbstractVerificationProvider(final Map<String, String> parameter) { + + } + +} diff --git a/src/main/java/im/quicksy/server/verification/MetaVerificationProvider.java b/src/main/java/im/quicksy/server/verification/MetaVerificationProvider.java new file mode 100644 index 0000000..1068f6c --- /dev/null +++ b/src/main/java/im/quicksy/server/verification/MetaVerificationProvider.java @@ -0,0 +1,92 @@ +package im.quicksy.server.verification; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.i18n.phonenumbers.Phonenumber; +import im.quicksy.server.configuration.Configuration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +public class MetaVerificationProvider implements VerificationProvider { + + private static final Logger LOGGER = LoggerFactory.getLogger(MetaVerificationProvider.class); + + final List<ProviderWrapper> providerList; + + public MetaVerificationProvider() { + final TreeMap<String, Configuration.ProviderConfiguration> provider = Configuration.getInstance().getProvider(); + ImmutableList.Builder<ProviderWrapper> providerListBuilder = ImmutableList.builder(); + for(final Map.Entry<String,Configuration.ProviderConfiguration> entry : provider.entrySet()) { + final String className = entry.getKey(); + final Configuration.ProviderConfiguration configuration = entry.getValue(); + final Class<? extends AbstractVerificationProvider> clazz; + try { + clazz = (Class<? extends AbstractVerificationProvider>) Class.forName(className); + } catch (ClassNotFoundException | ClassCastException e) { + LOGGER.warn("No VerificationProvider found matching for name {}", className); + continue; + } + final AbstractVerificationProvider providerInstance; + try { + Constructor<? extends AbstractVerificationProvider> constructor = clazz.getConstructor(Map.class); + providerInstance = constructor.newInstance(configuration.getParameter()); + } catch (NoSuchMethodException e) { + LOGGER.warn("{} does not implement Map<String,String> constructor", clazz.getName()); + continue; + } catch (IllegalAccessException | InstantiationException | InvocationTargetException e) { + LOGGER.warn("Unable to construct VerificationProvider",e); + continue; + } + providerListBuilder.add(new ProviderWrapper(configuration.getDeny(), providerInstance)); + LOGGER.info("found provider {} ", className); + } + final ImmutableList<ProviderWrapper> providerList = providerListBuilder.build(); + LOGGER.info("Found {} providers", providerList.size()); + if (providerList.size() == 0) { + throw new IllegalStateException("No VerificationProviders found"); + } + this.providerList = providerList; + } + + @Override + public boolean verify(Phonenumber.PhoneNumber phoneNumber, String pin) throws RequestFailedException { + return getVerificationProvider(phoneNumber).verify(phoneNumber, pin); + } + + @Override + public void request(Phonenumber.PhoneNumber phoneNumber, Method method) throws RequestFailedException { + getVerificationProvider(phoneNumber).request(phoneNumber, method); + } + + @Override + public void request(Phonenumber.PhoneNumber phoneNumber, Method method, String language) throws RequestFailedException { + getVerificationProvider(phoneNumber).request(phoneNumber, method, language); + } + + private AbstractVerificationProvider getVerificationProvider(Phonenumber.PhoneNumber phoneNumber) throws RequestFailedException { + final int countryCode = phoneNumber.getCountryCode(); + for(ProviderWrapper providerWrapper : this.providerList) { + if (providerWrapper.deny.contains(countryCode)) { + continue; + } + return providerWrapper.provider; + } + throw new RequestFailedException(String.format("No Verification Provider found to handle country code %d", countryCode)); + } + + private static class ProviderWrapper { + private final List<Integer> deny; + private final AbstractVerificationProvider provider; + + private ProviderWrapper(List<Integer> deny, AbstractVerificationProvider provider) { + this.deny = deny; + this.provider = provider; + } + } +} diff --git a/src/main/java/im/quicksy/server/verification/MockVerificationProvider.java b/src/main/java/im/quicksy/server/verification/MockVerificationProvider.java index 6465d68..c6c3e91 100644 --- a/src/main/java/im/quicksy/server/verification/MockVerificationProvider.java +++ b/src/main/java/im/quicksy/server/verification/MockVerificationProvider.java @@ -20,10 +20,16 @@ import com.google.i18n.phonenumbers.Phonenumber; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class MockVerificationProvider implements VerificationProvider { +import java.util.Map; + +public class MockVerificationProvider extends AbstractVerificationProvider { private static final Logger LOGGER = LoggerFactory.getLogger(MockVerificationProvider.class); + public MockVerificationProvider(Map<String, String> parameter) { + super(parameter); + } + @Override public boolean verify(Phonenumber.PhoneNumber phoneNumber, String pin) { return pin != null && pin.length() == 6 && String.valueOf(phoneNumber.getNationalNumber()).startsWith(pin); diff --git a/src/main/java/im/quicksy/server/verification/NexmoVerificationProvider.java b/src/main/java/im/quicksy/server/verification/NexmoVerificationProvider.java index 4a99758..a4bcba9 100644 --- a/src/main/java/im/quicksy/server/verification/NexmoVerificationProvider.java +++ b/src/main/java/im/quicksy/server/verification/NexmoVerificationProvider.java @@ -1,5 +1,6 @@ package im.quicksy.server.verification; +import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; @@ -18,8 +19,9 @@ import java.security.SecureRandom; import java.time.Duration; import java.util.Arrays; import java.util.List; +import java.util.Map; -public class NexmoVerificationProvider implements VerificationProvider { +public class NexmoVerificationProvider extends AbstractVerificationProvider { private static final Logger LOGGER = LoggerFactory.getLogger(NexmoVerificationProvider.class); @@ -44,6 +46,17 @@ public class NexmoVerificationProvider implements VerificationProvider { .expireAfterWrite(Duration.ofMinutes(5)) .build(); + private final String phoneNumber; + private final String apiKey; + private final String apiSecret; + + public NexmoVerificationProvider(Map<String, String> parameter) { + super(parameter); + this.phoneNumber = parameter.get("phone_number"); + this.apiKey = Preconditions.checkNotNull(parameter.get("api_key")); + this.apiSecret = Preconditions.checkNotNull(parameter.get("api_secret")); + } + @Override public boolean verify(Phonenumber.PhoneNumber phoneNumber, String input) throws RequestFailedException { final Pin pin = PIN_CACHE.getIfPresent(phoneNumber); @@ -70,7 +83,7 @@ public class NexmoVerificationProvider implements VerificationProvider { final Pin pin = Pin.generate(); PIN_CACHE.put(phoneNumber, pin); final String to = String.format("%d%d", phoneNumber.getCountryCode(), phoneNumber.getNationalNumber()); - final String nexmoPhoneNumber = Configuration.getInstance().getNexmoPhoneNumber(); + final String nexmoPhoneNumber = this.phoneNumber; final String from; if (Strings.isNullOrEmpty(nexmoPhoneNumber) || COUNTRY_CODES_SUPPORTING_ALPHA_NUMERIC.contains(phoneNumber.getCountryCode())) { from = BRAND_NAME; @@ -83,8 +96,8 @@ public class NexmoVerificationProvider implements VerificationProvider { .add("from", from) .add("text", String.format(MESSAGE, pin.toString())) .add("to", to) - .add("api_key", Configuration.getInstance().getNexmoApiKey()) - .add("api_secret", Configuration.getInstance().getNexmoApiSecret()) + .add("api_key", this.apiKey) + .add("api_secret", this.apiSecret) .build()) .url(NEXMO_API_URL) .build()); diff --git a/src/main/java/im/quicksy/server/verification/TwilioVerificationProvider.java b/src/main/java/im/quicksy/server/verification/TwilioVerificationProvider.java index d8ad2f9..f366e0d 100644 --- a/src/main/java/im/quicksy/server/verification/TwilioVerificationProvider.java +++ b/src/main/java/im/quicksy/server/verification/TwilioVerificationProvider.java @@ -16,6 +16,7 @@ package im.quicksy.server.verification; +import com.google.common.base.Preconditions; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonSyntaxException; @@ -34,12 +35,10 @@ import java.io.UnsupportedEncodingException; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLEncoder; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; +import java.util.*; import java.util.stream.Collectors; -public class TwilioVerificationProvider implements VerificationProvider { +public class TwilioVerificationProvider extends AbstractVerificationProvider { public static final int PHONE_VERIFICATION_INCORRECT = 60022; @@ -51,6 +50,23 @@ public class TwilioVerificationProvider implements VerificationProvider { private static final Logger LOGGER = LoggerFactory.getLogger(TwilioVerificationProvider.class); private final GsonBuilder gsonBuilder = new GsonBuilder(); + private final String authToken; + + public TwilioVerificationProvider(Map<String, String> parameter) { + super(parameter); + this.authToken = Preconditions.checkNotNull(parameter.get("auth_token")); + } + + public TwilioVerificationProvider() { + super(Collections.emptyMap()); + final TreeMap<String, Configuration.ProviderConfiguration> provider = Configuration.getInstance().getProvider(); + final Configuration.ProviderConfiguration myConfiguration = provider.get(getClass().getName()); + if (myConfiguration == null) { + throw new RuntimeException("No configuration found for "+getClass().getSimpleName()); + } + this.authToken = Preconditions.checkNotNull(myConfiguration.getParameter().get("auth_token")); + } + @Override public boolean verify(Phonenumber.PhoneNumber phoneNumber, String pin) throws RequestFailedException { Map<String, String> params = new HashMap<>(); @@ -124,7 +140,7 @@ public class TwilioVerificationProvider implements VerificationProvider { try { final Gson gson = this.gsonBuilder.create(); final HttpURLConnection connection = (HttpURLConnection) new URL(TWILIO_API_URL + method).openConnection(); - connection.setRequestProperty("X-Authy-API-Key", Configuration.getInstance().getTwilioAuthToken()); + connection.setRequestProperty("X-Authy-API-Key", this.authToken); if (params != null && params.size() > 0) { connection.setRequestMethod("POST"); final String output = getQuery(params); -- GitLab