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();
+ }
}