mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-19 16:19:33 +01:00
Handle 428 rate limiting.
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
package org.thoughtcrime.securesms.ratelimit;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobs.PushGroupSendJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushMediaSendJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushTextSendJob;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
public final class RateLimitUtil {
|
||||
|
||||
private static final String TAG = Log.tag(RateLimitUtil.class);
|
||||
|
||||
private RateLimitUtil() {}
|
||||
|
||||
/**
|
||||
* Forces a retry of all rate limited messages by editing jobs that are in the queue.
|
||||
*/
|
||||
@WorkerThread
|
||||
public static void retryAllRateLimitedMessages(@NonNull Context context) {
|
||||
Set<Long> sms = DatabaseFactory.getSmsDatabase(context).getAllRateLimitedMessageIds();
|
||||
Set<Long> mms = DatabaseFactory.getMmsDatabase(context).getAllRateLimitedMessageIds();
|
||||
|
||||
if (sms.isEmpty() && mms.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log.i(TAG, "Retrying " + sms.size() + " sms records and " + mms.size() + " mms records.");
|
||||
|
||||
DatabaseFactory.getSmsDatabase(context).clearRateLimitStatus(sms);
|
||||
DatabaseFactory.getMmsDatabase(context).clearRateLimitStatus(mms);
|
||||
|
||||
ApplicationDependencies.getJobManager().update((job, serializer) -> {
|
||||
Data data = serializer.deserialize(job.getSerializedData());
|
||||
|
||||
if (job.getFactoryKey().equals(PushTextSendJob.KEY) && sms.contains(PushTextSendJob.getMessageId(data))) {
|
||||
return job.withNextRunAttemptTime(System.currentTimeMillis());
|
||||
} else if (job.getFactoryKey().equals(PushMediaSendJob.KEY) && mms.contains(PushMediaSendJob.getMessageId(data))) {
|
||||
return job.withNextRunAttemptTime(System.currentTimeMillis());
|
||||
} else if (job.getFactoryKey().equals(PushGroupSendJob.KEY) && mms.contains(PushGroupSendJob.getMessageId(data))) {
|
||||
return job.withNextRunAttemptTime(System.currentTimeMillis());
|
||||
} else {
|
||||
return job;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package org.thoughtcrime.securesms.ratelimit;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.view.MenuItem;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.BuildConfig;
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Asks the user to solve a reCAPTCHA. If successful, triggers resends of all relevant message jobs.
|
||||
*/
|
||||
public class RecaptchaProofActivity extends PassphraseRequiredActivity {
|
||||
private static final String TAG = Log.tag(RecaptchaProofActivity.class);
|
||||
|
||||
private static final String RECAPTCHA_SCHEME = "signalcaptcha://";
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
protected void onCreate(Bundle savedInstanceState, boolean ready) {
|
||||
super.onCreate(savedInstanceState, ready);
|
||||
|
||||
setContentView(R.layout.recaptcha_activity);
|
||||
|
||||
requireSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
requireSupportActionBar().setTitle(R.string.RecaptchaProofActivity_complete_verification);
|
||||
|
||||
WebView webView = findViewById(R.id.recaptcha_webview);
|
||||
webView.getSettings().setJavaScriptEnabled(true);
|
||||
webView.clearCache(true);
|
||||
webView.setBackgroundColor(Color.TRANSPARENT);
|
||||
webView.setWebViewClient(new WebViewClient() {
|
||||
@Override
|
||||
public boolean shouldOverrideUrlLoading(WebView view, String url) {
|
||||
if (url != null && url.startsWith(RECAPTCHA_SCHEME)) {
|
||||
handleToken(url.substring(RECAPTCHA_SCHEME.length()));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
webView.loadUrl(BuildConfig.RECAPTCHA_PROOF_URL);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
dynamicTheme.onResume(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == android.R.id.home) {
|
||||
onBackPressed();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
private void handleToken(@NonNull String token) {
|
||||
SimpleProgressDialog.DismissibleDialog dialog = SimpleProgressDialog.showDelayed(this, 1000, 500);
|
||||
SimpleTask.run(() -> {
|
||||
String challenge = SignalStore.rateLimit().getChallenge();
|
||||
if (Util.isEmpty(challenge)) {
|
||||
Log.w(TAG, "No challenge available?");
|
||||
return new TokenResult(true, false);
|
||||
}
|
||||
|
||||
try {
|
||||
for (int i = 0; i < 3; i++) {
|
||||
try {
|
||||
ApplicationDependencies.getSignalServiceAccountManager().submitRateLimitRecaptchaChallenge(challenge, token);
|
||||
RateLimitUtil.retryAllRateLimitedMessages(this);
|
||||
Log.i(TAG, "Successfully completed reCAPTCHA.");
|
||||
return new TokenResult(true, true);
|
||||
} catch (PushNetworkException e) {
|
||||
Log.w(TAG, "Network error during submission. Retrying.", e);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Terminal failure during submission. Will clear state. May get a 428 later.", e);
|
||||
return new TokenResult(true, false);
|
||||
}
|
||||
|
||||
return new TokenResult(false, false);
|
||||
}, result -> {
|
||||
dialog.dismiss();
|
||||
|
||||
if (result.clearState) {
|
||||
Log.i(TAG, "Considering the response sufficient to clear the slate.");
|
||||
SignalStore.rateLimit().onProofAccepted();
|
||||
}
|
||||
|
||||
if (!result.success) {
|
||||
Log.w(TAG, "Response was not a true success.");
|
||||
Toast.makeText(this, R.string.RecaptchaProofActivity_failed_to_submit, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
finish();
|
||||
});
|
||||
}
|
||||
|
||||
private static final class TokenResult {
|
||||
final boolean clearState;
|
||||
final boolean success;
|
||||
|
||||
private TokenResult(boolean clearState, boolean success) {
|
||||
this.clearState = clearState;
|
||||
this.success = success;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package org.thoughtcrime.securesms.ratelimit;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.BottomSheetUtil;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
|
||||
/**
|
||||
* A bottom sheet to be shown when we need to prompt the user to fill out a reCAPTCHA.
|
||||
*/
|
||||
public final class RecaptchaProofBottomSheetFragment extends BottomSheetDialogFragment {
|
||||
|
||||
private static final String TAG = Log.tag(RecaptchaProofBottomSheetFragment.class);
|
||||
|
||||
public static void show(@NonNull FragmentManager manager) {
|
||||
new RecaptchaProofBottomSheetFragment().show(manager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
setStyle(DialogFragment.STYLE_NORMAL, R.style.Signal_DayNight_BottomSheet_Rounded);
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
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 -> {
|
||||
dismissAllowingStateLoss();
|
||||
startActivity(RecaptchaProofActivity.getIntent(requireContext()));
|
||||
});
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void show(@NonNull FragmentManager manager, @Nullable String tag) {
|
||||
if (manager.findFragmentByTag(tag) == null) {
|
||||
BottomSheetUtil.show(manager, tag, this);
|
||||
} else {
|
||||
Log.i(TAG, "Ignoring repeat show.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package org.thoughtcrime.securesms.ratelimit;
|
||||
|
||||
public final class RecaptchaRequiredEvent {
|
||||
}
|
||||
Reference in New Issue
Block a user