Get captcha clients from spam-filter module
This commit is contained in:
parent
cacd4afbbb
commit
dbb9a8dcf6
|
@ -72,8 +72,6 @@ cdn3StorageManager.clientSecret: test
|
||||||
unidentifiedDelivery.certificate: ABCD1234
|
unidentifiedDelivery.certificate: ABCD1234
|
||||||
unidentifiedDelivery.privateKey: ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789AAAAAAA
|
unidentifiedDelivery.privateKey: ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789AAAAAAA
|
||||||
|
|
||||||
hCaptcha.apiKey: unset
|
|
||||||
|
|
||||||
storageService.userAuthenticationTokenSharedSecret: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
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
|
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
|
||||||
|
|
|
@ -284,9 +284,6 @@ unidentifiedDelivery:
|
||||||
privateKey: secret://unidentifiedDelivery.privateKey
|
privateKey: secret://unidentifiedDelivery.privateKey
|
||||||
expiresDays: 7
|
expiresDays: 7
|
||||||
|
|
||||||
hCaptcha:
|
|
||||||
apiKey: secret://hCaptcha.apiKey
|
|
||||||
|
|
||||||
shortCode:
|
shortCode:
|
||||||
baseUrl: https://example.com/shortcodes/
|
baseUrl: https://example.com/shortcodes/
|
||||||
|
|
||||||
|
|
|
@ -37,7 +37,6 @@ import org.whispersystems.textsecuregcm.configuration.FcmConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.GcpAttachmentsConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.GcpAttachmentsConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.GenericZkConfig;
|
import org.whispersystems.textsecuregcm.configuration.GenericZkConfig;
|
||||||
import org.whispersystems.textsecuregcm.configuration.GooglePlayBillingConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.GooglePlayBillingConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.HCaptchaClientFactory;
|
|
||||||
import org.whispersystems.textsecuregcm.configuration.KeyTransparencyServiceConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.KeyTransparencyServiceConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.LinkDeviceSecretConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.LinkDeviceSecretConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.MaxDeviceConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.MaxDeviceConfiguration;
|
||||||
|
@ -202,11 +201,6 @@ public class WhisperServerConfiguration extends Configuration {
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
private UnidentifiedDeliveryConfiguration unidentifiedDelivery;
|
private UnidentifiedDeliveryConfiguration unidentifiedDelivery;
|
||||||
|
|
||||||
@Valid
|
|
||||||
@NotNull
|
|
||||||
@JsonProperty
|
|
||||||
private HCaptchaClientFactory hCaptcha;
|
|
||||||
|
|
||||||
@Valid
|
@Valid
|
||||||
@NotNull
|
@NotNull
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
|
@ -379,10 +373,6 @@ public class WhisperServerConfiguration extends Configuration {
|
||||||
return dynamoDbTables;
|
return dynamoDbTables;
|
||||||
}
|
}
|
||||||
|
|
||||||
public HCaptchaClientFactory getHCaptchaConfiguration() {
|
|
||||||
return hCaptcha;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ShortCodeExpanderConfiguration getShortCodeRetrieverConfiguration() {
|
public ShortCodeExpanderConfiguration getShortCodeRetrieverConfiguration() {
|
||||||
return shortCode;
|
return shortCode;
|
||||||
}
|
}
|
||||||
|
|
|
@ -100,7 +100,7 @@ import org.whispersystems.textsecuregcm.calls.routing.CallRoutingTableManager;
|
||||||
import org.whispersystems.textsecuregcm.calls.routing.DynamicConfigTurnRouter;
|
import org.whispersystems.textsecuregcm.calls.routing.DynamicConfigTurnRouter;
|
||||||
import org.whispersystems.textsecuregcm.calls.routing.TurnCallRouter;
|
import org.whispersystems.textsecuregcm.calls.routing.TurnCallRouter;
|
||||||
import org.whispersystems.textsecuregcm.captcha.CaptchaChecker;
|
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.RegistrationCaptchaManager;
|
||||||
import org.whispersystems.textsecuregcm.captcha.ShortCodeExpander;
|
import org.whispersystems.textsecuregcm.captcha.ShortCodeExpander;
|
||||||
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
|
||||||
|
@ -499,8 +499,6 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||||
.scheduledExecutorService(name(getClass(), "secureValueRecoveryServiceRetry-%d")).threads(1).build();
|
.scheduledExecutorService(name(getClass(), "secureValueRecoveryServiceRetry-%d")).threads(1).build();
|
||||||
ScheduledExecutorService storageServiceRetryExecutor = environment.lifecycle()
|
ScheduledExecutorService storageServiceRetryExecutor = environment.lifecycle()
|
||||||
.scheduledExecutorService(name(getClass(), "storageServiceRetry-%d")).threads(1).build();
|
.scheduledExecutorService(name(getClass(), "storageServiceRetry-%d")).threads(1).build();
|
||||||
ScheduledExecutorService hcaptchaRetryExecutor = environment.lifecycle()
|
|
||||||
.scheduledExecutorService(name(getClass(), "hCaptchaRetry-%d")).threads(1).build();
|
|
||||||
ScheduledExecutorService remoteStorageRetryExecutor = environment.lifecycle()
|
ScheduledExecutorService remoteStorageRetryExecutor = environment.lifecycle()
|
||||||
.scheduledExecutorService(name(getClass(), "remoteStorageRetry-%d")).threads(1).build();
|
.scheduledExecutorService(name(getClass(), "remoteStorageRetry-%d")).threads(1).build();
|
||||||
ScheduledExecutorService registrationIdentityTokenRefreshExecutor = environment.lifecycle()
|
ScheduledExecutorService registrationIdentityTokenRefreshExecutor = environment.lifecycle()
|
||||||
|
@ -551,14 +549,6 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||||
.maxThreads(8)
|
.maxThreads(8)
|
||||||
.build();
|
.build();
|
||||||
// unbounded executor (same as cachedThreadPool)
|
// unbounded executor (same as cachedThreadPool)
|
||||||
ExecutorService hcaptchaHttpExecutor = environment.lifecycle()
|
|
||||||
.executorService(name(getClass(), "hcaptcha-%d"))
|
|
||||||
.minThreads(0)
|
|
||||||
.maxThreads(Integer.MAX_VALUE)
|
|
||||||
.workQueue(new SynchronousQueue<>())
|
|
||||||
.keepAliveTime(io.dropwizard.util.Duration.seconds(60L))
|
|
||||||
.build();
|
|
||||||
// unbounded executor (same as cachedThreadPool)
|
|
||||||
ExecutorService remoteStorageHttpExecutor = environment.lifecycle()
|
ExecutorService remoteStorageHttpExecutor = environment.lifecycle()
|
||||||
.executorService(name(getClass(), "remoteStorage-%d"))
|
.executorService(name(getClass(), "remoteStorage-%d"))
|
||||||
.minThreads(0)
|
.minThreads(0)
|
||||||
|
@ -706,13 +696,6 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||||
"message_byte_limit",
|
"message_byte_limit",
|
||||||
config.getMessageByteLimitCardinalityEstimator().period());
|
config.getMessageByteLimitCardinalityEstimator().period());
|
||||||
|
|
||||||
HCaptchaClient hCaptchaClient = config.getHCaptchaConfiguration()
|
|
||||||
.build(hcaptchaRetryExecutor, hcaptchaHttpExecutor, dynamicConfigurationManager);
|
|
||||||
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(hCaptchaClient));
|
|
||||||
|
|
||||||
PushChallengeManager pushChallengeManager = new PushChallengeManager(pushNotificationManager,
|
PushChallengeManager pushChallengeManager = new PushChallengeManager(pushNotificationManager,
|
||||||
pushChallengeDynamoDb);
|
pushChallengeDynamoDb);
|
||||||
|
|
||||||
|
@ -765,7 +748,6 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||||
environment.lifecycle().manage(virtualThreadPinEventMonitor);
|
environment.lifecycle().manage(virtualThreadPinEventMonitor);
|
||||||
environment.lifecycle().manage(accountsManager);
|
environment.lifecycle().manage(accountsManager);
|
||||||
|
|
||||||
final RegistrationCaptchaManager registrationCaptchaManager = new RegistrationCaptchaManager(captchaChecker);
|
|
||||||
|
|
||||||
AwsCredentialsProvider cdnCredentialsProvider = config.getCdnConfiguration().credentials().build();
|
AwsCredentialsProvider cdnCredentialsProvider = config.getCdnConfiguration().credentials().build();
|
||||||
S3Client cdnS3Client = S3Client.builder()
|
S3Client cdnS3Client = S3Client.builder()
|
||||||
|
@ -1075,10 +1057,22 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||||
log.warn("No registration-recovery-checkers found; using default (no-op) provider as a default");
|
log.warn("No registration-recovery-checkers found; using default (no-op) provider as a default");
|
||||||
return RegistrationRecoveryChecker.noop();
|
return RegistrationRecoveryChecker.noop();
|
||||||
});
|
});
|
||||||
|
final List<CaptchaClient> 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);
|
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,
|
final RateLimitChallengeManager rateLimitChallengeManager = new RateLimitChallengeManager(pushChallengeManager,
|
||||||
captchaChecker, rateLimiters, spamFilter.map(SpamFilter::getRateLimitChallengeListener).stream().toList());
|
captchaChecker, rateLimiters, spamFilter.map(SpamFilter::getRateLimitChallengeListener).stream().toList());
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
package org.whispersystems.textsecuregcm.captcha;
|
package org.whispersystems.textsecuregcm.captcha;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
@ -40,4 +41,24 @@ public interface CaptchaClient {
|
||||||
final String token,
|
final String token,
|
||||||
final String ip,
|
final String ip,
|
||||||
final String userAgent) throws IOException;
|
final String userAgent) throws IOException;
|
||||||
|
|
||||||
|
static CaptchaClient noop() {
|
||||||
|
return new CaptchaClient() {
|
||||||
|
@Override
|
||||||
|
public String scheme() {
|
||||||
|
return "noop";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<String> 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();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<DynamicConfiguration> dynamicConfigurationManager;
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
HCaptchaClient(final String apiKey,
|
|
||||||
final FaultTolerantHttpClient faultTolerantHttpClient,
|
|
||||||
final DynamicConfigurationManager<DynamicConfiguration> 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<DynamicConfiguration> 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<String> 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<String> 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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
* <p>
|
|
||||||
* see <a href="https://docs.hcaptcha.com/#verify-the-user-response-server-side">...</a>
|
|
||||||
*/
|
|
||||||
public class HCaptchaResponse {
|
|
||||||
|
|
||||||
@JsonProperty
|
|
||||||
boolean success;
|
|
||||||
|
|
||||||
@JsonProperty(value = "challenge_ts")
|
|
||||||
Instant challengeTs;
|
|
||||||
|
|
||||||
@JsonProperty
|
|
||||||
String hostname;
|
|
||||||
|
|
||||||
@JsonProperty(value = "error-codes")
|
|
||||||
List<String> errorCodes = Collections.emptyList();
|
|
||||||
|
|
||||||
@JsonProperty
|
|
||||||
float score;
|
|
||||||
|
|
||||||
@JsonProperty(value = "score_reason")
|
|
||||||
List<String> scoreReasons = Collections.emptyList();
|
|
||||||
|
|
||||||
public HCaptchaResponse() {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return "HCaptchaResponse{" +
|
|
||||||
"success=" + success +
|
|
||||||
", challengeTs=" + challengeTs +
|
|
||||||
", hostname='" + hostname + '\'' +
|
|
||||||
", errorCodes=" + errorCodes +
|
|
||||||
", score=" + score +
|
|
||||||
", scoreReasons=" + scoreReasons +
|
|
||||||
'}';
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<DynamicConfiguration> dynamicConfigurationManager);
|
|
||||||
}
|
|
|
@ -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<DynamicConfiguration> dynamicConfigurationManager) {
|
|
||||||
return new HCaptchaClient(
|
|
||||||
apiKey.value(),
|
|
||||||
retryExecutor,
|
|
||||||
httpExecutor,
|
|
||||||
circuitBreaker,
|
|
||||||
retry,
|
|
||||||
dynamicConfigurationManager);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -8,7 +8,10 @@ package org.whispersystems.textsecuregcm.spam;
|
||||||
import io.dropwizard.configuration.ConfigurationValidationException;
|
import io.dropwizard.configuration.ConfigurationValidationException;
|
||||||
import io.dropwizard.lifecycle.Managed;
|
import io.dropwizard.lifecycle.Managed;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
import javax.validation.Validator;
|
import javax.validation.Validator;
|
||||||
|
import org.whispersystems.textsecuregcm.captcha.CaptchaChecker;
|
||||||
|
import org.whispersystems.textsecuregcm.captcha.CaptchaClient;
|
||||||
import org.whispersystems.textsecuregcm.storage.ReportedMessageListener;
|
import org.whispersystems.textsecuregcm.storage.ReportedMessageListener;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -80,4 +83,6 @@ public interface SpamFilter extends Managed {
|
||||||
* @return a {@link RegistrationRecoveryChecker} controlled by the spam filter
|
* @return a {@link RegistrationRecoveryChecker} controlled by the spam filter
|
||||||
*/
|
*/
|
||||||
RegistrationRecoveryChecker getRegistrationRecoveryChecker();
|
RegistrationRecoveryChecker getRegistrationRecoveryChecker();
|
||||||
|
|
||||||
|
List<CaptchaClient> getCaptchaClients();
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@ org.whispersystems.textsecuregcm.configuration.AwsCredentialsProviderFactory
|
||||||
org.whispersystems.textsecuregcm.configuration.DatadogConfiguration
|
org.whispersystems.textsecuregcm.configuration.DatadogConfiguration
|
||||||
org.whispersystems.textsecuregcm.configuration.DynamicConfigurationManagerFactory
|
org.whispersystems.textsecuregcm.configuration.DynamicConfigurationManagerFactory
|
||||||
org.whispersystems.textsecuregcm.configuration.DynamoDbClientFactory
|
org.whispersystems.textsecuregcm.configuration.DynamoDbClientFactory
|
||||||
org.whispersystems.textsecuregcm.configuration.HCaptchaClientFactory
|
|
||||||
org.whispersystems.textsecuregcm.configuration.FaultTolerantRedisClusterFactory
|
org.whispersystems.textsecuregcm.configuration.FaultTolerantRedisClusterFactory
|
||||||
org.whispersystems.textsecuregcm.configuration.FaultTolerantRedisClientFactory
|
org.whispersystems.textsecuregcm.configuration.FaultTolerantRedisClientFactory
|
||||||
org.whispersystems.textsecuregcm.configuration.PaymentsServiceClientsFactory
|
org.whispersystems.textsecuregcm.configuration.PaymentsServiceClientsFactory
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
org.whispersystems.textsecuregcm.configuration.StubHCaptchaClientFactory
|
|
|
@ -281,9 +281,6 @@ unidentifiedDelivery:
|
||||||
privateKey: secret://unidentifiedDelivery.privateKey
|
privateKey: secret://unidentifiedDelivery.privateKey
|
||||||
expiresDays: 7
|
expiresDays: 7
|
||||||
|
|
||||||
hCaptcha:
|
|
||||||
type: stub
|
|
||||||
|
|
||||||
shortCode:
|
shortCode:
|
||||||
baseUrl: https://example.com/shortcodes/
|
baseUrl: https://example.com/shortcodes/
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue