0
0
mirror of https://github.com/signalapp/Signal-Server.git synced 2024-09-19 19:42:18 +02:00

Add support for Twilio voice verification.

This commit is contained in:
Moxie Marlinspike 2013-12-09 17:50:25 -08:00
parent 4ad0dad3d9
commit c194ce153d
7 changed files with 78 additions and 40 deletions

View File

@ -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.

View File

@ -9,7 +9,7 @@
<groupId>org.whispersystems.textsecure</groupId>
<artifactId>TextSecureServer</artifactId>
<version>0.1</version>
<version>0.2</version>
<dependencies>
<dependency>
@ -91,6 +91,11 @@
<artifactId>dropwizard-testing</artifactId>
<version>0.6.2</version>
</dependency>
<dependency>
<groupId>com.twilio.sdk</groupId>
<artifactId>twilio-java-sdk</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>postgresql</groupId>
@ -205,4 +210,4 @@
</repository>
</repositories>
</project>
</project>

View File

@ -41,7 +41,7 @@ public class WhisperServerConfiguration extends Configuration {
private TwilioConfiguration twilio;
@JsonProperty
private NexmoConfiguration nexmo = new NexmoConfiguration();
private NexmoConfiguration nexmo;
@NotNull
@JsonProperty

View File

@ -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;
}
}

View File

@ -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");

View File

@ -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) {

View File

@ -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 = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
"<Response>\n" +
" <Say voice=\"woman\" language=\"en\">%s</Say>\n" +
"</Response>";
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<String, String> 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<NameValuePair> 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<String, String> 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();
}
}