Add a captcha short-code expander
This commit is contained in:
parent
1dde612855
commit
3ac7aba6b2
|
@ -243,6 +243,9 @@ recaptcha:
|
|||
hCaptcha:
|
||||
apiKey: secret://hCaptcha.apiKey
|
||||
|
||||
shortCode:
|
||||
baseUrl: https://example.com/shortcodes/
|
||||
|
||||
storageService:
|
||||
uri: storage.example.com
|
||||
userAuthenticationTokenSharedSecret: secret://storageService.userAuthenticationTokenSharedSecret
|
||||
|
|
|
@ -50,6 +50,7 @@ import org.whispersystems.textsecuregcm.configuration.ReportMessageConfiguration
|
|||
import org.whispersystems.textsecuregcm.configuration.SecureBackupServiceConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.SecureStorageServiceConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.SecureValueRecovery2Configuration;
|
||||
import org.whispersystems.textsecuregcm.configuration.ShortCodeExpanderConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.SpamFilterConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.StripeConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.SubscriptionConfiguration;
|
||||
|
@ -202,6 +203,11 @@ public class WhisperServerConfiguration extends Configuration {
|
|||
@JsonProperty
|
||||
private HCaptchaConfiguration hCaptcha;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private ShortCodeExpanderConfiguration shortCode;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
|
@ -334,6 +340,10 @@ public class WhisperServerConfiguration extends Configuration {
|
|||
return hCaptcha;
|
||||
}
|
||||
|
||||
public ShortCodeExpanderConfiguration getShortCodeRetrieverConfiguration() {
|
||||
return shortCode;
|
||||
}
|
||||
|
||||
public WebSocketConfiguration getWebSocketConfiguration() {
|
||||
return webSocket;
|
||||
}
|
||||
|
|
|
@ -83,6 +83,7 @@ import org.whispersystems.textsecuregcm.captcha.CaptchaChecker;
|
|||
import org.whispersystems.textsecuregcm.captcha.HCaptchaClient;
|
||||
import org.whispersystems.textsecuregcm.captcha.RecaptchaClient;
|
||||
import org.whispersystems.textsecuregcm.captcha.RegistrationCaptchaManager;
|
||||
import org.whispersystems.textsecuregcm.captcha.ShortCodeExpander;
|
||||
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.secrets.SecretStore;
|
||||
import org.whispersystems.textsecuregcm.configuration.secrets.SecretsModule;
|
||||
|
@ -581,7 +582,10 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
|||
.connectTimeout(Duration.ofSeconds(10)).build();
|
||||
HCaptchaClient hCaptchaClient = new HCaptchaClient(config.getHCaptchaConfiguration().apiKey().value(), hcaptchaHttpClient,
|
||||
dynamicConfigurationManager);
|
||||
CaptchaChecker captchaChecker = new CaptchaChecker(List.of(recaptchaClient, hCaptchaClient));
|
||||
HttpClient shortCodeRetrieverHttpClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2)
|
||||
.connectTimeout(Duration.ofSeconds(10)).build();
|
||||
ShortCodeExpander shortCodeRetriever = new ShortCodeExpander(shortCodeRetrieverHttpClient, config.getShortCodeRetrieverConfiguration().baseUrl());
|
||||
CaptchaChecker captchaChecker = new CaptchaChecker(shortCodeRetriever, List.of(recaptchaClient, hCaptchaClient));
|
||||
|
||||
PushChallengeManager pushChallengeManager = new PushChallengeManager(pushNotificationManager,
|
||||
pushChallengeDynamoDb);
|
||||
|
|
|
@ -29,9 +29,15 @@ public class CaptchaChecker {
|
|||
@VisibleForTesting
|
||||
static final String SEPARATOR = ".";
|
||||
|
||||
private static final String SHORT_SUFFIX = "-short";
|
||||
|
||||
private final ShortCodeExpander shortCodeExpander;
|
||||
private final Map<String, CaptchaClient> captchaClientMap;
|
||||
|
||||
public CaptchaChecker(final List<CaptchaClient> captchaClients) {
|
||||
public CaptchaChecker(
|
||||
final ShortCodeExpander shortCodeRetriever,
|
||||
final List<CaptchaClient> captchaClients) {
|
||||
this.shortCodeExpander = shortCodeRetriever;
|
||||
this.captchaClientMap = captchaClients.stream()
|
||||
.collect(Collectors.toMap(CaptchaClient::scheme, Function.identity()));
|
||||
}
|
||||
|
@ -63,9 +69,17 @@ public class CaptchaChecker {
|
|||
final String prefix = parts[0];
|
||||
final String siteKey = parts[1].toLowerCase(Locale.ROOT).strip();
|
||||
final String action = parts[2];
|
||||
final String token = parts[3];
|
||||
String token = parts[3];
|
||||
|
||||
final CaptchaClient client = this.captchaClientMap.get(prefix);
|
||||
String provider = prefix;
|
||||
if (prefix.endsWith(SHORT_SUFFIX)) {
|
||||
// This is a "short" solution that points to the actual solution. We need to fetch the
|
||||
// full solution before proceeding
|
||||
provider = prefix.substring(0, prefix.length() - SHORT_SUFFIX.length());
|
||||
token = shortCodeExpander.retrieve(token).orElseThrow(() -> new BadRequestException("invalid shortcode"));
|
||||
}
|
||||
|
||||
final CaptchaClient client = this.captchaClientMap.get(provider);
|
||||
if (client == null) {
|
||||
throw new BadRequestException("invalid captcha scheme");
|
||||
}
|
||||
|
@ -92,7 +106,7 @@ public class CaptchaChecker {
|
|||
Metrics.counter(ASSESSMENTS_COUNTER_NAME,
|
||||
"action", action,
|
||||
"score", result.getScoreString(),
|
||||
"provider", prefix)
|
||||
"provider", provider)
|
||||
.increment();
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.captcha;
|
||||
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import org.apache.http.HttpStatus;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
|
||||
|
||||
public class ShortCodeExpander {
|
||||
private static final String EXPAND_COUNTER_NAME = name(ShortCodeExpander.class, "expand");
|
||||
|
||||
private final HttpClient client;
|
||||
private final URI shortenerHost;
|
||||
|
||||
public ShortCodeExpander(final HttpClient client, final String shortenerHost) {
|
||||
this.client = client;
|
||||
this.shortenerHost = URI.create(shortenerHost);
|
||||
}
|
||||
|
||||
public Optional<String> retrieve(final String shortCode) throws IOException {
|
||||
final URI uri = shortenerHost.resolve("/" + shortCode);
|
||||
final HttpRequest request = HttpRequest.newBuilder().uri(uri).GET().build();
|
||||
|
||||
try {
|
||||
final HttpResponse<String> response = this.client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
Metrics.counter(EXPAND_COUNTER_NAME, "responseCode", Integer.toString(response.statusCode())).increment();
|
||||
return switch (response.statusCode()) {
|
||||
case HttpStatus.SC_OK -> Optional.of(response.body());
|
||||
case HttpStatus.SC_NOT_FOUND -> Optional.empty();
|
||||
default -> throw new IOException("Failed to look up shortcode");
|
||||
};
|
||||
} catch (InterruptedException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
public record ShortCodeExpanderConfiguration(String baseUrl) {
|
||||
}
|
|
@ -18,6 +18,7 @@ import static org.whispersystems.textsecuregcm.captcha.CaptchaChecker.SEPARATOR;
|
|||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
import javax.ws.rs.BadRequestException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
@ -77,7 +78,7 @@ public class CaptchaCheckerTest {
|
|||
final String siteKey,
|
||||
final Action expectedAction) throws IOException {
|
||||
final CaptchaClient captchaClient = mockClient(PREFIX);
|
||||
new CaptchaChecker(List.of(captchaClient)).verify(expectedAction, input, null);
|
||||
new CaptchaChecker(null, List.of(captchaClient)).verify(expectedAction, input, null);
|
||||
verify(captchaClient, times(1)).verify(eq(siteKey), eq(expectedAction), eq(expectedToken), any());
|
||||
}
|
||||
|
||||
|
@ -105,10 +106,10 @@ public class CaptchaCheckerTest {
|
|||
final CaptchaClient a = mockClient(PREFIX_A);
|
||||
final CaptchaClient b = mockClient(PREFIX_B);
|
||||
|
||||
new CaptchaChecker(List.of(a, b)).verify(Action.CHALLENGE, ainput, null);
|
||||
new CaptchaChecker(null, List.of(a, b)).verify(Action.CHALLENGE, ainput, null);
|
||||
verify(a, times(1)).verify(any(), any(), any(), any());
|
||||
|
||||
new CaptchaChecker(List.of(a, b)).verify(Action.CHALLENGE, binput, null);
|
||||
new CaptchaChecker(null, List.of(a, b)).verify(Action.CHALLENGE, binput, null);
|
||||
verify(b, times(1)).verify(any(), any(), any(), any());
|
||||
}
|
||||
|
||||
|
@ -130,7 +131,18 @@ public class CaptchaCheckerTest {
|
|||
public void badArgs(final String input) throws IOException {
|
||||
final CaptchaClient cc = mockClient(PREFIX);
|
||||
assertThrows(BadRequestException.class,
|
||||
() -> new CaptchaChecker(List.of(cc)).verify(Action.CHALLENGE, input, null));
|
||||
() -> new CaptchaChecker(null, List.of(cc)).verify(Action.CHALLENGE, input, null));
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShortened() throws IOException {
|
||||
final CaptchaClient captchaClient = mockClient(PREFIX);
|
||||
final ShortCodeExpander retriever = mock(ShortCodeExpander.class);
|
||||
when(retriever.retrieve("abc")).thenReturn(Optional.of(TOKEN));
|
||||
final String input = String.join(SEPARATOR, PREFIX + "-short", REG_SITE_KEY, "registration", "abc");
|
||||
new CaptchaChecker(retriever, List.of(captchaClient)).verify(Action.REGISTRATION, input, null);
|
||||
verify(captchaClient, times(1)).verify(eq(REG_SITE_KEY), eq(Action.REGISTRATION), eq(TOKEN), any());
|
||||
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue