Add experiment to test standalone registration service

This commit is contained in:
Jon Chambers
2022-10-06 15:42:53 -04:00
committed by GitHub
parent d6c9652a70
commit d2fa00f0c6
19 changed files with 705 additions and 136 deletions

View File

@@ -0,0 +1,32 @@
package org.whispersystems.textsecuregcm.registration;
import io.grpc.CallCredentials;
import io.grpc.Metadata;
import java.util.concurrent.Executor;
class ApiKeyCallCredentials extends CallCredentials {
private final String apiKey;
private static final Metadata.Key<String> API_KEY_METADATA_KEY =
Metadata.Key.of("x-signal-api-key", Metadata.ASCII_STRING_MARSHALLER);
ApiKeyCallCredentials(final String apiKey) {
this.apiKey = apiKey;
}
@Override
public void applyRequestMetadata(final RequestInfo requestInfo,
final Executor appExecutor,
final MetadataApplier applier) {
final Metadata metadata = new Metadata();
metadata.put(API_KEY_METADATA_KEY, apiKey);
applier.apply(metadata);
}
@Override
public void thisUsesUnstableApi() {
}
}

View File

@@ -0,0 +1,13 @@
/*
* Copyright 2022 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.registration;
public enum ClientType {
IOS,
ANDROID_WITH_FCM,
ANDROID_WITHOUT_FCM,
UNKNOWN
}

View File

@@ -0,0 +1,14 @@
/*
* Copyright 2022 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.registration;
/**
* A message transport is a medium via which verification codes can be delivered to a destination phone.
*/
public enum MessageTransport {
SMS,
VOICE
}

View File

@@ -0,0 +1,138 @@
package org.whispersystems.textsecuregcm.registration;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.i18n.phonenumbers.PhoneNumberUtil;
import com.google.i18n.phonenumbers.Phonenumber;
import com.google.protobuf.ByteString;
import io.dropwizard.lifecycle.Managed;
import io.grpc.ChannelCredentials;
import io.grpc.Deadline;
import io.grpc.Grpc;
import io.grpc.ManagedChannel;
import io.grpc.TlsChannelCredentials;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import org.apache.commons.lang3.StringUtils;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.signal.registration.rpc.CheckVerificationCodeRequest;
import org.signal.registration.rpc.CheckVerificationCodeResponse;
import org.signal.registration.rpc.RegistrationServiceGrpc;
import org.signal.registration.rpc.SendVerificationCodeRequest;
public class RegistrationServiceClient implements Managed {
private final ManagedChannel channel;
private final RegistrationServiceGrpc.RegistrationServiceFutureStub stub;
private final Executor callbackExecutor;
public RegistrationServiceClient(final String host,
final int port,
final String apiKey,
final String caCertificatePem,
final Executor callbackExecutor) throws IOException {
try (final ByteArrayInputStream certificateInputStream = new ByteArrayInputStream(caCertificatePem.getBytes(StandardCharsets.UTF_8))) {
final ChannelCredentials tlsChannelCredentials = TlsChannelCredentials.newBuilder()
.trustManager(certificateInputStream)
.build();
this.channel = Grpc.newChannelBuilderForAddress(host, port, tlsChannelCredentials).build();
}
this.stub = RegistrationServiceGrpc.newFutureStub(channel)
.withCallCredentials(new ApiKeyCallCredentials(apiKey));
this.callbackExecutor = callbackExecutor;
}
public CompletableFuture<byte[]> sendRegistrationCode(final Phonenumber.PhoneNumber phoneNumber,
final MessageTransport messageTransport,
final ClientType clientType,
@Nullable final String acceptLanguage,
final Duration timeout) {
final long e164 = Long.parseLong(
PhoneNumberUtil.getInstance().format(phoneNumber, PhoneNumberUtil.PhoneNumberFormat.E164).substring(1));
final SendVerificationCodeRequest.Builder requestBuilder = SendVerificationCodeRequest.newBuilder()
.setE164(e164)
.setTransport(getRpcMessageTransport(messageTransport))
.setClientType(getRpcClientType(clientType));
if (StringUtils.isNotBlank(acceptLanguage)) {
requestBuilder.setAcceptLanguage(acceptLanguage);
}
return toCompletableFuture(stub.withDeadline(toDeadline(timeout))
.sendVerificationCode(requestBuilder.build()))
.thenApply(response -> response.getSessionId().toByteArray());
}
public CompletableFuture<Boolean> checkVerificationCode(final byte[] sessionId,
final String verificationCode,
final Duration timeout) {
return toCompletableFuture(stub.withDeadline(toDeadline(timeout))
.checkVerificationCode(CheckVerificationCodeRequest.newBuilder()
.setSessionId(ByteString.copyFrom(sessionId))
.setVerificationCode(verificationCode)
.build()))
.thenApply(CheckVerificationCodeResponse::getVerified);
}
private static Deadline toDeadline(final Duration timeout) {
return Deadline.after(timeout.toMillis(), TimeUnit.MILLISECONDS);
}
private static org.signal.registration.rpc.ClientType getRpcClientType(final ClientType clientType) {
return switch (clientType) {
case IOS -> org.signal.registration.rpc.ClientType.CLIENT_TYPE_IOS;
case ANDROID_WITH_FCM -> org.signal.registration.rpc.ClientType.CLIENT_TYPE_ANDROID_WITH_FCM;
case ANDROID_WITHOUT_FCM -> org.signal.registration.rpc.ClientType.CLIENT_TYPE_ANDROID_WITHOUT_FCM;
case UNKNOWN -> org.signal.registration.rpc.ClientType.CLIENT_TYPE_UNSPECIFIED;
};
}
private static org.signal.registration.rpc.MessageTransport getRpcMessageTransport(final MessageTransport transport) {
return switch (transport) {
case SMS -> org.signal.registration.rpc.MessageTransport.MESSAGE_TRANSPORT_SMS;
case VOICE -> org.signal.registration.rpc.MessageTransport.MESSAGE_TRANSPORT_VOICE;
};
}
private <T> CompletableFuture<T> toCompletableFuture(final ListenableFuture<T> listenableFuture) {
final CompletableFuture<T> completableFuture = new CompletableFuture<>();
Futures.addCallback(listenableFuture, new FutureCallback<T>() {
@Override
public void onSuccess(@Nullable final T result) {
completableFuture.complete(result);
}
@Override
public void onFailure(final Throwable throwable) {
completableFuture.completeExceptionally(throwable);
}
}, callbackExecutor);
return completableFuture;
}
@Override
public void start() throws Exception {
}
@Override
public void stop() throws Exception {
if (channel != null) {
channel.shutdown();
}
}
}