diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java index 688d85e57..f82ed9c90 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java @@ -143,6 +143,7 @@ import org.whispersystems.textsecuregcm.filters.TimestampResponseFilter; import org.whispersystems.textsecuregcm.grpc.AccountsAnonymousGrpcService; import org.whispersystems.textsecuregcm.grpc.AccountsGrpcService; import org.whispersystems.textsecuregcm.grpc.CallQualitySurveyGrpcService; +import org.whispersystems.textsecuregcm.grpc.ErrorConformanceInterceptor; import org.whispersystems.textsecuregcm.grpc.GrpcAllowListInterceptor; import org.whispersystems.textsecuregcm.grpc.ErrorMappingInterceptor; import org.whispersystems.textsecuregcm.grpc.ExternalServiceCredentialsAnonymousGrpcService; @@ -877,6 +878,7 @@ public class WhisperServerService extends Application authenticatedServices = Stream.of( new AccountsGrpcService(accountsManager, rateLimiters, usernameHashZkProofVerifier, registrationRecoveryPasswordsManager), ExternalServiceCredentialsGrpcService.createForAllExternalServices(config, rateLimiters), - new KeysGrpcService(accountsManager, keysManager, rateLimiters), - new ProfileGrpcService(clock, accountsManager, profilesManager, dynamicConfigurationManager, - config.getBadges(), profileCdnPolicyGenerator, profileCdnPolicySigner, profileBadgeConverter, rateLimiters, zkProfileOperations)) + new KeysGrpcService(accountsManager, keysManager, rateLimiters)) .map(bindableService -> ServerInterceptors.intercept(bindableService, // Note: interceptors run in the reverse order they are added; the remote deprecation filter // depends on the user-agent context so it has to come first here! @@ -902,6 +902,7 @@ public class WhisperServerService extends Application ServerInterceptors.intercept(bindableService, // Note: interceptors run in the reverse order they are added; the remote deprecation filter // depends on the user-agent context so it has to come first here! @@ -922,6 +922,7 @@ public class WhisperServerService extends Application DETAILS_HEADER_KEY = + Metadata.Key.of("grpc-status-details-bin", Metadata.BINARY_BYTE_MARSHALLER); + + @Override + public ServerCall.Listener interceptCall( + final ServerCall call, + final Metadata headers, + final ServerCallHandler next) { + return next.startCall(new ForwardingServerCall.SimpleForwardingServerCall<>(call) { + @Override + public void close(final Status status, final Metadata trailers) { + if (status.getCode() == Status.Code.OK) { + super.close(status, trailers); + return; + } + if (!trailers.containsKey(DETAILS_HEADER_KEY)) { + log.error("Intercepted call {} returned status {} but did not include status details", + call.getMethodDescriptor().getFullMethodName(), status); + assert false; + } + + switch (status.getCode()) { + case UNAUTHENTICATED, UNAVAILABLE, INVALID_ARGUMENT, RESOURCE_EXHAUSTED -> { + } + default -> { + log.error("Intercepted call {} returned illegal application status {}: {}", + call.getMethodDescriptor().getFullMethodName(), status, status.getDescription()); + assert false; + } + } + super.close(status, trailers); + } + }, headers); + } +} diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/IdentityTypeUtil.java b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/IdentityTypeUtil.java index a94e7e25f..c01857160 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/IdentityTypeUtil.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/IdentityTypeUtil.java @@ -17,7 +17,7 @@ public class IdentityTypeUtil { return switch (grpcIdentityType) { case IDENTITY_TYPE_ACI -> IdentityType.ACI; case IDENTITY_TYPE_PNI -> IdentityType.PNI; - case IDENTITY_TYPE_UNSPECIFIED, UNRECOGNIZED -> throw Status.INVALID_ARGUMENT.asRuntimeException(); + case IDENTITY_TYPE_UNSPECIFIED, UNRECOGNIZED -> throw GrpcExceptions.invalidArguments("invalid identity type"); }; } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/PaymentsGrpcService.java b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/PaymentsGrpcService.java index 845ca4fb4..08545cc79 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/PaymentsGrpcService.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/PaymentsGrpcService.java @@ -37,7 +37,7 @@ public class PaymentsGrpcService extends ReactorPaymentsGrpc.PaymentsImplBase { final CurrencyConversionEntityList currencyConversionEntityList = currencyManager .getCurrencyConversions() - .orElseThrow(Status.UNAVAILABLE::asRuntimeException); + .orElseThrow(() -> GrpcExceptions.unavailable("currency conversions not available")); final List currencyConversionEntities = currencyConversionEntityList .getCurrencies() diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ValidatingInterceptor.java b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ValidatingInterceptor.java index ca571278c..11ad9b278 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ValidatingInterceptor.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ValidatingInterceptor.java @@ -5,8 +5,6 @@ package org.whispersystems.textsecuregcm.grpc; -import static org.whispersystems.textsecuregcm.grpc.validators.ValidatorUtils.internalError; - import com.google.protobuf.Descriptors; import com.google.protobuf.Message; import io.grpc.ForwardingServerCallListener; @@ -14,12 +12,15 @@ import io.grpc.Metadata; import io.grpc.ServerCall; import io.grpc.ServerCallHandler; import io.grpc.ServerInterceptor; -import io.grpc.StatusException; +import io.grpc.StatusRuntimeException; import java.util.List; import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.whispersystems.textsecuregcm.grpc.validators.E164FieldValidator; import org.whispersystems.textsecuregcm.grpc.validators.EnumSpecifiedFieldValidator; import org.whispersystems.textsecuregcm.grpc.validators.ExactlySizeFieldValidator; +import org.whispersystems.textsecuregcm.grpc.validators.FieldValidationException; import org.whispersystems.textsecuregcm.grpc.validators.FieldValidator; import org.whispersystems.textsecuregcm.grpc.validators.NonEmptyFieldValidator; import org.whispersystems.textsecuregcm.grpc.validators.PresentFieldValidator; @@ -28,6 +29,7 @@ import org.whispersystems.textsecuregcm.grpc.validators.SizeFieldValidator; public class ValidatingInterceptor implements ServerInterceptor { + private static final Logger log = LoggerFactory.getLogger(ValidatingInterceptor.class); private final Map fieldValidators = Map.of( "org.signal.chat.require.nonEmpty", new NonEmptyFieldValidator(), "org.signal.chat.require.present", new PresentFieldValidator(), @@ -60,8 +62,18 @@ public class ValidatingInterceptor implements ServerInterceptor { try { validateMessage(message); super.onMessage(message); - } catch (final StatusException e) { - call.close(e.getStatus(), new Metadata()); + } catch (final StatusRuntimeException e) { + call.close(e.getStatus(), e.getTrailers()); + forwardCalls = false; + } catch (RuntimeException runtimeException) { + final StatusRuntimeException grpcException = switch (runtimeException) { + case StatusRuntimeException e -> e; + default -> { + log.error("Failure applying request validation to message {}", call.getMethodDescriptor().getFullMethodName(), runtimeException); + yield GrpcExceptions.unavailable("failure applying request validation"); + } + }; + call.close(grpcException.getStatus(), grpcException.getTrailers()); forwardCalls = false; } } @@ -75,40 +87,39 @@ public class ValidatingInterceptor implements ServerInterceptor { }; } - private void validateMessage(final Object message) throws StatusException { + private void validateMessage(final Object message) { if (message instanceof Message msg) { - try { - for (final Descriptors.FieldDescriptor fd: msg.getDescriptorForType().getFields()) { - for (final Map.Entry entry: fd.getOptions().getAllFields().entrySet()) { - final Descriptors.FieldDescriptor extensionFieldDescriptor = entry.getKey(); - final String extensionName = extensionFieldDescriptor.getFullName(); + for (final Descriptors.FieldDescriptor fd : msg.getDescriptorForType().getFields()) { + for (final Map.Entry entry : fd.getOptions().getAllFields().entrySet()) { + final Descriptors.FieldDescriptor extensionFieldDescriptor = entry.getKey(); + final String extensionName = extensionFieldDescriptor.getFullName(); - // first validate the field - final FieldValidator validator = fieldValidators.get(extensionName); - // not all extensions are validators, so `validator` value here could legitimately be `null` - if (validator != null) { + // first validate the field + final FieldValidator validator = fieldValidators.get(extensionName); + // not all extensions are validators, so `validator` value here could legitimately be `null` + if (validator != null) { + try { validator.validate(entry.getValue(), fd, msg); + } catch (FieldValidationException e) { + throw GrpcExceptions.fieldViolation(fd.getName(), + "extension %s: %s".formatted(extensionName, e.getMessage())); } } - - // Recursively validate the field's value(s) if it is a message or a repeated field - // gRPC's proto deserialization limits nesting to 100 so this has bounded stack usage - if (fd.isRepeated() && msg.getField(fd) instanceof List list) { - // Checking for repeated fields also handles maps, because maps are syntax sugar for repeated MapEntries - // which themselves are Messages that will be recursively descended. - for (final Object o : list) { - validateMessage(o); - } - } else if (fd.hasPresence() && msg.hasField(fd)) { - // If the field has presence information and is present, recursively validate it. Not all fields have - // presence, but we only validate Message type fields anyway, which always have explicit presence. - validateMessage(msg.getField(fd)); - } } - } catch (final StatusException e) { - throw e; - } catch (final Exception e) { - throw internalError(e); + + // Recursively validate the field's value(s) if it is a message or a repeated field + // gRPC's proto deserialization limits nesting to 100 so this has bounded stack usage + if (fd.isRepeated() && msg.getField(fd) instanceof List list) { + // Checking for repeated fields also handles maps, because maps are syntax sugar for repeated MapEntries + // which themselves are Messages that will be recursively descended. + for (final Object o : list) { + validateMessage(o); + } + } else if (fd.hasPresence() && msg.hasField(fd)) { + // If the field has presence information and is present, recursively validate it. Not all fields have + // presence, but we only validate Message type fields anyway, which always have explicit presence. + validateMessage(msg.getField(fd)); + } } } } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/validators/BaseFieldValidator.java b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/validators/BaseFieldValidator.java index 4c84d3980..f614a6806 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/validators/BaseFieldValidator.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/validators/BaseFieldValidator.java @@ -6,14 +6,10 @@ package org.whispersystems.textsecuregcm.grpc.validators; import static java.util.Objects.requireNonNull; -import static org.whispersystems.textsecuregcm.grpc.validators.ValidatorUtils.internalError; -import static org.whispersystems.textsecuregcm.grpc.validators.ValidatorUtils.invalidArgument; import com.google.protobuf.ByteString; import com.google.protobuf.Descriptors; import com.google.protobuf.Message; -import io.grpc.Status; -import io.grpc.StatusException; import java.util.Set; public abstract class BaseFieldValidator implements FieldValidator { @@ -48,111 +44,100 @@ public abstract class BaseFieldValidator implements FieldValidator { public void validate( final Object extensionValue, final Descriptors.FieldDescriptor fd, - final Message msg) throws StatusException { - try { - final T extensionValueTyped = resolveExtensionValue(extensionValue); + final Message msg) throws FieldValidationException { + final T extensionValueTyped = resolveExtensionValue(extensionValue); - // for the fields with an `optional` modifier, checking if the field was set - // and if not, checking if extension allows missing optional field - if (fd.hasPresence() && !msg.hasField(fd)) { - switch (missingOptionalAction) { - case FAIL -> { - throw invalidArgument("extension requires a value to be set"); - } - case SUCCEED -> { - return; - } - case VALIDATE_DEFAULT_VALUE -> { - // just continuing - } + // for the fields with an `optional` modifier, checking if the field was set + // and if not, checking if extension allows missing optional field + if (fd.hasPresence() && !msg.hasField(fd)) { + switch (missingOptionalAction) { + case FAIL -> { + throw new FieldValidationException("extension requires a value to be set"); } - } - - // for the `repeated` fields, checking if it's supported by the extension - if (fd.isRepeated()) { - if (applicableToRepeated) { - validateRepeatedField(extensionValueTyped, fd, msg); + case SUCCEED -> { return; } - throw internalError("can't apply extension to a `repeated` field"); - } - - // checking field type against the set of supported types - final Descriptors.FieldDescriptor.Type type = fd.getType(); - if (!supportedTypes.contains(type)) { - throw internalError("can't apply extension to a field of type [%s]".formatted(type)); - } - switch (type) { - case INT64, UINT64, INT32, FIXED64, FIXED32, UINT32, SFIXED32, SFIXED64, SINT32, SINT64 -> - validateIntegerNumber(extensionValueTyped, ((Number) msg.getField(fd)).longValue(), type); - case STRING -> - validateStringValue(extensionValueTyped, (String) msg.getField(fd)); - case BYTES -> - validateBytesValue(extensionValueTyped, (ByteString) msg.getField(fd)); - case ENUM -> - validateEnumValue(extensionValueTyped, (Descriptors.EnumValueDescriptor) msg.getField(fd)); - case MESSAGE -> { - validateMessageValue(extensionValueTyped, (Message) msg.getField(fd)); - } - case FLOAT, DOUBLE, BOOL, GROUP -> { - // at this moment, there are no validations specific to these types of fields + case VALIDATE_DEFAULT_VALUE -> { + // just continuing } } - } catch (StatusException e) { - throw new StatusException(e.getStatus().withDescription( - "field [%s], extension [%s]: %s".formatted(fd.getName(), extensionName, e.getStatus().getDescription()) - ), e.getTrailers()); - } catch (RuntimeException e) { - throw Status.INTERNAL - .withDescription("field [%s], extension [%s]: %s".formatted(fd.getName(), extensionName, e.getMessage())) - .withCause(e) - .asException(); } + + // for the `repeated` fields, checking if it's supported by the extension + if (fd.isRepeated()) { + if (applicableToRepeated) { + validateRepeatedField(extensionValueTyped, fd, msg); + return; + } + throw new IllegalArgumentException("can't apply extension %s to `repeated` field %s" + .formatted(extensionName, fd.getFullName())); + } + + // checking field type against the set of supported types + final Descriptors.FieldDescriptor.Type type = fd.getType(); + if (!supportedTypes.contains(type)) { + throw new IllegalArgumentException("can't apply extension %s to field %s of type %s".formatted( + extensionName, fd.getFullName(), type)); + } + switch (type) { + case INT64, UINT64, INT32, FIXED64, FIXED32, UINT32, SFIXED32, SFIXED64, SINT32, SINT64 -> + validateIntegerNumber(extensionValueTyped, ((Number) msg.getField(fd)).longValue(), type); + case STRING -> validateStringValue(extensionValueTyped, (String) msg.getField(fd)); + case BYTES -> validateBytesValue(extensionValueTyped, (ByteString) msg.getField(fd)); + case ENUM -> validateEnumValue(extensionValueTyped, (Descriptors.EnumValueDescriptor) msg.getField(fd)); + case MESSAGE -> { + validateMessageValue(extensionValueTyped, (Message) msg.getField(fd)); + } + case FLOAT, DOUBLE, BOOL, GROUP -> { + // at this moment, there are no validations specific to these types of fields + } + } + } - protected abstract T resolveExtensionValue(final Object extensionValue) throws StatusException; + protected abstract T resolveExtensionValue(final Object extensionValue) throws FieldValidationException; protected void validateRepeatedField( final T extensionValue, final Descriptors.FieldDescriptor fd, - final Message msg) throws StatusException { - throw internalError("`validateRepeatedField` method needs to be implemented"); + final Message msg) throws FieldValidationException { + throw new UnsupportedOperationException("`validateRepeatedField` method needs to be implemented"); } protected void validateIntegerNumber( final T extensionValue, - final long fieldValue, final Descriptors.FieldDescriptor.Type type) throws StatusException { - throw internalError("`validateIntegerNumber` method needs to be implemented"); + final long fieldValue, final Descriptors.FieldDescriptor.Type type) throws FieldValidationException { + throw new UnsupportedOperationException("`validateIntegerNumber` method needs to be implemented"); } protected void validateStringValue( final T extensionValue, - final String fieldValue) throws StatusException { - throw internalError("`validateStringValue` method needs to be implemented"); + final String fieldValue) throws FieldValidationException { + throw new UnsupportedOperationException("`validateStringValue` method needs to be implemented"); } protected void validateBytesValue( final T extensionValue, - final ByteString fieldValue) throws StatusException { - throw internalError("`validateBytesValue` method needs to be implemented"); + final ByteString fieldValue) throws FieldValidationException { + throw new UnsupportedOperationException("`validateBytesValue` method needs to be implemented"); } protected void validateEnumValue( final T extensionValue, - final Descriptors.EnumValueDescriptor enumValueDescriptor) throws StatusException { - throw internalError("`validateEnumValue` method needs to be implemented"); + final Descriptors.EnumValueDescriptor enumValueDescriptor) throws FieldValidationException { + throw new UnsupportedOperationException("`validateEnumValue` method needs to be implemented"); } protected void validateMessageValue( final T extensionValue, - final Message message) throws StatusException { - throw internalError("`validateMessageValue` method needs to be implemented"); + final Message message) throws FieldValidationException { + throw new UnsupportedOperationException("`validateMessageValue` method needs to be implemented"); } - protected static boolean requireFlagExtension(final Object extensionValue) throws StatusException { + protected static boolean requireFlagExtension(final Object extensionValue) throws FieldValidationException { if (extensionValue instanceof Boolean flagIsOn && flagIsOn) { return true; } - throw internalError("only value `true` is allowed"); + throw new UnsupportedOperationException("only value `true` is allowed"); } } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/validators/E164FieldValidator.java b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/validators/E164FieldValidator.java index e675f032f..424b0484e 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/validators/E164FieldValidator.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/validators/E164FieldValidator.java @@ -5,10 +5,7 @@ package org.whispersystems.textsecuregcm.grpc.validators; -import static org.whispersystems.textsecuregcm.grpc.validators.ValidatorUtils.invalidArgument; - import com.google.protobuf.Descriptors; -import io.grpc.StatusException; import java.util.Set; import org.whispersystems.textsecuregcm.util.ImpossiblePhoneNumberException; import org.whispersystems.textsecuregcm.util.NonNormalizedPhoneNumberException; @@ -21,18 +18,18 @@ public class E164FieldValidator extends BaseFieldValidator { } @Override - protected Boolean resolveExtensionValue(final Object extensionValue) throws StatusException { + protected Boolean resolveExtensionValue(final Object extensionValue) throws FieldValidationException { return requireFlagExtension(extensionValue); } @Override protected void validateStringValue( final Boolean extensionValue, - final String fieldValue) throws StatusException { + final String fieldValue) throws FieldValidationException { try { Util.requireNormalizedNumber(fieldValue); } catch (final ImpossiblePhoneNumberException | NonNormalizedPhoneNumberException e) { - throw invalidArgument("value is not in E164 format"); + throw new FieldValidationException("value is not in E164 format"); } } } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/validators/EnumSpecifiedFieldValidator.java b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/validators/EnumSpecifiedFieldValidator.java index a86ecee77..5bf104943 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/validators/EnumSpecifiedFieldValidator.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/validators/EnumSpecifiedFieldValidator.java @@ -5,10 +5,7 @@ package org.whispersystems.textsecuregcm.grpc.validators; -import static org.whispersystems.textsecuregcm.grpc.validators.ValidatorUtils.invalidArgument; - import com.google.protobuf.Descriptors; -import io.grpc.StatusException; import java.util.Set; public class EnumSpecifiedFieldValidator extends BaseFieldValidator { @@ -18,16 +15,16 @@ public class EnumSpecifiedFieldValidator extends BaseFieldValidator { } @Override - protected Boolean resolveExtensionValue(final Object extensionValue) throws StatusException { + protected Boolean resolveExtensionValue(final Object extensionValue) throws FieldValidationException { return requireFlagExtension(extensionValue); } @Override protected void validateEnumValue( final Boolean extensionValue, - final Descriptors.EnumValueDescriptor enumValueDescriptor) throws StatusException { + final Descriptors.EnumValueDescriptor enumValueDescriptor) throws FieldValidationException { if (enumValueDescriptor.getIndex() <= 0) { - throw invalidArgument("enum field must be specified"); + throw new FieldValidationException("enum field must be specified"); } } } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/validators/ExactlySizeFieldValidator.java b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/validators/ExactlySizeFieldValidator.java index 6eed659d1..d3402d024 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/validators/ExactlySizeFieldValidator.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/validators/ExactlySizeFieldValidator.java @@ -5,12 +5,9 @@ package org.whispersystems.textsecuregcm.grpc.validators; -import static org.whispersystems.textsecuregcm.grpc.validators.ValidatorUtils.invalidArgument; - import com.google.protobuf.ByteString; import com.google.protobuf.Descriptors; import com.google.protobuf.Message; -import io.grpc.StatusException; import java.util.List; import java.util.Set; @@ -24,7 +21,7 @@ public class ExactlySizeFieldValidator extends BaseFieldValidator> } @Override - protected Set resolveExtensionValue(final Object extensionValue) throws StatusException { + protected Set resolveExtensionValue(final Object extensionValue) { //noinspection unchecked return Set.copyOf((List) extensionValue); } @@ -32,32 +29,32 @@ public class ExactlySizeFieldValidator extends BaseFieldValidator> @Override protected void validateBytesValue( final Set permittedSizes, - final ByteString fieldValue) throws StatusException { + final ByteString fieldValue) throws FieldValidationException { if (permittedSizes.contains(fieldValue.size())) { return; } - throw invalidArgument("byte array length is [%d] but expected to be one of %s".formatted(fieldValue.size(), permittedSizes)); + throw new FieldValidationException("byte array length is [%d] but expected to be one of %s".formatted(fieldValue.size(), permittedSizes)); } @Override protected void validateStringValue( final Set permittedSizes, - final String fieldValue) throws StatusException { + final String fieldValue) throws FieldValidationException { if (permittedSizes.contains(fieldValue.length())) { return; } - throw invalidArgument("string length is [%d] but expected to be one of %s".formatted(fieldValue.length(), permittedSizes)); + throw new FieldValidationException("string length is [%d] but expected to be one of %s".formatted(fieldValue.length(), permittedSizes)); } @Override protected void validateRepeatedField( final Set permittedSizes, final Descriptors.FieldDescriptor fd, - final Message msg) throws StatusException { + final Message msg) throws FieldValidationException { final int size = msg.getRepeatedFieldCount(fd); if (permittedSizes.contains(size)) { return; } - throw invalidArgument("list size is [%d] but expected to be one of %s".formatted(size, permittedSizes)); + throw new FieldValidationException("list size is [%d] but expected to be one of %s".formatted(size, permittedSizes)); } } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/validators/FieldValidationException.java b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/validators/FieldValidationException.java new file mode 100644 index 000000000..f267a605b --- /dev/null +++ b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/validators/FieldValidationException.java @@ -0,0 +1,11 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.grpc.validators; + +public class FieldValidationException extends Exception { + public FieldValidationException(String message) { + super(message); + } +} diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/validators/FieldValidator.java b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/validators/FieldValidator.java index cdf668e90..c52faecbc 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/validators/FieldValidator.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/validators/FieldValidator.java @@ -7,10 +7,9 @@ package org.whispersystems.textsecuregcm.grpc.validators; import com.google.protobuf.Descriptors; import com.google.protobuf.Message; -import io.grpc.StatusException; public interface FieldValidator { void validate(Object extensionValue, Descriptors.FieldDescriptor fd, Message msg) - throws StatusException; + throws FieldValidationException; } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/validators/NonEmptyFieldValidator.java b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/validators/NonEmptyFieldValidator.java index 91bb256b3..6989d7aab 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/validators/NonEmptyFieldValidator.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/validators/NonEmptyFieldValidator.java @@ -5,12 +5,9 @@ package org.whispersystems.textsecuregcm.grpc.validators; -import static org.whispersystems.textsecuregcm.grpc.validators.ValidatorUtils.invalidArgument; - import com.google.protobuf.ByteString; import com.google.protobuf.Descriptors; import com.google.protobuf.Message; -import io.grpc.StatusException; import java.util.Set; import org.apache.commons.lang3.StringUtils; @@ -24,38 +21,38 @@ public class NonEmptyFieldValidator extends BaseFieldValidator { } @Override - protected Boolean resolveExtensionValue(final Object extensionValue) throws StatusException { + protected Boolean resolveExtensionValue(final Object extensionValue) throws FieldValidationException { return requireFlagExtension(extensionValue); } @Override protected void validateBytesValue( final Boolean extensionValue, - final ByteString fieldValue) throws StatusException { + final ByteString fieldValue) throws FieldValidationException { if (!fieldValue.isEmpty()) { return; } - throw invalidArgument("byte array expected to be non-empty"); + throw new FieldValidationException("byte array expected to be non-empty"); } @Override protected void validateStringValue( final Boolean extensionValue, - final String fieldValue) throws StatusException { + final String fieldValue) throws FieldValidationException { if (StringUtils.isNotEmpty(fieldValue)) { return; } - throw invalidArgument("string expected to be non-empty"); + throw new FieldValidationException("string expected to be non-empty"); } @Override protected void validateRepeatedField( final Boolean extensionValue, final Descriptors.FieldDescriptor fd, - final Message msg) throws StatusException { + final Message msg) throws FieldValidationException { if (msg.getRepeatedFieldCount(fd) > 0) { return; } - throw invalidArgument("repeated field is expected to be non-empty"); + throw new FieldValidationException("repeated field is expected to be non-empty"); } } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/validators/PresentFieldValidator.java b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/validators/PresentFieldValidator.java index a516b476b..e025dca9a 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/validators/PresentFieldValidator.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/validators/PresentFieldValidator.java @@ -5,11 +5,8 @@ package org.whispersystems.textsecuregcm.grpc.validators; -import static org.whispersystems.textsecuregcm.grpc.validators.ValidatorUtils.invalidArgument; - import com.google.protobuf.Descriptors; import com.google.protobuf.Message; -import io.grpc.StatusException; import java.util.Set; public class PresentFieldValidator extends BaseFieldValidator { @@ -22,14 +19,14 @@ public class PresentFieldValidator extends BaseFieldValidator { } @Override - protected Boolean resolveExtensionValue(final Object extensionValue) throws StatusException { + protected Boolean resolveExtensionValue(final Object extensionValue) throws FieldValidationException { return requireFlagExtension(extensionValue); } @Override - protected void validateMessageValue(final Boolean extensionValue, final Message msg) throws StatusException { + protected void validateMessageValue(final Boolean extensionValue, final Message msg) throws FieldValidationException { if (msg == null) { - throw invalidArgument("message expected to be present"); + throw new FieldValidationException("message expected to be present"); } } } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/validators/RangeFieldValidator.java b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/validators/RangeFieldValidator.java index cd84f386a..0af3a84d7 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/validators/RangeFieldValidator.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/validators/RangeFieldValidator.java @@ -5,10 +5,7 @@ package org.whispersystems.textsecuregcm.grpc.validators; -import static org.whispersystems.textsecuregcm.grpc.validators.ValidatorUtils.invalidArgument; - import com.google.protobuf.Descriptors; -import io.grpc.StatusException; import java.util.Set; import org.signal.chat.require.ValueRangeConstraint; @@ -37,7 +34,7 @@ public class RangeFieldValidator extends BaseFieldValidator { } @Override - protected Range resolveExtensionValue(final Object extensionValue) throws StatusException { + protected Range resolveExtensionValue(final Object extensionValue) { final ValueRangeConstraint rangeConstraint = (ValueRangeConstraint) extensionValue; final long min = rangeConstraint.hasMin() ? rangeConstraint.getMin() : Long.MIN_VALUE; final long max = rangeConstraint.hasMax() ? rangeConstraint.getMax() : Long.MAX_VALUE; @@ -48,13 +45,13 @@ public class RangeFieldValidator extends BaseFieldValidator { protected void validateIntegerNumber( final Range range, final long fieldValue, - final Descriptors.FieldDescriptor.Type type) throws StatusException { + final Descriptors.FieldDescriptor.Type type) throws FieldValidationException { if (fieldValue < 0 && UNSIGNED_TYPES.contains(type)) { - throw invalidArgument("field value is expected to be within the [%d, %d] range".formatted( + throw new FieldValidationException("field value is expected to be within the [%d, %d] range".formatted( range.min(), range.max())); } if (fieldValue < range.min() || fieldValue > range.max()) { - throw invalidArgument("field value is [%d] but expected to be within the [%d, %d] range".formatted( + throw new FieldValidationException("field value is [%d] but expected to be within the [%d, %d] range".formatted( fieldValue, range.min(), range.max())); } } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/validators/SizeFieldValidator.java b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/validators/SizeFieldValidator.java index 8d8fd2298..bfe1dbcbc 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/validators/SizeFieldValidator.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/validators/SizeFieldValidator.java @@ -5,12 +5,9 @@ package org.whispersystems.textsecuregcm.grpc.validators; -import static org.whispersystems.textsecuregcm.grpc.validators.ValidatorUtils.invalidArgument; - import com.google.protobuf.ByteString; import com.google.protobuf.Descriptors; import com.google.protobuf.Message; -import io.grpc.StatusException; import java.util.Set; import org.signal.chat.require.SizeConstraint; @@ -24,7 +21,7 @@ public class SizeFieldValidator extends BaseFieldValidator { } @Override - protected Range resolveExtensionValue(final Object extensionValue) throws StatusException { + protected Range resolveExtensionValue(final Object extensionValue) throws FieldValidationException { final SizeConstraint sizeConstraint = (SizeConstraint) extensionValue; final int min = sizeConstraint.hasMin() ? sizeConstraint.getMin() : 0; final int max = sizeConstraint.hasMax() ? sizeConstraint.getMax() : Integer.MAX_VALUE; @@ -32,26 +29,26 @@ public class SizeFieldValidator extends BaseFieldValidator { } @Override - protected void validateBytesValue(final Range range, final ByteString fieldValue) throws StatusException { + protected void validateBytesValue(final Range range, final ByteString fieldValue) throws FieldValidationException { if (fieldValue.size() < range.min() || fieldValue.size() > range.max()) { - throw invalidArgument("field value is [%d] but expected to be within the [%d, %d] range".formatted( + throw new FieldValidationException("field value is [%d] but expected to be within the [%d, %d] range".formatted( fieldValue.size(), range.min(), range.max())); } } @Override - protected void validateStringValue(final Range range, final String fieldValue) throws StatusException { + protected void validateStringValue(final Range range, final String fieldValue) throws FieldValidationException { if (fieldValue.length() < range.min() || fieldValue.length() > range.max()) { - throw invalidArgument("field value is [%d] but expected to be within the [%d, %d] range".formatted( + throw new FieldValidationException("field value is [%d] but expected to be within the [%d, %d] range".formatted( fieldValue.length(), range.min(), range.max())); } } @Override - protected void validateRepeatedField(final Range range, final Descriptors.FieldDescriptor fd, final Message msg) throws StatusException { + protected void validateRepeatedField(final Range range, final Descriptors.FieldDescriptor fd, final Message msg) throws FieldValidationException { final int size = msg.getRepeatedFieldCount(fd); if (size < range.min() || size > range.max()) { - throw invalidArgument("field value is [%d] but expected to be within the [%d, %d] range".formatted( + throw new FieldValidationException("field value is [%d] but expected to be within the [%d, %d] range".formatted( size, range.min(), range.max())); } } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/validators/ValidatorUtils.java b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/validators/ValidatorUtils.java index 04afa4adb..ee7937691 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/validators/ValidatorUtils.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/validators/ValidatorUtils.java @@ -23,18 +23,6 @@ public final class ValidatorUtils { // noop } - public static StatusException invalidArgument(final String description) { - return Status.INVALID_ARGUMENT.withDescription(description).asException(); - } - - public static StatusException internalError(final String description) { - return Status.INTERNAL.withDescription(description).asException(); - } - - public static StatusException internalError(final Exception cause) { - return Status.INTERNAL.withCause(cause).asException(); - } - public static Optional serviceAuthExtensionValue(final ServerServiceDefinition serviceDefinition) { return serviceExtensionValueByName(serviceDefinition, REQUIRE_AUTH_EXTENSION_NAME) .map(val -> Auth.valueOf((Descriptors.EnumValueDescriptor) val)); diff --git a/service/src/main/proto/org/signal/chat/README.md b/service/src/main/proto/org/signal/chat/README.md index 234859ac4..e1b39f38f 100644 --- a/service/src/main/proto/org/signal/chat/README.md +++ b/service/src/main/proto/org/signal/chat/README.md @@ -1,44 +1,38 @@ # Chat gRPC API +Eventually, all chat protocol endpoints will be available via gRPC. + +Clients may provide headers for gRPC requests via [gRPC metadata](https://grpc.io/docs/guides/metadata/) which translates directly to HTTP/2 headers. + +- Clients should provide a `User-Agent` header on all gRPC requests. +- Clients may provide an `Accept-Language` on any gRPC requests. ## Errors -The chat-server gRPC API uses the [well-defined gRPC status codes](https://grpc.github.io/grpc/core/md_doc_statuscodes.html) returned as part of every RPC call. Common error conditions in the API are mapped to some status code and rather than being explicitly represented in the RPC messages. Broadly, there are 3 categories of errors: +In the gRPC protocol all errors are at the request level. That is, errors are returned in response to individual requests and do not impact other H2 streams on the same connection nor terminate the connection. -1. Common status errors: gRPC error status that should be handled identically across many RPC calls -2. Domain specific status errors: gRPC error statuses that accurately represent an error case of an individual RPC -3. Domain specific custom errors: Errors represented explicitly in the protobuf return type for an RPC call +For errors that may be returned by any RPCs, the chat server will return the well-defined gRPC status codes returned as part of every RPC call. For errors that are specific to a particular RPC, the error must be encoded in the service proto definition and will be returned with a `Status` of `OK`. -Server APIs attempt to include additional debugging information about the nature of the error in the detail string of the status. Status messages may change at any time and are intended only as a debugging aid; consumers of status messages must never make assumptions about their structure or content. -### Common status errors +Status errors include additional metadata as described in [AIP-193 (google's richer error model)](https://google.aip.dev/193#error_model). Every `Status != OK` response returned by the chat server's application layer will include a `Grpc-Status-Details-Bin` response trailer with a `google.rpc.Status` proto. -Common status errors are errors that may be handled similarly, regardless of which RPC is being called. This usually means retrying, ignoring, or logging for bug reporting. For example, the chat gRPC API reserves `RESOURCE_EXHAUSTED` for temporary exhaustion of a per-user resource. Regardless of the RPC being called, the caller should handle this error by retrying with a backoff, using the `retry-after` time interval specified via trailing metadata if the server provides it. +Each `google.rpc.Status` must have a status matching the top-level status on the gRPC response. Additionally, a single `ErrorInfo` must always be present in the details field of the `Status`. The `ErrorInfo` must contain a `domain` field and a `reason` field. -Common status errors are unexpected. They represent a transient condition that causes requests to fail or a misbehaving client/server. Errors expected in the normal path of operation should use one of the domain specific error types. For example, looking up a username that does not exist by hash does _not_ return common status error since that is an expected response. However, if the lookup request does not contain a valid hash, that _is_ a common status error because it means the client did not properly construct the request. +The `domain` for a status error generated by the chat server will always be `grpc.chat.signal.org` -Currently, any RPC may return one of these common errors. Unless otherwise indicated, consult the [gRPC docs](https://grpc.github.io/grpc/core/md_doc_statuscodes.html) for their interpretation. +The server may set the `reason` to match the enum string for the `Status.Code` if there is no need to further distinguish a code. Clients should inspect the `reason`, not the `status`, for automated error handling. +The chat server may return the following errors from any RPC -| code | number | notes | -|----------------------|--------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `UNKNOWN` | 2 | | -| `INVALID_ARGUMENT` | 3 | Violates some constraint documented in the message protobuf. Constraints may be expressed via comments or custom `require` annotations on the request. | -| `RESOURCE_EXHAUSTED` | 8 | A per-user resource has been temporarily exhausted (usually a rate-limit). A `retry-after` header containing an ISO8601 duration string may be present in the response headers, indicating how long it will be before a retry can succeed. | -| `INTERNAL` | 13 | | -| `UNAVAILABLE` | 14 | | -| `UNAUTHENTICATED` | 16 | Indicates some problem with the caller's authentication status. For example, calling an authenticated endpoint from an unauthenticated connection, or calling an group send endorsement (GSE) authenticated endpoint with an invalid GSE. | +| Status Code | Reason | Description | +| :---- | :---- |:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `INVALID_ARGUMENT` | `UPGRADE_REQUIRED` | The client version provided in the `User-Agent` is no longer supported. The client must upgrade to use the service. | +| `INVALID_ARGUMENT` | `CONSTRAINT_VIOLATED` | The RPC argument violated a constraint that was annotated or documented in the service definition. It is always possible to check this constraint without communicating with the chat server. This always represents a client bug or out of date client.

The `details` may include a [`BadRequest` message](https://github.com/googleapis/googleapis/blob/8c06c1e04ae562f49f411357577c700e9142f33c/google/rpc/error_details.proto#L236) that indicates additional information about the invalid field. | +| `INVALID_ARGUMENT` | `BAD_AUTHENTICATION` | The request has incorrectly set authentication credentials for the RPC. This represents a client bug where the authorization mode is not correct for the RPC. For example,

The RPC was for an anonymous service, but included an Authentication header in the RPC metadata.

The RPC should only be made by the primary device, but the request had linked device credentials. | +| `UNAUTHENTICATED` | `INVALID_CREDENTIALS` | The account credentials provided in the authorization header are not valid. | +| `RESOURCE_EXHAUSTED` | `RESOURCE_EXHAUSTED` | A server-side resource was exhausted. The `details` field may include a [`RetryInfo` message](https://github.com/googleapis/googleapis/blob/8c06c1e04ae562f49f411357577c700e9142f33c/google/rpc/error_details.proto#L92) that includes the amount of time in seconds the client should wait before retrying the request.

If a `RetryInfo` is present, the client must wait the indicated time before retrying the request. If absent, the client should retry with an exponential backoff. | +| `UNAVAILABLE` | `UNAVAILABLE` | There was an internal error processing the RPC. The client should retry the request with exponential backoff. | +### Logging Errors -If one of these errors has a more specific meaning for a certain RPC, it will be documented on the RPC. Note this section does not include errors that may be generated on the client side (like `CANCELLED` or `DEADLINE_EXCEEDED`), consult your client's gRPC documentation. +When logging error responses, clients may always log the status code, domain, and reason. -### Domain specific status errors - -Occasionally, an RPC will return a status error for a widely understood error scenario that is not applicable across all RPCs or has some domain specific handling. For example, sending a message to a user may return `NOT_FOUND` if that user does not exist. - -Any domain specific errors will always be documented on the RPC or enclosing service. - -### Domain specific custom errors - -All other errors are explicitly represented as part of the response definitions for the RPC. Typically, an RPC will provide custom errors when: -- No gRPC status maps to the represented error -- Additional structured data needs to be returned along with the error(s) -- The error does not represent a client bug, but requires handling beyond retrying or ignoring +For errors with domain `grpc.chat.signal.org` and reason `CONSTRAINT_VIOLATED`, clients should check the `details` field for a `BadRequest` proto. If present, they should log the `field` of each violation in `field_violations`. diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/grpc/GrpcTestUtils.java b/service/src/test/java/org/whispersystems/textsecuregcm/grpc/GrpcTestUtils.java index 8ebbe2f8a..55e518055 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/grpc/GrpcTestUtils.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/grpc/GrpcTestUtils.java @@ -34,26 +34,6 @@ public final class GrpcTestUtils { // noop } - public static void setupAuthenticatedExtension( - final GrpcServerExtension extension, - final MockAuthenticationInterceptor mockAuthenticationInterceptor, - final MockRequestAttributesInterceptor mockRequestAttributesInterceptor, - final UUID authenticatedAci, - final byte authenticatedDeviceId, - final BindableService service) { - mockAuthenticationInterceptor.setAuthenticatedDevice(authenticatedAci, authenticatedDeviceId); - extension.getServiceRegistry() - .addService(ServerInterceptors.intercept(service, new ValidatingInterceptor(), mockRequestAttributesInterceptor, mockAuthenticationInterceptor, new ErrorMappingInterceptor())); - } - - public static void setupUnauthenticatedExtension( - final GrpcServerExtension extension, - final MockRequestAttributesInterceptor mockRequestAttributesInterceptor, - final BindableService service) { - extension.getServiceRegistry() - .addService(ServerInterceptors.intercept(service, new ValidatingInterceptor(), mockRequestAttributesInterceptor, new ErrorMappingInterceptor())); - } - public static void assertStatusException(final Status expected, final Executable serviceCall) { final StatusRuntimeException exception = Assertions.assertThrows(StatusRuntimeException.class, serviceCall); assertEquals(expected.getCode(), exception.getStatus().getCode()); diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/grpc/KeyTransparencyGrpcServiceTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/grpc/KeyTransparencyGrpcServiceTest.java index 241b26f3a..3ce374541 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/grpc/KeyTransparencyGrpcServiceTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/grpc/KeyTransparencyGrpcServiceTest.java @@ -7,6 +7,7 @@ package org.whispersystems.textsecuregcm.grpc; import com.google.protobuf.ByteString; import io.grpc.Channel; +import io.grpc.ServerInterceptor; import io.grpc.Status; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -34,6 +35,7 @@ import org.whispersystems.textsecuregcm.limits.RateLimiters; import reactor.core.publisher.Mono; import java.time.Duration; +import java.util.List; import java.util.Optional; import java.util.stream.Stream; @@ -302,4 +304,13 @@ public class KeyTransparencyGrpcServiceTest extends SimpleBaseGrpcTest customizeInterceptors(List serverInterceptors) { + return serverInterceptors.stream() + // For now, don't validate conformance of KeyTransparency errors since they are forwarded directly from a + // backing service + .filter(interceptor -> !(interceptor instanceof ErrorConformanceInterceptor)) + .toList(); + } } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/grpc/MessagesAnonymousGrpcServiceTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/grpc/MessagesAnonymousGrpcServiceTest.java index c5b0e2fb4..18839f7aa 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/grpc/MessagesAnonymousGrpcServiceTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/grpc/MessagesAnonymousGrpcServiceTest.java @@ -427,7 +427,7 @@ class MessagesAnonymousGrpcServiceTest extends when(spamChecker.checkForIndividualRecipientSpamGrpc(any(), any(), any(), any())) .thenReturn(new SpamCheckResult<>( - Optional.of(GrpcResponse.withStatusException(Status.RESOURCE_EXHAUSTED.asRuntimeException())), + Optional.of(GrpcResponse.withStatusException(GrpcExceptions.rateLimitExceeded(null))), Optional.empty())); //noinspection ResultOfMethodCallIgnored @@ -793,7 +793,7 @@ class MessagesAnonymousGrpcServiceTest extends when(spamChecker.checkForMultiRecipientSpamGrpc(any())) .thenReturn(new SpamCheckResult<>( - Optional.of(GrpcResponse.withStatusException(Status.RESOURCE_EXHAUSTED.asRuntimeException())), + Optional.of(GrpcResponse.withStatusException(GrpcExceptions.rateLimitExceeded(null))), Optional.empty())); //noinspection ResultOfMethodCallIgnored @@ -1052,7 +1052,7 @@ class MessagesAnonymousGrpcServiceTest extends when(spamChecker.checkForIndividualRecipientSpamGrpc(any(), any(), any(), any())) .thenReturn(new SpamCheckResult<>( - Optional.of(GrpcResponse.withStatusException(Status.RESOURCE_EXHAUSTED.asRuntimeException())), + Optional.of(GrpcResponse.withStatusException(GrpcExceptions.rateLimitExceeded(null))), Optional.empty())); //noinspection ResultOfMethodCallIgnored @@ -1334,8 +1334,8 @@ class MessagesAnonymousGrpcServiceTest extends .build(); when(spamChecker.checkForMultiRecipientSpamGrpc(any())) - .thenReturn(new SpamCheckResult<>(Optional.of( - GrpcResponse.withStatusException(Status.RESOURCE_EXHAUSTED.asRuntimeException())), + .thenReturn(new SpamCheckResult<>( + Optional.of(GrpcResponse.withStatusException(GrpcExceptions.rateLimitExceeded(null))), Optional.empty())); //noinspection ResultOfMethodCallIgnored diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/grpc/MessagesGrpcServiceTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/grpc/MessagesGrpcServiceTest.java index 04dd659de..03439fa5f 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/grpc/MessagesGrpcServiceTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/grpc/MessagesGrpcServiceTest.java @@ -359,7 +359,7 @@ class MessagesGrpcServiceTest extends SimpleBaseGrpcTest( - Optional.of(GrpcResponse.withStatusException(Status.RESOURCE_EXHAUSTED.asRuntimeException())), + Optional.of(GrpcResponse.withStatusException(GrpcExceptions.rateLimitExceeded(null))), Optional.empty())); //noinspection ResultOfMethodCallIgnored diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/grpc/ProfileAnonymousGrpcServiceTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/grpc/ProfileAnonymousGrpcServiceTest.java index 0a1d75c8f..f30e33eb0 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/grpc/ProfileAnonymousGrpcServiceTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/grpc/ProfileAnonymousGrpcServiceTest.java @@ -17,6 +17,7 @@ import static org.whispersystems.textsecuregcm.grpc.GrpcTestUtils.assertStatusEx import com.google.common.net.InetAddresses; import com.google.protobuf.ByteString; +import io.grpc.ServerInterceptor; import io.grpc.Status; import java.nio.charset.StandardCharsets; import java.time.Duration; @@ -593,4 +594,13 @@ public class ProfileAnonymousGrpcServiceTest extends SimpleBaseGrpcTest customizeInterceptors(List serverInterceptors) { + return serverInterceptors.stream() + // For now, don't validate error conformance because the profiles gRPC service has not been converted to the + // updated error model + .filter(interceptor -> !(interceptor instanceof ErrorConformanceInterceptor)) + .toList(); + } } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/grpc/ProfileGrpcServiceTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/grpc/ProfileGrpcServiceTest.java index 3729d1f13..e302e7b9e 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/grpc/ProfileGrpcServiceTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/grpc/ProfileGrpcServiceTest.java @@ -29,6 +29,9 @@ import com.google.common.net.InetAddresses; import com.google.i18n.phonenumbers.PhoneNumberUtil; import com.google.i18n.phonenumbers.Phonenumber; import com.google.protobuf.ByteString; +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerInterceptor; import io.grpc.Status; import java.nio.charset.StandardCharsets; import java.time.Clock; @@ -753,4 +756,13 @@ public class ProfileGrpcServiceTest extends SimpleBaseGrpcTest customizeInterceptors(List serverInterceptors) { + return serverInterceptors.stream() + // For now, don't validate error conformance because the profiles gRPC service has not been converted to the + // updated error model + .filter(interceptor -> !(interceptor instanceof ErrorConformanceInterceptor)) + .toList(); + } } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/grpc/SimpleBaseGrpcTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/grpc/SimpleBaseGrpcTest.java index bf24c6750..f546dcc5b 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/grpc/SimpleBaseGrpcTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/grpc/SimpleBaseGrpcTest.java @@ -9,9 +9,12 @@ import static java.util.Objects.requireNonNull; import io.grpc.BindableService; import io.grpc.Channel; +import io.grpc.ServerInterceptor; +import io.grpc.ServerInterceptors; import io.grpc.stub.AbstractBlockingStub; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.util.List; import java.util.UUID; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -113,9 +116,18 @@ public abstract class SimpleBaseGrpcTest authenticatedInterceptors = + List.of(new ValidatingInterceptor(), mockRequestAttributesInterceptor, mockAuthenticationInterceptor, new ErrorMappingInterceptor(), new ErrorConformanceInterceptor()); + GRPC_SERVER_EXTENSION_AUTHENTICATED + .getServiceRegistry() + .addService(ServerInterceptors.intercept(service, customizeInterceptors(authenticatedInterceptors))); + + final List unauthenticatedInterceptors = + List.of(new ValidatingInterceptor(), mockRequestAttributesInterceptor, new ErrorMappingInterceptor(), new ErrorConformanceInterceptor()); + GRPC_SERVER_EXTENSION_UNAUTHENTICATED.getServiceRegistry() + .addService(ServerInterceptors.intercept(service, customizeInterceptors(unauthenticatedInterceptors))); try { authenticatedServiceStub = createStub(GRPC_SERVER_EXTENSION_AUTHENTICATED.getChannel()); unauthenticatedServiceStub = createStub(GRPC_SERVER_EXTENSION_UNAUTHENTICATED.getChannel()); @@ -152,4 +164,8 @@ public abstract class SimpleBaseGrpcTest customizeInterceptors(List serverInterceptors) { + return serverInterceptors; + } }