Convert `UserAgent` to a record

This commit is contained in:
Jon Chambers 2025-04-15 12:38:00 -04:00 committed by Jon Chambers
parent 3c40e72d27
commit f5e49b6db7
11 changed files with 33 additions and 85 deletions

View File

@ -402,7 +402,7 @@ public class DeviceController {
private AtomicInteger getCounterForLinkedDeviceListeners(final String userAgent) { private AtomicInteger getCounterForLinkedDeviceListeners(final String userAgent) {
try { try {
return linkedDeviceListenersByPlatform.get(UserAgentUtil.parseUserAgentString(userAgent).getPlatform()); return linkedDeviceListenersByPlatform.get(UserAgentUtil.parseUserAgentString(userAgent).platform());
} catch (final UnrecognizedUserAgentException ignored) { } catch (final UnrecognizedUserAgentException ignored) {
return linkedDeviceListenersForUnrecognizedPlatforms; return linkedDeviceListenersForUnrecognizedPlatforms;
} }

View File

@ -428,7 +428,7 @@ public class OneTimeDonationController {
@Nullable @Nullable
private static ClientPlatform getClientPlatform(@Nullable final String userAgentString) { private static ClientPlatform getClientPlatform(@Nullable final String userAgentString) {
try { try {
return UserAgentUtil.parseUserAgentString(userAgentString).getPlatform(); return UserAgentUtil.parseUserAgentString(userAgentString).platform();
} catch (final UnrecognizedUserAgentException e) { } catch (final UnrecognizedUserAgentException e) {
return null; return null;
} }

View File

@ -755,7 +755,7 @@ public class SubscriptionController {
@Nullable @Nullable
private static ClientPlatform getClientPlatform(@Nullable final String userAgentString) { private static ClientPlatform getClientPlatform(@Nullable final String userAgentString) {
try { try {
return UserAgentUtil.parseUserAgentString(userAgentString).getPlatform(); return UserAgentUtil.parseUserAgentString(userAgentString).platform();
} catch (final UnrecognizedUserAgentException e) { } catch (final UnrecognizedUserAgentException e) {
return null; return null;
} }

View File

@ -108,28 +108,28 @@ public class RemoteDeprecationFilter implements Filter, ServerInterceptor {
return true; return true;
} }
if (blockedVersionsByPlatform.containsKey(userAgent.getPlatform())) { if (blockedVersionsByPlatform.containsKey(userAgent.platform())) {
if (blockedVersionsByPlatform.get(userAgent.getPlatform()).contains(userAgent.getVersion())) { if (blockedVersionsByPlatform.get(userAgent.platform()).contains(userAgent.version())) {
recordDeprecation(userAgent, BLOCKED_CLIENT_REASON); recordDeprecation(userAgent, BLOCKED_CLIENT_REASON);
shouldBlock = true; shouldBlock = true;
} }
} }
if (minimumVersionsByPlatform.containsKey(userAgent.getPlatform())) { if (minimumVersionsByPlatform.containsKey(userAgent.platform())) {
if (userAgent.getVersion().isLowerThan(minimumVersionsByPlatform.get(userAgent.getPlatform()))) { if (userAgent.version().isLowerThan(minimumVersionsByPlatform.get(userAgent.platform()))) {
recordDeprecation(userAgent, EXPIRED_CLIENT_REASON); recordDeprecation(userAgent, EXPIRED_CLIENT_REASON);
shouldBlock = true; shouldBlock = true;
} }
} }
if (versionsPendingBlockByPlatform.containsKey(userAgent.getPlatform())) { if (versionsPendingBlockByPlatform.containsKey(userAgent.platform())) {
if (versionsPendingBlockByPlatform.get(userAgent.getPlatform()).contains(userAgent.getVersion())) { if (versionsPendingBlockByPlatform.get(userAgent.platform()).contains(userAgent.version())) {
recordPendingDeprecation(userAgent, BLOCKED_CLIENT_REASON); recordPendingDeprecation(userAgent, BLOCKED_CLIENT_REASON);
} }
} }
if (versionsPendingDeprecationByPlatform.containsKey(userAgent.getPlatform())) { if (versionsPendingDeprecationByPlatform.containsKey(userAgent.platform())) {
if (userAgent.getVersion().isLowerThan(versionsPendingDeprecationByPlatform.get(userAgent.getPlatform()))) { if (userAgent.version().isLowerThan(versionsPendingDeprecationByPlatform.get(userAgent.platform()))) {
recordPendingDeprecation(userAgent, EXPIRED_CLIENT_REASON); recordPendingDeprecation(userAgent, EXPIRED_CLIENT_REASON);
} }
} }
@ -139,13 +139,13 @@ public class RemoteDeprecationFilter implements Filter, ServerInterceptor {
private void recordDeprecation(final UserAgent userAgent, final String reason) { private void recordDeprecation(final UserAgent userAgent, final String reason) {
Metrics.counter(DEPRECATED_CLIENT_COUNTER_NAME, Metrics.counter(DEPRECATED_CLIENT_COUNTER_NAME,
PLATFORM_TAG, userAgent != null ? userAgent.getPlatform().name().toLowerCase() : "unrecognized", PLATFORM_TAG, userAgent != null ? userAgent.platform().name().toLowerCase() : "unrecognized",
REASON_TAG_NAME, reason).increment(); REASON_TAG_NAME, reason).increment();
} }
private void recordPendingDeprecation(final UserAgent userAgent, final String reason) { private void recordPendingDeprecation(final UserAgent userAgent, final String reason) {
Metrics.counter(PENDING_DEPRECATION_COUNTER_NAME, Metrics.counter(PENDING_DEPRECATION_COUNTER_NAME,
PLATFORM_TAG, userAgent.getPlatform().name().toLowerCase(), PLATFORM_TAG, userAgent.platform().name().toLowerCase(),
REASON_TAG_NAME, reason).increment(); REASON_TAG_NAME, reason).increment();
} }
} }

View File

@ -15,8 +15,6 @@ import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.core.SecurityContext; import jakarta.ws.rs.core.SecurityContext;
import java.io.IOException; import java.io.IOException;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.AuthenticatedDevice; import org.whispersystems.textsecuregcm.auth.AuthenticatedDevice;
@ -70,8 +68,8 @@ public class RestDeprecationFilter implements ContainerRequestFilter {
try { try {
final UserAgent userAgent = UserAgentUtil.parseUserAgentString(userAgentString); final UserAgent userAgent = UserAgentUtil.parseUserAgentString(userAgentString);
final ClientPlatform platform = userAgent.getPlatform(); final ClientPlatform platform = userAgent.platform();
final Semver version = userAgent.getVersion(); final Semver version = userAgent.version();
if (!minimumRestFreeVersion.containsKey(platform)) { if (!minimumRestFreeVersion.containsKey(platform)) {
return; return;
} }

View File

@ -67,7 +67,7 @@ public class OpenWebSocketCounter {
try { try {
final ClientPlatform clientPlatform = final ClientPlatform clientPlatform =
UserAgentUtil.parseUserAgentString(context.getClient().getUserAgent()).getPlatform(); UserAgentUtil.parseUserAgentString(context.getClient().getUserAgent()).platform();
calculatedOpenWebSocketCounter = openWebsocketsByClientPlatform.get(clientPlatform); calculatedOpenWebSocketCounter = openWebsocketsByClientPlatform.get(clientPlatform);
calculatedDurationTimer = durationTimersByClientPlatform.get(clientPlatform); calculatedDurationTimer = durationTimersByClientPlatform.get(clientPlatform);

View File

@ -9,6 +9,7 @@ import io.micrometer.core.instrument.Tag;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import org.apache.commons.lang3.StringUtils;
import org.whispersystems.textsecuregcm.WhisperServerVersion; import org.whispersystems.textsecuregcm.WhisperServerVersion;
import org.whispersystems.textsecuregcm.storage.ClientReleaseManager; import org.whispersystems.textsecuregcm.storage.ClientReleaseManager;
import org.whispersystems.textsecuregcm.util.ua.UnrecognizedUserAgentException; import org.whispersystems.textsecuregcm.util.ua.UnrecognizedUserAgentException;
@ -48,15 +49,15 @@ public class UserAgentTagUtil {
} }
public static Tag getPlatformTag(@Nullable final UserAgent userAgent) { public static Tag getPlatformTag(@Nullable final UserAgent userAgent) {
return Tag.of(PLATFORM_TAG, userAgent != null ? userAgent.getPlatform().name().toLowerCase() : "unrecognized"); return Tag.of(PLATFORM_TAG, userAgent != null ? userAgent.platform().name().toLowerCase() : "unrecognized");
} }
public static Optional<Tag> getClientVersionTag(final String userAgentString, final ClientReleaseManager clientReleaseManager) { public static Optional<Tag> getClientVersionTag(final String userAgentString, final ClientReleaseManager clientReleaseManager) {
try { try {
final UserAgent userAgent = UserAgentUtil.parseUserAgentString(userAgentString); final UserAgent userAgent = UserAgentUtil.parseUserAgentString(userAgentString);
if (clientReleaseManager.isVersionActive(userAgent.getPlatform(), userAgent.getVersion())) { if (clientReleaseManager.isVersionActive(userAgent.platform(), userAgent.version())) {
return Optional.of(Tag.of(VERSION_TAG, userAgent.getVersion().toString())); return Optional.of(Tag.of(VERSION_TAG, userAgent.version().toString()));
} }
} catch (final UnrecognizedUserAgentException ignored) { } catch (final UnrecognizedUserAgentException ignored) {
} }
@ -70,10 +71,8 @@ public class UserAgentTagUtil {
try { try {
final UserAgent userAgent = UserAgentUtil.parseUserAgentString(userAgentString); final UserAgent userAgent = UserAgentUtil.parseUserAgentString(userAgentString);
platform = userAgent.getPlatform().name().toLowerCase(); platform = userAgent.platform().name().toLowerCase();
libsignal = userAgent.getAdditionalSpecifiers() libsignal = StringUtils.contains(userAgent.additionalSpecifiers(), "libsignal");
.map(additionalSpecifiers -> additionalSpecifiers.contains("libsignal"))
.orElse(false);
} catch (final UnrecognizedUserAgentException e) { } catch (final UnrecognizedUserAgentException e) {
platform = "unrecognized"; platform = "unrecognized";
libsignal = false; libsignal = false;

View File

@ -46,7 +46,7 @@ public class LoggingUnhandledExceptionMapper extends LoggingExceptionMapper<Thro
// streamline the user-agent if it is recognized // streamline the user-agent if it is recognized
final UserAgent ua = UserAgentUtil.parseUserAgentString(userAgent); final UserAgent ua = UserAgentUtil.parseUserAgentString(userAgent);
userAgent = String.format("%s %s", ua.getPlatform(), ua.getVersion()); userAgent = String.format("%s %s", ua.platform(), ua.version());
} catch (final UnrecognizedUserAgentException ignored) { } catch (final UnrecognizedUserAgentException ignored) {
} catch (final Exception e) { } catch (final Exception e) {

View File

@ -6,58 +6,8 @@
package org.whispersystems.textsecuregcm.util.ua; package org.whispersystems.textsecuregcm.util.ua;
import com.vdurmont.semver4j.Semver; import com.vdurmont.semver4j.Semver;
import javax.annotation.Nullable;
import java.util.Objects; import java.util.Objects;
import java.util.Optional;
public class UserAgent { public record UserAgent(ClientPlatform platform, Semver version, @Nullable String additionalSpecifiers) {
private final ClientPlatform platform;
private final Semver version;
private final String additionalSpecifiers;
public UserAgent(final ClientPlatform platform, final Semver version) {
this(platform, version, null);
}
public UserAgent(final ClientPlatform platform, final Semver version, final String additionalSpecifiers) {
this.platform = platform;
this.version = version;
this.additionalSpecifiers = additionalSpecifiers;
}
public ClientPlatform getPlatform() {
return platform;
}
public Semver getVersion() {
return version;
}
public Optional<String> getAdditionalSpecifiers() {
return Optional.ofNullable(additionalSpecifiers);
}
@Override
public boolean equals(final Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final UserAgent userAgent = (UserAgent)o;
return platform == userAgent.platform &&
version.equals(userAgent.version) &&
Objects.equals(additionalSpecifiers, userAgent.additionalSpecifiers);
}
@Override
public int hashCode() {
return Objects.hash(platform, version, additionalSpecifiers);
}
@Override
public String toString() {
return "UserAgent{" +
"platform=" + platform +
", version=" + version +
", additionalSpecifiers='" + additionalSpecifiers + '\'' +
'}';
}
} }

View File

@ -1,6 +1,7 @@
package org.whispersystems.textsecuregcm.grpc; package org.whispersystems.textsecuregcm.grpc;
import io.grpc.stub.StreamObserver; import io.grpc.stub.StreamObserver;
import org.apache.commons.lang3.StringUtils;
import org.signal.chat.rpc.GetAuthenticatedDeviceRequest; import org.signal.chat.rpc.GetAuthenticatedDeviceRequest;
import org.signal.chat.rpc.GetAuthenticatedDeviceResponse; import org.signal.chat.rpc.GetAuthenticatedDeviceResponse;
import org.signal.chat.rpc.GetRequestAttributesRequest; import org.signal.chat.rpc.GetRequestAttributesRequest;
@ -28,9 +29,9 @@ public class RequestAttributesServiceImpl extends RequestAttributesGrpc.RequestA
responseBuilder.setRemoteAddress(RequestAttributesUtil.getRemoteAddress().getHostAddress()); responseBuilder.setRemoteAddress(RequestAttributesUtil.getRemoteAddress().getHostAddress());
RequestAttributesUtil.getUserAgent().ifPresent(userAgent -> responseBuilder.setUserAgent(UserAgent.newBuilder() RequestAttributesUtil.getUserAgent().ifPresent(userAgent -> responseBuilder.setUserAgent(UserAgent.newBuilder()
.setPlatform(userAgent.getPlatform().toString()) .setPlatform(userAgent.platform().toString())
.setVersion(userAgent.getVersion().toString()) .setVersion(userAgent.version().toString())
.setAdditionalSpecifiers(userAgent.getAdditionalSpecifiers().orElse("")) .setAdditionalSpecifiers(StringUtils.stripToEmpty(userAgent.additionalSpecifiers()))
.build())); .build()));
RequestAttributesUtil.getRawUserAgent().ifPresent(responseBuilder::setRawUserAgent); RequestAttributesUtil.getRawUserAgent().ifPresent(responseBuilder::setRawUserAgent);

View File

@ -42,18 +42,18 @@ class UserAgentUtilTest {
Arguments.of("This is obviously not a reasonable User-Agent string.", null), Arguments.of("This is obviously not a reasonable User-Agent string.", null),
Arguments.of("Signal-Android/4.68.3 Android/25", Arguments.of("Signal-Android/4.68.3 Android/25",
new UserAgent(ClientPlatform.ANDROID, new Semver("4.68.3"), "Android/25")), new UserAgent(ClientPlatform.ANDROID, new Semver("4.68.3"), "Android/25")),
Arguments.of("Signal-Android/4.68.3", new UserAgent(ClientPlatform.ANDROID, new Semver("4.68.3"))), Arguments.of("Signal-Android/4.68.3", new UserAgent(ClientPlatform.ANDROID, new Semver("4.68.3"), null)),
Arguments.of("Signal-Desktop/1.2.3 Linux", new UserAgent(ClientPlatform.DESKTOP, new Semver("1.2.3"), "Linux")), Arguments.of("Signal-Desktop/1.2.3 Linux", new UserAgent(ClientPlatform.DESKTOP, new Semver("1.2.3"), "Linux")),
Arguments.of("Signal-Desktop/1.2.3 macOS", new UserAgent(ClientPlatform.DESKTOP, new Semver("1.2.3"), "macOS")), Arguments.of("Signal-Desktop/1.2.3 macOS", new UserAgent(ClientPlatform.DESKTOP, new Semver("1.2.3"), "macOS")),
Arguments.of("Signal-Desktop/1.2.3 Windows", Arguments.of("Signal-Desktop/1.2.3 Windows",
new UserAgent(ClientPlatform.DESKTOP, new Semver("1.2.3"), "Windows")), new UserAgent(ClientPlatform.DESKTOP, new Semver("1.2.3"), "Windows")),
Arguments.of("Signal-Desktop/1.2.3", new UserAgent(ClientPlatform.DESKTOP, new Semver("1.2.3"))), Arguments.of("Signal-Desktop/1.2.3", new UserAgent(ClientPlatform.DESKTOP, new Semver("1.2.3"), null)),
Arguments.of("Signal-Desktop/1.32.0-beta.3", Arguments.of("Signal-Desktop/1.32.0-beta.3",
new UserAgent(ClientPlatform.DESKTOP, new Semver("1.32.0-beta.3"))), new UserAgent(ClientPlatform.DESKTOP, new Semver("1.32.0-beta.3"), null)),
Arguments.of("Signal-iOS/3.9.0 (iPhone; iOS 12.2; Scale/3.00)", Arguments.of("Signal-iOS/3.9.0 (iPhone; iOS 12.2; Scale/3.00)",
new UserAgent(ClientPlatform.IOS, new Semver("3.9.0"), "(iPhone; iOS 12.2; Scale/3.00)")), new UserAgent(ClientPlatform.IOS, new Semver("3.9.0"), "(iPhone; iOS 12.2; Scale/3.00)")),
Arguments.of("Signal-iOS/3.9.0 iOS/14.2", new UserAgent(ClientPlatform.IOS, new Semver("3.9.0"), "iOS/14.2")), Arguments.of("Signal-iOS/3.9.0 iOS/14.2", new UserAgent(ClientPlatform.IOS, new Semver("3.9.0"), "iOS/14.2")),
Arguments.of("Signal-iOS/3.9.0", new UserAgent(ClientPlatform.IOS, new Semver("3.9.0"))), Arguments.of("Signal-iOS/3.9.0", new UserAgent(ClientPlatform.IOS, new Semver("3.9.0"), null)),
Arguments.of("Signal-Android/7.11.23-nightly-1982-06-28-07-07-07 tonic/0.31", Arguments.of("Signal-Android/7.11.23-nightly-1982-06-28-07-07-07 tonic/0.31",
new UserAgent(ClientPlatform.ANDROID, new Semver("7.11.23-nightly-1982-06-28-07-07-07"), "tonic/0.31")), new UserAgent(ClientPlatform.ANDROID, new Semver("7.11.23-nightly-1982-06-28-07-07-07"), "tonic/0.31")),
Arguments.of("Signal-Android/7.11.23-nightly-1982-06-28-07-07-07 Android/42 tonic/0.31", Arguments.of("Signal-Android/7.11.23-nightly-1982-06-28-07-07-07 Android/42 tonic/0.31",