initial grpc service code in chat

This commit is contained in:
Jonathan Klabunde Tomer
2023-06-26 17:10:13 -07:00
committed by GitHub
parent cc3cab9c88
commit 8d995e456e
15 changed files with 540 additions and 184 deletions

View File

@@ -5,6 +5,8 @@
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.anyInt;
import static org.mockito.Mockito.mock;
@@ -13,105 +15,165 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.google.common.net.HttpHeaders;
import com.google.protobuf.ByteString;
import com.vdurmont.semver4j.Semver;
import java.io.IOException;
import java.util.EnumMap;
import java.util.Set;
import java.util.stream.Stream;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.junit.jupiter.api.Test;
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.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.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 {
@Test
void testEmptyMap() throws IOException, ServletException {
// We're happy as long as there's no exception
final DynamicConfigurationManager dynamicConfigurationManager = mock(DynamicConfigurationManager.class);
final DynamicConfiguration dynamicConfiguration = mock(DynamicConfiguration.class);
final DynamicRemoteDeprecationConfiguration emptyConfiguration = new DynamicRemoteDeprecationConfiguration();
@Test
void testEmptyMap() throws IOException, ServletException {
// We're happy as long as there's no exception
final DynamicConfigurationManager dynamicConfigurationManager = mock(DynamicConfigurationManager.class);
final DynamicConfiguration dynamicConfiguration = mock(DynamicConfiguration.class);
final DynamicRemoteDeprecationConfiguration emptyConfiguration = new DynamicRemoteDeprecationConfiguration();
when(dynamicConfigurationManager.getConfiguration()).thenReturn(dynamicConfiguration);
when(dynamicConfiguration.getRemoteDeprecationConfiguration()).thenReturn(emptyConfiguration);
when(dynamicConfigurationManager.getConfiguration()).thenReturn(dynamicConfiguration);
when(dynamicConfiguration.getRemoteDeprecationConfiguration()).thenReturn(emptyConfiguration);
final RemoteDeprecationFilter filter = new RemoteDeprecationFilter(dynamicConfigurationManager);
final RemoteDeprecationFilter filter = new RemoteDeprecationFilter(dynamicConfigurationManager);
final HttpServletRequest servletRequest = mock(HttpServletRequest.class);
final HttpServletResponse servletResponse = mock(HttpServletResponse.class);
final FilterChain filterChain = mock(FilterChain.class);
final HttpServletRequest servletRequest = mock(HttpServletRequest.class);
final HttpServletResponse servletResponse = mock(HttpServletResponse.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(servletResponse, never()).sendError(anyInt());
verify(filterChain).doFilter(servletRequest, servletResponse);
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
@CsvSource(delimiter = '|', value =
{"Unrecognized UA | false",
"Signal-Android/4.68.3 | false",
"Signal-iOS/3.9.0 | false",
"Signal-Desktop/1.2.3 | false",
"Signal-Android/0.68.3 | true",
"Signal-iOS/0.9.0 | true",
"Signal-Desktop/0.2.3 | true",
"Signal-Desktop/8.0.0-beta.2 | true",
"Signal-Desktop/8.0.0-beta.1 | false",
"Signal-iOS/8.0.0-beta.2 | false"})
void testFilter(final String userAgent, final boolean expectDeprecation) throws IOException, ServletException {
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"));
@ParameterizedTest
@MethodSource(value="testFilter")
void testGrpcFilter(final String userAgent, final boolean expectDeprecation) throws Exception {
final Server testServer = InProcessServerBuilder.forName("RemoteDeprecationFilterTest")
.directExecutor()
.addService(new EchoServiceImpl())
.intercept(filterConfiguredForTest())
.intercept(new UserAgentInterceptor())
.build()
.start();
final ManagedChannel channel = InProcessChannelBuilder.forName("RemoteDeprecationFilterTest")
.directExecutor()
.userAgent(userAgent)
.build();
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);
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);
try {
final EchoServiceGrpc.EchoServiceBlockingStub client = EchoServiceGrpc.newBlockingStub(channel);
final EchoRequest req = EchoRequest.newBuilder().setPayload(ByteString.copyFromUtf8("cluck cluck, i'm a parrot")).build();
if (expectDeprecation) {
verify(filterChain, never()).doFilter(any(), any());
verify(servletResponse).sendError(499);
final StatusRuntimeException e = assertThrows(
StatusRuntimeException.class,
() -> client.echo(req));
assertEquals(StatusConstants.UPGRADE_NEEDED_STATUS.toString(), e.getStatus().toString());
} else {
verify(filterChain).doFilter(servletRequest, servletResponse);
verify(servletResponse, never()).sendError(anyInt());
assertEquals("cluck cluck, i'm a parrot", client.echo(req).getPayload().toStringUtf8());
}
} 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));
}
}

View File

@@ -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();
}
}

View File

@@ -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"));
}
}

View File

@@ -53,7 +53,10 @@ class UserAgentUtilTest {
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)")),
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")));
}
}