From c194ce153dc2961a786ae9e77f5cb0f578887abc Mon Sep 17 00:00:00 2001 From: Moxie Marlinspike Date: Mon, 9 Dec 2013 17:50:25 -0800 Subject: [PATCH] Add support for Twilio voice verification. --- config/sample.yml | 1 + pom.xml | 9 ++- .../WhisperServerConfiguration.java | 2 +- .../configuration/TwilioConfiguration.java | 8 ++ .../controllers/AccountController.java | 14 +++- .../textsecuregcm/sms/SenderFactory.java | 4 +- .../textsecuregcm/sms/TwilioSmsSender.java | 80 +++++++++++-------- 7 files changed, 78 insertions(+), 40 deletions(-) diff --git a/config/sample.yml b/config/sample.yml index c72b918f3..94168a539 100644 --- a/config/sample.yml +++ b/config/sample.yml @@ -2,6 +2,7 @@ twilio: accountId: accountToken: number: + localDomain: # The domain Twilio can call back to. # Optional. If specified, Nexmo will be used for non-US SMS and # voice verification. diff --git a/pom.xml b/pom.xml index d067fc74f..7cbec2359 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ org.whispersystems.textsecure TextSecureServer - 0.1 + 0.2 @@ -91,6 +91,11 @@ dropwizard-testing 0.6.2 + + com.twilio.sdk + twilio-java-sdk + 3.4.1 + postgresql @@ -205,4 +210,4 @@ - \ No newline at end of file + diff --git a/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java b/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java index 447631808..7bf5e32e7 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java +++ b/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java @@ -41,7 +41,7 @@ public class WhisperServerConfiguration extends Configuration { private TwilioConfiguration twilio; @JsonProperty - private NexmoConfiguration nexmo = new NexmoConfiguration(); + private NexmoConfiguration nexmo; @NotNull @JsonProperty diff --git a/src/main/java/org/whispersystems/textsecuregcm/configuration/TwilioConfiguration.java b/src/main/java/org/whispersystems/textsecuregcm/configuration/TwilioConfiguration.java index cdc4691e3..d9831aa94 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/configuration/TwilioConfiguration.java +++ b/src/main/java/org/whispersystems/textsecuregcm/configuration/TwilioConfiguration.java @@ -33,6 +33,10 @@ public class TwilioConfiguration { @JsonProperty private String number; + @NotEmpty + @JsonProperty + private String localDomain; + public String getAccountId() { return accountId; } @@ -44,4 +48,8 @@ public class TwilioConfiguration { public String getNumber() { return number; } + + public String getLocalDomain() { + return localDomain; + } } diff --git a/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java b/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java index f41139bdb..190381bec 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java +++ b/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java @@ -29,6 +29,7 @@ import org.whispersystems.textsecuregcm.entities.ApnRegistrationId; import org.whispersystems.textsecuregcm.entities.GcmRegistrationId; import org.whispersystems.textsecuregcm.limits.RateLimiters; import org.whispersystems.textsecuregcm.sms.SenderFactory; +import org.whispersystems.textsecuregcm.sms.TwilioSmsSender; import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.PendingAccountsManager; @@ -40,9 +41,11 @@ import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.HeaderParam; +import javax.ws.rs.POST; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; @@ -71,7 +74,6 @@ public class AccountController { this.senderFactory = smsSenderFactory; } - @Timed @GET @Path("/{transport}/code/{number}") @@ -182,6 +184,16 @@ public class AccountController { accounts.update(account); } + @Timed + @POST + @Path("/voice/twiml/{code}") + @Produces(MediaType.APPLICATION_XML) + public Response getTwiml(@PathParam("code") String encodedVerificationText) { + return Response.ok().entity(String.format(TwilioSmsSender.SAY_TWIML, + SenderFactory.VoxSender.VERIFICATION_TEXT + + encodedVerificationText)).build(); + } + private VerificationCode generateVerificationCode() { try { SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); diff --git a/src/main/java/org/whispersystems/textsecuregcm/sms/SenderFactory.java b/src/main/java/org/whispersystems/textsecuregcm/sms/SenderFactory.java index e74796602..2926ca570 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/sms/SenderFactory.java +++ b/src/main/java/org/whispersystems/textsecuregcm/sms/SenderFactory.java @@ -49,9 +49,9 @@ public class SenderFactory { public VoxSender getVoxSender(String number) { if (nexmoSender.isPresent()) { return nexmoSender.get(); + } else { + return twilioSender; } - - throw new AssertionError("FIX ME!"); } private boolean isTwilioDestination(String number) { diff --git a/src/main/java/org/whispersystems/textsecuregcm/sms/TwilioSmsSender.java b/src/main/java/org/whispersystems/textsecuregcm/sms/TwilioSmsSender.java index 46fecbd79..41b778ad0 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/sms/TwilioSmsSender.java +++ b/src/main/java/org/whispersystems/textsecuregcm/sms/TwilioSmsSender.java @@ -16,66 +16,78 @@ */ package org.whispersystems.textsecuregcm.sms; -import com.sun.jersey.core.util.Base64; +import com.twilio.sdk.TwilioRestClient; +import com.twilio.sdk.TwilioRestException; +import com.twilio.sdk.resource.factory.CallFactory; +import com.twilio.sdk.resource.factory.MessageFactory; import com.yammer.metrics.Metrics; import com.yammer.metrics.core.Meter; +import org.apache.http.NameValuePair; +import org.apache.http.message.BasicNameValuePair; import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration; -import org.whispersystems.textsecuregcm.util.Util; -import java.io.BufferedReader; import java.io.IOException; -import java.io.InputStreamReader; -import java.io.OutputStreamWriter; -import java.net.URL; -import java.net.URLConnection; import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; -public class TwilioSmsSender implements SenderFactory.SmsSender { +public class TwilioSmsSender implements SenderFactory.SmsSender, SenderFactory.VoxSender { + + public static final String SAY_TWIML = "\n" + + "\n" + + " %s\n" + + ""; private final Meter smsMeter = Metrics.newMeter(TwilioSmsSender.class, "sms", "delivered", TimeUnit.MINUTES); - - private static final String TWILIO_URL = "https://api.twilio.com/2010-04-01/Accounts/%s/SMS/Messages"; + private final Meter voxMeter = Metrics.newMeter(TwilioSmsSender.class, "vox", "delivered", TimeUnit.MINUTES); private final String accountId; private final String accountToken; private final String number; + private final String localDomain; public TwilioSmsSender(TwilioConfiguration config) { this.accountId = config.getAccountId(); this.accountToken = config.getAccountToken(); this.number = config.getNumber(); + this.localDomain = config.getLocalDomain(); } @Override - public void deliverSmsVerification(String destination, String verificationCode) throws IOException { - URL url = new URL(String.format(TWILIO_URL, accountId)); - URLConnection connection = url.openConnection(); - connection.setDoOutput(true); - connection.setRequestProperty("Authorization", getTwilioAuthorizationHeader()); - connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); - - Map formData = new HashMap<>(); - formData.put("From", number); - formData.put("To", destination); - formData.put("Body", VERIFICATION_TEXT + verificationCode); - - OutputStreamWriter writer = new OutputStreamWriter(connection.getOutputStream()); - writer.write(Util.encodeFormParams(formData)); - writer.flush(); - - BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream())); - while (reader.readLine() != null) {} - writer.close(); - reader.close(); + public void deliverSmsVerification(String destination, String verificationCode) + throws IOException + { + try { + TwilioRestClient client = new TwilioRestClient(accountId, accountToken); + MessageFactory messageFactory = client.getAccount().getMessageFactory(); + List messageParams = new LinkedList<>(); + messageParams.add(new BasicNameValuePair("To", destination)); + messageParams.add(new BasicNameValuePair("From", number)); + messageParams.add(new BasicNameValuePair("Body", SenderFactory.SmsSender.VERIFICATION_TEXT + verificationCode)); + messageFactory.create(messageParams); + } catch (TwilioRestException e) { + throw new IOException(e); + } smsMeter.mark(); } - private String getTwilioAuthorizationHeader() { - String encoded = new String(Base64.encode(String.format("%s:%s", accountId, accountToken))); - return "Basic " + encoded.replace("\n", ""); - } + @Override + public void deliverVoxVerification(String destination, String verificationCode) throws IOException { + try { + TwilioRestClient client = new TwilioRestClient(accountId, accountToken); + CallFactory callFactory = client.getAccount().getCallFactory(); + Map callParams = new HashMap<>(); + callParams.put("To", destination); + callParams.put("From", number); + callParams.put("Url", "https://" + localDomain + "/v1/accounts/voice/twiml/" + verificationCode); + callFactory.create(callParams); + } catch (TwilioRestException e) { + throw new IOException(e); + } + voxMeter.mark(); + } }