From dbb9a8dcf62c6c32a4ed8e84137edd2cf3dbe65e Mon Sep 17 00:00:00 2001 From: Ameya Lokare Date: Tue, 15 Oct 2024 08:39:44 -0700 Subject: [PATCH] Get captcha clients from spam-filter module --- service/config/sample-secrets-bundle.yml | 2 - service/config/sample.yml | 3 - .../WhisperServerConfiguration.java | 10 -- .../textsecuregcm/WhisperServerService.java | 34 ++-- .../textsecuregcm/captcha/CaptchaClient.java | 21 +++ .../textsecuregcm/captcha/HCaptchaClient.java | 167 ------------------ .../captcha/HCaptchaResponse.java | 52 ------ .../configuration/HCaptchaClientFactory.java | 21 --- .../configuration/HCaptchaConfiguration.java | 62 ------- .../textsecuregcm/spam/SpamFilter.java | 5 + .../io.dropwizard.jackson.Discoverable | 1 - .../captcha/HCaptchaClientTest.java | 140 --------------- .../captcha/HCaptchaResponseTest.java | 39 ---- .../StubHCaptchaClientFactory.java | 58 ------ ...uregcm.configuration.HCaptchaClientFactory | 1 - service/src/test/resources/config/test.yml | 3 - 16 files changed, 40 insertions(+), 579 deletions(-) delete mode 100644 service/src/main/java/org/whispersystems/textsecuregcm/captcha/HCaptchaClient.java delete mode 100644 service/src/main/java/org/whispersystems/textsecuregcm/captcha/HCaptchaResponse.java delete mode 100644 service/src/main/java/org/whispersystems/textsecuregcm/configuration/HCaptchaClientFactory.java delete mode 100644 service/src/main/java/org/whispersystems/textsecuregcm/configuration/HCaptchaConfiguration.java delete mode 100644 service/src/test/java/org/whispersystems/textsecuregcm/captcha/HCaptchaClientTest.java delete mode 100644 service/src/test/java/org/whispersystems/textsecuregcm/captcha/HCaptchaResponseTest.java delete mode 100644 service/src/test/java/org/whispersystems/textsecuregcm/configuration/StubHCaptchaClientFactory.java delete mode 100644 service/src/test/resources/META-INF/services/org.whispersystems.textsecuregcm.configuration.HCaptchaClientFactory diff --git a/service/config/sample-secrets-bundle.yml b/service/config/sample-secrets-bundle.yml index 2fd2c506c..ae06ed7d8 100644 --- a/service/config/sample-secrets-bundle.yml +++ b/service/config/sample-secrets-bundle.yml @@ -72,8 +72,6 @@ cdn3StorageManager.clientSecret: test unidentifiedDelivery.certificate: ABCD1234 unidentifiedDelivery.privateKey: ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789AAAAAAA -hCaptcha.apiKey: unset - storageService.userAuthenticationTokenSharedSecret: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= zkConfig-libsignal-0.42.serverSecret: ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdef diff --git a/service/config/sample.yml b/service/config/sample.yml index 074e11198..42dd4bb6a 100644 --- a/service/config/sample.yml +++ b/service/config/sample.yml @@ -284,9 +284,6 @@ unidentifiedDelivery: privateKey: secret://unidentifiedDelivery.privateKey expiresDays: 7 -hCaptcha: - apiKey: secret://hCaptcha.apiKey - shortCode: baseUrl: https://example.com/shortcodes/ diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java index 59fd9933b..e4b4ef1eb 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java @@ -37,7 +37,6 @@ import org.whispersystems.textsecuregcm.configuration.FcmConfiguration; import org.whispersystems.textsecuregcm.configuration.GcpAttachmentsConfiguration; import org.whispersystems.textsecuregcm.configuration.GenericZkConfig; import org.whispersystems.textsecuregcm.configuration.GooglePlayBillingConfiguration; -import org.whispersystems.textsecuregcm.configuration.HCaptchaClientFactory; import org.whispersystems.textsecuregcm.configuration.KeyTransparencyServiceConfiguration; import org.whispersystems.textsecuregcm.configuration.LinkDeviceSecretConfiguration; import org.whispersystems.textsecuregcm.configuration.MaxDeviceConfiguration; @@ -202,11 +201,6 @@ public class WhisperServerConfiguration extends Configuration { @JsonProperty private UnidentifiedDeliveryConfiguration unidentifiedDelivery; - @Valid - @NotNull - @JsonProperty - private HCaptchaClientFactory hCaptcha; - @Valid @NotNull @JsonProperty @@ -379,10 +373,6 @@ public class WhisperServerConfiguration extends Configuration { return dynamoDbTables; } - public HCaptchaClientFactory getHCaptchaConfiguration() { - return hCaptcha; - } - public ShortCodeExpanderConfiguration getShortCodeRetrieverConfiguration() { return shortCode; } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java index 09f2842ac..dd436b43f 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java @@ -100,7 +100,7 @@ import org.whispersystems.textsecuregcm.calls.routing.CallRoutingTableManager; import org.whispersystems.textsecuregcm.calls.routing.DynamicConfigTurnRouter; import org.whispersystems.textsecuregcm.calls.routing.TurnCallRouter; import org.whispersystems.textsecuregcm.captcha.CaptchaChecker; -import org.whispersystems.textsecuregcm.captcha.HCaptchaClient; +import org.whispersystems.textsecuregcm.captcha.CaptchaClient; import org.whispersystems.textsecuregcm.captcha.RegistrationCaptchaManager; import org.whispersystems.textsecuregcm.captcha.ShortCodeExpander; import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; @@ -499,8 +499,6 @@ public class WhisperServerService extends Application()) - .keepAliveTime(io.dropwizard.util.Duration.seconds(60L)) - .build(); - // unbounded executor (same as cachedThreadPool) ExecutorService remoteStorageHttpExecutor = environment.lifecycle() .executorService(name(getClass(), "remoteStorage-%d")) .minThreads(0) @@ -706,13 +696,6 @@ public class WhisperServerService extends Application captchaClients = spamFilter + .map(SpamFilter::getCaptchaClients) + .orElseGet(() -> { + log.warn("No captcha clients found; using default (no-op) client as default"); + return List.of(CaptchaClient.noop()); + }); spamFilter.map(SpamFilter::getReportedMessageListener).ifPresent(reportMessageManager::addListener); + final HttpClient shortCodeRetrieverHttpClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2) + .connectTimeout(Duration.ofSeconds(10)).build(); + final ShortCodeExpander shortCodeRetriever = new ShortCodeExpander(shortCodeRetrieverHttpClient, config.getShortCodeRetrieverConfiguration().baseUrl()); + final CaptchaChecker captchaChecker = new CaptchaChecker(shortCodeRetriever, captchaClients); + + final RegistrationCaptchaManager registrationCaptchaManager = new RegistrationCaptchaManager(captchaChecker); + final RateLimitChallengeManager rateLimitChallengeManager = new RateLimitChallengeManager(pushChallengeManager, captchaChecker, rateLimiters, spamFilter.map(SpamFilter::getRateLimitChallengeListener).stream().toList()); diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/captcha/CaptchaClient.java b/service/src/main/java/org/whispersystems/textsecuregcm/captcha/CaptchaClient.java index bc2da6e3c..0676011cc 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/captcha/CaptchaClient.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/captcha/CaptchaClient.java @@ -6,6 +6,7 @@ package org.whispersystems.textsecuregcm.captcha; import java.io.IOException; +import java.util.Collections; import java.util.Optional; import java.util.Set; @@ -40,4 +41,24 @@ public interface CaptchaClient { final String token, final String ip, final String userAgent) throws IOException; + + static CaptchaClient noop() { + return new CaptchaClient() { + @Override + public String scheme() { + return "noop"; + } + + @Override + public Set validSiteKeys(final Action action) { + return Set.of("noop"); + } + + @Override + public AssessmentResult verify(final String siteKey, final Action action, final String token, final String ip, + final String userAgent) throws IOException { + return AssessmentResult.alwaysValid(); + } + }; + } } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/captcha/HCaptchaClient.java b/service/src/main/java/org/whispersystems/textsecuregcm/captcha/HCaptchaClient.java deleted file mode 100644 index 6c40977d9..000000000 --- a/service/src/main/java/org/whispersystems/textsecuregcm/captcha/HCaptchaClient.java +++ /dev/null @@ -1,167 +0,0 @@ -/* - * Copyright 2021 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.whispersystems.textsecuregcm.captcha; - -import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name; - -import com.google.common.annotations.VisibleForTesting; -import io.micrometer.core.instrument.Metrics; -import java.io.IOException; -import java.math.BigDecimal; -import java.net.URI; -import java.net.URLEncoder; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.nio.charset.StandardCharsets; -import java.time.Duration; -import java.util.Collections; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.CompletionException; -import java.util.concurrent.Executor; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import javax.ws.rs.core.Response; -import io.micrometer.core.instrument.Tag; -import io.micrometer.core.instrument.Tags; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.whispersystems.textsecuregcm.configuration.CircuitBreakerConfiguration; -import org.whispersystems.textsecuregcm.configuration.RetryConfiguration; -import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicCaptchaConfiguration; -import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; -import org.whispersystems.textsecuregcm.http.FaultTolerantHttpClient; -import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil; -import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; -import org.whispersystems.textsecuregcm.util.ExceptionUtils; -import org.whispersystems.textsecuregcm.util.SystemMapper; - -public class HCaptchaClient implements CaptchaClient { - - private static final Logger logger = LoggerFactory.getLogger(HCaptchaClient.class); - private static final String PREFIX = "signal-hcaptcha"; - private static final String ASSESSMENT_REASON_COUNTER_NAME = name(HCaptchaClient.class, "assessmentReason"); - private static final String INVALID_REASON_COUNTER_NAME = name(HCaptchaClient.class, "invalidReason"); - private final String apiKey; - private final FaultTolerantHttpClient client; - private final DynamicConfigurationManager dynamicConfigurationManager; - - @VisibleForTesting - HCaptchaClient(final String apiKey, - final FaultTolerantHttpClient faultTolerantHttpClient, - final DynamicConfigurationManager dynamicConfigurationManager) { - this.apiKey = apiKey; - this.client = faultTolerantHttpClient; - this.dynamicConfigurationManager = dynamicConfigurationManager; - } - - public HCaptchaClient( - final String apiKey, - final ScheduledExecutorService retryExecutor, - final ExecutorService httpExecutor, - final CircuitBreakerConfiguration circuitBreakerConfiguration, - final RetryConfiguration retryConfiguration, - final DynamicConfigurationManager dynamicConfigurationManager) { - this(apiKey, - FaultTolerantHttpClient.newBuilder() - .withName("hcaptcha") - .withCircuitBreaker(circuitBreakerConfiguration) - .withExecutor(httpExecutor) - .withRetryExecutor(retryExecutor) - .withRetry(retryConfiguration) - .withRetryOnException(ex -> ex instanceof IOException) - .withConnectTimeout(Duration.ofSeconds(10)) - .withVersion(HttpClient.Version.HTTP_2) - .build(), - dynamicConfigurationManager); - } - - @Override - public String scheme() { - return PREFIX; - } - - @Override - public Set validSiteKeys(final Action action) { - final DynamicCaptchaConfiguration config = dynamicConfigurationManager.getConfiguration().getCaptchaConfiguration(); - if (!config.isAllowHCaptcha()) { - logger.warn("Received request to verify an hCaptcha, but hCaptcha is not enabled"); - return Collections.emptySet(); - } - return Optional - .ofNullable(config.getHCaptchaSiteKeys().get(action)) - .orElse(Collections.emptySet()); - } - - @Override - public AssessmentResult verify( - final String siteKey, - final Action action, - final String token, - final String ip, - final String userAgent) - throws IOException { - - final DynamicCaptchaConfiguration config = dynamicConfigurationManager.getConfiguration().getCaptchaConfiguration(); - final String body = String.format("response=%s&secret=%s&remoteip=%s", - URLEncoder.encode(token, StandardCharsets.UTF_8), - URLEncoder.encode(this.apiKey, StandardCharsets.UTF_8), - ip); - final HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create("https://hcaptcha.com/siteverify")) - .header("Content-Type", "application/x-www-form-urlencoded") - .POST(HttpRequest.BodyPublishers.ofString(body)) - .build(); - - final HttpResponse response; - try { - response = this.client.sendAsync(request, HttpResponse.BodyHandlers.ofString()).join(); - } catch (CompletionException e) { - logger.warn("failed to make http request to hCaptcha: {}", e.getMessage()); - throw new IOException(ExceptionUtils.unwrap(e)); - } - - if (response.statusCode() != Response.Status.OK.getStatusCode()) { - logger.warn("failure submitting token to hCaptcha (code={}): {}", response.statusCode(), response); - throw new IOException("hCaptcha http failure : " + response.statusCode()); - } - - final HCaptchaResponse hCaptchaResponse = SystemMapper.jsonMapper() - .readValue(response.body(), HCaptchaResponse.class); - - logger.debug("received hCaptcha response: {}", hCaptchaResponse); - - if (!hCaptchaResponse.success) { - for (String errorCode : hCaptchaResponse.errorCodes) { - Metrics.counter(INVALID_REASON_COUNTER_NAME, Tags.of( - Tag.of("action", action.getActionName()), - Tag.of("reason", errorCode), - UserAgentTagUtil.getPlatformTag(userAgent) - )).increment(); - } - return AssessmentResult.invalid(); - } - - // hcaptcha uses the inverse scheme of recaptcha (for hcaptcha, a low score is less risky) - final float score = 1.0f - hCaptchaResponse.score; - if (score < 0.0f || score > 1.0f) { - logger.error("Invalid score {} from hcaptcha response {}", hCaptchaResponse.score, hCaptchaResponse); - return AssessmentResult.invalid(); - } - final BigDecimal threshold = config.getScoreFloorByAction().getOrDefault(action, config.getScoreFloor()); - final AssessmentResult assessmentResult = AssessmentResult.fromScore(score, threshold.floatValue()); - - for (String reason : hCaptchaResponse.scoreReasons) { - Metrics.counter(ASSESSMENT_REASON_COUNTER_NAME, - "action", action.getActionName(), - "reason", reason, - "score", assessmentResult.getScoreString()).increment(); - } - return assessmentResult; - } -} diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/captcha/HCaptchaResponse.java b/service/src/main/java/org/whispersystems/textsecuregcm/captcha/HCaptchaResponse.java deleted file mode 100644 index d0d35dc28..000000000 --- a/service/src/main/java/org/whispersystems/textsecuregcm/captcha/HCaptchaResponse.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2022 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.whispersystems.textsecuregcm.captcha; - -import com.fasterxml.jackson.annotation.JsonProperty; -import java.time.Instant; -import java.util.Collections; -import java.util.List; - -/** - * Verify response returned by hcaptcha - *

