mirror of
https://github.com/signalapp/Signal-Server
synced 2026-04-21 09:58:06 +01:00
Get captcha clients from spam-filter module
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user