Handle 428 rate limiting.

This commit is contained in:
Greyson Parrelli
2021-05-05 12:49:18 -04:00
parent 02d060ca0a
commit 31e1c6f7aa
60 changed files with 1235 additions and 57 deletions

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,4 @@
package org.thoughtcrime.securesms.ratelimit;
public final class RecaptchaRequiredEvent {
}