Get captcha clients from spam-filter module

This commit is contained in:
Ameya Lokare
2024-10-15 08:39:44 -07:00
parent cacd4afbbb
commit dbb9a8dcf6
16 changed files with 40 additions and 579 deletions

View File

@@ -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<Arguments> 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<Object> 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<DynamicConfiguration> 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<DynamicConfiguration> m = mock(
DynamicConfigurationManager.class);
final DynamicConfiguration d = mock(DynamicConfiguration.class);
when(m.getConfiguration()).thenReturn(d);
when(d.getCaptchaConfiguration()).thenReturn(config);
return m;
}
}

View File

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

View File

@@ -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<DynamicConfiguration> 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<DynamicConfiguration> dynamicConfigurationManager) {
super(null, retryExecutor, httpExecutor, circuitBreakerConfiguration, null, dynamicConfigurationManager);
}
@Override
public String scheme() {
return "test";
}
@Override
public Set<String> 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();
}
}
}