From 40684a93a21fc4bd326e5fa7b964c0618d94b8f4 Mon Sep 17 00:00:00 2001 From: Jon Chambers Date: Mon, 3 Aug 2020 15:20:29 -0400 Subject: [PATCH] Restrict user-agent version matching to a more confined space. --- .../metrics/UserAgentTagUtil.java | 73 ++++++------------- .../metrics/UserAgentTagUtilTest.java | 61 +++++++--------- 2 files changed, 51 insertions(+), 83 deletions(-) diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/metrics/UserAgentTagUtil.java b/service/src/main/java/org/whispersystems/textsecuregcm/metrics/UserAgentTagUtil.java index aa4c91850..a2b66c8f7 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/metrics/UserAgentTagUtil.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/metrics/UserAgentTagUtil.java @@ -5,6 +5,7 @@ import org.whispersystems.textsecuregcm.util.Pair; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.regex.Matcher; @@ -15,66 +16,38 @@ import java.util.regex.Pattern; */ public class UserAgentTagUtil { - static final int MAX_VERSIONS = 10_000; + public static final String PLATFORM_TAG = "platform"; + public static final String VERSION_TAG = "clientVersion"; + static final List OVERFLOW_TAGS = List.of(Tag.of(PLATFORM_TAG, "overflow"), Tag.of(VERSION_TAG, "overflow")); + static final List UNRECOGNIZED_TAGS = List.of(Tag.of(PLATFORM_TAG, "unrecognized"), Tag.of(VERSION_TAG, "unrecognized")); - public static final String PLATFORM_TAG = "platform"; - public static final String VERSION_TAG = "clientVersion"; + private static final Map PATTERNS_BY_PLATFORM = Map.of( + "android", Pattern.compile("^Signal-Android (4[^ ]+).*$", Pattern.CASE_INSENSITIVE), + "desktop", Pattern.compile("^Signal Desktop (1[^ ]+).*$", Pattern.CASE_INSENSITIVE), + "ios", Pattern.compile("^Signal/(3[^ ]+) \\(.*ios.*\\)$", Pattern.CASE_INSENSITIVE)); - static final List OVERFLOW_TAGS = List.of(Tag.of(PLATFORM_TAG, "overflow"), Tag.of(VERSION_TAG, "overflow")); - static final List UNRECOGNIZED_TAGS = List.of(Tag.of(PLATFORM_TAG, "unrecognized"), Tag.of(VERSION_TAG, "unrecognized")); - - private static final Pattern USER_AGENT_PATTERN = Pattern.compile("^Signal[ \\-]([^ ]+) ([^ ]+).*$", Pattern.CASE_INSENSITIVE); - private static final Pattern IOS_USER_AGENT_PATTERN = Pattern.compile("^Signal/([^ ]+) \\(.*ios.*\\)$", Pattern.CASE_INSENSITIVE); - - private static final Set> SEEN_VERSIONS = new HashSet<>(); + static final int MAX_VERSIONS = 10_000; + private static final Set> SEEN_VERSIONS = new HashSet<>(); private UserAgentTagUtil() { } public static List getUserAgentTags(final String userAgent) { - final List tags; + if (userAgent != null) { + for (final Map.Entry entry : PATTERNS_BY_PLATFORM.entrySet()) { + final String platform = entry.getKey(); + final Pattern pattern = entry.getValue(); + final Matcher matcher = pattern.matcher(userAgent); - if (userAgent == null) { - tags = UNRECOGNIZED_TAGS; - } else { - tags = getAndroidOrDesktopUserAgentTags(userAgent) - .orElseGet(() -> getIOSUserAgentTags(userAgent) - .orElse(UNRECOGNIZED_TAGS)); + if (matcher.matches()) { + final String version = matcher.group(1); + + return allowVersion(platform, version) ? List.of(Tag.of(PLATFORM_TAG, platform), Tag.of(VERSION_TAG, version)) : OVERFLOW_TAGS; + } + } } - return tags; - } - - private static Optional> getAndroidOrDesktopUserAgentTags(final String userAgent) { - final Matcher matcher = USER_AGENT_PATTERN.matcher(userAgent); - final Optional> maybeTags; - - if (matcher.matches()) { - final String platform = matcher.group(1).toLowerCase(); - final String version = matcher.group(2); - - maybeTags = Optional.of(allowVersion(platform, version) ? List.of(Tag.of(PLATFORM_TAG, platform), Tag.of(VERSION_TAG, version)) : OVERFLOW_TAGS); - } else { - maybeTags = Optional.empty(); - } - - return maybeTags; - } - - private static Optional> getIOSUserAgentTags(final String userAgent) { - final Matcher matcher = IOS_USER_AGENT_PATTERN.matcher(userAgent); - final Optional> maybeTags; - - if (matcher.matches()) { - final String platform = "ios"; - final String version = matcher.group(1); - - maybeTags = Optional.of(allowVersion(platform, version) ? List.of(Tag.of(PLATFORM_TAG, platform), Tag.of(VERSION_TAG, version)) : OVERFLOW_TAGS); - } else { - maybeTags = Optional.empty(); - } - - return maybeTags; + return UNRECOGNIZED_TAGS; } private static boolean allowVersion(final String platform, final String version) { diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/metrics/UserAgentTagUtilTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/metrics/UserAgentTagUtilTest.java index f9545648a..172d42f7e 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/metrics/UserAgentTagUtilTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/metrics/UserAgentTagUtilTest.java @@ -1,59 +1,54 @@ package org.whispersystems.textsecuregcm.metrics; import io.micrometer.core.instrument.Tag; +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; import org.junit.Test; +import org.junit.runner.RunWith; +import java.util.HashSet; import java.util.List; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +@RunWith(JUnitParamsRunner.class) public class UserAgentTagUtilTest { @Test - public void testGetUserAgentTags() { - assertEquals(UserAgentTagUtil.UNRECOGNIZED_TAGS, - UserAgentTagUtil.getUserAgentTags("This is obviously not a reasonable User-Agent string.")); + @Parameters(method = "argumentsForTestGetUserAgentTags") + public void testGetUserAgentTags(final String userAgent, final List expectedTags) { + assertEquals(new HashSet<>(expectedTags), + new HashSet<>(UserAgentTagUtil.getUserAgentTags(userAgent))); + } - assertEquals(UserAgentTagUtil.UNRECOGNIZED_TAGS, UserAgentTagUtil.getUserAgentTags(null)); - - { - final List tags = UserAgentTagUtil.getUserAgentTags("Signal-Android 4.53.7 (Android 8.1)"); - - assertEquals(2, tags.size()); - assertTrue(tags.contains(Tag.of(UserAgentTagUtil.PLATFORM_TAG, "android"))); - assertTrue(tags.contains(Tag.of(UserAgentTagUtil.VERSION_TAG, "4.53.7"))); - } - - { - final List tags = UserAgentTagUtil.getUserAgentTags("Signal Desktop 1.2.3"); - - assertEquals(2, tags.size()); - assertTrue(tags.contains(Tag.of(UserAgentTagUtil.PLATFORM_TAG, "desktop"))); - assertTrue(tags.contains(Tag.of(UserAgentTagUtil.VERSION_TAG, "1.2.3"))); - } - - { - final List tags = UserAgentTagUtil.getUserAgentTags("Signal/3.9.0 (iPhone; iOS 12.2; Scale/3.00)"); - - assertEquals(2, tags.size()); - assertTrue(tags.contains(Tag.of(UserAgentTagUtil.PLATFORM_TAG, "ios"))); - assertTrue(tags.contains(Tag.of(UserAgentTagUtil.VERSION_TAG, "3.9.0"))); - } + @SuppressWarnings("unused") + private Object argumentsForTestGetUserAgentTags() { + return new Object[] { + new Object[] { "This is obviously not a reasonable User-Agent string.", UserAgentTagUtil.UNRECOGNIZED_TAGS }, + new Object[] { null, UserAgentTagUtil.UNRECOGNIZED_TAGS }, + new Object[] { "Signal-Android 4.53.7 (Android 8.1)", List.of(Tag.of(UserAgentTagUtil.PLATFORM_TAG, "android"), Tag.of(UserAgentTagUtil.VERSION_TAG, "4.53.7")) }, + new Object[] { "Signal Desktop 1.2.3", List.of(Tag.of(UserAgentTagUtil.PLATFORM_TAG, "desktop"), Tag.of(UserAgentTagUtil.VERSION_TAG, "1.2.3")) }, + new Object[] { "Signal/3.9.0 (iPhone; iOS 12.2; Scale/3.00)", List.of(Tag.of(UserAgentTagUtil.PLATFORM_TAG, "ios"), Tag.of(UserAgentTagUtil.VERSION_TAG, "3.9.0")) }, + new Object[] { "Signal-Android 1.2.3 (Android 8.1)", UserAgentTagUtil.UNRECOGNIZED_TAGS }, + new Object[] { "Signal Desktop 3.9.0", UserAgentTagUtil.UNRECOGNIZED_TAGS }, + new Object[] { "Signal/4.53.7 (iPhone; iOS 12.2; Scale/3.00)", UserAgentTagUtil.UNRECOGNIZED_TAGS }, + }; } @Test public void testGetUserAgentTagsFlooded() { for (int i = 0; i < UserAgentTagUtil.MAX_VERSIONS; i++) { - UserAgentTagUtil.getUserAgentTags(String.format("Signal-Android 1.0.%d (Android 8.1)", i)); + UserAgentTagUtil.getUserAgentTags(String.format("Signal-Android 4.0.%d (Android 8.1)", i)); } assertEquals(UserAgentTagUtil.OVERFLOW_TAGS, - UserAgentTagUtil.getUserAgentTags("Signal-Android 2.0.0 (Android 8.1)")); + UserAgentTagUtil.getUserAgentTags("Signal-Android 4.1.0 (Android 8.1)")); - final List tags = UserAgentTagUtil.getUserAgentTags("Signal-Android 1.0.0 (Android 8.1)"); + final List tags = UserAgentTagUtil.getUserAgentTags("Signal-Android 4.0.0 (Android 8.1)"); assertEquals(2, tags.size()); assertTrue(tags.contains(Tag.of(UserAgentTagUtil.PLATFORM_TAG, "android"))); - assertTrue(tags.contains(Tag.of(UserAgentTagUtil.VERSION_TAG, "1.0.0"))); + assertTrue(tags.contains(Tag.of(UserAgentTagUtil.VERSION_TAG, "4.0.0"))); } }