Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #223: Improve User-Agent parsing and add tests #224

Merged
merged 13 commits into from
Dec 11, 2023
2 changes: 1 addition & 1 deletion audit-base/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.2</version>
</plugin>
</plugins>
</build>
Expand Down
14 changes: 14 additions & 0 deletions http-common/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,20 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
* Utility class for processing our standard user agent strings.
*
* @author Petr Dvorak, [email protected]
* @author Lubos Racansky, [email protected]
*/
@Slf4j
public final class UserAgent {
Expand All @@ -43,15 +44,18 @@ public static class Device {
private String model;
}

private static final String PREFIX = "((^PowerAuthNetworking)|.*PowerAuth2)/(?<networkVersion>\\d+\\.\\d+\\.\\d+)";
private static final Pattern PATTERN_PREFIX = Pattern.compile(PREFIX + ".*");

private static final String LANGUAGE_AND_CONNECTION = "(\\((?<language>[a-zA-Z]{2}); (?<connection>[a-zA-Z0-9]+)\\) )?";
private static final String PRODUCT_AND_VERSION = "((?<product>[a-zA-Z0-9-_.]+)/(?<version>[0-9.]+(-[^ ]*)?) )?";
private static final String PLATFORM_OS_VERSION_MODEL = "(\\(((?<platform>[^;]+); )?(?<os>[^/ ]+)[/ ](?<osVersion>[^;,]+)[;,] (?<model>[^)]+)\\))?";
private static final Pattern PATTERN_V1 = Pattern.compile(PREFIX + " " + LANGUAGE_AND_CONNECTION + PRODUCT_AND_VERSION + PLATFORM_OS_VERSION_MODEL + ".*");

private UserAgent() {
throw new IllegalStateException("Should not be instantiated");
}

private static final Pattern patternPrefix = Pattern.compile("^PowerAuthNetworking/(?<networkVersion>[0-9]+\\.[0-9]+\\.[0-9]+).*");
private static final Pattern patternV1 = Pattern.compile("^PowerAuthNetworking/(?<networkVersion>[0-9]+\\.[0-9]+\\.[0-9]+) " +
"\\((?<language>[a-zA-Z]{2}); (?<connection>[a-zA-Z0-9]+)\\) " +
"(?<product>[a-zA-Z0-9-_.]+)/(?<version>[0-9.]+) .*" +
"\\((?<platform>[^;]+); (?<os>[^/]+)/(?<osVersion>[^;]+); (?<model>[^)]+)\\)$");

/**
* Parse client user from the HTTP header value.
*
Expand All @@ -61,7 +65,7 @@ private UserAgent() {
public static Optional<Device> parse(String userAgent) {
// Identify if the user agent is ours and in what version
logger.debug("Parsing user agent value: {}", userAgent);
final Matcher matcherPrefix = patternPrefix.matcher(userAgent);
final Matcher matcherPrefix = PATTERN_PREFIX.matcher(userAgent);
if (!matcherPrefix.matches()) {
return Optional.empty();
}
Expand All @@ -83,7 +87,7 @@ public static Optional<Device> parse(String userAgent) {
* @return Parsed device info, or empty if the user agent header cannot be parsed.
*/
private static Optional<Device> parseUserAgentV1(String userAgent) {
final Matcher matcher = patternV1.matcher(userAgent);
final Matcher matcher = PATTERN_V1.matcher(userAgent);
if (matcher.matches()) {
final Device device = new Device();
device.setNetworkVersion(matcher.group("networkVersion"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,35 +15,120 @@
*/
package com.wultra.core.http.common.headers;

import org.junit.jupiter.api.Test;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import java.util.stream.Stream;

import java.util.Optional;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

/**
* Test for the user agent parser.
* Test for {@link UserAgent}.
*
* @author Petr Dvorak, [email protected]
* @author Lubos Racansky, [email protected]
*/
class UserAgentTest {

@Test
void parse() {
final String sample = "PowerAuthNetworking/1.1.7 (en; cellular) com.wultra.app.Mobile-Token.wultra_test/2.0.0 (Apple; iOS/16.6.1; iphone12,3)";
final Optional<UserAgent.Device> deviceOptional = UserAgent.parse(sample);
assertTrue(deviceOptional.isPresent());

final UserAgent.Device device = deviceOptional.get();
assertEquals("1.1.7", device.getNetworkVersion());
assertEquals("en", device.getLanguage());
assertEquals("cellular", device.getConnection());
assertEquals("com.wultra.app.Mobile-Token.wultra_test", device.getProduct());
assertEquals("2.0.0", device.getVersion());
assertEquals("Apple", device.getPlatform());
assertEquals("iOS", device.getOs());
assertEquals("16.6.1", device.getOsVersion());
assertEquals("iphone12,3", device.getModel());
@ParameterizedTest
@MethodSource("provideUserAgents")
void testParse(final String userAgent, final UserAgent.Device expectedDevice) {
final Optional<UserAgent.Device> deviceOptional = UserAgent.parse(userAgent);
assertTrue(deviceOptional.isPresent(), "Unable to parse user-agent: " + userAgent);
assertEquals(expectedDevice, deviceOptional.get());
}

private static Stream<Arguments> provideUserAgents() throws JsonProcessingException {
return Stream.of(
Arguments.of("PowerAuthNetworking/1.1.7 (en; cellular) com.wultra.app.Mobile-Token.wultra_test/2.0.0 (Apple; iOS/16.6.1; iphone12,3)", readDevice("""
{
"networkVersion": "1.1.7",
"language": "en",
"connection": "cellular",
"product": "com.wultra.app.Mobile-Token.wultra_test",
"version": "2.0.0",
"platform": "Apple",
"os": "iOS",
"osVersion": "16.6.1",
"model": "iphone12,3"
}
""")),
Arguments.of("PowerAuthNetworking/1.2.1 (uk; wifi) com.wultra.android.mtoken.gdnexttest/1.0.0-gdnexttest (samsung; Android/13; SM-A047F)", readDevice("""
{
"networkVersion": "1.2.1",
"language": "uk",
"connection": "wifi",
"product": "com.wultra.android.mtoken.gdnexttest",
"version": "1.0.0-gdnexttest",
"platform": "samsung",
"os": "Android",
"osVersion": "13",
"model": "SM-A047F"
}
""")),
Arguments.of("PowerAuthNetworking/1.1.7 (en; unknown) com.wultra.app.MobileToken.wtest/2.0.0 (Apple; iOS/16.6.1; iphone10,6)", readDevice("""
{
"networkVersion": "1.1.7",
"language": "en",
"connection": "unknown",
"product": "com.wultra.app.MobileToken.wtest",
"version": "2.0.0",
"platform": "Apple",
"os": "iOS",
"osVersion": "16.6.1",
"model": "iphone10,6"
}
""")),
Arguments.of("PowerAuthNetworking/1.1.7 (en; wifi) com.wultra.app.MobileToken.wtest/2.0.0 (Apple; iOS/16.7.1; iphone10,6)", readDevice("""
{
"networkVersion": "1.1.7",
"language": "en",
"connection": "wifi",
"product": "com.wultra.app.MobileToken.wtest",
"version": "2.0.0",
"platform": "Apple",
"os": "iOS",
"osVersion": "16.7.1",
"model": "iphone10,6"
}
""")),
// MainBundle/Version PowerAuth2/Version (iOS Version, deviceString)
Arguments.of("PowerAuth2TestsHostApp-ios/1.0 PowerAuth2/1.7.8 (iOS 17.0, simulator)", readDevice("""
{
"networkVersion": "1.7.8",
"os": "iOS",
"osVersion": "17.0",
"model": "simulator"
}
""")),
// PowerAuth2/Version (Android Version, Build.MANUFACTURER Build.MODEL)
Arguments.of("PowerAuth2/1.7.8 (Android 13, Google Pixel 4)", readDevice("""
{
"networkVersion": "1.7.8",
"os": "Android",
"osVersion": "13",
"model": "Google Pixel 4"
}
""")),
Arguments.of("MobileToken/1.2.0 PowerAuth2/1.7.8 (iOS 15.7.9, iPhone9,3)", readDevice("""
{
"networkVersion": "1.7.8",
"os": "iOS",
"osVersion": "15.7.9",
"model": "iPhone9,3"
}
"""))
);
}
}

private static UserAgent.Device readDevice(final String json) throws JsonProcessingException {
return new ObjectMapper().readValue(json, UserAgent.Device.class);
}

}
11 changes: 11 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<maven-surefire-plugin.version>3.2.2</maven-surefire-plugin.version>

<!-- Dependencies -->
<spring-boot.version>3.1.6</spring-boot.version>
Expand Down Expand Up @@ -96,6 +97,16 @@
</dependencies>

<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${maven-surefire-plugin.version}</version>
</plugin>
</plugins>
</pluginManagement>

<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
Expand Down
2 changes: 1 addition & 1 deletion rest-client-base/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.2</version>
</plugin>
</plugins>
</build>
Expand Down