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 new file mode 100644 index 0000000..eb97420 --- /dev/null +++ b/http-common/src/main/java/com/wultra/core/http/common/headers/UserAgent.java @@ -0,0 +1,98 @@ +/* + * 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 lombok.extern.slf4j.Slf4j; + +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 + */ +@Slf4j +public final 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 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 + 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; + } + + // 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 Matcher matcher = patternV1.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; + } + 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 new file mode 100644 index 0000000..5462b72 --- /dev/null +++ b/http-common/src/test/java/com/wultra/core/http/common/headers/UserAgentTest.java @@ -0,0 +1,45 @@ +/* + * 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; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * 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); + assertNotNull(device); + 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