Add support for remote client deprecation

This commit is contained in:
Jon Chambers
2021-02-09 12:24:44 -05:00
committed by GitHub
parent b4350ec77b
commit 2f105ed0a4
6 changed files with 362 additions and 0 deletions

View File

@@ -71,6 +71,7 @@ import org.whispersystems.textsecuregcm.controllers.SecureStorageController;
import org.whispersystems.textsecuregcm.controllers.StickerController;
import org.whispersystems.textsecuregcm.controllers.VoiceVerificationController;
import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager;
import org.whispersystems.textsecuregcm.filters.RemoteDeprecationFilter;
import org.whispersystems.textsecuregcm.filters.TimestampResponseFilter;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.liquibase.NameableMigrationsBundle;
@@ -427,6 +428,9 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
AuthFilter<BasicCredentials, Account> accountAuthFilter = new BasicCredentialAuthFilter.Builder<Account>().setAuthenticator(accountAuthenticator).buildAuthFilter ();
AuthFilter<BasicCredentials, DisabledPermittedAccount> disabledPermittedAccountAuthFilter = new BasicCredentialAuthFilter.Builder<DisabledPermittedAccount>().setAuthenticator(disabledPermittedAccountAuthenticator).buildAuthFilter();
environment.servlets().addFilter("RemoteDeprecationFilter", new RemoteDeprecationFilter(dynamicConfigurationManager))
.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/*");
environment.jersey().register(new MetricsApplicationEventListener(TrafficSource.HTTP));
environment.jersey().register(new PolymorphicAuthDynamicFeature<>(ImmutableMap.of(Account.class, accountAuthFilter,

View File

@@ -17,6 +17,10 @@ public class DynamicConfiguration {
@Valid
private DynamicRateLimitsConfiguration limits = new DynamicRateLimitsConfiguration();
@JsonProperty
@Valid
private DynamicRemoteDeprecationConfiguration remoteDeprecation = new DynamicRemoteDeprecationConfiguration();
public Optional<DynamicExperimentEnrollmentConfiguration> getExperimentEnrollmentConfiguration(final String experimentName) {
return Optional.ofNullable(experiments.get(experimentName));
}
@@ -24,4 +28,8 @@ public class DynamicConfiguration {
public DynamicRateLimitsConfiguration getLimits() {
return limits;
}
public DynamicRemoteDeprecationConfiguration getRemoteDeprecationConfiguration() {
return remoteDeprecation;
}
}

View File

@@ -0,0 +1,79 @@
/*
* Copyright 2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration.dynamic;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.annotations.VisibleForTesting;
import com.vdurmont.semver4j.Semver;
import org.whispersystems.textsecuregcm.util.ua.ClientPlatform;
import javax.validation.constraints.NotNull;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
public class DynamicRemoteDeprecationConfiguration {
@JsonProperty
private Map<ClientPlatform, Semver> minimumVersions = Collections.emptyMap();
@JsonProperty
private Map<ClientPlatform, Semver> versionsPendingDeprecation = Collections.emptyMap();
@JsonProperty
private Map<ClientPlatform, Set<Semver>> blockedVersions = Collections.emptyMap();
@JsonProperty
private Map<ClientPlatform, Set<Semver>> versionsPendingBlock = Collections.emptyMap();
@JsonProperty
private boolean unrecognizedUserAgentAllowed = true;
@VisibleForTesting
public void setMinimumVersions(final Map<ClientPlatform, Semver> minimumVersions) {
this.minimumVersions = minimumVersions;
}
public Map<ClientPlatform, Semver> getMinimumVersions() {
return minimumVersions;
}
@VisibleForTesting
public void setVersionsPendingDeprecation(final Map<ClientPlatform, Semver> versionsPendingDeprecation) {
this.versionsPendingDeprecation = versionsPendingDeprecation;
}
public Map<ClientPlatform, Semver> getVersionsPendingDeprecation() {
return versionsPendingDeprecation;
}
@VisibleForTesting
public void setUnrecognizedUserAgentAllowed(final boolean allowUnrecognizedUserAgents) {
this.unrecognizedUserAgentAllowed = allowUnrecognizedUserAgents;
}
public boolean isUnrecognizedUserAgentAllowed() {
return unrecognizedUserAgentAllowed;
}
@VisibleForTesting
public void setBlockedVersions(final Map<ClientPlatform, Set<Semver>> blockedVersions) {
this.blockedVersions = blockedVersions;
}
public Map<ClientPlatform, Set<Semver>> getBlockedVersions() {
return blockedVersions;
}
@VisibleForTesting
public void setVersionsPendingBlock(final Map<ClientPlatform, Set<Semver>> versionsPendingBlock) {
this.versionsPendingBlock = versionsPendingBlock;
}
public Map<ClientPlatform, Set<Semver>> getVersionsPendingBlock() {
return versionsPendingBlock;
}
}

View File

@@ -0,0 +1,118 @@
/*
* Copyright 2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.filters;
import static com.codahale.metrics.MetricRegistry.name;
import com.vdurmont.semver4j.Semver;
import io.micrometer.core.instrument.Metrics;
import java.io.IOException;
import java.util.Map;
import java.util.Set;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicRemoteDeprecationConfiguration;
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
import org.whispersystems.textsecuregcm.util.ua.ClientPlatform;
import org.whispersystems.textsecuregcm.util.ua.UnrecognizedUserAgentException;
import org.whispersystems.textsecuregcm.util.ua.UserAgent;
import org.whispersystems.textsecuregcm.util.ua.UserAgentUtil;
/**
* The remote deprecation filter rejects traffic from clients older than a configured minimum
* version. It may optionally also reject traffic from clients with unrecognized User-Agent strings.
* If a client platform does not have a configured minimum version, all traffic from that client
* platform is allowed.
*/
public class RemoteDeprecationFilter implements Filter {
private final DynamicConfigurationManager dynamicConfigurationManager;
private static final String DEPRECATED_CLIENT_COUNTER_NAME = name(RemoteDeprecationFilter.class, "deprecated");
private static final String PENDING_DEPRECATION_COUNTER_NAME = name(RemoteDeprecationFilter.class, "pendingDeprecation");
private static final String PLATFORM_TAG = "platform";
private static final String REASON_TAG_NAME = "reason";
private static final String EXPIRED_CLIENT_REASON = "expired";
private static final String BLOCKED_CLIENT_REASON = "blocked";
private static final String UNRECOGNIZED_UA_REASON = "unrecognized_user_agent";
public RemoteDeprecationFilter(final DynamicConfigurationManager dynamicConfigurationManager) {
this.dynamicConfigurationManager = dynamicConfigurationManager;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
final DynamicRemoteDeprecationConfiguration configuration = dynamicConfigurationManager
.getConfiguration().getRemoteDeprecationConfiguration();
final Map<ClientPlatform, Semver> minimumVersionsByPlatform = configuration.getMinimumVersions();
final Map<ClientPlatform, Semver> versionsPendingDeprecationByPlatform = configuration.getVersionsPendingDeprecation();
final Map<ClientPlatform, Set<Semver>> blockedVersionsByPlatform = configuration.getBlockedVersions();
final Map<ClientPlatform, Set<Semver>> versionsPendingBlockByPlatform = configuration.getVersionsPendingBlock();
final boolean allowUnrecognizedUserAgents = configuration.isUnrecognizedUserAgentAllowed();
boolean shouldBlock = false;
try {
final String userAgentString = ((HttpServletRequest) request).getHeader("User-Agent");
final UserAgent userAgent = UserAgentUtil.parseUserAgentString(userAgentString);
if (blockedVersionsByPlatform.containsKey(userAgent.getPlatform())) {
if (blockedVersionsByPlatform.get(userAgent.getPlatform()).contains(userAgent.getVersion())) {
recordDeprecation(userAgent, BLOCKED_CLIENT_REASON);
shouldBlock = true;
}
}
if (minimumVersionsByPlatform.containsKey(userAgent.getPlatform())) {
if (userAgent.getVersion().isLowerThan(minimumVersionsByPlatform.get(userAgent.getPlatform()))) {
recordDeprecation(userAgent, EXPIRED_CLIENT_REASON);
shouldBlock = true;
}
}
if (versionsPendingBlockByPlatform.containsKey(userAgent.getPlatform())) {
if (versionsPendingBlockByPlatform.get(userAgent.getPlatform()).contains(userAgent.getVersion())) {
recordPendingDeprecation(userAgent, BLOCKED_CLIENT_REASON);
}
}
if (versionsPendingDeprecationByPlatform.containsKey(userAgent.getPlatform())) {
if (userAgent.getVersion().isLowerThan(versionsPendingDeprecationByPlatform.get(userAgent.getPlatform()))) {
recordPendingDeprecation(userAgent, EXPIRED_CLIENT_REASON);
}
}
} catch (final UnrecognizedUserAgentException e) {
if (!allowUnrecognizedUserAgents) {
recordDeprecation(null, UNRECOGNIZED_UA_REASON);
shouldBlock = true;
}
}
if (shouldBlock) {
((HttpServletResponse) response).sendError(499);
} else {
chain.doFilter(request, response);
}
}
private void recordDeprecation(final UserAgent userAgent, final String reason) {
Metrics.counter(DEPRECATED_CLIENT_COUNTER_NAME,
PLATFORM_TAG, userAgent != null ? userAgent.getPlatform().name().toLowerCase() : "unrecognized",
REASON_TAG_NAME, reason).increment();
}
private void recordPendingDeprecation(final UserAgent userAgent, final String reason) {
Metrics.counter(PENDING_DEPRECATION_COUNTER_NAME,
PLATFORM_TAG, userAgent.getPlatform().name().toLowerCase(),
REASON_TAG_NAME, reason).increment();
}
}