Support for registration lock
This commit is contained in:
parent
c0bbebd532
commit
18bab4aa7d
|
@ -107,9 +107,6 @@ public class WhisperServerConfiguration extends Configuration {
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
private RateLimitsConfiguration limits = new RateLimitsConfiguration();
|
private RateLimitsConfiguration limits = new RateLimitsConfiguration();
|
||||||
|
|
||||||
@JsonProperty
|
|
||||||
private RedPhoneConfiguration redphone = new RedPhoneConfiguration();
|
|
||||||
|
|
||||||
@Valid
|
@Valid
|
||||||
@NotNull
|
@NotNull
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
|
@ -183,10 +180,6 @@ public class WhisperServerConfiguration extends Configuration {
|
||||||
return federation;
|
return federation;
|
||||||
}
|
}
|
||||||
|
|
||||||
public RedPhoneConfiguration getRedphoneConfiguration() {
|
|
||||||
return redphone;
|
|
||||||
}
|
|
||||||
|
|
||||||
public TurnConfiguration getTurnConfiguration() {
|
public TurnConfiguration getTurnConfiguration() {
|
||||||
return turn;
|
return turn;
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,7 +58,6 @@ import org.whispersystems.textsecuregcm.metrics.NetworkReceivedGauge;
|
||||||
import org.whispersystems.textsecuregcm.metrics.NetworkSentGauge;
|
import org.whispersystems.textsecuregcm.metrics.NetworkSentGauge;
|
||||||
import org.whispersystems.textsecuregcm.providers.RedisClientFactory;
|
import org.whispersystems.textsecuregcm.providers.RedisClientFactory;
|
||||||
import org.whispersystems.textsecuregcm.providers.RedisHealthCheck;
|
import org.whispersystems.textsecuregcm.providers.RedisHealthCheck;
|
||||||
import org.whispersystems.textsecuregcm.providers.TimeProvider;
|
|
||||||
import org.whispersystems.textsecuregcm.push.APNSender;
|
import org.whispersystems.textsecuregcm.push.APNSender;
|
||||||
import org.whispersystems.textsecuregcm.push.ApnFallbackManager;
|
import org.whispersystems.textsecuregcm.push.ApnFallbackManager;
|
||||||
import org.whispersystems.textsecuregcm.push.GCMSender;
|
import org.whispersystems.textsecuregcm.push.GCMSender;
|
||||||
|
@ -186,7 +185,6 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||||
PushSender pushSender = new PushSender(apnFallbackManager, gcmSender, apnSender, websocketSender, config.getPushConfiguration().getQueueSize());
|
PushSender pushSender = new PushSender(apnFallbackManager, gcmSender, apnSender, websocketSender, config.getPushConfiguration().getQueueSize());
|
||||||
ReceiptSender receiptSender = new ReceiptSender(accountsManager, pushSender, federatedClientManager);
|
ReceiptSender receiptSender = new ReceiptSender(accountsManager, pushSender, federatedClientManager);
|
||||||
TurnTokenGenerator turnTokenGenerator = new TurnTokenGenerator(config.getTurnConfiguration());
|
TurnTokenGenerator turnTokenGenerator = new TurnTokenGenerator(config.getTurnConfiguration());
|
||||||
Optional<byte[]> authorizationKey = config.getRedphoneConfiguration().getAuthorizationKey();
|
|
||||||
|
|
||||||
apnSender.setApnFallbackManager(apnFallbackManager);
|
apnSender.setApnFallbackManager(apnFallbackManager);
|
||||||
environment.lifecycle().manage(apnFallbackManager);
|
environment.lifecycle().manage(apnFallbackManager);
|
||||||
|
@ -208,7 +206,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||||
.buildAuthFilter()));
|
.buildAuthFilter()));
|
||||||
environment.jersey().register(new AuthValueFactoryProvider.Binder());
|
environment.jersey().register(new AuthValueFactoryProvider.Binder());
|
||||||
|
|
||||||
environment.jersey().register(new AccountController(pendingAccountsManager, accountsManager, rateLimiters, smsSender, messagesManager, new TimeProvider(), authorizationKey, turnTokenGenerator, config.getTestDevices()));
|
environment.jersey().register(new AccountController(pendingAccountsManager, accountsManager, rateLimiters, smsSender, messagesManager, turnTokenGenerator, config.getTestDevices()));
|
||||||
environment.jersey().register(new DeviceController(pendingDevicesManager, accountsManager, messagesManager, rateLimiters, config.getMaxDevices()));
|
environment.jersey().register(new DeviceController(pendingDevicesManager, accountsManager, messagesManager, rateLimiters, config.getMaxDevices()));
|
||||||
environment.jersey().register(new DirectoryController(rateLimiters, directory));
|
environment.jersey().register(new DirectoryController(rateLimiters, directory));
|
||||||
environment.jersey().register(new FederationControllerV1(accountsManager, attachmentController, messageController));
|
environment.jersey().register(new FederationControllerV1(accountsManager, attachmentController, messageController));
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
/**
|
/*
|
||||||
* Copyright (C) 2013 Open WhisperSystems
|
* Copyright (C) 2013 Open WhisperSystems
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
@ -32,6 +32,9 @@ public class RateLimitsConfiguration {
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
private RateLimitConfiguration verifyNumber = new RateLimitConfiguration(2, 2);
|
private RateLimitConfiguration verifyNumber = new RateLimitConfiguration(2, 2);
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private RateLimitConfiguration verifyPin = new RateLimitConfiguration(10, 1 / (24.0 * 60.0));
|
||||||
|
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
private RateLimitConfiguration attachments = new RateLimitConfiguration(50, 50);
|
private RateLimitConfiguration attachments = new RateLimitConfiguration(50, 50);
|
||||||
|
|
||||||
|
@ -96,6 +99,10 @@ public class RateLimitsConfiguration {
|
||||||
return verifyNumber;
|
return verifyNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public RateLimitConfiguration getVerifyPin() {
|
||||||
|
return verifyPin;
|
||||||
|
}
|
||||||
|
|
||||||
public RateLimitConfiguration getTurnAllocations() {
|
public RateLimitConfiguration getTurnAllocations() {
|
||||||
return turnAllocations;
|
return turnAllocations;
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,8 +26,6 @@ import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
|
import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
|
||||||
import org.whispersystems.textsecuregcm.auth.AuthorizationHeader;
|
import org.whispersystems.textsecuregcm.auth.AuthorizationHeader;
|
||||||
import org.whispersystems.textsecuregcm.auth.AuthorizationToken;
|
|
||||||
import org.whispersystems.textsecuregcm.auth.AuthorizationTokenGenerator;
|
|
||||||
import org.whispersystems.textsecuregcm.auth.InvalidAuthorizationHeaderException;
|
import org.whispersystems.textsecuregcm.auth.InvalidAuthorizationHeaderException;
|
||||||
import org.whispersystems.textsecuregcm.auth.StoredVerificationCode;
|
import org.whispersystems.textsecuregcm.auth.StoredVerificationCode;
|
||||||
import org.whispersystems.textsecuregcm.auth.TurnToken;
|
import org.whispersystems.textsecuregcm.auth.TurnToken;
|
||||||
|
@ -35,8 +33,9 @@ import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator;
|
||||||
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
|
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
|
||||||
import org.whispersystems.textsecuregcm.entities.ApnRegistrationId;
|
import org.whispersystems.textsecuregcm.entities.ApnRegistrationId;
|
||||||
import org.whispersystems.textsecuregcm.entities.GcmRegistrationId;
|
import org.whispersystems.textsecuregcm.entities.GcmRegistrationId;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.RegistrationLock;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.RegistrationLockFailure;
|
||||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||||
import org.whispersystems.textsecuregcm.providers.TimeProvider;
|
|
||||||
import org.whispersystems.textsecuregcm.sms.SmsSender;
|
import org.whispersystems.textsecuregcm.sms.SmsSender;
|
||||||
import org.whispersystems.textsecuregcm.sms.TwilioSmsSender;
|
import org.whispersystems.textsecuregcm.sms.TwilioSmsSender;
|
||||||
import org.whispersystems.textsecuregcm.storage.Account;
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
|
@ -63,9 +62,10 @@ import javax.ws.rs.WebApplicationException;
|
||||||
import javax.ws.rs.core.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.MessageDigest;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import static com.codahale.metrics.MetricRegistry.name;
|
import static com.codahale.metrics.MetricRegistry.name;
|
||||||
import io.dropwizard.auth.Auth;
|
import io.dropwizard.auth.Auth;
|
||||||
|
@ -82,8 +82,6 @@ public class AccountController {
|
||||||
private final RateLimiters rateLimiters;
|
private final RateLimiters rateLimiters;
|
||||||
private final SmsSender smsSender;
|
private final SmsSender smsSender;
|
||||||
private final MessagesManager messagesManager;
|
private final MessagesManager messagesManager;
|
||||||
private final TimeProvider timeProvider;
|
|
||||||
private final Optional<AuthorizationTokenGenerator> tokenGenerator;
|
|
||||||
private final TurnTokenGenerator turnTokenGenerator;
|
private final TurnTokenGenerator turnTokenGenerator;
|
||||||
private final Map<String, Integer> testDevices;
|
private final Map<String, Integer> testDevices;
|
||||||
|
|
||||||
|
@ -92,8 +90,6 @@ public class AccountController {
|
||||||
RateLimiters rateLimiters,
|
RateLimiters rateLimiters,
|
||||||
SmsSender smsSenderFactory,
|
SmsSender smsSenderFactory,
|
||||||
MessagesManager messagesManager,
|
MessagesManager messagesManager,
|
||||||
TimeProvider timeProvider,
|
|
||||||
Optional<byte[]> authorizationKey,
|
|
||||||
TurnTokenGenerator turnTokenGenerator,
|
TurnTokenGenerator turnTokenGenerator,
|
||||||
Map<String, Integer> testDevices)
|
Map<String, Integer> testDevices)
|
||||||
{
|
{
|
||||||
|
@ -102,15 +98,8 @@ public class AccountController {
|
||||||
this.rateLimiters = rateLimiters;
|
this.rateLimiters = rateLimiters;
|
||||||
this.smsSender = smsSenderFactory;
|
this.smsSender = smsSenderFactory;
|
||||||
this.messagesManager = messagesManager;
|
this.messagesManager = messagesManager;
|
||||||
this.timeProvider = timeProvider;
|
|
||||||
this.testDevices = testDevices;
|
this.testDevices = testDevices;
|
||||||
this.turnTokenGenerator = turnTokenGenerator;
|
this.turnTokenGenerator = turnTokenGenerator;
|
||||||
|
|
||||||
if (authorizationKey.isPresent()) {
|
|
||||||
tokenGenerator = Optional.of(new AuthorizationTokenGenerator(authorizationKey.get()));
|
|
||||||
} else {
|
|
||||||
tokenGenerator = Optional.absent();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Timed
|
@Timed
|
||||||
|
@ -158,6 +147,7 @@ public class AccountController {
|
||||||
@Timed
|
@Timed
|
||||||
@PUT
|
@PUT
|
||||||
@Consumes(MediaType.APPLICATION_JSON)
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
@Path("/code/{verification_code}")
|
@Path("/code/{verification_code}")
|
||||||
public void verifyAccount(@PathParam("verification_code") String verificationCode,
|
public void verifyAccount(@PathParam("verification_code") String verificationCode,
|
||||||
@HeaderParam("Authorization") String authorizationHeader,
|
@HeaderParam("Authorization") String authorizationHeader,
|
||||||
|
@ -178,8 +168,26 @@ public class AccountController {
|
||||||
throw new WebApplicationException(Response.status(403).build());
|
throw new WebApplicationException(Response.status(403).build());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (accounts.isRelayListed(number)) {
|
Optional<Account> existingAccount = accounts.get(number);
|
||||||
throw new WebApplicationException(Response.status(417).build());
|
|
||||||
|
if (existingAccount.isPresent() &&
|
||||||
|
existingAccount.get().getPin().isPresent() &&
|
||||||
|
System.currentTimeMillis() - existingAccount.get().getLastSeen() < TimeUnit.DAYS.toMillis(7))
|
||||||
|
{
|
||||||
|
rateLimiters.getVerifyLimiter().clear(number);
|
||||||
|
rateLimiters.getPinLimiter().validate(number);
|
||||||
|
|
||||||
|
if (accountAttributes.getPin() == null ||
|
||||||
|
!MessageDigest.isEqual(existingAccount.get().getPin().get().getBytes(), accountAttributes.getPin().getBytes()))
|
||||||
|
{
|
||||||
|
long timeRemaining = TimeUnit.DAYS.toMillis(7) - (System.currentTimeMillis() - existingAccount.get().getLastSeen());
|
||||||
|
|
||||||
|
throw new WebApplicationException(Response.status(423)
|
||||||
|
.entity(new RegistrationLockFailure(timeRemaining))
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
rateLimiters.getPinLimiter().clear(number);
|
||||||
}
|
}
|
||||||
|
|
||||||
createAccount(number, password, userAgent, accountAttributes);
|
createAccount(number, password, userAgent, accountAttributes);
|
||||||
|
@ -189,54 +197,6 @@ public class AccountController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Timed
|
|
||||||
@PUT
|
|
||||||
@Consumes(MediaType.APPLICATION_JSON)
|
|
||||||
@Path("/token/{verification_token}")
|
|
||||||
public void verifyToken(@PathParam("verification_token") String verificationToken,
|
|
||||||
@HeaderParam("Authorization") String authorizationHeader,
|
|
||||||
@HeaderParam("X-Signal-Agent") String userAgent,
|
|
||||||
@Valid AccountAttributes accountAttributes)
|
|
||||||
throws RateLimitExceededException
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
AuthorizationHeader header = AuthorizationHeader.fromFullHeader(authorizationHeader);
|
|
||||||
String number = header.getNumber();
|
|
||||||
String password = header.getPassword();
|
|
||||||
|
|
||||||
rateLimiters.getVerifyLimiter().validate(number);
|
|
||||||
|
|
||||||
if (!tokenGenerator.isPresent()) {
|
|
||||||
logger.debug("Attempt to authorize with key but not configured...");
|
|
||||||
throw new WebApplicationException(Response.status(403).build());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tokenGenerator.get().isValid(verificationToken, number, timeProvider.getCurrentTimeMillis())) {
|
|
||||||
throw new WebApplicationException(Response.status(403).build());
|
|
||||||
}
|
|
||||||
|
|
||||||
createAccount(number, password, userAgent, accountAttributes);
|
|
||||||
} catch (InvalidAuthorizationHeaderException e) {
|
|
||||||
logger.info("Bad authorization header", e);
|
|
||||||
throw new WebApplicationException(Response.status(401).build());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Timed
|
|
||||||
@GET
|
|
||||||
@Path("/token/")
|
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
|
||||||
public AuthorizationToken verifyToken(@Auth Account account)
|
|
||||||
throws RateLimitExceededException
|
|
||||||
{
|
|
||||||
if (!tokenGenerator.isPresent()) {
|
|
||||||
logger.debug("Attempt to authorize with key but not configured...");
|
|
||||||
throw new WebApplicationException(Response.status(404).build());
|
|
||||||
}
|
|
||||||
|
|
||||||
return tokenGenerator.get().generateFor(account.getNumber());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Timed
|
@Timed
|
||||||
@GET
|
@GET
|
||||||
@Path("/turn/")
|
@Path("/turn/")
|
||||||
|
@ -302,6 +262,23 @@ public class AccountController {
|
||||||
accounts.update(account);
|
accounts.update(account);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Timed
|
||||||
|
@PUT
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@Path("/pin/")
|
||||||
|
public void setPin(@Auth Account account, @Valid RegistrationLock accountLock) {
|
||||||
|
account.setPin(accountLock.getPin());
|
||||||
|
accounts.update(account);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Timed
|
||||||
|
@DELETE
|
||||||
|
@Path("/pin/")
|
||||||
|
public void removePin(@Auth Account account) {
|
||||||
|
account.setPin(null);
|
||||||
|
accounts.update(account);
|
||||||
|
}
|
||||||
|
|
||||||
@Timed
|
@Timed
|
||||||
@PUT
|
@PUT
|
||||||
@Path("/attributes/")
|
@Path("/attributes/")
|
||||||
|
@ -321,6 +298,8 @@ public class AccountController {
|
||||||
device.setSignalingKey(attributes.getSignalingKey());
|
device.setSignalingKey(attributes.getSignalingKey());
|
||||||
device.setUserAgent(userAgent);
|
device.setUserAgent(userAgent);
|
||||||
|
|
||||||
|
account.setPin(attributes.getPin());
|
||||||
|
|
||||||
accounts.update(account);
|
accounts.update(account);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -350,6 +329,7 @@ public class AccountController {
|
||||||
Account account = new Account();
|
Account account = new Account();
|
||||||
account.setNumber(number);
|
account.setNumber(number);
|
||||||
account.addDevice(device);
|
account.addDevice(device);
|
||||||
|
account.setPin(accountAttributes.getPin());
|
||||||
|
|
||||||
if (accounts.create(account)) {
|
if (accounts.create(account)) {
|
||||||
newUserMeter.mark();
|
newUserMeter.mark();
|
||||||
|
|
|
@ -43,21 +43,25 @@ public class AccountAttributes {
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
private boolean video;
|
private boolean video;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String pin;
|
||||||
|
|
||||||
public AccountAttributes() {}
|
public AccountAttributes() {}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
public AccountAttributes(String signalingKey, boolean fetchesMessages, int registrationId) {
|
public AccountAttributes(String signalingKey, boolean fetchesMessages, int registrationId, String pin) {
|
||||||
this(signalingKey, fetchesMessages, registrationId, null, false, false);
|
this(signalingKey, fetchesMessages, registrationId, null, false, false, pin);
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
public AccountAttributes(String signalingKey, boolean fetchesMessages, int registrationId, String name, boolean voice, boolean video) {
|
public AccountAttributes(String signalingKey, boolean fetchesMessages, int registrationId, String name, boolean voice, boolean video, String pin) {
|
||||||
this.signalingKey = signalingKey;
|
this.signalingKey = signalingKey;
|
||||||
this.fetchesMessages = fetchesMessages;
|
this.fetchesMessages = fetchesMessages;
|
||||||
this.registrationId = registrationId;
|
this.registrationId = registrationId;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.voice = voice;
|
this.voice = voice;
|
||||||
this.video = video;
|
this.video = video;
|
||||||
|
this.pin = pin;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getSignalingKey() {
|
public String getSignalingKey() {
|
||||||
|
@ -84,4 +88,7 @@ public class AccountAttributes {
|
||||||
return video;
|
return video;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getPin() {
|
||||||
|
return pin;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
package org.whispersystems.textsecuregcm.entities;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import org.hibernate.validator.constraints.Length;
|
||||||
|
import org.hibernate.validator.constraints.NotEmpty;
|
||||||
|
|
||||||
|
public class RegistrationLock {
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@NotEmpty
|
||||||
|
@Length(min=4,max=20)
|
||||||
|
private String pin;
|
||||||
|
|
||||||
|
public RegistrationLock() {}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
public RegistrationLock(String pin) {
|
||||||
|
this.pin = pin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPin() {
|
||||||
|
return pin;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
package org.whispersystems.textsecuregcm.entities;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
public class RegistrationLockFailure {
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private long timeRemaining;
|
||||||
|
|
||||||
|
public RegistrationLockFailure() {}
|
||||||
|
|
||||||
|
public RegistrationLockFailure(long timeRemaining) {
|
||||||
|
this.timeRemaining = timeRemaining;
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
public long getTimeRemaining() {
|
||||||
|
return timeRemaining;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
package org.whispersystems.textsecuregcm.limits;
|
||||||
|
|
||||||
|
import com.codahale.metrics.Meter;
|
||||||
|
import com.codahale.metrics.MetricRegistry;
|
||||||
|
import com.codahale.metrics.SharedMetricRegistries;
|
||||||
|
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
|
||||||
|
import org.whispersystems.textsecuregcm.util.Constants;
|
||||||
|
|
||||||
|
import static com.codahale.metrics.MetricRegistry.name;
|
||||||
|
import redis.clients.jedis.Jedis;
|
||||||
|
import redis.clients.jedis.JedisPool;
|
||||||
|
|
||||||
|
public class LockingRateLimiter extends RateLimiter {
|
||||||
|
|
||||||
|
private final Meter meter;
|
||||||
|
|
||||||
|
public LockingRateLimiter(JedisPool cacheClient, String name, int bucketSize, double leakRatePerMinute) {
|
||||||
|
super(cacheClient, name, bucketSize, leakRatePerMinute);
|
||||||
|
|
||||||
|
MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
|
||||||
|
this.meter = metricRegistry.meter(name(getClass(), name, "locked"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void validate(String key, int amount) throws RateLimitExceededException {
|
||||||
|
if (!acquireLock(key)) {
|
||||||
|
meter.mark();
|
||||||
|
throw new RateLimitExceededException("Locked");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
super.validate(key, amount);
|
||||||
|
} finally {
|
||||||
|
releaseLock(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void validate(String key) throws RateLimitExceededException {
|
||||||
|
validate(key, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void releaseLock(String key) {
|
||||||
|
try (Jedis jedis = cacheClient.getResource()) {
|
||||||
|
jedis.del(getLockName(key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean acquireLock(String key) {
|
||||||
|
try (Jedis jedis = cacheClient.getResource()) {
|
||||||
|
return jedis.set(getLockName(key), "L", "NX", "EX", 10) != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getLockName(String key) {
|
||||||
|
return "leaky_lock::" + name + "::" + key;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -39,13 +39,21 @@ public class RateLimiter {
|
||||||
private final ObjectMapper mapper = SystemMapper.getMapper();
|
private final ObjectMapper mapper = SystemMapper.getMapper();
|
||||||
|
|
||||||
private final Meter meter;
|
private final Meter meter;
|
||||||
private final JedisPool cacheClient;
|
protected final JedisPool cacheClient;
|
||||||
private final String name;
|
protected final String name;
|
||||||
private final int bucketSize;
|
private final int bucketSize;
|
||||||
private final double leakRatePerMillis;
|
private final double leakRatePerMillis;
|
||||||
|
private final boolean reportLimits;
|
||||||
|
|
||||||
public RateLimiter(JedisPool cacheClient, String name,
|
public RateLimiter(JedisPool cacheClient, String name,
|
||||||
int bucketSize, double leakRatePerMinute)
|
int bucketSize, double leakRatePerMinute)
|
||||||
|
{
|
||||||
|
this(cacheClient, name, bucketSize, leakRatePerMinute, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RateLimiter(JedisPool cacheClient, String name,
|
||||||
|
int bucketSize, double leakRatePerMinute,
|
||||||
|
boolean reportLimits)
|
||||||
{
|
{
|
||||||
MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
|
MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
|
||||||
|
|
||||||
|
@ -54,6 +62,7 @@ public class RateLimiter {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.bucketSize = bucketSize;
|
this.bucketSize = bucketSize;
|
||||||
this.leakRatePerMillis = leakRatePerMinute / (60.0 * 1000.0);
|
this.leakRatePerMillis = leakRatePerMinute / (60.0 * 1000.0);
|
||||||
|
this.reportLimits = reportLimits;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void validate(String key, int amount) throws RateLimitExceededException {
|
public void validate(String key, int amount) throws RateLimitExceededException {
|
||||||
|
@ -71,6 +80,12 @@ public class RateLimiter {
|
||||||
validate(key, 1);
|
validate(key, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void clear(String key) {
|
||||||
|
try (Jedis jedis = cacheClient.getResource()) {
|
||||||
|
jedis.del(getBucketName(key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void setBucket(String key, LeakyBucket bucket) {
|
private void setBucket(String key, LeakyBucket bucket) {
|
||||||
try (Jedis jedis = cacheClient.getResource()) {
|
try (Jedis jedis = cacheClient.getResource()) {
|
||||||
String serialized = bucket.serialize(mapper);
|
String serialized = bucket.serialize(mapper);
|
||||||
|
|
|
@ -27,6 +27,7 @@ public class RateLimiters {
|
||||||
private final RateLimiter voiceDestinationLimiter;
|
private final RateLimiter voiceDestinationLimiter;
|
||||||
private final RateLimiter voiceDestinationDailyLimiter;
|
private final RateLimiter voiceDestinationDailyLimiter;
|
||||||
private final RateLimiter verifyLimiter;
|
private final RateLimiter verifyLimiter;
|
||||||
|
private final RateLimiter pinLimiter;
|
||||||
|
|
||||||
private final RateLimiter attachmentLimiter;
|
private final RateLimiter attachmentLimiter;
|
||||||
private final RateLimiter contactsLimiter;
|
private final RateLimiter contactsLimiter;
|
||||||
|
@ -57,6 +58,10 @@ public class RateLimiters {
|
||||||
config.getVerifyNumber().getBucketSize(),
|
config.getVerifyNumber().getBucketSize(),
|
||||||
config.getVerifyNumber().getLeakRatePerMinute());
|
config.getVerifyNumber().getLeakRatePerMinute());
|
||||||
|
|
||||||
|
this.pinLimiter = new LockingRateLimiter(cacheClient, "pin",
|
||||||
|
config.getVerifyPin().getBucketSize(),
|
||||||
|
config.getVerifyPin().getLeakRatePerMinute());
|
||||||
|
|
||||||
this.attachmentLimiter = new RateLimiter(cacheClient, "attachmentCreate",
|
this.attachmentLimiter = new RateLimiter(cacheClient, "attachmentCreate",
|
||||||
config.getAttachments().getBucketSize(),
|
config.getAttachments().getBucketSize(),
|
||||||
config.getAttachments().getLeakRatePerMinute());
|
config.getAttachments().getLeakRatePerMinute());
|
||||||
|
@ -130,6 +135,10 @@ public class RateLimiters {
|
||||||
return verifyLimiter;
|
return verifyLimiter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public RateLimiter getPinLimiter() {
|
||||||
|
return pinLimiter;
|
||||||
|
}
|
||||||
|
|
||||||
public RateLimiter getTurnLimiter() {
|
public RateLimiter getTurnLimiter() {
|
||||||
return turnLimiter;
|
return turnLimiter;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
/**
|
/*
|
||||||
* Copyright (C) 2013 Open WhisperSystems
|
* Copyright (C) 2013 Open WhisperSystems
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
@ -48,6 +48,9 @@ public class Account {
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
private String avatarDigest;
|
private String avatarDigest;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String pin;
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
private Device authenticatedDevice;
|
private Device authenticatedDevice;
|
||||||
|
|
||||||
|
@ -204,4 +207,12 @@ public class Account {
|
||||||
public void setAvatarDigest(String avatarDigest) {
|
public void setAvatarDigest(String avatarDigest) {
|
||||||
this.avatarDigest = avatarDigest;
|
this.avatarDigest = avatarDigest;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Optional<String> getPin() {
|
||||||
|
return Optional.fromNullable(pin);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPin(String pin) {
|
||||||
|
this.pin = pin;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
package org.whispersystems.textsecuregcm.tests.controllers;
|
package org.whispersystems.textsecuregcm.tests.controllers;
|
||||||
|
|
||||||
import com.google.common.base.Optional;
|
import com.google.common.base.Optional;
|
||||||
import org.apache.commons.codec.DecoderException;
|
|
||||||
import org.apache.commons.codec.binary.Hex;
|
|
||||||
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
|
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
|
@ -11,9 +9,13 @@ import org.whispersystems.dropwizard.simpleauth.AuthValueFactoryProvider;
|
||||||
import org.whispersystems.textsecuregcm.auth.StoredVerificationCode;
|
import org.whispersystems.textsecuregcm.auth.StoredVerificationCode;
|
||||||
import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator;
|
import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator;
|
||||||
import org.whispersystems.textsecuregcm.controllers.AccountController;
|
import org.whispersystems.textsecuregcm.controllers.AccountController;
|
||||||
|
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
|
||||||
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
|
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.RegistrationLock;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.RegistrationLockFailure;
|
||||||
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
||||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||||
|
import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper;
|
||||||
import org.whispersystems.textsecuregcm.providers.TimeProvider;
|
import org.whispersystems.textsecuregcm.providers.TimeProvider;
|
||||||
import org.whispersystems.textsecuregcm.sms.SmsSender;
|
import org.whispersystems.textsecuregcm.sms.SmsSender;
|
||||||
import org.whispersystems.textsecuregcm.storage.Account;
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
|
@ -38,21 +40,25 @@ public class AccountControllerTest {
|
||||||
|
|
||||||
private static final String SENDER = "+14152222222";
|
private static final String SENDER = "+14152222222";
|
||||||
private static final String SENDER_OLD = "+14151111111";
|
private static final String SENDER_OLD = "+14151111111";
|
||||||
|
private static final String SENDER_PIN = "+14153333333";
|
||||||
|
private static final String SENDER_OVER_PIN = "+14154444444";
|
||||||
|
|
||||||
private PendingAccountsManager pendingAccountsManager = mock(PendingAccountsManager.class);
|
private PendingAccountsManager pendingAccountsManager = mock(PendingAccountsManager.class);
|
||||||
private AccountsManager accountsManager = mock(AccountsManager.class );
|
private AccountsManager accountsManager = mock(AccountsManager.class );
|
||||||
private RateLimiters rateLimiters = mock(RateLimiters.class );
|
private RateLimiters rateLimiters = mock(RateLimiters.class );
|
||||||
private RateLimiter rateLimiter = mock(RateLimiter.class );
|
private RateLimiter rateLimiter = mock(RateLimiter.class );
|
||||||
|
private RateLimiter pinLimiter = mock(RateLimiter.class );
|
||||||
private SmsSender smsSender = mock(SmsSender.class );
|
private SmsSender smsSender = mock(SmsSender.class );
|
||||||
private MessagesManager storedMessages = mock(MessagesManager.class );
|
private MessagesManager storedMessages = mock(MessagesManager.class );
|
||||||
private TimeProvider timeProvider = mock(TimeProvider.class );
|
private TimeProvider timeProvider = mock(TimeProvider.class );
|
||||||
private TurnTokenGenerator turnTokenGenerator = mock(TurnTokenGenerator.class);
|
private TurnTokenGenerator turnTokenGenerator = mock(TurnTokenGenerator.class);
|
||||||
private static byte[] authorizationKey = decodeHex("3a078586eea8971155f5c1ebd73c8c923cbec1c3ed22a54722e4e88321dc749f");
|
private Account senderPinAccount = mock(Account.class);
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
public final ResourceTestRule resources = ResourceTestRule.builder()
|
public final ResourceTestRule resources = ResourceTestRule.builder()
|
||||||
.addProvider(AuthHelper.getAuthFilter())
|
.addProvider(AuthHelper.getAuthFilter())
|
||||||
.addProvider(new AuthValueFactoryProvider.Binder())
|
.addProvider(new AuthValueFactoryProvider.Binder())
|
||||||
|
.addProvider(new RateLimitExceededExceptionMapper())
|
||||||
.setMapper(SystemMapper.getMapper())
|
.setMapper(SystemMapper.getMapper())
|
||||||
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
||||||
.addResource(new AccountController(pendingAccountsManager,
|
.addResource(new AccountController(pendingAccountsManager,
|
||||||
|
@ -60,10 +66,8 @@ public class AccountControllerTest {
|
||||||
rateLimiters,
|
rateLimiters,
|
||||||
smsSender,
|
smsSender,
|
||||||
storedMessages,
|
storedMessages,
|
||||||
timeProvider,
|
|
||||||
Optional.of(authorizationKey),
|
|
||||||
turnTokenGenerator,
|
turnTokenGenerator,
|
||||||
new HashMap<String, Integer>()))
|
new HashMap<>()))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
|
||||||
|
@ -72,11 +76,25 @@ public class AccountControllerTest {
|
||||||
when(rateLimiters.getSmsDestinationLimiter()).thenReturn(rateLimiter);
|
when(rateLimiters.getSmsDestinationLimiter()).thenReturn(rateLimiter);
|
||||||
when(rateLimiters.getVoiceDestinationLimiter()).thenReturn(rateLimiter);
|
when(rateLimiters.getVoiceDestinationLimiter()).thenReturn(rateLimiter);
|
||||||
when(rateLimiters.getVerifyLimiter()).thenReturn(rateLimiter);
|
when(rateLimiters.getVerifyLimiter()).thenReturn(rateLimiter);
|
||||||
|
when(rateLimiters.getPinLimiter()).thenReturn(pinLimiter);
|
||||||
|
|
||||||
when(timeProvider.getCurrentTimeMillis()).thenReturn(System.currentTimeMillis());
|
when(timeProvider.getCurrentTimeMillis()).thenReturn(System.currentTimeMillis());
|
||||||
|
|
||||||
|
when(senderPinAccount.getPin()).thenReturn(Optional.of("31337"));
|
||||||
|
when(senderPinAccount.getLastSeen()).thenReturn(System.currentTimeMillis());
|
||||||
|
|
||||||
|
|
||||||
when(pendingAccountsManager.getCodeForNumber(SENDER)).thenReturn(Optional.of(new StoredVerificationCode("1234", System.currentTimeMillis())));
|
when(pendingAccountsManager.getCodeForNumber(SENDER)).thenReturn(Optional.of(new StoredVerificationCode("1234", System.currentTimeMillis())));
|
||||||
when(pendingAccountsManager.getCodeForNumber(SENDER_OLD)).thenReturn(Optional.of(new StoredVerificationCode("1234", System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(31))));
|
when(pendingAccountsManager.getCodeForNumber(SENDER_OLD)).thenReturn(Optional.of(new StoredVerificationCode("1234", System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(31))));
|
||||||
|
when(pendingAccountsManager.getCodeForNumber(SENDER_PIN)).thenReturn(Optional.of(new StoredVerificationCode("333333", System.currentTimeMillis())));
|
||||||
|
when(pendingAccountsManager.getCodeForNumber(SENDER_OVER_PIN)).thenReturn(Optional.of(new StoredVerificationCode("444444", System.currentTimeMillis())));
|
||||||
|
|
||||||
|
when(accountsManager.get(eq(SENDER_PIN))).thenReturn(Optional.of(senderPinAccount));
|
||||||
|
when(accountsManager.get(eq(SENDER_OVER_PIN))).thenReturn(Optional.of(senderPinAccount));
|
||||||
|
when(accountsManager.get(eq(SENDER))).thenReturn(Optional.absent());
|
||||||
|
when(accountsManager.get(eq(SENDER_OLD))).thenReturn(Optional.absent());
|
||||||
|
|
||||||
|
doThrow(new RateLimitExceededException(SENDER_OVER_PIN)).when(pinLimiter).validate(eq(SENDER_OVER_PIN));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -89,7 +107,7 @@ public class AccountControllerTest {
|
||||||
|
|
||||||
assertThat(response.getStatus()).isEqualTo(200);
|
assertThat(response.getStatus()).isEqualTo(200);
|
||||||
|
|
||||||
verify(smsSender).deliverSmsVerification(eq(SENDER), eq(Optional.<String>absent()), anyString());
|
verify(smsSender).deliverSmsVerification(eq(SENDER), eq(Optional.absent()), anyString());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -113,7 +131,7 @@ public class AccountControllerTest {
|
||||||
.target(String.format("/v1/accounts/code/%s", "1234"))
|
.target(String.format("/v1/accounts/code/%s", "1234"))
|
||||||
.request()
|
.request()
|
||||||
.header("Authorization", AuthHelper.getAuthHeader(SENDER, "bar"))
|
.header("Authorization", AuthHelper.getAuthHeader(SENDER, "bar"))
|
||||||
.put(Entity.entity(new AccountAttributes("keykeykeykey", false, 2222),
|
.put(Entity.entity(new AccountAttributes("keykeykeykey", false, 2222, null),
|
||||||
MediaType.APPLICATION_JSON_TYPE));
|
MediaType.APPLICATION_JSON_TYPE));
|
||||||
|
|
||||||
assertThat(response.getStatus()).isEqualTo(204);
|
assertThat(response.getStatus()).isEqualTo(204);
|
||||||
|
@ -128,7 +146,7 @@ public class AccountControllerTest {
|
||||||
.target(String.format("/v1/accounts/code/%s", "1234"))
|
.target(String.format("/v1/accounts/code/%s", "1234"))
|
||||||
.request()
|
.request()
|
||||||
.header("Authorization", AuthHelper.getAuthHeader(SENDER_OLD, "bar"))
|
.header("Authorization", AuthHelper.getAuthHeader(SENDER_OLD, "bar"))
|
||||||
.put(Entity.entity(new AccountAttributes("keykeykeykey", false, 2222),
|
.put(Entity.entity(new AccountAttributes("keykeykeykey", false, 2222, null),
|
||||||
MediaType.APPLICATION_JSON_TYPE));
|
MediaType.APPLICATION_JSON_TYPE));
|
||||||
|
|
||||||
assertThat(response.getStatus()).isEqualTo(403);
|
assertThat(response.getStatus()).isEqualTo(403);
|
||||||
|
@ -143,7 +161,7 @@ public class AccountControllerTest {
|
||||||
.target(String.format("/v1/accounts/code/%s", "1111"))
|
.target(String.format("/v1/accounts/code/%s", "1111"))
|
||||||
.request()
|
.request()
|
||||||
.header("Authorization", AuthHelper.getAuthHeader(SENDER, "bar"))
|
.header("Authorization", AuthHelper.getAuthHeader(SENDER, "bar"))
|
||||||
.put(Entity.entity(new AccountAttributes("keykeykeykey", false, 3333),
|
.put(Entity.entity(new AccountAttributes("keykeykeykey", false, 3333, null),
|
||||||
MediaType.APPLICATION_JSON_TYPE));
|
MediaType.APPLICATION_JSON_TYPE));
|
||||||
|
|
||||||
assertThat(response.getStatus()).isEqualTo(403);
|
assertThat(response.getStatus()).isEqualTo(403);
|
||||||
|
@ -152,87 +170,116 @@ public class AccountControllerTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testVerifyToken() throws Exception {
|
public void testVerifyPin() throws Exception {
|
||||||
when(timeProvider.getCurrentTimeMillis()).thenReturn(1415917053106L);
|
|
||||||
|
|
||||||
String token = SENDER + ":1415906573:af4f046107c21721224a";
|
|
||||||
|
|
||||||
Response response =
|
Response response =
|
||||||
resources.getJerseyTest()
|
resources.getJerseyTest()
|
||||||
.target(String.format("/v1/accounts/token/%s", token))
|
.target(String.format("/v1/accounts/code/%s", "333333"))
|
||||||
.request()
|
.request()
|
||||||
.header("Authorization", AuthHelper.getAuthHeader(SENDER, "bar"))
|
.header("Authorization", AuthHelper.getAuthHeader(SENDER_PIN, "bar"))
|
||||||
.put(Entity.entity(new AccountAttributes("keykeykeykey", false, 4444),
|
.put(Entity.entity(new AccountAttributes("keykeykeykey", false, 3333, "31337"),
|
||||||
MediaType.APPLICATION_JSON_TYPE));
|
MediaType.APPLICATION_JSON_TYPE));
|
||||||
|
|
||||||
assertThat(response.getStatus()).isEqualTo(204);
|
assertThat(response.getStatus()).isEqualTo(204);
|
||||||
|
|
||||||
verify(accountsManager, times(1)).create(isA(Account.class));
|
verify(pinLimiter).validate(eq(SENDER_PIN));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testVerifyBadToken() throws Exception {
|
public void testVerifyWrongPin() throws Exception {
|
||||||
when(timeProvider.getCurrentTimeMillis()).thenReturn(1415917053106L);
|
|
||||||
|
|
||||||
String token = SENDER + ":1415906574:af4f046107c21721224a";
|
|
||||||
|
|
||||||
Response response =
|
Response response =
|
||||||
resources.getJerseyTest()
|
resources.getJerseyTest()
|
||||||
.target(String.format("/v1/accounts/token/%s", token))
|
.target(String.format("/v1/accounts/code/%s", "333333"))
|
||||||
.request()
|
.request()
|
||||||
.header("Authorization", AuthHelper.getAuthHeader(SENDER, "bar"))
|
.header("Authorization", AuthHelper.getAuthHeader(SENDER_PIN, "bar"))
|
||||||
.put(Entity.entity(new AccountAttributes("keykeykeykey", false, 4444),
|
.put(Entity.entity(new AccountAttributes("keykeykeykey", false, 3333, "31338"),
|
||||||
MediaType.APPLICATION_JSON_TYPE));
|
MediaType.APPLICATION_JSON_TYPE));
|
||||||
|
|
||||||
assertThat(response.getStatus()).isEqualTo(403);
|
assertThat(response.getStatus()).isEqualTo(423);
|
||||||
|
|
||||||
verifyNoMoreInteractions(accountsManager);
|
verify(pinLimiter).validate(eq(SENDER_PIN));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testVerifyWrongToken() throws Exception {
|
public void testVerifyNoPin() throws Exception {
|
||||||
when(timeProvider.getCurrentTimeMillis()).thenReturn(1415917053106L);
|
|
||||||
|
|
||||||
String token = SENDER + ":1415906573:af4f046107c21721224a";
|
|
||||||
|
|
||||||
Response response =
|
Response response =
|
||||||
resources.getJerseyTest()
|
resources.getJerseyTest()
|
||||||
.target(String.format("/v1/accounts/token/%s", token))
|
.target(String.format("/v1/accounts/code/%s", "333333"))
|
||||||
.request()
|
.request()
|
||||||
.header("Authorization", AuthHelper.getAuthHeader("+14151111111", "bar"))
|
.header("Authorization", AuthHelper.getAuthHeader(SENDER_PIN, "bar"))
|
||||||
.put(Entity.entity(new AccountAttributes("keykeykeykey", false, 4444),
|
.put(Entity.entity(new AccountAttributes("keykeykeykey", false, 3333, null),
|
||||||
MediaType.APPLICATION_JSON_TYPE));
|
MediaType.APPLICATION_JSON_TYPE));
|
||||||
|
|
||||||
assertThat(response.getStatus()).isEqualTo(403);
|
assertThat(response.getStatus()).isEqualTo(423);
|
||||||
|
|
||||||
verifyNoMoreInteractions(accountsManager);
|
RegistrationLockFailure failure = response.readEntity(RegistrationLockFailure.class);
|
||||||
|
|
||||||
|
verify(pinLimiter).validate(eq(SENDER_PIN));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testVerifyExpiredToken() throws Exception {
|
public void testVerifyLimitPin() throws Exception {
|
||||||
when(timeProvider.getCurrentTimeMillis()).thenReturn(1416003757901L);
|
|
||||||
|
|
||||||
String token = SENDER + ":1415906573:af4f046107c21721224a";
|
|
||||||
|
|
||||||
Response response =
|
Response response =
|
||||||
resources.getJerseyTest()
|
resources.getJerseyTest()
|
||||||
.target(String.format("/v1/accounts/token/%s", token))
|
.target(String.format("/v1/accounts/code/%s", "444444"))
|
||||||
.request()
|
.request()
|
||||||
.header("Authorization", AuthHelper.getAuthHeader(SENDER, "bar"))
|
.header("Authorization", AuthHelper.getAuthHeader(SENDER_OVER_PIN, "bar"))
|
||||||
.put(Entity.entity(new AccountAttributes("keykeykeykey", false, 4444),
|
.put(Entity.entity(new AccountAttributes("keykeykeykey", false, 3333, "31337"),
|
||||||
MediaType.APPLICATION_JSON_TYPE));
|
MediaType.APPLICATION_JSON_TYPE));
|
||||||
|
|
||||||
assertThat(response.getStatus()).isEqualTo(403);
|
assertThat(response.getStatus()).isEqualTo(413);
|
||||||
|
|
||||||
verifyNoMoreInteractions(accountsManager);
|
verify(rateLimiter).clear(eq(SENDER_OVER_PIN));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static byte[] decodeHex(String hex) {
|
@Test
|
||||||
|
public void testVerifyOldPin() throws Exception {
|
||||||
try {
|
try {
|
||||||
return Hex.decodeHex(hex.toCharArray());
|
when(senderPinAccount.getLastSeen()).thenReturn(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(7));
|
||||||
} catch (DecoderException e) {
|
|
||||||
throw new AssertionError(e);
|
Response response =
|
||||||
|
resources.getJerseyTest()
|
||||||
|
.target(String.format("/v1/accounts/code/%s", "444444"))
|
||||||
|
.request()
|
||||||
|
.header("Authorization", AuthHelper.getAuthHeader(SENDER_OVER_PIN, "bar"))
|
||||||
|
.put(Entity.entity(new AccountAttributes("keykeykeykey", false, 3333, null),
|
||||||
|
MediaType.APPLICATION_JSON_TYPE));
|
||||||
|
|
||||||
|
assertThat(response.getStatus()).isEqualTo(204);
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
when(senderPinAccount.getLastSeen()).thenReturn(System.currentTimeMillis());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSetPin() throws Exception {
|
||||||
|
Response response =
|
||||||
|
resources.getJerseyTest()
|
||||||
|
.target("/v1/accounts/pin/")
|
||||||
|
.request()
|
||||||
|
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
||||||
|
.put(Entity.json(new RegistrationLock("31337")));
|
||||||
|
|
||||||
|
assertThat(response.getStatus()).isEqualTo(204);
|
||||||
|
|
||||||
|
verify(AuthHelper.VALID_ACCOUNT).setPin(eq("31337"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSetShortPin() throws Exception {
|
||||||
|
Response response =
|
||||||
|
resources.getJerseyTest()
|
||||||
|
.target("/v1/accounts/pin/")
|
||||||
|
.request()
|
||||||
|
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
||||||
|
.put(Entity.json(new RegistrationLock("313")));
|
||||||
|
|
||||||
|
assertThat(response.getStatus()).isEqualTo(422);
|
||||||
|
|
||||||
|
verify(AuthHelper.VALID_ACCOUNT, never()).setPin(anyString());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -125,7 +125,7 @@ public class DeviceControllerTest {
|
||||||
.target("/v1/devices/5678901")
|
.target("/v1/devices/5678901")
|
||||||
.request()
|
.request()
|
||||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, "password1"))
|
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, "password1"))
|
||||||
.put(Entity.entity(new AccountAttributes("keykeykeykey", false, 1234),
|
.put(Entity.entity(new AccountAttributes("keykeykeykey", false, 1234, null),
|
||||||
MediaType.APPLICATION_JSON_TYPE),
|
MediaType.APPLICATION_JSON_TYPE),
|
||||||
DeviceResponse.class);
|
DeviceResponse.class);
|
||||||
|
|
||||||
|
@ -149,7 +149,7 @@ public class DeviceControllerTest {
|
||||||
.target("/v1/devices/5678902")
|
.target("/v1/devices/5678902")
|
||||||
.request()
|
.request()
|
||||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, "password1"))
|
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, "password1"))
|
||||||
.put(Entity.entity(new AccountAttributes("keykeykeykey", false, 1234),
|
.put(Entity.entity(new AccountAttributes("keykeykeykey", false, 1234, null),
|
||||||
MediaType.APPLICATION_JSON_TYPE));
|
MediaType.APPLICATION_JSON_TYPE));
|
||||||
|
|
||||||
assertThat(response.getStatus()).isEqualTo(403);
|
assertThat(response.getStatus()).isEqualTo(403);
|
||||||
|
@ -163,7 +163,7 @@ public class DeviceControllerTest {
|
||||||
.target("/v1/devices/1112223")
|
.target("/v1/devices/1112223")
|
||||||
.request()
|
.request()
|
||||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER_TWO, AuthHelper.VALID_PASSWORD_TWO))
|
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER_TWO, AuthHelper.VALID_PASSWORD_TWO))
|
||||||
.put(Entity.entity(new AccountAttributes("keykeykeykey", false, 1234),
|
.put(Entity.entity(new AccountAttributes("keykeykeykey", false, 1234, null),
|
||||||
MediaType.APPLICATION_JSON_TYPE));
|
MediaType.APPLICATION_JSON_TYPE));
|
||||||
|
|
||||||
assertThat(response.getStatus()).isEqualTo(403);
|
assertThat(response.getStatus()).isEqualTo(403);
|
||||||
|
@ -189,7 +189,7 @@ public class DeviceControllerTest {
|
||||||
.target("/v1/devices/5678901")
|
.target("/v1/devices/5678901")
|
||||||
.request()
|
.request()
|
||||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, "password1"))
|
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, "password1"))
|
||||||
.put(Entity.entity(new AccountAttributes("keykeykeykey", false, 1234, "this is a really long name that is longer than 80 characters", true, true),
|
.put(Entity.entity(new AccountAttributes("keykeykeykey", false, 1234, "this is a really long name that is longer than 80 characters", true, true, null),
|
||||||
MediaType.APPLICATION_JSON_TYPE));
|
MediaType.APPLICATION_JSON_TYPE));
|
||||||
|
|
||||||
assertEquals(response.getStatus(), 422);
|
assertEquals(response.getStatus(), 422);
|
||||||
|
|
Loading…
Reference in New Issue