mirror of
https://github.com/oxen-io/session-android.git
synced 2025-06-09 09:28:34 +00:00
Local SMS rate limiting.
This commit is contained in:
parent
8c037456e7
commit
16f41555ba
@ -203,6 +203,14 @@ public final class EnterPhoneNumberFragment extends BaseRegistrationFragment {
|
|||||||
|
|
||||||
NavController navController = Navigation.findNavController(register);
|
NavController navController = Navigation.findNavController(register);
|
||||||
|
|
||||||
|
if (!model.getRequestLimiter().canRequest(mode, e164number, System.currentTimeMillis())) {
|
||||||
|
Log.i(TAG, "Local rate limited");
|
||||||
|
navController.navigate(EnterPhoneNumberFragmentDirections.actionEnterVerificationCode());
|
||||||
|
cancelSpinning(register);
|
||||||
|
enableAllEntries();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
RegistrationService registrationService = RegistrationService.getInstance(e164number, model.getRegistrationSecret());
|
RegistrationService registrationService = RegistrationService.getInstance(e164number, model.getRegistrationSecret());
|
||||||
|
|
||||||
registrationService.requestVerificationCode(requireActivity(), mode, captcha,
|
registrationService.requestVerificationCode(requireActivity(), mode, captcha,
|
||||||
@ -213,6 +221,8 @@ public final class EnterPhoneNumberFragment extends BaseRegistrationFragment {
|
|||||||
navController.navigate(EnterPhoneNumberFragmentDirections.actionRequestCaptcha());
|
navController.navigate(EnterPhoneNumberFragmentDirections.actionRequestCaptcha());
|
||||||
cancelSpinning(register);
|
cancelSpinning(register);
|
||||||
enableAllEntries();
|
enableAllEntries();
|
||||||
|
model.getRequestLimiter().onUnsuccessfulRequest();
|
||||||
|
model.updateLimiter();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -222,6 +232,8 @@ public final class EnterPhoneNumberFragment extends BaseRegistrationFragment {
|
|||||||
navController.navigate(EnterPhoneNumberFragmentDirections.actionEnterVerificationCode());
|
navController.navigate(EnterPhoneNumberFragmentDirections.actionEnterVerificationCode());
|
||||||
cancelSpinning(register);
|
cancelSpinning(register);
|
||||||
enableAllEntries();
|
enableAllEntries();
|
||||||
|
model.getRequestLimiter().onSuccessfulRequest(mode, e164number, System.currentTimeMillis());
|
||||||
|
model.updateLimiter();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -229,6 +241,8 @@ public final class EnterPhoneNumberFragment extends BaseRegistrationFragment {
|
|||||||
Toast.makeText(register.getContext(), R.string.RegistrationActivity_unable_to_connect_to_service, Toast.LENGTH_LONG).show();
|
Toast.makeText(register.getContext(), R.string.RegistrationActivity_unable_to_connect_to_service, Toast.LENGTH_LONG).show();
|
||||||
cancelSpinning(register);
|
cancelSpinning(register);
|
||||||
enableAllEntries();
|
enableAllEntries();
|
||||||
|
model.getRequestLimiter().onUnsuccessfulRequest();
|
||||||
|
model.updateLimiter();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,104 @@
|
|||||||
|
package org.thoughtcrime.securesms.registration.viewmodel;
|
||||||
|
|
||||||
|
import android.os.Parcel;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
|
||||||
|
import androidx.annotation.MainThread;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.registration.service.RegistrationCodeRequest;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public final class LocalCodeRequestRateLimiter implements Parcelable {
|
||||||
|
|
||||||
|
private final long timePeriod;
|
||||||
|
private final Map<RegistrationCodeRequest.Mode, Data> dataMap;
|
||||||
|
|
||||||
|
public LocalCodeRequestRateLimiter(long timePeriod) {
|
||||||
|
this.timePeriod = timePeriod;
|
||||||
|
this.dataMap = new HashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainThread
|
||||||
|
public boolean canRequest(@NonNull RegistrationCodeRequest.Mode mode, @NonNull String e164Number, long currentTime) {
|
||||||
|
Data data = dataMap.get(mode);
|
||||||
|
|
||||||
|
return data == null || !data.limited(e164Number, currentTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call this when the server has returned that it was successful in requesting a code via the specified mode.
|
||||||
|
*/
|
||||||
|
@MainThread
|
||||||
|
public void onSuccessfulRequest(@NonNull RegistrationCodeRequest.Mode mode, @NonNull String e164Number, long currentTime) {
|
||||||
|
dataMap.put(mode, new Data(e164Number, currentTime + timePeriod));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call this if a mode was unsuccessful in sending.
|
||||||
|
*/
|
||||||
|
@MainThread
|
||||||
|
public void onUnsuccessfulRequest() {
|
||||||
|
dataMap.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
static class Data {
|
||||||
|
|
||||||
|
final String e164Number;
|
||||||
|
final long limitedUntil;
|
||||||
|
|
||||||
|
Data(@NonNull String e164Number, long limitedUntil) {
|
||||||
|
this.e164Number = e164Number;
|
||||||
|
this.limitedUntil = limitedUntil;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean limited(String e164Number, long currentTime) {
|
||||||
|
return this.e164Number.equals(e164Number) && currentTime < limitedUntil;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final Creator<LocalCodeRequestRateLimiter> CREATOR = new Creator<LocalCodeRequestRateLimiter>() {
|
||||||
|
@Override
|
||||||
|
public LocalCodeRequestRateLimiter createFromParcel(Parcel in) {
|
||||||
|
long timePeriod = in.readLong();
|
||||||
|
int numberOfMapEntries = in.readInt();
|
||||||
|
|
||||||
|
LocalCodeRequestRateLimiter localCodeRequestRateLimiter = new LocalCodeRequestRateLimiter(timePeriod);
|
||||||
|
|
||||||
|
for (int i = 0; i < numberOfMapEntries; i++) {
|
||||||
|
RegistrationCodeRequest.Mode mode = RegistrationCodeRequest.Mode.values()[in.readInt()];
|
||||||
|
String e164Number = in.readString();
|
||||||
|
long limitedUntil = in.readLong();
|
||||||
|
|
||||||
|
localCodeRequestRateLimiter.dataMap.put(mode, new Data(Objects.requireNonNull(e164Number), limitedUntil));
|
||||||
|
}
|
||||||
|
return localCodeRequestRateLimiter;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LocalCodeRequestRateLimiter[] newArray(int size) {
|
||||||
|
return new LocalCodeRequestRateLimiter[size];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int describeContents() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeToParcel(Parcel dest, int flags) {
|
||||||
|
dest.writeLong(timePeriod);
|
||||||
|
dest.writeInt(dataMap.size());
|
||||||
|
|
||||||
|
for (Map.Entry<RegistrationCodeRequest.Mode, Data> a : dataMap.entrySet()) {
|
||||||
|
dest.writeInt(a.getKey().ordinal());
|
||||||
|
dest.writeString(a.getValue().e164Number);
|
||||||
|
dest.writeLong(a.getValue().limitedUntil);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -12,13 +12,14 @@ import org.thoughtcrime.securesms.util.Util;
|
|||||||
|
|
||||||
public final class RegistrationViewModel extends ViewModel {
|
public final class RegistrationViewModel extends ViewModel {
|
||||||
|
|
||||||
private final String secret;
|
private final String secret;
|
||||||
private final MutableLiveData<NumberViewState> number;
|
private final MutableLiveData<NumberViewState> number;
|
||||||
private final MutableLiveData<String> textCodeEntered;
|
private final MutableLiveData<String> textCodeEntered;
|
||||||
private final MutableLiveData<String> captchaToken;
|
private final MutableLiveData<String> captchaToken;
|
||||||
private final MutableLiveData<String> fcmToken;
|
private final MutableLiveData<String> fcmToken;
|
||||||
private final MutableLiveData<Boolean> restoreFlowShown;
|
private final MutableLiveData<Boolean> restoreFlowShown;
|
||||||
private final MutableLiveData<Integer> successfulCodeRequestAttempts;
|
private final MutableLiveData<Integer> successfulCodeRequestAttempts;
|
||||||
|
private final MutableLiveData<LocalCodeRequestRateLimiter> requestLimiter;
|
||||||
|
|
||||||
public RegistrationViewModel(@NonNull SavedStateHandle savedStateHandle) {
|
public RegistrationViewModel(@NonNull SavedStateHandle savedStateHandle) {
|
||||||
secret = loadValue(savedStateHandle, "REGISTRATION_SECRET", Util.getSecret(18));
|
secret = loadValue(savedStateHandle, "REGISTRATION_SECRET", Util.getSecret(18));
|
||||||
@ -29,6 +30,7 @@ public final class RegistrationViewModel extends ViewModel {
|
|||||||
fcmToken = savedStateHandle.getLiveData("FCM_TOKEN");
|
fcmToken = savedStateHandle.getLiveData("FCM_TOKEN");
|
||||||
restoreFlowShown = savedStateHandle.getLiveData("RESTORE_FLOW_SHOWN", false);
|
restoreFlowShown = savedStateHandle.getLiveData("RESTORE_FLOW_SHOWN", false);
|
||||||
successfulCodeRequestAttempts = savedStateHandle.getLiveData("SUCCESSFUL_CODE_REQUEST_ATTEMPTS", 0);
|
successfulCodeRequestAttempts = savedStateHandle.getLiveData("SUCCESSFUL_CODE_REQUEST_ATTEMPTS", 0);
|
||||||
|
requestLimiter = savedStateHandle.getLiveData("REQUEST_RATE_LIMITER", new LocalCodeRequestRateLimiter(60_000));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static <T> T loadValue(@NonNull SavedStateHandle savedStateHandle, @NonNull String key, @NonNull T initialValue) {
|
private static <T> T loadValue(@NonNull SavedStateHandle savedStateHandle, @NonNull String key, @NonNull T initialValue) {
|
||||||
@ -127,4 +129,13 @@ public final class RegistrationViewModel extends ViewModel {
|
|||||||
public LiveData<Integer> getSuccessfulCodeRequestAttempts() {
|
public LiveData<Integer> getSuccessfulCodeRequestAttempts() {
|
||||||
return successfulCodeRequestAttempts;
|
return successfulCodeRequestAttempts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public @NonNull LocalCodeRequestRateLimiter getRequestLimiter() {
|
||||||
|
//noinspection ConstantConditions Live data was given an initial value
|
||||||
|
return requestLimiter.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateLimiter() {
|
||||||
|
requestLimiter.setValue(requestLimiter.getValue());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,74 @@
|
|||||||
|
package org.thoughtcrime.securesms.registration.viewmodel;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.thoughtcrime.securesms.registration.service.RegistrationCodeRequest;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public final class LocalCodeRequestRateLimiterTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void initially_can_request() {
|
||||||
|
LocalCodeRequestRateLimiter limiter = new LocalCodeRequestRateLimiter(60_000);
|
||||||
|
|
||||||
|
assertTrue(limiter.canRequest(RegistrationCodeRequest.Mode.SMS_NO_FCM, "+155512345678", 1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void cant_request_within_same_time_period() {
|
||||||
|
LocalCodeRequestRateLimiter limiter = new LocalCodeRequestRateLimiter(60_000);
|
||||||
|
|
||||||
|
assertTrue(limiter.canRequest(RegistrationCodeRequest.Mode.SMS_NO_FCM, "+155512345678", 1000));
|
||||||
|
|
||||||
|
limiter.onSuccessfulRequest(RegistrationCodeRequest.Mode.SMS_NO_FCM, "+155512345678", 1000);
|
||||||
|
|
||||||
|
assertFalse(limiter.canRequest(RegistrationCodeRequest.Mode.SMS_NO_FCM, "+155512345678", 1000 + 59_000));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void can_request_within_same_time_period_if_different_number() {
|
||||||
|
LocalCodeRequestRateLimiter limiter = new LocalCodeRequestRateLimiter(60_000);
|
||||||
|
|
||||||
|
assertTrue(limiter.canRequest(RegistrationCodeRequest.Mode.SMS_NO_FCM, "+155512345678", 1000));
|
||||||
|
|
||||||
|
limiter.onSuccessfulRequest(RegistrationCodeRequest.Mode.SMS_NO_FCM, "+155512345678", 1000);
|
||||||
|
|
||||||
|
assertTrue(limiter.canRequest(RegistrationCodeRequest.Mode.SMS_NO_FCM, "+15559874566", 1000 + 59_000));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void can_request_within_same_time_period_if_different_mode() {
|
||||||
|
LocalCodeRequestRateLimiter limiter = new LocalCodeRequestRateLimiter(60_000);
|
||||||
|
|
||||||
|
assertTrue(limiter.canRequest(RegistrationCodeRequest.Mode.SMS_NO_FCM, "+155512345678", 1000));
|
||||||
|
|
||||||
|
limiter.onSuccessfulRequest(RegistrationCodeRequest.Mode.SMS_NO_FCM, "+155512345678", 1000);
|
||||||
|
|
||||||
|
assertTrue(limiter.canRequest(RegistrationCodeRequest.Mode.SMS_FCM_NO_LISTENER, "+155512345678", 1000 + 59_000));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void can_request_after_time_period() {
|
||||||
|
LocalCodeRequestRateLimiter limiter = new LocalCodeRequestRateLimiter(60_000);
|
||||||
|
|
||||||
|
assertTrue(limiter.canRequest(RegistrationCodeRequest.Mode.SMS_NO_FCM, "+155512345678", 1000));
|
||||||
|
|
||||||
|
limiter.onSuccessfulRequest(RegistrationCodeRequest.Mode.SMS_NO_FCM, "+155512345678", 1000);
|
||||||
|
|
||||||
|
assertTrue(limiter.canRequest(RegistrationCodeRequest.Mode.SMS_NO_FCM, "+155512345678", 1000 + 60_001));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void can_request_within_same_time_period_if_an_unsuccessful_request_is_seen() {
|
||||||
|
LocalCodeRequestRateLimiter limiter = new LocalCodeRequestRateLimiter(60_000);
|
||||||
|
|
||||||
|
assertTrue(limiter.canRequest(RegistrationCodeRequest.Mode.SMS_NO_FCM, "+155512345678", 1000));
|
||||||
|
|
||||||
|
limiter.onSuccessfulRequest(RegistrationCodeRequest.Mode.SMS_NO_FCM, "+155512345678", 1000);
|
||||||
|
|
||||||
|
limiter.onUnsuccessfulRequest();
|
||||||
|
|
||||||
|
assertTrue(limiter.canRequest(RegistrationCodeRequest.Mode.SMS_NO_FCM, "+155512345678", 1000 + 59_000));
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user