- * see ... - */ -public class HCaptchaResponse { - - @JsonProperty - boolean success; - - @JsonProperty(value = "challenge_ts") - Instant challengeTs; - - @JsonProperty - String hostname; - - @JsonProperty(value = "error-codes") - List errorCodes = Collections.emptyList(); - - @JsonProperty - float score; - - @JsonProperty(value = "score_reason") - List scoreReasons = Collections.emptyList(); - - public HCaptchaResponse() { - } - - @Override - public String toString() { - return "HCaptchaResponse{" + - "success=" + success + - ", challengeTs=" + challengeTs + - ", hostname='" + hostname + '\'' + - ", errorCodes=" + errorCodes + - ", score=" + score + - ", scoreReasons=" + scoreReasons + - '}'; - } -} diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/configuration/HCaptchaClientFactory.java b/service/src/main/java/org/whispersystems/textsecuregcm/configuration/HCaptchaClientFactory.java deleted file mode 100644 index 5029e9cdc..000000000 --- a/service/src/main/java/org/whispersystems/textsecuregcm/configuration/HCaptchaClientFactory.java +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright 2024 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.whispersystems.textsecuregcm.configuration; - -import com.fasterxml.jackson.annotation.JsonTypeInfo; -import io.dropwizard.jackson.Discoverable; -import org.whispersystems.textsecuregcm.captcha.HCaptchaClient; -import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; -import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.ScheduledExecutorService; - -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", defaultImpl = HCaptchaConfiguration.class) -public interface HCaptchaClientFactory extends Discoverable { - - HCaptchaClient build(ScheduledExecutorService retryExecutor, ExecutorService httpExecutor, - DynamicConfigurationManager dynamicConfigurationManager); -} diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/configuration/HCaptchaConfiguration.java b/service/src/main/java/org/whispersystems/textsecuregcm/configuration/HCaptchaConfiguration.java deleted file mode 100644 index 31a311a7c..000000000 --- a/service/src/main/java/org/whispersystems/textsecuregcm/configuration/HCaptchaConfiguration.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2021 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.whispersystems.textsecuregcm.configuration; - -import com.fasterxml.jackson.annotation.JsonProperty; -import javax.validation.Valid; -import javax.validation.constraints.NotNull; -import com.fasterxml.jackson.annotation.JsonTypeName; -import org.whispersystems.textsecuregcm.captcha.HCaptchaClient; -import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; -import org.whispersystems.textsecuregcm.configuration.secrets.SecretString; -import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.ScheduledExecutorService; - -@JsonTypeName("default") -public class HCaptchaConfiguration implements HCaptchaClientFactory { - - @JsonProperty - @NotNull - SecretString apiKey; - - @JsonProperty - @NotNull - @Valid - CircuitBreakerConfiguration circuitBreaker = new CircuitBreakerConfiguration(); - - @JsonProperty - @NotNull - @Valid - RetryConfiguration retry = new RetryConfiguration(); - - - public SecretString getApiKey() { - return apiKey; - } - - public CircuitBreakerConfiguration getCircuitBreaker() { - return circuitBreaker; - } - - public RetryConfiguration getRetry() { - return retry; - } - - @Override - public HCaptchaClient build( - final ScheduledExecutorService retryExecutor, - final ExecutorService httpExecutor, - final DynamicConfigurationManager dynamicConfigurationManager) { - return new HCaptchaClient( - apiKey.value(), - retryExecutor, - httpExecutor, - circuitBreaker, - retry, - dynamicConfigurationManager); - } -} diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/spam/SpamFilter.java b/service/src/main/java/org/whispersystems/textsecuregcm/spam/SpamFilter.java index 6d05858b0..985d04148 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/spam/SpamFilter.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/spam/SpamFilter.java @@ -8,7 +8,10 @@ package org.whispersystems.textsecuregcm.spam; import io.dropwizard.configuration.ConfigurationValidationException; import io.dropwizard.lifecycle.Managed; import java.io.IOException; +import java.util.List; import javax.validation.Validator; +import org.whispersystems.textsecuregcm.captcha.CaptchaChecker; +import org.whispersystems.textsecuregcm.captcha.CaptchaClient; import org.whispersystems.textsecuregcm.storage.ReportedMessageListener; /** @@ -80,4 +83,6 @@ public interface SpamFilter extends Managed { * @return a {@link RegistrationRecoveryChecker} controlled by the spam filter */ RegistrationRecoveryChecker getRegistrationRecoveryChecker(); + + List getCaptchaClients(); } diff --git a/service/src/main/resources/META-INF/services/io.dropwizard.jackson.Discoverable b/service/src/main/resources/META-INF/services/io.dropwizard.jackson.Discoverable index 83a0af492..b264f2bf1 100644 --- a/service/src/main/resources/META-INF/services/io.dropwizard.jackson.Discoverable +++ b/service/src/main/resources/META-INF/services/io.dropwizard.jackson.Discoverable @@ -2,7 +2,6 @@ org.whispersystems.textsecuregcm.configuration.AwsCredentialsProviderFactory org.whispersystems.textsecuregcm.configuration.DatadogConfiguration org.whispersystems.textsecuregcm.configuration.DynamicConfigurationManagerFactory org.whispersystems.textsecuregcm.configuration.DynamoDbClientFactory -org.whispersystems.textsecuregcm.configuration.HCaptchaClientFactory org.whispersystems.textsecuregcm.configuration.FaultTolerantRedisClusterFactory org.whispersystems.textsecuregcm.configuration.FaultTolerantRedisClientFactory org.whispersystems.textsecuregcm.configuration.PaymentsServiceClientsFactory diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/captcha/HCaptchaClientTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/captcha/HCaptchaClientTest.java deleted file mode 100644 index 12410c396..000000000 --- a/service/src/test/java/org/whispersystems/textsecuregcm/captcha/HCaptchaClientTest.java +++ /dev/null @@ -1,140 +0,0 @@ -package org.whispersystems.textsecuregcm.captcha; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.io.IOException; -import java.math.BigDecimal; -import java.net.http.HttpClient; -import java.net.http.HttpResponse; -import java.util.Arrays; -import java.util.Collections; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.CompletableFuture; -import java.util.stream.Stream; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicCaptchaConfiguration; -import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; -import org.whispersystems.textsecuregcm.http.FaultTolerantHttpClient; -import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; - -public class HCaptchaClientTest { - - private static final String SITE_KEY = "site-key"; - private static final String TOKEN = "token"; - private static final String USER_AGENT = "user-agent"; - - - static Stream captchaProcessed() { - return Stream.of( - // hCaptcha scores are inverted compared to recaptcha scores. (low score is good) - Arguments.of(true, 0.4f, 0.5f, true), - Arguments.of(false, 0.4f, 0.5f, false), - Arguments.of(true, 0.6f, 0.5f, false), - Arguments.of(false, 0.6f, 0.5f, false), - Arguments.of(true, 0.6f, 0.4f, true), - Arguments.of(true, 0.61f, 0.4f, false), - Arguments.of(true, 0.7f, 0.3f, true) - ); - } - - @ParameterizedTest - @MethodSource - public void captchaProcessed(final boolean success, final float hCaptchaScore, final float scoreFloor, final boolean expectedResult) - throws IOException, InterruptedException { - - final FaultTolerantHttpClient client = mockResponder(200, String.format(""" - { - "success": %b, - "score": %f, - "score-reasons": ["great job doing this captcha"] - } - """, - success, hCaptchaScore)); - - final AssessmentResult result = new HCaptchaClient("fake", client, mockConfig(true, scoreFloor)) - .verify(SITE_KEY, Action.CHALLENGE, TOKEN, null, USER_AGENT); - if (!success) { - assertThat(result).isEqualTo(AssessmentResult.invalid()); - } else { - assertThat(result.isValid()).isEqualTo(expectedResult); - } - } - - @Test - public void errorResponse() throws IOException, InterruptedException { - final FaultTolerantHttpClient httpClient = mockResponder(503, ""); - final HCaptchaClient client = new HCaptchaClient("fake", httpClient, mockConfig(true, 0.5)); - assertThrows(IOException.class, () -> client.verify(SITE_KEY, Action.CHALLENGE, TOKEN, null, USER_AGENT)); - } - - @Test - public void invalidScore() throws IOException, InterruptedException { - final FaultTolerantHttpClient httpClient = mockResponder(200, """ - {"success" : true, "score": 1.1} - """); - final HCaptchaClient client = new HCaptchaClient("fake", httpClient, mockConfig(true, 0.5)); - assertThat(client.verify(SITE_KEY, Action.CHALLENGE, TOKEN, null, USER_AGENT)).isEqualTo(AssessmentResult.invalid()); - } - - @Test - public void badBody() throws IOException, InterruptedException { - final FaultTolerantHttpClient httpClient = mockResponder(200, """ - {"success" : true, - """); - final HCaptchaClient client = new HCaptchaClient("fake", httpClient, mockConfig(true, 0.5)); - assertThrows(IOException.class, () -> client.verify(SITE_KEY, Action.CHALLENGE, TOKEN, null, USER_AGENT)); - } - - @Test - public void disabled() throws IOException { - final HCaptchaClient hc = new HCaptchaClient("fake", null, mockConfig(false, 0.5)); - assertTrue(Arrays.stream(Action.values()).map(hc::validSiteKeys).allMatch(Set::isEmpty)); - } - - @Test - public void badSiteKey() throws IOException { - final HCaptchaClient hc = new HCaptchaClient("fake", null, mockConfig(true, 0.5)); - for (Action action : Action.values()) { - assertThat(hc.validSiteKeys(action)).contains(SITE_KEY); - assertThat(hc.validSiteKeys(action)).doesNotContain("invalid"); - } - } - - private static FaultTolerantHttpClient mockResponder(final int statusCode, final String jsonBody) { - FaultTolerantHttpClient httpClient = mock(FaultTolerantHttpClient.class); - @SuppressWarnings("unchecked") final HttpResponse httpResponse = mock(HttpResponse.class); - - when(httpResponse.body()).thenReturn(jsonBody); - when(httpResponse.statusCode()).thenReturn(statusCode); - - when(httpClient.sendAsync(any(), any())).thenReturn(CompletableFuture.completedFuture(httpResponse)); - return httpClient; - } - - private static DynamicConfigurationManager mockConfig(boolean enabled, double scoreFloor) { - final DynamicCaptchaConfiguration config = new DynamicCaptchaConfiguration(); - config.setAllowHCaptcha(enabled); - config.setScoreFloor(BigDecimal.valueOf(scoreFloor)); - config.setHCaptchaSiteKeys(Map.of( - Action.REGISTRATION, Collections.singleton(SITE_KEY), - Action.CHALLENGE, Collections.singleton(SITE_KEY) - )); - - @SuppressWarnings("unchecked") final DynamicConfigurationManager m = mock( - DynamicConfigurationManager.class); - final DynamicConfiguration d = mock(DynamicConfiguration.class); - when(m.getConfiguration()).thenReturn(d); - when(d.getCaptchaConfiguration()).thenReturn(config); - return m; - } - -} diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/captcha/HCaptchaResponseTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/captcha/HCaptchaResponseTest.java deleted file mode 100644 index 19810a3d7..000000000 --- a/service/src/test/java/org/whispersystems/textsecuregcm/captcha/HCaptchaResponseTest.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2024 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.whispersystems.textsecuregcm.captcha; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import java.time.Instant; -import java.util.List; -import org.junit.jupiter.api.Test; -import org.whispersystems.textsecuregcm.util.SystemMapper; - -class HCaptchaResponseTest { - - @Test - void testParse() throws Exception { - - final Instant challengeTs = Instant.parse("2024-09-13T21:36:15Z"); - - final HCaptchaResponse response = - SystemMapper.jsonMapper().readValue(""" - { - "success": "true", - "challenge_ts": "2024-09-13T21:36:15.000000Z", - "hostname": "example.com", - "error-codes": ["one", "two"], - "score": 0.5, - "score_reason": ["three", "four"] - } - """, HCaptchaResponse.class); - - assertEquals(challengeTs, response.challengeTs); - assertEquals(List.of("one", "two"), response.errorCodes); - assertEquals(List.of("three", "four"), response.scoreReasons); - } - -} diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/configuration/StubHCaptchaClientFactory.java b/service/src/test/java/org/whispersystems/textsecuregcm/configuration/StubHCaptchaClientFactory.java deleted file mode 100644 index 599fa6d2e..000000000 --- a/service/src/test/java/org/whispersystems/textsecuregcm/configuration/StubHCaptchaClientFactory.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2024 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.whispersystems.textsecuregcm.configuration; - -import com.fasterxml.jackson.annotation.JsonTypeName; -import java.io.IOException; -import java.util.Set; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.ScheduledExecutorService; -import org.whispersystems.textsecuregcm.captcha.Action; -import org.whispersystems.textsecuregcm.captcha.AssessmentResult; -import org.whispersystems.textsecuregcm.captcha.HCaptchaClient; -import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; -import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; - -@JsonTypeName("stub") -public class StubHCaptchaClientFactory implements HCaptchaClientFactory { - - @Override - public HCaptchaClient build(final ScheduledExecutorService retryExecutor, ExecutorService httpExecutor, - final DynamicConfigurationManager dynamicConfigurationManager) { - - return new StubHCaptchaClient(retryExecutor, httpExecutor, new CircuitBreakerConfiguration(), - dynamicConfigurationManager); - } - - /** - * Accepts any token of the format "test.test.*.*" - */ - private static class StubHCaptchaClient extends HCaptchaClient { - - public StubHCaptchaClient(final ScheduledExecutorService retryExecutor, ExecutorService httpExecutor, - final CircuitBreakerConfiguration circuitBreakerConfiguration, - final DynamicConfigurationManager dynamicConfigurationManager) { - super(null, retryExecutor, httpExecutor, circuitBreakerConfiguration, null, dynamicConfigurationManager); - } - - @Override - public String scheme() { - return "test"; - } - - @Override - public Set validSiteKeys(final Action action) { - return Set.of("test"); - } - - @Override - public AssessmentResult verify(final String siteKey, final Action action, final String token, final String ip, - final String userAgent) - throws IOException { - return AssessmentResult.alwaysValid(); - } - } -} diff --git a/service/src/test/resources/META-INF/services/org.whispersystems.textsecuregcm.configuration.HCaptchaClientFactory b/service/src/test/resources/META-INF/services/org.whispersystems.textsecuregcm.configuration.HCaptchaClientFactory deleted file mode 100644 index 17d4ad3d3..000000000 --- a/service/src/test/resources/META-INF/services/org.whispersystems.textsecuregcm.configuration.HCaptchaClientFactory +++ /dev/null @@ -1 +0,0 @@ -org.whispersystems.textsecuregcm.configuration.StubHCaptchaClientFactory diff --git a/service/src/test/resources/config/test.yml b/service/src/test/resources/config/test.yml index 2f00fe1d3..ed2e475f1 100644 --- a/service/src/test/resources/config/test.yml +++ b/service/src/test/resources/config/test.yml @@ -281,9 +281,6 @@ unidentifiedDelivery: privateKey: secret://unidentifiedDelivery.privateKey expiresDays: 7 -hCaptcha: - type: stub - shortCode: baseUrl: https://example.com/shortcodes/