mirror of
https://github.com/signalapp/Signal-Server.git
synced 2025-12-05 01:10:13 +00:00
Compare commits
7 Commits
aa2f9e5a65
...
08f6ec639c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08f6ec639c | ||
|
|
6c3cfc88b5 | ||
|
|
389d44fd80 | ||
|
|
7604306818 | ||
|
|
92e133b21f | ||
|
|
4af50986e0 | ||
|
|
c72458b47a |
@@ -4,8 +4,8 @@
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm;
|
||||
|
||||
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.webauthn4j.appattest.DeviceCheckManager;
|
||||
@@ -125,7 +125,6 @@ import org.whispersystems.textsecuregcm.controllers.ProfileController;
|
||||
import org.whispersystems.textsecuregcm.controllers.ProvisioningController;
|
||||
import org.whispersystems.textsecuregcm.controllers.RegistrationController;
|
||||
import org.whispersystems.textsecuregcm.controllers.RemoteConfigController;
|
||||
import org.whispersystems.textsecuregcm.controllers.RemoteConfigControllerV1;
|
||||
import org.whispersystems.textsecuregcm.controllers.SecureStorageController;
|
||||
import org.whispersystems.textsecuregcm.controllers.SecureValueRecovery2Controller;
|
||||
import org.whispersystems.textsecuregcm.controllers.StickerController;
|
||||
@@ -1085,7 +1084,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
ReceiptCredentialPresentation::new),
|
||||
new KeysController(rateLimiters, keysManager, accountsManager, zkSecretParams, Clock.systemUTC()),
|
||||
new KeyTransparencyController(keyTransparencyServiceClient),
|
||||
new MessageController(rateLimiters, messageByteLimitCardinalityEstimator, messageSender, receiptSender,
|
||||
new MessageController(rateLimiters, messageByteLimitCardinalityEstimator, messageSender,
|
||||
accountsManager, messagesManager, phoneNumberIdentifiers, pushNotificationManager, pushNotificationScheduler,
|
||||
reportMessageManager, messageDeliveryScheduler, clientReleaseManager,
|
||||
zkSecretParams, spamChecker, messageMetrics, messageDeliveryLoopMonitor,
|
||||
@@ -1097,7 +1096,6 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
new ProvisioningController(rateLimiters, provisioningManager),
|
||||
new RegistrationController(accountsManager, phoneVerificationTokenManager, registrationLockVerificationManager,
|
||||
rateLimiters),
|
||||
new RemoteConfigControllerV1(remoteConfigsManager, config.getRemoteConfigConfiguration().globalConfig(), clock),
|
||||
new RemoteConfigController(remoteConfigsManager, config.getRemoteConfigConfiguration().globalConfig(), clock),
|
||||
new SecureStorageController(storageCredentialsGenerator),
|
||||
new SecureValueRecovery2Controller(svr2CredentialsGenerator, accountsManager),
|
||||
|
||||
@@ -66,11 +66,15 @@ public class CallQualitySurveyController {
|
||||
try {
|
||||
submitCallQualitySurveyRequest = SubmitCallQualitySurveyRequest.parseFrom(surveyResponse);
|
||||
} catch (final InvalidProtocolBufferException e) {
|
||||
throw new WebApplicationException(422);
|
||||
throw new WebApplicationException("Invalid protobuf entity", 422);
|
||||
}
|
||||
|
||||
final String remoteAddress = (String) requestContext.getProperty(RemoteAddressFilter.REMOTE_ADDRESS_ATTRIBUTE_NAME);
|
||||
|
||||
callQualitySurveyManager.submitCallQualitySurvey(submitCallQualitySurveyRequest, remoteAddress, userAgentString);
|
||||
try {
|
||||
callQualitySurveyManager.submitCallQualitySurvey(submitCallQualitySurveyRequest, remoteAddress, userAgentString);
|
||||
} catch (final IllegalArgumentException e) {
|
||||
throw new WebApplicationException(e.getMessage(), 422);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.ws.rs.BadRequestException;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.DELETE;
|
||||
import jakarta.ws.rs.DefaultValue;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.HeaderParam;
|
||||
@@ -74,7 +73,6 @@ import org.whispersystems.textsecuregcm.entities.AccountStaleDevices;
|
||||
import org.whispersystems.textsecuregcm.entities.IncomingMessage;
|
||||
import org.whispersystems.textsecuregcm.entities.IncomingMessageList;
|
||||
import org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
|
||||
import org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope.Type;
|
||||
import org.whispersystems.textsecuregcm.entities.MismatchedDevicesResponse;
|
||||
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntity;
|
||||
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntityList;
|
||||
@@ -97,7 +95,6 @@ import org.whispersystems.textsecuregcm.push.MessageTooLargeException;
|
||||
import org.whispersystems.textsecuregcm.push.MessageUtil;
|
||||
import org.whispersystems.textsecuregcm.push.PushNotificationManager;
|
||||
import org.whispersystems.textsecuregcm.push.PushNotificationScheduler;
|
||||
import org.whispersystems.textsecuregcm.push.ReceiptSender;
|
||||
import org.whispersystems.textsecuregcm.spam.MessageType;
|
||||
import org.whispersystems.textsecuregcm.spam.SpamCheckResult;
|
||||
import org.whispersystems.textsecuregcm.spam.SpamChecker;
|
||||
@@ -123,7 +120,6 @@ public class MessageController {
|
||||
private final RateLimiters rateLimiters;
|
||||
private final CardinalityEstimator messageByteLimitEstimator;
|
||||
private final MessageSender messageSender;
|
||||
private final ReceiptSender receiptSender;
|
||||
private final AccountsManager accountsManager;
|
||||
private final MessagesManager messagesManager;
|
||||
private final PhoneNumberIdentifiers phoneNumberIdentifiers;
|
||||
@@ -177,7 +173,6 @@ public class MessageController {
|
||||
RateLimiters rateLimiters,
|
||||
CardinalityEstimator messageByteLimitEstimator,
|
||||
MessageSender messageSender,
|
||||
ReceiptSender receiptSender,
|
||||
AccountsManager accountsManager,
|
||||
MessagesManager messagesManager,
|
||||
PhoneNumberIdentifiers phoneNumberIdentifiers,
|
||||
@@ -194,7 +189,6 @@ public class MessageController {
|
||||
this.rateLimiters = rateLimiters;
|
||||
this.messageByteLimitEstimator = messageByteLimitEstimator;
|
||||
this.messageSender = messageSender;
|
||||
this.receiptSender = receiptSender;
|
||||
this.accountsManager = accountsManager;
|
||||
this.messagesManager = messagesManager;
|
||||
this.phoneNumberIdentifiers = phoneNumberIdentifiers;
|
||||
@@ -843,37 +837,6 @@ public class MessageController {
|
||||
return size;
|
||||
}
|
||||
|
||||
@DELETE
|
||||
@Path("/uuid/{uuid}")
|
||||
public CompletableFuture<Response> removePendingMessage(@Auth AuthenticatedDevice auth, @PathParam("uuid") UUID uuid) {
|
||||
final Account account = accountsManager.getByAccountIdentifier(auth.accountIdentifier())
|
||||
.orElseThrow(() -> new WebApplicationException(Status.UNAUTHORIZED));
|
||||
|
||||
final Device device = account.getDevice(auth.deviceId())
|
||||
.orElseThrow(() -> new WebApplicationException(Status.UNAUTHORIZED));
|
||||
|
||||
return messagesManager.delete(auth.accountIdentifier(), device, uuid, null)
|
||||
.thenAccept(maybeRemovedMessage -> maybeRemovedMessage.ifPresent(removedMessage -> {
|
||||
if (removedMessage.sourceServiceId().isPresent()
|
||||
&& removedMessage.envelopeType() != Type.SERVER_DELIVERY_RECEIPT) {
|
||||
if (removedMessage.sourceServiceId().get() instanceof AciServiceIdentifier aciServiceIdentifier) {
|
||||
try {
|
||||
receiptSender.sendReceipt(removedMessage.destinationServiceId(), auth.deviceId(),
|
||||
aciServiceIdentifier, removedMessage.clientTimestamp());
|
||||
} catch (Exception e) {
|
||||
logger.warn("Failed to send delivery receipt", e);
|
||||
}
|
||||
} else {
|
||||
// If source service ID is present and the envelope type is not a server delivery receipt, then
|
||||
// the source service ID *should always* be an ACI -- PNIs are receive-only, so they can only be the
|
||||
// "source" via server delivery receipts
|
||||
logger.warn("Source service ID unexpectedly a PNI service ID");
|
||||
}
|
||||
}
|
||||
}))
|
||||
.thenApply(Util.ASYNC_EMPTY_RESPONSE);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Path("/report/{source}/{messageGuid}")
|
||||
|
||||
@@ -92,7 +92,8 @@ public class RegistrationController {
|
||||
2. gets 409 from device available for transfer \n
|
||||
3. success \n
|
||||
""")
|
||||
@ApiResponse(responseCode = "200", description = "The phone number associated with the authenticated account was changed successfully", useReturnTypeSchema = true)
|
||||
@ApiResponse(responseCode = "200", description = "Account creation succeeded", useReturnTypeSchema = true)
|
||||
@ApiResponse(responseCode = "401", description = "The session identified in the request is not verified")
|
||||
@ApiResponse(responseCode = "403", description = "Verification failed for the provided Registration Recovery Password")
|
||||
@ApiResponse(responseCode = "409", description = "The caller has not explicitly elected to skip transferring data from another device, but a device transfer is technically possible")
|
||||
@ApiResponse(responseCode = "422", description = "The request did not pass validation")
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import io.dropwizard.auth.Auth;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.Clock;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedDevice;
|
||||
import org.whispersystems.textsecuregcm.entities.UserRemoteConfig;
|
||||
import org.whispersystems.textsecuregcm.entities.UserRemoteConfigList;
|
||||
import org.whispersystems.textsecuregcm.storage.RemoteConfigsManager;
|
||||
import org.whispersystems.textsecuregcm.util.Conversions;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
@Path("/v1/config")
|
||||
@Tag(name = "Remote Config")
|
||||
public class RemoteConfigControllerV1 {
|
||||
|
||||
private final RemoteConfigsManager remoteConfigsManager;
|
||||
private final Map<String, String> globalConfig;
|
||||
|
||||
private final Clock clock;
|
||||
|
||||
private static final String GLOBAL_CONFIG_PREFIX = "global.";
|
||||
|
||||
public RemoteConfigControllerV1(RemoteConfigsManager remoteConfigsManager,
|
||||
Map<String, String> globalConfig,
|
||||
final Clock clock) {
|
||||
this.remoteConfigsManager = remoteConfigsManager;
|
||||
this.globalConfig = globalConfig;
|
||||
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Deprecated
|
||||
@Operation(
|
||||
summary = "Fetch remote configuration (deprecated)",
|
||||
description = """
|
||||
Remote configuration is a list of namespaced keys that clients may use for consistent configuration or behavior.
|
||||
|
||||
Configuration values change over time, and the list should be refreshed periodically, typically at client
|
||||
launch and every few hours thereafter.
|
||||
|
||||
This endpoint is deprecated; use GET /v2/config instead
|
||||
"""
|
||||
)
|
||||
@ApiResponse(responseCode = "200", description = "Remote configuration values for the authenticated user", useReturnTypeSchema = true)
|
||||
public UserRemoteConfigList getAll(@Auth AuthenticatedDevice auth) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
|
||||
final Stream<UserRemoteConfig> globalConfigStream = globalConfig.entrySet().stream()
|
||||
.map(entry -> new UserRemoteConfig(GLOBAL_CONFIG_PREFIX + entry.getKey(), true, entry.getValue()));
|
||||
return new UserRemoteConfigList(Stream.concat(remoteConfigsManager.getAll().stream().map(config -> {
|
||||
final byte[] hashKey = config.getHashKey() != null ? config.getHashKey().getBytes(StandardCharsets.UTF_8)
|
||||
: config.getName().getBytes(StandardCharsets.UTF_8);
|
||||
boolean inBucket = isInBucket(digest, auth.accountIdentifier(), hashKey, config.getPercentage(),
|
||||
config.getUuids());
|
||||
return new UserRemoteConfig(config.getName(), inBucket,
|
||||
inBucket ? config.getValue() : config.getDefaultValue());
|
||||
}), globalConfigStream).collect(Collectors.toList()), clock.instant());
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public static boolean isInBucket(MessageDigest digest, UUID uid, byte[] hashKey, int configPercentage,
|
||||
Set<UUID> uuidsInBucket) {
|
||||
if (uuidsInBucket.contains(uid)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
ByteBuffer bb = ByteBuffer.wrap(new byte[16]);
|
||||
bb.putLong(uid.getMostSignificantBits());
|
||||
bb.putLong(uid.getLeastSignificantBits());
|
||||
|
||||
digest.update(bb.array());
|
||||
|
||||
byte[] hash = digest.digest(hashKey);
|
||||
int bucket = (int) (Util.ensureNonNegativeLong(Conversions.byteArrayToLong(hash)) % 100);
|
||||
|
||||
return bucket < configPercentage;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
package org.whispersystems.textsecuregcm.grpc;
|
||||
|
||||
import io.grpc.Status;
|
||||
import io.grpc.StatusRuntimeException;
|
||||
import org.signal.chat.calling.quality.SimpleCallQualityGrpc;
|
||||
import org.signal.chat.calling.quality.SubmitCallQualitySurveyRequest;
|
||||
import org.signal.chat.calling.quality.SubmitCallQualitySurveyResponse;
|
||||
@@ -32,9 +34,13 @@ public class CallQualitySurveyGrpcService extends SimpleCallQualityGrpc.CallQual
|
||||
|
||||
rateLimiters.getSubmitCallQualitySurveyLimiter().validate(remoteAddress);
|
||||
|
||||
callQualitySurveyManager.submitCallQualitySurvey(request,
|
||||
remoteAddress,
|
||||
RequestAttributesUtil.getUserAgent().orElse(null));
|
||||
try {
|
||||
callQualitySurveyManager.submitCallQualitySurvey(request,
|
||||
remoteAddress,
|
||||
RequestAttributesUtil.getUserAgent().orElse(null));
|
||||
} catch (final IllegalArgumentException e) {
|
||||
throw Status.INVALID_ARGUMENT.withDescription(e.getMessage()).asRuntimeException();
|
||||
}
|
||||
|
||||
return SubmitCallQualitySurveyResponse.getDefaultInstance();
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
package org.whispersystems.textsecuregcm.metrics;
|
||||
|
||||
import com.google.cloud.pubsub.v1.PublisherInterface;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.pubsub.v1.PubsubMessage;
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import java.time.Clock;
|
||||
@@ -147,4 +148,23 @@ public class CallQualitySurveyManager {
|
||||
.increment();
|
||||
});
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static void validateRequest(final SubmitCallQualitySurveyRequest request) {
|
||||
if (request.getStartTimestamp() == 0) {
|
||||
throw new IllegalArgumentException("Start timestamp not specified");
|
||||
}
|
||||
|
||||
if (request.getEndTimestamp() == 0) {
|
||||
throw new IllegalArgumentException("End timestamp not specified");
|
||||
}
|
||||
|
||||
if (StringUtils.isBlank(request.getCallType())) {
|
||||
throw new IllegalArgumentException("Call type not specified");
|
||||
}
|
||||
|
||||
if (StringUtils.isBlank(request.getCallEndReason())) {
|
||||
throw new IllegalArgumentException("Call end reason not specified");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
|
||||
import static io.micrometer.core.instrument.Metrics.timer;
|
||||
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
@@ -30,9 +30,6 @@ import org.whispersystems.textsecuregcm.entities.MessageProtos;
|
||||
import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager;
|
||||
import org.whispersystems.textsecuregcm.util.AttributeValues;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Scheduler;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
import software.amazon.awssdk.core.SdkBytes;
|
||||
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
|
||||
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
|
||||
@@ -51,9 +48,6 @@ public class MessagesDynamoDb extends AbstractDynamoDbStore {
|
||||
@VisibleForTesting
|
||||
static final String KEY_SORT = "S";
|
||||
|
||||
@VisibleForTesting
|
||||
static final String LOCAL_INDEX_MESSAGE_UUID_NAME = "Message_UUID_Index";
|
||||
|
||||
@VisibleForTesting
|
||||
static final String LOCAL_INDEX_MESSAGE_UUID_KEY_SORT = "U";
|
||||
|
||||
@@ -69,7 +63,6 @@ public class MessagesDynamoDb extends AbstractDynamoDbStore {
|
||||
private final String tableName;
|
||||
private final Duration timeToLive;
|
||||
private final ExecutorService messageDeletionExecutor;
|
||||
private final Scheduler messageDeletionScheduler;
|
||||
private final ExperimentEnrollmentManager experimentEnrollmentManager;
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(MessagesDynamoDb.class);
|
||||
@@ -84,7 +77,6 @@ public class MessagesDynamoDb extends AbstractDynamoDbStore {
|
||||
this.timeToLive = timeToLive;
|
||||
|
||||
this.messageDeletionExecutor = messageDeletionExecutor;
|
||||
this.messageDeletionScheduler = Schedulers.fromExecutor(messageDeletionExecutor);
|
||||
this.experimentEnrollmentManager = experimentEnrollmentManager;
|
||||
}
|
||||
|
||||
@@ -164,48 +156,6 @@ public class MessagesDynamoDb extends AbstractDynamoDbStore {
|
||||
.filter(Predicate.not(Objects::isNull));
|
||||
}
|
||||
|
||||
public CompletableFuture<Optional<MessageProtos.Envelope>> deleteMessageByDestinationAndGuid(
|
||||
final UUID destinationAccountUuid, final Device destinationDevice, final UUID messageUuid) {
|
||||
final AttributeValue partitionKey = convertPartitionKey(destinationAccountUuid, destinationDevice);
|
||||
final QueryRequest queryRequest = QueryRequest.builder()
|
||||
.tableName(tableName)
|
||||
.indexName(LOCAL_INDEX_MESSAGE_UUID_NAME)
|
||||
.projectionExpression(KEY_SORT)
|
||||
.consistentRead(true)
|
||||
.keyConditionExpression("#part = :part AND #uuid = :uuid")
|
||||
.expressionAttributeNames(Map.of(
|
||||
"#part", KEY_PARTITION,
|
||||
"#uuid", LOCAL_INDEX_MESSAGE_UUID_KEY_SORT))
|
||||
.expressionAttributeValues(Map.of(
|
||||
":part", partitionKey,
|
||||
":uuid", convertLocalIndexMessageUuidSortKey(messageUuid)))
|
||||
.build();
|
||||
|
||||
// because we are filtering on message UUID, this query should return at most one item,
|
||||
// but it’s simpler to handle the full stream and return the “last” item
|
||||
return Flux.from(dbAsyncClient.queryPaginator(queryRequest).items())
|
||||
.flatMap(item -> Mono.fromCompletionStage(dbAsyncClient.deleteItem(DeleteItemRequest.builder()
|
||||
.tableName(tableName)
|
||||
.key(Map.of(KEY_PARTITION, partitionKey, KEY_SORT,
|
||||
AttributeValues.fromByteArray(item.get(KEY_SORT).b().asByteArray())))
|
||||
.returnValues(ReturnValue.ALL_OLD)
|
||||
.build())))
|
||||
.mapNotNull(deleteItemResponse -> {
|
||||
try {
|
||||
if (deleteItemResponse.attributes() != null && deleteItemResponse.attributes().containsKey(KEY_PARTITION)) {
|
||||
return convertItemToEnvelope(deleteItemResponse.attributes(), experimentEnrollmentManager);
|
||||
}
|
||||
} catch (final InvalidProtocolBufferException e) {
|
||||
logger.error("Failed to parse envelope", e);
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.map(Optional::ofNullable)
|
||||
.subscribeOn(messageDeletionScheduler)
|
||||
.last(Optional.empty()) // if the flux is empty, last() will throw without a default
|
||||
.toFuture();
|
||||
}
|
||||
|
||||
public CompletableFuture<Optional<MessageProtos.Envelope>> deleteMessage(final UUID destinationAccountUuid,
|
||||
final Device destinationDevice, final UUID messageUuid, final long serverTimestamp) {
|
||||
DeleteItemRequest.Builder deleteItemRequest = DeleteItemRequest.builder()
|
||||
|
||||
@@ -28,7 +28,6 @@ import org.reactivestreams.Publisher;
|
||||
import org.signal.libsignal.protocol.SealedSenderMultiRecipientMessage;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.entities.MessageProtos;
|
||||
import org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
|
||||
import org.whispersystems.textsecuregcm.identity.IdentityType;
|
||||
import org.whispersystems.textsecuregcm.identity.ServiceIdentifier;
|
||||
@@ -253,26 +252,17 @@ public class MessagesManager {
|
||||
return messagesCache.clear(destinationUuid, deviceId);
|
||||
}
|
||||
|
||||
public CompletableFuture<Optional<RemovedMessage>> delete(UUID destinationUuid, Device destinationDevice, UUID guid,
|
||||
@Nullable Long serverTimestamp) {
|
||||
public CompletableFuture<Optional<RemovedMessage>> delete(final UUID destinationUuid,
|
||||
final Device destinationDevice,
|
||||
final UUID guid,
|
||||
final long serverTimestamp) {
|
||||
|
||||
return messagesCache.remove(destinationUuid, destinationDevice.getId(), guid)
|
||||
.thenComposeAsync(removed -> {
|
||||
|
||||
if (removed.isPresent()) {
|
||||
return CompletableFuture.completedFuture(removed);
|
||||
}
|
||||
|
||||
final CompletableFuture<Optional<MessageProtos.Envelope>> maybeDeletedEnvelope;
|
||||
if (serverTimestamp == null) {
|
||||
maybeDeletedEnvelope = messagesDynamoDb.deleteMessageByDestinationAndGuid(destinationUuid,
|
||||
destinationDevice, guid);
|
||||
} else {
|
||||
maybeDeletedEnvelope = messagesDynamoDb.deleteMessage(destinationUuid, destinationDevice, guid,
|
||||
serverTimestamp);
|
||||
}
|
||||
|
||||
return maybeDeletedEnvelope.thenApply(maybeEnvelope -> maybeEnvelope.map(RemovedMessage::fromEnvelope));
|
||||
}, messageDeletionExecutor);
|
||||
.thenComposeAsync(removed -> removed
|
||||
.map(_ -> CompletableFuture.completedFuture(removed))
|
||||
.orElseGet(() -> messagesDynamoDb.deleteMessage(destinationUuid, destinationDevice, guid, serverTimestamp)
|
||||
.thenApply(maybeEnvelope -> maybeEnvelope.map(RemovedMessage::fromEnvelope))
|
||||
), messageDeletionExecutor);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -58,7 +58,7 @@ public abstract class AbstractCommandWithDependencies extends EnvironmentCommand
|
||||
|
||||
} finally {
|
||||
logger.info("Stopping command dependencies");
|
||||
environment.lifecycle().getManagedObjects().forEach(managedObject -> {
|
||||
environment.lifecycle().getManagedObjects().reversed().forEach(managedObject -> {
|
||||
try {
|
||||
managedObject.stop();
|
||||
} catch (final Exception e) {
|
||||
|
||||
@@ -7,10 +7,12 @@ package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.reset;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import io.dropwizard.auth.AuthValueFactoryProvider;
|
||||
import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
|
||||
@@ -22,6 +24,9 @@ import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.signal.chat.calling.quality.SubmitCallQualitySurveyRequest;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedDevice;
|
||||
import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper;
|
||||
@@ -29,6 +34,7 @@ import org.whispersystems.textsecuregcm.metrics.CallQualitySurveyManager;
|
||||
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
||||
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||
import org.whispersystems.textsecuregcm.util.TestRemoteAddressFilterProvider;
|
||||
import java.util.List;
|
||||
|
||||
@ExtendWith(DropwizardExtensionsSupport.class)
|
||||
class CallQualitySurveyControllerTest {
|
||||
@@ -83,4 +89,21 @@ class CallQualitySurveyControllerTest {
|
||||
verify(CALL_QUALITY_SURVEY_MANAGER, never()).submitCallQualitySurvey(any(), any(), any());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void submitCallQualitySurveyInvalidArgument() {
|
||||
final SubmitCallQualitySurveyRequest request = SubmitCallQualitySurveyRequest.getDefaultInstance();
|
||||
|
||||
doThrow(new IllegalArgumentException())
|
||||
.when(CALL_QUALITY_SURVEY_MANAGER).submitCallQualitySurvey(request, REMOTE_ADDRESS, USER_AGENT);
|
||||
|
||||
try (final Response response = RESOURCE_EXTENSION.getJerseyTest()
|
||||
.target("/v1/call_quality_survey")
|
||||
.request()
|
||||
.header("User-Agent", USER_AGENT)
|
||||
.put(Entity.entity(request.toByteArray(), MediaType.APPLICATION_OCTET_STREAM_TYPE))) {
|
||||
|
||||
assertEquals(422, response.getStatus());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +100,6 @@ import org.whispersystems.textsecuregcm.push.MessageSender;
|
||||
import org.whispersystems.textsecuregcm.push.MessageTooLargeException;
|
||||
import org.whispersystems.textsecuregcm.push.PushNotificationManager;
|
||||
import org.whispersystems.textsecuregcm.push.PushNotificationScheduler;
|
||||
import org.whispersystems.textsecuregcm.push.ReceiptSender;
|
||||
import org.whispersystems.textsecuregcm.spam.SpamChecker;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
@@ -108,13 +107,11 @@ import org.whispersystems.textsecuregcm.storage.ClientReleaseManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.storage.MessagesManager;
|
||||
import org.whispersystems.textsecuregcm.storage.PhoneNumberIdentifiers;
|
||||
import org.whispersystems.textsecuregcm.storage.RemovedMessage;
|
||||
import org.whispersystems.textsecuregcm.storage.ReportMessageManager;
|
||||
import org.whispersystems.textsecuregcm.tests.util.AccountsHelper;
|
||||
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
||||
import org.whispersystems.textsecuregcm.tests.util.MultiRecipientMessageHelper;
|
||||
import org.whispersystems.textsecuregcm.tests.util.TestRecipient;
|
||||
import org.whispersystems.textsecuregcm.util.CompletableFutureTestUtil;
|
||||
import org.whispersystems.textsecuregcm.util.HeaderUtils;
|
||||
import org.whispersystems.textsecuregcm.util.Pair;
|
||||
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||
@@ -165,7 +162,6 @@ class MessageControllerTest {
|
||||
private static final RedisAdvancedClusterCommands<String, String> redisCommands = mock(RedisAdvancedClusterCommands.class);
|
||||
|
||||
private static final MessageSender messageSender = mock(MessageSender.class);
|
||||
private static final ReceiptSender receiptSender = mock(ReceiptSender.class);
|
||||
private static final AccountsManager accountsManager = mock(AccountsManager.class);
|
||||
private static final MessagesManager messagesManager = mock(MessagesManager.class);
|
||||
private static final RateLimiters rateLimiters = mock(RateLimiters.class);
|
||||
@@ -192,7 +188,7 @@ class MessageControllerTest {
|
||||
.addProvider(MultiRecipientMessageProvider.class)
|
||||
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
||||
.addResource(
|
||||
new MessageController(rateLimiters, cardinalityEstimator, messageSender, receiptSender, accountsManager,
|
||||
new MessageController(rateLimiters, cardinalityEstimator, messageSender, accountsManager,
|
||||
messagesManager, phoneNumberIdentifiers, pushNotificationManager, pushNotificationScheduler,
|
||||
reportMessageManager, messageDeliveryScheduler, mock(ClientReleaseManager.class),
|
||||
serverSecretParams, SpamChecker.noop(), new MessageMetrics(), mock(MessageDeliveryLoopMonitor.class),
|
||||
@@ -269,7 +265,6 @@ class MessageControllerTest {
|
||||
reset(
|
||||
redisCommands,
|
||||
messageSender,
|
||||
receiptSender,
|
||||
accountsManager,
|
||||
messagesManager,
|
||||
rateLimiters,
|
||||
@@ -798,80 +793,6 @@ class MessageControllerTest {
|
||||
assertThat("Unauthorized response", response.getStatus(), is(equalTo(401)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteMessages() {
|
||||
long clientTimestamp = System.currentTimeMillis();
|
||||
|
||||
UUID sourceUuid = UUID.randomUUID();
|
||||
|
||||
UUID uuid1 = UUID.randomUUID();
|
||||
|
||||
final long serverTimestamp = 0;
|
||||
when(messagesManager.delete(AuthHelper.VALID_UUID, AuthHelper.VALID_DEVICE, uuid1, null))
|
||||
.thenReturn(
|
||||
CompletableFutureTestUtil.almostCompletedFuture(Optional.of(
|
||||
new RemovedMessage(Optional.of(new AciServiceIdentifier(sourceUuid)),
|
||||
new AciServiceIdentifier(AuthHelper.VALID_UUID), uuid1, serverTimestamp, clientTimestamp,
|
||||
Envelope.Type.CIPHERTEXT))));
|
||||
|
||||
UUID uuid2 = UUID.randomUUID();
|
||||
when(messagesManager.delete(AuthHelper.VALID_UUID, AuthHelper.VALID_DEVICE, uuid2, null))
|
||||
.thenReturn(
|
||||
CompletableFutureTestUtil.almostCompletedFuture(Optional.of(
|
||||
new RemovedMessage(Optional.of(new AciServiceIdentifier(sourceUuid)),
|
||||
new AciServiceIdentifier(AuthHelper.VALID_UUID), uuid2, serverTimestamp, clientTimestamp,
|
||||
Envelope.Type.SERVER_DELIVERY_RECEIPT))));
|
||||
|
||||
UUID uuid3 = UUID.randomUUID();
|
||||
when(messagesManager.delete(AuthHelper.VALID_UUID, AuthHelper.VALID_DEVICE, uuid3, null))
|
||||
.thenReturn(CompletableFutureTestUtil.almostCompletedFuture(Optional.empty()));
|
||||
|
||||
UUID uuid4 = UUID.randomUUID();
|
||||
when(messagesManager.delete(AuthHelper.VALID_UUID, AuthHelper.VALID_DEVICE, uuid4, null))
|
||||
.thenReturn(CompletableFuture.failedFuture(new RuntimeException("Oh No")));
|
||||
|
||||
try (final Response response = resources.getJerseyTest()
|
||||
.target(String.format("/v1/messages/uuid/%s", uuid1))
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.delete()) {
|
||||
|
||||
assertThat("Good Response Code", response.getStatus(), is(equalTo(204)));
|
||||
verify(receiptSender).sendReceipt(eq(new AciServiceIdentifier(AuthHelper.VALID_UUID)), eq((byte) 1),
|
||||
eq(new AciServiceIdentifier(sourceUuid)), eq(clientTimestamp));
|
||||
}
|
||||
|
||||
try (final Response response = resources.getJerseyTest()
|
||||
.target(String.format("/v1/messages/uuid/%s", uuid2))
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.delete()) {
|
||||
|
||||
assertThat("Good Response Code", response.getStatus(), is(equalTo(204)));
|
||||
verifyNoMoreInteractions(receiptSender);
|
||||
}
|
||||
|
||||
try (final Response response = resources.getJerseyTest()
|
||||
.target(String.format("/v1/messages/uuid/%s", uuid3))
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.delete()) {
|
||||
|
||||
assertThat("Good Response Code", response.getStatus(), is(equalTo(204)));
|
||||
verifyNoMoreInteractions(receiptSender);
|
||||
}
|
||||
|
||||
try (final Response response = resources.getJerseyTest()
|
||||
.target(String.format("/v1/messages/uuid/%s", uuid4))
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.delete()) {
|
||||
|
||||
assertThat("Bad Response Code", response.getStatus(), is(equalTo(500)));
|
||||
verifyNoMoreInteractions(receiptSender);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testReportMessageByE164() {
|
||||
final String senderNumber = "+12125550001";
|
||||
|
||||
@@ -1,238 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.reset;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import io.dropwizard.auth.AuthValueFactoryProvider;
|
||||
import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
|
||||
import io.dropwizard.testing.junit5.ResourceExtension;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.assertj.core.api.InstanceOfAssertFactory;
|
||||
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedDevice;
|
||||
import org.whispersystems.textsecuregcm.entities.UserRemoteConfig;
|
||||
import org.whispersystems.textsecuregcm.entities.UserRemoteConfigList;
|
||||
import org.whispersystems.textsecuregcm.mappers.DeviceLimitExceededExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.storage.RemoteConfig;
|
||||
import org.whispersystems.textsecuregcm.storage.RemoteConfigsManager;
|
||||
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
||||
import org.whispersystems.textsecuregcm.util.TestClock;
|
||||
|
||||
@ExtendWith(DropwizardExtensionsSupport.class)
|
||||
class RemoteConfigControllerV1Test {
|
||||
|
||||
private static final RemoteConfigsManager remoteConfigsManager = mock(RemoteConfigsManager.class);
|
||||
|
||||
private static final long PINNED_EPOCH_SECONDS = 1701287216L;
|
||||
private static final TestClock TEST_CLOCK = TestClock.pinned(Instant.ofEpochSecond(PINNED_EPOCH_SECONDS));
|
||||
|
||||
|
||||
private static final ResourceExtension resources = ResourceExtension.builder()
|
||||
.addProvider(AuthHelper.getAuthFilter())
|
||||
.addProvider(new AuthValueFactoryProvider.Binder<>(AuthenticatedDevice.class))
|
||||
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
||||
.addProvider(new DeviceLimitExceededExceptionMapper())
|
||||
.addResource(new RemoteConfigControllerV1(remoteConfigsManager, Map.of("maxGroupSize", "42"), TEST_CLOCK))
|
||||
.build();
|
||||
|
||||
|
||||
@BeforeEach
|
||||
void setup() throws Exception {
|
||||
when(remoteConfigsManager.getAll()).thenReturn(new LinkedList<>() {{
|
||||
add(new RemoteConfig("android.stickers", 25, Set.of(AuthHelper.VALID_UUID_3, AuthHelper.INVALID_UUID), null,
|
||||
null, null));
|
||||
add(new RemoteConfig("ios.stickers", 50, Set.of(), null, null, null));
|
||||
add(new RemoteConfig("always.true", 100, Set.of(), null, null, null));
|
||||
add(new RemoteConfig("only.special", 0, Set.of(AuthHelper.VALID_UUID), null, null, null));
|
||||
add(new RemoteConfig("value.always.true", 100, Set.of(), "foo", "bar", null));
|
||||
add(new RemoteConfig("value.only.special", 0, Set.of(AuthHelper.VALID_UUID), "abc", "xyz", null));
|
||||
add(new RemoteConfig("value.always.false", 0, Set.of(), "red", "green", null));
|
||||
add(new RemoteConfig("linked.config.0", 50, Set.of(), null, null, null));
|
||||
add(new RemoteConfig("linked.config.1", 50, Set.of(), null, null, "linked.config.0"));
|
||||
add(new RemoteConfig("unlinked.config", 50, Set.of(), null, null, null));
|
||||
}});
|
||||
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void teardown() {
|
||||
reset(remoteConfigsManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRetrieveConfig() {
|
||||
UserRemoteConfigList configuration = resources.getJerseyTest()
|
||||
.target("/v1/config/")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.get(UserRemoteConfigList.class);
|
||||
|
||||
verify(remoteConfigsManager, times(1)).getAll();
|
||||
|
||||
assertThat(configuration.getConfig()).hasSize(11);
|
||||
assertThat(configuration.getConfig().get(0).getName()).isEqualTo("android.stickers");
|
||||
assertThat(configuration.getConfig().get(1).getName()).isEqualTo("ios.stickers");
|
||||
assertThat(configuration.getConfig().get(2).getName()).isEqualTo("always.true");
|
||||
assertThat(configuration.getConfig().get(2).isEnabled()).isEqualTo(true);
|
||||
assertThat(configuration.getConfig().get(2).getValue()).isNull();
|
||||
assertThat(configuration.getConfig().get(3).getName()).isEqualTo("only.special");
|
||||
assertThat(configuration.getConfig().get(3).isEnabled()).isEqualTo(true);
|
||||
assertThat(configuration.getConfig().get(2).getValue()).isNull();
|
||||
assertThat(configuration.getConfig().get(4).getName()).isEqualTo("value.always.true");
|
||||
assertThat(configuration.getConfig().get(4).isEnabled()).isEqualTo(true);
|
||||
assertThat(configuration.getConfig().get(4).getValue()).isEqualTo("bar");
|
||||
assertThat(configuration.getConfig().get(5).getName()).isEqualTo("value.only.special");
|
||||
assertThat(configuration.getConfig().get(5).isEnabled()).isEqualTo(true);
|
||||
assertThat(configuration.getConfig().get(5).getValue()).isEqualTo("xyz");
|
||||
assertThat(configuration.getConfig().get(6).getName()).isEqualTo("value.always.false");
|
||||
assertThat(configuration.getConfig().get(6).isEnabled()).isEqualTo(false);
|
||||
assertThat(configuration.getConfig().get(6).getValue()).isEqualTo("red");
|
||||
assertThat(configuration.getConfig().get(7).getName()).isEqualTo("linked.config.0");
|
||||
assertThat(configuration.getConfig().get(8).getName()).isEqualTo("linked.config.1");
|
||||
assertThat(configuration.getConfig().get(9).getName()).isEqualTo("unlinked.config");
|
||||
assertThat(configuration.getConfig().get(10).getName()).isEqualTo("global.maxGroupSize");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testServerEpochTime() {
|
||||
Object serverEpochTime = resources.getJerseyTest()
|
||||
.target("/v1/config/")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.get(Map.class)
|
||||
.get("serverEpochTime");
|
||||
|
||||
assertThat(serverEpochTime).asInstanceOf(new InstanceOfAssertFactory<>(Number.class, Assertions::assertThat))
|
||||
.extracting(Number::longValue)
|
||||
.isEqualTo(PINNED_EPOCH_SECONDS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRetrieveConfigNotSpecial() {
|
||||
UserRemoteConfigList configuration = resources.getJerseyTest()
|
||||
.target("/v1/config/")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_PASSWORD_TWO))
|
||||
.get(UserRemoteConfigList.class);
|
||||
|
||||
verify(remoteConfigsManager, times(1)).getAll();
|
||||
|
||||
assertThat(configuration.getConfig()).hasSize(11);
|
||||
assertThat(configuration.getConfig().get(0).getName()).isEqualTo("android.stickers");
|
||||
assertThat(configuration.getConfig().get(1).getName()).isEqualTo("ios.stickers");
|
||||
assertThat(configuration.getConfig().get(2).getName()).isEqualTo("always.true");
|
||||
assertThat(configuration.getConfig().get(2).isEnabled()).isEqualTo(true);
|
||||
assertThat(configuration.getConfig().get(2).getValue()).isNull();
|
||||
assertThat(configuration.getConfig().get(3).getName()).isEqualTo("only.special");
|
||||
assertThat(configuration.getConfig().get(3).isEnabled()).isEqualTo(false);
|
||||
assertThat(configuration.getConfig().get(2).getValue()).isNull();
|
||||
assertThat(configuration.getConfig().get(4).getName()).isEqualTo("value.always.true");
|
||||
assertThat(configuration.getConfig().get(4).isEnabled()).isEqualTo(true);
|
||||
assertThat(configuration.getConfig().get(4).getValue()).isEqualTo("bar");
|
||||
assertThat(configuration.getConfig().get(5).getName()).isEqualTo("value.only.special");
|
||||
assertThat(configuration.getConfig().get(5).isEnabled()).isEqualTo(false);
|
||||
assertThat(configuration.getConfig().get(5).getValue()).isEqualTo("abc");
|
||||
assertThat(configuration.getConfig().get(6).getName()).isEqualTo("value.always.false");
|
||||
assertThat(configuration.getConfig().get(6).isEnabled()).isEqualTo(false);
|
||||
assertThat(configuration.getConfig().get(6).getValue()).isEqualTo("red");
|
||||
assertThat(configuration.getConfig().get(7).getName()).isEqualTo("linked.config.0");
|
||||
assertThat(configuration.getConfig().get(8).getName()).isEqualTo("linked.config.1");
|
||||
assertThat(configuration.getConfig().get(9).getName()).isEqualTo("unlinked.config");
|
||||
assertThat(configuration.getConfig().get(10).getName()).isEqualTo("global.maxGroupSize");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testHashKeyLinkedConfigs() {
|
||||
boolean allUnlinkedConfigsMatched = true;
|
||||
for (AuthHelper.TestAccount testAccount : AuthHelper.TEST_ACCOUNTS) {
|
||||
UserRemoteConfigList configuration = resources.getJerseyTest()
|
||||
.target("/v1/config/")
|
||||
.request()
|
||||
.header("Authorization", testAccount.getAuthHeader())
|
||||
.get(UserRemoteConfigList.class);
|
||||
|
||||
assertThat(configuration.getConfig()).hasSize(11);
|
||||
|
||||
final UserRemoteConfig linkedConfig0 = configuration.getConfig().get(7);
|
||||
assertThat(linkedConfig0.getName()).isEqualTo("linked.config.0");
|
||||
|
||||
final UserRemoteConfig linkedConfig1 = configuration.getConfig().get(8);
|
||||
assertThat(linkedConfig1.getName()).isEqualTo("linked.config.1");
|
||||
|
||||
final UserRemoteConfig unlinkedConfig = configuration.getConfig().get(9);
|
||||
assertThat(unlinkedConfig.getName()).isEqualTo("unlinked.config");
|
||||
|
||||
assertThat(linkedConfig0.isEnabled() == linkedConfig1.isEnabled()).isTrue();
|
||||
allUnlinkedConfigsMatched &= (linkedConfig0.isEnabled() == unlinkedConfig.isEnabled());
|
||||
}
|
||||
assertThat(allUnlinkedConfigsMatched).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRetrieveConfigUnauthorized() {
|
||||
Response response = resources.getJerseyTest()
|
||||
.target("/v1/config/")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.INVALID_PASSWORD))
|
||||
.get();
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(401);
|
||||
|
||||
verifyNoMoreInteractions(remoteConfigsManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMath() throws NoSuchAlgorithmException {
|
||||
List<RemoteConfig> remoteConfigList = remoteConfigsManager.getAll();
|
||||
Map<String, Integer> enabledMap = new HashMap<>();
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA256");
|
||||
int iterations = 100000;
|
||||
Random random = new Random(9424242L); // the seed value doesn't matter so much as it's constant to make the test not flaky
|
||||
|
||||
for (int i=0;i<iterations;i++) {
|
||||
for (RemoteConfig config : remoteConfigList) {
|
||||
int count = enabledMap.getOrDefault(config.getName(), 0);
|
||||
|
||||
if (RemoteConfigControllerV1.isInBucket(digest, AuthHelper.getRandomUUID(random), config.getName().getBytes(), config.getPercentage(), new HashSet<>())) {
|
||||
count++;
|
||||
}
|
||||
|
||||
enabledMap.put(config.getName(), count);
|
||||
}
|
||||
}
|
||||
|
||||
for (RemoteConfig config : remoteConfigList) {
|
||||
double targetNumber = iterations * (config.getPercentage() / 100.0);
|
||||
double variance = targetNumber * 0.01;
|
||||
|
||||
assertThat(enabledMap.get(config.getName())).isBetween((int) (targetNumber - variance),
|
||||
(int) (targetNumber + variance));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.common.net.InetAddresses;
|
||||
import java.time.Duration;
|
||||
import io.grpc.Status;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mock;
|
||||
@@ -67,4 +68,16 @@ class CallQualitySurveyGrpcServiceTest extends SimpleBaseGrpcTest<CallQualitySur
|
||||
GrpcTestUtils.assertRateLimitExceeded(retryAfter,
|
||||
() -> unauthenticatedServiceStub().submitCallQualitySurvey(SubmitCallQualitySurveyRequest.getDefaultInstance()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void submitCallQualitySurveyInvalidArgument() {
|
||||
final SubmitCallQualitySurveyRequest request = SubmitCallQualitySurveyRequest.getDefaultInstance();
|
||||
|
||||
doThrow(new IllegalArgumentException())
|
||||
.when(callQualitySurveyManager).submitCallQualitySurvey(request, REMOTE_ADDRESS, USER_AGENT);
|
||||
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
GrpcTestUtils.assertStatusException(Status.INVALID_ARGUMENT,
|
||||
() -> unauthenticatedServiceStub().submitCallQualitySurvey(request));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
@@ -27,6 +28,11 @@ import java.util.UUID;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.function.Executable;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.junit.platform.commons.util.StringUtils;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.signal.calling.survey.CallQualitySurveyResponsePubSubMessage;
|
||||
import org.signal.chat.calling.quality.SubmitCallQualitySurveyRequest;
|
||||
@@ -140,4 +146,36 @@ class CallQualitySurveyManagerTest {
|
||||
assertEquals(videoSendPacketLossFraction, callQualitySurveyResponsePubSubMessage.getVideoSendPacketLossFraction());
|
||||
assertArrayEquals(telemetryBytes, callQualitySurveyResponsePubSubMessage.getCallTelemetry().toByteArray());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource
|
||||
void validateRequest(final SubmitCallQualitySurveyRequest request, final boolean expectValid) {
|
||||
final Executable validateRequest = () -> CallQualitySurveyManager.validateRequest(request);
|
||||
|
||||
if (expectValid) {
|
||||
assertDoesNotThrow(validateRequest);
|
||||
} else {
|
||||
final IllegalArgumentException illegalArgumentException =
|
||||
assertThrows(IllegalArgumentException.class, validateRequest);
|
||||
|
||||
assertTrue(StringUtils.isNotBlank(illegalArgumentException.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
private static List<Arguments> validateRequest() {
|
||||
final SubmitCallQualitySurveyRequest validRequest = SubmitCallQualitySurveyRequest.newBuilder()
|
||||
.setStartTimestamp(Instant.now().toEpochMilli())
|
||||
.setEndTimestamp(Instant.now().plusSeconds(60).toEpochMilli())
|
||||
.setCallType("test")
|
||||
.setCallEndReason("test")
|
||||
.build();
|
||||
|
||||
return List.of(
|
||||
Arguments.argumentSet("Valid survey response", validRequest, true),
|
||||
Arguments.argumentSet("No start timestamp", validRequest.toBuilder().clearStartTimestamp().build(), false),
|
||||
Arguments.argumentSet("No end timestamp", validRequest.toBuilder().clearEndTimestamp().build(), false),
|
||||
Arguments.argumentSet("No call type", validRequest.toBuilder().clearCallType().build(), false),
|
||||
Arguments.argumentSet("No call end reason", validRequest.toBuilder().clearCallEndReason().build(), false)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ package org.whispersystems.textsecuregcm.storage;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupsDb;
|
||||
import org.whispersystems.textsecuregcm.scheduler.JobScheduler;
|
||||
import org.whispersystems.textsecuregcm.experiment.PushNotificationExperimentSamples;
|
||||
import org.whispersystems.textsecuregcm.scheduler.JobScheduler;
|
||||
import org.whispersystems.textsecuregcm.storage.devicecheck.AppleDeviceChecks;
|
||||
import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition;
|
||||
import software.amazon.awssdk.services.dynamodb.model.GlobalSecondaryIndex;
|
||||
@@ -246,20 +246,8 @@ public final class DynamoDbExtensionSchema {
|
||||
MessagesDynamoDb.KEY_SORT,
|
||||
List.of(
|
||||
AttributeDefinition.builder().attributeName(MessagesDynamoDb.KEY_PARTITION).attributeType(ScalarAttributeType.B).build(),
|
||||
AttributeDefinition.builder().attributeName(MessagesDynamoDb.KEY_SORT).attributeType(ScalarAttributeType.B).build(),
|
||||
AttributeDefinition.builder().attributeName(MessagesDynamoDb.LOCAL_INDEX_MESSAGE_UUID_KEY_SORT)
|
||||
.attributeType(ScalarAttributeType.B).build()),
|
||||
List.of(),
|
||||
List.of(LocalSecondaryIndex.builder()
|
||||
.indexName(MessagesDynamoDb.LOCAL_INDEX_MESSAGE_UUID_NAME)
|
||||
.keySchema(
|
||||
KeySchemaElement.builder().attributeName(MessagesDynamoDb.KEY_PARTITION).keyType(KeyType.HASH).build(),
|
||||
KeySchemaElement.builder()
|
||||
.attributeName(MessagesDynamoDb.LOCAL_INDEX_MESSAGE_UUID_KEY_SORT)
|
||||
.keyType(KeyType.RANGE)
|
||||
.build())
|
||||
.projection(Projection.builder().projectionType(ProjectionType.KEYS_ONLY).build())
|
||||
.build())),
|
||||
AttributeDefinition.builder().attributeName(MessagesDynamoDb.KEY_SORT).attributeType(ScalarAttributeType.B).build()),
|
||||
List.of(), List.of()),
|
||||
|
||||
ONETIME_DONATIONS("onetime_donations_test",
|
||||
OneTimeDonationsManager.KEY_PAYMENT_ID,
|
||||
|
||||
@@ -11,9 +11,9 @@ import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyByte;
|
||||
import static org.mockito.ArgumentMatchers.anyList;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isNotNull;
|
||||
import static org.mockito.Mockito.atLeastOnce;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
@@ -346,15 +346,16 @@ class MessagePersisterTest {
|
||||
.thenReturn(Flux.concat(
|
||||
Flux.fromIterable(persistedMessages),
|
||||
Flux.fromIterable(cachedMessages)));
|
||||
when(messagesManager.delete(any(), any(), any(), any()))
|
||||
when(messagesManager.delete(any(), any(), any(), anyLong()))
|
||||
.thenReturn(CompletableFuture.completedFuture(Optional.empty()));
|
||||
|
||||
assertTimeoutPreemptively(Duration.ofSeconds(10), () ->
|
||||
messagePersister.persistNextQueues(Clock.systemUTC().instant()));
|
||||
|
||||
verify(messagesManager, times(expectedClearedGuids.size()))
|
||||
.delete(eq(DESTINATION_ACCOUNT_UUID), eq(primary), argThat(expectedClearedGuids::contains), isNotNull());
|
||||
verify(messagesManager, never()).delete(any(), any(), argThat(guid -> !expectedClearedGuids.contains(guid)), any());
|
||||
.delete(eq(DESTINATION_ACCOUNT_UUID), eq(primary), argThat(expectedClearedGuids::contains), anyLong());
|
||||
verify(messagesManager, never())
|
||||
.delete(any(), any(), argThat(guid -> !expectedClearedGuids.contains(guid)), anyLong());
|
||||
|
||||
final List<String> queuesToPersist = messagesCache.getQueuesToPersist(SlotHash.getSlot(queueName),
|
||||
Clock.systemUTC().instant(), 1);
|
||||
|
||||
@@ -12,7 +12,6 @@ import com.google.protobuf.ByteString;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
@@ -188,47 +187,6 @@ class MessagesDynamoDbTest {
|
||||
.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteMessageByDestinationAndGuid() throws Exception {
|
||||
final UUID destinationUuid = UUID.randomUUID();
|
||||
final UUID secondDestinationUuid = UUID.randomUUID();
|
||||
final Device primary = DevicesHelper.createDevice((byte) 1);
|
||||
final Device device2 = DevicesHelper.createDevice((byte) 2);
|
||||
|
||||
messagesDynamoDb.store(List.of(MESSAGE1), destinationUuid, primary);
|
||||
messagesDynamoDb.store(List.of(MESSAGE2), secondDestinationUuid, primary);
|
||||
messagesDynamoDb.store(List.of(MESSAGE3), destinationUuid, device2);
|
||||
|
||||
assertThat(load(destinationUuid, primary, MessagesDynamoDb.RESULT_SET_CHUNK_SIZE)).isNotNull().hasSize(1)
|
||||
.element(0).isEqualTo(MESSAGE1);
|
||||
assertThat(load(destinationUuid, device2, MessagesDynamoDb.RESULT_SET_CHUNK_SIZE)).isNotNull()
|
||||
.hasSize(1)
|
||||
.element(0).isEqualTo(MESSAGE3);
|
||||
assertThat(load(secondDestinationUuid, primary, MessagesDynamoDb.RESULT_SET_CHUNK_SIZE)).isNotNull()
|
||||
.hasSize(1).element(0).isEqualTo(MESSAGE2);
|
||||
|
||||
final Optional<MessageProtos.Envelope> deletedMessage = messagesDynamoDb.deleteMessageByDestinationAndGuid(
|
||||
secondDestinationUuid, primary,
|
||||
UUID.fromString(MESSAGE2.getServerGuid())).get(5, TimeUnit.SECONDS);
|
||||
|
||||
assertThat(deletedMessage).isPresent();
|
||||
|
||||
assertThat(load(destinationUuid, primary, MessagesDynamoDb.RESULT_SET_CHUNK_SIZE)).isNotNull().hasSize(1)
|
||||
.element(0).isEqualTo(MESSAGE1);
|
||||
assertThat(load(destinationUuid, device2, MessagesDynamoDb.RESULT_SET_CHUNK_SIZE)).isNotNull()
|
||||
.hasSize(1)
|
||||
.element(0).isEqualTo(MESSAGE3);
|
||||
assertThat(load(secondDestinationUuid, primary, MessagesDynamoDb.RESULT_SET_CHUNK_SIZE)).isNotNull()
|
||||
.isEmpty();
|
||||
|
||||
final Optional<MessageProtos.Envelope> alreadyDeletedMessage = messagesDynamoDb.deleteMessageByDestinationAndGuid(
|
||||
secondDestinationUuid, primary,
|
||||
UUID.fromString(MESSAGE2.getServerGuid())).get(5, TimeUnit.SECONDS);
|
||||
|
||||
assertThat(alreadyDeletedMessage).isNotPresent();
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteSingleMessage() throws Exception {
|
||||
final UUID destinationUuid = UUID.randomUUID();
|
||||
@@ -274,19 +232,14 @@ class MessagesDynamoDbTest {
|
||||
final Device primary = DevicesHelper.createDevice((byte) 1);
|
||||
primary.setCreated(System.currentTimeMillis());
|
||||
|
||||
messagesDynamoDb.store(List.of(MESSAGE1, MESSAGE2, MESSAGE3), destinationUuid, primary);
|
||||
messagesDynamoDb.store(List.of(MESSAGE1, MESSAGE2), destinationUuid, primary);
|
||||
assertThat(load(destinationUuid, primary, MessagesDynamoDb.RESULT_SET_CHUNK_SIZE))
|
||||
.as("load should return all messages stored").containsOnly(MESSAGE1, MESSAGE2, MESSAGE3);
|
||||
.as("load should return all messages stored").containsOnly(MESSAGE1, MESSAGE2);
|
||||
|
||||
messagesDynamoDb.deleteMessageByDestinationAndGuid(destinationUuid, primary, UUID.fromString(MESSAGE1.getServerGuid()))
|
||||
messagesDynamoDb.deleteMessage(destinationUuid, primary, UUID.fromString(MESSAGE1.getServerGuid()), MESSAGE1.getServerTimestamp())
|
||||
.get(1, TimeUnit.SECONDS);
|
||||
assertThat(load(destinationUuid, primary, MessagesDynamoDb.RESULT_SET_CHUNK_SIZE))
|
||||
.as("deleting message by guid should work").containsExactly(MESSAGE3, MESSAGE2);
|
||||
|
||||
messagesDynamoDb.deleteMessage(destinationUuid, primary, UUID.fromString(MESSAGE2.getServerGuid()), MESSAGE2.getServerTimestamp())
|
||||
.get(1, TimeUnit.SECONDS);
|
||||
assertThat(load(destinationUuid, primary, MessagesDynamoDb.RESULT_SET_CHUNK_SIZE))
|
||||
.as("deleting message by guid and timestamp should work").containsExactly(MESSAGE3);
|
||||
.as("deleting message by guid and timestamp should work").containsExactly(MESSAGE2);
|
||||
|
||||
primary.setCreated(primary.getCreated() + 1000);
|
||||
assertThat(load(destinationUuid, primary, MessagesDynamoDb.RESULT_SET_CHUNK_SIZE))
|
||||
|
||||
Submodule spam-filter updated: e5ff57120e...d3e132292f
Reference in New Issue
Block a user