Add controllers/service implementations for receiving call quality survey responses

This commit is contained in:
Jon Chambers
2025-10-09 13:30:21 -04:00
committed by Jon Chambers
parent c68e3103c4
commit 9378b9a6e6
6 changed files with 282 additions and 0 deletions

View File

@@ -107,6 +107,7 @@ import org.whispersystems.textsecuregcm.controllers.AccountControllerV2;
import org.whispersystems.textsecuregcm.controllers.ArchiveController;
import org.whispersystems.textsecuregcm.controllers.AttachmentControllerV4;
import org.whispersystems.textsecuregcm.controllers.CallLinkController;
import org.whispersystems.textsecuregcm.controllers.CallQualitySurveyController;
import org.whispersystems.textsecuregcm.controllers.CallRoutingControllerV2;
import org.whispersystems.textsecuregcm.controllers.CertificateController;
import org.whispersystems.textsecuregcm.controllers.ChallengeController;
@@ -142,6 +143,7 @@ import org.whispersystems.textsecuregcm.filters.RestDeprecationFilter;
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.ErrorMappingInterceptor;
import org.whispersystems.textsecuregcm.grpc.ExternalServiceCredentialsAnonymousGrpcService;
import org.whispersystems.textsecuregcm.grpc.ExternalServiceCredentialsGrpcService;
@@ -875,6 +877,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
final List<ServerServiceDefinition> unauthenticatedServices = Stream.of(
new AccountsAnonymousGrpcService(accountsManager, rateLimiters),
new CallQualitySurveyGrpcService(callQualitySurveyManager, rateLimiters),
new KeysAnonymousGrpcService(accountsManager, keysManager, zkSecretParams, Clock.systemUTC()),
new PaymentsGrpcService(currencyManager),
ExternalServiceCredentialsAnonymousGrpcService.create(accountsManager, config),
@@ -1052,6 +1055,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
new ArchiveController(accountsManager, backupAuthManager, backupManager, backupMetrics),
new CallRoutingControllerV2(rateLimiters, cloudflareTurnCredentialsManager),
new CallLinkController(rateLimiters, callingGenericZkSecretParams),
new CallQualitySurveyController(callQualitySurveyManager),
new CertificateController(accountsManager, new CertificateGenerator(config.getDeliveryCertificate().certificate(),
config.getDeliveryCertificate().ecPrivateKey(), config.getDeliveryCertificate().expiresDays()),
zkAuthOperations, callingGenericZkSecretParams, clock),

View File

@@ -0,0 +1,76 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.controllers;
import com.google.common.net.HttpHeaders;
import com.google.protobuf.InvalidProtocolBufferException;
import io.dropwizard.auth.Auth;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.headers.Header;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import jakarta.validation.constraints.NotNull;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.ForbiddenException;
import jakarta.ws.rs.HeaderParam;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import java.util.Optional;
import org.signal.chat.calling.quality.SubmitCallQualitySurveyRequest;
import org.whispersystems.textsecuregcm.auth.AuthenticatedDevice;
import org.whispersystems.textsecuregcm.filters.RemoteAddressFilter;
import org.whispersystems.textsecuregcm.limits.RateLimitedByIp;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.metrics.CallQualitySurveyManager;
@Path("/v1/call_quality_survey")
@io.swagger.v3.oas.annotations.tags.Tag(name = "Account")
public class CallQualitySurveyController {
private final CallQualitySurveyManager callQualitySurveyManager;
public CallQualitySurveyController(final CallQualitySurveyManager callQualitySurveyManager) {
this.callQualitySurveyManager = callQualitySurveyManager;
}
@PUT
@Consumes(MediaType.APPLICATION_OCTET_STREAM)
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Submit survey response", description = "Submits a call quality survey response")
@ApiResponse(responseCode = "204", description = "The survey response was submitted successfully")
@ApiResponse(responseCode = "422", description = "The survey response could not be parsed")
@ApiResponse(responseCode = "429", description = "Too many attempts", headers = @Header(
name = "Retry-After",
description = "If present, an positive integer indicating the number of seconds before a subsequent attempt could succeed"))
@RateLimitedByIp(RateLimiters.For.SUBMIT_CALL_QUALITY_SURVERY)
public void submitCallQualitySurvey(@Auth final Optional<AuthenticatedDevice> authenticatedDevice,
@RequestBody(description = "A serialized survey response protobuf entity")
@NotNull final byte[] surveyResponse,
@HeaderParam(HttpHeaders.USER_AGENT) final String userAgentString,
@Context final ContainerRequestContext requestContext) {
if (authenticatedDevice.isPresent()) {
throw new ForbiddenException("must not use authenticated connection for call quality survey submissions");
}
final SubmitCallQualitySurveyRequest submitCallQualitySurveyRequest;
try {
submitCallQualitySurveyRequest = SubmitCallQualitySurveyRequest.parseFrom(surveyResponse);
} catch (final InvalidProtocolBufferException e) {
throw new WebApplicationException(422);
}
final String remoteAddress = (String) requestContext.getProperty(RemoteAddressFilter.REMOTE_ADDRESS_ATTRIBUTE_NAME);
callQualitySurveyManager.submitCallQualitySurvey(submitCallQualitySurveyRequest, remoteAddress, userAgentString);
}
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.grpc;
import org.signal.chat.calling.quality.SimpleCallQualityGrpc;
import org.signal.chat.calling.quality.SubmitCallQualitySurveyRequest;
import org.signal.chat.calling.quality.SubmitCallQualitySurveyResponse;
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.metrics.CallQualitySurveyManager;
public class CallQualitySurveyGrpcService extends SimpleCallQualityGrpc.CallQualityImplBase {
private final CallQualitySurveyManager callQualitySurveyManager;
private final RateLimiters rateLimiters;
public CallQualitySurveyGrpcService(final CallQualitySurveyManager callQualitySurveyManager,
final RateLimiters rateLimiters) {
this.callQualitySurveyManager = callQualitySurveyManager;
this.rateLimiters = rateLimiters;
}
@Override
public SubmitCallQualitySurveyResponse submitCallQualitySurvey(final SubmitCallQualitySurveyRequest request)
throws RateLimitExceededException {
final String remoteAddress = RequestAttributesUtil.getRemoteAddress().getHostAddress();
rateLimiters.getSubmitCallQualitySurveyLimiter().validate(remoteAddress);
callQualitySurveyManager.submitCallQualitySurvey(request,
remoteAddress,
RequestAttributesUtil.getUserAgent().orElse(null));
return SubmitCallQualitySurveyResponse.getDefaultInstance();
}
}

View File

@@ -56,6 +56,7 @@ public class RateLimiters extends BaseRateLimiters<RateLimiters.For> {
RECORD_DEVICE_TRANSFER_REQUEST("recordDeviceTransferRequest", new RateLimiterConfig(10, Duration.ofMillis(100), true)),
WAIT_FOR_DEVICE_TRANSFER_REQUEST("waitForDeviceTransferRequest", new RateLimiterConfig(10, Duration.ofMillis(100), true)),
DEVICE_CHECK_CHALLENGE("deviceCheckChallenge", new RateLimiterConfig(10, Duration.ofMinutes(1), false)),
SUBMIT_CALL_QUALITY_SURVERY("submitCallQualitySurvey", new RateLimiterConfig(100, Duration.ofMinutes(1), true))
;
private final String id;
@@ -221,4 +222,8 @@ public class RateLimiters extends BaseRateLimiters<RateLimiters.For> {
public RateLimiter getKeyTransparencyMonitorLimiter() {
return forDescriptor(For.KEY_TRANSPARENCY_MONITOR_PER_IP);
}
public RateLimiter getSubmitCallQualitySurveyLimiter() {
return forDescriptor(For.SUBMIT_CALL_QUALITY_SURVERY);
}
}