mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-19 16:19:33 +01:00
Ensure rate limit dialog appears during calls.
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.ratelimit
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.WorkerThread
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
import org.greenrobot.eventbus.ThreadMode
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.ParentStoryId
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobs.SubmitRateLimitPushChallengeJob.SuccessEvent
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.notifications.v2.ConversationId
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException
|
||||
import org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* Reusable ProofRequiredException handling code.
|
||||
*/
|
||||
object ProofRequiredExceptionHandler {
|
||||
|
||||
private val TAG = Log.tag(ProofRequiredExceptionHandler::class)
|
||||
private val PUSH_CHALLENGE_TIMEOUT: Duration = 10.seconds
|
||||
|
||||
/**
|
||||
* Handles the given exception, updating state as necessary.
|
||||
*/
|
||||
@JvmStatic
|
||||
@WorkerThread
|
||||
fun handle(context: Context, proofRequired: ProofRequiredException, recipient: Recipient?, threadId: Long, messageId: Long): Result {
|
||||
Log.w(TAG, "[Proof Required] Options: ${proofRequired.options}")
|
||||
|
||||
try {
|
||||
if (ProofRequiredException.Option.PUSH_CHALLENGE in proofRequired.options) {
|
||||
AppDependencies.signalServiceAccountManager.requestRateLimitPushChallenge()
|
||||
Log.i(TAG, "[Proof Required] Successfully requested a challenge. Waiting up to $PUSH_CHALLENGE_TIMEOUT ms.")
|
||||
|
||||
val success = PushChallengeRequest(PUSH_CHALLENGE_TIMEOUT).blockUntilSuccess()
|
||||
|
||||
if (success) {
|
||||
Log.i(TAG, "Successfully responded to a push challenge. Retrying message send.")
|
||||
return Result.RETRY_NOW
|
||||
} else {
|
||||
Log.w(TAG, "Failed to respond to the push challeng in time. Falling back.")
|
||||
}
|
||||
}
|
||||
} catch (e: NonSuccessfulResponseCodeException) {
|
||||
Log.w(TAG, "[Proof Required] Could not request a push challenge (${e.code}). Falling back.", e)
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "[Proof Required] Network error when requesting push challenge. Retrying later.")
|
||||
return Result.RETRY_LATER
|
||||
}
|
||||
|
||||
if (messageId > 0) {
|
||||
Log.w(TAG, "[Proof Required] Marking message as rate-limited. (id: $messageId, thread: $threadId)")
|
||||
SignalDatabase.messages.markAsRateLimited(messageId)
|
||||
}
|
||||
|
||||
if (ProofRequiredException.Option.CAPTCHA in proofRequired.options) {
|
||||
Log.i(TAG, "[Proof Required] CAPTCHA required.")
|
||||
SignalStore.rateLimit.markNeedsRecaptcha(proofRequired.token)
|
||||
|
||||
if (recipient != null && messageId > -1L) {
|
||||
val groupReply: ParentStoryId.GroupReply? = SignalDatabase.messages.getParentStoryIdForGroupReply(messageId)
|
||||
AppDependencies.messageNotifier.notifyProofRequired(context, recipient, ConversationId.fromThreadAndReply(threadId, groupReply))
|
||||
} else {
|
||||
Log.w(TAG, "[Proof Required] No recipient! Couldn't notify.")
|
||||
}
|
||||
}
|
||||
|
||||
return Result.RETHROW
|
||||
}
|
||||
|
||||
enum class Result {
|
||||
/**
|
||||
* The challenge was successful and the message send can be retried immediately.
|
||||
*/
|
||||
RETRY_NOW,
|
||||
|
||||
/**
|
||||
* The challenge failed due to a network error and should be scheduled to retry with some offset.
|
||||
*/
|
||||
RETRY_LATER,
|
||||
|
||||
/**
|
||||
* The caller should rethrow the original error.
|
||||
*/
|
||||
RETHROW;
|
||||
|
||||
fun isRetry() = this != RETHROW
|
||||
}
|
||||
|
||||
private class PushChallengeRequest(val timeout: Duration) {
|
||||
private val latch = CountDownLatch(1)
|
||||
private val eventBus = EventBus.getDefault()
|
||||
|
||||
fun blockUntilSuccess(): Boolean {
|
||||
eventBus.register(this)
|
||||
|
||||
return try {
|
||||
latch.await(timeout.inWholeMilliseconds, TimeUnit.MILLISECONDS)
|
||||
} catch (e: InterruptedException) {
|
||||
Log.w(TAG, "[Proof Required] Interrupted?", e)
|
||||
false
|
||||
} finally {
|
||||
eventBus.unregister(this)
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.POSTING)
|
||||
fun onSuccessReceived(event: SuccessEvent) {
|
||||
Log.i(TAG, "[Proof Required] Received a successful result!")
|
||||
latch.countDown()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,11 @@ import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.result.contract.ActivityResultContract;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.signal.core.util.concurrent.SimpleTask;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.BuildConfig;
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
|
||||
@@ -20,7 +23,6 @@ import org.thoughtcrime.securesms.dependencies.AppDependencies;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.signal.core.util.concurrent.SimpleTask;
|
||||
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
||||
|
||||
@@ -36,10 +38,6 @@ public class RecaptchaProofActivity extends PassphraseRequiredActivity {
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicTheme();
|
||||
|
||||
public static @NonNull Intent getIntent(@NonNull Context context) {
|
||||
return new Intent(context, RecaptchaProofActivity.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPreCreate() {
|
||||
dynamicTheme.onCreate(this);
|
||||
@@ -120,6 +118,7 @@ public class RecaptchaProofActivity extends PassphraseRequiredActivity {
|
||||
if (result.clearState) {
|
||||
Log.i(TAG, "Considering the response sufficient to clear the slate.");
|
||||
SignalStore.rateLimit().onProofAccepted();
|
||||
setResult(RESULT_OK);
|
||||
}
|
||||
|
||||
if (!result.success) {
|
||||
@@ -140,4 +139,17 @@ public class RecaptchaProofActivity extends PassphraseRequiredActivity {
|
||||
this.success = success;
|
||||
}
|
||||
}
|
||||
|
||||
public static class RecaptchaProofContract extends ActivityResultContract<Void, Boolean> {
|
||||
|
||||
@Override
|
||||
public @NonNull Intent createIntent(@NonNull Context context, Void unused) {
|
||||
return new Intent(context, RecaptchaProofActivity.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean parseResult(int resultCode, @Nullable Intent intent) {
|
||||
return resultCode == RESULT_OK;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
package org.thoughtcrime.securesms.ratelimit;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
@@ -24,6 +26,8 @@ public final class RecaptchaProofBottomSheetFragment extends BottomSheetDialogFr
|
||||
|
||||
private static final String TAG = Log.tag(RecaptchaProofBottomSheetFragment.class);
|
||||
|
||||
private ActivityResultLauncher<Void> launcher;
|
||||
|
||||
public static void show(@NonNull FragmentManager manager) {
|
||||
new RecaptchaProofBottomSheetFragment().show(manager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG);
|
||||
}
|
||||
@@ -38,11 +42,25 @@ public final class RecaptchaProofBottomSheetFragment extends BottomSheetDialogFr
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.recaptcha_required_bottom_sheet, container, false);
|
||||
|
||||
view.findViewById(R.id.recaptcha_sheet_ok_button).setOnClickListener(v -> {
|
||||
Activity activity = requireActivity();
|
||||
final Callback callback;
|
||||
|
||||
if (activity instanceof Callback) {
|
||||
callback = (Callback) activity;
|
||||
} else {
|
||||
callback = null;
|
||||
}
|
||||
|
||||
launcher = registerForActivityResult(new RecaptchaProofActivity.RecaptchaProofContract(), (isOk) -> {
|
||||
if (isOk && callback != null) {
|
||||
callback.onProofCompleted();
|
||||
}
|
||||
|
||||
dismissAllowingStateLoss();
|
||||
startActivity(RecaptchaProofActivity.getIntent(requireContext()));
|
||||
});
|
||||
|
||||
view.findViewById(R.id.recaptcha_sheet_ok_button).setOnClickListener(v -> launcher.launch(null));
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@@ -62,4 +80,12 @@ public final class RecaptchaProofBottomSheetFragment extends BottomSheetDialogFr
|
||||
Log.i(TAG, "Ignoring repeat show.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional callback interface to be invoked when the user successfully completes a push challenge.
|
||||
* This is expected to be implemented on the activity which is displaying this fragment.
|
||||
*/
|
||||
public interface Callback {
|
||||
void onProofCompleted();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user