initial grpc service code in chat
This commit is contained in:
parent
cc3cab9c88
commit
8d995e456e
1
pom.xml
1
pom.xml
|
@ -405,6 +405,7 @@
|
||||||
<goal>compile</goal>
|
<goal>compile</goal>
|
||||||
<goal>compile-custom</goal>
|
<goal>compile-custom</goal>
|
||||||
<goal>test-compile</goal>
|
<goal>test-compile</goal>
|
||||||
|
<goal>test-compile-custom</goal>
|
||||||
</goals>
|
</goals>
|
||||||
</execution>
|
</execution>
|
||||||
</executions>
|
</executions>
|
||||||
|
|
|
@ -51,6 +51,8 @@ adminEventLoggingConfiguration:
|
||||||
projectId: some-project-id
|
projectId: some-project-id
|
||||||
logName: some-log-name
|
logName: some-log-name
|
||||||
|
|
||||||
|
grpcPort: 8080
|
||||||
|
|
||||||
stripe:
|
stripe:
|
||||||
apiKey: secret://stripe.apiKey
|
apiKey: secret://stripe.apiKey
|
||||||
idempotencyKeyGenerator: secret://stripe.idempotencyKeyGenerator
|
idempotencyKeyGenerator: secret://stripe.idempotencyKeyGenerator
|
||||||
|
|
|
@ -270,6 +270,11 @@ public class WhisperServerConfiguration extends Configuration {
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
private TurnSecretConfiguration turn;
|
private TurnSecretConfiguration turn;
|
||||||
|
|
||||||
|
@Valid
|
||||||
|
@NotNull
|
||||||
|
@JsonProperty
|
||||||
|
private int grpcPort;
|
||||||
|
|
||||||
public AdminEventLoggingConfiguration getAdminEventLoggingConfiguration() {
|
public AdminEventLoggingConfiguration getAdminEventLoggingConfiguration() {
|
||||||
return adminEventLoggingConfiguration;
|
return adminEventLoggingConfiguration;
|
||||||
}
|
}
|
||||||
|
@ -448,4 +453,9 @@ public class WhisperServerConfiguration extends Configuration {
|
||||||
public TurnSecretConfiguration getTurnSecretConfiguration() {
|
public TurnSecretConfiguration getTurnSecretConfiguration() {
|
||||||
return turn;
|
return turn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getGrpcPort() {
|
||||||
|
return grpcPort;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,8 +23,11 @@ import io.dropwizard.auth.basic.BasicCredentialAuthFilter;
|
||||||
import io.dropwizard.auth.basic.BasicCredentials;
|
import io.dropwizard.auth.basic.BasicCredentials;
|
||||||
import io.dropwizard.setup.Bootstrap;
|
import io.dropwizard.setup.Bootstrap;
|
||||||
import io.dropwizard.setup.Environment;
|
import io.dropwizard.setup.Environment;
|
||||||
|
import io.grpc.Server;
|
||||||
|
import io.grpc.ServerBuilder;
|
||||||
import io.lettuce.core.resource.ClientResources;
|
import io.lettuce.core.resource.ClientResources;
|
||||||
import io.micrometer.core.instrument.Metrics;
|
import io.micrometer.core.instrument.Metrics;
|
||||||
|
import io.micrometer.core.instrument.binder.grpc.MetricCollectingServerInterceptor;
|
||||||
import io.micrometer.core.instrument.binder.jvm.ExecutorServiceMetrics;
|
import io.micrometer.core.instrument.binder.jvm.ExecutorServiceMetrics;
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.net.http.HttpClient;
|
import java.net.http.HttpClient;
|
||||||
|
@ -105,6 +108,8 @@ import org.whispersystems.textsecuregcm.controllers.VerificationController;
|
||||||
import org.whispersystems.textsecuregcm.currency.CoinMarketCapClient;
|
import org.whispersystems.textsecuregcm.currency.CoinMarketCapClient;
|
||||||
import org.whispersystems.textsecuregcm.currency.CurrencyConversionManager;
|
import org.whispersystems.textsecuregcm.currency.CurrencyConversionManager;
|
||||||
import org.whispersystems.textsecuregcm.currency.FixerClient;
|
import org.whispersystems.textsecuregcm.currency.FixerClient;
|
||||||
|
import org.whispersystems.textsecuregcm.grpc.GrpcServerManagedWrapper;
|
||||||
|
import org.whispersystems.textsecuregcm.grpc.UserAgentInterceptor;
|
||||||
import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager;
|
import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager;
|
||||||
import org.whispersystems.textsecuregcm.filters.RemoteDeprecationFilter;
|
import org.whispersystems.textsecuregcm.filters.RemoteDeprecationFilter;
|
||||||
import org.whispersystems.textsecuregcm.filters.RequestStatisticsFilter;
|
import org.whispersystems.textsecuregcm.filters.RequestStatisticsFilter;
|
||||||
|
@ -626,10 +631,22 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||||
AuthFilter<BasicCredentials, DisabledPermittedAuthenticatedAccount> disabledPermittedAccountAuthFilter = new BasicCredentialAuthFilter.Builder<DisabledPermittedAuthenticatedAccount>().setAuthenticator(
|
AuthFilter<BasicCredentials, DisabledPermittedAuthenticatedAccount> disabledPermittedAccountAuthFilter = new BasicCredentialAuthFilter.Builder<DisabledPermittedAuthenticatedAccount>().setAuthenticator(
|
||||||
disabledPermittedAccountAuthenticator).buildAuthFilter();
|
disabledPermittedAccountAuthenticator).buildAuthFilter();
|
||||||
|
|
||||||
|
final ServerBuilder<?> grpcServer = ServerBuilder.forPort(config.getGrpcPort())
|
||||||
|
.intercept(new MetricCollectingServerInterceptor(Metrics.globalRegistry)); /* TODO: specialize metrics with user-agent platform */
|
||||||
|
|
||||||
|
RemoteDeprecationFilter remoteDeprecationFilter = new RemoteDeprecationFilter(dynamicConfigurationManager);
|
||||||
environment.servlets()
|
environment.servlets()
|
||||||
.addFilter("RemoteDeprecationFilter", new RemoteDeprecationFilter(dynamicConfigurationManager))
|
.addFilter("RemoteDeprecationFilter", remoteDeprecationFilter)
|
||||||
.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/*");
|
.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/*");
|
||||||
|
|
||||||
|
// Note: interceptors run in the reverse order they are added; the remote deprecation filter
|
||||||
|
// depends on the user-agent context so it has to come first here!
|
||||||
|
// http://grpc.github.io/grpc-java/javadoc/io/grpc/ServerBuilder.html#intercept-io.grpc.ServerInterceptor-
|
||||||
|
grpcServer.intercept(remoteDeprecationFilter);
|
||||||
|
grpcServer.intercept(new UserAgentInterceptor());
|
||||||
|
|
||||||
|
environment.lifecycle().manage(new GrpcServerManagedWrapper(grpcServer.build()));
|
||||||
|
|
||||||
environment.jersey().register(new RequestStatisticsFilter(TrafficSource.HTTP));
|
environment.jersey().register(new RequestStatisticsFilter(TrafficSource.HTTP));
|
||||||
environment.jersey().register(MultiRecipientMessageProvider.class);
|
environment.jersey().register(MultiRecipientMessageProvider.class);
|
||||||
environment.jersey().register(new MetricsApplicationEventListener(TrafficSource.HTTP));
|
environment.jersey().register(new MetricsApplicationEventListener(TrafficSource.HTTP));
|
||||||
|
|
|
@ -7,8 +7,15 @@ package org.whispersystems.textsecuregcm.filters;
|
||||||
|
|
||||||
import static com.codahale.metrics.MetricRegistry.name;
|
import static com.codahale.metrics.MetricRegistry.name;
|
||||||
|
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
import com.google.common.net.HttpHeaders;
|
import com.google.common.net.HttpHeaders;
|
||||||
import com.vdurmont.semver4j.Semver;
|
import com.vdurmont.semver4j.Semver;
|
||||||
|
|
||||||
|
import io.grpc.Metadata;
|
||||||
|
import io.grpc.ServerCall;
|
||||||
|
import io.grpc.ServerCallHandler;
|
||||||
|
import io.grpc.ServerInterceptor;
|
||||||
|
import io.grpc.Status;
|
||||||
import io.micrometer.core.instrument.Metrics;
|
import io.micrometer.core.instrument.Metrics;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
@ -22,6 +29,8 @@ import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicRemoteDeprecationConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicRemoteDeprecationConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.grpc.StatusConstants;
|
||||||
|
import org.whispersystems.textsecuregcm.grpc.UserAgentInterceptor;
|
||||||
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
||||||
import org.whispersystems.textsecuregcm.util.ua.ClientPlatform;
|
import org.whispersystems.textsecuregcm.util.ua.ClientPlatform;
|
||||||
import org.whispersystems.textsecuregcm.util.ua.UnrecognizedUserAgentException;
|
import org.whispersystems.textsecuregcm.util.ua.UnrecognizedUserAgentException;
|
||||||
|
@ -34,7 +43,7 @@ import org.whispersystems.textsecuregcm.util.ua.UserAgentUtil;
|
||||||
* If a client platform does not have a configured minimum version, all traffic from that client
|
* If a client platform does not have a configured minimum version, all traffic from that client
|
||||||
* platform is allowed.
|
* platform is allowed.
|
||||||
*/
|
*/
|
||||||
public class RemoteDeprecationFilter implements Filter {
|
public class RemoteDeprecationFilter implements Filter, ServerInterceptor {
|
||||||
|
|
||||||
private final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager;
|
private final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager;
|
||||||
|
|
||||||
|
@ -52,60 +61,84 @@ public class RemoteDeprecationFilter implements Filter {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
|
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
|
||||||
final DynamicRemoteDeprecationConfiguration configuration = dynamicConfigurationManager
|
final String userAgentString = ((HttpServletRequest) request).getHeader(HttpHeaders.USER_AGENT);
|
||||||
.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;
|
|
||||||
|
|
||||||
|
UserAgent userAgent;
|
||||||
try {
|
try {
|
||||||
final String userAgentString = ((HttpServletRequest) request).getHeader(HttpHeaders.USER_AGENT);
|
userAgent = UserAgentUtil.parseUserAgentString(userAgentString);
|
||||||
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) {
|
} catch (final UnrecognizedUserAgentException e) {
|
||||||
if (!allowUnrecognizedUserAgents) {
|
userAgent = null;
|
||||||
recordDeprecation(null, UNRECOGNIZED_UA_REASON);
|
|
||||||
shouldBlock = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldBlock) {
|
if (shouldBlock(userAgent)) {
|
||||||
((HttpServletResponse) response).sendError(499);
|
((HttpServletResponse) response).sendError(499);
|
||||||
} else {
|
} else {
|
||||||
chain.doFilter(request, response);
|
chain.doFilter(request, response);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
|
||||||
|
final ServerCall<ReqT, RespT> call,
|
||||||
|
final Metadata headers,
|
||||||
|
final ServerCallHandler<ReqT, RespT> next) {
|
||||||
|
|
||||||
|
if (shouldBlock(UserAgentUtil.userAgentFromGrpcContext())) {
|
||||||
|
call.close(StatusConstants.UPGRADE_NEEDED_STATUS, new Metadata());
|
||||||
|
return new ServerCall.Listener<>() {};
|
||||||
|
} else {
|
||||||
|
return next.startCall(call, headers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shouldBlock(final UserAgent userAgent) {
|
||||||
|
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();
|
||||||
|
|
||||||
|
boolean shouldBlock = false;
|
||||||
|
|
||||||
|
if (userAgent == null) {
|
||||||
|
if (configuration.isUnrecognizedUserAgentAllowed()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
recordDeprecation(null, UNRECOGNIZED_UA_REASON);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return shouldBlock;
|
||||||
|
}
|
||||||
|
|
||||||
private void recordDeprecation(final UserAgent userAgent, final String reason) {
|
private void recordDeprecation(final UserAgent userAgent, final String reason) {
|
||||||
Metrics.counter(DEPRECATED_CLIENT_COUNTER_NAME,
|
Metrics.counter(DEPRECATED_CLIENT_COUNTER_NAME,
|
||||||
PLATFORM_TAG, userAgent != null ? userAgent.getPlatform().name().toLowerCase() : "unrecognized",
|
PLATFORM_TAG, userAgent != null ? userAgent.getPlatform().name().toLowerCase() : "unrecognized",
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.grpc;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import io.dropwizard.lifecycle.Managed;
|
||||||
|
import io.grpc.Server;
|
||||||
|
|
||||||
|
public class GrpcServerManagedWrapper implements Managed {
|
||||||
|
|
||||||
|
private final Server server;
|
||||||
|
|
||||||
|
public GrpcServerManagedWrapper(final Server server) {
|
||||||
|
this.server = server;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void start() throws IOException {
|
||||||
|
server.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void stop() {
|
||||||
|
try {
|
||||||
|
server.shutdown().awaitTermination(5, TimeUnit.MINUTES);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
server.shutdownNow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.grpc;
|
||||||
|
|
||||||
|
import io.grpc.Status;
|
||||||
|
|
||||||
|
public abstract class StatusConstants {
|
||||||
|
public static final Status UPGRADE_NEEDED_STATUS = Status.INVALID_ARGUMENT.withDescription("signal-upgrade-required");
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.grpc;
|
||||||
|
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
|
||||||
|
import org.whispersystems.textsecuregcm.util.ua.UnrecognizedUserAgentException;
|
||||||
|
import org.whispersystems.textsecuregcm.util.ua.UserAgent;
|
||||||
|
import org.whispersystems.textsecuregcm.util.ua.UserAgentUtil;
|
||||||
|
|
||||||
|
import io.grpc.Context;
|
||||||
|
import io.grpc.Contexts;
|
||||||
|
import io.grpc.Metadata;
|
||||||
|
import io.grpc.ServerCall;
|
||||||
|
import io.grpc.ServerCallHandler;
|
||||||
|
import io.grpc.ServerInterceptor;
|
||||||
|
import io.grpc.Status;
|
||||||
|
|
||||||
|
public class UserAgentInterceptor implements ServerInterceptor {
|
||||||
|
@VisibleForTesting
|
||||||
|
public static final Metadata.Key<String> USER_AGENT_GRPC_HEADER =
|
||||||
|
Metadata.Key.of("user-agent", Metadata.ASCII_STRING_MARSHALLER);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(final ServerCall<ReqT, RespT> call,
|
||||||
|
final Metadata headers,
|
||||||
|
final ServerCallHandler<ReqT, RespT> next) {
|
||||||
|
|
||||||
|
UserAgent userAgent;
|
||||||
|
try {
|
||||||
|
userAgent = UserAgentUtil.parseUserAgentString(headers.get(USER_AGENT_GRPC_HEADER));
|
||||||
|
} catch (final UnrecognizedUserAgentException e) {
|
||||||
|
userAgent = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Context context = Context.current().withValue(UserAgentUtil.USER_AGENT_CONTEXT_KEY, userAgent);
|
||||||
|
return Contexts.interceptCall(context, call, headers, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,64 +1,63 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 Signal Messenger, LLC
|
* Copyright 2013-2023 Signal Messenger, LLC
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.whispersystems.textsecuregcm.util.ua;
|
package org.whispersystems.textsecuregcm.util.ua;
|
||||||
|
|
||||||
import com.vdurmont.semver4j.Semver;
|
import com.vdurmont.semver4j.Semver;
|
||||||
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
public class UserAgent {
|
public class UserAgent {
|
||||||
|
|
||||||
private final ClientPlatform platform;
|
private final ClientPlatform platform;
|
||||||
private final Semver version;
|
private final Semver version;
|
||||||
private final String additionalSpecifiers;
|
private final String additionalSpecifiers;
|
||||||
|
|
||||||
public UserAgent(final ClientPlatform platform, final Semver version) {
|
public UserAgent(final ClientPlatform platform, final Semver version) {
|
||||||
this(platform, version, null);
|
this(platform, version, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public UserAgent(final ClientPlatform platform, final Semver version, final String additionalSpecifiers) {
|
public UserAgent(final ClientPlatform platform, final Semver version, final String additionalSpecifiers) {
|
||||||
this.platform = platform;
|
this.platform = platform;
|
||||||
this.version = version;
|
this.version = version;
|
||||||
this.additionalSpecifiers = additionalSpecifiers;
|
this.additionalSpecifiers = additionalSpecifiers;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ClientPlatform getPlatform() {
|
public ClientPlatform getPlatform() {
|
||||||
return platform;
|
return platform;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Semver getVersion() {
|
public Semver getVersion() {
|
||||||
return version;
|
return version;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<String> getAdditionalSpecifiers() {
|
public Optional<String> getAdditionalSpecifiers() {
|
||||||
return Optional.ofNullable(additionalSpecifiers);
|
return Optional.ofNullable(additionalSpecifiers);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(final Object o) {
|
public boolean equals(final Object o) {
|
||||||
if (this == o) return true;
|
if (this == o) return true;
|
||||||
if (o == null || getClass() != o.getClass()) return false;
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
final UserAgent userAgent = (UserAgent)o;
|
final UserAgent userAgent = (UserAgent)o;
|
||||||
return platform == userAgent.platform &&
|
return platform == userAgent.platform &&
|
||||||
version.equals(userAgent.version) &&
|
version.equals(userAgent.version) &&
|
||||||
Objects.equals(additionalSpecifiers, userAgent.additionalSpecifiers);
|
Objects.equals(additionalSpecifiers, userAgent.additionalSpecifiers);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
return Objects.hash(platform, version, additionalSpecifiers);
|
return Objects.hash(platform, version, additionalSpecifiers);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "UserAgent{" +
|
return "UserAgent{" +
|
||||||
"platform=" + platform +
|
"platform=" + platform +
|
||||||
", version=" + version +
|
", version=" + version +
|
||||||
", additionalSpecifiers='" + additionalSpecifiers + '\'' +
|
", additionalSpecifiers='" + additionalSpecifiers + '\'' +
|
||||||
'}';
|
'}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,40 +7,47 @@ package org.whispersystems.textsecuregcm.util.ua;
|
||||||
|
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
import com.vdurmont.semver4j.Semver;
|
import com.vdurmont.semver4j.Semver;
|
||||||
|
import io.grpc.Context;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
public class UserAgentUtil {
|
public class UserAgentUtil {
|
||||||
|
|
||||||
private static final Pattern STANDARD_UA_PATTERN = Pattern.compile("^Signal-(Android|Desktop|iOS)/([^ ]+)( (.+))?$", Pattern.CASE_INSENSITIVE);
|
public static final Context.Key<UserAgent> USER_AGENT_CONTEXT_KEY = Context.key("x-signal-user-agent");
|
||||||
|
|
||||||
public static UserAgent parseUserAgentString(final String userAgentString) throws UnrecognizedUserAgentException {
|
private static final Pattern STANDARD_UA_PATTERN = Pattern.compile("^Signal-(Android|Desktop|iOS)/([^ ]+)( (.+))?$", Pattern.CASE_INSENSITIVE);
|
||||||
if (StringUtils.isBlank(userAgentString)) {
|
|
||||||
throw new UnrecognizedUserAgentException("User-Agent string is blank");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
public static UserAgent parseUserAgentString(final String userAgentString) throws UnrecognizedUserAgentException {
|
||||||
final UserAgent standardUserAgent = parseStandardUserAgentString(userAgentString);
|
if (StringUtils.isBlank(userAgentString)) {
|
||||||
|
throw new UnrecognizedUserAgentException("User-Agent string is blank");
|
||||||
if (standardUserAgent != null) {
|
|
||||||
return standardUserAgent;
|
|
||||||
}
|
|
||||||
} catch (final Exception e) {
|
|
||||||
throw new UnrecognizedUserAgentException(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new UnrecognizedUserAgentException();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
try {
|
||||||
static UserAgent parseStandardUserAgentString(final String userAgentString) {
|
final UserAgent standardUserAgent = parseStandardUserAgentString(userAgentString);
|
||||||
final Matcher matcher = STANDARD_UA_PATTERN.matcher(userAgentString);
|
|
||||||
|
|
||||||
if (matcher.matches()) {
|
if (standardUserAgent != null) {
|
||||||
return new UserAgent(ClientPlatform.valueOf(matcher.group(1).toUpperCase()), new Semver(matcher.group(2)), StringUtils.stripToNull(matcher.group(4)));
|
return standardUserAgent;
|
||||||
}
|
}
|
||||||
|
} catch (final Exception e) {
|
||||||
return null;
|
throw new UnrecognizedUserAgentException(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw new UnrecognizedUserAgentException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static UserAgent userAgentFromGrpcContext() {
|
||||||
|
return USER_AGENT_CONTEXT_KEY.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
static UserAgent parseStandardUserAgentString(final String userAgentString) {
|
||||||
|
final Matcher matcher = STANDARD_UA_PATTERN.matcher(userAgentString);
|
||||||
|
|
||||||
|
if (matcher.matches()) {
|
||||||
|
return new UserAgent(ClientPlatform.valueOf(matcher.group(1).toUpperCase()), new Semver(matcher.group(2)), StringUtils.stripToNull(matcher.group(4)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,8 @@
|
||||||
|
|
||||||
package org.whispersystems.textsecuregcm.filters;
|
package org.whispersystems.textsecuregcm.filters;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.anyInt;
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
|
@ -13,105 +15,165 @@ import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
import com.google.common.net.HttpHeaders;
|
import com.google.common.net.HttpHeaders;
|
||||||
|
import com.google.protobuf.ByteString;
|
||||||
import com.vdurmont.semver4j.Semver;
|
import com.vdurmont.semver4j.Semver;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.EnumMap;
|
import java.util.EnumMap;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import javax.servlet.FilterChain;
|
import javax.servlet.FilterChain;
|
||||||
import javax.servlet.ServletException;
|
import javax.servlet.ServletException;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.params.ParameterizedTest;
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
import org.junit.jupiter.params.provider.CsvSource;
|
import org.junit.jupiter.params.provider.Arguments;
|
||||||
|
import org.junit.jupiter.params.provider.MethodSource;
|
||||||
|
import org.signal.chat.rpc.EchoServiceGrpc;
|
||||||
|
import org.signal.chat.rpc.EchoRequest;
|
||||||
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicRemoteDeprecationConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicRemoteDeprecationConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.grpc.EchoServiceImpl;
|
||||||
|
import org.whispersystems.textsecuregcm.grpc.StatusConstants;
|
||||||
|
import org.whispersystems.textsecuregcm.grpc.UserAgentInterceptor;
|
||||||
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
||||||
import org.whispersystems.textsecuregcm.util.ua.ClientPlatform;
|
import org.whispersystems.textsecuregcm.util.ua.ClientPlatform;
|
||||||
|
|
||||||
|
import io.grpc.Metadata;
|
||||||
|
import io.grpc.ManagedChannel;
|
||||||
|
import io.grpc.Server;
|
||||||
|
import io.grpc.ServerBuilder;
|
||||||
|
import io.grpc.StatusRuntimeException;
|
||||||
|
import io.grpc.inprocess.InProcessServerBuilder;
|
||||||
|
import io.grpc.inprocess.InProcessChannelBuilder;
|
||||||
|
import io.grpc.stub.MetadataUtils;
|
||||||
|
|
||||||
class RemoteDeprecationFilterTest {
|
class RemoteDeprecationFilterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testEmptyMap() throws IOException, ServletException {
|
void testEmptyMap() throws IOException, ServletException {
|
||||||
// We're happy as long as there's no exception
|
// We're happy as long as there's no exception
|
||||||
final DynamicConfigurationManager dynamicConfigurationManager = mock(DynamicConfigurationManager.class);
|
final DynamicConfigurationManager dynamicConfigurationManager = mock(DynamicConfigurationManager.class);
|
||||||
final DynamicConfiguration dynamicConfiguration = mock(DynamicConfiguration.class);
|
final DynamicConfiguration dynamicConfiguration = mock(DynamicConfiguration.class);
|
||||||
final DynamicRemoteDeprecationConfiguration emptyConfiguration = new DynamicRemoteDeprecationConfiguration();
|
final DynamicRemoteDeprecationConfiguration emptyConfiguration = new DynamicRemoteDeprecationConfiguration();
|
||||||
|
|
||||||
when(dynamicConfigurationManager.getConfiguration()).thenReturn(dynamicConfiguration);
|
when(dynamicConfigurationManager.getConfiguration()).thenReturn(dynamicConfiguration);
|
||||||
when(dynamicConfiguration.getRemoteDeprecationConfiguration()).thenReturn(emptyConfiguration);
|
when(dynamicConfiguration.getRemoteDeprecationConfiguration()).thenReturn(emptyConfiguration);
|
||||||
|
|
||||||
final RemoteDeprecationFilter filter = new RemoteDeprecationFilter(dynamicConfigurationManager);
|
final RemoteDeprecationFilter filter = new RemoteDeprecationFilter(dynamicConfigurationManager);
|
||||||
|
|
||||||
final HttpServletRequest servletRequest = mock(HttpServletRequest.class);
|
final HttpServletRequest servletRequest = mock(HttpServletRequest.class);
|
||||||
final HttpServletResponse servletResponse = mock(HttpServletResponse.class);
|
final HttpServletResponse servletResponse = mock(HttpServletResponse.class);
|
||||||
final FilterChain filterChain = mock(FilterChain.class);
|
final FilterChain filterChain = mock(FilterChain.class);
|
||||||
|
|
||||||
when(servletRequest.getHeader("UserAgent")).thenReturn("Signal-Android/4.68.3");
|
when(servletRequest.getHeader("UserAgent")).thenReturn("Signal-Android/4.68.3");
|
||||||
|
|
||||||
filter.doFilter(servletRequest, servletResponse, filterChain);
|
filter.doFilter(servletRequest, servletResponse, filterChain);
|
||||||
|
|
||||||
verify(filterChain).doFilter(servletRequest, servletResponse);
|
verify(filterChain).doFilter(servletRequest, servletResponse);
|
||||||
verify(servletResponse, never()).sendError(anyInt());
|
verify(servletResponse, never()).sendError(anyInt());
|
||||||
|
}
|
||||||
|
|
||||||
|
private RemoteDeprecationFilter filterConfiguredForTest() {
|
||||||
|
final EnumMap<ClientPlatform, Semver> minimumVersionsByPlatform = new EnumMap<>(ClientPlatform.class);
|
||||||
|
minimumVersionsByPlatform.put(ClientPlatform.ANDROID, new Semver("1.0.0"));
|
||||||
|
minimumVersionsByPlatform.put(ClientPlatform.IOS, new Semver("1.0.0"));
|
||||||
|
minimumVersionsByPlatform.put(ClientPlatform.DESKTOP, new Semver("1.0.0"));
|
||||||
|
|
||||||
|
final EnumMap<ClientPlatform, Semver> versionsPendingDeprecationByPlatform = new EnumMap<>(ClientPlatform.class);
|
||||||
|
minimumVersionsByPlatform.put(ClientPlatform.ANDROID, new Semver("1.1.0"));
|
||||||
|
minimumVersionsByPlatform.put(ClientPlatform.IOS, new Semver("1.1.0"));
|
||||||
|
minimumVersionsByPlatform.put(ClientPlatform.DESKTOP, new Semver("1.1.0"));
|
||||||
|
|
||||||
|
final EnumMap<ClientPlatform, Set<Semver>> blockedVersionsByPlatform = new EnumMap<>(ClientPlatform.class);
|
||||||
|
blockedVersionsByPlatform.put(ClientPlatform.DESKTOP, Set.of(new Semver("8.0.0-beta.2")));
|
||||||
|
|
||||||
|
final EnumMap<ClientPlatform, Set<Semver>> versionsPendingBlockByPlatform = new EnumMap<>(ClientPlatform.class);
|
||||||
|
versionsPendingBlockByPlatform.put(ClientPlatform.DESKTOP, Set.of(new Semver("8.0.0-beta.3")));
|
||||||
|
|
||||||
|
final DynamicRemoteDeprecationConfiguration remoteDeprecationConfiguration = new DynamicRemoteDeprecationConfiguration();
|
||||||
|
remoteDeprecationConfiguration.setMinimumVersions(minimumVersionsByPlatform);
|
||||||
|
remoteDeprecationConfiguration.setVersionsPendingDeprecation(versionsPendingDeprecationByPlatform);
|
||||||
|
remoteDeprecationConfiguration.setBlockedVersions(blockedVersionsByPlatform);
|
||||||
|
remoteDeprecationConfiguration.setVersionsPendingBlock(versionsPendingBlockByPlatform);
|
||||||
|
remoteDeprecationConfiguration.setUnrecognizedUserAgentAllowed(true);
|
||||||
|
|
||||||
|
final DynamicConfiguration dynamicConfiguration = mock(DynamicConfiguration.class);
|
||||||
|
final DynamicConfigurationManager dynamicConfigurationManager = mock(DynamicConfigurationManager.class);
|
||||||
|
|
||||||
|
when(dynamicConfigurationManager.getConfiguration()).thenReturn(dynamicConfiguration);
|
||||||
|
when(dynamicConfiguration.getRemoteDeprecationConfiguration()).thenReturn(remoteDeprecationConfiguration);
|
||||||
|
|
||||||
|
return new RemoteDeprecationFilter(dynamicConfigurationManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource
|
||||||
|
void testFilter(final String userAgent, final boolean expectDeprecation) throws IOException, ServletException {
|
||||||
|
final HttpServletRequest servletRequest = mock(HttpServletRequest.class);
|
||||||
|
final HttpServletResponse servletResponse = mock(HttpServletResponse.class);
|
||||||
|
final FilterChain filterChain = mock(FilterChain.class);
|
||||||
|
|
||||||
|
when(servletRequest.getHeader(HttpHeaders.USER_AGENT)).thenReturn(userAgent);
|
||||||
|
|
||||||
|
final RemoteDeprecationFilter filter = filterConfiguredForTest();
|
||||||
|
filter.doFilter(servletRequest, servletResponse, filterChain);
|
||||||
|
|
||||||
|
if (expectDeprecation) {
|
||||||
|
verify(filterChain, never()).doFilter(any(), any());
|
||||||
|
verify(servletResponse).sendError(499);
|
||||||
|
} else {
|
||||||
|
verify(filterChain).doFilter(servletRequest, servletResponse);
|
||||||
|
verify(servletResponse, never()).sendError(anyInt());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@CsvSource(delimiter = '|', value =
|
@MethodSource(value="testFilter")
|
||||||
{"Unrecognized UA | false",
|
void testGrpcFilter(final String userAgent, final boolean expectDeprecation) throws Exception {
|
||||||
"Signal-Android/4.68.3 | false",
|
final Server testServer = InProcessServerBuilder.forName("RemoteDeprecationFilterTest")
|
||||||
"Signal-iOS/3.9.0 | false",
|
.directExecutor()
|
||||||
"Signal-Desktop/1.2.3 | false",
|
.addService(new EchoServiceImpl())
|
||||||
"Signal-Android/0.68.3 | true",
|
.intercept(filterConfiguredForTest())
|
||||||
"Signal-iOS/0.9.0 | true",
|
.intercept(new UserAgentInterceptor())
|
||||||
"Signal-Desktop/0.2.3 | true",
|
.build()
|
||||||
"Signal-Desktop/8.0.0-beta.2 | true",
|
.start();
|
||||||
"Signal-Desktop/8.0.0-beta.1 | false",
|
final ManagedChannel channel = InProcessChannelBuilder.forName("RemoteDeprecationFilterTest")
|
||||||
"Signal-iOS/8.0.0-beta.2 | false"})
|
.directExecutor()
|
||||||
void testFilter(final String userAgent, final boolean expectDeprecation) throws IOException, ServletException {
|
.userAgent(userAgent)
|
||||||
final EnumMap<ClientPlatform, Semver> minimumVersionsByPlatform = new EnumMap<>(ClientPlatform.class);
|
.build();
|
||||||
minimumVersionsByPlatform.put(ClientPlatform.ANDROID, new Semver("1.0.0"));
|
|
||||||
minimumVersionsByPlatform.put(ClientPlatform.IOS, new Semver("1.0.0"));
|
|
||||||
minimumVersionsByPlatform.put(ClientPlatform.DESKTOP, new Semver("1.0.0"));
|
|
||||||
|
|
||||||
final EnumMap<ClientPlatform, Semver> versionsPendingDeprecationByPlatform = new EnumMap<>(ClientPlatform.class);
|
try {
|
||||||
minimumVersionsByPlatform.put(ClientPlatform.ANDROID, new Semver("1.1.0"));
|
final EchoServiceGrpc.EchoServiceBlockingStub client = EchoServiceGrpc.newBlockingStub(channel);
|
||||||
minimumVersionsByPlatform.put(ClientPlatform.IOS, new Semver("1.1.0"));
|
|
||||||
minimumVersionsByPlatform.put(ClientPlatform.DESKTOP, new Semver("1.1.0"));
|
|
||||||
|
|
||||||
final EnumMap<ClientPlatform, Set<Semver>> blockedVersionsByPlatform = new EnumMap<>(ClientPlatform.class);
|
|
||||||
blockedVersionsByPlatform.put(ClientPlatform.DESKTOP, Set.of(new Semver("8.0.0-beta.2")));
|
|
||||||
|
|
||||||
final EnumMap<ClientPlatform, Set<Semver>> versionsPendingBlockByPlatform = new EnumMap<>(ClientPlatform.class);
|
|
||||||
versionsPendingBlockByPlatform.put(ClientPlatform.DESKTOP, Set.of(new Semver("8.0.0-beta.3")));
|
|
||||||
|
|
||||||
final DynamicRemoteDeprecationConfiguration remoteDeprecationConfiguration = new DynamicRemoteDeprecationConfiguration();
|
|
||||||
remoteDeprecationConfiguration.setMinimumVersions(minimumVersionsByPlatform);
|
|
||||||
remoteDeprecationConfiguration.setVersionsPendingDeprecation(versionsPendingDeprecationByPlatform);
|
|
||||||
remoteDeprecationConfiguration.setBlockedVersions(blockedVersionsByPlatform);
|
|
||||||
remoteDeprecationConfiguration.setVersionsPendingBlock(versionsPendingBlockByPlatform);
|
|
||||||
remoteDeprecationConfiguration.setUnrecognizedUserAgentAllowed(true);
|
|
||||||
|
|
||||||
final DynamicConfiguration dynamicConfiguration = mock(DynamicConfiguration.class);
|
|
||||||
final DynamicConfigurationManager dynamicConfigurationManager = mock(DynamicConfigurationManager.class);
|
|
||||||
|
|
||||||
when(dynamicConfigurationManager.getConfiguration()).thenReturn(dynamicConfiguration);
|
|
||||||
when(dynamicConfiguration.getRemoteDeprecationConfiguration()).thenReturn(remoteDeprecationConfiguration);
|
|
||||||
|
|
||||||
final HttpServletRequest servletRequest = mock(HttpServletRequest.class);
|
|
||||||
final HttpServletResponse servletResponse = mock(HttpServletResponse.class);
|
|
||||||
final FilterChain filterChain = mock(FilterChain.class);
|
|
||||||
|
|
||||||
when(servletRequest.getHeader(HttpHeaders.USER_AGENT)).thenReturn(userAgent);
|
|
||||||
|
|
||||||
final RemoteDeprecationFilter filter = new RemoteDeprecationFilter(dynamicConfigurationManager);
|
|
||||||
filter.doFilter(servletRequest, servletResponse, filterChain);
|
|
||||||
|
|
||||||
|
final EchoRequest req = EchoRequest.newBuilder().setPayload(ByteString.copyFromUtf8("cluck cluck, i'm a parrot")).build();
|
||||||
if (expectDeprecation) {
|
if (expectDeprecation) {
|
||||||
verify(filterChain, never()).doFilter(any(), any());
|
final StatusRuntimeException e = assertThrows(
|
||||||
verify(servletResponse).sendError(499);
|
StatusRuntimeException.class,
|
||||||
|
() -> client.echo(req));
|
||||||
|
assertEquals(StatusConstants.UPGRADE_NEEDED_STATUS.toString(), e.getStatus().toString());
|
||||||
} else {
|
} else {
|
||||||
verify(filterChain).doFilter(servletRequest, servletResponse);
|
assertEquals("cluck cluck, i'm a parrot", client.echo(req).getPayload().toStringUtf8());
|
||||||
verify(servletResponse, never()).sendError(anyInt());
|
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
testServer.shutdownNow();
|
||||||
|
testServer.awaitTermination();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Stream<Arguments> testFilter() {
|
||||||
|
return Stream.of(
|
||||||
|
Arguments.of("Unrecognized UA", false),
|
||||||
|
Arguments.of("Signal-Android/4.68.3", false),
|
||||||
|
Arguments.of("Signal-iOS/3.9.0", false),
|
||||||
|
Arguments.of("Signal-Desktop/1.2.3", false),
|
||||||
|
Arguments.of("Signal-Android/0.68.3", true),
|
||||||
|
Arguments.of("Signal-iOS/0.9.0", true),
|
||||||
|
Arguments.of("Signal-Desktop/0.2.3", true),
|
||||||
|
Arguments.of("Signal-Desktop/8.0.0-beta.2", true),
|
||||||
|
Arguments.of("Signal-Desktop/8.0.0-beta.1", false),
|
||||||
|
Arguments.of("Signal-iOS/8.0.0-beta.2", false));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.grpc;
|
||||||
|
|
||||||
|
import io.grpc.stub.StreamObserver;
|
||||||
|
import org.signal.chat.rpc.EchoServiceGrpc;
|
||||||
|
import org.signal.chat.rpc.EchoRequest;
|
||||||
|
import org.signal.chat.rpc.EchoResponse;
|
||||||
|
|
||||||
|
public class EchoServiceImpl extends EchoServiceGrpc.EchoServiceImplBase {
|
||||||
|
@Override
|
||||||
|
public void echo(EchoRequest req, StreamObserver<EchoResponse> responseObserver) {
|
||||||
|
responseObserver.onNext(EchoResponse.newBuilder().setPayload(req.getPayload()).build());
|
||||||
|
responseObserver.onCompleted();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,90 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.grpc;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
|
||||||
|
import com.google.protobuf.ByteString;
|
||||||
|
import com.vdurmont.semver4j.Semver;
|
||||||
|
import io.grpc.ManagedChannel;
|
||||||
|
import io.grpc.Metadata;
|
||||||
|
import io.grpc.Server;
|
||||||
|
import io.grpc.inprocess.InProcessChannelBuilder;
|
||||||
|
import io.grpc.inprocess.InProcessServerBuilder;
|
||||||
|
import io.grpc.stub.MetadataUtils;
|
||||||
|
import io.grpc.stub.StreamObserver;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.Arguments;
|
||||||
|
import org.junit.jupiter.params.provider.MethodSource;
|
||||||
|
import org.signal.chat.rpc.EchoRequest;
|
||||||
|
import org.signal.chat.rpc.EchoResponse;
|
||||||
|
import org.signal.chat.rpc.EchoServiceGrpc;
|
||||||
|
import org.whispersystems.textsecuregcm.grpc.EchoServiceImpl;
|
||||||
|
import org.whispersystems.textsecuregcm.util.ua.ClientPlatform;
|
||||||
|
import org.whispersystems.textsecuregcm.util.ua.UserAgent;
|
||||||
|
import org.whispersystems.textsecuregcm.util.ua.UserAgentUtil;
|
||||||
|
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
public class UserAgentInterceptorTest {
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource
|
||||||
|
void testInterceptor(final String header, final ClientPlatform platform, final String version) throws Exception {
|
||||||
|
|
||||||
|
final AtomicReference<UserAgent> observedUserAgent = new AtomicReference<>(null);
|
||||||
|
final EchoServiceImpl serviceImpl = new EchoServiceImpl() {
|
||||||
|
@Override
|
||||||
|
public void echo(EchoRequest req, StreamObserver<EchoResponse> responseObserver) {
|
||||||
|
observedUserAgent.set(UserAgentUtil.userAgentFromGrpcContext());
|
||||||
|
super.echo(req, responseObserver);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
final Server testServer = InProcessServerBuilder.forName("RemoteDeprecationFilterTest")
|
||||||
|
.directExecutor()
|
||||||
|
.addService(serviceImpl)
|
||||||
|
.intercept(new UserAgentInterceptor())
|
||||||
|
.build()
|
||||||
|
.start();
|
||||||
|
|
||||||
|
try {
|
||||||
|
final ManagedChannel channel = InProcessChannelBuilder.forName("RemoteDeprecationFilterTest")
|
||||||
|
.directExecutor()
|
||||||
|
.userAgent(header)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
final EchoServiceGrpc.EchoServiceBlockingStub client = EchoServiceGrpc.newBlockingStub(channel);
|
||||||
|
|
||||||
|
final EchoRequest req = EchoRequest.newBuilder().setPayload(ByteString.copyFromUtf8("cluck cluck, i'm a parrot")).build();
|
||||||
|
assertEquals("cluck cluck, i'm a parrot", client.echo(req).getPayload().toStringUtf8());
|
||||||
|
if (platform == null) {
|
||||||
|
assertNull(observedUserAgent.get());
|
||||||
|
} else {
|
||||||
|
assertEquals(platform, observedUserAgent.get().getPlatform());
|
||||||
|
assertEquals(new Semver(version), observedUserAgent.get().getVersion());
|
||||||
|
// can't assert on the additional specifiers because they include internal details of the grpc in-process channel itself
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
testServer.shutdownNow();
|
||||||
|
testServer.awaitTermination();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Stream<Arguments> testInterceptor() {
|
||||||
|
return Stream.of(
|
||||||
|
Arguments.of(null, null, null),
|
||||||
|
Arguments.of("", null, null),
|
||||||
|
Arguments.of("Unrecognized UA", null, null),
|
||||||
|
Arguments.of("Signal-Android/4.68.3", ClientPlatform.ANDROID, "4.68.3"),
|
||||||
|
Arguments.of("Signal-iOS/3.9.0", ClientPlatform.IOS, "3.9.0"),
|
||||||
|
Arguments.of("Signal-Desktop/1.2.3", ClientPlatform.DESKTOP, "1.2.3"),
|
||||||
|
Arguments.of("Signal-Desktop/8.0.0-beta.2", ClientPlatform.DESKTOP, "8.0.0-beta.2"),
|
||||||
|
Arguments.of("Signal-iOS/8.0.0-beta.2", ClientPlatform.IOS, "8.0.0-beta.2"));
|
||||||
|
}
|
||||||
|
}
|
|
@ -53,7 +53,10 @@ class UserAgentUtilTest {
|
||||||
Arguments.of("Signal-iOS/3.9.0 (iPhone; iOS 12.2; Scale/3.00)",
|
Arguments.of("Signal-iOS/3.9.0 (iPhone; iOS 12.2; Scale/3.00)",
|
||||||
new UserAgent(ClientPlatform.IOS, new Semver("3.9.0"), "(iPhone; iOS 12.2; Scale/3.00)")),
|
new UserAgent(ClientPlatform.IOS, new Semver("3.9.0"), "(iPhone; iOS 12.2; Scale/3.00)")),
|
||||||
Arguments.of("Signal-iOS/3.9.0 iOS/14.2", new UserAgent(ClientPlatform.IOS, new Semver("3.9.0"), "iOS/14.2")),
|
Arguments.of("Signal-iOS/3.9.0 iOS/14.2", new UserAgent(ClientPlatform.IOS, new Semver("3.9.0"), "iOS/14.2")),
|
||||||
Arguments.of("Signal-iOS/3.9.0", new UserAgent(ClientPlatform.IOS, new Semver("3.9.0")))
|
Arguments.of("Signal-iOS/3.9.0", new UserAgent(ClientPlatform.IOS, new Semver("3.9.0"))),
|
||||||
);
|
Arguments.of("Signal-Android/7.11.23-nightly-1982-06-28-07-07-07 tonic/0.31",
|
||||||
|
new UserAgent(ClientPlatform.ANDROID, new Semver("7.11.23-nightly-1982-06-28-07-07-07"), "tonic/0.31")),
|
||||||
|
Arguments.of("Signal-Android/7.11.23-nightly-1982-06-28-07-07-07 Android/42 tonic/0.31",
|
||||||
|
new UserAgent(ClientPlatform.ANDROID, new Semver("7.11.23-nightly-1982-06-28-07-07-07"), "Android/42 tonic/0.31")));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
option java_multiple_files = true;
|
||||||
|
|
||||||
|
package org.signal.chat.rpc;
|
||||||
|
|
||||||
|
// A simple service for testing gRPC interceptors
|
||||||
|
service EchoService {
|
||||||
|
rpc echo (EchoRequest) returns (EchoResponse) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
message EchoRequest {
|
||||||
|
bytes payload = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message EchoResponse {
|
||||||
|
bytes payload = 1;
|
||||||
|
}
|
Loading…
Reference in New Issue