From 6995cd4b58c7ee7f31e0b625d49c16a803c5d61e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Dvo=C5=99=C3=A1k?= Date: Mon, 25 Sep 2023 18:21:08 +0200 Subject: [PATCH 1/2] Fix #217: Add User-Agent header parser --- .../core/http/common/headers/UserAgent.java | 92 +++++++++++++++++++ .../http/common/headers/UserAgentTest.java | 44 +++++++++ 2 files changed, 136 insertions(+) create mode 100644 http-common/src/main/java/com/wultra/core/http/common/headers/UserAgent.java create mode 100644 http-common/src/test/java/com/wultra/core/http/common/headers/UserAgentTest.java diff --git a/http-common/src/main/java/com/wultra/core/http/common/headers/UserAgent.java b/http-common/src/main/java/com/wultra/core/http/common/headers/UserAgent.java new file mode 100644 index 0000000..a93dab5 --- /dev/null +++ b/http-common/src/main/java/com/wultra/core/http/common/headers/UserAgent.java @@ -0,0 +1,92 @@ +/* + * Copyright 2023 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.wultra.core.http.common.headers; + +import lombok.Data; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Utility class for processing our standard user agent strings. + * + * @author Petr Dvorak, petr@wultra.com + */ +public class UserAgent { + + @Data + public static class Device { + private String networkVersion; + private String language; + private String connection; + private String product; + private String version; + private String platform; + private String os; + private String osVersion; + private String model; + } + + private static final String USER_AGENT_TEMPLATE_PREFIX_V1 = "^PowerAuthNetworking/(?[0-9]+\\.[0-9]+\\.[0-9]+).*"; + private static final String USER_AGENT_TEMPLATE_V1 = "^PowerAuthNetworking/(?[0-9]+\\.[0-9]+\\.[0-9]+) " + + "\\((?[a-zA-Z]{2}); (?[a-zA-Z0-9]+)\\) " + + "(?[a-zA-Z0-9-_.]+)/(?[0-9.]+) .*" + + "\\((?[^;]+); (?[^/]+)/(?[^;]+); (?[^)]+)\\)$"; + + public static Device parse(String userAgent) { + // Identify if the user agent is ours and in what version + final Pattern patternPrefix = Pattern.compile(USER_AGENT_TEMPLATE_PREFIX_V1); + final Matcher matcherPrefix = patternPrefix.matcher(userAgent); + if (!matcherPrefix.matches()) { + return null; + } + final String networkVersion = matcherPrefix.group("networkVersion"); + if (!networkVersion.startsWith("1.")) { // simplistic matching for current v1.x clients + return null; + } + + // Parse the device object + return parseUserAgentV1(userAgent); + + } + + /** + * Private method for parsing client user from the v1.x mobile clients. It is added for convenience + * when new versions with another formats will be eventually introduced. + * + * @param userAgent User-Agent Header String + * @return Parsed device info, or null if the user agent header cannot be parsed. + */ + private static Device parseUserAgentV1(String userAgent) { + final Pattern pattern = Pattern.compile(USER_AGENT_TEMPLATE_V1); + final Matcher matcher = pattern.matcher(userAgent); + if (matcher.matches()) { + final Device device = new Device(); + device.setNetworkVersion(matcher.group("networkVersion")); + device.setLanguage(matcher.group("language")); + device.setConnection(matcher.group("connection")); + device.setProduct(matcher.group("product")); + device.setVersion(matcher.group("version")); + device.setPlatform(matcher.group("platform")); + device.setOs(matcher.group("os")); + device.setOsVersion(matcher.group("osVersion")); + device.setModel(matcher.group("model")); + return device; + } + return null; + } + +} diff --git a/http-common/src/test/java/com/wultra/core/http/common/headers/UserAgentTest.java b/http-common/src/test/java/com/wultra/core/http/common/headers/UserAgentTest.java new file mode 100644 index 0000000..7ea3eb1 --- /dev/null +++ b/http-common/src/test/java/com/wultra/core/http/common/headers/UserAgentTest.java @@ -0,0 +1,44 @@ +/* + * Copyright 2023 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.wultra.core.http.common.headers; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Test for the user agent parser. + * + * @author Petr Dvorak, petr@wultra.com + */ +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 UserAgent.Device device = UserAgent.parse(sample); + assert device != null; + 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()); + } +} \ No newline at end of file From 821b713b5078bf626cdbaf7b3373314d0f531188 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Dvo=C5=99=C3=A1k?= Date: Tue, 26 Sep 2023 15:01:42 +0200 Subject: [PATCH 2/2] Fix comments from code review --- http-common/pom.xml | 4 ++++ .../core/http/common/headers/UserAgent.java | 20 ++++++++++++------- .../http/common/headers/UserAgentTest.java | 3 ++- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/http-common/pom.xml b/http-common/pom.xml index b04ce0b..c53e75b 100644 --- a/http-common/pom.xml +++ b/http-common/pom.xml @@ -19,6 +19,10 @@ jakarta.servlet-api provided + + org.slf4j + slf4j-api + org.springframework.boot diff --git a/http-common/src/main/java/com/wultra/core/http/common/headers/UserAgent.java b/http-common/src/main/java/com/wultra/core/http/common/headers/UserAgent.java index a93dab5..eb97420 100644 --- a/http-common/src/main/java/com/wultra/core/http/common/headers/UserAgent.java +++ b/http-common/src/main/java/com/wultra/core/http/common/headers/UserAgent.java @@ -16,6 +16,7 @@ package com.wultra.core.http.common.headers; import lombok.Data; +import lombok.extern.slf4j.Slf4j; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -25,7 +26,8 @@ * * @author Petr Dvorak, petr@wultra.com */ -public class UserAgent { +@Slf4j +public final class UserAgent { @Data public static class Device { @@ -40,20 +42,24 @@ public static class Device { private String model; } - private static final String USER_AGENT_TEMPLATE_PREFIX_V1 = "^PowerAuthNetworking/(?[0-9]+\\.[0-9]+\\.[0-9]+).*"; - private static final String USER_AGENT_TEMPLATE_V1 = "^PowerAuthNetworking/(?[0-9]+\\.[0-9]+\\.[0-9]+) " + + private UserAgent() { + } + + private static final Pattern patternPrefix = Pattern.compile("^PowerAuthNetworking/(?[0-9]+\\.[0-9]+\\.[0-9]+).*"); + private static final Pattern patternV1 = Pattern.compile("^PowerAuthNetworking/(?[0-9]+\\.[0-9]+\\.[0-9]+) " + "\\((?[a-zA-Z]{2}); (?[a-zA-Z0-9]+)\\) " + "(?[a-zA-Z0-9-_.]+)/(?[0-9.]+) .*" + - "\\((?[^;]+); (?[^/]+)/(?[^;]+); (?[^)]+)\\)$"; + "\\((?[^;]+); (?[^/]+)/(?[^;]+); (?[^)]+)\\)$"); public static Device parse(String userAgent) { // Identify if the user agent is ours and in what version - final Pattern patternPrefix = Pattern.compile(USER_AGENT_TEMPLATE_PREFIX_V1); + logger.debug("Parsing user agent value: {}", userAgent); final Matcher matcherPrefix = patternPrefix.matcher(userAgent); if (!matcherPrefix.matches()) { return null; } final String networkVersion = matcherPrefix.group("networkVersion"); + logger.debug("Declared networkVersion: {}", networkVersion); if (!networkVersion.startsWith("1.")) { // simplistic matching for current v1.x clients return null; } @@ -71,8 +77,7 @@ public static Device parse(String userAgent) { * @return Parsed device info, or null if the user agent header cannot be parsed. */ private static Device parseUserAgentV1(String userAgent) { - final Pattern pattern = Pattern.compile(USER_AGENT_TEMPLATE_V1); - final Matcher matcher = pattern.matcher(userAgent); + final Matcher matcher = patternV1.matcher(userAgent); if (matcher.matches()) { final Device device = new Device(); device.setNetworkVersion(matcher.group("networkVersion")); @@ -86,6 +91,7 @@ private static Device parseUserAgentV1(String userAgent) { device.setModel(matcher.group("model")); return device; } + logger.debug("The user agent value does not match v1 client format"); return null; } diff --git a/http-common/src/test/java/com/wultra/core/http/common/headers/UserAgentTest.java b/http-common/src/test/java/com/wultra/core/http/common/headers/UserAgentTest.java index 7ea3eb1..5462b72 100644 --- a/http-common/src/test/java/com/wultra/core/http/common/headers/UserAgentTest.java +++ b/http-common/src/test/java/com/wultra/core/http/common/headers/UserAgentTest.java @@ -18,6 +18,7 @@ import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; /** * Test for the user agent parser. @@ -30,7 +31,7 @@ class UserAgentTest { 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 UserAgent.Device device = UserAgent.parse(sample); - assert device != null; + assertNotNull(device); assertEquals("1.1.7", device.getNetworkVersion()); assertEquals("en", device.getLanguage()); assertEquals("cellular", device.getConnection());