Add a general utility class for parsing user-agent strings.
This commit is contained in:
parent
b041fbe3ec
commit
baab6b951b
|
@ -0,0 +1,7 @@
|
|||
package org.whispersystems.textsecuregcm.util.ua;
|
||||
|
||||
public enum ClientPlatform {
|
||||
ANDROID,
|
||||
DESKTOP,
|
||||
IOS;
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package org.whispersystems.textsecuregcm.util.ua;
|
||||
|
||||
public class UnrecognizedUserAgentException extends Exception {
|
||||
|
||||
public UnrecognizedUserAgentException() {
|
||||
}
|
||||
|
||||
public UnrecognizedUserAgentException(final String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public UnrecognizedUserAgentException(final Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
package org.whispersystems.textsecuregcm.util.ua;
|
||||
|
||||
import com.vdurmont.semver4j.Semver;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
public class UserAgent {
|
||||
|
||||
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 + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
package org.whispersystems.textsecuregcm.util.ua;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.vdurmont.semver4j.Semver;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.EnumMap;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class UserAgentUtil {
|
||||
|
||||
private static final Pattern STANDARD_UA_PATTERN = Pattern.compile("^Signal-(Android|Desktop|iOS)/([^ ]+)( (.+))?$", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
private static final Map<ClientPlatform, Pattern> LEGACY_PATTERNS_BY_PLATFORM = new EnumMap<>(ClientPlatform.class);
|
||||
|
||||
static {
|
||||
LEGACY_PATTERNS_BY_PLATFORM.put(ClientPlatform.ANDROID, Pattern.compile("^Signal-Android ([^ ]+)( (.+))?$", Pattern.CASE_INSENSITIVE));
|
||||
LEGACY_PATTERNS_BY_PLATFORM.put(ClientPlatform.DESKTOP, Pattern.compile("^Signal Desktop (.+)$", Pattern.CASE_INSENSITIVE));
|
||||
LEGACY_PATTERNS_BY_PLATFORM.put(ClientPlatform.IOS, Pattern.compile("^Signal/([^ ]+)( (.+))?$", Pattern.CASE_INSENSITIVE));
|
||||
}
|
||||
|
||||
public static UserAgent parseUserAgentString(final String userAgentString) throws UnrecognizedUserAgentException {
|
||||
if (StringUtils.isBlank(userAgentString)) {
|
||||
throw new UnrecognizedUserAgentException("User-Agent string is blank");
|
||||
}
|
||||
|
||||
try {
|
||||
final UserAgent standardUserAgent = parseStandardUserAgentString(userAgentString);
|
||||
|
||||
if (standardUserAgent != null) {
|
||||
return standardUserAgent;
|
||||
}
|
||||
|
||||
final UserAgent legacyUserAgent = parseLegacyUserAgentString(userAgentString);
|
||||
|
||||
if (legacyUserAgent != null) {
|
||||
return legacyUserAgent;
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
throw new UnrecognizedUserAgentException(e);
|
||||
}
|
||||
|
||||
throw new UnrecognizedUserAgentException();
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static UserAgent parseStandardUserAgentString(final String userAgentString) {
|
||||
final Matcher matcher = STANDARD_UA_PATTERN.matcher(userAgentString);
|
||||
|
||||
if (matcher.matches()) {
|
||||
return new UserAgent(ClientPlatform.valueOf(matcher.group(1).toUpperCase()), new Semver(matcher.group(2)), StringUtils.stripToNull(matcher.group(4)));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static UserAgent parseLegacyUserAgentString(final String userAgentString) {
|
||||
for (final Map.Entry<ClientPlatform, Pattern> entry : LEGACY_PATTERNS_BY_PLATFORM.entrySet()) {
|
||||
final ClientPlatform platform = entry.getKey();
|
||||
final Pattern pattern = entry.getValue();
|
||||
final Matcher matcher = pattern.matcher(userAgentString);
|
||||
|
||||
if (matcher.matches()) {
|
||||
final UserAgent userAgent;
|
||||
|
||||
if (matcher.groupCount() > 1) {
|
||||
userAgent = new UserAgent(platform, new Semver(matcher.group(1)), StringUtils.stripToNull(matcher.group(matcher.groupCount())));
|
||||
} else {
|
||||
userAgent = new UserAgent(platform, new Semver(matcher.group(1)));
|
||||
}
|
||||
|
||||
return userAgent;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
package org.whispersystems.textsecuregcm.util.ua;
|
||||
|
||||
import com.vdurmont.semver4j.Semver;
|
||||
import junitparams.JUnitParamsRunner;
|
||||
import junitparams.Parameters;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
@RunWith(JUnitParamsRunner.class)
|
||||
public class UserAgentUtilTest {
|
||||
|
||||
@Test
|
||||
@Parameters(method = "argumentsForTestParseUserAgentString")
|
||||
public void testParseUserAgentString(final String userAgentString, final UserAgent expectedUserAgent) throws UnrecognizedUserAgentException {
|
||||
assertEquals(expectedUserAgent, UserAgentUtil.parseUserAgentString(userAgentString));
|
||||
}
|
||||
|
||||
private static Object argumentsForTestParseUserAgentString() {
|
||||
return new Object[] {
|
||||
new Object[] { "Signal-Android/4.68.3 Android/25", new UserAgent(ClientPlatform.ANDROID, new Semver("4.68.3"), "Android/25") },
|
||||
new Object[] { "Signal-Android 4.53.7 (Android 8.1)", new UserAgent(ClientPlatform.ANDROID, new Semver("4.53.7"), "(Android 8.1)") },
|
||||
};
|
||||
}
|
||||
|
||||
@Test
|
||||
@Parameters(method = "argumentsForTestParseBogusUserAgentString")
|
||||
public void testParseBogusUserAgentString(final String userAgentString) {
|
||||
assertThrows(UnrecognizedUserAgentException.class, () -> UserAgentUtil.parseUserAgentString(userAgentString));
|
||||
}
|
||||
|
||||
private static Object argumentsForTestParseBogusUserAgentString() {
|
||||
return new Object[] {
|
||||
null,
|
||||
"This is obviously not a reasonable User-Agent string.",
|
||||
"Signal-Android/4.6-8.3.unreasonableversionstring-17"
|
||||
};
|
||||
}
|
||||
|
||||
@Test
|
||||
@Parameters(method = "argumentsForTestParseStandardUserAgentString")
|
||||
public void testParseStandardUserAgentString(final String userAgentString, final UserAgent expectedUserAgent) {
|
||||
assertEquals(expectedUserAgent, UserAgentUtil.parseStandardUserAgentString(userAgentString));
|
||||
}
|
||||
|
||||
private static Object argumentsForTestParseStandardUserAgentString() {
|
||||
return new Object[] {
|
||||
new Object[] { "This is obviously not a reasonable User-Agent string.", null },
|
||||
new Object[] { "Signal-Android/4.68.3 Android/25", new UserAgent(ClientPlatform.ANDROID, new Semver("4.68.3"), "Android/25") },
|
||||
new Object[] { "Signal-Android/4.68.3", new UserAgent(ClientPlatform.ANDROID, new Semver("4.68.3")) },
|
||||
new Object[] { "Signal-Desktop/1.32.0-beta.3", new UserAgent(ClientPlatform.DESKTOP, new Semver("1.32.0-beta.3")) },
|
||||
new Object[] { "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 Object[] { "Signal-iOS/3.9.0", new UserAgent(ClientPlatform.IOS, new Semver("3.9.0")) }
|
||||
};
|
||||
}
|
||||
|
||||
@Test
|
||||
@Parameters(method = "argumentsForTestParseLegacyUserAgentString")
|
||||
public void testParseLegacyUserAgentString(final String userAgentString, final UserAgent expectedUserAgent) {
|
||||
assertEquals(expectedUserAgent, UserAgentUtil.parseLegacyUserAgentString(userAgentString));
|
||||
}
|
||||
|
||||
private static Object argumentsForTestParseLegacyUserAgentString() {
|
||||
return new Object[] {
|
||||
new Object[] { "This is obviously not a reasonable User-Agent string.", null },
|
||||
new Object[] { "Signal-Android 4.53.7 (Android 8.1)", new UserAgent(ClientPlatform.ANDROID, new Semver("4.53.7"), "(Android 8.1)") },
|
||||
new Object[] { "Signal Desktop 1.2.3", new UserAgent(ClientPlatform.DESKTOP, new Semver("1.2.3")) },
|
||||
new Object[] { "Signal Desktop 1.32.0-beta.3", new UserAgent(ClientPlatform.DESKTOP, new Semver("1.32.0-beta.3")) },
|
||||
new Object[] { "Signal/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)") }
|
||||
};
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue