Compare commits
16 Commits
v20250409.
...
main
Author | SHA1 | Date |
---|---|---|
![]() |
23bb8277d5 | |
![]() |
8099d6465c | |
![]() |
28a0b9e84e | |
![]() |
9287aaf7ce | |
![]() |
0585f862cb | |
![]() |
7cac6f6f72 | |
![]() |
57be4d798b | |
![]() |
05c74f1997 | |
![]() |
f5e49b6db7 | |
![]() |
3c40e72d27 | |
![]() |
2f2ae7cec5 | |
![]() |
b236b53dc3 | |
![]() |
eb71e30046 | |
![]() |
aa5fd52302 | |
![]() |
d6bc2765b6 | |
![]() |
01258de560 |
17
pom.xml
17
pom.xml
|
@ -46,7 +46,7 @@
|
||||||
<!-- can be updated to latest version with Dropwizard 5 (Jetty 12); will then need to disable telemetry -->
|
<!-- can be updated to latest version with Dropwizard 5 (Jetty 12); will then need to disable telemetry -->
|
||||||
<dynamodblocal.version>2.2.1</dynamodblocal.version>
|
<dynamodblocal.version>2.2.1</dynamodblocal.version>
|
||||||
<google-cloud-libraries.version>26.57.0</google-cloud-libraries.version>
|
<google-cloud-libraries.version>26.57.0</google-cloud-libraries.version>
|
||||||
<grpc.version>1.69.0</grpc.version> <!-- should be kept in sync with the value from Google libraries-bom -->
|
<grpc.version>1.70.0</grpc.version> <!-- should be kept in sync with the value from Google libraries-bom -->
|
||||||
<gson.version>2.12.1</gson.version>
|
<gson.version>2.12.1</gson.version>
|
||||||
<!-- several libraries (AWS, Google Cloud) use Apache http components transitively, and we need to align them -->
|
<!-- several libraries (AWS, Google Cloud) use Apache http components transitively, and we need to align them -->
|
||||||
<httpcore.version>4.4.16</httpcore.version>
|
<httpcore.version>4.4.16</httpcore.version>
|
||||||
|
@ -65,9 +65,9 @@
|
||||||
<luajava.version>3.5.0</luajava.version>
|
<luajava.version>3.5.0</luajava.version>
|
||||||
<micrometer.version>1.14.5</micrometer.version>
|
<micrometer.version>1.14.5</micrometer.version>
|
||||||
<netty.version>4.1.119.Final</netty.version>
|
<netty.version>4.1.119.Final</netty.version>
|
||||||
<!-- Must be greater than or equal to the value from Google libraries-bom
|
<!-- Must be less than or equal to the value from Google libraries-bom which controls the protobuf runtime version.
|
||||||
since some of its libraries generate code. See https://protobuf.dev/support/cross-version-runtime-guarantee/. -->
|
See https://protobuf.dev/support/cross-version-runtime-guarantee/. -->
|
||||||
<protobuf.version>3.25.5</protobuf.version>
|
<protoc.version>4.29.4</protoc.version>
|
||||||
<pushy.version>0.15.4</pushy.version>
|
<pushy.version>0.15.4</pushy.version>
|
||||||
<reactive.grpc.version>1.2.4</reactive.grpc.version>
|
<reactive.grpc.version>1.2.4</reactive.grpc.version>
|
||||||
<reactor-bom.version>2024.0.4</reactor-bom.version> <!-- 3.7.4, see https://github.com/reactor/reactor#bom-versioning-scheme -->
|
<reactor-bom.version>2024.0.4</reactor-bom.version> <!-- 3.7.4, see https://github.com/reactor/reactor#bom-versioning-scheme -->
|
||||||
|
@ -127,7 +127,7 @@
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.google.cloud</groupId>
|
<groupId>com.google.cloud</groupId>
|
||||||
<artifactId>libraries-bom-protobuf3</artifactId>
|
<artifactId>libraries-bom</artifactId>
|
||||||
<version>${google-cloud-libraries.version}</version>
|
<version>${google-cloud-libraries.version}</version>
|
||||||
<type>pom</type>
|
<type>pom</type>
|
||||||
<scope>import</scope>
|
<scope>import</scope>
|
||||||
|
@ -175,11 +175,6 @@
|
||||||
<artifactId>pushy-dropwizard-metrics-listener</artifactId>
|
<artifactId>pushy-dropwizard-metrics-listener</artifactId>
|
||||||
<version>${pushy.version}</version>
|
<version>${pushy.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>com.google.protobuf</groupId>
|
|
||||||
<artifactId>protobuf-java</artifactId>
|
|
||||||
<version>${protobuf.version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.googlecode.libphonenumber</groupId>
|
<groupId>com.googlecode.libphonenumber</groupId>
|
||||||
<artifactId>libphonenumber</artifactId>
|
<artifactId>libphonenumber</artifactId>
|
||||||
|
@ -443,7 +438,7 @@
|
||||||
<version>0.6.1</version>
|
<version>0.6.1</version>
|
||||||
<configuration>
|
<configuration>
|
||||||
<checkStaleness>false</checkStaleness>
|
<checkStaleness>false</checkStaleness>
|
||||||
<protocArtifact>com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier}</protocArtifact>
|
<protocArtifact>com.google.protobuf:protoc:${protoc.version}:exe:${os.detected.classifier}</protocArtifact>
|
||||||
<pluginId>grpc-java</pluginId>
|
<pluginId>grpc-java</pluginId>
|
||||||
<pluginArtifact>io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}</pluginArtifact>
|
<pluginArtifact>io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}</pluginArtifact>
|
||||||
|
|
||||||
|
|
|
@ -482,7 +482,8 @@ turn:
|
||||||
- turn:%s
|
- turn:%s
|
||||||
- turn:%s:80?transport=tcp
|
- turn:%s:80?transport=tcp
|
||||||
- turns:%s:443?transport=tcp
|
- turns:%s:443?transport=tcp
|
||||||
ttl: 86400
|
requestedCredentialTtl: PT24H
|
||||||
|
clientCredentialTtl: PT12H
|
||||||
hostname: turn.cloudflare.example.com
|
hostname: turn.cloudflare.example.com
|
||||||
numHttpClients: 1
|
numHttpClients: 1
|
||||||
|
|
||||||
|
|
|
@ -673,7 +673,8 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||||
final CloudflareTurnCredentialsManager cloudflareTurnCredentialsManager = new CloudflareTurnCredentialsManager(
|
final CloudflareTurnCredentialsManager cloudflareTurnCredentialsManager = new CloudflareTurnCredentialsManager(
|
||||||
config.getTurnConfiguration().cloudflare().apiToken().value(),
|
config.getTurnConfiguration().cloudflare().apiToken().value(),
|
||||||
config.getTurnConfiguration().cloudflare().endpoint(),
|
config.getTurnConfiguration().cloudflare().endpoint(),
|
||||||
config.getTurnConfiguration().cloudflare().ttl(),
|
config.getTurnConfiguration().cloudflare().requestedCredentialTtl(),
|
||||||
|
config.getTurnConfiguration().cloudflare().clientCredentialTtl(),
|
||||||
config.getTurnConfiguration().cloudflare().urls(),
|
config.getTurnConfiguration().cloudflare().urls(),
|
||||||
config.getTurnConfiguration().cloudflare().urlsWithIps(),
|
config.getTurnConfiguration().cloudflare().urlsWithIps(),
|
||||||
config.getTurnConfiguration().cloudflare().hostname(),
|
config.getTurnConfiguration().cloudflare().hostname(),
|
||||||
|
@ -693,7 +694,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||||
PushChallengeManager pushChallengeManager = new PushChallengeManager(pushNotificationManager,
|
PushChallengeManager pushChallengeManager = new PushChallengeManager(pushNotificationManager,
|
||||||
pushChallengeDynamoDb);
|
pushChallengeDynamoDb);
|
||||||
|
|
||||||
ChangeNumberManager changeNumberManager = new ChangeNumberManager(messageSender, accountsManager);
|
ChangeNumberManager changeNumberManager = new ChangeNumberManager(messageSender, accountsManager, Clock.systemUTC());
|
||||||
|
|
||||||
HttpClient currencyClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2).connectTimeout(Duration.ofSeconds(10)).build();
|
HttpClient currencyClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2).connectTimeout(Duration.ofSeconds(10)).build();
|
||||||
FixerClient fixerClient = config.getPaymentsServiceConfiguration().externalClients()
|
FixerClient fixerClient = config.getPaymentsServiceConfiguration().externalClients()
|
||||||
|
|
|
@ -15,6 +15,7 @@ import java.net.Inet6Address;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.http.HttpRequest;
|
import java.net.http.HttpRequest;
|
||||||
import java.net.http.HttpResponse;
|
import java.net.http.HttpResponse;
|
||||||
|
import java.time.Duration;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.CompletionException;
|
import java.util.concurrent.CompletionException;
|
||||||
|
@ -39,16 +40,18 @@ public class CloudflareTurnCredentialsManager {
|
||||||
private final List<String> cloudflareTurnUrls;
|
private final List<String> cloudflareTurnUrls;
|
||||||
private final List<String> cloudflareTurnUrlsWithIps;
|
private final List<String> cloudflareTurnUrlsWithIps;
|
||||||
private final String cloudflareTurnHostname;
|
private final String cloudflareTurnHostname;
|
||||||
private final HttpRequest request;
|
private final HttpRequest getCredentialsRequest;
|
||||||
|
|
||||||
private final FaultTolerantHttpClient cloudflareTurnClient;
|
private final FaultTolerantHttpClient cloudflareTurnClient;
|
||||||
private final DnsNameResolver dnsNameResolver;
|
private final DnsNameResolver dnsNameResolver;
|
||||||
|
|
||||||
record CredentialRequest(long ttl) {}
|
private final Duration clientCredentialTtl;
|
||||||
|
|
||||||
record CloudflareTurnResponse(IceServer iceServers) {
|
private record CredentialRequest(long ttl) {}
|
||||||
|
|
||||||
record IceServer(
|
private record CloudflareTurnResponse(IceServer iceServers) {
|
||||||
|
|
||||||
|
private record IceServer(
|
||||||
String username,
|
String username,
|
||||||
String credential,
|
String credential,
|
||||||
List<String> urls) {
|
List<String> urls) {
|
||||||
|
@ -56,10 +59,17 @@ public class CloudflareTurnCredentialsManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
public CloudflareTurnCredentialsManager(final String cloudflareTurnApiToken,
|
public CloudflareTurnCredentialsManager(final String cloudflareTurnApiToken,
|
||||||
final String cloudflareTurnEndpoint, final long cloudflareTurnTtl, final List<String> cloudflareTurnUrls,
|
final String cloudflareTurnEndpoint,
|
||||||
final List<String> cloudflareTurnUrlsWithIps, final String cloudflareTurnHostname,
|
final Duration requestedCredentialTtl,
|
||||||
final int cloudflareTurnNumHttpClients, final CircuitBreakerConfiguration circuitBreaker,
|
final Duration clientCredentialTtl,
|
||||||
final ExecutorService executor, final RetryConfiguration retry, final ScheduledExecutorService retryExecutor,
|
final List<String> cloudflareTurnUrls,
|
||||||
|
final List<String> cloudflareTurnUrlsWithIps,
|
||||||
|
final String cloudflareTurnHostname,
|
||||||
|
final int cloudflareTurnNumHttpClients,
|
||||||
|
final CircuitBreakerConfiguration circuitBreaker,
|
||||||
|
final ExecutorService executor,
|
||||||
|
final RetryConfiguration retry,
|
||||||
|
final ScheduledExecutorService retryExecutor,
|
||||||
final DnsNameResolver dnsNameResolver) {
|
final DnsNameResolver dnsNameResolver) {
|
||||||
|
|
||||||
this.cloudflareTurnClient = FaultTolerantHttpClient.newBuilder()
|
this.cloudflareTurnClient = FaultTolerantHttpClient.newBuilder()
|
||||||
|
@ -75,17 +85,24 @@ public class CloudflareTurnCredentialsManager {
|
||||||
this.cloudflareTurnHostname = cloudflareTurnHostname;
|
this.cloudflareTurnHostname = cloudflareTurnHostname;
|
||||||
this.dnsNameResolver = dnsNameResolver;
|
this.dnsNameResolver = dnsNameResolver;
|
||||||
|
|
||||||
|
final String credentialsRequestBody;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final String body = SystemMapper.jsonMapper().writeValueAsString(new CredentialRequest(cloudflareTurnTtl));
|
credentialsRequestBody =
|
||||||
this.request = HttpRequest.newBuilder()
|
SystemMapper.jsonMapper().writeValueAsString(new CredentialRequest(requestedCredentialTtl.toSeconds()));
|
||||||
.uri(URI.create(cloudflareTurnEndpoint))
|
} catch (final JsonProcessingException e) {
|
||||||
.header("Content-Type", "application/json")
|
|
||||||
.header("Authorization", String.format("Bearer %s", cloudflareTurnApiToken))
|
|
||||||
.POST(HttpRequest.BodyPublishers.ofString(body))
|
|
||||||
.build();
|
|
||||||
} catch (JsonProcessingException e) {
|
|
||||||
throw new IllegalArgumentException(e);
|
throw new IllegalArgumentException(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We repeat the same request to Cloudflare every time, so we can construct it once and re-use it
|
||||||
|
this.getCredentialsRequest = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(cloudflareTurnEndpoint))
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.header("Authorization", String.format("Bearer %s", cloudflareTurnApiToken))
|
||||||
|
.POST(HttpRequest.BodyPublishers.ofString(credentialsRequestBody))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
this.clientCredentialTtl = clientCredentialTtl;
|
||||||
}
|
}
|
||||||
|
|
||||||
public TurnToken retrieveFromCloudflare() throws IOException {
|
public TurnToken retrieveFromCloudflare() throws IOException {
|
||||||
|
@ -105,7 +122,7 @@ public class CloudflareTurnCredentialsManager {
|
||||||
final Timer.Sample sample = Timer.start();
|
final Timer.Sample sample = Timer.start();
|
||||||
final HttpResponse<String> response;
|
final HttpResponse<String> response;
|
||||||
try {
|
try {
|
||||||
response = cloudflareTurnClient.sendAsync(request, HttpResponse.BodyHandlers.ofString()).join();
|
response = cloudflareTurnClient.sendAsync(getCredentialsRequest, HttpResponse.BodyHandlers.ofString()).join();
|
||||||
sample.stop(Timer.builder(CREDENTIAL_FETCH_TIMER_NAME)
|
sample.stop(Timer.builder(CREDENTIAL_FETCH_TIMER_NAME)
|
||||||
.publishPercentileHistogram(true)
|
.publishPercentileHistogram(true)
|
||||||
.tags("outcome", "success")
|
.tags("outcome", "success")
|
||||||
|
@ -130,6 +147,7 @@ public class CloudflareTurnCredentialsManager {
|
||||||
return new TurnToken(
|
return new TurnToken(
|
||||||
cloudflareTurnResponse.iceServers().username(),
|
cloudflareTurnResponse.iceServers().username(),
|
||||||
cloudflareTurnResponse.iceServers().credential(),
|
cloudflareTurnResponse.iceServers().credential(),
|
||||||
|
clientCredentialTtl.toSeconds(),
|
||||||
cloudflareTurnUrls == null ? Collections.emptyList() : cloudflareTurnUrls,
|
cloudflareTurnUrls == null ? Collections.emptyList() : cloudflareTurnUrls,
|
||||||
cloudflareTurnComposedUrls,
|
cloudflareTurnComposedUrls,
|
||||||
cloudflareTurnHostname
|
cloudflareTurnHostname
|
||||||
|
|
|
@ -5,13 +5,15 @@
|
||||||
|
|
||||||
package org.whispersystems.textsecuregcm.auth;
|
package org.whispersystems.textsecuregcm.auth;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import java.util.List;
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public record TurnToken(
|
public record TurnToken(
|
||||||
String username,
|
String username,
|
||||||
String password,
|
String password,
|
||||||
|
@JsonProperty("ttl") long ttlSeconds,
|
||||||
@Nonnull List<String> urls,
|
@Nonnull List<String> urls,
|
||||||
@Nonnull List<String> urlsWithIps,
|
@Nonnull List<String> urlsWithIps,
|
||||||
@Nullable String hostname) {
|
@Nullable String hostname) {
|
||||||
|
|
|
@ -1,34 +1,22 @@
|
||||||
package org.whispersystems.textsecuregcm.auth.grpc;
|
package org.whispersystems.textsecuregcm.auth.grpc;
|
||||||
|
|
||||||
import io.grpc.Grpc;
|
|
||||||
import io.grpc.Metadata;
|
|
||||||
import io.grpc.ServerCall;
|
import io.grpc.ServerCall;
|
||||||
import io.grpc.ServerInterceptor;
|
import io.grpc.ServerInterceptor;
|
||||||
import io.grpc.Status;
|
|
||||||
import io.netty.channel.local.LocalAddress;
|
|
||||||
import org.whispersystems.textsecuregcm.grpc.net.GrpcClientConnectionManager;
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import org.whispersystems.textsecuregcm.grpc.ChannelNotFoundException;
|
||||||
|
import org.whispersystems.textsecuregcm.grpc.net.GrpcClientConnectionManager;
|
||||||
|
|
||||||
abstract class AbstractAuthenticationInterceptor implements ServerInterceptor {
|
abstract class AbstractAuthenticationInterceptor implements ServerInterceptor {
|
||||||
|
|
||||||
private final GrpcClientConnectionManager grpcClientConnectionManager;
|
private final GrpcClientConnectionManager grpcClientConnectionManager;
|
||||||
|
|
||||||
private static final Metadata EMPTY_TRAILERS = new Metadata();
|
|
||||||
|
|
||||||
AbstractAuthenticationInterceptor(final GrpcClientConnectionManager grpcClientConnectionManager) {
|
AbstractAuthenticationInterceptor(final GrpcClientConnectionManager grpcClientConnectionManager) {
|
||||||
this.grpcClientConnectionManager = grpcClientConnectionManager;
|
this.grpcClientConnectionManager = grpcClientConnectionManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Optional<AuthenticatedDevice> getAuthenticatedDevice(final ServerCall<?, ?> call) {
|
protected Optional<AuthenticatedDevice> getAuthenticatedDevice(final ServerCall<?, ?> call)
|
||||||
if (call.getAttributes().get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR) instanceof LocalAddress localAddress) {
|
throws ChannelNotFoundException {
|
||||||
return grpcClientConnectionManager.getAuthenticatedDevice(localAddress);
|
|
||||||
} else {
|
|
||||||
throw new AssertionError("Unexpected channel type: " + call.getAttributes().get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected <ReqT, RespT> ServerCall.Listener<ReqT> closeAsUnauthenticated(final ServerCall<ReqT, RespT> call) {
|
return grpcClientConnectionManager.getAuthenticatedDevice(call);
|
||||||
call.close(Status.UNAUTHENTICATED, EMPTY_TRAILERS);
|
|
||||||
return new ServerCall.Listener<>() {};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,12 +3,17 @@ package org.whispersystems.textsecuregcm.auth.grpc;
|
||||||
import io.grpc.Metadata;
|
import io.grpc.Metadata;
|
||||||
import io.grpc.ServerCall;
|
import io.grpc.ServerCall;
|
||||||
import io.grpc.ServerCallHandler;
|
import io.grpc.ServerCallHandler;
|
||||||
|
import io.grpc.Status;
|
||||||
|
import org.whispersystems.textsecuregcm.grpc.ChannelNotFoundException;
|
||||||
|
import org.whispersystems.textsecuregcm.grpc.ServerInterceptorUtil;
|
||||||
import org.whispersystems.textsecuregcm.grpc.net.GrpcClientConnectionManager;
|
import org.whispersystems.textsecuregcm.grpc.net.GrpcClientConnectionManager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A "prohibit authentication" interceptor ensures that requests to endpoints that should be invoked anonymously do not
|
* A "prohibit authentication" interceptor ensures that requests to endpoints that should be invoked anonymously do not
|
||||||
* originate from a channel that is associated with an authenticated device. Calls with an associated authenticated
|
* originate from a channel that is associated with an authenticated device. Calls with an associated authenticated
|
||||||
* device are closed with an {@code UNAUTHENTICATED} status.
|
* device are closed with an {@code UNAUTHENTICATED} status. If a call's authentication status cannot be determined
|
||||||
|
* (i.e. because the underlying remote channel closed before the {@code ServerCall} started), the interceptor will
|
||||||
|
* reject the call with a status of {@code UNAVAILABLE}.
|
||||||
*/
|
*/
|
||||||
public class ProhibitAuthenticationInterceptor extends AbstractAuthenticationInterceptor {
|
public class ProhibitAuthenticationInterceptor extends AbstractAuthenticationInterceptor {
|
||||||
|
|
||||||
|
@ -21,8 +26,15 @@ public class ProhibitAuthenticationInterceptor extends AbstractAuthenticationInt
|
||||||
final Metadata headers,
|
final Metadata headers,
|
||||||
final ServerCallHandler<ReqT, RespT> next) {
|
final ServerCallHandler<ReqT, RespT> next) {
|
||||||
|
|
||||||
return getAuthenticatedDevice(call)
|
try {
|
||||||
.map(ignored -> closeAsUnauthenticated(call))
|
return getAuthenticatedDevice(call)
|
||||||
.orElseGet(() -> next.startCall(call, headers));
|
// Status.INTERNAL may seem a little surprising here, but if a caller is reaching an authentication-prohibited
|
||||||
|
// service via an authenticated connection, then that's actually a server configuration issue and not a
|
||||||
|
// problem with the client's request.
|
||||||
|
.map(ignored -> ServerInterceptorUtil.closeWithStatus(call, Status.INTERNAL))
|
||||||
|
.orElseGet(() -> next.startCall(call, headers));
|
||||||
|
} catch (final ChannelNotFoundException e) {
|
||||||
|
return ServerInterceptorUtil.closeWithStatus(call, Status.UNAVAILABLE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,12 +5,16 @@ import io.grpc.Contexts;
|
||||||
import io.grpc.Metadata;
|
import io.grpc.Metadata;
|
||||||
import io.grpc.ServerCall;
|
import io.grpc.ServerCall;
|
||||||
import io.grpc.ServerCallHandler;
|
import io.grpc.ServerCallHandler;
|
||||||
|
import io.grpc.Status;
|
||||||
|
import org.whispersystems.textsecuregcm.grpc.ChannelNotFoundException;
|
||||||
|
import org.whispersystems.textsecuregcm.grpc.ServerInterceptorUtil;
|
||||||
import org.whispersystems.textsecuregcm.grpc.net.GrpcClientConnectionManager;
|
import org.whispersystems.textsecuregcm.grpc.net.GrpcClientConnectionManager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A "require authentication" interceptor requires that requests be issued from a connection that is associated with an
|
* A "require authentication" interceptor requires that requests be issued from a connection that is associated with an
|
||||||
* authenticated device. Calls without an associated authenticated device are closed with an {@code UNAUTHENTICATED}
|
* authenticated device. Calls without an associated authenticated device are closed with an {@code UNAUTHENTICATED}
|
||||||
* status.
|
* status. If a call's authentication status cannot be determined (i.e. because the underlying remote channel closed
|
||||||
|
* before the {@code ServerCall} started), the interceptor will reject the call with a status of {@code UNAVAILABLE}.
|
||||||
*/
|
*/
|
||||||
public class RequireAuthenticationInterceptor extends AbstractAuthenticationInterceptor {
|
public class RequireAuthenticationInterceptor extends AbstractAuthenticationInterceptor {
|
||||||
|
|
||||||
|
@ -23,10 +27,17 @@ public class RequireAuthenticationInterceptor extends AbstractAuthenticationInte
|
||||||
final Metadata headers,
|
final Metadata headers,
|
||||||
final ServerCallHandler<ReqT, RespT> next) {
|
final ServerCallHandler<ReqT, RespT> next) {
|
||||||
|
|
||||||
return getAuthenticatedDevice(call)
|
try {
|
||||||
.map(authenticatedDevice -> Contexts.interceptCall(Context.current()
|
return getAuthenticatedDevice(call)
|
||||||
.withValue(AuthenticationUtil.CONTEXT_AUTHENTICATED_DEVICE, authenticatedDevice),
|
.map(authenticatedDevice -> Contexts.interceptCall(Context.current()
|
||||||
call, headers, next))
|
.withValue(AuthenticationUtil.CONTEXT_AUTHENTICATED_DEVICE, authenticatedDevice),
|
||||||
.orElseGet(() -> closeAsUnauthenticated(call));
|
call, headers, next))
|
||||||
|
// Status.INTERNAL may seem a little surprising here, but if a caller is reaching an authentication-required
|
||||||
|
// service via an unauthenticated connection, then that's actually a server configuration issue and not a
|
||||||
|
// problem with the client's request.
|
||||||
|
.orElseGet(() -> ServerInterceptorUtil.closeWithStatus(call, Status.INTERNAL));
|
||||||
|
} catch (final ChannelNotFoundException e) {
|
||||||
|
return ServerInterceptorUtil.closeWithStatus(call, Status.UNAVAILABLE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,16 +6,36 @@
|
||||||
package org.whispersystems.textsecuregcm.configuration;
|
package org.whispersystems.textsecuregcm.configuration;
|
||||||
|
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
|
import jakarta.validation.constraints.AssertTrue;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import jakarta.validation.constraints.NotEmpty;
|
import jakarta.validation.constraints.NotEmpty;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import java.time.Duration;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import jakarta.validation.constraints.Positive;
|
import jakarta.validation.constraints.Positive;
|
||||||
import org.whispersystems.textsecuregcm.configuration.secrets.SecretString;
|
import org.whispersystems.textsecuregcm.configuration.secrets.SecretString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration properties for Cloudflare TURN integration.
|
||||||
|
*
|
||||||
|
* @param apiToken the API token to use when requesting TURN tokens from Cloudflare
|
||||||
|
* @param endpoint the URI of the Cloudflare API endpoint that vends TURN tokens
|
||||||
|
* @param requestedCredentialTtl the lifetime of TURN tokens to request from Cloudflare
|
||||||
|
* @param clientCredentialTtl the time clients may cache a TURN token; must be less than or equal to {@link #requestedCredentialTtl}
|
||||||
|
* @param urls a collection of TURN URLs to include verbatim in responses to clients
|
||||||
|
* @param urlsWithIps a collection of {@link String#format(String, Object...)} patterns to be populated with resolved IP
|
||||||
|
* addresses for {@link #hostname} in responses to clients; each pattern must include a single
|
||||||
|
* {@code %s} placeholder for the IP address
|
||||||
|
* @param circuitBreaker a circuit breaker for requests to Cloudflare
|
||||||
|
* @param retry a retry policy for requests to Cloudflare
|
||||||
|
* @param hostname the hostname to resolve to IP addresses for use with {@link #urlsWithIps}; also transmitted to
|
||||||
|
* clients for use as an SNI when connecting to pre-resolved hosts
|
||||||
|
* @param numHttpClients the number of parallel HTTP clients to use to communicate with Cloudflare
|
||||||
|
*/
|
||||||
public record CloudflareTurnConfiguration(@NotNull SecretString apiToken,
|
public record CloudflareTurnConfiguration(@NotNull SecretString apiToken,
|
||||||
@NotBlank String endpoint,
|
@NotBlank String endpoint,
|
||||||
@NotBlank long ttl,
|
@NotNull Duration requestedCredentialTtl,
|
||||||
|
@NotNull Duration clientCredentialTtl,
|
||||||
@NotNull @NotEmpty @Valid List<@NotBlank String> urls,
|
@NotNull @NotEmpty @Valid List<@NotBlank String> urls,
|
||||||
@NotNull @NotEmpty @Valid List<@NotBlank String> urlsWithIps,
|
@NotNull @NotEmpty @Valid List<@NotBlank String> urlsWithIps,
|
||||||
@NotNull @Valid CircuitBreakerConfiguration circuitBreaker,
|
@NotNull @Valid CircuitBreakerConfiguration circuitBreaker,
|
||||||
|
@ -35,4 +55,9 @@ public record CloudflareTurnConfiguration(@NotNull SecretString apiToken,
|
||||||
retry = new RetryConfiguration();
|
retry = new RetryConfiguration();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@AssertTrue
|
||||||
|
public boolean isClientTtlShorterThanRequestedTtl() {
|
||||||
|
return clientCredentialTtl.compareTo(requestedCredentialTtl) <= 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,16 +15,12 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
import jakarta.ws.rs.GET;
|
import jakarta.ws.rs.GET;
|
||||||
import jakarta.ws.rs.Path;
|
import jakarta.ws.rs.Path;
|
||||||
import jakarta.ws.rs.Produces;
|
import jakarta.ws.rs.Produces;
|
||||||
import jakarta.ws.rs.container.ContainerRequestContext;
|
|
||||||
import jakarta.ws.rs.core.Context;
|
|
||||||
import jakarta.ws.rs.core.MediaType;
|
import jakarta.ws.rs.core.MediaType;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedDevice;
|
import org.whispersystems.textsecuregcm.auth.AuthenticatedDevice;
|
||||||
import org.whispersystems.textsecuregcm.auth.CloudflareTurnCredentialsManager;
|
import org.whispersystems.textsecuregcm.auth.CloudflareTurnCredentialsManager;
|
||||||
import org.whispersystems.textsecuregcm.auth.TurnToken;
|
|
||||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||||
import org.whispersystems.websocket.auth.ReadOnly;
|
import org.whispersystems.websocket.auth.ReadOnly;
|
||||||
|
|
||||||
|
@ -32,14 +28,16 @@ import org.whispersystems.websocket.auth.ReadOnly;
|
||||||
@Path("/v2/calling")
|
@Path("/v2/calling")
|
||||||
public class CallRoutingControllerV2 {
|
public class CallRoutingControllerV2 {
|
||||||
|
|
||||||
private static final Counter CLOUDFLARE_TURN_ERROR_COUNTER = Metrics.counter(name(CallRoutingControllerV2.class, "cloudflareTurnError"));
|
|
||||||
private final RateLimiters rateLimiters;
|
private final RateLimiters rateLimiters;
|
||||||
private final CloudflareTurnCredentialsManager cloudflareTurnCredentialsManager;
|
private final CloudflareTurnCredentialsManager cloudflareTurnCredentialsManager;
|
||||||
|
|
||||||
|
private static final Counter CLOUDFLARE_TURN_ERROR_COUNTER =
|
||||||
|
Metrics.counter(name(CallRoutingControllerV2.class, "cloudflareTurnError"));
|
||||||
|
|
||||||
public CallRoutingControllerV2(
|
public CallRoutingControllerV2(
|
||||||
final RateLimiters rateLimiters,
|
final RateLimiters rateLimiters,
|
||||||
final CloudflareTurnCredentialsManager cloudflareTurnCredentialsManager
|
final CloudflareTurnCredentialsManager cloudflareTurnCredentialsManager) {
|
||||||
) {
|
|
||||||
this.rateLimiters = rateLimiters;
|
this.rateLimiters = rateLimiters;
|
||||||
this.cloudflareTurnCredentialsManager = cloudflareTurnCredentialsManager;
|
this.cloudflareTurnCredentialsManager = cloudflareTurnCredentialsManager;
|
||||||
}
|
}
|
||||||
|
@ -58,25 +56,17 @@ public class CallRoutingControllerV2 {
|
||||||
@ApiResponse(responseCode = "401", description = "Account authentication check failed.")
|
@ApiResponse(responseCode = "401", description = "Account authentication check failed.")
|
||||||
@ApiResponse(responseCode = "422", description = "Invalid request format.")
|
@ApiResponse(responseCode = "422", description = "Invalid request format.")
|
||||||
@ApiResponse(responseCode = "429", description = "Rate limited.")
|
@ApiResponse(responseCode = "429", description = "Rate limited.")
|
||||||
public GetCallingRelaysResponse getCallingRelays(
|
public GetCallingRelaysResponse getCallingRelays(final @ReadOnly @Auth AuthenticatedDevice auth)
|
||||||
final @ReadOnly @Auth AuthenticatedDevice auth
|
throws RateLimitExceededException, IOException {
|
||||||
) throws RateLimitExceededException, IOException {
|
|
||||||
UUID aci = auth.getAccount().getUuid();
|
final UUID aci = auth.getAccount().getUuid();
|
||||||
rateLimiters.getCallEndpointLimiter().validate(aci);
|
rateLimiters.getCallEndpointLimiter().validate(aci);
|
||||||
|
|
||||||
List<TurnToken> tokens = new ArrayList<>();
|
|
||||||
try {
|
try {
|
||||||
tokens.add(cloudflareTurnCredentialsManager.retrieveFromCloudflare());
|
return new GetCallingRelaysResponse(List.of(cloudflareTurnCredentialsManager.retrieveFromCloudflare()));
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
CallRoutingControllerV2.CLOUDFLARE_TURN_ERROR_COUNTER.increment();
|
CLOUDFLARE_TURN_ERROR_COUNTER.increment();
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new GetCallingRelaysResponse(tokens);
|
|
||||||
}
|
|
||||||
|
|
||||||
public record GetCallingRelaysResponse(
|
|
||||||
List<TurnToken> relays
|
|
||||||
) {
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -402,7 +402,7 @@ public class DeviceController {
|
||||||
|
|
||||||
private AtomicInteger getCounterForLinkedDeviceListeners(final String userAgent) {
|
private AtomicInteger getCounterForLinkedDeviceListeners(final String userAgent) {
|
||||||
try {
|
try {
|
||||||
return linkedDeviceListenersByPlatform.get(UserAgentUtil.parseUserAgentString(userAgent).getPlatform());
|
return linkedDeviceListenersByPlatform.get(UserAgentUtil.parseUserAgentString(userAgent).platform());
|
||||||
} catch (final UnrecognizedUserAgentException ignored) {
|
} catch (final UnrecognizedUserAgentException ignored) {
|
||||||
return linkedDeviceListenersForUnrecognizedPlatforms;
|
return linkedDeviceListenersForUnrecognizedPlatforms;
|
||||||
}
|
}
|
||||||
|
|
|
@ -97,21 +97,20 @@ public class DonationController {
|
||||||
return redeemedReceiptsManager.put(
|
return redeemedReceiptsManager.put(
|
||||||
receiptSerial, receiptExpiration.getEpochSecond(), receiptLevel, auth.getAccount().getUuid())
|
receiptSerial, receiptExpiration.getEpochSecond(), receiptLevel, auth.getAccount().getUuid())
|
||||||
.thenCompose(receiptMatched -> {
|
.thenCompose(receiptMatched -> {
|
||||||
if (!receiptMatched) {
|
if (!receiptMatched) {
|
||||||
return CompletableFuture.completedFuture(
|
|
||||||
Response.status(Status.BAD_REQUEST).entity("receipt serial is already redeemed")
|
|
||||||
.type(MediaType.TEXT_PLAIN_TYPE).build());
|
|
||||||
}
|
|
||||||
|
|
||||||
return accountsManager.getByAccountIdentifierAsync(auth.getAccount().getUuid())
|
return CompletableFuture.completedFuture(
|
||||||
.thenCompose(optionalAccount ->
|
Response.status(Status.BAD_REQUEST).entity("receipt serial is already redeemed")
|
||||||
optionalAccount.map(account -> accountsManager.updateAsync(account, a -> {
|
.type(MediaType.TEXT_PLAIN_TYPE).build());
|
||||||
|
}
|
||||||
|
|
||||||
|
return accountsManager.updateAsync(auth.getAccount(), a -> {
|
||||||
a.addBadge(clock, new AccountBadge(badgeId, receiptExpiration, request.isVisible()));
|
a.addBadge(clock, new AccountBadge(badgeId, receiptExpiration, request.isVisible()));
|
||||||
if (request.isPrimary()) {
|
if (request.isPrimary()) {
|
||||||
a.makeBadgePrimaryIfExists(clock, badgeId);
|
a.makeBadgePrimaryIfExists(clock, badgeId);
|
||||||
}
|
}
|
||||||
})).orElse(CompletableFuture.completedFuture(null)))
|
})
|
||||||
.thenApply(ignored -> Response.ok().build());
|
.thenApply(ignored -> Response.ok().build());
|
||||||
});
|
});
|
||||||
}).thenCompose(Function.identity());
|
}).thenCompose(Function.identity());
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.controllers;
|
||||||
|
|
||||||
|
import org.whispersystems.textsecuregcm.auth.TurnToken;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public record GetCallingRelaysResponse(List<TurnToken> relays) {
|
||||||
|
}
|
|
@ -436,11 +436,16 @@ public class MessageController {
|
||||||
final Map<Byte, Integer> registrationIdsByDeviceId = messages.messages().stream()
|
final Map<Byte, Integer> registrationIdsByDeviceId = messages.messages().stream()
|
||||||
.collect(Collectors.toMap(IncomingMessage::destinationDeviceId, IncomingMessage::destinationRegistrationId));
|
.collect(Collectors.toMap(IncomingMessage::destinationDeviceId, IncomingMessage::destinationRegistrationId));
|
||||||
|
|
||||||
|
final Optional<Byte> syncMessageSenderDeviceId = messageType == MessageType.SYNC
|
||||||
|
? Optional.ofNullable(sender).map(authenticatedDevice -> authenticatedDevice.getAuthenticatedDevice().getId())
|
||||||
|
: Optional.empty();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
messageSender.sendMessages(destination,
|
messageSender.sendMessages(destination,
|
||||||
destinationIdentifier,
|
destinationIdentifier,
|
||||||
messagesByDeviceId,
|
messagesByDeviceId,
|
||||||
registrationIdsByDeviceId,
|
registrationIdsByDeviceId,
|
||||||
|
syncMessageSenderDeviceId,
|
||||||
userAgent);
|
userAgent);
|
||||||
} catch (final MismatchedDevicesException e) {
|
} catch (final MismatchedDevicesException e) {
|
||||||
if (!e.getMismatchedDevices().staleDeviceIds().isEmpty()) {
|
if (!e.getMismatchedDevices().staleDeviceIds().isEmpty()) {
|
||||||
|
|
|
@ -428,7 +428,7 @@ public class OneTimeDonationController {
|
||||||
@Nullable
|
@Nullable
|
||||||
private static ClientPlatform getClientPlatform(@Nullable final String userAgentString) {
|
private static ClientPlatform getClientPlatform(@Nullable final String userAgentString) {
|
||||||
try {
|
try {
|
||||||
return UserAgentUtil.parseUserAgentString(userAgentString).getPlatform();
|
return UserAgentUtil.parseUserAgentString(userAgentString).platform();
|
||||||
} catch (final UnrecognizedUserAgentException e) {
|
} catch (final UnrecognizedUserAgentException e) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -204,11 +204,12 @@ public class ProfileController {
|
||||||
.build()));
|
.build()));
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<AccountBadge> updatedBadges = request.badges()
|
|
||||||
.map(badges -> ProfileHelper.mergeBadgeIdsWithExistingAccountBadges(clock, badgeConfigurationMap, badges, auth.getAccount().getBadges()))
|
|
||||||
.orElseGet(() -> auth.getAccount().getBadges());
|
|
||||||
|
|
||||||
accountsManager.update(auth.getAccount(), a -> {
|
accountsManager.update(auth.getAccount(), a -> {
|
||||||
|
|
||||||
|
final List<AccountBadge> updatedBadges = request.badges()
|
||||||
|
.map(badges -> ProfileHelper.mergeBadgeIdsWithExistingAccountBadges(clock, badgeConfigurationMap, badges, a.getBadges()))
|
||||||
|
.orElseGet(a::getBadges);
|
||||||
|
|
||||||
a.setBadges(clock, updatedBadges);
|
a.setBadges(clock, updatedBadges);
|
||||||
a.setCurrentProfileVersion(request.version());
|
a.setCurrentProfileVersion(request.version());
|
||||||
});
|
});
|
||||||
|
|
|
@ -755,7 +755,7 @@ public class SubscriptionController {
|
||||||
@Nullable
|
@Nullable
|
||||||
private static ClientPlatform getClientPlatform(@Nullable final String userAgentString) {
|
private static ClientPlatform getClientPlatform(@Nullable final String userAgentString) {
|
||||||
try {
|
try {
|
||||||
return UserAgentUtil.parseUserAgentString(userAgentString).getPlatform();
|
return UserAgentUtil.parseUserAgentString(userAgentString).platform();
|
||||||
} catch (final UnrecognizedUserAgentException e) {
|
} catch (final UnrecognizedUserAgentException e) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,7 +81,16 @@ public class RemoteDeprecationFilter implements Filter, ServerInterceptor {
|
||||||
final Metadata headers,
|
final Metadata headers,
|
||||||
final ServerCallHandler<ReqT, RespT> next) {
|
final ServerCallHandler<ReqT, RespT> next) {
|
||||||
|
|
||||||
if (shouldBlock(RequestAttributesUtil.getUserAgent().orElse(null))) {
|
@Nullable final UserAgent userAgent = RequestAttributesUtil.getUserAgent()
|
||||||
|
.map(userAgentString -> {
|
||||||
|
try {
|
||||||
|
return UserAgentUtil.parseUserAgentString(userAgentString);
|
||||||
|
} catch (final UnrecognizedUserAgentException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}).orElse(null);
|
||||||
|
|
||||||
|
if (shouldBlock(userAgent)) {
|
||||||
call.close(StatusConstants.UPGRADE_NEEDED_STATUS, new Metadata());
|
call.close(StatusConstants.UPGRADE_NEEDED_STATUS, new Metadata());
|
||||||
return new ServerCall.Listener<>() {};
|
return new ServerCall.Listener<>() {};
|
||||||
} else {
|
} else {
|
||||||
|
@ -108,28 +117,28 @@ public class RemoteDeprecationFilter implements Filter, ServerInterceptor {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (blockedVersionsByPlatform.containsKey(userAgent.getPlatform())) {
|
if (blockedVersionsByPlatform.containsKey(userAgent.platform())) {
|
||||||
if (blockedVersionsByPlatform.get(userAgent.getPlatform()).contains(userAgent.getVersion())) {
|
if (blockedVersionsByPlatform.get(userAgent.platform()).contains(userAgent.version())) {
|
||||||
recordDeprecation(userAgent, BLOCKED_CLIENT_REASON);
|
recordDeprecation(userAgent, BLOCKED_CLIENT_REASON);
|
||||||
shouldBlock = true;
|
shouldBlock = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (minimumVersionsByPlatform.containsKey(userAgent.getPlatform())) {
|
if (minimumVersionsByPlatform.containsKey(userAgent.platform())) {
|
||||||
if (userAgent.getVersion().isLowerThan(minimumVersionsByPlatform.get(userAgent.getPlatform()))) {
|
if (userAgent.version().isLowerThan(minimumVersionsByPlatform.get(userAgent.platform()))) {
|
||||||
recordDeprecation(userAgent, EXPIRED_CLIENT_REASON);
|
recordDeprecation(userAgent, EXPIRED_CLIENT_REASON);
|
||||||
shouldBlock = true;
|
shouldBlock = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (versionsPendingBlockByPlatform.containsKey(userAgent.getPlatform())) {
|
if (versionsPendingBlockByPlatform.containsKey(userAgent.platform())) {
|
||||||
if (versionsPendingBlockByPlatform.get(userAgent.getPlatform()).contains(userAgent.getVersion())) {
|
if (versionsPendingBlockByPlatform.get(userAgent.platform()).contains(userAgent.version())) {
|
||||||
recordPendingDeprecation(userAgent, BLOCKED_CLIENT_REASON);
|
recordPendingDeprecation(userAgent, BLOCKED_CLIENT_REASON);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (versionsPendingDeprecationByPlatform.containsKey(userAgent.getPlatform())) {
|
if (versionsPendingDeprecationByPlatform.containsKey(userAgent.platform())) {
|
||||||
if (userAgent.getVersion().isLowerThan(versionsPendingDeprecationByPlatform.get(userAgent.getPlatform()))) {
|
if (userAgent.version().isLowerThan(versionsPendingDeprecationByPlatform.get(userAgent.platform()))) {
|
||||||
recordPendingDeprecation(userAgent, EXPIRED_CLIENT_REASON);
|
recordPendingDeprecation(userAgent, EXPIRED_CLIENT_REASON);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -139,13 +148,13 @@ public class RemoteDeprecationFilter implements Filter, ServerInterceptor {
|
||||||
|
|
||||||
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.platform().name().toLowerCase() : "unrecognized",
|
||||||
REASON_TAG_NAME, reason).increment();
|
REASON_TAG_NAME, reason).increment();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void recordPendingDeprecation(final UserAgent userAgent, final String reason) {
|
private void recordPendingDeprecation(final UserAgent userAgent, final String reason) {
|
||||||
Metrics.counter(PENDING_DEPRECATION_COUNTER_NAME,
|
Metrics.counter(PENDING_DEPRECATION_COUNTER_NAME,
|
||||||
PLATFORM_TAG, userAgent.getPlatform().name().toLowerCase(),
|
PLATFORM_TAG, userAgent.platform().name().toLowerCase(),
|
||||||
REASON_TAG_NAME, reason).increment();
|
REASON_TAG_NAME, reason).increment();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,8 +15,6 @@ import jakarta.ws.rs.container.ContainerRequestFilter;
|
||||||
import jakarta.ws.rs.core.SecurityContext;
|
import jakarta.ws.rs.core.SecurityContext;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.Set;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedDevice;
|
import org.whispersystems.textsecuregcm.auth.AuthenticatedDevice;
|
||||||
|
@ -70,8 +68,8 @@ public class RestDeprecationFilter implements ContainerRequestFilter {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final UserAgent userAgent = UserAgentUtil.parseUserAgentString(userAgentString);
|
final UserAgent userAgent = UserAgentUtil.parseUserAgentString(userAgentString);
|
||||||
final ClientPlatform platform = userAgent.getPlatform();
|
final ClientPlatform platform = userAgent.platform();
|
||||||
final Semver version = userAgent.getVersion();
|
final Semver version = userAgent.version();
|
||||||
if (!minimumRestFreeVersion.containsKey(platform)) {
|
if (!minimumRestFreeVersion.containsKey(platform)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.grpc;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that a remote channel was not found for a given server call or remote address.
|
||||||
|
*/
|
||||||
|
public class ChannelNotFoundException extends Exception {
|
||||||
|
}
|
|
@ -187,7 +187,8 @@ public class MessagesAnonymousGrpcService extends SimpleMessagesAnonymousGrpc.Me
|
||||||
destination,
|
destination,
|
||||||
destinationServiceIdentifier,
|
destinationServiceIdentifier,
|
||||||
messagesByDeviceId,
|
messagesByDeviceId,
|
||||||
registrationIdsByDeviceId);
|
registrationIdsByDeviceId,
|
||||||
|
Optional.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -252,7 +253,7 @@ public class MessagesAnonymousGrpcService extends SimpleMessagesAnonymousGrpc.Me
|
||||||
story,
|
story,
|
||||||
ephemeral,
|
ephemeral,
|
||||||
urgent,
|
urgent,
|
||||||
RequestAttributesUtil.getRawUserAgent().orElse(null));
|
RequestAttributesUtil.getUserAgent().orElse(null));
|
||||||
|
|
||||||
final SendMultiRecipientMessageResponse.Builder responseBuilder = SendMultiRecipientMessageResponse.newBuilder();
|
final SendMultiRecipientMessageResponse.Builder responseBuilder = SendMultiRecipientMessageResponse.newBuilder();
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ package org.whispersystems.textsecuregcm.grpc;
|
||||||
import io.grpc.Status;
|
import io.grpc.Status;
|
||||||
import io.grpc.StatusException;
|
import io.grpc.StatusException;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
import org.signal.chat.messages.MismatchedDevices;
|
import org.signal.chat.messages.MismatchedDevices;
|
||||||
import org.signal.chat.messages.SendMessageResponse;
|
import org.signal.chat.messages.SendMessageResponse;
|
||||||
import org.whispersystems.textsecuregcm.controllers.MismatchedDevicesException;
|
import org.whispersystems.textsecuregcm.controllers.MismatchedDevicesException;
|
||||||
|
@ -31,6 +32,8 @@ public class MessagesGrpcHelper {
|
||||||
* @param destinationServiceIdentifier the service identifier for the destination account
|
* @param destinationServiceIdentifier the service identifier for the destination account
|
||||||
* @param messagesByDeviceId a map of device IDs to message payloads
|
* @param messagesByDeviceId a map of device IDs to message payloads
|
||||||
* @param registrationIdsByDeviceId a map of device IDs to device registration IDs
|
* @param registrationIdsByDeviceId a map of device IDs to device registration IDs
|
||||||
|
* @param syncMessageSenderDeviceId if the message is a sync message (i.e. a message to other devices linked to the
|
||||||
|
* caller's own account), contains the ID of the device that sent the message
|
||||||
*
|
*
|
||||||
* @return a response object to send to callers
|
* @return a response object to send to callers
|
||||||
*
|
*
|
||||||
|
@ -42,14 +45,17 @@ public class MessagesGrpcHelper {
|
||||||
final Account destination,
|
final Account destination,
|
||||||
final ServiceIdentifier destinationServiceIdentifier,
|
final ServiceIdentifier destinationServiceIdentifier,
|
||||||
final Map<Byte, MessageProtos.Envelope> messagesByDeviceId,
|
final Map<Byte, MessageProtos.Envelope> messagesByDeviceId,
|
||||||
final Map<Byte, Integer> registrationIdsByDeviceId) throws StatusException, RateLimitExceededException {
|
final Map<Byte, Integer> registrationIdsByDeviceId,
|
||||||
|
@SuppressWarnings("OptionalUsedAsFieldOrParameterType") final Optional<Byte> syncMessageSenderDeviceId)
|
||||||
|
throws StatusException, RateLimitExceededException {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
messageSender.sendMessages(destination,
|
messageSender.sendMessages(destination,
|
||||||
destinationServiceIdentifier,
|
destinationServiceIdentifier,
|
||||||
messagesByDeviceId,
|
messagesByDeviceId,
|
||||||
registrationIdsByDeviceId,
|
registrationIdsByDeviceId,
|
||||||
RequestAttributesUtil.getRawUserAgent().orElse(null));
|
syncMessageSenderDeviceId,
|
||||||
|
RequestAttributesUtil.getUserAgent().orElse(null));
|
||||||
|
|
||||||
return SEND_MESSAGE_SUCCESS_RESPONSE;
|
return SEND_MESSAGE_SUCCESS_RESPONSE;
|
||||||
} catch (final MismatchedDevicesException e) {
|
} catch (final MismatchedDevicesException e) {
|
||||||
|
|
|
@ -172,7 +172,8 @@ public class MessagesGrpcService extends SimpleMessagesGrpc.MessagesImplBase {
|
||||||
destination,
|
destination,
|
||||||
destinationServiceIdentifier,
|
destinationServiceIdentifier,
|
||||||
messagesByDeviceId,
|
messagesByDeviceId,
|
||||||
registrationIdsByDeviceId);
|
registrationIdsByDeviceId,
|
||||||
|
messageType == MessageType.SYNC ? Optional.of(sender.deviceId()) : Optional.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static MessageProtos.Envelope.Type getEnvelopeType(final AuthenticatedSenderMessageType type) {
|
private static MessageProtos.Envelope.Type getEnvelopeType(final AuthenticatedSenderMessageType type) {
|
||||||
|
|
|
@ -145,11 +145,13 @@ public class ProfileGrpcService extends ReactorProfileGrpc.ProfileImplBase {
|
||||||
request.getCommitment().toByteArray())));
|
request.getCommitment().toByteArray())));
|
||||||
|
|
||||||
final List<Mono<?>> updates = new ArrayList<>(2);
|
final List<Mono<?>> updates = new ArrayList<>(2);
|
||||||
final List<AccountBadge> updatedBadges = Optional.of(request.getBadgeIdsList())
|
|
||||||
.map(badges -> ProfileHelper.mergeBadgeIdsWithExistingAccountBadges(clock, badgeConfigurationMap, badges, account.getBadges()))
|
|
||||||
.orElseGet(account::getBadges);
|
|
||||||
|
|
||||||
updates.add(Mono.fromFuture(() -> accountsManager.updateAsync(account, a -> {
|
updates.add(Mono.fromFuture(() -> accountsManager.updateAsync(account, a -> {
|
||||||
|
|
||||||
|
final List<AccountBadge> updatedBadges = Optional.of(request.getBadgeIdsList())
|
||||||
|
.map(badges -> ProfileHelper.mergeBadgeIdsWithExistingAccountBadges(clock, badgeConfigurationMap, badges, a.getBadges()))
|
||||||
|
.orElseGet(a::getBadges);
|
||||||
|
|
||||||
a.setBadges(clock, updatedBadges);
|
a.setBadges(clock, updatedBadges);
|
||||||
a.setCurrentProfileVersion(request.getVersion());
|
a.setCurrentProfileVersion(request.getVersion());
|
||||||
})));
|
})));
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.grpc;
|
||||||
|
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
public record RequestAttributes(InetAddress remoteAddress,
|
||||||
|
@Nullable String userAgent,
|
||||||
|
List<Locale.LanguageRange> acceptLanguage) {
|
||||||
|
}
|
|
@ -2,28 +2,25 @@ package org.whispersystems.textsecuregcm.grpc;
|
||||||
|
|
||||||
import io.grpc.Context;
|
import io.grpc.Context;
|
||||||
import io.grpc.Contexts;
|
import io.grpc.Contexts;
|
||||||
import io.grpc.Grpc;
|
|
||||||
import io.grpc.Metadata;
|
import io.grpc.Metadata;
|
||||||
import io.grpc.ServerCall;
|
import io.grpc.ServerCall;
|
||||||
import io.grpc.ServerCallHandler;
|
import io.grpc.ServerCallHandler;
|
||||||
import io.grpc.ServerInterceptor;
|
import io.grpc.ServerInterceptor;
|
||||||
import io.grpc.Status;
|
import io.grpc.Status;
|
||||||
import io.netty.channel.local.LocalAddress;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.whispersystems.textsecuregcm.grpc.net.GrpcClientConnectionManager;
|
import org.whispersystems.textsecuregcm.grpc.net.GrpcClientConnectionManager;
|
||||||
import org.whispersystems.textsecuregcm.util.ua.UserAgent;
|
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The request attributes interceptor makes request attributes from the underlying remote channel available to service
|
||||||
|
* implementations by attaching them to a {@link Context} attribute that can be read via {@link RequestAttributesUtil}.
|
||||||
|
* All server calls should have request attributes, and calls will be rejected with a status of {@code UNAVAILABLE} if
|
||||||
|
* request attributes are unavailable (i.e. the underlying channel closed before the {@code ServerCall} started).
|
||||||
|
*
|
||||||
|
* @see RequestAttributesUtil
|
||||||
|
*/
|
||||||
public class RequestAttributesInterceptor implements ServerInterceptor {
|
public class RequestAttributesInterceptor implements ServerInterceptor {
|
||||||
|
|
||||||
private final GrpcClientConnectionManager grpcClientConnectionManager;
|
private final GrpcClientConnectionManager grpcClientConnectionManager;
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(RequestAttributesInterceptor.class);
|
|
||||||
|
|
||||||
public RequestAttributesInterceptor(final GrpcClientConnectionManager grpcClientConnectionManager) {
|
public RequestAttributesInterceptor(final GrpcClientConnectionManager grpcClientConnectionManager) {
|
||||||
this.grpcClientConnectionManager = grpcClientConnectionManager;
|
this.grpcClientConnectionManager = grpcClientConnectionManager;
|
||||||
}
|
}
|
||||||
|
@ -33,52 +30,12 @@ public class RequestAttributesInterceptor implements ServerInterceptor {
|
||||||
final Metadata headers,
|
final Metadata headers,
|
||||||
final ServerCallHandler<ReqT, RespT> next) {
|
final ServerCallHandler<ReqT, RespT> next) {
|
||||||
|
|
||||||
if (call.getAttributes().get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR) instanceof LocalAddress localAddress) {
|
try {
|
||||||
Context context = Context.current();
|
return Contexts.interceptCall(Context.current()
|
||||||
|
.withValue(RequestAttributesUtil.REQUEST_ATTRIBUTES_CONTEXT_KEY,
|
||||||
{
|
grpcClientConnectionManager.getRequestAttributes(call)), call, headers, next);
|
||||||
final Optional<InetAddress> maybeRemoteAddress = grpcClientConnectionManager.getRemoteAddress(localAddress);
|
} catch (final ChannelNotFoundException e) {
|
||||||
|
return ServerInterceptorUtil.closeWithStatus(call, Status.UNAVAILABLE);
|
||||||
if (maybeRemoteAddress.isEmpty()) {
|
|
||||||
// We should never have a call from a party whose remote address we can't identify
|
|
||||||
log.warn("No remote address available");
|
|
||||||
|
|
||||||
call.close(Status.INTERNAL, new Metadata());
|
|
||||||
return new ServerCall.Listener<>() {};
|
|
||||||
}
|
|
||||||
|
|
||||||
context = context.withValue(RequestAttributesUtil.REMOTE_ADDRESS_CONTEXT_KEY, maybeRemoteAddress.get());
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
final Optional<List<Locale.LanguageRange>> maybeAcceptLanguage =
|
|
||||||
grpcClientConnectionManager.getAcceptableLanguages(localAddress);
|
|
||||||
|
|
||||||
if (maybeAcceptLanguage.isPresent()) {
|
|
||||||
context = context.withValue(RequestAttributesUtil.ACCEPT_LANGUAGE_CONTEXT_KEY, maybeAcceptLanguage.get());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
final Optional<String> maybeRawUserAgent =
|
|
||||||
grpcClientConnectionManager.getRawUserAgent(localAddress);
|
|
||||||
|
|
||||||
if (maybeRawUserAgent.isPresent()) {
|
|
||||||
context = context.withValue(RequestAttributesUtil.RAW_USER_AGENT_CONTEXT_KEY, maybeRawUserAgent.get());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
final Optional<UserAgent> maybeUserAgent = grpcClientConnectionManager.getUserAgent(localAddress);
|
|
||||||
|
|
||||||
if (maybeUserAgent.isPresent()) {
|
|
||||||
context = context.withValue(RequestAttributesUtil.USER_AGENT_CONTEXT_KEY, maybeUserAgent.get());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Contexts.interceptCall(context, call, headers, next);
|
|
||||||
} else {
|
|
||||||
throw new AssertionError("Unexpected channel type: " + call.getAttributes().get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,18 +3,13 @@ package org.whispersystems.textsecuregcm.grpc;
|
||||||
import io.grpc.Context;
|
import io.grpc.Context;
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import org.whispersystems.textsecuregcm.util.ua.UserAgent;
|
|
||||||
|
|
||||||
public class RequestAttributesUtil {
|
public class RequestAttributesUtil {
|
||||||
|
|
||||||
static final Context.Key<List<Locale.LanguageRange>> ACCEPT_LANGUAGE_CONTEXT_KEY = Context.key("accept-language");
|
static final Context.Key<RequestAttributes> REQUEST_ATTRIBUTES_CONTEXT_KEY = Context.key("request-attributes");
|
||||||
static final Context.Key<InetAddress> REMOTE_ADDRESS_CONTEXT_KEY = Context.key("remote-address");
|
|
||||||
static final Context.Key<String> RAW_USER_AGENT_CONTEXT_KEY = Context.key("unparsed-user-agent");
|
|
||||||
static final Context.Key<UserAgent> USER_AGENT_CONTEXT_KEY = Context.key("parsed-user-agent");
|
|
||||||
|
|
||||||
private static final List<Locale> AVAILABLE_LOCALES = Arrays.asList(Locale.getAvailableLocales());
|
private static final List<Locale> AVAILABLE_LOCALES = Arrays.asList(Locale.getAvailableLocales());
|
||||||
|
|
||||||
|
@ -23,8 +18,8 @@ public class RequestAttributesUtil {
|
||||||
*
|
*
|
||||||
* @return the acceptable languages listed by the remote client; may be empty if unparseable or not specified
|
* @return the acceptable languages listed by the remote client; may be empty if unparseable or not specified
|
||||||
*/
|
*/
|
||||||
public static Optional<List<Locale.LanguageRange>> getAcceptableLanguages() {
|
public static List<Locale.LanguageRange> getAcceptableLanguages() {
|
||||||
return Optional.ofNullable(ACCEPT_LANGUAGE_CONTEXT_KEY.get());
|
return REQUEST_ATTRIBUTES_CONTEXT_KEY.get().acceptLanguage();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -35,9 +30,7 @@ public class RequestAttributesUtil {
|
||||||
* @return a list of distinct locales acceptable to the remote client and available in this JVM
|
* @return a list of distinct locales acceptable to the remote client and available in this JVM
|
||||||
*/
|
*/
|
||||||
public static List<Locale> getAvailableAcceptedLocales() {
|
public static List<Locale> getAvailableAcceptedLocales() {
|
||||||
return getAcceptableLanguages()
|
return Locale.filter(getAcceptableLanguages(), AVAILABLE_LOCALES);
|
||||||
.map(languageRanges -> Locale.filter(languageRanges, AVAILABLE_LOCALES))
|
|
||||||
.orElseGet(Collections::emptyList);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -46,16 +39,7 @@ public class RequestAttributesUtil {
|
||||||
* @return the remote address of the remote client
|
* @return the remote address of the remote client
|
||||||
*/
|
*/
|
||||||
public static InetAddress getRemoteAddress() {
|
public static InetAddress getRemoteAddress() {
|
||||||
return REMOTE_ADDRESS_CONTEXT_KEY.get();
|
return REQUEST_ATTRIBUTES_CONTEXT_KEY.get().remoteAddress();
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the parsed user-agent of the remote client in the current gRPC request context.
|
|
||||||
*
|
|
||||||
* @return the parsed user-agent of the remote client; may be empty if unparseable or not specified
|
|
||||||
*/
|
|
||||||
public static Optional<UserAgent> getUserAgent() {
|
|
||||||
return Optional.ofNullable(USER_AGENT_CONTEXT_KEY.get());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -63,7 +47,7 @@ public class RequestAttributesUtil {
|
||||||
*
|
*
|
||||||
* @return the unparsed user-agent of the remote client; may be empty if not specified
|
* @return the unparsed user-agent of the remote client; may be empty if not specified
|
||||||
*/
|
*/
|
||||||
public static Optional<String> getRawUserAgent() {
|
public static Optional<String> getUserAgent() {
|
||||||
return Optional.ofNullable(RAW_USER_AGENT_CONTEXT_KEY.get());
|
return Optional.ofNullable(REQUEST_ATTRIBUTES_CONTEXT_KEY.get().userAgent());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.grpc;
|
||||||
|
|
||||||
|
import io.grpc.Metadata;
|
||||||
|
import io.grpc.ServerCall;
|
||||||
|
import io.grpc.Status;
|
||||||
|
|
||||||
|
public class ServerInterceptorUtil {
|
||||||
|
|
||||||
|
@SuppressWarnings("rawtypes")
|
||||||
|
private static final ServerCall.Listener NO_OP_LISTENER = new ServerCall.Listener<>() {};
|
||||||
|
|
||||||
|
private static final Metadata EMPTY_TRAILERS = new Metadata();
|
||||||
|
|
||||||
|
private ServerInterceptorUtil() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the given server call with the given status, returning a no-op listener.
|
||||||
|
*
|
||||||
|
* @param call the server call to close
|
||||||
|
* @param status the status with which to close the call
|
||||||
|
*
|
||||||
|
* @return a no-op server call listener
|
||||||
|
*
|
||||||
|
* @param <ReqT> the type of request object handled by the server call
|
||||||
|
* @param <RespT> the type of response object returned by the server call
|
||||||
|
*/
|
||||||
|
public static <ReqT, RespT> ServerCall.Listener<ReqT> closeWithStatus(final ServerCall<ReqT, RespT> call, final Status status) {
|
||||||
|
call.close(status, EMPTY_TRAILERS);
|
||||||
|
|
||||||
|
//noinspection unchecked
|
||||||
|
return NO_OP_LISTENER;
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,7 +8,7 @@ package org.whispersystems.textsecuregcm.grpc;
|
||||||
import static org.whispersystems.textsecuregcm.grpc.validators.ValidatorUtils.internalError;
|
import static org.whispersystems.textsecuregcm.grpc.validators.ValidatorUtils.internalError;
|
||||||
|
|
||||||
import com.google.protobuf.Descriptors;
|
import com.google.protobuf.Descriptors;
|
||||||
import com.google.protobuf.GeneratedMessageV3;
|
import com.google.protobuf.Message;
|
||||||
import io.grpc.ForwardingServerCallListener;
|
import io.grpc.ForwardingServerCallListener;
|
||||||
import io.grpc.Metadata;
|
import io.grpc.Metadata;
|
||||||
import io.grpc.ServerCall;
|
import io.grpc.ServerCall;
|
||||||
|
@ -75,7 +75,7 @@ public class ValidatingInterceptor implements ServerInterceptor {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validateMessage(final Object message) throws StatusException {
|
private void validateMessage(final Object message) throws StatusException {
|
||||||
if (message instanceof GeneratedMessageV3 msg) {
|
if (message instanceof Message msg) {
|
||||||
try {
|
try {
|
||||||
for (final Descriptors.FieldDescriptor fd: msg.getDescriptorForType().getFields()) {
|
for (final Descriptors.FieldDescriptor fd: msg.getDescriptorForType().getFields()) {
|
||||||
for (final Map.Entry<Descriptors.FieldDescriptor, Object> entry: fd.getOptions().getAllFields().entrySet()) {
|
for (final Map.Entry<Descriptors.FieldDescriptor, Object> entry: fd.getOptions().getAllFields().entrySet()) {
|
||||||
|
|
|
@ -12,8 +12,10 @@ import io.netty.handler.codec.http.websocketx.WebSocketCloseStatus;
|
||||||
import io.netty.util.ReferenceCountUtil;
|
import io.netty.util.ReferenceCountUtil;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.whispersystems.textsecuregcm.auth.grpc.AuthenticatedDevice;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An "establish local connection" handler waits for a Noise handshake to complete upstream in the pipeline, buffering
|
* An "establish local connection" handler waits for a Noise handshake to complete upstream in the pipeline, buffering
|
||||||
|
@ -48,12 +50,12 @@ class EstablishLocalGrpcConnectionHandler extends ChannelInboundHandlerAdapter {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void userEventTriggered(final ChannelHandlerContext remoteChannelContext, final Object event) {
|
public void userEventTriggered(final ChannelHandlerContext remoteChannelContext, final Object event) {
|
||||||
if (event instanceof NoiseIdentityDeterminedEvent noiseIdentityDeterminedEvent) {
|
if (event instanceof NoiseIdentityDeterminedEvent(final Optional<AuthenticatedDevice> authenticatedDevice)) {
|
||||||
// We assume that we'll only get a completed handshake event if the handshake met all authentication requirements
|
// We assume that we'll only get a completed handshake event if the handshake met all authentication requirements
|
||||||
// for the requested service. If the handshake doesn't have an authenticated device, we assume we're trying to
|
// for the requested service. If the handshake doesn't have an authenticated device, we assume we're trying to
|
||||||
// connect to the anonymous service. If it does have an authenticated device, we assume we're aiming for the
|
// connect to the anonymous service. If it does have an authenticated device, we assume we're aiming for the
|
||||||
// authenticated service.
|
// authenticated service.
|
||||||
final LocalAddress grpcServerAddress = noiseIdentityDeterminedEvent.authenticatedDevice().isPresent()
|
final LocalAddress grpcServerAddress = authenticatedDevice.isPresent()
|
||||||
? authenticatedGrpcServerAddress
|
? authenticatedGrpcServerAddress
|
||||||
: anonymousGrpcServerAddress;
|
: anonymousGrpcServerAddress;
|
||||||
|
|
||||||
|
@ -72,7 +74,7 @@ class EstablishLocalGrpcConnectionHandler extends ChannelInboundHandlerAdapter {
|
||||||
if (localChannelFuture.isSuccess()) {
|
if (localChannelFuture.isSuccess()) {
|
||||||
grpcClientConnectionManager.handleConnectionEstablished((LocalChannel) localChannelFuture.channel(),
|
grpcClientConnectionManager.handleConnectionEstablished((LocalChannel) localChannelFuture.channel(),
|
||||||
remoteChannelContext.channel(),
|
remoteChannelContext.channel(),
|
||||||
noiseIdentityDeterminedEvent.authenticatedDevice());
|
authenticatedDevice);
|
||||||
|
|
||||||
// Close the local connection if the remote channel closes and vice versa
|
// Close the local connection if the remote channel closes and vice versa
|
||||||
remoteChannelContext.channel().closeFuture().addListener(closeFuture -> localChannelFuture.channel().close());
|
remoteChannelContext.channel().closeFuture().addListener(closeFuture -> localChannelFuture.channel().close());
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package org.whispersystems.textsecuregcm.grpc.net;
|
package org.whispersystems.textsecuregcm.grpc.net;
|
||||||
|
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import io.grpc.Grpc;
|
||||||
|
import io.grpc.ServerCall;
|
||||||
import io.netty.channel.Channel;
|
import io.netty.channel.Channel;
|
||||||
import io.netty.channel.ChannelFutureListener;
|
import io.netty.channel.ChannelFutureListener;
|
||||||
import io.netty.channel.local.LocalAddress;
|
import io.netty.channel.local.LocalAddress;
|
||||||
|
@ -23,15 +25,25 @@ import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.textsecuregcm.auth.DisconnectionRequestListener;
|
import org.whispersystems.textsecuregcm.auth.DisconnectionRequestListener;
|
||||||
import org.whispersystems.textsecuregcm.auth.grpc.AuthenticatedDevice;
|
import org.whispersystems.textsecuregcm.auth.grpc.AuthenticatedDevice;
|
||||||
import org.whispersystems.textsecuregcm.util.ua.UnrecognizedUserAgentException;
|
import org.whispersystems.textsecuregcm.grpc.ChannelNotFoundException;
|
||||||
import org.whispersystems.textsecuregcm.util.ua.UserAgent;
|
import org.whispersystems.textsecuregcm.grpc.RequestAttributes;
|
||||||
import org.whispersystems.textsecuregcm.util.ua.UserAgentUtil;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A client connection manager associates a local connection to a local gRPC server with a remote connection through a
|
* A client connection manager associates a local connection to a local gRPC server with a remote connection through a
|
||||||
* Noise-over-WebSocket tunnel. It provides access to metadata associated with the remote connection, including the
|
* Noise tunnel. It provides access to metadata associated with the remote connection, including the authenticated
|
||||||
* authenticated identity of the device that opened the connection (for non-anonymous connections). It can also close
|
* identity of the device that opened the connection (for non-anonymous connections). It can also close connections
|
||||||
* connections associated with a given device if that device's credentials have changed and clients must reauthenticate.
|
* associated with a given device if that device's credentials have changed and clients must reauthenticate.
|
||||||
|
* <p>
|
||||||
|
* In general, all {@link ServerCall}s <em>must</em> have a local address that in turn <em>should</em> be resolvable to
|
||||||
|
* a remote channel, which <em>must</em> have associated request attributes and authentication status. It is possible
|
||||||
|
* that a server call's local address may not be resolvable to a remote channel if the remote channel closed in the
|
||||||
|
* narrow window between a server call being created and the start of call execution, in which case accessor methods
|
||||||
|
* in this class will throw a {@link ChannelNotFoundException}.
|
||||||
|
* <p>
|
||||||
|
* A gRPC client connection manager's methods for getting request attributes accept {@link ServerCall} entities to
|
||||||
|
* identify connections. In general, these methods should only be called from {@link io.grpc.ServerInterceptor}s.
|
||||||
|
* Methods for requesting connection closure accept an {@link AuthenticatedDevice} to identify the connection and may
|
||||||
|
* be called from any application code.
|
||||||
*/
|
*/
|
||||||
public class GrpcClientConnectionManager implements DisconnectionRequestListener {
|
public class GrpcClientConnectionManager implements DisconnectionRequestListener {
|
||||||
|
|
||||||
|
@ -43,94 +55,56 @@ public class GrpcClientConnectionManager implements DisconnectionRequestListener
|
||||||
AttributeKey.valueOf(GrpcClientConnectionManager.class, "authenticatedDevice");
|
AttributeKey.valueOf(GrpcClientConnectionManager.class, "authenticatedDevice");
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
static final AttributeKey<InetAddress> REMOTE_ADDRESS_ATTRIBUTE_KEY =
|
public static final AttributeKey<RequestAttributes> REQUEST_ATTRIBUTES_KEY =
|
||||||
AttributeKey.valueOf(WebsocketHandshakeCompleteHandler.class, "remoteAddress");
|
AttributeKey.valueOf(GrpcClientConnectionManager.class, "requestAttributes");
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
static final AttributeKey<String> RAW_USER_AGENT_ATTRIBUTE_KEY =
|
|
||||||
AttributeKey.valueOf(WebsocketHandshakeCompleteHandler.class, "rawUserAgent");
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
static final AttributeKey<UserAgent> PARSED_USER_AGENT_ATTRIBUTE_KEY =
|
|
||||||
AttributeKey.valueOf(WebsocketHandshakeCompleteHandler.class, "userAgent");
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
static final AttributeKey<List<Locale.LanguageRange>> ACCEPT_LANGUAGE_ATTRIBUTE_KEY =
|
|
||||||
AttributeKey.valueOf(WebsocketHandshakeCompleteHandler.class, "acceptLanguage");
|
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(GrpcClientConnectionManager.class);
|
private static final Logger log = LoggerFactory.getLogger(GrpcClientConnectionManager.class);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the authenticated device associated with the given local address, if any. An authenticated device is
|
* Returns the authenticated device associated with the given server call, if any. If the connection is anonymous
|
||||||
* available if and only if the given local address maps to an active local connection and that connection is
|
* (i.e. unauthenticated), the returned value will be empty.
|
||||||
* authenticated (i.e. not anonymous).
|
|
||||||
*
|
*
|
||||||
* @param localAddress the local address for which to find an authenticated device
|
* @param serverCall the gRPC server call for which to find an authenticated device
|
||||||
*
|
*
|
||||||
* @return the authenticated device associated with the given local address, if any
|
* @return the authenticated device associated with the given local address, if any
|
||||||
|
*
|
||||||
|
* @throws ChannelNotFoundException if the server call is not associated with a known channel; in practice, this
|
||||||
|
* generally indicates that the channel has closed while request processing is still in progress
|
||||||
*/
|
*/
|
||||||
public Optional<AuthenticatedDevice> getAuthenticatedDevice(final LocalAddress localAddress) {
|
public Optional<AuthenticatedDevice> getAuthenticatedDevice(final ServerCall<?, ?> serverCall)
|
||||||
return getAuthenticatedDevice(remoteChannelsByLocalAddress.get(localAddress));
|
throws ChannelNotFoundException {
|
||||||
|
|
||||||
|
return getAuthenticatedDevice(getRemoteChannel(serverCall));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Optional<AuthenticatedDevice> getAuthenticatedDevice(@Nullable final Channel remoteChannel) {
|
@VisibleForTesting
|
||||||
return Optional.ofNullable(remoteChannel)
|
Optional<AuthenticatedDevice> getAuthenticatedDevice(final Channel remoteChannel) {
|
||||||
.map(channel -> channel.attr(AUTHENTICATED_DEVICE_ATTRIBUTE_KEY).get());
|
return Optional.ofNullable(remoteChannel.attr(AUTHENTICATED_DEVICE_ATTRIBUTE_KEY).get());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the parsed acceptable languages associated with the given local address, if any. Acceptable languages may
|
* Returns the request attributes associated with the given server call.
|
||||||
* be unavailable if the local connection associated with the given local address has already closed, if the client
|
|
||||||
* did not provide a list of acceptable languages, or the list provided by the client could not be parsed.
|
|
||||||
*
|
*
|
||||||
* @param localAddress the local address for which to find acceptable languages
|
* @param serverCall the gRPC server call for which to retrieve request attributes
|
||||||
*
|
*
|
||||||
* @return the acceptable languages associated with the given local address, if any
|
* @return the request attributes associated with the given server call
|
||||||
|
*
|
||||||
|
* @throws ChannelNotFoundException if the server call is not associated with a known channel; in practice, this
|
||||||
|
* generally indicates that the channel has closed while request processing is still in progress
|
||||||
*/
|
*/
|
||||||
public Optional<List<Locale.LanguageRange>> getAcceptableLanguages(final LocalAddress localAddress) {
|
public RequestAttributes getRequestAttributes(final ServerCall<?, ?> serverCall) throws ChannelNotFoundException {
|
||||||
return Optional.ofNullable(remoteChannelsByLocalAddress.get(localAddress))
|
return getRequestAttributes(getRemoteChannel(serverCall));
|
||||||
.map(remoteChannel -> remoteChannel.attr(ACCEPT_LANGUAGE_ATTRIBUTE_KEY).get());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@VisibleForTesting
|
||||||
* Returns the remote address associated with the given local address, if any. A remote address may be unavailable if
|
RequestAttributes getRequestAttributes(final Channel remoteChannel) {
|
||||||
* the local connection associated with the given local address has already closed.
|
final RequestAttributes requestAttributes = remoteChannel.attr(REQUEST_ATTRIBUTES_KEY).get();
|
||||||
*
|
|
||||||
* @param localAddress the local address for which to find a remote address
|
|
||||||
*
|
|
||||||
* @return the remote address associated with the given local address, if any
|
|
||||||
*/
|
|
||||||
public Optional<InetAddress> getRemoteAddress(final LocalAddress localAddress) {
|
|
||||||
return Optional.ofNullable(remoteChannelsByLocalAddress.get(localAddress))
|
|
||||||
.map(remoteChannel -> remoteChannel.attr(REMOTE_ADDRESS_ATTRIBUTE_KEY).get());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
if (requestAttributes == null) {
|
||||||
* Returns the unparsed user agent provided by the client that opened the connection associated with the given local
|
throw new IllegalStateException("Channel does not have request attributes");
|
||||||
* address. This method may return an empty value if no active local connection is associated with the given local
|
}
|
||||||
* address.
|
|
||||||
*
|
|
||||||
* @param localAddress the local address for which to find a User-Agent string
|
|
||||||
*
|
|
||||||
* @return the user agent string associated with the given local address
|
|
||||||
*/
|
|
||||||
public Optional<String> getRawUserAgent(final LocalAddress localAddress) {
|
|
||||||
return Optional.ofNullable(remoteChannelsByLocalAddress.get(localAddress))
|
|
||||||
.map(remoteChannel -> remoteChannel.attr(RAW_USER_AGENT_ATTRIBUTE_KEY).get());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
return requestAttributes;
|
||||||
* Returns the parsed user agent provided by the client that opened the connection associated with the given local
|
|
||||||
* address. This method may return an empty value if no active local connection is associated with the given local
|
|
||||||
* address or if the client's user-agent string was not recognized.
|
|
||||||
*
|
|
||||||
* @param localAddress the local address for which to find a User-Agent string
|
|
||||||
*
|
|
||||||
* @return the user agent associated with the given local address
|
|
||||||
*/
|
|
||||||
public Optional<UserAgent> getUserAgent(final LocalAddress localAddress) {
|
|
||||||
return Optional.ofNullable(remoteChannelsByLocalAddress.get(localAddress))
|
|
||||||
.map(remoteChannel -> remoteChannel.attr(PARSED_USER_AGENT_ATTRIBUTE_KEY).get());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -139,8 +113,13 @@ public class GrpcClientConnectionManager implements DisconnectionRequestListener
|
||||||
* @param authenticatedDevice the authenticated device for which to close connections
|
* @param authenticatedDevice the authenticated device for which to close connections
|
||||||
*/
|
*/
|
||||||
public void closeConnection(final AuthenticatedDevice authenticatedDevice) {
|
public void closeConnection(final AuthenticatedDevice authenticatedDevice) {
|
||||||
// Channels will actually get removed from the list/map by their closeFuture listeners
|
// Channels will actually get removed from the list/map by their closeFuture listeners. We copy the list to avoid
|
||||||
remoteChannelsByAuthenticatedDevice.getOrDefault(authenticatedDevice, Collections.emptyList()).forEach(channel ->
|
// concurrent modification; it's possible (though practically unlikely) that a channel can close and remove itself
|
||||||
|
// from the list while we're still iterating, resulting in a `ConcurrentModificationException`.
|
||||||
|
final List<Channel> channelsToClose =
|
||||||
|
new ArrayList<>(remoteChannelsByAuthenticatedDevice.getOrDefault(authenticatedDevice, Collections.emptyList()));
|
||||||
|
|
||||||
|
channelsToClose.forEach(channel ->
|
||||||
channel.writeAndFlush(new CloseWebSocketFrame(ApplicationWebSocketCloseReason.REAUTHENTICATION_REQUIRED
|
channel.writeAndFlush(new CloseWebSocketFrame(ApplicationWebSocketCloseReason.REAUTHENTICATION_REQUIRED
|
||||||
.toWebSocketCloseStatus("Reauthentication required")))
|
.toWebSocketCloseStatus("Reauthentication required")))
|
||||||
.addListener(ChannelFutureListener.CLOSE_ON_FAILURE));
|
.addListener(ChannelFutureListener.CLOSE_ON_FAILURE));
|
||||||
|
@ -151,11 +130,32 @@ public class GrpcClientConnectionManager implements DisconnectionRequestListener
|
||||||
return remoteChannelsByAuthenticatedDevice.get(authenticatedDevice);
|
return remoteChannelsByAuthenticatedDevice.get(authenticatedDevice);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Channel getRemoteChannel(final ServerCall<?, ?> serverCall) throws ChannelNotFoundException {
|
||||||
|
return getRemoteChannel(getLocalAddress(serverCall));
|
||||||
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
Channel getRemoteChannelByLocalAddress(final LocalAddress localAddress) {
|
Channel getRemoteChannel(final LocalAddress localAddress) throws ChannelNotFoundException {
|
||||||
|
final Channel remoteChannel = remoteChannelsByLocalAddress.get(localAddress);
|
||||||
|
|
||||||
|
if (remoteChannel == null) {
|
||||||
|
throw new ChannelNotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
return remoteChannelsByLocalAddress.get(localAddress);
|
return remoteChannelsByLocalAddress.get(localAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static LocalAddress getLocalAddress(final ServerCall<?, ?> serverCall) {
|
||||||
|
// In this server, gRPC's "remote" channel is actually a local channel that proxies to a distinct Noise channel.
|
||||||
|
// The gRPC "remote" address is the "local address" for the proxy connection, and the local address uniquely maps to
|
||||||
|
// a proxied Noise channel.
|
||||||
|
if (!(serverCall.getAttributes().get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR) instanceof LocalAddress localAddress)) {
|
||||||
|
throw new IllegalArgumentException("Unexpected channel type: " + serverCall.getAttributes().get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR));
|
||||||
|
}
|
||||||
|
|
||||||
|
return localAddress;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles successful completion of a WebSocket handshake and associates attributes and headers from the handshake
|
* Handles successful completion of a WebSocket handshake and associates attributes and headers from the handshake
|
||||||
* request with the channel via which the handshake took place.
|
* request with the channel via which the handshake took place.
|
||||||
|
@ -166,30 +166,23 @@ public class GrpcClientConnectionManager implements DisconnectionRequestListener
|
||||||
* @param acceptLanguageHeader the value of the Accept-Language header provided in the handshake request; may be
|
* @param acceptLanguageHeader the value of the Accept-Language header provided in the handshake request; may be
|
||||||
* {@code null}
|
* {@code null}
|
||||||
*/
|
*/
|
||||||
static void handleWebSocketHandshakeComplete(final Channel channel,
|
static void handleHandshakeComplete(final Channel channel,
|
||||||
final InetAddress preferredRemoteAddress,
|
final InetAddress preferredRemoteAddress,
|
||||||
@Nullable final String userAgentHeader,
|
@Nullable final String userAgentHeader,
|
||||||
@Nullable final String acceptLanguageHeader) {
|
@Nullable final String acceptLanguageHeader) {
|
||||||
|
|
||||||
channel.attr(GrpcClientConnectionManager.REMOTE_ADDRESS_ATTRIBUTE_KEY).set(preferredRemoteAddress);
|
@Nullable List<Locale.LanguageRange> acceptLanguages = Collections.emptyList();
|
||||||
|
|
||||||
if (StringUtils.isNotBlank(userAgentHeader)) {
|
|
||||||
channel.attr(GrpcClientConnectionManager.RAW_USER_AGENT_ATTRIBUTE_KEY).set(userAgentHeader);
|
|
||||||
|
|
||||||
try {
|
|
||||||
channel.attr(GrpcClientConnectionManager.PARSED_USER_AGENT_ATTRIBUTE_KEY)
|
|
||||||
.set(UserAgentUtil.parseUserAgentString(userAgentHeader));
|
|
||||||
} catch (final UnrecognizedUserAgentException ignored) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (StringUtils.isNotBlank(acceptLanguageHeader)) {
|
if (StringUtils.isNotBlank(acceptLanguageHeader)) {
|
||||||
try {
|
try {
|
||||||
channel.attr(GrpcClientConnectionManager.ACCEPT_LANGUAGE_ATTRIBUTE_KEY).set(Locale.LanguageRange.parse(acceptLanguageHeader));
|
acceptLanguages = Locale.LanguageRange.parse(acceptLanguageHeader);
|
||||||
} catch (final IllegalArgumentException e) {
|
} catch (final IllegalArgumentException e) {
|
||||||
log.debug("Invalid Accept-Language header from User-Agent {}: {}", userAgentHeader, acceptLanguageHeader, e);
|
log.debug("Invalid Accept-Language header from User-Agent {}: {}", userAgentHeader, acceptLanguageHeader, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
channel.attr(REQUEST_ATTRIBUTES_KEY)
|
||||||
|
.set(new RequestAttributes(preferredRemoteAddress, userAgentHeader, acceptLanguages));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -74,7 +74,7 @@ class WebsocketHandshakeCompleteHandler extends ChannelInboundHandlerAdapter {
|
||||||
preferredRemoteAddress = maybePreferredRemoteAddress.get();
|
preferredRemoteAddress = maybePreferredRemoteAddress.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
GrpcClientConnectionManager.handleWebSocketHandshakeComplete(context.channel(),
|
GrpcClientConnectionManager.handleHandshakeComplete(context.channel(),
|
||||||
preferredRemoteAddress,
|
preferredRemoteAddress,
|
||||||
handshakeCompleteEvent.requestHeaders().getAsString(HttpHeaderNames.USER_AGENT),
|
handshakeCompleteEvent.requestHeaders().getAsString(HttpHeaderNames.USER_AGENT),
|
||||||
handshakeCompleteEvent.requestHeaders().getAsString(HttpHeaderNames.ACCEPT_LANGUAGE));
|
handshakeCompleteEvent.requestHeaders().getAsString(HttpHeaderNames.ACCEPT_LANGUAGE));
|
||||||
|
|
|
@ -11,7 +11,6 @@ import static org.whispersystems.textsecuregcm.grpc.validators.ValidatorUtils.in
|
||||||
|
|
||||||
import com.google.protobuf.ByteString;
|
import com.google.protobuf.ByteString;
|
||||||
import com.google.protobuf.Descriptors;
|
import com.google.protobuf.Descriptors;
|
||||||
import com.google.protobuf.GeneratedMessageV3;
|
|
||||||
import com.google.protobuf.Message;
|
import com.google.protobuf.Message;
|
||||||
import io.grpc.Status;
|
import io.grpc.Status;
|
||||||
import io.grpc.StatusException;
|
import io.grpc.StatusException;
|
||||||
|
@ -49,7 +48,7 @@ public abstract class BaseFieldValidator<T> implements FieldValidator {
|
||||||
public void validate(
|
public void validate(
|
||||||
final Object extensionValue,
|
final Object extensionValue,
|
||||||
final Descriptors.FieldDescriptor fd,
|
final Descriptors.FieldDescriptor fd,
|
||||||
final GeneratedMessageV3 msg) throws StatusException {
|
final Message msg) throws StatusException {
|
||||||
try {
|
try {
|
||||||
final T extensionValueTyped = resolveExtensionValue(extensionValue);
|
final T extensionValueTyped = resolveExtensionValue(extensionValue);
|
||||||
|
|
||||||
|
@ -116,7 +115,7 @@ public abstract class BaseFieldValidator<T> implements FieldValidator {
|
||||||
protected void validateRepeatedField(
|
protected void validateRepeatedField(
|
||||||
final T extensionValue,
|
final T extensionValue,
|
||||||
final Descriptors.FieldDescriptor fd,
|
final Descriptors.FieldDescriptor fd,
|
||||||
final GeneratedMessageV3 msg) throws StatusException {
|
final Message msg) throws StatusException {
|
||||||
throw internalError("`validateRepeatedField` method needs to be implemented");
|
throw internalError("`validateRepeatedField` method needs to be implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ import static org.whispersystems.textsecuregcm.grpc.validators.ValidatorUtils.in
|
||||||
|
|
||||||
import com.google.protobuf.ByteString;
|
import com.google.protobuf.ByteString;
|
||||||
import com.google.protobuf.Descriptors;
|
import com.google.protobuf.Descriptors;
|
||||||
import com.google.protobuf.GeneratedMessageV3;
|
import com.google.protobuf.Message;
|
||||||
import io.grpc.StatusException;
|
import io.grpc.StatusException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
@ -53,7 +53,7 @@ public class ExactlySizeFieldValidator extends BaseFieldValidator<Set<Integer>>
|
||||||
protected void validateRepeatedField(
|
protected void validateRepeatedField(
|
||||||
final Set<Integer> permittedSizes,
|
final Set<Integer> permittedSizes,
|
||||||
final Descriptors.FieldDescriptor fd,
|
final Descriptors.FieldDescriptor fd,
|
||||||
final GeneratedMessageV3 msg) throws StatusException {
|
final Message msg) throws StatusException {
|
||||||
final int size = msg.getRepeatedFieldCount(fd);
|
final int size = msg.getRepeatedFieldCount(fd);
|
||||||
if (permittedSizes.contains(size)) {
|
if (permittedSizes.contains(size)) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -6,11 +6,11 @@
|
||||||
package org.whispersystems.textsecuregcm.grpc.validators;
|
package org.whispersystems.textsecuregcm.grpc.validators;
|
||||||
|
|
||||||
import com.google.protobuf.Descriptors;
|
import com.google.protobuf.Descriptors;
|
||||||
import com.google.protobuf.GeneratedMessageV3;
|
import com.google.protobuf.Message;
|
||||||
import io.grpc.StatusException;
|
import io.grpc.StatusException;
|
||||||
|
|
||||||
public interface FieldValidator {
|
public interface FieldValidator {
|
||||||
|
|
||||||
void validate(Object extensionValue, Descriptors.FieldDescriptor fd, GeneratedMessageV3 msg)
|
void validate(Object extensionValue, Descriptors.FieldDescriptor fd, Message msg)
|
||||||
throws StatusException;
|
throws StatusException;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import static org.whispersystems.textsecuregcm.grpc.validators.ValidatorUtils.in
|
||||||
|
|
||||||
import com.google.protobuf.ByteString;
|
import com.google.protobuf.ByteString;
|
||||||
import com.google.protobuf.Descriptors;
|
import com.google.protobuf.Descriptors;
|
||||||
import com.google.protobuf.GeneratedMessageV3;
|
import com.google.protobuf.Message;
|
||||||
import io.grpc.StatusException;
|
import io.grpc.StatusException;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
@ -52,7 +52,7 @@ public class NonEmptyFieldValidator extends BaseFieldValidator<Boolean> {
|
||||||
protected void validateRepeatedField(
|
protected void validateRepeatedField(
|
||||||
final Boolean extensionValue,
|
final Boolean extensionValue,
|
||||||
final Descriptors.FieldDescriptor fd,
|
final Descriptors.FieldDescriptor fd,
|
||||||
final GeneratedMessageV3 msg) throws StatusException {
|
final Message msg) throws StatusException {
|
||||||
if (msg.getRepeatedFieldCount(fd) > 0) {
|
if (msg.getRepeatedFieldCount(fd) > 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import static org.whispersystems.textsecuregcm.grpc.validators.ValidatorUtils.in
|
||||||
|
|
||||||
import com.google.protobuf.ByteString;
|
import com.google.protobuf.ByteString;
|
||||||
import com.google.protobuf.Descriptors;
|
import com.google.protobuf.Descriptors;
|
||||||
import com.google.protobuf.GeneratedMessageV3;
|
import com.google.protobuf.Message;
|
||||||
import io.grpc.StatusException;
|
import io.grpc.StatusException;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import org.signal.chat.require.SizeConstraint;
|
import org.signal.chat.require.SizeConstraint;
|
||||||
|
@ -48,7 +48,7 @@ public class SizeFieldValidator extends BaseFieldValidator<Range> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void validateRepeatedField(final Range range, final Descriptors.FieldDescriptor fd, final GeneratedMessageV3 msg) throws StatusException {
|
protected void validateRepeatedField(final Range range, final Descriptors.FieldDescriptor fd, final Message msg) throws StatusException {
|
||||||
final int size = msg.getRepeatedFieldCount(fd);
|
final int size = msg.getRepeatedFieldCount(fd);
|
||||||
if (size < range.min() || size > range.max()) {
|
if (size < range.min() || size > range.max()) {
|
||||||
throw invalidArgument("field value is [%d] but expected to be within the [%d, %d] range".formatted(
|
throw invalidArgument("field value is [%d] but expected to be within the [%d, %d] range".formatted(
|
||||||
|
|
|
@ -67,7 +67,7 @@ public class OpenWebSocketCounter {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final ClientPlatform clientPlatform =
|
final ClientPlatform clientPlatform =
|
||||||
UserAgentUtil.parseUserAgentString(context.getClient().getUserAgent()).getPlatform();
|
UserAgentUtil.parseUserAgentString(context.getClient().getUserAgent()).platform();
|
||||||
|
|
||||||
calculatedOpenWebSocketCounter = openWebsocketsByClientPlatform.get(clientPlatform);
|
calculatedOpenWebSocketCounter = openWebsocketsByClientPlatform.get(clientPlatform);
|
||||||
calculatedDurationTimer = durationTimersByClientPlatform.get(clientPlatform);
|
calculatedDurationTimer = durationTimersByClientPlatform.get(clientPlatform);
|
||||||
|
|
|
@ -9,6 +9,7 @@ import io.micrometer.core.instrument.Tag;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.whispersystems.textsecuregcm.WhisperServerVersion;
|
import org.whispersystems.textsecuregcm.WhisperServerVersion;
|
||||||
import org.whispersystems.textsecuregcm.storage.ClientReleaseManager;
|
import org.whispersystems.textsecuregcm.storage.ClientReleaseManager;
|
||||||
import org.whispersystems.textsecuregcm.util.ua.UnrecognizedUserAgentException;
|
import org.whispersystems.textsecuregcm.util.ua.UnrecognizedUserAgentException;
|
||||||
|
@ -48,15 +49,15 @@ public class UserAgentTagUtil {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Tag getPlatformTag(@Nullable final UserAgent userAgent) {
|
public static Tag getPlatformTag(@Nullable final UserAgent userAgent) {
|
||||||
return Tag.of(PLATFORM_TAG, userAgent != null ? userAgent.getPlatform().name().toLowerCase() : "unrecognized");
|
return Tag.of(PLATFORM_TAG, userAgent != null ? userAgent.platform().name().toLowerCase() : "unrecognized");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Optional<Tag> getClientVersionTag(final String userAgentString, final ClientReleaseManager clientReleaseManager) {
|
public static Optional<Tag> getClientVersionTag(final String userAgentString, final ClientReleaseManager clientReleaseManager) {
|
||||||
try {
|
try {
|
||||||
final UserAgent userAgent = UserAgentUtil.parseUserAgentString(userAgentString);
|
final UserAgent userAgent = UserAgentUtil.parseUserAgentString(userAgentString);
|
||||||
|
|
||||||
if (clientReleaseManager.isVersionActive(userAgent.getPlatform(), userAgent.getVersion())) {
|
if (clientReleaseManager.isVersionActive(userAgent.platform(), userAgent.version())) {
|
||||||
return Optional.of(Tag.of(VERSION_TAG, userAgent.getVersion().toString()));
|
return Optional.of(Tag.of(VERSION_TAG, userAgent.version().toString()));
|
||||||
}
|
}
|
||||||
} catch (final UnrecognizedUserAgentException ignored) {
|
} catch (final UnrecognizedUserAgentException ignored) {
|
||||||
}
|
}
|
||||||
|
@ -70,10 +71,8 @@ public class UserAgentTagUtil {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final UserAgent userAgent = UserAgentUtil.parseUserAgentString(userAgentString);
|
final UserAgent userAgent = UserAgentUtil.parseUserAgentString(userAgentString);
|
||||||
platform = userAgent.getPlatform().name().toLowerCase();
|
platform = userAgent.platform().name().toLowerCase();
|
||||||
libsignal = userAgent.getAdditionalSpecifiers()
|
libsignal = StringUtils.contains(userAgent.additionalSpecifiers(), "libsignal");
|
||||||
.map(additionalSpecifiers -> additionalSpecifiers.contains("libsignal"))
|
|
||||||
.orElse(false);
|
|
||||||
} catch (final UnrecognizedUserAgentException e) {
|
} catch (final UnrecognizedUserAgentException e) {
|
||||||
platform = "unrecognized";
|
platform = "unrecognized";
|
||||||
libsignal = false;
|
libsignal = false;
|
||||||
|
|
|
@ -20,6 +20,7 @@ import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.signal.libsignal.protocol.SealedSenderMultiRecipientMessage;
|
import org.signal.libsignal.protocol.SealedSenderMultiRecipientMessage;
|
||||||
import org.signal.libsignal.protocol.util.Pair;
|
import org.signal.libsignal.protocol.util.Pair;
|
||||||
|
@ -35,7 +36,6 @@ import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
import org.whispersystems.textsecuregcm.storage.Device;
|
import org.whispersystems.textsecuregcm.storage.Device;
|
||||||
import org.whispersystems.textsecuregcm.storage.MessagesManager;
|
import org.whispersystems.textsecuregcm.storage.MessagesManager;
|
||||||
import org.whispersystems.textsecuregcm.util.Util;
|
import org.whispersystems.textsecuregcm.util.Util;
|
||||||
import javax.annotation.Nullable;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A MessageSender sends Signal messages to destination devices. Messages may be "normal" user-to-user messages,
|
* A MessageSender sends Signal messages to destination devices. Messages may be "normal" user-to-user messages,
|
||||||
|
@ -56,6 +56,7 @@ public class MessageSender {
|
||||||
// Note that these names deliberately reference `MessageController` for metric continuity
|
// Note that these names deliberately reference `MessageController` for metric continuity
|
||||||
private static final String REJECT_OVERSIZE_MESSAGE_COUNTER_NAME = name(MessageController.class, "rejectOversizeMessage");
|
private static final String REJECT_OVERSIZE_MESSAGE_COUNTER_NAME = name(MessageController.class, "rejectOversizeMessage");
|
||||||
private static final String CONTENT_SIZE_DISTRIBUTION_NAME = MetricsUtil.name(MessageController.class, "messageContentSize");
|
private static final String CONTENT_SIZE_DISTRIBUTION_NAME = MetricsUtil.name(MessageController.class, "messageContentSize");
|
||||||
|
private static final String EMPTY_MESSAGE_LIST_COUNTER_NAME = MetricsUtil.name(MessageSender.class, "emptyMessageList");
|
||||||
|
|
||||||
private static final String SEND_COUNTER_NAME = name(MessageSender.class, "sendMessage");
|
private static final String SEND_COUNTER_NAME = name(MessageSender.class, "sendMessage");
|
||||||
private static final String EPHEMERAL_TAG_NAME = "ephemeral";
|
private static final String EPHEMERAL_TAG_NAME = "ephemeral";
|
||||||
|
@ -85,6 +86,8 @@ public class MessageSender {
|
||||||
* @param destinationIdentifier the service identifier to which the messages are addressed
|
* @param destinationIdentifier the service identifier to which the messages are addressed
|
||||||
* @param messagesByDeviceId a map of device IDs to message payloads
|
* @param messagesByDeviceId a map of device IDs to message payloads
|
||||||
* @param registrationIdsByDeviceId a map of device IDs to device registration IDs
|
* @param registrationIdsByDeviceId a map of device IDs to device registration IDs
|
||||||
|
* @param syncMessageSenderDeviceId if the message is a sync message (i.e. a message to other devices linked to the
|
||||||
|
* caller's own account), contains the ID of the device that sent the message
|
||||||
* @param userAgent the User-Agent string for the sender; may be {@code null} if not known
|
* @param userAgent the User-Agent string for the sender; may be {@code null} if not known
|
||||||
*
|
*
|
||||||
* @throws MismatchedDevicesException if the given bundle of messages did not include a message for all required
|
* @throws MismatchedDevicesException if the given bundle of messages did not include a message for all required
|
||||||
|
@ -96,34 +99,48 @@ public class MessageSender {
|
||||||
final ServiceIdentifier destinationIdentifier,
|
final ServiceIdentifier destinationIdentifier,
|
||||||
final Map<Byte, Envelope> messagesByDeviceId,
|
final Map<Byte, Envelope> messagesByDeviceId,
|
||||||
final Map<Byte, Integer> registrationIdsByDeviceId,
|
final Map<Byte, Integer> registrationIdsByDeviceId,
|
||||||
|
@SuppressWarnings("OptionalUsedAsFieldOrParameterType") final Optional<Byte> syncMessageSenderDeviceId,
|
||||||
@Nullable final String userAgent) throws MismatchedDevicesException, MessageTooLargeException {
|
@Nullable final String userAgent) throws MismatchedDevicesException, MessageTooLargeException {
|
||||||
|
|
||||||
if (messagesByDeviceId.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!destination.isIdentifiedBy(destinationIdentifier)) {
|
if (!destination.isIdentifiedBy(destinationIdentifier)) {
|
||||||
throw new IllegalArgumentException("Destination account not identified by destination service identifier");
|
throw new IllegalArgumentException("Destination account not identified by destination service identifier");
|
||||||
}
|
}
|
||||||
|
|
||||||
final Envelope firstMessage = messagesByDeviceId.values().iterator().next();
|
final Tag platformTag = UserAgentTagUtil.getPlatformTag(userAgent);
|
||||||
|
|
||||||
final boolean isSyncMessage = StringUtils.isNotBlank(firstMessage.getSourceServiceId()) &&
|
if (messagesByDeviceId.isEmpty()) {
|
||||||
destination.isIdentifiedBy(ServiceIdentifier.valueOf(firstMessage.getSourceServiceId()));
|
Metrics.counter(EMPTY_MESSAGE_LIST_COUNTER_NAME,
|
||||||
|
Tags.of("sync", String.valueOf(syncMessageSenderDeviceId.isPresent())).and(platformTag)).increment();
|
||||||
|
}
|
||||||
|
|
||||||
final boolean isStory = firstMessage.getStory();
|
final byte excludedDeviceId;
|
||||||
|
if (syncMessageSenderDeviceId.isPresent()) {
|
||||||
|
if (messagesByDeviceId.values().stream().anyMatch(message -> StringUtils.isBlank(message.getSourceServiceId()) ||
|
||||||
|
!destination.isIdentifiedBy(ServiceIdentifier.valueOf(message.getSourceServiceId())))) {
|
||||||
|
|
||||||
validateIndividualMessageContentLength(messagesByDeviceId.values(), isSyncMessage, isStory, userAgent);
|
throw new IllegalArgumentException("Sync message sender device ID specified, but one or more messages are not addressed to sender");
|
||||||
|
}
|
||||||
|
excludedDeviceId = syncMessageSenderDeviceId.get();
|
||||||
|
} else {
|
||||||
|
if (messagesByDeviceId.values().stream().anyMatch(message -> StringUtils.isNotBlank(message.getSourceServiceId()) &&
|
||||||
|
destination.isIdentifiedBy(ServiceIdentifier.valueOf(message.getSourceServiceId())))) {
|
||||||
|
|
||||||
|
throw new IllegalArgumentException("Sync message sender device ID not specified, but one or more messages are addressed to sender");
|
||||||
|
}
|
||||||
|
excludedDeviceId = NO_EXCLUDED_DEVICE_ID;
|
||||||
|
}
|
||||||
|
|
||||||
final Optional<MismatchedDevices> maybeMismatchedDevices = getMismatchedDevices(destination,
|
final Optional<MismatchedDevices> maybeMismatchedDevices = getMismatchedDevices(destination,
|
||||||
destinationIdentifier,
|
destinationIdentifier,
|
||||||
registrationIdsByDeviceId,
|
registrationIdsByDeviceId,
|
||||||
isSyncMessage ? (byte) firstMessage.getSourceDevice() : NO_EXCLUDED_DEVICE_ID);
|
excludedDeviceId);
|
||||||
|
|
||||||
if (maybeMismatchedDevices.isPresent()) {
|
if (maybeMismatchedDevices.isPresent()) {
|
||||||
throw new MismatchedDevicesException(maybeMismatchedDevices.get());
|
throw new MismatchedDevicesException(maybeMismatchedDevices.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validateIndividualMessageContentLength(messagesByDeviceId.values(), syncMessageSenderDeviceId.isPresent(), userAgent);
|
||||||
|
|
||||||
messagesManager.insert(destination.getIdentifier(IdentityType.ACI), messagesByDeviceId)
|
messagesManager.insert(destination.getIdentifier(IdentityType.ACI), messagesByDeviceId)
|
||||||
.forEach((deviceId, destinationPresent) -> {
|
.forEach((deviceId, destinationPresent) -> {
|
||||||
final Envelope message = messagesByDeviceId.get(deviceId);
|
final Envelope message = messagesByDeviceId.get(deviceId);
|
||||||
|
@ -142,7 +159,7 @@ public class MessageSender {
|
||||||
STORY_TAG_NAME, String.valueOf(message.getStory()),
|
STORY_TAG_NAME, String.valueOf(message.getStory()),
|
||||||
SEALED_SENDER_TAG_NAME, String.valueOf(!message.hasSourceServiceId()),
|
SEALED_SENDER_TAG_NAME, String.valueOf(!message.hasSourceServiceId()),
|
||||||
MULTI_RECIPIENT_TAG_NAME, "false")
|
MULTI_RECIPIENT_TAG_NAME, "false")
|
||||||
.and(UserAgentTagUtil.getPlatformTag(userAgent));
|
.and(platformTag);
|
||||||
|
|
||||||
Metrics.counter(SEND_COUNTER_NAME, tags).increment();
|
Metrics.counter(SEND_COUNTER_NAME, tags).increment();
|
||||||
});
|
});
|
||||||
|
@ -308,14 +325,13 @@ public class MessageSender {
|
||||||
|
|
||||||
private static void validateIndividualMessageContentLength(final Iterable<Envelope> messages,
|
private static void validateIndividualMessageContentLength(final Iterable<Envelope> messages,
|
||||||
final boolean isSyncMessage,
|
final boolean isSyncMessage,
|
||||||
final boolean isStory,
|
|
||||||
@Nullable final String userAgent) throws MessageTooLargeException {
|
@Nullable final String userAgent) throws MessageTooLargeException {
|
||||||
|
|
||||||
for (final Envelope message : messages) {
|
for (final Envelope message : messages) {
|
||||||
MessageSender.validateContentLength(message.getContent().size(),
|
MessageSender.validateContentLength(message.getContent().size(),
|
||||||
false,
|
false,
|
||||||
isSyncMessage,
|
isSyncMessage,
|
||||||
isStory,
|
message.getStory(),
|
||||||
userAgent);
|
userAgent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ package org.whispersystems.textsecuregcm.push;
|
||||||
import io.micrometer.core.instrument.Metrics;
|
import io.micrometer.core.instrument.Metrics;
|
||||||
import io.micrometer.core.instrument.binder.jvm.ExecutorServiceMetrics;
|
import io.micrometer.core.instrument.binder.jvm.ExecutorServiceMetrics;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
|
@ -70,6 +71,7 @@ public class ReceiptSender {
|
||||||
destinationIdentifier,
|
destinationIdentifier,
|
||||||
messagesByDeviceId,
|
messagesByDeviceId,
|
||||||
registrationIdsByDeviceId,
|
registrationIdsByDeviceId,
|
||||||
|
Optional.empty(),
|
||||||
UserAgentTagUtil.SERVER_UA);
|
UserAgentTagUtil.SERVER_UA);
|
||||||
} catch (final Exception e) {
|
} catch (final Exception e) {
|
||||||
logger.warn("Could not send delivery receipt", e);
|
logger.warn("Could not send delivery receipt", e);
|
||||||
|
|
|
@ -5,8 +5,10 @@
|
||||||
package org.whispersystems.textsecuregcm.storage;
|
package org.whispersystems.textsecuregcm.storage;
|
||||||
|
|
||||||
import com.google.protobuf.ByteString;
|
import com.google.protobuf.ByteString;
|
||||||
|
import java.time.Clock;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import org.apache.commons.lang3.ObjectUtils;
|
import org.apache.commons.lang3.ObjectUtils;
|
||||||
|
@ -28,12 +30,16 @@ public class ChangeNumberManager {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(ChangeNumberManager.class);
|
private static final Logger logger = LoggerFactory.getLogger(ChangeNumberManager.class);
|
||||||
private final MessageSender messageSender;
|
private final MessageSender messageSender;
|
||||||
private final AccountsManager accountsManager;
|
private final AccountsManager accountsManager;
|
||||||
|
private final Clock clock;
|
||||||
|
|
||||||
public ChangeNumberManager(
|
public ChangeNumberManager(
|
||||||
final MessageSender messageSender,
|
final MessageSender messageSender,
|
||||||
final AccountsManager accountsManager) {
|
final AccountsManager accountsManager,
|
||||||
|
final Clock clock) {
|
||||||
|
|
||||||
this.messageSender = messageSender;
|
this.messageSender = messageSender;
|
||||||
this.accountsManager = accountsManager;
|
this.accountsManager = accountsManager;
|
||||||
|
this.clock = clock;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Account changeNumber(final Account account, final String number,
|
public Account changeNumber(final Account account, final String number,
|
||||||
|
@ -96,7 +102,7 @@ public class ChangeNumberManager {
|
||||||
final String senderUserAgent) throws MessageTooLargeException, MismatchedDevicesException {
|
final String senderUserAgent) throws MessageTooLargeException, MismatchedDevicesException {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final long serverTimestamp = System.currentTimeMillis();
|
final long serverTimestamp = clock.millis();
|
||||||
final ServiceIdentifier serviceIdentifier = new AciServiceIdentifier(account.getUuid());
|
final ServiceIdentifier serviceIdentifier = new AciServiceIdentifier(account.getUuid());
|
||||||
|
|
||||||
final Map<Byte, Envelope> messagesByDeviceId = deviceMessages.stream()
|
final Map<Byte, Envelope> messagesByDeviceId = deviceMessages.stream()
|
||||||
|
@ -113,10 +119,15 @@ public class ChangeNumberManager {
|
||||||
.setEphemeral(false)
|
.setEphemeral(false)
|
||||||
.build()));
|
.build()));
|
||||||
|
|
||||||
final Map<Byte, Integer> registrationIdsByDeviceId = account.getDevices().stream()
|
final Map<Byte, Integer> registrationIdsByDeviceId = deviceMessages.stream()
|
||||||
.collect(Collectors.toMap(Device::getId, Device::getRegistrationId));
|
.collect(Collectors.toMap(IncomingMessage::destinationDeviceId, IncomingMessage::destinationRegistrationId));
|
||||||
|
|
||||||
messageSender.sendMessages(account, serviceIdentifier, messagesByDeviceId, registrationIdsByDeviceId, senderUserAgent);
|
messageSender.sendMessages(account,
|
||||||
|
serviceIdentifier,
|
||||||
|
messagesByDeviceId,
|
||||||
|
registrationIdsByDeviceId,
|
||||||
|
Optional.of(Device.PRIMARY_ID),
|
||||||
|
senderUserAgent);
|
||||||
} catch (final RuntimeException e) {
|
} catch (final RuntimeException e) {
|
||||||
logger.warn("Changed number but could not send all device messages on {}", account.getUuid(), e);
|
logger.warn("Changed number but could not send all device messages on {}", account.getUuid(), e);
|
||||||
throw e;
|
throw e;
|
||||||
|
|
|
@ -7,6 +7,7 @@ package org.whispersystems.textsecuregcm.subscriptions;
|
||||||
|
|
||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
|
import com.stripe.Stripe;
|
||||||
import com.stripe.StripeClient;
|
import com.stripe.StripeClient;
|
||||||
import com.stripe.exception.CardException;
|
import com.stripe.exception.CardException;
|
||||||
import com.stripe.exception.IdempotencyException;
|
import com.stripe.exception.IdempotencyException;
|
||||||
|
@ -71,6 +72,7 @@ import javax.crypto.spec.SecretKeySpec;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.whispersystems.textsecuregcm.WhisperServerVersion;
|
||||||
import org.whispersystems.textsecuregcm.storage.PaymentTime;
|
import org.whispersystems.textsecuregcm.storage.PaymentTime;
|
||||||
import org.whispersystems.textsecuregcm.storage.SubscriptionException;
|
import org.whispersystems.textsecuregcm.storage.SubscriptionException;
|
||||||
import org.whispersystems.textsecuregcm.util.Conversions;
|
import org.whispersystems.textsecuregcm.util.Conversions;
|
||||||
|
@ -97,6 +99,9 @@ public class StripeManager implements CustomerAwareSubscriptionPaymentProcessor
|
||||||
if (Strings.isNullOrEmpty(apiKey)) {
|
if (Strings.isNullOrEmpty(apiKey)) {
|
||||||
throw new IllegalArgumentException("apiKey cannot be empty");
|
throw new IllegalArgumentException("apiKey cannot be empty");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Stripe.setAppInfo("Signal-Server", WhisperServerVersion.getServerVersion());
|
||||||
|
|
||||||
this.stripeClient = new StripeClient(apiKey);
|
this.stripeClient = new StripeClient(apiKey);
|
||||||
this.executor = Objects.requireNonNull(executor);
|
this.executor = Objects.requireNonNull(executor);
|
||||||
this.idempotencyKeyGenerator = Objects.requireNonNull(idempotencyKeyGenerator);
|
this.idempotencyKeyGenerator = Objects.requireNonNull(idempotencyKeyGenerator);
|
||||||
|
|
|
@ -46,7 +46,7 @@ public class LoggingUnhandledExceptionMapper extends LoggingExceptionMapper<Thro
|
||||||
|
|
||||||
// streamline the user-agent if it is recognized
|
// streamline the user-agent if it is recognized
|
||||||
final UserAgent ua = UserAgentUtil.parseUserAgentString(userAgent);
|
final UserAgent ua = UserAgentUtil.parseUserAgentString(userAgent);
|
||||||
userAgent = String.format("%s %s", ua.getPlatform(), ua.getVersion());
|
userAgent = String.format("%s %s", ua.platform(), ua.version());
|
||||||
} catch (final UnrecognizedUserAgentException ignored) {
|
} catch (final UnrecognizedUserAgentException ignored) {
|
||||||
|
|
||||||
} catch (final Exception e) {
|
} catch (final Exception e) {
|
||||||
|
|
|
@ -6,58 +6,8 @@
|
||||||
package org.whispersystems.textsecuregcm.util.ua;
|
package org.whispersystems.textsecuregcm.util.ua;
|
||||||
|
|
||||||
import com.vdurmont.semver4j.Semver;
|
import com.vdurmont.semver4j.Semver;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
public class UserAgent {
|
public record UserAgent(ClientPlatform platform, Semver version, @Nullable String additionalSpecifiers) {
|
||||||
|
|
||||||
private final ClientPlatform platform;
|
|
||||||
private final Semver version;
|
|
||||||
private final String additionalSpecifiers;
|
|
||||||
|
|
||||||
public UserAgent(final ClientPlatform platform, final Semver version) {
|
|
||||||
this(platform, version, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public UserAgent(final ClientPlatform platform, final Semver version, final String additionalSpecifiers) {
|
|
||||||
this.platform = platform;
|
|
||||||
this.version = version;
|
|
||||||
this.additionalSpecifiers = additionalSpecifiers;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ClientPlatform getPlatform() {
|
|
||||||
return platform;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Semver getVersion() {
|
|
||||||
return version;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<String> getAdditionalSpecifiers() {
|
|
||||||
return Optional.ofNullable(additionalSpecifiers);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(final Object o) {
|
|
||||||
if (this == o) return true;
|
|
||||||
if (o == null || getClass() != o.getClass()) return false;
|
|
||||||
final UserAgent userAgent = (UserAgent)o;
|
|
||||||
return platform == userAgent.platform &&
|
|
||||||
version.equals(userAgent.version) &&
|
|
||||||
Objects.equals(additionalSpecifiers, userAgent.additionalSpecifiers);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
return Objects.hash(platform, version, additionalSpecifiers);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return "UserAgent{" +
|
|
||||||
"platform=" + platform +
|
|
||||||
", version=" + version +
|
|
||||||
", additionalSpecifiers='" + additionalSpecifiers + '\'' +
|
|
||||||
'}';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
|
|
||||||
package org.whispersystems.textsecuregcm.util.ua;
|
package org.whispersystems.textsecuregcm.util.ua;
|
||||||
|
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
|
||||||
import com.vdurmont.semver4j.Semver;
|
import com.vdurmont.semver4j.Semver;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
@ -21,10 +20,10 @@ public class UserAgentUtil {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final UserAgent standardUserAgent = parseStandardUserAgentString(userAgentString);
|
final Matcher matcher = STANDARD_UA_PATTERN.matcher(userAgentString);
|
||||||
|
|
||||||
if (standardUserAgent != null) {
|
if (matcher.matches()) {
|
||||||
return standardUserAgent;
|
return new UserAgent(ClientPlatform.valueOf(matcher.group(1).toUpperCase()), new Semver(matcher.group(2)), StringUtils.stripToNull(matcher.group(4)));
|
||||||
}
|
}
|
||||||
} catch (final Exception e) {
|
} catch (final Exception e) {
|
||||||
throw new UnrecognizedUserAgentException(e);
|
throw new UnrecognizedUserAgentException(e);
|
||||||
|
@ -32,15 +31,4 @@ public class UserAgentUtil {
|
||||||
|
|
||||||
throw new UnrecognizedUserAgentException();
|
throw new UnrecognizedUserAgentException();
|
||||||
}
|
}
|
||||||
|
|
||||||
@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,8 +5,11 @@
|
||||||
|
|
||||||
package org.whispersystems.textsecuregcm.auth;
|
package org.whispersystems.textsecuregcm.auth;
|
||||||
|
|
||||||
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
|
import static com.github.tomakehurst.wiremock.client.WireMock.created;
|
||||||
|
import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
|
||||||
|
import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson;
|
||||||
import static com.github.tomakehurst.wiremock.client.WireMock.post;
|
import static com.github.tomakehurst.wiremock.client.WireMock.post;
|
||||||
|
import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor;
|
||||||
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
|
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
|
||||||
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
|
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
@ -15,17 +18,19 @@ import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
|
import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
|
||||||
import io.netty.resolver.dns.DnsNameResolver;
|
import io.netty.resolver.dns.DnsNameResolver;
|
||||||
import io.netty.util.concurrent.Future;
|
import io.netty.util.concurrent.GlobalEventExecutor;
|
||||||
|
import io.netty.util.concurrent.SucceededFuture;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
import java.security.cert.CertificateException;
|
import java.time.Duration;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.CancellationException;
|
import java.util.concurrent.CancellationException;
|
||||||
import java.util.concurrent.ExecutionException;
|
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import org.apache.commons.lang3.RandomStringUtils;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
@ -35,31 +40,41 @@ import org.whispersystems.textsecuregcm.configuration.RetryConfiguration;
|
||||||
|
|
||||||
public class CloudflareTurnCredentialsManagerTest {
|
public class CloudflareTurnCredentialsManagerTest {
|
||||||
@RegisterExtension
|
@RegisterExtension
|
||||||
private final WireMockExtension wireMock = WireMockExtension.newInstance()
|
private static final WireMockExtension wireMock = WireMockExtension.newInstance()
|
||||||
.options(wireMockConfig().dynamicPort().dynamicHttpsPort())
|
.options(wireMockConfig().dynamicPort().dynamicHttpsPort())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
private static final String GET_CREDENTIALS_PATH = "/v1/turn/keys/LMNOP/credentials/generate";
|
|
||||||
private static final String TURN_HOSTNAME = "localhost";
|
|
||||||
private ExecutorService httpExecutor;
|
private ExecutorService httpExecutor;
|
||||||
private ScheduledExecutorService retryExecutor;
|
private ScheduledExecutorService retryExecutor;
|
||||||
private DnsNameResolver dnsResolver;
|
private DnsNameResolver dnsResolver;
|
||||||
private Future<List<InetAddress>> dnsResult;
|
|
||||||
|
|
||||||
private CloudflareTurnCredentialsManager cloudflareTurnCredentialsManager = null;
|
private CloudflareTurnCredentialsManager cloudflareTurnCredentialsManager;
|
||||||
|
|
||||||
|
private static final String GET_CREDENTIALS_PATH = "/v1/turn/keys/LMNOP/credentials/generate";
|
||||||
|
private static final String TURN_HOSTNAME = "localhost";
|
||||||
|
|
||||||
|
private static final String API_TOKEN = RandomStringUtils.insecure().nextAlphanumeric(16);
|
||||||
|
private static final String USERNAME = RandomStringUtils.insecure().nextAlphanumeric(16);
|
||||||
|
private static final String CREDENTIAL = RandomStringUtils.insecure().nextAlphanumeric(16);
|
||||||
|
private static final List<String> CLOUDFLARE_TURN_URLS = List.of("turn:cf.example.com");
|
||||||
|
private static final Duration REQUESTED_CREDENTIAL_TTL = Duration.ofSeconds(100);
|
||||||
|
private static final Duration CLIENT_CREDENTIAL_TTL = REQUESTED_CREDENTIAL_TTL.dividedBy(2);
|
||||||
|
private static final List<String> IP_URL_PATTERNS = List.of("turn:%s", "turn:%s:80?transport=tcp", "turns:%s:443?transport=tcp");
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() throws CertificateException {
|
void setUp() {
|
||||||
httpExecutor = Executors.newSingleThreadExecutor();
|
httpExecutor = Executors.newSingleThreadExecutor();
|
||||||
retryExecutor = Executors.newSingleThreadScheduledExecutor();
|
retryExecutor = Executors.newSingleThreadScheduledExecutor();
|
||||||
|
|
||||||
dnsResolver = mock(DnsNameResolver.class);
|
dnsResolver = mock(DnsNameResolver.class);
|
||||||
dnsResult = mock(Future.class);
|
|
||||||
cloudflareTurnCredentialsManager = new CloudflareTurnCredentialsManager(
|
cloudflareTurnCredentialsManager = new CloudflareTurnCredentialsManager(
|
||||||
"API_TOKEN",
|
API_TOKEN,
|
||||||
"http://localhost:" + wireMock.getPort() + GET_CREDENTIALS_PATH,
|
"http://localhost:" + wireMock.getPort() + GET_CREDENTIALS_PATH,
|
||||||
100,
|
REQUESTED_CREDENTIAL_TTL,
|
||||||
List.of("turn:cf.example.com"),
|
CLIENT_CREDENTIAL_TTL,
|
||||||
List.of("turn:%s", "turn:%s:80?transport=tcp", "turns:%s:443?transport=tcp"),
|
CLOUDFLARE_TURN_URLS,
|
||||||
|
IP_URL_PATTERNS,
|
||||||
TURN_HOSTNAME,
|
TURN_HOSTNAME,
|
||||||
2,
|
2,
|
||||||
new CircuitBreakerConfiguration(),
|
new CircuitBreakerConfiguration(),
|
||||||
|
@ -73,26 +88,61 @@ public class CloudflareTurnCredentialsManagerTest {
|
||||||
@AfterEach
|
@AfterEach
|
||||||
void tearDown() throws InterruptedException {
|
void tearDown() throws InterruptedException {
|
||||||
httpExecutor.shutdown();
|
httpExecutor.shutdown();
|
||||||
httpExecutor.awaitTermination(1, TimeUnit.SECONDS);
|
|
||||||
retryExecutor.shutdown();
|
retryExecutor.shutdown();
|
||||||
|
|
||||||
|
//noinspection ResultOfMethodCallIgnored
|
||||||
|
httpExecutor.awaitTermination(1, TimeUnit.SECONDS);
|
||||||
|
|
||||||
|
//noinspection ResultOfMethodCallIgnored
|
||||||
retryExecutor.awaitTermination(1, TimeUnit.SECONDS);
|
retryExecutor.awaitTermination(1, TimeUnit.SECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSuccess() throws IOException, CancellationException, ExecutionException, InterruptedException {
|
public void testSuccess() throws IOException, CancellationException {
|
||||||
wireMock.stubFor(post(urlEqualTo(GET_CREDENTIALS_PATH))
|
wireMock.stubFor(post(urlEqualTo(GET_CREDENTIALS_PATH))
|
||||||
.willReturn(aResponse().withStatus(201).withHeader("Content-Type", new String[]{"application/json"}).withBody("{\"iceServers\":{\"urls\":[\"turn:cloudflare.example.com:3478?transport=udp\"],\"username\":\"ABC\",\"credential\":\"XYZ\"}}")));
|
.willReturn(created()
|
||||||
when(dnsResult.get())
|
.withHeader("Content-Type", "application/json")
|
||||||
.thenReturn(List.of(InetAddress.getByName("127.0.0.1"), InetAddress.getByName("::1")));
|
.withBody("""
|
||||||
|
{
|
||||||
|
"iceServers": {
|
||||||
|
"urls": [
|
||||||
|
"turn:cloudflare.example.com:3478?transport=udp"
|
||||||
|
],
|
||||||
|
"username": "%s",
|
||||||
|
"credential": "%s"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""".formatted(USERNAME, CREDENTIAL))));
|
||||||
|
|
||||||
when(dnsResolver.resolveAll(TURN_HOSTNAME))
|
when(dnsResolver.resolveAll(TURN_HOSTNAME))
|
||||||
.thenReturn(dnsResult);
|
.thenReturn(new SucceededFuture<>(GlobalEventExecutor.INSTANCE,
|
||||||
|
List.of(InetAddress.getByName("127.0.0.1"), InetAddress.getByName("::1"))));
|
||||||
|
|
||||||
TurnToken token = cloudflareTurnCredentialsManager.retrieveFromCloudflare();
|
TurnToken token = cloudflareTurnCredentialsManager.retrieveFromCloudflare();
|
||||||
|
|
||||||
assertThat(token.username()).isEqualTo("ABC");
|
wireMock.verify(postRequestedFor(urlEqualTo(GET_CREDENTIALS_PATH))
|
||||||
assertThat(token.password()).isEqualTo("XYZ");
|
.withHeader("Content-Type", equalTo("application/json"))
|
||||||
assertThat(token.hostname()).isEqualTo("localhost");
|
.withHeader("Authorization", equalTo("Bearer " + API_TOKEN))
|
||||||
assertThat(token.urlsWithIps()).containsAll(List.of("turn:127.0.0.1", "turn:127.0.0.1:80?transport=tcp", "turns:127.0.0.1:443?transport=tcp", "turn:[0:0:0:0:0:0:0:1]", "turn:[0:0:0:0:0:0:0:1]:80?transport=tcp", "turns:[0:0:0:0:0:0:0:1]:443?transport=tcp"));;
|
.withRequestBody(equalToJson("""
|
||||||
assertThat(token.urls()).isEqualTo(List.of("turn:cf.example.com"));
|
{
|
||||||
|
"ttl": %d
|
||||||
|
}
|
||||||
|
""".formatted(REQUESTED_CREDENTIAL_TTL.toSeconds()))));
|
||||||
|
|
||||||
|
assertThat(token.username()).isEqualTo(USERNAME);
|
||||||
|
assertThat(token.password()).isEqualTo(CREDENTIAL);
|
||||||
|
assertThat(token.hostname()).isEqualTo(TURN_HOSTNAME);
|
||||||
|
assertThat(token.urls()).isEqualTo(CLOUDFLARE_TURN_URLS);
|
||||||
|
assertThat(token.ttlSeconds()).isEqualTo(CLIENT_CREDENTIAL_TTL.toSeconds());
|
||||||
|
|
||||||
|
final List<String> expectedUrlsWithIps = new ArrayList<>();
|
||||||
|
|
||||||
|
for (final String ip : new String[] {"127.0.0.1", "[0:0:0:0:0:0:0:1]"}) {
|
||||||
|
for (final String pattern : IP_URL_PATTERNS) {
|
||||||
|
expectedUrlsWithIps.add(pattern.formatted(ip));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThat(token.urlsWithIps()).containsExactlyElementsOf(expectedUrlsWithIps);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package org.whispersystems.textsecuregcm.auth.grpc;
|
||||||
import io.grpc.Status;
|
import io.grpc.Status;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.signal.chat.rpc.GetAuthenticatedDeviceResponse;
|
import org.signal.chat.rpc.GetAuthenticatedDeviceResponse;
|
||||||
|
import org.whispersystems.textsecuregcm.grpc.ChannelNotFoundException;
|
||||||
import org.whispersystems.textsecuregcm.grpc.GrpcTestUtils;
|
import org.whispersystems.textsecuregcm.grpc.GrpcTestUtils;
|
||||||
import org.whispersystems.textsecuregcm.grpc.net.GrpcClientConnectionManager;
|
import org.whispersystems.textsecuregcm.grpc.net.GrpcClientConnectionManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.Device;
|
import org.whispersystems.textsecuregcm.storage.Device;
|
||||||
|
@ -22,7 +23,7 @@ class ProhibitAuthenticationInterceptorTest extends AbstractAuthenticationInterc
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void interceptCall() {
|
void interceptCall() throws ChannelNotFoundException {
|
||||||
final GrpcClientConnectionManager grpcClientConnectionManager = getClientConnectionManager();
|
final GrpcClientConnectionManager grpcClientConnectionManager = getClientConnectionManager();
|
||||||
|
|
||||||
when(grpcClientConnectionManager.getAuthenticatedDevice(any())).thenReturn(Optional.empty());
|
when(grpcClientConnectionManager.getAuthenticatedDevice(any())).thenReturn(Optional.empty());
|
||||||
|
@ -34,6 +35,10 @@ class ProhibitAuthenticationInterceptorTest extends AbstractAuthenticationInterc
|
||||||
final AuthenticatedDevice authenticatedDevice = new AuthenticatedDevice(UUID.randomUUID(), Device.PRIMARY_ID);
|
final AuthenticatedDevice authenticatedDevice = new AuthenticatedDevice(UUID.randomUUID(), Device.PRIMARY_ID);
|
||||||
when(grpcClientConnectionManager.getAuthenticatedDevice(any())).thenReturn(Optional.of(authenticatedDevice));
|
when(grpcClientConnectionManager.getAuthenticatedDevice(any())).thenReturn(Optional.of(authenticatedDevice));
|
||||||
|
|
||||||
GrpcTestUtils.assertStatusException(Status.UNAUTHENTICATED, this::getAuthenticatedDevice);
|
GrpcTestUtils.assertStatusException(Status.INTERNAL, this::getAuthenticatedDevice);
|
||||||
|
|
||||||
|
when(grpcClientConnectionManager.getAuthenticatedDevice(any())).thenThrow(ChannelNotFoundException.class);
|
||||||
|
|
||||||
|
GrpcTestUtils.assertStatusException(Status.UNAVAILABLE, this::getAuthenticatedDevice);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.signal.chat.rpc.GetAuthenticatedDeviceResponse;
|
import org.signal.chat.rpc.GetAuthenticatedDeviceResponse;
|
||||||
|
import org.whispersystems.textsecuregcm.grpc.ChannelNotFoundException;
|
||||||
import org.whispersystems.textsecuregcm.grpc.GrpcTestUtils;
|
import org.whispersystems.textsecuregcm.grpc.GrpcTestUtils;
|
||||||
import org.whispersystems.textsecuregcm.grpc.net.GrpcClientConnectionManager;
|
import org.whispersystems.textsecuregcm.grpc.net.GrpcClientConnectionManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.Device;
|
import org.whispersystems.textsecuregcm.storage.Device;
|
||||||
|
@ -22,12 +23,12 @@ class RequireAuthenticationInterceptorTest extends AbstractAuthenticationInterce
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void interceptCall() {
|
void interceptCall() throws ChannelNotFoundException {
|
||||||
final GrpcClientConnectionManager grpcClientConnectionManager = getClientConnectionManager();
|
final GrpcClientConnectionManager grpcClientConnectionManager = getClientConnectionManager();
|
||||||
|
|
||||||
when(grpcClientConnectionManager.getAuthenticatedDevice(any())).thenReturn(Optional.empty());
|
when(grpcClientConnectionManager.getAuthenticatedDevice(any())).thenReturn(Optional.empty());
|
||||||
|
|
||||||
GrpcTestUtils.assertStatusException(Status.UNAUTHENTICATED, this::getAuthenticatedDevice);
|
GrpcTestUtils.assertStatusException(Status.INTERNAL, this::getAuthenticatedDevice);
|
||||||
|
|
||||||
final AuthenticatedDevice authenticatedDevice = new AuthenticatedDevice(UUID.randomUUID(), Device.PRIMARY_ID);
|
final AuthenticatedDevice authenticatedDevice = new AuthenticatedDevice(UUID.randomUUID(), Device.PRIMARY_ID);
|
||||||
when(grpcClientConnectionManager.getAuthenticatedDevice(any())).thenReturn(Optional.of(authenticatedDevice));
|
when(grpcClientConnectionManager.getAuthenticatedDevice(any())).thenReturn(Optional.of(authenticatedDevice));
|
||||||
|
@ -35,5 +36,9 @@ class RequireAuthenticationInterceptorTest extends AbstractAuthenticationInterce
|
||||||
final GetAuthenticatedDeviceResponse response = getAuthenticatedDevice();
|
final GetAuthenticatedDeviceResponse response = getAuthenticatedDevice();
|
||||||
assertEquals(UUIDUtil.toByteString(authenticatedDevice.accountIdentifier()), response.getAccountIdentifier());
|
assertEquals(UUIDUtil.toByteString(authenticatedDevice.accountIdentifier()), response.getAccountIdentifier());
|
||||||
assertEquals(authenticatedDevice.deviceId(), response.getDeviceId());
|
assertEquals(authenticatedDevice.deviceId(), response.getDeviceId());
|
||||||
|
|
||||||
|
when(grpcClientConnectionManager.getAuthenticatedDevice(any())).thenThrow(ChannelNotFoundException.class);
|
||||||
|
|
||||||
|
GrpcTestUtils.assertStatusException(Status.UNAVAILABLE, this::getAuthenticatedDevice);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,14 +40,15 @@ class CallRoutingControllerV2Test {
|
||||||
private static final TurnToken CLOUDFLARE_TURN_TOKEN = new TurnToken(
|
private static final TurnToken CLOUDFLARE_TURN_TOKEN = new TurnToken(
|
||||||
"ABC",
|
"ABC",
|
||||||
"XYZ",
|
"XYZ",
|
||||||
|
43_200,
|
||||||
List.of("turn:cloudflare.example.com:3478?transport=udp"),
|
List.of("turn:cloudflare.example.com:3478?transport=udp"),
|
||||||
null,
|
null,
|
||||||
"cf.example.com");
|
"cf.example.com");
|
||||||
|
|
||||||
private static final RateLimiters rateLimiters = mock(RateLimiters.class);
|
private static final RateLimiters rateLimiters = mock(RateLimiters.class);
|
||||||
private static final RateLimiter getCallEndpointLimiter = mock(RateLimiter.class);
|
private static final RateLimiter getCallEndpointLimiter = mock(RateLimiter.class);
|
||||||
private static final CloudflareTurnCredentialsManager cloudflareTurnCredentialsManager = mock(
|
private static final CloudflareTurnCredentialsManager cloudflareTurnCredentialsManager =
|
||||||
CloudflareTurnCredentialsManager.class);
|
mock(CloudflareTurnCredentialsManager.class);
|
||||||
|
|
||||||
private static final ResourceExtension resources = ResourceExtension.builder()
|
private static final ResourceExtension resources = ResourceExtension.builder()
|
||||||
.addProvider(AuthHelper.getAuthFilter())
|
.addProvider(AuthHelper.getAuthFilter())
|
||||||
|
@ -66,21 +67,14 @@ class CallRoutingControllerV2Test {
|
||||||
|
|
||||||
@AfterEach
|
@AfterEach
|
||||||
void tearDown() {
|
void tearDown() {
|
||||||
reset( rateLimiters, getCallEndpointLimiter);
|
reset(rateLimiters, getCallEndpointLimiter);
|
||||||
}
|
|
||||||
|
|
||||||
void initializeMocksWith(TurnToken cloudflareToken) {
|
|
||||||
try {
|
|
||||||
when(cloudflareTurnCredentialsManager.retrieveFromCloudflare()).thenReturn(cloudflareToken);
|
|
||||||
} catch (IOException ignored) {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testGetRelaysBothRouting() {
|
void testGetRelaysBothRouting() throws IOException {
|
||||||
initializeMocksWith(CLOUDFLARE_TURN_TOKEN);
|
when(cloudflareTurnCredentialsManager.retrieveFromCloudflare()).thenReturn(CLOUDFLARE_TURN_TOKEN);
|
||||||
|
|
||||||
try (Response rawResponse = resources.getJerseyTest()
|
try (final Response rawResponse = resources.getJerseyTest()
|
||||||
.target(GET_CALL_RELAYS_PATH)
|
.target(GET_CALL_RELAYS_PATH)
|
||||||
.request()
|
.request()
|
||||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||||
|
@ -88,11 +82,8 @@ class CallRoutingControllerV2Test {
|
||||||
|
|
||||||
assertThat(rawResponse.getStatus()).isEqualTo(200);
|
assertThat(rawResponse.getStatus()).isEqualTo(200);
|
||||||
|
|
||||||
CallRoutingControllerV2.GetCallingRelaysResponse response = rawResponse.readEntity(
|
assertThat(rawResponse.readEntity(GetCallingRelaysResponse.class).relays())
|
||||||
CallRoutingControllerV2.GetCallingRelaysResponse.class);
|
.isEqualTo(List.of(CLOUDFLARE_TURN_TOKEN));
|
||||||
|
|
||||||
List<TurnToken> relays = response.relays();
|
|
||||||
assertThat(relays).isEqualTo(List.of(CLOUDFLARE_TURN_TOKEN));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -292,7 +292,7 @@ class MessageControllerTest {
|
||||||
assertThat("Good Response", response.getStatus(), is(equalTo(200)));
|
assertThat("Good Response", response.getStatus(), is(equalTo(200)));
|
||||||
|
|
||||||
@SuppressWarnings("unchecked") final ArgumentCaptor<Map<Byte, Envelope>> captor = ArgumentCaptor.forClass(Map.class);
|
@SuppressWarnings("unchecked") final ArgumentCaptor<Map<Byte, Envelope>> captor = ArgumentCaptor.forClass(Map.class);
|
||||||
verify(messageSender).sendMessages(any(), any(), captor.capture(), any(), any());
|
verify(messageSender).sendMessages(any(), any(), captor.capture(), any(), eq(Optional.empty()), any());
|
||||||
|
|
||||||
assertEquals(1, captor.getValue().size());
|
assertEquals(1, captor.getValue().size());
|
||||||
final Envelope message = captor.getValue().values().stream().findFirst().orElseThrow();
|
final Envelope message = captor.getValue().values().stream().findFirst().orElseThrow();
|
||||||
|
@ -319,7 +319,19 @@ class MessageControllerTest {
|
||||||
IncomingMessageList.class),
|
IncomingMessageList.class),
|
||||||
MediaType.APPLICATION_JSON_TYPE))) {
|
MediaType.APPLICATION_JSON_TYPE))) {
|
||||||
|
|
||||||
assertThat(response.getStatus(), is(equalTo(sendToPni ? 403 : 200)));
|
if (sendToPni) {
|
||||||
|
assertThat(response.getStatus(), is(equalTo(403)));
|
||||||
|
verify(messageSender, never()).sendMessages(any(), any(), any(), any(), any(), any());
|
||||||
|
} else {
|
||||||
|
assertThat(response.getStatus(), is(equalTo(200)));
|
||||||
|
|
||||||
|
verify(messageSender).sendMessages(any(),
|
||||||
|
eq(serviceIdentifier),
|
||||||
|
any(),
|
||||||
|
any(),
|
||||||
|
eq(Optional.of(Device.PRIMARY_ID)),
|
||||||
|
any());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -337,7 +349,7 @@ class MessageControllerTest {
|
||||||
assertThat("Good Response", response.getStatus(), is(equalTo(200)));
|
assertThat("Good Response", response.getStatus(), is(equalTo(200)));
|
||||||
|
|
||||||
@SuppressWarnings("unchecked") final ArgumentCaptor<Map<Byte, Envelope>> captor = ArgumentCaptor.forClass(Map.class);
|
@SuppressWarnings("unchecked") final ArgumentCaptor<Map<Byte, Envelope>> captor = ArgumentCaptor.forClass(Map.class);
|
||||||
verify(messageSender).sendMessages(any(), any(), captor.capture(), any(), any());
|
verify(messageSender).sendMessages(any(), any(), captor.capture(), any(), eq(Optional.empty()), any());
|
||||||
|
|
||||||
assertEquals(1, captor.getValue().size());
|
assertEquals(1, captor.getValue().size());
|
||||||
final Envelope message = captor.getValue().values().stream().findFirst().orElseThrow();
|
final Envelope message = captor.getValue().values().stream().findFirst().orElseThrow();
|
||||||
|
@ -362,7 +374,7 @@ class MessageControllerTest {
|
||||||
assertThat("Good Response", response.getStatus(), is(equalTo(200)));
|
assertThat("Good Response", response.getStatus(), is(equalTo(200)));
|
||||||
|
|
||||||
@SuppressWarnings("unchecked") final ArgumentCaptor<Map<Byte, Envelope>> captor = ArgumentCaptor.forClass(Map.class);
|
@SuppressWarnings("unchecked") final ArgumentCaptor<Map<Byte, Envelope>> captor = ArgumentCaptor.forClass(Map.class);
|
||||||
verify(messageSender).sendMessages(any(), any(), captor.capture(), any(), any());
|
verify(messageSender).sendMessages(any(), any(), captor.capture(), any(), eq(Optional.empty()), any());
|
||||||
|
|
||||||
assertEquals(1, captor.getValue().size());
|
assertEquals(1, captor.getValue().size());
|
||||||
final Envelope message = captor.getValue().values().stream().findFirst().orElseThrow();
|
final Envelope message = captor.getValue().values().stream().findFirst().orElseThrow();
|
||||||
|
@ -400,7 +412,7 @@ class MessageControllerTest {
|
||||||
assertThat("Good Response", response.getStatus(), is(equalTo(200)));
|
assertThat("Good Response", response.getStatus(), is(equalTo(200)));
|
||||||
|
|
||||||
@SuppressWarnings("unchecked") final ArgumentCaptor<Map<Byte, Envelope>> captor = ArgumentCaptor.forClass(Map.class);
|
@SuppressWarnings("unchecked") final ArgumentCaptor<Map<Byte, Envelope>> captor = ArgumentCaptor.forClass(Map.class);
|
||||||
verify(messageSender).sendMessages(any(), any(), captor.capture(), any(), any());
|
verify(messageSender).sendMessages(any(), any(), captor.capture(), any(), eq(Optional.empty()), any());
|
||||||
|
|
||||||
assertEquals(1, captor.getValue().size());
|
assertEquals(1, captor.getValue().size());
|
||||||
final Envelope message = captor.getValue().values().stream().findFirst().orElseThrow();
|
final Envelope message = captor.getValue().values().stream().findFirst().orElseThrow();
|
||||||
|
@ -439,7 +451,7 @@ class MessageControllerTest {
|
||||||
assertThat("Good Response", response.getStatus(), is(equalTo(expectedResponse)));
|
assertThat("Good Response", response.getStatus(), is(equalTo(expectedResponse)));
|
||||||
if (expectedResponse == 200) {
|
if (expectedResponse == 200) {
|
||||||
@SuppressWarnings("unchecked") final ArgumentCaptor<Map<Byte, Envelope>> captor = ArgumentCaptor.forClass(Map.class);
|
@SuppressWarnings("unchecked") final ArgumentCaptor<Map<Byte, Envelope>> captor = ArgumentCaptor.forClass(Map.class);
|
||||||
verify(messageSender).sendMessages(any(), any(), captor.capture(), any(), any());
|
verify(messageSender).sendMessages(any(), any(), captor.capture(), any(), eq(Optional.empty()), any());
|
||||||
|
|
||||||
assertEquals(1, captor.getValue().size());
|
assertEquals(1, captor.getValue().size());
|
||||||
final Envelope message = captor.getValue().values().stream().findFirst().orElseThrow();
|
final Envelope message = captor.getValue().values().stream().findFirst().orElseThrow();
|
||||||
|
@ -536,7 +548,7 @@ class MessageControllerTest {
|
||||||
@Test
|
@Test
|
||||||
void testMultiDeviceMissing() throws Exception {
|
void testMultiDeviceMissing() throws Exception {
|
||||||
doThrow(new MismatchedDevicesException(new MismatchedDevices(Set.of((byte) 2, (byte) 3), Collections.emptySet(), Collections.emptySet())))
|
doThrow(new MismatchedDevicesException(new MismatchedDevices(Set.of((byte) 2, (byte) 3), Collections.emptySet(), Collections.emptySet())))
|
||||||
.when(messageSender).sendMessages(any(), any(), any(), any(), any());
|
.when(messageSender).sendMessages(any(), any(), any(), any(), any(), any());
|
||||||
|
|
||||||
try (final Response response =
|
try (final Response response =
|
||||||
resources.getJerseyTest()
|
resources.getJerseyTest()
|
||||||
|
@ -558,7 +570,7 @@ class MessageControllerTest {
|
||||||
@Test
|
@Test
|
||||||
void testMultiDeviceExtra() throws Exception {
|
void testMultiDeviceExtra() throws Exception {
|
||||||
doThrow(new MismatchedDevicesException(new MismatchedDevices(Set.of((byte) 2), Set.of((byte) 4), Collections.emptySet())))
|
doThrow(new MismatchedDevicesException(new MismatchedDevices(Set.of((byte) 2), Set.of((byte) 4), Collections.emptySet())))
|
||||||
.when(messageSender).sendMessages(any(), any(), any(), any(), any());
|
.when(messageSender).sendMessages(any(), any(), any(), any(), any(), any());
|
||||||
|
|
||||||
try (final Response response =
|
try (final Response response =
|
||||||
resources.getJerseyTest()
|
resources.getJerseyTest()
|
||||||
|
@ -609,7 +621,7 @@ class MessageControllerTest {
|
||||||
@SuppressWarnings("unchecked") final ArgumentCaptor<Map<Byte, Envelope>> envelopeCaptor =
|
@SuppressWarnings("unchecked") final ArgumentCaptor<Map<Byte, Envelope>> envelopeCaptor =
|
||||||
ArgumentCaptor.forClass(Map.class);
|
ArgumentCaptor.forClass(Map.class);
|
||||||
|
|
||||||
verify(messageSender).sendMessages(any(Account.class), any(), envelopeCaptor.capture(), any(), any());
|
verify(messageSender).sendMessages(any(Account.class), any(), envelopeCaptor.capture(), any(), eq(Optional.empty()), any());
|
||||||
|
|
||||||
assertEquals(3, envelopeCaptor.getValue().size());
|
assertEquals(3, envelopeCaptor.getValue().size());
|
||||||
|
|
||||||
|
@ -633,7 +645,7 @@ class MessageControllerTest {
|
||||||
@SuppressWarnings("unchecked") final ArgumentCaptor<Map<Byte, Envelope>> envelopeCaptor =
|
@SuppressWarnings("unchecked") final ArgumentCaptor<Map<Byte, Envelope>> envelopeCaptor =
|
||||||
ArgumentCaptor.forClass(Map.class);
|
ArgumentCaptor.forClass(Map.class);
|
||||||
|
|
||||||
verify(messageSender).sendMessages(any(Account.class), any(), envelopeCaptor.capture(), any(), any());
|
verify(messageSender).sendMessages(any(Account.class), any(), envelopeCaptor.capture(), any(), eq(Optional.empty()), any());
|
||||||
|
|
||||||
assertEquals(3, envelopeCaptor.getValue().size());
|
assertEquals(3, envelopeCaptor.getValue().size());
|
||||||
|
|
||||||
|
@ -658,6 +670,7 @@ class MessageControllerTest {
|
||||||
any(),
|
any(),
|
||||||
argThat(messagesByDeviceId -> messagesByDeviceId.size() == 3),
|
argThat(messagesByDeviceId -> messagesByDeviceId.size() == 3),
|
||||||
any(),
|
any(),
|
||||||
|
eq(Optional.empty()),
|
||||||
any());
|
any());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -665,7 +678,7 @@ class MessageControllerTest {
|
||||||
@Test
|
@Test
|
||||||
void testRegistrationIdMismatch() throws Exception {
|
void testRegistrationIdMismatch() throws Exception {
|
||||||
doThrow(new MismatchedDevicesException(new MismatchedDevices(Collections.emptySet(), Collections.emptySet(), Set.of((byte) 2))))
|
doThrow(new MismatchedDevicesException(new MismatchedDevices(Collections.emptySet(), Collections.emptySet(), Set.of((byte) 2))))
|
||||||
.when(messageSender).sendMessages(any(), any(), any(), any(), any());
|
.when(messageSender).sendMessages(any(), any(), any(), any(), any(), any());
|
||||||
|
|
||||||
try (final Response response =
|
try (final Response response =
|
||||||
resources.getJerseyTest().target(String.format("/v1/messages/%s", MULTI_DEVICE_UUID))
|
resources.getJerseyTest().target(String.format("/v1/messages/%s", MULTI_DEVICE_UUID))
|
||||||
|
@ -1090,7 +1103,7 @@ class MessageControllerTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testValidateContentLength() throws MismatchedDevicesException, MessageTooLargeException, IOException {
|
void testValidateContentLength() throws MismatchedDevicesException, MessageTooLargeException, IOException {
|
||||||
doThrow(new MessageTooLargeException()).when(messageSender).sendMessages(any(), any(), any(), any(), any());
|
doThrow(new MessageTooLargeException()).when(messageSender).sendMessages(any(), any(), any(), any(), any(), any());
|
||||||
|
|
||||||
try (final Response response =
|
try (final Response response =
|
||||||
resources.getJerseyTest()
|
resources.getJerseyTest()
|
||||||
|
@ -1119,10 +1132,10 @@ class MessageControllerTest {
|
||||||
|
|
||||||
if (expectOk) {
|
if (expectOk) {
|
||||||
assertEquals(200, response.getStatus());
|
assertEquals(200, response.getStatus());
|
||||||
verify(messageSender).sendMessages(any(), any(), any(), any(), any());
|
verify(messageSender).sendMessages(any(), any(), any(), any(), any(), any());
|
||||||
} else {
|
} else {
|
||||||
assertEquals(422, response.getStatus());
|
assertEquals(422, response.getStatus());
|
||||||
verify(messageSender, never()).sendMessages(any(), any(), any(), any(), any());
|
verify(messageSender, never()).sendMessages(any(), any(), any(), any(), any(), any());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1140,6 +1140,58 @@ class ProfileControllerTest {
|
||||||
new AccountBadge("TEST2", Instant.ofEpochSecond(42 + 86400), false),
|
new AccountBadge("TEST2", Instant.ofEpochSecond(42 + 86400), false),
|
||||||
new AccountBadge("TEST3", Instant.ofEpochSecond(42 + 86400), false));
|
new AccountBadge("TEST3", Instant.ofEpochSecond(42 + 86400), false));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSetProfileBadgeAfterUpdateTries() throws Exception {
|
||||||
|
final ProfileKeyCommitment commitment = new ProfileKey(new byte[32]).getCommitment(
|
||||||
|
new ServiceId.Aci(AuthHelper.VALID_UUID));
|
||||||
|
|
||||||
|
final byte[] name = TestRandomUtil.nextBytes(81);
|
||||||
|
final byte[] emoji = TestRandomUtil.nextBytes(60);
|
||||||
|
final byte[] about = TestRandomUtil.nextBytes(156);
|
||||||
|
final String version = versionHex("anotherversion");
|
||||||
|
|
||||||
|
clearInvocations(AuthHelper.VALID_ACCOUNT_TWO);
|
||||||
|
reset(accountsManager);
|
||||||
|
final int accountsManagerUpdateRetryCount = 2;
|
||||||
|
AccountsHelper.setupMockUpdateWithRetries(accountsManager, accountsManagerUpdateRetryCount);
|
||||||
|
// set up two invocations -- one for each AccountsManager#update try
|
||||||
|
when(AuthHelper.VALID_ACCOUNT_TWO.getBadges())
|
||||||
|
.thenReturn(List.of(
|
||||||
|
new AccountBadge("TEST2", Instant.ofEpochSecond(42 + 86400), true),
|
||||||
|
new AccountBadge("TEST3", Instant.ofEpochSecond(42 + 86400), true)
|
||||||
|
))
|
||||||
|
.thenReturn(List.of(
|
||||||
|
new AccountBadge("TEST2", Instant.ofEpochSecond(42 + 86400), true),
|
||||||
|
new AccountBadge("TEST3", Instant.ofEpochSecond(42 + 86400), true),
|
||||||
|
new AccountBadge("TEST4", Instant.ofEpochSecond(43 + 86400), true)
|
||||||
|
));
|
||||||
|
|
||||||
|
try (final Response response = resources.getJerseyTest()
|
||||||
|
.target("/v1/profile/")
|
||||||
|
.request()
|
||||||
|
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_PASSWORD_TWO))
|
||||||
|
.put(Entity.entity(new CreateProfileRequest(commitment, version, name, emoji, about, null, false, false,
|
||||||
|
Optional.of(List.of("TEST1")), null), MediaType.APPLICATION_JSON_TYPE))) {
|
||||||
|
|
||||||
|
assertThat(response.getStatus()).isEqualTo(200);
|
||||||
|
assertThat(response.hasEntity()).isFalse();
|
||||||
|
|
||||||
|
//noinspection unchecked
|
||||||
|
final ArgumentCaptor<List<AccountBadge>> badgeCaptor = ArgumentCaptor.forClass(List.class);
|
||||||
|
verify(AuthHelper.VALID_ACCOUNT_TWO, times(accountsManagerUpdateRetryCount)).setBadges(refEq(clock), badgeCaptor.capture());
|
||||||
|
// since the stubbing of getBadges() is brittle, we need to verify the number of invocations, to protect against upstream changes
|
||||||
|
verify(AuthHelper.VALID_ACCOUNT_TWO, times(accountsManagerUpdateRetryCount)).getBadges();
|
||||||
|
|
||||||
|
final List<AccountBadge> badges = badgeCaptor.getValue();
|
||||||
|
assertThat(badges).isNotNull().hasSize(4).containsOnly(
|
||||||
|
new AccountBadge("TEST1", Instant.ofEpochSecond(42 + 86400), true),
|
||||||
|
new AccountBadge("TEST2", Instant.ofEpochSecond(42 + 86400), false),
|
||||||
|
new AccountBadge("TEST3", Instant.ofEpochSecond(42 + 86400), false),
|
||||||
|
new AccountBadge("TEST4", Instant.ofEpochSecond(43 + 86400), false));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
|
|
|
@ -7,6 +7,7 @@ package org.whispersystems.textsecuregcm.filters;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
import com.google.common.net.InetAddresses;
|
||||||
import com.google.protobuf.ByteString;
|
import com.google.protobuf.ByteString;
|
||||||
import io.dropwizard.core.Application;
|
import io.dropwizard.core.Application;
|
||||||
import io.dropwizard.core.Configuration;
|
import io.dropwizard.core.Configuration;
|
||||||
|
@ -24,7 +25,6 @@ import jakarta.ws.rs.GET;
|
||||||
import jakarta.ws.rs.Path;
|
import jakarta.ws.rs.Path;
|
||||||
import jakarta.ws.rs.client.Client;
|
import jakarta.ws.rs.client.Client;
|
||||||
import jakarta.ws.rs.core.Response;
|
import jakarta.ws.rs.core.Response;
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.EnumSet;
|
import java.util.EnumSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
@ -39,6 +39,7 @@ import org.signal.chat.rpc.EchoServiceGrpc;
|
||||||
import org.whispersystems.textsecuregcm.grpc.EchoServiceImpl;
|
import org.whispersystems.textsecuregcm.grpc.EchoServiceImpl;
|
||||||
import org.whispersystems.textsecuregcm.grpc.GrpcTestUtils;
|
import org.whispersystems.textsecuregcm.grpc.GrpcTestUtils;
|
||||||
import org.whispersystems.textsecuregcm.grpc.MockRequestAttributesInterceptor;
|
import org.whispersystems.textsecuregcm.grpc.MockRequestAttributesInterceptor;
|
||||||
|
import org.whispersystems.textsecuregcm.grpc.RequestAttributes;
|
||||||
import org.whispersystems.textsecuregcm.util.InetAddressRange;
|
import org.whispersystems.textsecuregcm.util.InetAddressRange;
|
||||||
|
|
||||||
@ExtendWith(DropwizardExtensionsSupport.class)
|
@ExtendWith(DropwizardExtensionsSupport.class)
|
||||||
|
@ -157,7 +158,7 @@ class ExternalRequestFilterTest {
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() throws Exception {
|
void setUp() throws Exception {
|
||||||
final MockRequestAttributesInterceptor mockRequestAttributesInterceptor = new MockRequestAttributesInterceptor();
|
final MockRequestAttributesInterceptor mockRequestAttributesInterceptor = new MockRequestAttributesInterceptor();
|
||||||
mockRequestAttributesInterceptor.setRemoteAddress(InetAddress.getByName("127.0.0.1"));
|
mockRequestAttributesInterceptor.setRequestAttributes(new RequestAttributes(InetAddresses.forString("127.0.0.1"), null, null));
|
||||||
|
|
||||||
testServer = InProcessServerBuilder.forName("ExternalRequestFilterTest")
|
testServer = InProcessServerBuilder.forName("ExternalRequestFilterTest")
|
||||||
.directExecutor()
|
.directExecutor()
|
||||||
|
|
|
@ -15,6 +15,7 @@ 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.common.net.InetAddresses;
|
||||||
import com.google.protobuf.ByteString;
|
import com.google.protobuf.ByteString;
|
||||||
import com.vdurmont.semver4j.Semver;
|
import com.vdurmont.semver4j.Semver;
|
||||||
import io.grpc.ManagedChannel;
|
import io.grpc.ManagedChannel;
|
||||||
|
@ -40,11 +41,10 @@ import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfigurati
|
||||||
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.EchoServiceImpl;
|
||||||
import org.whispersystems.textsecuregcm.grpc.MockRequestAttributesInterceptor;
|
import org.whispersystems.textsecuregcm.grpc.MockRequestAttributesInterceptor;
|
||||||
|
import org.whispersystems.textsecuregcm.grpc.RequestAttributes;
|
||||||
import org.whispersystems.textsecuregcm.grpc.StatusConstants;
|
import org.whispersystems.textsecuregcm.grpc.StatusConstants;
|
||||||
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.UserAgentUtil;
|
|
||||||
|
|
||||||
class RemoteDeprecationFilterTest {
|
class RemoteDeprecationFilterTest {
|
||||||
|
|
||||||
|
@ -130,11 +130,7 @@ class RemoteDeprecationFilterTest {
|
||||||
@MethodSource(value="testFilter")
|
@MethodSource(value="testFilter")
|
||||||
void testGrpcFilter(final String userAgentString, final boolean expectDeprecation) throws IOException, InterruptedException {
|
void testGrpcFilter(final String userAgentString, final boolean expectDeprecation) throws IOException, InterruptedException {
|
||||||
final MockRequestAttributesInterceptor mockRequestAttributesInterceptor = new MockRequestAttributesInterceptor();
|
final MockRequestAttributesInterceptor mockRequestAttributesInterceptor = new MockRequestAttributesInterceptor();
|
||||||
|
mockRequestAttributesInterceptor.setRequestAttributes(new RequestAttributes(InetAddresses.forString("127.0.0.1"), userAgentString, null));
|
||||||
try {
|
|
||||||
mockRequestAttributesInterceptor.setUserAgent(UserAgentUtil.parseUserAgentString(userAgentString));
|
|
||||||
} catch (UnrecognizedUserAgentException ignored) {
|
|
||||||
}
|
|
||||||
|
|
||||||
final Server testServer = InProcessServerBuilder.forName("RemoteDeprecationFilterTest")
|
final Server testServer = InProcessServerBuilder.forName("RemoteDeprecationFilterTest")
|
||||||
.directExecutor()
|
.directExecutor()
|
||||||
|
|
|
@ -72,7 +72,8 @@ class AccountsAnonymousGrpcServiceTest extends
|
||||||
|
|
||||||
when(rateLimiter.validateReactive(anyString())).thenReturn(Mono.empty());
|
when(rateLimiter.validateReactive(anyString())).thenReturn(Mono.empty());
|
||||||
|
|
||||||
getMockRequestAttributesInterceptor().setRemoteAddress(InetAddresses.forString("127.0.0.1"));
|
getMockRequestAttributesInterceptor().setRequestAttributes(
|
||||||
|
new RequestAttributes(InetAddresses.forString("127.0.0.1"), null, null));
|
||||||
|
|
||||||
return new AccountsAnonymousGrpcService(accountsManager, rateLimiters);
|
return new AccountsAnonymousGrpcService(accountsManager, rateLimiters);
|
||||||
}
|
}
|
||||||
|
|
|
@ -215,6 +215,7 @@ class MessagesAnonymousGrpcServiceTest extends
|
||||||
serviceIdentifier,
|
serviceIdentifier,
|
||||||
Map.of(deviceId, expectedEnvelopeBuilder.build()),
|
Map.of(deviceId, expectedEnvelopeBuilder.build()),
|
||||||
Map.of(deviceId, registrationId),
|
Map.of(deviceId, registrationId),
|
||||||
|
Optional.empty(),
|
||||||
null);
|
null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -238,7 +239,7 @@ class MessagesAnonymousGrpcServiceTest extends
|
||||||
|
|
||||||
doThrow(new MismatchedDevicesException(new org.whispersystems.textsecuregcm.controllers.MismatchedDevices(
|
doThrow(new MismatchedDevicesException(new org.whispersystems.textsecuregcm.controllers.MismatchedDevices(
|
||||||
Set.of(missingDeviceId), Set.of(extraDeviceId), Set.of(staleDeviceId))))
|
Set.of(missingDeviceId), Set.of(extraDeviceId), Set.of(staleDeviceId))))
|
||||||
.when(messageSender).sendMessages(any(), any(), any(), any(), any());
|
.when(messageSender).sendMessages(any(), any(), any(), any(), any(), any());
|
||||||
|
|
||||||
final SendMessageResponse response = unauthenticatedServiceStub().sendSingleRecipientMessage(
|
final SendMessageResponse response = unauthenticatedServiceStub().sendSingleRecipientMessage(
|
||||||
generateRequest(serviceIdentifier, false, true, messages, UNIDENTIFIED_ACCESS_KEY, null));
|
generateRequest(serviceIdentifier, false, true, messages, UNIDENTIFIED_ACCESS_KEY, null));
|
||||||
|
@ -290,7 +291,7 @@ class MessagesAnonymousGrpcServiceTest extends
|
||||||
useUak ? incorrectUnidentifiedAccessKey : null,
|
useUak ? incorrectUnidentifiedAccessKey : null,
|
||||||
useUak ? null : incorrectGroupSendToken)));
|
useUak ? null : incorrectGroupSendToken)));
|
||||||
|
|
||||||
verify(messageSender, never()).sendMessages(any(), any(), any(), any(), any());
|
verify(messageSender, never()).sendMessages(any(), any(), any(), any(), any(), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -308,7 +309,7 @@ class MessagesAnonymousGrpcServiceTest extends
|
||||||
() -> unauthenticatedServiceStub().sendSingleRecipientMessage(
|
() -> unauthenticatedServiceStub().sendSingleRecipientMessage(
|
||||||
generateRequest(serviceIdentifier, false, true, messages, UNIDENTIFIED_ACCESS_KEY, null)));
|
generateRequest(serviceIdentifier, false, true, messages, UNIDENTIFIED_ACCESS_KEY, null)));
|
||||||
|
|
||||||
verify(messageSender, never()).sendMessages(any(), any(), any(), any(), any());
|
verify(messageSender, never()).sendMessages(any(), any(), any(), any(), any(), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -341,7 +342,7 @@ class MessagesAnonymousGrpcServiceTest extends
|
||||||
() -> unauthenticatedServiceStub().sendSingleRecipientMessage(
|
() -> unauthenticatedServiceStub().sendSingleRecipientMessage(
|
||||||
generateRequest(serviceIdentifier, false, true, messages, UNIDENTIFIED_ACCESS_KEY, null)));
|
generateRequest(serviceIdentifier, false, true, messages, UNIDENTIFIED_ACCESS_KEY, null)));
|
||||||
|
|
||||||
verify(messageSender, never()).sendMessages(any(), any(), any(), any(), any());
|
verify(messageSender, never()).sendMessages(any(), any(), any(), any(), any(), any());
|
||||||
verify(messageByteLimitEstimator).add(serviceIdentifier.uuid().toString());
|
verify(messageByteLimitEstimator).add(serviceIdentifier.uuid().toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -364,7 +365,7 @@ class MessagesAnonymousGrpcServiceTest extends
|
||||||
.build());
|
.build());
|
||||||
|
|
||||||
doThrow(new MessageTooLargeException())
|
doThrow(new MessageTooLargeException())
|
||||||
.when(messageSender).sendMessages(any(), any(), any(), any(), any());
|
.when(messageSender).sendMessages(any(), any(), any(), any(), any(), any());
|
||||||
|
|
||||||
//noinspection ResultOfMethodCallIgnored
|
//noinspection ResultOfMethodCallIgnored
|
||||||
GrpcTestUtils.assertStatusException(Status.INVALID_ARGUMENT,
|
GrpcTestUtils.assertStatusException(Status.INVALID_ARGUMENT,
|
||||||
|
@ -406,7 +407,7 @@ class MessagesAnonymousGrpcServiceTest extends
|
||||||
Optional.of(destinationAccount),
|
Optional.of(destinationAccount),
|
||||||
serviceIdentifier);
|
serviceIdentifier);
|
||||||
|
|
||||||
verify(messageSender, never()).sendMessages(any(), any(), any(), any(), any());
|
verify(messageSender, never()).sendMessages(any(), any(), any(), any(), any(), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -446,7 +447,7 @@ class MessagesAnonymousGrpcServiceTest extends
|
||||||
Optional.of(destinationAccount),
|
Optional.of(destinationAccount),
|
||||||
serviceIdentifier);
|
serviceIdentifier);
|
||||||
|
|
||||||
verify(messageSender, never()).sendMessages(any(), any(), any(), any(), any());
|
verify(messageSender, never()).sendMessages(any(), any(), any(), any(), any(), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static SendSealedSenderMessageRequest generateRequest(final ServiceIdentifier serviceIdentifier,
|
private static SendSealedSenderMessageRequest generateRequest(final ServiceIdentifier serviceIdentifier,
|
||||||
|
@ -873,6 +874,7 @@ class MessagesAnonymousGrpcServiceTest extends
|
||||||
serviceIdentifier,
|
serviceIdentifier,
|
||||||
Map.of(deviceId, expectedEnvelopeBuilder.build()),
|
Map.of(deviceId, expectedEnvelopeBuilder.build()),
|
||||||
Map.of(deviceId, registrationId),
|
Map.of(deviceId, registrationId),
|
||||||
|
Optional.empty(),
|
||||||
null);
|
null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -896,7 +898,7 @@ class MessagesAnonymousGrpcServiceTest extends
|
||||||
|
|
||||||
doThrow(new MismatchedDevicesException(new org.whispersystems.textsecuregcm.controllers.MismatchedDevices(
|
doThrow(new MismatchedDevicesException(new org.whispersystems.textsecuregcm.controllers.MismatchedDevices(
|
||||||
Set.of(missingDeviceId), Set.of(extraDeviceId), Set.of(staleDeviceId))))
|
Set.of(missingDeviceId), Set.of(extraDeviceId), Set.of(staleDeviceId))))
|
||||||
.when(messageSender).sendMessages(any(), any(), any(), any(), any());
|
.when(messageSender).sendMessages(any(), any(), any(), any(), any(), any());
|
||||||
|
|
||||||
final SendMessageResponse response = unauthenticatedServiceStub().sendStory(
|
final SendMessageResponse response = unauthenticatedServiceStub().sendStory(
|
||||||
generateRequest(serviceIdentifier, false, messages));
|
generateRequest(serviceIdentifier, false, messages));
|
||||||
|
@ -926,7 +928,7 @@ class MessagesAnonymousGrpcServiceTest extends
|
||||||
|
|
||||||
assertEquals(SendMessageResponse.newBuilder().build(), response);
|
assertEquals(SendMessageResponse.newBuilder().build(), response);
|
||||||
|
|
||||||
verify(messageSender, never()).sendMessages(any(), any(), any(), any(), any());
|
verify(messageSender, never()).sendMessages(any(), any(), any(), any(), any(), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -957,7 +959,7 @@ class MessagesAnonymousGrpcServiceTest extends
|
||||||
GrpcTestUtils.assertRateLimitExceeded(retryDuration,
|
GrpcTestUtils.assertRateLimitExceeded(retryDuration,
|
||||||
() -> unauthenticatedServiceStub().sendStory(generateRequest(serviceIdentifier, true, messages)));
|
() -> unauthenticatedServiceStub().sendStory(generateRequest(serviceIdentifier, true, messages)));
|
||||||
|
|
||||||
verify(messageSender, never()).sendMessages(any(), any(), any(), any(), any());
|
verify(messageSender, never()).sendMessages(any(), any(), any(), any(), any(), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -978,7 +980,7 @@ class MessagesAnonymousGrpcServiceTest extends
|
||||||
.setPayload(ByteString.copyFrom(TestRandomUtil.nextBytes(128)))
|
.setPayload(ByteString.copyFrom(TestRandomUtil.nextBytes(128)))
|
||||||
.build());
|
.build());
|
||||||
|
|
||||||
doThrow(new MessageTooLargeException()).when(messageSender).sendMessages(any(), any(), any(), any(), any());
|
doThrow(new MessageTooLargeException()).when(messageSender).sendMessages(any(), any(), any(), any(), any(), any());
|
||||||
|
|
||||||
//noinspection ResultOfMethodCallIgnored
|
//noinspection ResultOfMethodCallIgnored
|
||||||
GrpcTestUtils.assertStatusInvalidArgument(
|
GrpcTestUtils.assertStatusInvalidArgument(
|
||||||
|
@ -1017,7 +1019,7 @@ class MessagesAnonymousGrpcServiceTest extends
|
||||||
Optional.of(destinationAccount),
|
Optional.of(destinationAccount),
|
||||||
serviceIdentifier);
|
serviceIdentifier);
|
||||||
|
|
||||||
verify(messageSender, never()).sendMessages(any(), any(), any(), any(), any());
|
verify(messageSender, never()).sendMessages(any(), any(), any(), any(), any(), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -1056,7 +1058,7 @@ class MessagesAnonymousGrpcServiceTest extends
|
||||||
Optional.of(destinationAccount),
|
Optional.of(destinationAccount),
|
||||||
serviceIdentifier);
|
serviceIdentifier);
|
||||||
|
|
||||||
verify(messageSender, never()).sendMessages(any(), any(), any(), any(), any());
|
verify(messageSender, never()).sendMessages(any(), any(), any(), any(), any(), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static SendStoryMessageRequest generateRequest(final ServiceIdentifier serviceIdentifier,
|
private static SendStoryMessageRequest generateRequest(final ServiceIdentifier serviceIdentifier,
|
||||||
|
|
|
@ -218,6 +218,7 @@ class MessagesGrpcServiceTest extends SimpleBaseGrpcTest<MessagesGrpcService, Me
|
||||||
serviceIdentifier,
|
serviceIdentifier,
|
||||||
Map.of(deviceId, expectedEnvelopeBuilder.build()),
|
Map.of(deviceId, expectedEnvelopeBuilder.build()),
|
||||||
Map.of(deviceId, registrationId),
|
Map.of(deviceId, registrationId),
|
||||||
|
Optional.empty(),
|
||||||
null);
|
null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -240,7 +241,7 @@ class MessagesGrpcServiceTest extends SimpleBaseGrpcTest<MessagesGrpcService, Me
|
||||||
|
|
||||||
doThrow(new MismatchedDevicesException(new org.whispersystems.textsecuregcm.controllers.MismatchedDevices(
|
doThrow(new MismatchedDevicesException(new org.whispersystems.textsecuregcm.controllers.MismatchedDevices(
|
||||||
Set.of(missingDeviceId), Set.of(extraDeviceId), Set.of(staleDeviceId))))
|
Set.of(missingDeviceId), Set.of(extraDeviceId), Set.of(staleDeviceId))))
|
||||||
.when(messageSender).sendMessages(any(), any(), any(), any(), any());
|
.when(messageSender).sendMessages(any(), any(), any(), any(), any(), any());
|
||||||
|
|
||||||
final SendMessageResponse response = authenticatedServiceStub().sendMessage(
|
final SendMessageResponse response = authenticatedServiceStub().sendMessage(
|
||||||
generateRequest(serviceIdentifier, AuthenticatedSenderMessageType.DOUBLE_RATCHET, false, true, messages));
|
generateRequest(serviceIdentifier, AuthenticatedSenderMessageType.DOUBLE_RATCHET, false, true, messages));
|
||||||
|
@ -272,7 +273,7 @@ class MessagesGrpcServiceTest extends SimpleBaseGrpcTest<MessagesGrpcService, Me
|
||||||
() -> authenticatedServiceStub().sendMessage(
|
() -> authenticatedServiceStub().sendMessage(
|
||||||
generateRequest(serviceIdentifier, AuthenticatedSenderMessageType.DOUBLE_RATCHET, false, true, messages)));
|
generateRequest(serviceIdentifier, AuthenticatedSenderMessageType.DOUBLE_RATCHET, false, true, messages)));
|
||||||
|
|
||||||
verify(messageSender, never()).sendMessages(any(), any(), any(), any(), any());
|
verify(messageSender, never()).sendMessages(any(), any(), any(), any(), any(), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -305,7 +306,7 @@ class MessagesGrpcServiceTest extends SimpleBaseGrpcTest<MessagesGrpcService, Me
|
||||||
() -> authenticatedServiceStub().sendMessage(
|
() -> authenticatedServiceStub().sendMessage(
|
||||||
generateRequest(serviceIdentifier, AuthenticatedSenderMessageType.DOUBLE_RATCHET, false, true, messages)));
|
generateRequest(serviceIdentifier, AuthenticatedSenderMessageType.DOUBLE_RATCHET, false, true, messages)));
|
||||||
|
|
||||||
verify(messageSender, never()).sendMessages(any(), any(), any(), any(), any());
|
verify(messageSender, never()).sendMessages(any(), any(), any(), any(), any(), any());
|
||||||
verify(messageByteLimitEstimator).add(serviceIdentifier.uuid().toString());
|
verify(messageByteLimitEstimator).add(serviceIdentifier.uuid().toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -327,7 +328,7 @@ class MessagesGrpcServiceTest extends SimpleBaseGrpcTest<MessagesGrpcService, Me
|
||||||
.build());
|
.build());
|
||||||
|
|
||||||
doThrow(new MessageTooLargeException())
|
doThrow(new MessageTooLargeException())
|
||||||
.when(messageSender).sendMessages(any(), any(), any(), any(), any());
|
.when(messageSender).sendMessages(any(), any(), any(), any(), any(), any());
|
||||||
|
|
||||||
//noinspection ResultOfMethodCallIgnored
|
//noinspection ResultOfMethodCallIgnored
|
||||||
GrpcTestUtils.assertStatusException(Status.INVALID_ARGUMENT,
|
GrpcTestUtils.assertStatusException(Status.INVALID_ARGUMENT,
|
||||||
|
@ -368,7 +369,7 @@ class MessagesGrpcServiceTest extends SimpleBaseGrpcTest<MessagesGrpcService, Me
|
||||||
Optional.of(destinationAccount),
|
Optional.of(destinationAccount),
|
||||||
serviceIdentifier);
|
serviceIdentifier);
|
||||||
|
|
||||||
verify(messageSender, never()).sendMessages(any(), any(), any(), any(), any());
|
verify(messageSender, never()).sendMessages(any(), any(), any(), any(), any(), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -407,7 +408,7 @@ class MessagesGrpcServiceTest extends SimpleBaseGrpcTest<MessagesGrpcService, Me
|
||||||
Optional.of(destinationAccount),
|
Optional.of(destinationAccount),
|
||||||
serviceIdentifier);
|
serviceIdentifier);
|
||||||
|
|
||||||
verify(messageSender, never()).sendMessages(any(), any(), any(), any(), any());
|
verify(messageSender, never()).sendMessages(any(), any(), any(), any(), any(), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static SendAuthenticatedSenderMessageRequest generateRequest(final ServiceIdentifier serviceIdentifier,
|
private static SendAuthenticatedSenderMessageRequest generateRequest(final ServiceIdentifier serviceIdentifier,
|
||||||
|
@ -515,6 +516,7 @@ class MessagesGrpcServiceTest extends SimpleBaseGrpcTest<MessagesGrpcService, Me
|
||||||
expectedEnvelopes,
|
expectedEnvelopes,
|
||||||
Map.of(LINKED_DEVICE_ID, LINKED_DEVICE_REGISTRATION_ID,
|
Map.of(LINKED_DEVICE_ID, LINKED_DEVICE_REGISTRATION_ID,
|
||||||
SECOND_LINKED_DEVICE_ID, SECOND_LINKED_DEVICE_REGISTRATION_ID),
|
SECOND_LINKED_DEVICE_ID, SECOND_LINKED_DEVICE_REGISTRATION_ID),
|
||||||
|
Optional.of(AUTHENTICATED_DEVICE_ID),
|
||||||
null);
|
null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -532,7 +534,7 @@ class MessagesGrpcServiceTest extends SimpleBaseGrpcTest<MessagesGrpcService, Me
|
||||||
|
|
||||||
doThrow(new MismatchedDevicesException(new org.whispersystems.textsecuregcm.controllers.MismatchedDevices(
|
doThrow(new MismatchedDevicesException(new org.whispersystems.textsecuregcm.controllers.MismatchedDevices(
|
||||||
Set.of(missingDeviceId), Set.of(extraDeviceId), Set.of(staleDeviceId))))
|
Set.of(missingDeviceId), Set.of(extraDeviceId), Set.of(staleDeviceId))))
|
||||||
.when(messageSender).sendMessages(any(), any(), any(), any(), any());
|
.when(messageSender).sendMessages(any(), any(), any(), any(), any(), any());
|
||||||
|
|
||||||
final SendMessageResponse response = authenticatedServiceStub().sendSyncMessage(
|
final SendMessageResponse response = authenticatedServiceStub().sendSyncMessage(
|
||||||
generateRequest(AuthenticatedSenderMessageType.DOUBLE_RATCHET, true, messages));
|
generateRequest(AuthenticatedSenderMessageType.DOUBLE_RATCHET, true, messages));
|
||||||
|
@ -566,7 +568,7 @@ class MessagesGrpcServiceTest extends SimpleBaseGrpcTest<MessagesGrpcService, Me
|
||||||
() -> authenticatedServiceStub().sendSyncMessage(
|
() -> authenticatedServiceStub().sendSyncMessage(
|
||||||
generateRequest(AuthenticatedSenderMessageType.DOUBLE_RATCHET, true, messages)));
|
generateRequest(AuthenticatedSenderMessageType.DOUBLE_RATCHET, true, messages)));
|
||||||
|
|
||||||
verify(messageSender, never()).sendMessages(any(), any(), any(), any(), any());
|
verify(messageSender, never()).sendMessages(any(), any(), any(), any(), any(), any());
|
||||||
verify(messageByteLimitEstimator).add(AUTHENTICATED_ACI.toString());
|
verify(messageByteLimitEstimator).add(AUTHENTICATED_ACI.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -588,7 +590,7 @@ class MessagesGrpcServiceTest extends SimpleBaseGrpcTest<MessagesGrpcService, Me
|
||||||
.build());
|
.build());
|
||||||
|
|
||||||
doThrow(new MessageTooLargeException())
|
doThrow(new MessageTooLargeException())
|
||||||
.when(messageSender).sendMessages(any(), any(), any(), any(), any());
|
.when(messageSender).sendMessages(any(), any(), any(), any(), any(), any());
|
||||||
|
|
||||||
//noinspection ResultOfMethodCallIgnored
|
//noinspection ResultOfMethodCallIgnored
|
||||||
GrpcTestUtils.assertStatusException(Status.INVALID_ARGUMENT,
|
GrpcTestUtils.assertStatusException(Status.INVALID_ARGUMENT,
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
package org.whispersystems.textsecuregcm.grpc;
|
package org.whispersystems.textsecuregcm.grpc;
|
||||||
|
|
||||||
|
import com.google.common.net.InetAddresses;
|
||||||
import io.grpc.Context;
|
import io.grpc.Context;
|
||||||
import io.grpc.Contexts;
|
import io.grpc.Contexts;
|
||||||
import io.grpc.Metadata;
|
import io.grpc.Metadata;
|
||||||
|
@ -19,25 +20,10 @@ import org.whispersystems.textsecuregcm.util.ua.UserAgent;
|
||||||
|
|
||||||
public class MockRequestAttributesInterceptor implements ServerInterceptor {
|
public class MockRequestAttributesInterceptor implements ServerInterceptor {
|
||||||
|
|
||||||
@Nullable
|
private RequestAttributes requestAttributes = new RequestAttributes(InetAddresses.forString("127.0.0.1"), null, null);
|
||||||
private InetAddress remoteAddress;
|
|
||||||
|
|
||||||
@Nullable
|
public void setRequestAttributes(final RequestAttributes requestAttributes) {
|
||||||
private UserAgent userAgent;
|
this.requestAttributes = requestAttributes;
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private List<Locale.LanguageRange> acceptLanguage;
|
|
||||||
|
|
||||||
public void setRemoteAddress(@Nullable final InetAddress remoteAddress) {
|
|
||||||
this.remoteAddress = remoteAddress;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setUserAgent(@Nullable final UserAgent userAgent) {
|
|
||||||
this.userAgent = userAgent;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setAcceptLanguage(@Nullable final List<Locale.LanguageRange> acceptLanguage) {
|
|
||||||
this.acceptLanguage = acceptLanguage;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -45,20 +31,7 @@ public class MockRequestAttributesInterceptor implements ServerInterceptor {
|
||||||
final Metadata headers,
|
final Metadata headers,
|
||||||
final ServerCallHandler<ReqT, RespT> next) {
|
final ServerCallHandler<ReqT, RespT> next) {
|
||||||
|
|
||||||
Context context = Context.current();
|
return Contexts.interceptCall(Context.current()
|
||||||
|
.withValue(RequestAttributesUtil.REQUEST_ATTRIBUTES_CONTEXT_KEY, requestAttributes), serverCall, headers, next);
|
||||||
if (remoteAddress != null) {
|
|
||||||
context = context.withValue(RequestAttributesUtil.REMOTE_ADDRESS_CONTEXT_KEY, remoteAddress);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userAgent != null) {
|
|
||||||
context = context.withValue(RequestAttributesUtil.USER_AGENT_CONTEXT_KEY, userAgent);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (acceptLanguage != null) {
|
|
||||||
context = context.withValue(RequestAttributesUtil.ACCEPT_LANGUAGE_CONTEXT_KEY, acceptLanguage);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Contexts.interceptCall(context, serverCall, headers, next);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ import static org.mockito.Mockito.verifyNoInteractions;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
import static org.whispersystems.textsecuregcm.grpc.GrpcTestUtils.assertStatusException;
|
import static org.whispersystems.textsecuregcm.grpc.GrpcTestUtils.assertStatusException;
|
||||||
|
|
||||||
|
import com.google.common.net.InetAddresses;
|
||||||
import com.google.protobuf.ByteString;
|
import com.google.protobuf.ByteString;
|
||||||
import io.grpc.Status;
|
import io.grpc.Status;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
@ -75,8 +76,6 @@ import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
||||||
import org.whispersystems.textsecuregcm.tests.util.ProfileTestHelper;
|
import org.whispersystems.textsecuregcm.tests.util.ProfileTestHelper;
|
||||||
import org.whispersystems.textsecuregcm.util.TestRandomUtil;
|
import org.whispersystems.textsecuregcm.util.TestRandomUtil;
|
||||||
import org.whispersystems.textsecuregcm.util.UUIDUtil;
|
import org.whispersystems.textsecuregcm.util.UUIDUtil;
|
||||||
import org.whispersystems.textsecuregcm.util.ua.UnrecognizedUserAgentException;
|
|
||||||
import org.whispersystems.textsecuregcm.util.ua.UserAgentUtil;
|
|
||||||
|
|
||||||
public class ProfileAnonymousGrpcServiceTest extends SimpleBaseGrpcTest<ProfileAnonymousGrpcService, ProfileAnonymousGrpc.ProfileAnonymousBlockingStub> {
|
public class ProfileAnonymousGrpcServiceTest extends SimpleBaseGrpcTest<ProfileAnonymousGrpcService, ProfileAnonymousGrpc.ProfileAnonymousBlockingStub> {
|
||||||
|
|
||||||
|
@ -96,13 +95,9 @@ public class ProfileAnonymousGrpcServiceTest extends SimpleBaseGrpcTest<ProfileA
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected ProfileAnonymousGrpcService createServiceBeforeEachTest() {
|
protected ProfileAnonymousGrpcService createServiceBeforeEachTest() {
|
||||||
getMockRequestAttributesInterceptor().setAcceptLanguage(Locale.LanguageRange.parse("en-us"));
|
getMockRequestAttributesInterceptor().setRequestAttributes(new RequestAttributes(InetAddresses.forString("127.0.0.1"),
|
||||||
|
"Signal-Android/1.2.3",
|
||||||
try {
|
Locale.LanguageRange.parse("en-us")));
|
||||||
getMockRequestAttributesInterceptor().setUserAgent(UserAgentUtil.parseUserAgentString("Signal-Android/1.2.3"));
|
|
||||||
} catch (final UnrecognizedUserAgentException e) {
|
|
||||||
throw new IllegalArgumentException(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ProfileAnonymousGrpcService(
|
return new ProfileAnonymousGrpcService(
|
||||||
accountsManager,
|
accountsManager,
|
||||||
|
|
|
@ -15,13 +15,16 @@ import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.anyBoolean;
|
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||||
import static org.mockito.ArgumentMatchers.anyString;
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.ArgumentMatchers.refEq;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.verifyNoInteractions;
|
import static org.mockito.Mockito.verifyNoInteractions;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
import static org.whispersystems.textsecuregcm.grpc.GrpcTestUtils.assertRateLimitExceeded;
|
import static org.whispersystems.textsecuregcm.grpc.GrpcTestUtils.assertRateLimitExceeded;
|
||||||
import static org.whispersystems.textsecuregcm.grpc.GrpcTestUtils.assertStatusException;
|
import static org.whispersystems.textsecuregcm.grpc.GrpcTestUtils.assertStatusException;
|
||||||
|
|
||||||
|
import com.google.common.net.InetAddresses;
|
||||||
import com.google.i18n.phonenumbers.PhoneNumberUtil;
|
import com.google.i18n.phonenumbers.PhoneNumberUtil;
|
||||||
import com.google.i18n.phonenumbers.Phonenumber;
|
import com.google.i18n.phonenumbers.Phonenumber;
|
||||||
import com.google.protobuf.ByteString;
|
import com.google.protobuf.ByteString;
|
||||||
|
@ -30,6 +33,7 @@ import java.nio.charset.StandardCharsets;
|
||||||
import java.time.Clock;
|
import java.time.Clock;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.time.ZoneId;
|
||||||
import java.time.temporal.ChronoUnit;
|
import java.time.temporal.ChronoUnit;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -93,18 +97,18 @@ import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||||
import org.whispersystems.textsecuregcm.s3.PolicySigner;
|
import org.whispersystems.textsecuregcm.s3.PolicySigner;
|
||||||
import org.whispersystems.textsecuregcm.s3.PostPolicyGenerator;
|
import org.whispersystems.textsecuregcm.s3.PostPolicyGenerator;
|
||||||
import org.whispersystems.textsecuregcm.storage.Account;
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.AccountBadge;
|
||||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.DeviceCapability;
|
import org.whispersystems.textsecuregcm.storage.DeviceCapability;
|
||||||
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.ProfilesManager;
|
import org.whispersystems.textsecuregcm.storage.ProfilesManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.VersionedProfile;
|
import org.whispersystems.textsecuregcm.storage.VersionedProfile;
|
||||||
|
import org.whispersystems.textsecuregcm.tests.util.AccountsHelper;
|
||||||
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
||||||
import org.whispersystems.textsecuregcm.tests.util.ProfileTestHelper;
|
import org.whispersystems.textsecuregcm.tests.util.ProfileTestHelper;
|
||||||
import org.whispersystems.textsecuregcm.util.MockUtils;
|
import org.whispersystems.textsecuregcm.util.MockUtils;
|
||||||
import org.whispersystems.textsecuregcm.util.TestRandomUtil;
|
import org.whispersystems.textsecuregcm.util.TestRandomUtil;
|
||||||
import org.whispersystems.textsecuregcm.util.UUIDUtil;
|
import org.whispersystems.textsecuregcm.util.UUIDUtil;
|
||||||
import org.whispersystems.textsecuregcm.util.ua.UnrecognizedUserAgentException;
|
|
||||||
import org.whispersystems.textsecuregcm.util.ua.UserAgentUtil;
|
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import software.amazon.awssdk.services.s3.S3AsyncClient;
|
import software.amazon.awssdk.services.s3.S3AsyncClient;
|
||||||
import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
|
import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
|
||||||
|
@ -144,6 +148,8 @@ public class ProfileGrpcServiceTest extends SimpleBaseGrpcTest<ProfileGrpcServic
|
||||||
@Mock
|
@Mock
|
||||||
private ServerZkProfileOperations serverZkProfileOperations;
|
private ServerZkProfileOperations serverZkProfileOperations;
|
||||||
|
|
||||||
|
private Clock clock;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected ProfileGrpcService createServiceBeforeEachTest() {
|
protected ProfileGrpcService createServiceBeforeEachTest() {
|
||||||
@SuppressWarnings("unchecked") final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager = mock(DynamicConfigurationManager.class);
|
@SuppressWarnings("unchecked") final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager = mock(DynamicConfigurationManager.class);
|
||||||
|
@ -170,13 +176,9 @@ public class ProfileGrpcServiceTest extends SimpleBaseGrpcTest<ProfileGrpcServic
|
||||||
PhoneNumberUtil.getInstance().getExampleNumber("US"),
|
PhoneNumberUtil.getInstance().getExampleNumber("US"),
|
||||||
PhoneNumberUtil.PhoneNumberFormat.E164);
|
PhoneNumberUtil.PhoneNumberFormat.E164);
|
||||||
|
|
||||||
getMockRequestAttributesInterceptor().setAcceptLanguage(Locale.LanguageRange.parse("en-us"));
|
getMockRequestAttributesInterceptor().setRequestAttributes(new RequestAttributes(InetAddresses.forString("127.0.0.1"),
|
||||||
|
"Signal-Android/1.2.3",
|
||||||
try {
|
Locale.LanguageRange.parse("en-us")));
|
||||||
getMockRequestAttributesInterceptor().setUserAgent(UserAgentUtil.parseUserAgentString("Signal-Android/1.2.3"));
|
|
||||||
} catch (final UnrecognizedUserAgentException e) {
|
|
||||||
throw new IllegalArgumentException(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
when(rateLimiters.getProfileLimiter()).thenReturn(rateLimiter);
|
when(rateLimiters.getProfileLimiter()).thenReturn(rateLimiter);
|
||||||
when(rateLimiter.validateReactive(any(UUID.class))).thenReturn(Mono.empty());
|
when(rateLimiter.validateReactive(any(UUID.class))).thenReturn(Mono.empty());
|
||||||
|
@ -203,8 +205,10 @@ public class ProfileGrpcServiceTest extends SimpleBaseGrpcTest<ProfileGrpcServic
|
||||||
|
|
||||||
when(asyncS3client.deleteObject(any(DeleteObjectRequest.class))).thenReturn(CompletableFuture.completedFuture(null));
|
when(asyncS3client.deleteObject(any(DeleteObjectRequest.class))).thenReturn(CompletableFuture.completedFuture(null));
|
||||||
|
|
||||||
|
clock = Clock.fixed(Instant.ofEpochSecond(42), ZoneId.of("Etc/UTC"));
|
||||||
|
|
||||||
return new ProfileGrpcService(
|
return new ProfileGrpcService(
|
||||||
Clock.systemUTC(),
|
clock,
|
||||||
accountsManager,
|
accountsManager,
|
||||||
profilesManager,
|
profilesManager,
|
||||||
dynamicConfigurationManager,
|
dynamicConfigurationManager,
|
||||||
|
@ -392,6 +396,42 @@ public class ProfileGrpcServiceTest extends SimpleBaseGrpcTest<ProfileGrpcServic
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void setProfileBadges() throws InvalidInputException {
|
||||||
|
|
||||||
|
final byte[] commitment = new ProfileKey(new byte[32]).getCommitment(new ServiceId.Aci(AUTHENTICATED_ACI)).serialize();
|
||||||
|
|
||||||
|
final SetProfileRequest request = SetProfileRequest.newBuilder()
|
||||||
|
.setVersion(VERSION)
|
||||||
|
.setName(ByteString.copyFrom(VALID_NAME))
|
||||||
|
.setAvatarChange(AvatarChange.AVATAR_CHANGE_UNCHANGED)
|
||||||
|
.addAllBadgeIds(List.of("TEST3"))
|
||||||
|
.setCommitment(ByteString.copyFrom(commitment))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
final int accountsManagerUpdateRetryCount = 2;
|
||||||
|
AccountsHelper.setupMockUpdateWithRetries(accountsManager, accountsManagerUpdateRetryCount);
|
||||||
|
// set up two invocations -- one for each AccountsManager#update try
|
||||||
|
when(account.getBadges())
|
||||||
|
.thenReturn(List.of(new AccountBadge("TEST3", Instant.ofEpochSecond(41), false)))
|
||||||
|
.thenReturn(List.of(new AccountBadge("TEST2", Instant.ofEpochSecond(41), true),
|
||||||
|
new AccountBadge("TEST3", Instant.ofEpochSecond(41), false)));
|
||||||
|
|
||||||
|
//noinspection ResultOfMethodCallIgnored
|
||||||
|
authenticatedServiceStub().setProfile(request);
|
||||||
|
|
||||||
|
//noinspection unchecked
|
||||||
|
final ArgumentCaptor<List<AccountBadge>> badgeCaptor = ArgumentCaptor.forClass(List.class);
|
||||||
|
verify(account, times(2)).setBadges(refEq(clock), badgeCaptor.capture());
|
||||||
|
// since the stubbing of getBadges() is brittle, we need to verify the number of invocations, to protect against upstream changes
|
||||||
|
verify(account, times(accountsManagerUpdateRetryCount)).getBadges();
|
||||||
|
|
||||||
|
assertEquals(List.of(
|
||||||
|
new AccountBadge("TEST3", Instant.ofEpochSecond(41), true),
|
||||||
|
new AccountBadge("TEST2", Instant.ofEpochSecond(41), false)),
|
||||||
|
badgeCaptor.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@EnumSource(value = org.signal.chat.common.IdentityType.class, names = {"IDENTITY_TYPE_ACI", "IDENTITY_TYPE_PNI"})
|
@EnumSource(value = org.signal.chat.common.IdentityType.class, names = {"IDENTITY_TYPE_ACI", "IDENTITY_TYPE_PNI"})
|
||||||
void getUnversionedProfile(final IdentityType identityType) {
|
void getUnversionedProfile(final IdentityType identityType) {
|
||||||
|
|
|
@ -6,7 +6,6 @@ import org.signal.chat.rpc.GetAuthenticatedDeviceResponse;
|
||||||
import org.signal.chat.rpc.GetRequestAttributesRequest;
|
import org.signal.chat.rpc.GetRequestAttributesRequest;
|
||||||
import org.signal.chat.rpc.GetRequestAttributesResponse;
|
import org.signal.chat.rpc.GetRequestAttributesResponse;
|
||||||
import org.signal.chat.rpc.RequestAttributesGrpc;
|
import org.signal.chat.rpc.RequestAttributesGrpc;
|
||||||
import org.signal.chat.rpc.UserAgent;
|
|
||||||
import org.whispersystems.textsecuregcm.auth.grpc.AuthenticatedDevice;
|
import org.whispersystems.textsecuregcm.auth.grpc.AuthenticatedDevice;
|
||||||
import org.whispersystems.textsecuregcm.auth.grpc.AuthenticationUtil;
|
import org.whispersystems.textsecuregcm.auth.grpc.AuthenticationUtil;
|
||||||
import org.whispersystems.textsecuregcm.util.UUIDUtil;
|
import org.whispersystems.textsecuregcm.util.UUIDUtil;
|
||||||
|
@ -19,21 +18,15 @@ public class RequestAttributesServiceImpl extends RequestAttributesGrpc.RequestA
|
||||||
|
|
||||||
final GetRequestAttributesResponse.Builder responseBuilder = GetRequestAttributesResponse.newBuilder();
|
final GetRequestAttributesResponse.Builder responseBuilder = GetRequestAttributesResponse.newBuilder();
|
||||||
|
|
||||||
RequestAttributesUtil.getAcceptableLanguages().ifPresent(acceptableLanguages ->
|
RequestAttributesUtil.getAcceptableLanguages()
|
||||||
acceptableLanguages.forEach(languageRange -> responseBuilder.addAcceptableLanguages(languageRange.toString())));
|
.forEach(languageRange -> responseBuilder.addAcceptableLanguages(languageRange.toString()));
|
||||||
|
|
||||||
RequestAttributesUtil.getAvailableAcceptedLocales().forEach(locale ->
|
RequestAttributesUtil.getAvailableAcceptedLocales().forEach(locale ->
|
||||||
responseBuilder.addAvailableAcceptedLocales(locale.toLanguageTag()));
|
responseBuilder.addAvailableAcceptedLocales(locale.toLanguageTag()));
|
||||||
|
|
||||||
responseBuilder.setRemoteAddress(RequestAttributesUtil.getRemoteAddress().getHostAddress());
|
responseBuilder.setRemoteAddress(RequestAttributesUtil.getRemoteAddress().getHostAddress());
|
||||||
|
|
||||||
RequestAttributesUtil.getUserAgent().ifPresent(userAgent -> responseBuilder.setUserAgent(UserAgent.newBuilder()
|
RequestAttributesUtil.getUserAgent().ifPresent(responseBuilder::setUserAgent);
|
||||||
.setPlatform(userAgent.getPlatform().toString())
|
|
||||||
.setVersion(userAgent.getVersion().toString())
|
|
||||||
.setAdditionalSpecifiers(userAgent.getAdditionalSpecifiers().orElse(""))
|
|
||||||
.build()));
|
|
||||||
|
|
||||||
RequestAttributesUtil.getRawUserAgent().ifPresent(responseBuilder::setRawUserAgent);
|
|
||||||
|
|
||||||
responseObserver.onNext(responseBuilder.build());
|
responseObserver.onNext(responseBuilder.build());
|
||||||
responseObserver.onCompleted();
|
responseObserver.onCompleted();
|
||||||
|
|
|
@ -3,172 +3,84 @@ package org.whispersystems.textsecuregcm.grpc;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
|
||||||
import static org.mockito.Mockito.mock;
|
|
||||||
import static org.mockito.Mockito.when;
|
|
||||||
|
|
||||||
import com.google.common.net.InetAddresses;
|
import com.google.common.net.InetAddresses;
|
||||||
import io.grpc.ManagedChannel;
|
import io.grpc.Context;
|
||||||
import io.grpc.Server;
|
import java.net.InetAddress;
|
||||||
import io.grpc.Status;
|
import java.util.Collections;
|
||||||
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 java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import org.junit.jupiter.api.AfterAll;
|
import java.util.concurrent.Callable;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import javax.annotation.Nullable;
|
||||||
import org.junit.jupiter.api.BeforeAll;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.signal.chat.rpc.GetRequestAttributesRequest;
|
|
||||||
import org.signal.chat.rpc.GetRequestAttributesResponse;
|
|
||||||
import org.signal.chat.rpc.RequestAttributesGrpc;
|
|
||||||
import org.whispersystems.textsecuregcm.grpc.net.GrpcClientConnectionManager;
|
|
||||||
import org.whispersystems.textsecuregcm.util.ua.UnrecognizedUserAgentException;
|
|
||||||
import org.whispersystems.textsecuregcm.util.ua.UserAgent;
|
|
||||||
import org.whispersystems.textsecuregcm.util.ua.UserAgentUtil;
|
|
||||||
|
|
||||||
class RequestAttributesUtilTest {
|
class RequestAttributesUtilTest {
|
||||||
|
|
||||||
private static DefaultEventLoopGroup eventLoopGroup;
|
private static final InetAddress REMOTE_ADDRESS = InetAddresses.forString("127.0.0.1");
|
||||||
|
|
||||||
private GrpcClientConnectionManager grpcClientConnectionManager;
|
@Test
|
||||||
|
void getAcceptableLanguages() throws Exception {
|
||||||
|
assertEquals(Collections.emptyList(),
|
||||||
|
callWithRequestAttributes(buildRequestAttributes(Collections.emptyList()),
|
||||||
|
RequestAttributesUtil::getAcceptableLanguages));
|
||||||
|
|
||||||
private Server server;
|
assertEquals(Locale.LanguageRange.parse("en,ja"),
|
||||||
private ManagedChannel managedChannel;
|
callWithRequestAttributes(buildRequestAttributes(Locale.LanguageRange.parse("en,ja")),
|
||||||
|
RequestAttributesUtil::getAcceptableLanguages));
|
||||||
@BeforeAll
|
|
||||||
static void setUpBeforeAll() {
|
|
||||||
eventLoopGroup = new DefaultEventLoopGroup();
|
|
||||||
}
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
void setUp() throws IOException {
|
|
||||||
final LocalAddress serverAddress = new LocalAddress("test-request-metadata-server");
|
|
||||||
|
|
||||||
grpcClientConnectionManager = mock(GrpcClientConnectionManager.class);
|
|
||||||
|
|
||||||
when(grpcClientConnectionManager.getRemoteAddress(any()))
|
|
||||||
.thenReturn(Optional.of(InetAddresses.forString("127.0.0.1")));
|
|
||||||
|
|
||||||
// `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(new RequestAttributesInterceptor(grpcClientConnectionManager))
|
|
||||||
.addService(new RequestAttributesServiceImpl())
|
|
||||||
.build()
|
|
||||||
.start();
|
|
||||||
|
|
||||||
managedChannel = NettyChannelBuilder.forAddress(serverAddress)
|
|
||||||
.channelType(LocalChannel.class)
|
|
||||||
.eventLoopGroup(eventLoopGroup)
|
|
||||||
.usePlaintext()
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@AfterEach
|
|
||||||
void tearDown() {
|
|
||||||
managedChannel.shutdown();
|
|
||||||
server.shutdown();
|
|
||||||
}
|
|
||||||
|
|
||||||
@AfterAll
|
|
||||||
static void tearDownAfterAll() throws InterruptedException {
|
|
||||||
eventLoopGroup.shutdownGracefully().await();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getAcceptableLanguages() {
|
void getAvailableAcceptedLocales() throws Exception {
|
||||||
when(grpcClientConnectionManager.getAcceptableLanguages(any()))
|
assertEquals(Collections.emptyList(),
|
||||||
.thenReturn(Optional.empty());
|
callWithRequestAttributes(buildRequestAttributes(Collections.emptyList()),
|
||||||
|
RequestAttributesUtil::getAvailableAcceptedLocales));
|
||||||
|
|
||||||
assertTrue(getRequestAttributes().getAcceptableLanguagesList().isEmpty());
|
final List<Locale> availableAcceptedLocales =
|
||||||
|
callWithRequestAttributes(buildRequestAttributes(Locale.LanguageRange.parse("en,ja")),
|
||||||
|
RequestAttributesUtil::getAvailableAcceptedLocales);
|
||||||
|
|
||||||
when(grpcClientConnectionManager.getAcceptableLanguages(any()))
|
assertFalse(availableAcceptedLocales.isEmpty());
|
||||||
.thenReturn(Optional.of(Locale.LanguageRange.parse("en,ja")));
|
|
||||||
|
|
||||||
assertEquals(List.of("en", "ja"), getRequestAttributes().getAcceptableLanguagesList());
|
availableAcceptedLocales.forEach(locale ->
|
||||||
|
assertTrue("en".equals(locale.getLanguage()) || "ja".equals(locale.getLanguage())));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getAvailableAcceptedLocales() {
|
void getRemoteAddress() throws Exception {
|
||||||
when(grpcClientConnectionManager.getAcceptableLanguages(any()))
|
assertEquals(REMOTE_ADDRESS,
|
||||||
.thenReturn(Optional.empty());
|
callWithRequestAttributes(new RequestAttributes(REMOTE_ADDRESS, null, null),
|
||||||
|
RequestAttributesUtil::getRemoteAddress));
|
||||||
assertTrue(getRequestAttributes().getAvailableAcceptedLocalesList().isEmpty());
|
|
||||||
|
|
||||||
when(grpcClientConnectionManager.getAcceptableLanguages(any()))
|
|
||||||
.thenReturn(Optional.of(Locale.LanguageRange.parse("en,ja")));
|
|
||||||
|
|
||||||
final GetRequestAttributesResponse response = getRequestAttributes();
|
|
||||||
|
|
||||||
assertFalse(response.getAvailableAcceptedLocalesList().isEmpty());
|
|
||||||
response.getAvailableAcceptedLocalesList().forEach(languageTag -> {
|
|
||||||
final Locale locale = Locale.forLanguageTag(languageTag);
|
|
||||||
assertTrue("en".equals(locale.getLanguage()) || "ja".equals(locale.getLanguage()));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getRemoteAddress() {
|
void getUserAgent() throws Exception {
|
||||||
when(grpcClientConnectionManager.getRemoteAddress(any()))
|
assertEquals(Optional.empty(),
|
||||||
.thenReturn(Optional.empty());
|
callWithRequestAttributes(buildRequestAttributes((String) null),
|
||||||
|
RequestAttributesUtil::getUserAgent));
|
||||||
|
|
||||||
GrpcTestUtils.assertStatusException(Status.INTERNAL, this::getRequestAttributes);
|
assertEquals(Optional.of("Signal-Desktop/1.2.3 Linux"),
|
||||||
|
callWithRequestAttributes(buildRequestAttributes("Signal-Desktop/1.2.3 Linux"),
|
||||||
final String remoteAddressString = "6.7.8.9";
|
RequestAttributesUtil::getUserAgent));
|
||||||
|
|
||||||
when(grpcClientConnectionManager.getRemoteAddress(any()))
|
|
||||||
.thenReturn(Optional.of(InetAddresses.forString(remoteAddressString)));
|
|
||||||
|
|
||||||
assertEquals(remoteAddressString, getRequestAttributes().getRemoteAddress());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
private static <V> V callWithRequestAttributes(final RequestAttributes requestAttributes, final Callable<V> callable) throws Exception {
|
||||||
void getUserAgent() throws UnrecognizedUserAgentException {
|
return Context.current()
|
||||||
when(grpcClientConnectionManager.getUserAgent(any()))
|
.withValue(RequestAttributesUtil.REQUEST_ATTRIBUTES_CONTEXT_KEY, requestAttributes)
|
||||||
.thenReturn(Optional.empty());
|
.call(callable);
|
||||||
|
|
||||||
assertFalse(getRequestAttributes().hasUserAgent());
|
|
||||||
|
|
||||||
final UserAgent userAgent = UserAgentUtil.parseUserAgentString("Signal-Desktop/1.2.3 Linux");
|
|
||||||
|
|
||||||
when(grpcClientConnectionManager.getUserAgent(any()))
|
|
||||||
.thenReturn(Optional.of(userAgent));
|
|
||||||
|
|
||||||
final GetRequestAttributesResponse response = getRequestAttributes();
|
|
||||||
assertTrue(response.hasUserAgent());
|
|
||||||
assertEquals("DESKTOP", response.getUserAgent().getPlatform());
|
|
||||||
assertEquals("1.2.3", response.getUserAgent().getVersion());
|
|
||||||
assertEquals("Linux", response.getUserAgent().getAdditionalSpecifiers());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
private static RequestAttributes buildRequestAttributes(final String userAgent) {
|
||||||
void getRawUserAgent() {
|
return buildRequestAttributes(userAgent, Collections.emptyList());
|
||||||
when(grpcClientConnectionManager.getRawUserAgent(any()))
|
|
||||||
.thenReturn(Optional.empty());
|
|
||||||
|
|
||||||
assertTrue(getRequestAttributes().getRawUserAgent().isBlank());
|
|
||||||
|
|
||||||
final String userAgentString = "Signal-Desktop/1.2.3 Linux";
|
|
||||||
|
|
||||||
when(grpcClientConnectionManager.getRawUserAgent(any()))
|
|
||||||
.thenReturn(Optional.of(userAgentString));
|
|
||||||
|
|
||||||
assertEquals(userAgentString, getRequestAttributes().getRawUserAgent());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private GetRequestAttributesResponse getRequestAttributes() {
|
private static RequestAttributes buildRequestAttributes(final List<Locale.LanguageRange> acceptLanguage) {
|
||||||
return RequestAttributesGrpc.newBlockingStub(managedChannel)
|
return buildRequestAttributes(null, acceptLanguage);
|
||||||
.getRequestAttributes(GetRequestAttributesRequest.newBuilder().build());
|
}
|
||||||
|
|
||||||
|
private static RequestAttributes buildRequestAttributes(@Nullable final String userAgent,
|
||||||
|
final List<Locale.LanguageRange> acceptLanguage) {
|
||||||
|
|
||||||
|
return new RequestAttributes(REMOTE_ADDRESS, userAgent, acceptLanguage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
package org.whispersystems.textsecuregcm.grpc.net;
|
package org.whispersystems.textsecuregcm.grpc.net;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
import com.google.common.net.InetAddresses;
|
import com.google.common.net.InetAddresses;
|
||||||
import com.vdurmont.semver4j.Semver;
|
|
||||||
import io.netty.bootstrap.Bootstrap;
|
import io.netty.bootstrap.Bootstrap;
|
||||||
import io.netty.bootstrap.ServerBootstrap;
|
import io.netty.bootstrap.ServerBootstrap;
|
||||||
import io.netty.channel.Channel;
|
import io.netty.channel.Channel;
|
||||||
|
@ -12,6 +16,12 @@ import io.netty.channel.embedded.EmbeddedChannel;
|
||||||
import io.netty.channel.local.LocalAddress;
|
import io.netty.channel.local.LocalAddress;
|
||||||
import io.netty.channel.local.LocalChannel;
|
import io.netty.channel.local.LocalChannel;
|
||||||
import io.netty.channel.local.LocalServerChannel;
|
import io.netty.channel.local.LocalServerChannel;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
import org.junit.jupiter.api.AfterAll;
|
import org.junit.jupiter.api.AfterAll;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeAll;
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
@ -21,20 +31,9 @@ import org.junit.jupiter.params.ParameterizedTest;
|
||||||
import org.junit.jupiter.params.provider.Arguments;
|
import org.junit.jupiter.params.provider.Arguments;
|
||||||
import org.junit.jupiter.params.provider.MethodSource;
|
import org.junit.jupiter.params.provider.MethodSource;
|
||||||
import org.whispersystems.textsecuregcm.auth.grpc.AuthenticatedDevice;
|
import org.whispersystems.textsecuregcm.auth.grpc.AuthenticatedDevice;
|
||||||
|
import org.whispersystems.textsecuregcm.grpc.ChannelNotFoundException;
|
||||||
|
import org.whispersystems.textsecuregcm.grpc.RequestAttributes;
|
||||||
import org.whispersystems.textsecuregcm.storage.Device;
|
import org.whispersystems.textsecuregcm.storage.Device;
|
||||||
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;
|
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
|
||||||
|
|
||||||
class GrpcClientConnectionManagerTest {
|
class GrpcClientConnectionManagerTest {
|
||||||
|
|
||||||
|
@ -103,7 +102,7 @@ class GrpcClientConnectionManagerTest {
|
||||||
grpcClientConnectionManager.handleConnectionEstablished(localChannel, remoteChannel, maybeAuthenticatedDevice);
|
grpcClientConnectionManager.handleConnectionEstablished(localChannel, remoteChannel, maybeAuthenticatedDevice);
|
||||||
|
|
||||||
assertEquals(maybeAuthenticatedDevice,
|
assertEquals(maybeAuthenticatedDevice,
|
||||||
grpcClientConnectionManager.getAuthenticatedDevice(localChannel.localAddress()));
|
grpcClientConnectionManager.getAuthenticatedDevice(remoteChannel));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<Optional<AuthenticatedDevice>> getAuthenticatedDevice() {
|
private static List<Optional<AuthenticatedDevice>> getAuthenticatedDevice() {
|
||||||
|
@ -114,170 +113,115 @@ class GrpcClientConnectionManagerTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getAcceptableLanguages() {
|
void getRequestAttributes() {
|
||||||
grpcClientConnectionManager.handleConnectionEstablished(localChannel, remoteChannel, Optional.empty());
|
grpcClientConnectionManager.handleConnectionEstablished(localChannel, remoteChannel, Optional.empty());
|
||||||
|
|
||||||
assertEquals(Optional.empty(),
|
assertThrows(IllegalStateException.class, () -> grpcClientConnectionManager.getRequestAttributes(remoteChannel));
|
||||||
grpcClientConnectionManager.getAcceptableLanguages(localChannel.localAddress()));
|
|
||||||
|
|
||||||
final List<Locale.LanguageRange> acceptLanguageRanges = Locale.LanguageRange.parse("en,ja");
|
final RequestAttributes requestAttributes = new RequestAttributes(InetAddresses.forString("6.7.8.9"), null, null);
|
||||||
remoteChannel.attr(GrpcClientConnectionManager.ACCEPT_LANGUAGE_ATTRIBUTE_KEY).set(acceptLanguageRanges);
|
remoteChannel.attr(GrpcClientConnectionManager.REQUEST_ATTRIBUTES_KEY).set(requestAttributes);
|
||||||
|
|
||||||
assertEquals(Optional.of(acceptLanguageRanges),
|
assertEquals(requestAttributes, grpcClientConnectionManager.getRequestAttributes(remoteChannel));
|
||||||
grpcClientConnectionManager.getAcceptableLanguages(localChannel.localAddress()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getRemoteAddress() {
|
void closeConnection() throws InterruptedException, ChannelNotFoundException {
|
||||||
grpcClientConnectionManager.handleConnectionEstablished(localChannel, remoteChannel, Optional.empty());
|
|
||||||
|
|
||||||
assertEquals(Optional.empty(),
|
|
||||||
grpcClientConnectionManager.getRemoteAddress(localChannel.localAddress()));
|
|
||||||
|
|
||||||
final InetAddress remoteAddress = InetAddresses.forString("6.7.8.9");
|
|
||||||
remoteChannel.attr(GrpcClientConnectionManager.REMOTE_ADDRESS_ATTRIBUTE_KEY).set(remoteAddress);
|
|
||||||
|
|
||||||
assertEquals(Optional.of(remoteAddress),
|
|
||||||
grpcClientConnectionManager.getRemoteAddress(localChannel.localAddress()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void getUserAgent() throws UnrecognizedUserAgentException {
|
|
||||||
grpcClientConnectionManager.handleConnectionEstablished(localChannel, remoteChannel, Optional.empty());
|
|
||||||
|
|
||||||
assertEquals(Optional.empty(),
|
|
||||||
grpcClientConnectionManager.getUserAgent(localChannel.localAddress()));
|
|
||||||
|
|
||||||
final UserAgent userAgent = UserAgentUtil.parseUserAgentString("Signal-Desktop/1.2.3 Linux");
|
|
||||||
remoteChannel.attr(GrpcClientConnectionManager.PARSED_USER_AGENT_ATTRIBUTE_KEY).set(userAgent);
|
|
||||||
|
|
||||||
assertEquals(Optional.of(userAgent),
|
|
||||||
grpcClientConnectionManager.getUserAgent(localChannel.localAddress()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void closeConnection() throws InterruptedException {
|
|
||||||
final AuthenticatedDevice authenticatedDevice = new AuthenticatedDevice(UUID.randomUUID(), Device.PRIMARY_ID);
|
final AuthenticatedDevice authenticatedDevice = new AuthenticatedDevice(UUID.randomUUID(), Device.PRIMARY_ID);
|
||||||
|
|
||||||
grpcClientConnectionManager.handleConnectionEstablished(localChannel, remoteChannel, Optional.of(authenticatedDevice));
|
grpcClientConnectionManager.handleConnectionEstablished(localChannel, remoteChannel, Optional.of(authenticatedDevice));
|
||||||
|
|
||||||
assertTrue(remoteChannel.isOpen());
|
assertTrue(remoteChannel.isOpen());
|
||||||
|
|
||||||
assertEquals(remoteChannel, grpcClientConnectionManager.getRemoteChannelByLocalAddress(localChannel.localAddress()));
|
assertEquals(remoteChannel, grpcClientConnectionManager.getRemoteChannel(localChannel.localAddress()));
|
||||||
assertEquals(List.of(remoteChannel),
|
assertEquals(List.of(remoteChannel),
|
||||||
grpcClientConnectionManager.getRemoteChannelsByAuthenticatedDevice(authenticatedDevice));
|
grpcClientConnectionManager.getRemoteChannelsByAuthenticatedDevice(authenticatedDevice));
|
||||||
|
|
||||||
remoteChannel.close().await();
|
remoteChannel.close().await();
|
||||||
|
|
||||||
assertNull(grpcClientConnectionManager.getRemoteChannelByLocalAddress(localChannel.localAddress()));
|
assertThrows(ChannelNotFoundException.class,
|
||||||
|
() -> grpcClientConnectionManager.getRemoteChannel(localChannel.localAddress()));
|
||||||
|
|
||||||
assertNull(grpcClientConnectionManager.getRemoteChannelsByAuthenticatedDevice(authenticatedDevice));
|
assertNull(grpcClientConnectionManager.getRemoteChannelsByAuthenticatedDevice(authenticatedDevice));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@ParameterizedTest
|
||||||
void handleWebSocketHandshakeCompleteRemoteAddress() {
|
@MethodSource
|
||||||
|
void handleHandshakeCompleteRequestAttributes(final InetAddress preferredRemoteAddress,
|
||||||
|
final String userAgentHeader,
|
||||||
|
final String acceptLanguageHeader,
|
||||||
|
final RequestAttributes expectedRequestAttributes) {
|
||||||
|
|
||||||
final EmbeddedChannel embeddedChannel = new EmbeddedChannel();
|
final EmbeddedChannel embeddedChannel = new EmbeddedChannel();
|
||||||
|
|
||||||
final InetAddress preferredRemoteAddress = InetAddresses.forString("192.168.1.1");
|
GrpcClientConnectionManager.handleHandshakeComplete(embeddedChannel,
|
||||||
|
|
||||||
GrpcClientConnectionManager.handleWebSocketHandshakeComplete(embeddedChannel,
|
|
||||||
preferredRemoteAddress,
|
preferredRemoteAddress,
|
||||||
null,
|
|
||||||
null);
|
|
||||||
|
|
||||||
assertEquals(preferredRemoteAddress,
|
|
||||||
embeddedChannel.attr(GrpcClientConnectionManager.REMOTE_ADDRESS_ATTRIBUTE_KEY).get());
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@MethodSource
|
|
||||||
void handleWebSocketHandshakeCompleteUserAgent(@Nullable final String userAgentHeader,
|
|
||||||
@Nullable final UserAgent expectedParsedUserAgent) {
|
|
||||||
|
|
||||||
final EmbeddedChannel embeddedChannel = new EmbeddedChannel();
|
|
||||||
|
|
||||||
GrpcClientConnectionManager.handleWebSocketHandshakeComplete(embeddedChannel,
|
|
||||||
InetAddresses.forString("127.0.0.1"),
|
|
||||||
userAgentHeader,
|
userAgentHeader,
|
||||||
null);
|
|
||||||
|
|
||||||
assertEquals(userAgentHeader,
|
|
||||||
embeddedChannel.attr(GrpcClientConnectionManager.RAW_USER_AGENT_ATTRIBUTE_KEY).get());
|
|
||||||
|
|
||||||
assertEquals(expectedParsedUserAgent,
|
|
||||||
embeddedChannel.attr(GrpcClientConnectionManager.PARSED_USER_AGENT_ATTRIBUTE_KEY).get());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<Arguments> handleWebSocketHandshakeCompleteUserAgent() {
|
|
||||||
return List.of(
|
|
||||||
// Recognized user-agent
|
|
||||||
Arguments.of("Signal-Desktop/1.2.3 Linux", new UserAgent(ClientPlatform.DESKTOP, new Semver("1.2.3"), "Linux")),
|
|
||||||
|
|
||||||
// Unrecognized user-agent
|
|
||||||
Arguments.of("Not a valid user-agent string", null),
|
|
||||||
|
|
||||||
// Missing user-agent
|
|
||||||
Arguments.of(null, null)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@MethodSource
|
|
||||||
void handleWebSocketHandshakeCompleteAcceptLanguage(@Nullable final String acceptLanguageHeader,
|
|
||||||
@Nullable final List<Locale.LanguageRange> expectedLanguageRanges) {
|
|
||||||
|
|
||||||
final EmbeddedChannel embeddedChannel = new EmbeddedChannel();
|
|
||||||
|
|
||||||
GrpcClientConnectionManager.handleWebSocketHandshakeComplete(embeddedChannel,
|
|
||||||
InetAddresses.forString("127.0.0.1"),
|
|
||||||
null,
|
|
||||||
acceptLanguageHeader);
|
acceptLanguageHeader);
|
||||||
|
|
||||||
assertEquals(expectedLanguageRanges,
|
assertEquals(expectedRequestAttributes,
|
||||||
embeddedChannel.attr(GrpcClientConnectionManager.ACCEPT_LANGUAGE_ATTRIBUTE_KEY).get());
|
embeddedChannel.attr(GrpcClientConnectionManager.REQUEST_ATTRIBUTES_KEY).get());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<Arguments> handleWebSocketHandshakeCompleteAcceptLanguage() {
|
private static List<Arguments> handleHandshakeCompleteRequestAttributes() {
|
||||||
|
final InetAddress preferredRemoteAddress = InetAddresses.forString("192.168.1.1");
|
||||||
|
|
||||||
return List.of(
|
return List.of(
|
||||||
// Parseable list
|
Arguments.argumentSet("Null User-Agent and Accept-Language headers",
|
||||||
Arguments.of("ja,en;q=0.4", Locale.LanguageRange.parse("ja,en;q=0.4")),
|
preferredRemoteAddress, null, null,
|
||||||
|
new RequestAttributes(preferredRemoteAddress, null, Collections.emptyList())),
|
||||||
|
|
||||||
// Unparsable list
|
Arguments.argumentSet("Recognized User-Agent and null Accept-Language header",
|
||||||
Arguments.of("This is not a valid language preference list", null),
|
preferredRemoteAddress, "Signal-Desktop/1.2.3 Linux", null,
|
||||||
|
new RequestAttributes(preferredRemoteAddress, "Signal-Desktop/1.2.3 Linux", Collections.emptyList())),
|
||||||
|
|
||||||
// Missing list
|
Arguments.argumentSet("Unparsable User-Agent and null Accept-Language header",
|
||||||
Arguments.of(null, null)
|
preferredRemoteAddress, "Not a valid user-agent string", null,
|
||||||
|
new RequestAttributes(preferredRemoteAddress, "Not a valid user-agent string", Collections.emptyList())),
|
||||||
|
|
||||||
|
Arguments.argumentSet("Null User-Agent and parsable Accept-Language header",
|
||||||
|
preferredRemoteAddress, null, "ja,en;q=0.4",
|
||||||
|
new RequestAttributes(preferredRemoteAddress, null, Locale.LanguageRange.parse("ja,en;q=0.4"))),
|
||||||
|
|
||||||
|
Arguments.argumentSet("Null User-Agent and unparsable Accept-Language header",
|
||||||
|
preferredRemoteAddress, null, "This is not a valid language preference list",
|
||||||
|
new RequestAttributes(preferredRemoteAddress, null, Collections.emptyList()))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void handleConnectionEstablishedAuthenticated() throws InterruptedException {
|
void handleConnectionEstablishedAuthenticated() throws InterruptedException, ChannelNotFoundException {
|
||||||
final AuthenticatedDevice authenticatedDevice = new AuthenticatedDevice(UUID.randomUUID(), Device.PRIMARY_ID);
|
final AuthenticatedDevice authenticatedDevice = new AuthenticatedDevice(UUID.randomUUID(), Device.PRIMARY_ID);
|
||||||
|
|
||||||
assertNull(grpcClientConnectionManager.getRemoteChannelByLocalAddress(localChannel.localAddress()));
|
assertThrows(ChannelNotFoundException.class,
|
||||||
|
() -> grpcClientConnectionManager.getRemoteChannel(localChannel.localAddress()));
|
||||||
|
|
||||||
assertNull(grpcClientConnectionManager.getRemoteChannelsByAuthenticatedDevice(authenticatedDevice));
|
assertNull(grpcClientConnectionManager.getRemoteChannelsByAuthenticatedDevice(authenticatedDevice));
|
||||||
|
|
||||||
grpcClientConnectionManager.handleConnectionEstablished(localChannel, remoteChannel, Optional.of(authenticatedDevice));
|
grpcClientConnectionManager.handleConnectionEstablished(localChannel, remoteChannel, Optional.of(authenticatedDevice));
|
||||||
|
|
||||||
assertEquals(remoteChannel, grpcClientConnectionManager.getRemoteChannelByLocalAddress(localChannel.localAddress()));
|
assertEquals(remoteChannel, grpcClientConnectionManager.getRemoteChannel(localChannel.localAddress()));
|
||||||
assertEquals(List.of(remoteChannel), grpcClientConnectionManager.getRemoteChannelsByAuthenticatedDevice(authenticatedDevice));
|
assertEquals(List.of(remoteChannel), grpcClientConnectionManager.getRemoteChannelsByAuthenticatedDevice(authenticatedDevice));
|
||||||
|
|
||||||
remoteChannel.close().await();
|
remoteChannel.close().await();
|
||||||
|
|
||||||
assertNull(grpcClientConnectionManager.getRemoteChannelByLocalAddress(localChannel.localAddress()));
|
assertThrows(ChannelNotFoundException.class,
|
||||||
|
() -> grpcClientConnectionManager.getRemoteChannel(localChannel.localAddress()));
|
||||||
|
|
||||||
assertNull(grpcClientConnectionManager.getRemoteChannelsByAuthenticatedDevice(authenticatedDevice));
|
assertNull(grpcClientConnectionManager.getRemoteChannelsByAuthenticatedDevice(authenticatedDevice));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void handleConnectionEstablishedAnonymous() throws InterruptedException {
|
void handleConnectionEstablishedAnonymous() throws InterruptedException, ChannelNotFoundException {
|
||||||
assertNull(grpcClientConnectionManager.getRemoteChannelByLocalAddress(localChannel.localAddress()));
|
assertThrows(ChannelNotFoundException.class,
|
||||||
|
() -> grpcClientConnectionManager.getRemoteChannel(localChannel.localAddress()));
|
||||||
|
|
||||||
grpcClientConnectionManager.handleConnectionEstablished(localChannel, remoteChannel, Optional.empty());
|
grpcClientConnectionManager.handleConnectionEstablished(localChannel, remoteChannel, Optional.empty());
|
||||||
|
|
||||||
assertEquals(remoteChannel, grpcClientConnectionManager.getRemoteChannelByLocalAddress(localChannel.localAddress()));
|
assertEquals(remoteChannel, grpcClientConnectionManager.getRemoteChannel(localChannel.localAddress()));
|
||||||
|
|
||||||
remoteChannel.close().await();
|
remoteChannel.close().await();
|
||||||
|
|
||||||
assertNull(grpcClientConnectionManager.getRemoteChannelByLocalAddress(localChannel.localAddress()));
|
assertThrows(ChannelNotFoundException.class,
|
||||||
|
() -> grpcClientConnectionManager.getRemoteChannel(localChannel.localAddress()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -523,10 +523,7 @@ class NoiseWebSocketTunnelServerIntegrationTest extends AbstractLeakDetectionTes
|
||||||
|
|
||||||
assertEquals(remoteAddress, response.getRemoteAddress());
|
assertEquals(remoteAddress, response.getRemoteAddress());
|
||||||
assertEquals(List.of(acceptLanguage), response.getAcceptableLanguagesList());
|
assertEquals(List.of(acceptLanguage), response.getAcceptableLanguagesList());
|
||||||
|
assertEquals(userAgent, response.getUserAgent());
|
||||||
assertEquals("DESKTOP", response.getUserAgent().getPlatform());
|
|
||||||
assertEquals("1.2.3", response.getUserAgent().getVersion());
|
|
||||||
assertEquals("Linux", response.getUserAgent().getAdditionalSpecifiers());
|
|
||||||
} finally {
|
} finally {
|
||||||
channel.shutdown();
|
channel.shutdown();
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.params.provider.Arguments.argumentSet;
|
||||||
import static org.junit.jupiter.params.provider.Arguments.arguments;
|
import static org.junit.jupiter.params.provider.Arguments.arguments;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
|
@ -16,6 +17,7 @@ import io.netty.channel.local.LocalAddress;
|
||||||
import io.netty.handler.codec.http.DefaultHttpHeaders;
|
import io.netty.handler.codec.http.DefaultHttpHeaders;
|
||||||
import io.netty.handler.codec.http.HttpHeaders;
|
import io.netty.handler.codec.http.HttpHeaders;
|
||||||
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
|
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
|
||||||
|
import io.netty.util.Attribute;
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.net.SocketAddress;
|
import java.net.SocketAddress;
|
||||||
|
@ -31,6 +33,7 @@ import org.junit.jupiter.params.ParameterizedTest;
|
||||||
import org.junit.jupiter.params.provider.Arguments;
|
import org.junit.jupiter.params.provider.Arguments;
|
||||||
import org.junit.jupiter.params.provider.MethodSource;
|
import org.junit.jupiter.params.provider.MethodSource;
|
||||||
import org.signal.libsignal.protocol.ecc.Curve;
|
import org.signal.libsignal.protocol.ecc.Curve;
|
||||||
|
import org.whispersystems.textsecuregcm.grpc.RequestAttributes;
|
||||||
import org.whispersystems.textsecuregcm.storage.ClientPublicKeysManager;
|
import org.whispersystems.textsecuregcm.storage.ClientPublicKeysManager;
|
||||||
|
|
||||||
class WebsocketHandshakeCompleteHandlerTest extends AbstractLeakDetectionTest {
|
class WebsocketHandshakeCompleteHandlerTest extends AbstractLeakDetectionTest {
|
||||||
|
@ -134,8 +137,13 @@ class WebsocketHandshakeCompleteHandlerTest extends AbstractLeakDetectionTest {
|
||||||
embeddedChannel.setRemoteAddress(remoteAddress);
|
embeddedChannel.setRemoteAddress(remoteAddress);
|
||||||
embeddedChannel.pipeline().fireUserEventTriggered(handshakeCompleteEvent);
|
embeddedChannel.pipeline().fireUserEventTriggered(handshakeCompleteEvent);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
assertEquals(expectedRemoteAddress,
|
assertEquals(expectedRemoteAddress,
|
||||||
embeddedChannel.attr(GrpcClientConnectionManager.REMOTE_ADDRESS_ATTRIBUTE_KEY).get());
|
Optional.ofNullable(embeddedChannel.attr(GrpcClientConnectionManager.REQUEST_ATTRIBUTES_KEY))
|
||||||
|
.map(Attribute::get)
|
||||||
|
.map(RequestAttributes::remoteAddress)
|
||||||
|
.orElse(null));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<Arguments> getRemoteAddress() {
|
private static List<Arguments> getRemoteAddress() {
|
||||||
|
@ -144,53 +152,53 @@ class WebsocketHandshakeCompleteHandlerTest extends AbstractLeakDetectionTest {
|
||||||
final InetAddress proxyAddress = InetAddresses.forString("4.3.2.1");
|
final InetAddress proxyAddress = InetAddresses.forString("4.3.2.1");
|
||||||
|
|
||||||
return List.of(
|
return List.of(
|
||||||
// Recognized proxy, single forwarded-for address
|
argumentSet("Recognized proxy, single forwarded-for address",
|
||||||
Arguments.of(new DefaultHttpHeaders()
|
new DefaultHttpHeaders()
|
||||||
.add(WebsocketHandshakeCompleteHandler.RECOGNIZED_PROXY_SECRET_HEADER, RECOGNIZED_PROXY_SECRET)
|
.add(WebsocketHandshakeCompleteHandler.RECOGNIZED_PROXY_SECRET_HEADER, RECOGNIZED_PROXY_SECRET)
|
||||||
.add(WebsocketHandshakeCompleteHandler.FORWARDED_FOR_HEADER, clientAddress.getHostAddress()),
|
.add(WebsocketHandshakeCompleteHandler.FORWARDED_FOR_HEADER, clientAddress.getHostAddress()),
|
||||||
remoteAddress,
|
remoteAddress,
|
||||||
clientAddress),
|
clientAddress),
|
||||||
|
|
||||||
// Recognized proxy, multiple forwarded-for addresses
|
argumentSet("Recognized proxy, multiple forwarded-for addresses",
|
||||||
Arguments.of(new DefaultHttpHeaders()
|
new DefaultHttpHeaders()
|
||||||
.add(WebsocketHandshakeCompleteHandler.RECOGNIZED_PROXY_SECRET_HEADER, RECOGNIZED_PROXY_SECRET)
|
.add(WebsocketHandshakeCompleteHandler.RECOGNIZED_PROXY_SECRET_HEADER, RECOGNIZED_PROXY_SECRET)
|
||||||
.add(WebsocketHandshakeCompleteHandler.FORWARDED_FOR_HEADER, clientAddress.getHostAddress() + "," + proxyAddress.getHostAddress()),
|
.add(WebsocketHandshakeCompleteHandler.FORWARDED_FOR_HEADER, clientAddress.getHostAddress() + "," + proxyAddress.getHostAddress()),
|
||||||
remoteAddress,
|
remoteAddress,
|
||||||
proxyAddress),
|
proxyAddress),
|
||||||
|
|
||||||
// No recognized proxy header, single forwarded-for address
|
argumentSet("No recognized proxy header, single forwarded-for address",
|
||||||
Arguments.of(new DefaultHttpHeaders()
|
new DefaultHttpHeaders()
|
||||||
.add(WebsocketHandshakeCompleteHandler.FORWARDED_FOR_HEADER, clientAddress.getHostAddress()),
|
.add(WebsocketHandshakeCompleteHandler.FORWARDED_FOR_HEADER, clientAddress.getHostAddress()),
|
||||||
remoteAddress,
|
remoteAddress,
|
||||||
remoteAddress.getAddress()),
|
remoteAddress.getAddress()),
|
||||||
|
|
||||||
// No recognized proxy header, no forwarded-for address
|
argumentSet("No recognized proxy header, no forwarded-for address",
|
||||||
Arguments.of(new DefaultHttpHeaders(),
|
new DefaultHttpHeaders(),
|
||||||
remoteAddress,
|
remoteAddress,
|
||||||
remoteAddress.getAddress()),
|
remoteAddress.getAddress()),
|
||||||
|
|
||||||
// Incorrect proxy header, single forwarded-for address
|
argumentSet("Incorrect proxy header, single forwarded-for address",
|
||||||
Arguments.of(new DefaultHttpHeaders()
|
new DefaultHttpHeaders()
|
||||||
.add(WebsocketHandshakeCompleteHandler.RECOGNIZED_PROXY_SECRET_HEADER, RECOGNIZED_PROXY_SECRET + "-incorrect")
|
.add(WebsocketHandshakeCompleteHandler.RECOGNIZED_PROXY_SECRET_HEADER, RECOGNIZED_PROXY_SECRET + "-incorrect")
|
||||||
.add(WebsocketHandshakeCompleteHandler.FORWARDED_FOR_HEADER, clientAddress.getHostAddress()),
|
.add(WebsocketHandshakeCompleteHandler.FORWARDED_FOR_HEADER, clientAddress.getHostAddress()),
|
||||||
remoteAddress,
|
remoteAddress,
|
||||||
remoteAddress.getAddress()),
|
remoteAddress.getAddress()),
|
||||||
|
|
||||||
// Recognized proxy, no forwarded-for address
|
argumentSet("Recognized proxy, no forwarded-for address",
|
||||||
Arguments.of(new DefaultHttpHeaders()
|
new DefaultHttpHeaders()
|
||||||
.add(WebsocketHandshakeCompleteHandler.RECOGNIZED_PROXY_SECRET_HEADER, RECOGNIZED_PROXY_SECRET),
|
.add(WebsocketHandshakeCompleteHandler.RECOGNIZED_PROXY_SECRET_HEADER, RECOGNIZED_PROXY_SECRET),
|
||||||
remoteAddress,
|
remoteAddress,
|
||||||
remoteAddress.getAddress()),
|
remoteAddress.getAddress()),
|
||||||
|
|
||||||
// Recognized proxy, bogus forwarded-for address
|
argumentSet("Recognized proxy, bogus forwarded-for address",
|
||||||
Arguments.of(new DefaultHttpHeaders()
|
new DefaultHttpHeaders()
|
||||||
.add(WebsocketHandshakeCompleteHandler.RECOGNIZED_PROXY_SECRET_HEADER, RECOGNIZED_PROXY_SECRET)
|
.add(WebsocketHandshakeCompleteHandler.RECOGNIZED_PROXY_SECRET_HEADER, RECOGNIZED_PROXY_SECRET)
|
||||||
.add(WebsocketHandshakeCompleteHandler.FORWARDED_FOR_HEADER, "not a valid address"),
|
.add(WebsocketHandshakeCompleteHandler.FORWARDED_FOR_HEADER, "not a valid address"),
|
||||||
remoteAddress,
|
remoteAddress,
|
||||||
null),
|
null),
|
||||||
|
|
||||||
// No forwarded-for address, non-InetSocketAddress remote address
|
argumentSet("No forwarded-for address, non-InetSocketAddress remote address",
|
||||||
Arguments.of(new DefaultHttpHeaders()
|
new DefaultHttpHeaders()
|
||||||
.add(WebsocketHandshakeCompleteHandler.RECOGNIZED_PROXY_SECRET_HEADER, RECOGNIZED_PROXY_SECRET),
|
.add(WebsocketHandshakeCompleteHandler.RECOGNIZED_PROXY_SECRET_HEADER, RECOGNIZED_PROXY_SECRET),
|
||||||
new LocalAddress("local-address"),
|
new LocalAddress("local-address"),
|
||||||
null)
|
null)
|
||||||
|
|
|
@ -104,6 +104,7 @@ class MessageSenderTest {
|
||||||
serviceIdentifier,
|
serviceIdentifier,
|
||||||
Map.of(device.getId(), message),
|
Map.of(device.getId(), message),
|
||||||
Map.of(device.getId(), registrationId),
|
Map.of(device.getId(), registrationId),
|
||||||
|
Optional.empty(),
|
||||||
null));
|
null));
|
||||||
|
|
||||||
final MessageProtos.Envelope expectedMessage = ephemeral
|
final MessageProtos.Envelope expectedMessage = ephemeral
|
||||||
|
@ -144,6 +145,7 @@ class MessageSenderTest {
|
||||||
serviceIdentifier,
|
serviceIdentifier,
|
||||||
Map.of(device.getId(), message),
|
Map.of(device.getId(), message),
|
||||||
Map.of(device.getId(), registrationId + 1),
|
Map.of(device.getId(), registrationId + 1),
|
||||||
|
Optional.empty(),
|
||||||
null));
|
null));
|
||||||
|
|
||||||
assertEquals(new MismatchedDevices(Collections.emptySet(), Collections.emptySet(), Set.of(deviceId)),
|
assertEquals(new MismatchedDevices(Collections.emptySet(), Collections.emptySet(), Set.of(deviceId)),
|
||||||
|
@ -344,4 +346,64 @@ class MessageSenderTest {
|
||||||
Optional.of(new MismatchedDevices(Set.of(primaryDeviceId), Set.of(extraDeviceId), Set.of(linkedDeviceId))))
|
Optional.of(new MismatchedDevices(Set.of(primaryDeviceId), Set.of(extraDeviceId), Set.of(linkedDeviceId))))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sendMessageEmptyMessageList() {
|
||||||
|
final ServiceIdentifier serviceIdentifier = new AciServiceIdentifier(UUID.randomUUID());
|
||||||
|
|
||||||
|
final Device device = mock(Device.class);
|
||||||
|
when(device.getId()).thenReturn(Device.PRIMARY_ID);
|
||||||
|
|
||||||
|
final Account account = mock(Account.class);
|
||||||
|
when(account.getDevices()).thenReturn(List.of(device));
|
||||||
|
when(account.isIdentifiedBy(serviceIdentifier)).thenReturn(true);
|
||||||
|
|
||||||
|
assertThrows(MismatchedDevicesException.class, () -> messageSender.sendMessages(account,
|
||||||
|
serviceIdentifier,
|
||||||
|
Collections.emptyMap(),
|
||||||
|
Collections.emptyMap(),
|
||||||
|
Optional.empty(),
|
||||||
|
null));
|
||||||
|
|
||||||
|
assertDoesNotThrow(() -> messageSender.sendMessages(account,
|
||||||
|
serviceIdentifier,
|
||||||
|
Collections.emptyMap(),
|
||||||
|
Collections.emptyMap(),
|
||||||
|
Optional.of(Device.PRIMARY_ID),
|
||||||
|
null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sendSyncMessageMismatchedAddressing() {
|
||||||
|
final UUID accountIdentifier = UUID.randomUUID();
|
||||||
|
final ServiceIdentifier serviceIdentifier = new AciServiceIdentifier(accountIdentifier);
|
||||||
|
final byte deviceId = Device.PRIMARY_ID;
|
||||||
|
|
||||||
|
final Account account = mock(Account.class);
|
||||||
|
when(account.getUuid()).thenReturn(accountIdentifier);
|
||||||
|
when(account.getIdentifier(IdentityType.ACI)).thenReturn(accountIdentifier);
|
||||||
|
when(account.isIdentifiedBy(serviceIdentifier)).thenReturn(true);
|
||||||
|
|
||||||
|
final Account nonSyncDestination = mock(Account.class);
|
||||||
|
when(nonSyncDestination.isIdentifiedBy(any())).thenReturn(true);
|
||||||
|
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> messageSender.sendMessages(nonSyncDestination,
|
||||||
|
new AciServiceIdentifier(UUID.randomUUID()),
|
||||||
|
Map.of(deviceId, MessageProtos.Envelope.newBuilder().build()),
|
||||||
|
Map.of(deviceId, 17),
|
||||||
|
Optional.of(deviceId),
|
||||||
|
null),
|
||||||
|
"Should throw an IllegalArgumentException for inter-account messages with a sync message device ID");
|
||||||
|
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> messageSender.sendMessages(account,
|
||||||
|
serviceIdentifier,
|
||||||
|
Map.of(deviceId, MessageProtos.Envelope.newBuilder()
|
||||||
|
.setSourceServiceId(serviceIdentifier.toServiceIdentifierString())
|
||||||
|
.setSourceDevice(deviceId)
|
||||||
|
.build()),
|
||||||
|
Map.of(deviceId, 17),
|
||||||
|
Optional.empty(),
|
||||||
|
null),
|
||||||
|
"Should throw an IllegalArgumentException for self-addressed messages without a sync message device ID");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,6 @@ import static org.whispersystems.textsecuregcm.tests.util.DevicesHelper.createDe
|
||||||
import com.fasterxml.jackson.annotation.JsonFilter;
|
import com.fasterxml.jackson.annotation.JsonFilter;
|
||||||
import java.lang.annotation.Annotation;
|
import java.lang.annotation.Annotation;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.time.Clock;
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
@ -187,7 +186,7 @@ class AccountTest {
|
||||||
@Test
|
@Test
|
||||||
void addAndRemoveBadges() {
|
void addAndRemoveBadges() {
|
||||||
final Account account = AccountsHelper.generateTestAccount("+14151234567", UUID.randomUUID(), UUID.randomUUID(), List.of(createDevice(Device.PRIMARY_ID)), new byte[0]);
|
final Account account = AccountsHelper.generateTestAccount("+14151234567", UUID.randomUUID(), UUID.randomUUID(), List.of(createDevice(Device.PRIMARY_ID)), new byte[0]);
|
||||||
final Clock clock = TestClock.pinned(Instant.ofEpochSecond(40));
|
final TestClock clock = TestClock.pinned(Instant.ofEpochSecond(40));
|
||||||
|
|
||||||
account.addBadge(clock, new AccountBadge("foo", Instant.ofEpochSecond(42), false));
|
account.addBadge(clock, new AccountBadge("foo", Instant.ofEpochSecond(42), false));
|
||||||
account.addBadge(clock, new AccountBadge("bar", Instant.ofEpochSecond(44), true));
|
account.addBadge(clock, new AccountBadge("bar", Instant.ofEpochSecond(44), true));
|
||||||
|
@ -214,6 +213,17 @@ class AccountTest {
|
||||||
assertThat(badge.expiration().getEpochSecond()).isEqualTo(51);
|
assertThat(badge.expiration().getEpochSecond()).isEqualTo(51);
|
||||||
assertThat(badge.visible()).isTrue();
|
assertThat(badge.visible()).isTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
clock.pin(Instant.ofEpochSecond(52));
|
||||||
|
|
||||||
|
// for a merged badge, visible = true is preferred
|
||||||
|
account.addBadge(clock, new AccountBadge("foo", Instant.ofEpochSecond(53), false));
|
||||||
|
|
||||||
|
assertThat(account.getBadges()).hasSize(1).element(0).satisfies(badge -> {
|
||||||
|
assertThat(badge.id()).isEqualTo("foo");
|
||||||
|
assertThat(badge.expiration().getEpochSecond()).isEqualTo(53);
|
||||||
|
assertThat(badge.visible()).isTrue();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -9,13 +9,16 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
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.anyByte;
|
import static org.mockito.ArgumentMatchers.anyByte;
|
||||||
|
import static org.mockito.ArgumentMatchers.argThat;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.never;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import com.google.protobuf.ByteString;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.time.Instant;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
@ -35,8 +38,10 @@ import org.whispersystems.textsecuregcm.entities.ECSignedPreKey;
|
||||||
import org.whispersystems.textsecuregcm.entities.IncomingMessage;
|
import org.whispersystems.textsecuregcm.entities.IncomingMessage;
|
||||||
import org.whispersystems.textsecuregcm.entities.KEMSignedPreKey;
|
import org.whispersystems.textsecuregcm.entities.KEMSignedPreKey;
|
||||||
import org.whispersystems.textsecuregcm.entities.MessageProtos;
|
import org.whispersystems.textsecuregcm.entities.MessageProtos;
|
||||||
|
import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier;
|
||||||
import org.whispersystems.textsecuregcm.push.MessageSender;
|
import org.whispersystems.textsecuregcm.push.MessageSender;
|
||||||
import org.whispersystems.textsecuregcm.tests.util.KeysHelper;
|
import org.whispersystems.textsecuregcm.tests.util.KeysHelper;
|
||||||
|
import org.whispersystems.textsecuregcm.util.TestClock;
|
||||||
|
|
||||||
public class ChangeNumberManagerTest {
|
public class ChangeNumberManagerTest {
|
||||||
private AccountsManager accountsManager;
|
private AccountsManager accountsManager;
|
||||||
|
@ -45,11 +50,13 @@ public class ChangeNumberManagerTest {
|
||||||
|
|
||||||
private Map<Account, UUID> updatedPhoneNumberIdentifiersByAccount;
|
private Map<Account, UUID> updatedPhoneNumberIdentifiersByAccount;
|
||||||
|
|
||||||
|
private static final TestClock CLOCK = TestClock.pinned(Instant.now());
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() throws Exception {
|
void setUp() throws Exception {
|
||||||
accountsManager = mock(AccountsManager.class);
|
accountsManager = mock(AccountsManager.class);
|
||||||
messageSender = mock(MessageSender.class);
|
messageSender = mock(MessageSender.class);
|
||||||
changeNumberManager = new ChangeNumberManager(messageSender, accountsManager);
|
changeNumberManager = new ChangeNumberManager(messageSender, accountsManager, CLOCK);
|
||||||
|
|
||||||
updatedPhoneNumberIdentifiersByAccount = new HashMap<>();
|
updatedPhoneNumberIdentifiersByAccount = new HashMap<>();
|
||||||
|
|
||||||
|
@ -103,7 +110,7 @@ public class ChangeNumberManagerTest {
|
||||||
changeNumberManager.changeNumber(account, "+18025551234", null, null, null, null, null, null);
|
changeNumberManager.changeNumber(account, "+18025551234", null, null, null, null, null, null);
|
||||||
verify(accountsManager).changeNumber(account, "+18025551234", null, null, null, null);
|
verify(accountsManager).changeNumber(account, "+18025551234", null, null, null, null);
|
||||||
verify(accountsManager, never()).updateDevice(any(), anyByte(), any());
|
verify(accountsManager, never()).updateDevice(any(), anyByte(), any());
|
||||||
verify(messageSender, never()).sendMessages(eq(account), any(), any(), any(), any());
|
verify(messageSender, never()).sendMessages(eq(account), any(), any(), any(), any(), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -117,7 +124,7 @@ public class ChangeNumberManagerTest {
|
||||||
|
|
||||||
changeNumberManager.changeNumber(account, "+18025551234", pniIdentityKey, prekeys, null, Collections.emptyList(), Collections.emptyMap(), null);
|
changeNumberManager.changeNumber(account, "+18025551234", pniIdentityKey, prekeys, null, Collections.emptyList(), Collections.emptyMap(), null);
|
||||||
verify(accountsManager).changeNumber(account, "+18025551234", pniIdentityKey, prekeys, null, Collections.emptyMap());
|
verify(accountsManager).changeNumber(account, "+18025551234", pniIdentityKey, prekeys, null, Collections.emptyMap());
|
||||||
verify(messageSender, never()).sendMessages(eq(account), any(), any(), any(), any());
|
verify(messageSender, never()).sendMessages(eq(account), any(), any(), any(), any(), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -132,45 +139,59 @@ public class ChangeNumberManagerTest {
|
||||||
when(account.getUuid()).thenReturn(aci);
|
when(account.getUuid()).thenReturn(aci);
|
||||||
when(account.getPhoneNumberIdentifier()).thenReturn(pni);
|
when(account.getPhoneNumberIdentifier()).thenReturn(pni);
|
||||||
|
|
||||||
final Device d2 = mock(Device.class);
|
final Device primaryDevice = mock(Device.class);
|
||||||
final byte deviceId2 = 2;
|
when(primaryDevice.getId()).thenReturn(Device.PRIMARY_ID);
|
||||||
when(d2.getId()).thenReturn(deviceId2);
|
when(primaryDevice.getRegistrationId()).thenReturn(7);
|
||||||
|
|
||||||
when(account.getDevice(deviceId2)).thenReturn(Optional.of(d2));
|
final Device linkedDevice = mock(Device.class);
|
||||||
when(account.getDevices()).thenReturn(List.of(d2));
|
final byte linkedDeviceId = Device.PRIMARY_ID + 1;
|
||||||
|
final int linkedDeviceRegistrationId = 17;
|
||||||
|
when(linkedDevice.getId()).thenReturn(linkedDeviceId);
|
||||||
|
when(linkedDevice.getRegistrationId()).thenReturn(linkedDeviceRegistrationId);
|
||||||
|
|
||||||
|
when(account.getDevice(anyByte())).thenReturn(Optional.empty());
|
||||||
|
when(account.getDevice(Device.PRIMARY_ID)).thenReturn(Optional.of(primaryDevice));
|
||||||
|
when(account.getDevice(linkedDeviceId)).thenReturn(Optional.of(linkedDevice));
|
||||||
|
when(account.getDevices()).thenReturn(List.of(primaryDevice, linkedDevice));
|
||||||
|
|
||||||
final ECKeyPair pniIdentityKeyPair = Curve.generateKeyPair();
|
final ECKeyPair pniIdentityKeyPair = Curve.generateKeyPair();
|
||||||
final IdentityKey pniIdentityKey = new IdentityKey(pniIdentityKeyPair.getPublicKey());
|
final IdentityKey pniIdentityKey = new IdentityKey(pniIdentityKeyPair.getPublicKey());
|
||||||
final Map<Byte, ECSignedPreKey> prekeys = Map.of(Device.PRIMARY_ID,
|
final Map<Byte, ECSignedPreKey> prekeys = Map.of(Device.PRIMARY_ID,
|
||||||
KeysHelper.signedECPreKey(1, pniIdentityKeyPair),
|
KeysHelper.signedECPreKey(1, pniIdentityKeyPair),
|
||||||
deviceId2, KeysHelper.signedECPreKey(2, pniIdentityKeyPair));
|
linkedDeviceId, KeysHelper.signedECPreKey(2, pniIdentityKeyPair));
|
||||||
final Map<Byte, Integer> registrationIds = Map.of(Device.PRIMARY_ID, 17, deviceId2, 19);
|
final Map<Byte, Integer> registrationIds = Map.of(Device.PRIMARY_ID, 17, linkedDeviceId, 19);
|
||||||
|
|
||||||
final IncomingMessage msg = mock(IncomingMessage.class);
|
final IncomingMessage msg = mock(IncomingMessage.class);
|
||||||
when(msg.destinationDeviceId()).thenReturn(deviceId2);
|
when(msg.type()).thenReturn(1);
|
||||||
|
when(msg.destinationDeviceId()).thenReturn(linkedDeviceId);
|
||||||
|
when(msg.destinationRegistrationId()).thenReturn(linkedDeviceRegistrationId);
|
||||||
when(msg.content()).thenReturn(new byte[]{1});
|
when(msg.content()).thenReturn(new byte[]{1});
|
||||||
|
|
||||||
changeNumberManager.changeNumber(account, changedE164, pniIdentityKey, prekeys, null, List.of(msg), registrationIds, null);
|
changeNumberManager.changeNumber(account, changedE164, pniIdentityKey, prekeys, null, List.of(msg), registrationIds, null);
|
||||||
|
|
||||||
verify(accountsManager).changeNumber(account, changedE164, pniIdentityKey, prekeys, null, registrationIds);
|
verify(accountsManager).changeNumber(account, changedE164, pniIdentityKey, prekeys, null, registrationIds);
|
||||||
|
|
||||||
@SuppressWarnings("unchecked") final ArgumentCaptor<Map<Byte, MessageProtos.Envelope>> envelopeCaptor =
|
final MessageProtos.Envelope expectedEnvelope = MessageProtos.Envelope.newBuilder()
|
||||||
ArgumentCaptor.forClass(Map.class);
|
.setType(MessageProtos.Envelope.Type.forNumber(msg.type()))
|
||||||
|
.setClientTimestamp(CLOCK.millis())
|
||||||
|
.setServerTimestamp(CLOCK.millis())
|
||||||
|
.setDestinationServiceId(new AciServiceIdentifier(aci).toServiceIdentifierString())
|
||||||
|
.setContent(ByteString.copyFrom(msg.content()))
|
||||||
|
.setSourceServiceId(new AciServiceIdentifier(aci).toServiceIdentifierString())
|
||||||
|
.setSourceDevice(Device.PRIMARY_ID)
|
||||||
|
.setUpdatedPni(updatedPhoneNumberIdentifiersByAccount.get(account).toString())
|
||||||
|
.setUrgent(true)
|
||||||
|
.setEphemeral(false)
|
||||||
|
.build();
|
||||||
|
|
||||||
verify(messageSender).sendMessages(any(), any(), envelopeCaptor.capture(), any(), any());
|
verify(messageSender).sendMessages(argThat(a -> a.getUuid().equals(aci)),
|
||||||
|
eq(new AciServiceIdentifier(aci)),
|
||||||
assertEquals(1, envelopeCaptor.getValue().size());
|
eq(Map.of(linkedDeviceId, expectedEnvelope)),
|
||||||
assertEquals(Set.of(deviceId2), envelopeCaptor.getValue().keySet());
|
eq(Map.of(linkedDeviceId, linkedDeviceRegistrationId)),
|
||||||
|
eq(Optional.of(Device.PRIMARY_ID)),
|
||||||
final MessageProtos.Envelope envelope = envelopeCaptor.getValue().get(deviceId2);
|
any());
|
||||||
|
|
||||||
assertEquals(aci, UUID.fromString(envelope.getDestinationServiceId()));
|
|
||||||
assertEquals(aci, UUID.fromString(envelope.getSourceServiceId()));
|
|
||||||
assertEquals(Device.PRIMARY_ID, envelope.getSourceDevice());
|
|
||||||
assertEquals(updatedPhoneNumberIdentifiersByAccount.get(account), UUID.fromString(envelope.getUpdatedPni()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void changeNumberSetPrimaryDevicePrekeyPqAndSendMessages() throws Exception {
|
void changeNumberSetPrimaryDevicePrekeyPqAndSendMessages() throws Exception {
|
||||||
final String originalE164 = "+18005551234";
|
final String originalE164 = "+18005551234";
|
||||||
|
@ -210,7 +231,7 @@ public class ChangeNumberManagerTest {
|
||||||
@SuppressWarnings("unchecked") final ArgumentCaptor<Map<Byte, MessageProtos.Envelope>> envelopeCaptor =
|
@SuppressWarnings("unchecked") final ArgumentCaptor<Map<Byte, MessageProtos.Envelope>> envelopeCaptor =
|
||||||
ArgumentCaptor.forClass(Map.class);
|
ArgumentCaptor.forClass(Map.class);
|
||||||
|
|
||||||
verify(messageSender).sendMessages(any(), any(), envelopeCaptor.capture(), any(), any());
|
verify(messageSender).sendMessages(any(), any(), envelopeCaptor.capture(), any(), any(), any());
|
||||||
|
|
||||||
assertEquals(1, envelopeCaptor.getValue().size());
|
assertEquals(1, envelopeCaptor.getValue().size());
|
||||||
assertEquals(Set.of(deviceId2), envelopeCaptor.getValue().keySet());
|
assertEquals(Set.of(deviceId2), envelopeCaptor.getValue().keySet());
|
||||||
|
@ -261,7 +282,7 @@ public class ChangeNumberManagerTest {
|
||||||
@SuppressWarnings("unchecked") final ArgumentCaptor<Map<Byte, MessageProtos.Envelope>> envelopeCaptor =
|
@SuppressWarnings("unchecked") final ArgumentCaptor<Map<Byte, MessageProtos.Envelope>> envelopeCaptor =
|
||||||
ArgumentCaptor.forClass(Map.class);
|
ArgumentCaptor.forClass(Map.class);
|
||||||
|
|
||||||
verify(messageSender).sendMessages(any(), any(), envelopeCaptor.capture(), any(), any());
|
verify(messageSender).sendMessages(any(), any(), envelopeCaptor.capture(), any(), any(), any());
|
||||||
|
|
||||||
assertEquals(1, envelopeCaptor.getValue().size());
|
assertEquals(1, envelopeCaptor.getValue().size());
|
||||||
assertEquals(Set.of(deviceId2), envelopeCaptor.getValue().keySet());
|
assertEquals(Set.of(deviceId2), envelopeCaptor.getValue().keySet());
|
||||||
|
@ -308,7 +329,7 @@ public class ChangeNumberManagerTest {
|
||||||
@SuppressWarnings("unchecked") final ArgumentCaptor<Map<Byte, MessageProtos.Envelope>> envelopeCaptor =
|
@SuppressWarnings("unchecked") final ArgumentCaptor<Map<Byte, MessageProtos.Envelope>> envelopeCaptor =
|
||||||
ArgumentCaptor.forClass(Map.class);
|
ArgumentCaptor.forClass(Map.class);
|
||||||
|
|
||||||
verify(messageSender).sendMessages(any(), any(), envelopeCaptor.capture(), any(), any());
|
verify(messageSender).sendMessages(any(), any(), envelopeCaptor.capture(), any(), any(), any());
|
||||||
|
|
||||||
assertEquals(1, envelopeCaptor.getValue().size());
|
assertEquals(1, envelopeCaptor.getValue().size());
|
||||||
assertEquals(Set.of(deviceId2), envelopeCaptor.getValue().keySet());
|
assertEquals(Set.of(deviceId2), envelopeCaptor.getValue().keySet());
|
||||||
|
@ -357,7 +378,7 @@ public class ChangeNumberManagerTest {
|
||||||
@SuppressWarnings("unchecked") final ArgumentCaptor<Map<Byte, MessageProtos.Envelope>> envelopeCaptor =
|
@SuppressWarnings("unchecked") final ArgumentCaptor<Map<Byte, MessageProtos.Envelope>> envelopeCaptor =
|
||||||
ArgumentCaptor.forClass(Map.class);
|
ArgumentCaptor.forClass(Map.class);
|
||||||
|
|
||||||
verify(messageSender).sendMessages(any(), any(), envelopeCaptor.capture(), any(), any());
|
verify(messageSender).sendMessages(any(), any(), envelopeCaptor.capture(), any(), any(), any());
|
||||||
|
|
||||||
assertEquals(1, envelopeCaptor.getValue().size());
|
assertEquals(1, envelopeCaptor.getValue().size());
|
||||||
assertEquals(Set.of(deviceId2), envelopeCaptor.getValue().keySet());
|
assertEquals(Set.of(deviceId2), envelopeCaptor.getValue().keySet());
|
||||||
|
|
|
@ -31,8 +31,8 @@ import org.whispersystems.textsecuregcm.auth.SaltedTokenHash;
|
||||||
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
|
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
|
||||||
import org.whispersystems.textsecuregcm.storage.Account;
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.DeviceSpec;
|
|
||||||
import org.whispersystems.textsecuregcm.storage.Device;
|
import org.whispersystems.textsecuregcm.storage.Device;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.DeviceSpec;
|
||||||
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||||
|
|
||||||
public class AccountsHelper {
|
public class AccountsHelper {
|
||||||
|
@ -62,6 +62,71 @@ public class AccountsHelper {
|
||||||
setupMockUpdate(mockAccountsManager, false);
|
setupMockUpdate(mockAccountsManager, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up stubbing for:
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link AccountsManager#update(Account, Consumer)}</li>
|
||||||
|
* <li>{@link AccountsManager#updateAsync(Account, Consumer)}</li>
|
||||||
|
* <li>{@link AccountsManager#updateDevice(Account, byte, Consumer)}</li>
|
||||||
|
* <li>{@link AccountsManager#updateDeviceAsync(Account, byte, Consumer)}</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* with multiple calls to the {@link Consumer<Account>}. This simulates retries from {@link org.whispersystems.textsecuregcm.storage.ContestedOptimisticLockException}.
|
||||||
|
* Callers will typically set up stubbing for relevant {@link Account} methods with multiple {@link org.mockito.stubbing.OngoingStubbing#thenReturn(Object)}
|
||||||
|
* calls:
|
||||||
|
* <pre>
|
||||||
|
* // example stubbing
|
||||||
|
* when(account.getNextDeviceId())
|
||||||
|
* .thenReturn(2)
|
||||||
|
* .thenReturn(3);
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public static void setupMockUpdateWithRetries(final AccountsManager mockAccountsManager, final int retryCount) {
|
||||||
|
when(mockAccountsManager.update(any(), any())).thenAnswer(answer -> {
|
||||||
|
final Account account = answer.getArgument(0, Account.class);
|
||||||
|
|
||||||
|
for (int i = 0; i < retryCount; i++) {
|
||||||
|
answer.getArgument(1, Consumer.class).accept(account);
|
||||||
|
}
|
||||||
|
|
||||||
|
return copyAndMarkStale(account);
|
||||||
|
});
|
||||||
|
|
||||||
|
when(mockAccountsManager.updateAsync(any(), any())).thenAnswer(answer -> {
|
||||||
|
final Account account = answer.getArgument(0, Account.class);
|
||||||
|
|
||||||
|
for (int i = 0; i < retryCount; i++) {
|
||||||
|
answer.getArgument(1, Consumer.class).accept(account);
|
||||||
|
}
|
||||||
|
|
||||||
|
return CompletableFuture.completedFuture(copyAndMarkStale(account));
|
||||||
|
});
|
||||||
|
|
||||||
|
when(mockAccountsManager.updateDevice(any(), anyByte(), any())).thenAnswer(answer -> {
|
||||||
|
final Account account = answer.getArgument(0, Account.class);
|
||||||
|
final byte deviceId = answer.getArgument(1, Byte.class);
|
||||||
|
|
||||||
|
for (int i = 0; i < retryCount; i++) {
|
||||||
|
account.getDevice(deviceId).ifPresent(answer.getArgument(2, Consumer.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
return copyAndMarkStale(account);
|
||||||
|
});
|
||||||
|
|
||||||
|
when(mockAccountsManager.updateDeviceAsync(any(), anyByte(), any())).thenAnswer(answer -> {
|
||||||
|
final Account account = answer.getArgument(0, Account.class);
|
||||||
|
final byte deviceId = answer.getArgument(1, Byte.class);
|
||||||
|
|
||||||
|
for (int i = 0; i < retryCount; i++) {
|
||||||
|
account.getDevice(deviceId).ifPresent(answer.getArgument(2, Consumer.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
return CompletableFuture.completedFuture(copyAndMarkStale(account));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
private static void setupMockUpdate(final AccountsManager mockAccountsManager, final boolean markStale) {
|
private static void setupMockUpdate(final AccountsManager mockAccountsManager, final boolean markStale) {
|
||||||
when(mockAccountsManager.update(any(), any())).thenAnswer(answer -> {
|
when(mockAccountsManager.update(any(), any())).thenAnswer(answer -> {
|
||||||
final Account account = answer.getArgument(0, Account.class);
|
final Account account = answer.getArgument(0, Account.class);
|
||||||
|
|
|
@ -13,28 +13,20 @@ import java.util.stream.Stream;
|
||||||
import org.junit.jupiter.params.ParameterizedTest;
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
import org.junit.jupiter.params.provider.Arguments;
|
import org.junit.jupiter.params.provider.Arguments;
|
||||||
import org.junit.jupiter.params.provider.MethodSource;
|
import org.junit.jupiter.params.provider.MethodSource;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
class UserAgentUtilTest {
|
class UserAgentUtilTest {
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@MethodSource
|
|
||||||
void testParseBogusUserAgentString(final String userAgentString) {
|
|
||||||
assertThrows(UnrecognizedUserAgentException.class, () -> UserAgentUtil.parseUserAgentString(userAgentString));
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
private static Stream<String> testParseBogusUserAgentString() {
|
|
||||||
return Stream.of(
|
|
||||||
null,
|
|
||||||
"This is obviously not a reasonable User-Agent string.",
|
|
||||||
"Signal-Android/4.6-8.3.unreasonableversionstring-17"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@MethodSource("argumentsForTestParseStandardUserAgentString")
|
@MethodSource("argumentsForTestParseStandardUserAgentString")
|
||||||
void testParseStandardUserAgentString(final String userAgentString, final UserAgent expectedUserAgent) {
|
void testParseStandardUserAgentString(final String userAgentString, @Nullable final UserAgent expectedUserAgent)
|
||||||
assertEquals(expectedUserAgent, UserAgentUtil.parseStandardUserAgentString(userAgentString));
|
throws UnrecognizedUserAgentException {
|
||||||
|
|
||||||
|
if (expectedUserAgent != null) {
|
||||||
|
assertEquals(expectedUserAgent, UserAgentUtil.parseUserAgentString(userAgentString));
|
||||||
|
} else {
|
||||||
|
assertThrows(UnrecognizedUserAgentException.class, () -> UserAgentUtil.parseUserAgentString(userAgentString));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Stream<Arguments> argumentsForTestParseStandardUserAgentString() {
|
private static Stream<Arguments> argumentsForTestParseStandardUserAgentString() {
|
||||||
|
@ -42,18 +34,18 @@ class UserAgentUtilTest {
|
||||||
Arguments.of("This is obviously not a reasonable User-Agent string.", null),
|
Arguments.of("This is obviously not a reasonable User-Agent string.", null),
|
||||||
Arguments.of("Signal-Android/4.68.3 Android/25",
|
Arguments.of("Signal-Android/4.68.3 Android/25",
|
||||||
new UserAgent(ClientPlatform.ANDROID, new Semver("4.68.3"), "Android/25")),
|
new UserAgent(ClientPlatform.ANDROID, new Semver("4.68.3"), "Android/25")),
|
||||||
Arguments.of("Signal-Android/4.68.3", new UserAgent(ClientPlatform.ANDROID, new Semver("4.68.3"))),
|
Arguments.of("Signal-Android/4.68.3", new UserAgent(ClientPlatform.ANDROID, new Semver("4.68.3"), null)),
|
||||||
Arguments.of("Signal-Desktop/1.2.3 Linux", new UserAgent(ClientPlatform.DESKTOP, new Semver("1.2.3"), "Linux")),
|
Arguments.of("Signal-Desktop/1.2.3 Linux", new UserAgent(ClientPlatform.DESKTOP, new Semver("1.2.3"), "Linux")),
|
||||||
Arguments.of("Signal-Desktop/1.2.3 macOS", new UserAgent(ClientPlatform.DESKTOP, new Semver("1.2.3"), "macOS")),
|
Arguments.of("Signal-Desktop/1.2.3 macOS", new UserAgent(ClientPlatform.DESKTOP, new Semver("1.2.3"), "macOS")),
|
||||||
Arguments.of("Signal-Desktop/1.2.3 Windows",
|
Arguments.of("Signal-Desktop/1.2.3 Windows",
|
||||||
new UserAgent(ClientPlatform.DESKTOP, new Semver("1.2.3"), "Windows")),
|
new UserAgent(ClientPlatform.DESKTOP, new Semver("1.2.3"), "Windows")),
|
||||||
Arguments.of("Signal-Desktop/1.2.3", new UserAgent(ClientPlatform.DESKTOP, new Semver("1.2.3"))),
|
Arguments.of("Signal-Desktop/1.2.3", new UserAgent(ClientPlatform.DESKTOP, new Semver("1.2.3"), null)),
|
||||||
Arguments.of("Signal-Desktop/1.32.0-beta.3",
|
Arguments.of("Signal-Desktop/1.32.0-beta.3",
|
||||||
new UserAgent(ClientPlatform.DESKTOP, new Semver("1.32.0-beta.3"))),
|
new UserAgent(ClientPlatform.DESKTOP, new Semver("1.32.0-beta.3"), null)),
|
||||||
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"), null)),
|
||||||
Arguments.of("Signal-Android/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 tonic/0.31",
|
||||||
new UserAgent(ClientPlatform.ANDROID, new Semver("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",
|
Arguments.of("Signal-Android/7.11.23-nightly-1982-06-28-07-07-07 Android/42 tonic/0.31",
|
||||||
|
|
|
@ -23,14 +23,7 @@ message GetRequestAttributesResponse {
|
||||||
repeated string acceptable_languages = 1;
|
repeated string acceptable_languages = 1;
|
||||||
repeated string available_accepted_locales = 2;
|
repeated string available_accepted_locales = 2;
|
||||||
string remote_address = 3;
|
string remote_address = 3;
|
||||||
string raw_user_agent = 4;
|
string user_agent = 4;
|
||||||
UserAgent user_agent = 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
message UserAgent {
|
|
||||||
string platform = 1;
|
|
||||||
string version = 2;
|
|
||||||
string additional_specifiers = 3;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message GetAuthenticatedDeviceRequest {
|
message GetAuthenticatedDeviceRequest {
|
||||||
|
|
|
@ -470,7 +470,8 @@ turn:
|
||||||
cloudflare:
|
cloudflare:
|
||||||
apiToken: secret://turn.cloudflare.apiToken
|
apiToken: secret://turn.cloudflare.apiToken
|
||||||
endpoint: https://rtc.live.cloudflare.com/v1/turn/keys/LMNOP/credentials/generate
|
endpoint: https://rtc.live.cloudflare.com/v1/turn/keys/LMNOP/credentials/generate
|
||||||
ttl: 86400
|
requestedCredentialTtl: PT24H
|
||||||
|
clientCredentialTtl: PT12H
|
||||||
urls:
|
urls:
|
||||||
- turn:turn.example.com:80
|
- turn:turn.example.com:80
|
||||||
urlsWithIps:
|
urlsWithIps:
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 8f566196d763c8eb1f3c8fcefd5be3c35ff8d148
|
Subproject commit d9852e294a853b88c7feaa748e17fee38acbf849
|
Loading…
Reference in New Issue