Skip to content

Commit

Permalink
Fix #223: Improve User-Agent parsing and add tests (#224)
Browse files Browse the repository at this point in the history
* Fix #223: Improve User-Agent parsing and add rests

* Reformat code

* Add test data

* Refactor to parametrized test

* Add surefire

* Add failure message

* Multiple prefixes

* Make v1 elements optional

* Parse network, os, osVersion and model from another examples

* Rename constant

* Prefer \\d

* Explicit operator precedence

* Extract sub patterns

---------

Co-authored-by: Lubos Racansky <[email protected]>
  • Loading branch information
petrdvorak and banterCZ authored Dec 11, 2023
1 parent 75fed3d commit 858e37c
Show file tree
Hide file tree
Showing 6 changed files with 143 additions and 29 deletions.
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

0 comments on commit 858e37c

Please sign in to comment.