mirror of
https://github.com/signalapp/Signal-Server
synced 2026-04-22 11:18:06 +01:00
Introduce a Noise-over-WebSocket client connection manager
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
package org.whispersystems.textsecuregcm.auth.grpc;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
import io.grpc.ManagedChannel;
|
||||
import io.grpc.Server;
|
||||
import io.grpc.netty.NettyChannelBuilder;
|
||||
import io.grpc.netty.NettyServerBuilder;
|
||||
import io.netty.channel.DefaultEventLoopGroup;
|
||||
import io.netty.channel.local.LocalAddress;
|
||||
import io.netty.channel.local.LocalChannel;
|
||||
import io.netty.channel.local.LocalServerChannel;
|
||||
import java.io.IOException;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.signal.chat.rpc.GetAuthenticatedDeviceRequest;
|
||||
import org.signal.chat.rpc.GetAuthenticatedDeviceResponse;
|
||||
import org.signal.chat.rpc.RequestAttributesGrpc;
|
||||
import org.whispersystems.textsecuregcm.grpc.RequestAttributesServiceImpl;
|
||||
import org.whispersystems.textsecuregcm.grpc.net.ClientConnectionManager;
|
||||
|
||||
abstract class AbstractAuthenticationInterceptorTest {
|
||||
|
||||
private static DefaultEventLoopGroup eventLoopGroup;
|
||||
|
||||
private ClientConnectionManager clientConnectionManager;
|
||||
|
||||
private Server server;
|
||||
private ManagedChannel managedChannel;
|
||||
|
||||
@BeforeAll
|
||||
static void setUpBeforeAll() {
|
||||
eventLoopGroup = new DefaultEventLoopGroup();
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws IOException {
|
||||
final LocalAddress serverAddress = new LocalAddress("test-authentication-interceptor-server");
|
||||
|
||||
clientConnectionManager = mock(ClientConnectionManager.class);
|
||||
|
||||
// `RequestAttributesInterceptor` operates on `LocalAddresses`, so we need to do some slightly fancy plumbing to make
|
||||
// sure that we're using local channels and addresses
|
||||
server = NettyServerBuilder.forAddress(serverAddress)
|
||||
.channelType(LocalServerChannel.class)
|
||||
.bossEventLoopGroup(eventLoopGroup)
|
||||
.workerEventLoopGroup(eventLoopGroup)
|
||||
.intercept(getInterceptor())
|
||||
.addService(new RequestAttributesServiceImpl())
|
||||
.build()
|
||||
.start();
|
||||
|
||||
managedChannel = NettyChannelBuilder.forAddress(serverAddress)
|
||||
.channelType(LocalChannel.class)
|
||||
.eventLoopGroup(eventLoopGroup)
|
||||
.usePlaintext()
|
||||
.build();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
managedChannel.shutdown();
|
||||
server.shutdown();
|
||||
}
|
||||
|
||||
protected abstract AbstractAuthenticationInterceptor getInterceptor();
|
||||
|
||||
protected ClientConnectionManager getClientConnectionManager() {
|
||||
return clientConnectionManager;
|
||||
}
|
||||
|
||||
protected GetAuthenticatedDeviceResponse getAuthenticatedDevice() {
|
||||
return RequestAttributesGrpc.newBlockingStub(managedChannel)
|
||||
.getAuthenticatedDevice(GetAuthenticatedDeviceRequest.newBuilder().build());
|
||||
}
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.auth.grpc;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
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.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import io.grpc.CallCredentials;
|
||||
import io.grpc.ManagedChannel;
|
||||
import io.grpc.Metadata;
|
||||
import io.grpc.Server;
|
||||
import io.grpc.Status;
|
||||
import io.grpc.StatusRuntimeException;
|
||||
import io.grpc.inprocess.InProcessChannelBuilder;
|
||||
import io.grpc.inprocess.InProcessServerBuilder;
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.stream.Stream;
|
||||
import org.apache.commons.lang3.RandomStringUtils;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
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.EchoServiceGrpc;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
|
||||
import org.whispersystems.textsecuregcm.grpc.EchoServiceImpl;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.util.HeaderUtils;
|
||||
import org.whispersystems.textsecuregcm.util.Pair;
|
||||
|
||||
class BasicCredentialAuthenticationInterceptorTest {
|
||||
|
||||
private Server server;
|
||||
private ManagedChannel managedChannel;
|
||||
|
||||
private AccountAuthenticator accountAuthenticator;
|
||||
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws IOException {
|
||||
accountAuthenticator = mock(AccountAuthenticator.class);
|
||||
|
||||
final BasicCredentialAuthenticationInterceptor authenticationInterceptor =
|
||||
new BasicCredentialAuthenticationInterceptor(accountAuthenticator);
|
||||
|
||||
final String serverName = InProcessServerBuilder.generateName();
|
||||
|
||||
server = InProcessServerBuilder.forName(serverName)
|
||||
.directExecutor()
|
||||
.intercept(authenticationInterceptor)
|
||||
.addService(new EchoServiceImpl())
|
||||
.build()
|
||||
.start();
|
||||
|
||||
managedChannel = InProcessChannelBuilder.forName(serverName)
|
||||
.directExecutor()
|
||||
.build();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
managedChannel.shutdown();
|
||||
server.shutdown();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource
|
||||
void interceptCall(final Metadata headers, final boolean acceptCredentials, final boolean expectAuthentication) {
|
||||
if (acceptCredentials) {
|
||||
final Account account = mock(Account.class);
|
||||
when(account.getUuid()).thenReturn(UUID.randomUUID());
|
||||
|
||||
final Device device = mock(Device.class);
|
||||
when(device.getId()).thenReturn(Device.PRIMARY_ID);
|
||||
|
||||
when(accountAuthenticator.authenticate(any()))
|
||||
.thenReturn(Optional.of(new AuthenticatedAccount(account, device)));
|
||||
} else {
|
||||
when(accountAuthenticator.authenticate(any()))
|
||||
.thenReturn(Optional.empty());
|
||||
}
|
||||
|
||||
final EchoServiceGrpc.EchoServiceBlockingStub stub = EchoServiceGrpc.newBlockingStub(managedChannel)
|
||||
.withCallCredentials(new CallCredentials() {
|
||||
@Override
|
||||
public void applyRequestMetadata(final RequestInfo requestInfo, final Executor appExecutor, final MetadataApplier applier) {
|
||||
applier.apply(headers);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void thisUsesUnstableApi() {
|
||||
}
|
||||
});
|
||||
|
||||
if (expectAuthentication) {
|
||||
assertDoesNotThrow(() -> stub.echo(EchoRequest.newBuilder().build()));
|
||||
} else {
|
||||
final StatusRuntimeException exception =
|
||||
assertThrows(StatusRuntimeException.class, () -> stub.echo(EchoRequest.newBuilder().build()));
|
||||
|
||||
assertEquals(Status.UNAUTHENTICATED.getCode(), exception.getStatus().getCode());
|
||||
}
|
||||
}
|
||||
|
||||
private static Stream<Arguments> interceptCall() {
|
||||
final Metadata malformedCredentialHeaders = new Metadata();
|
||||
malformedCredentialHeaders.put(BasicCredentialAuthenticationInterceptor.BASIC_CREDENTIALS, "Incorrect");
|
||||
|
||||
final Metadata structurallyValidCredentialHeaders = new Metadata();
|
||||
structurallyValidCredentialHeaders.put(
|
||||
BasicCredentialAuthenticationInterceptor.BASIC_CREDENTIALS,
|
||||
HeaderUtils.basicAuthHeader(UUID.randomUUID().toString(), RandomStringUtils.randomAlphanumeric(16))
|
||||
);
|
||||
|
||||
return Stream.of(
|
||||
Arguments.of(new Metadata(), true, false),
|
||||
Arguments.of(malformedCredentialHeaders, true, false),
|
||||
Arguments.of(structurallyValidCredentialHeaders, false, false),
|
||||
Arguments.of(structurallyValidCredentialHeaders, true, true)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -13,15 +13,14 @@ import io.grpc.ServerCallHandler;
|
||||
import io.grpc.ServerInterceptor;
|
||||
import java.util.UUID;
|
||||
import javax.annotation.Nullable;
|
||||
import org.whispersystems.textsecuregcm.util.Pair;
|
||||
|
||||
public class MockAuthenticationInterceptor implements ServerInterceptor {
|
||||
|
||||
@Nullable
|
||||
private Pair<UUID, Byte> authenticatedDevice;
|
||||
private AuthenticatedDevice authenticatedDevice;
|
||||
|
||||
public void setAuthenticatedDevice(final UUID accountIdentifier, final byte deviceId) {
|
||||
authenticatedDevice = new Pair<>(accountIdentifier, deviceId);
|
||||
authenticatedDevice = new AuthenticatedDevice(accountIdentifier, deviceId);
|
||||
}
|
||||
|
||||
public void clearAuthenticatedDevice() {
|
||||
@@ -33,14 +32,10 @@ public class MockAuthenticationInterceptor implements ServerInterceptor {
|
||||
final Metadata headers,
|
||||
final ServerCallHandler<ReqT, RespT> next) {
|
||||
|
||||
if (authenticatedDevice != null) {
|
||||
final Context context = Context.current()
|
||||
.withValue(AuthenticationUtil.CONTEXT_AUTHENTICATED_ACCOUNT_IDENTIFIER_KEY, authenticatedDevice.first())
|
||||
.withValue(AuthenticationUtil.CONTEXT_AUTHENTICATED_DEVICE_IDENTIFIER_KEY, authenticatedDevice.second());
|
||||
|
||||
return Contexts.interceptCall(context, call, headers, next);
|
||||
}
|
||||
|
||||
return next.startCall(call, headers);
|
||||
return authenticatedDevice != null
|
||||
? Contexts.interceptCall(
|
||||
Context.current().withValue(AuthenticationUtil.CONTEXT_AUTHENTICATED_DEVICE, authenticatedDevice),
|
||||
call, headers, next)
|
||||
: next.startCall(call, headers);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package org.whispersystems.textsecuregcm.auth.grpc;
|
||||
|
||||
import io.grpc.Status;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.signal.chat.rpc.GetAuthenticatedDeviceResponse;
|
||||
import org.whispersystems.textsecuregcm.grpc.GrpcTestUtils;
|
||||
import org.whispersystems.textsecuregcm.grpc.net.ClientConnectionManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.util.UUIDUtil;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class ProhibitAuthenticationInterceptorTest extends AbstractAuthenticationInterceptorTest {
|
||||
|
||||
@Override
|
||||
protected AbstractAuthenticationInterceptor getInterceptor() {
|
||||
return new ProhibitAuthenticationInterceptor(getClientConnectionManager());
|
||||
}
|
||||
|
||||
@Test
|
||||
void interceptCall() {
|
||||
final ClientConnectionManager clientConnectionManager = getClientConnectionManager();
|
||||
|
||||
when(clientConnectionManager.getAuthenticatedDevice(any())).thenReturn(Optional.empty());
|
||||
|
||||
final GetAuthenticatedDeviceResponse response = getAuthenticatedDevice();
|
||||
assertTrue(response.getAccountIdentifier().isEmpty());
|
||||
assertEquals(0, response.getDeviceId());
|
||||
|
||||
final AuthenticatedDevice authenticatedDevice = new AuthenticatedDevice(UUID.randomUUID(), Device.PRIMARY_ID);
|
||||
when(clientConnectionManager.getAuthenticatedDevice(any())).thenReturn(Optional.of(authenticatedDevice));
|
||||
|
||||
GrpcTestUtils.assertStatusException(Status.UNAUTHENTICATED, this::getAuthenticatedDevice);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package org.whispersystems.textsecuregcm.auth.grpc;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import io.grpc.Status;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.signal.chat.rpc.GetAuthenticatedDeviceResponse;
|
||||
import org.whispersystems.textsecuregcm.grpc.GrpcTestUtils;
|
||||
import org.whispersystems.textsecuregcm.grpc.net.ClientConnectionManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.util.UUIDUtil;
|
||||
|
||||
class RequireAuthenticationInterceptorTest extends AbstractAuthenticationInterceptorTest {
|
||||
|
||||
@Override
|
||||
protected AbstractAuthenticationInterceptor getInterceptor() {
|
||||
return new RequireAuthenticationInterceptor(getClientConnectionManager());
|
||||
}
|
||||
|
||||
@Test
|
||||
void interceptCall() {
|
||||
final ClientConnectionManager clientConnectionManager = getClientConnectionManager();
|
||||
|
||||
when(clientConnectionManager.getAuthenticatedDevice(any())).thenReturn(Optional.empty());
|
||||
|
||||
GrpcTestUtils.assertStatusException(Status.UNAUTHENTICATED, this::getAuthenticatedDevice);
|
||||
|
||||
final AuthenticatedDevice authenticatedDevice = new AuthenticatedDevice(UUID.randomUUID(), Device.PRIMARY_ID);
|
||||
when(clientConnectionManager.getAuthenticatedDevice(any())).thenReturn(Optional.of(authenticatedDevice));
|
||||
|
||||
final GetAuthenticatedDeviceResponse response = getAuthenticatedDevice();
|
||||
assertEquals(UUIDUtil.toByteString(authenticatedDevice.accountIdentifier()), response.getAccountIdentifier());
|
||||
assertEquals(authenticatedDevice.deviceId(), response.getDeviceId());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user