diff --git a/annotations/pom.xml b/annotations/pom.xml index 28c43e0..ba7f01a 100644 --- a/annotations/pom.xml +++ b/annotations/pom.xml @@ -7,7 +7,7 @@ io.getlime.core lime-java-core-parent - 1.7.0 + 1.8.0 annotations diff --git a/audit-base/pom.xml b/audit-base/pom.xml index dc8138a..bbccf8b 100644 --- a/audit-base/pom.xml +++ b/audit-base/pom.xml @@ -6,7 +6,7 @@ io.getlime.core lime-java-core-parent - 1.7.0 + 1.8.0 audit-base @@ -59,8 +59,8 @@ + org.apache.maven.plugins maven-surefire-plugin - 3.1.2 diff --git a/bom/pom.xml b/bom/pom.xml new file mode 100644 index 0000000..5bd68f3 --- /dev/null +++ b/bom/pom.xml @@ -0,0 +1,51 @@ + + + 4.0.0 + + + io.getlime.core + lime-java-core-parent + 1.8.0 + + + core-bom + Bill of Materials for Wultra Core + pom + + + + + io.getlime.core + annotations + ${project.version} + + + + io.getlime.core + audit-base + ${project.version} + + + + io.getlime.core + http-common + ${project.version} + + + + io.getlime.core + rest-client-base + ${project.version} + + + + io.getlime.core + rest-model-base + ${project.version} + + + + + \ No newline at end of file diff --git a/http-common/pom.xml b/http-common/pom.xml index 25798d3..d2e6bff 100644 --- a/http-common/pom.xml +++ b/http-common/pom.xml @@ -7,7 +7,7 @@ io.getlime.core lime-java-core-parent - 1.7.0 + 1.8.0 http-common @@ -19,12 +19,30 @@ jakarta.servlet-api provided + + org.slf4j + slf4j-api + org.springframework.boot spring-boot-starter-test test + + com.fasterxml.jackson.core + jackson-databind + test + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + + \ No newline at end of file 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..08d328b --- /dev/null +++ b/http-common/src/main/java/com/wultra/core/http/common/headers/UserAgent.java @@ -0,0 +1,108 @@ +/* + * 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.Optional; +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 + * @author Lubos Racansky, lubos.racansky@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 static final String PREFIX = "((^PowerAuthNetworking)|.*PowerAuth2)/(?\\d+\\.\\d+\\.\\d+)"; + private static final Pattern PATTERN_PREFIX = Pattern.compile(PREFIX + ".*"); + + private static final String LANGUAGE_AND_CONNECTION = "(\\((?[a-zA-Z]{2}); (?[a-zA-Z0-9]+)\\) )?"; + private static final String PRODUCT_AND_VERSION = "((?[a-zA-Z0-9-_.]+)/(?[0-9.]+(-[^ ]*)?) )?"; + private static final String PLATFORM_OS_VERSION_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"); + } + + /** + * Parse client user from the HTTP header value. + * + * @param userAgent User-Agent Header String + * @return Parsed device info, or empty if the user agent header cannot be parsed. + */ + public static Optional parse(String userAgent) { + // Identify if the user agent is ours and in what version + logger.debug("Parsing user agent value: {}", userAgent); + final Matcher matcherPrefix = PATTERN_PREFIX.matcher(userAgent); + if (!matcherPrefix.matches()) { + return Optional.empty(); + } + final String networkVersion = matcherPrefix.group("networkVersion"); + logger.debug("Declared networkVersion: {}", networkVersion); + if (!networkVersion.startsWith("1.")) { // simplistic matching for current v1.x clients + return Optional.empty(); + } + + // 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 empty if the user agent header cannot be parsed. + */ + private static Optional parseUserAgentV1(String userAgent) { + final Matcher matcher = PATTERN_V1.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 Optional.of(device); + } + logger.debug("The user agent value does not match v1 client format"); + return Optional.empty(); + } + +} 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..3796254 --- /dev/null +++ b/http-common/src/test/java/com/wultra/core/http/common/headers/UserAgentTest.java @@ -0,0 +1,134 @@ +/* + * 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 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 {@link UserAgent}. + * + * @author Petr Dvorak, petr@wultra.com + * @author Lubos Racansky, lubos.racansky@wultra.com + */ +class UserAgentTest { + + @ParameterizedTest + @MethodSource("provideUserAgents") + void testParse(final String userAgent, final UserAgent.Device expectedDevice) { + final Optional deviceOptional = UserAgent.parse(userAgent); + assertTrue(deviceOptional.isPresent(), "Unable to parse user-agent: " + userAgent); + assertEquals(expectedDevice, deviceOptional.get()); + } + + private static Stream 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); + } + +} diff --git a/pom.xml b/pom.xml index b2a2dc5..4da940f 100644 --- a/pom.xml +++ b/pom.xml @@ -8,7 +8,7 @@ Wultra - Core Java Libraries io.getlime.core lime-java-core-parent - 1.7.0 + 1.8.0 pom 2017 @@ -45,6 +45,7 @@ annotations audit-base + bom http-common rest-model-base rest-client-base @@ -54,9 +55,12 @@ UTF-8 17 17 + 3.2.2 - 3.1.2 + 3.1.6 + + 1.4.14 3.0.1 @@ -69,6 +73,18 @@ pom import + + + + ch.qos.logback + logback-classic + ${logback.version} + + + ch.qos.logback + logback-core + ${logback.version} + @@ -81,6 +97,16 @@ + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + + + org.apache.maven.plugins @@ -99,7 +125,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.5.0 + 3.6.3 false diff --git a/rest-client-base/pom.xml b/rest-client-base/pom.xml index 787ae09..43f0a6b 100644 --- a/rest-client-base/pom.xml +++ b/rest-client-base/pom.xml @@ -6,7 +6,7 @@ io.getlime.core lime-java-core-parent - 1.7.0 + 1.8.0 rest-client-base @@ -66,8 +66,8 @@ + org.apache.maven.plugins maven-surefire-plugin - 3.1.2 diff --git a/rest-client-base/src/main/java/com/wultra/core/rest/client/base/DefaultRestClient.java b/rest-client-base/src/main/java/com/wultra/core/rest/client/base/DefaultRestClient.java index befdb3c..e89b7e0 100644 --- a/rest-client-base/src/main/java/com/wultra/core/rest/client/base/DefaultRestClient.java +++ b/rest-client-base/src/main/java/com/wultra/core/rest/client/base/DefaultRestClient.java @@ -178,10 +178,14 @@ private void initializeWebClient() throws RestClientException { .build(); builder.exchangeStrategies(exchangeStrategies); - if (config.isHttpBasicAuthEnabled() && config.getHttpBasicAuthUsername() != null) { - logger.info("Configuring HTTP Basic Authentication"); - builder.filter(ExchangeFilterFunctions - .basicAuthentication(config.getHttpBasicAuthUsername(), config.getHttpBasicAuthPassword())); + if (config.isHttpBasicAuthEnabled()) { + if (config.getHttpBasicAuthUsername() != null && config.getHttpBasicAuthPassword() != null) { + logger.info("Configuring HTTP Basic Authentication"); + builder.filter(ExchangeFilterFunctions + .basicAuthentication(config.getHttpBasicAuthUsername(), config.getHttpBasicAuthPassword())); + } else { + logger.warn("HTTP Basic Authentication is enabled but username or password is null, baseUrl: {}", config.getBaseUrl()); + } } if (config.isHttpDigestAuthEnabled() && config.getHttpDigestAuthUsername() != null) { logger.info("Configuring HTTP Digest Authentication"); @@ -363,7 +367,7 @@ public ResponseEntity post(String path, Object request, MultiValueMap handleResponse(rs, responseType)) @@ -398,7 +402,7 @@ public void postNonBlocking(String path, Object request, MultiValueMap handleResponse(rs, responseType)) @@ -444,7 +448,7 @@ public ResponseEntity put(String path, Object request, MultiValueMap handleResponse(rs, responseType)) @@ -472,7 +476,7 @@ public void putNonBlocking(String path, Object request, MultiValueMap handleResponse(rs, responseType)) @@ -586,7 +590,7 @@ public ResponseEntity patch(String path, Object request, MultiValueMap handleResponse(rs, responseType)) @@ -614,7 +618,7 @@ public void patchNonBlocking(String path, Object request, MultiValueMap handleResponse(rs, responseType)) @@ -770,6 +774,15 @@ private Mono> handleResponse(ClientResponse response, Para }); } + private static MediaType resolveContentType(final RestClientConfiguration config, final MultiValueMap headers) { + if (headers != null && headers.containsKey(HttpHeaders.CONTENT_TYPE)) { + final MediaType contentType = MediaType.valueOf(headers.getFirst(HttpHeaders.CONTENT_TYPE)); + logger.debug("Overriding content type {} from config by {} from the given headers", config.getContentType(), contentType); + return contentType; + } + return config.getContentType(); + } + /** * In case base URL is specified, append the path to complete the URL. Otherwise use the path as the URL specification. * @param uriSpec Request headers URI specification. diff --git a/rest-client-base/src/test/java/com/wultra/core/rest/client/base/DefaultRestClientTest.java b/rest-client-base/src/test/java/com/wultra/core/rest/client/base/DefaultRestClientTest.java index 3e872fa..63482cc 100644 --- a/rest-client-base/src/test/java/com/wultra/core/rest/client/base/DefaultRestClientTest.java +++ b/rest-client-base/src/test/java/com/wultra/core/rest/client/base/DefaultRestClientTest.java @@ -662,6 +662,23 @@ void testPostWithMultipartData() throws RestClientException { assertEquals(requestData, responseEntity.getBody().getResponseObject().getResponse()); } + @Test + void testPostOctetStream() throws Exception { + final byte[] request = {1, 2}; + + final HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_OCTET_STREAM); + + final ResponseEntity> responseEntity = + restClient.post("/octet-stream", request, null, headers, new ParameterizedTypeReference<>(){}); + + assertNotNull(responseEntity); + assertNotNull(responseEntity.getBody()); + assertNotNull(responseEntity.getBody().getResponseObject()); + assertEquals("OK", responseEntity.getBody().getStatus()); + assertEquals("length: 2", responseEntity.getBody().getResponseObject().getResponse()); + } + @Test void testPostWithLargeServerResponse() { final Logger defaultRestClientLogger = (Logger) LoggerFactory.getLogger(DefaultRestClient.class); @@ -797,6 +814,19 @@ void testGetWithResponseDigestAuthFailed() throws RestClientException { assertEquals(HttpStatus.UNAUTHORIZED, exception.getStatusCode()); } + @Test + void testBasicAuthNullPassword() throws Exception { + final RestClientConfiguration config = new RestClientConfiguration(); + config.setHttpBasicAuthEnabled(true); + config.setHttpBasicAuthUsername("john"); + config.setHttpBasicAuthPassword(null); + config.setBaseUrl("https://localhost:" + port); + + final DefaultRestClient result = new DefaultRestClient(config); + + assertNotNull(result); + } + private static Object getField(final Object parentBean, String path) { final String[] pathParts = path.split("\\."); final String fieldName = pathParts[0]; diff --git a/rest-client-base/src/test/java/com/wultra/core/rest/client/base/controller/PublicTestRestController.java b/rest-client-base/src/test/java/com/wultra/core/rest/client/base/controller/PublicTestRestController.java index 874f6c1..b2c8ad4 100644 --- a/rest-client-base/src/test/java/com/wultra/core/rest/client/base/controller/PublicTestRestController.java +++ b/rest-client-base/src/test/java/com/wultra/core/rest/client/base/controller/PublicTestRestController.java @@ -21,13 +21,13 @@ import io.getlime.core.rest.model.base.request.ObjectRequest; import io.getlime.core.rest.model.base.response.ObjectResponse; import io.getlime.core.rest.model.base.response.Response; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; import java.net.URI; import java.util.Arrays; import java.util.Enumeration; @@ -85,6 +85,12 @@ public ObjectResponse testPostWithMultipartRequestAndResponse(@Req return new ObjectResponse<>(testResponse); } + @PostMapping(value = "/octet-stream", consumes = MediaType.APPLICATION_OCTET_STREAM_VALUE) + public ObjectResponse testPostOctetStream(@RequestBody byte[] request) { + final TestResponse testResponse = new TestResponse("length: " + request.length); + return new ObjectResponse<>(testResponse); + } + @PostMapping("/object-response-large") public ObjectResponse testPostWithLargeServerResponse() { TestResponse testResponse = new TestResponse(Arrays.toString(new byte[10 * 1024 * 1024])); diff --git a/rest-model-base/pom.xml b/rest-model-base/pom.xml index b344f15..015e291 100644 --- a/rest-model-base/pom.xml +++ b/rest-model-base/pom.xml @@ -6,7 +6,7 @@ io.getlime.core lime-java-core-parent - 1.7.0 + 1.8.0 rest-model-base diff --git a/rest-model-base/src/main/java/io/getlime/core/rest/model/base/entity/Error.java b/rest-model-base/src/main/java/io/getlime/core/rest/model/base/entity/Error.java index 236ed6d..b58df89 100644 --- a/rest-model-base/src/main/java/io/getlime/core/rest/model/base/entity/Error.java +++ b/rest-model-base/src/main/java/io/getlime/core/rest/model/base/entity/Error.java @@ -17,12 +17,16 @@ import jakarta.validation.constraints.NotBlank; +import lombok.EqualsAndHashCode; +import lombok.ToString; /** * Transport object for RESTful API representing an error instance. * * @author Petr Dvorak, petr@wultra.com */ +@ToString +@EqualsAndHashCode public class Error { /** diff --git a/rest-model-base/src/main/java/io/getlime/core/rest/model/base/request/ObjectRequest.java b/rest-model-base/src/main/java/io/getlime/core/rest/model/base/request/ObjectRequest.java index f0eacab..7b9425c 100644 --- a/rest-model-base/src/main/java/io/getlime/core/rest/model/base/request/ObjectRequest.java +++ b/rest-model-base/src/main/java/io/getlime/core/rest/model/base/request/ObjectRequest.java @@ -18,6 +18,8 @@ import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; +import lombok.EqualsAndHashCode; +import lombok.ToString; /** * Simple class representing a request with an object. @@ -26,6 +28,8 @@ * * @param Type of the request object. */ +@ToString +@EqualsAndHashCode public class ObjectRequest { @Valid diff --git a/rest-model-base/src/main/java/io/getlime/core/rest/model/base/response/ObjectResponse.java b/rest-model-base/src/main/java/io/getlime/core/rest/model/base/response/ObjectResponse.java index 6ac663e..77196a4 100644 --- a/rest-model-base/src/main/java/io/getlime/core/rest/model/base/response/ObjectResponse.java +++ b/rest-model-base/src/main/java/io/getlime/core/rest/model/base/response/ObjectResponse.java @@ -18,6 +18,8 @@ import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; +import lombok.EqualsAndHashCode; +import lombok.ToString; /** * Generic response with status and object of a custom class. @@ -26,6 +28,8 @@ * * @param Type of the response object */ +@ToString(callSuper = true) +@EqualsAndHashCode(callSuper = true) public class ObjectResponse extends Response { @Valid @@ -36,7 +40,7 @@ public class ObjectResponse extends Response { * Default constructor */ public ObjectResponse() { - this.status = Status.OK; + super(Status.OK); } /** @@ -45,7 +49,7 @@ public ObjectResponse() { * @param responseObject Response object. */ public ObjectResponse(T responseObject) { - this.status = Status.OK; + super(Status.OK); this.responseObject = responseObject; } @@ -56,28 +60,10 @@ public ObjectResponse(T responseObject) { * @param responseObject Response object. */ public ObjectResponse(String status, T responseObject) { - this.status = status; + super(status); this.responseObject = responseObject; } - /** - * Get response status. - * - * @return Response status. - */ - public String getStatus() { - return status; - } - - /** - * Set response status. - * - * @param status Response status. - */ - public void setStatus(String status) { - this.status = status; - } - /** * Get response object. * diff --git a/rest-model-base/src/main/java/io/getlime/core/rest/model/base/response/Response.java b/rest-model-base/src/main/java/io/getlime/core/rest/model/base/response/Response.java index 2eb09a9..32bfab1 100644 --- a/rest-model-base/src/main/java/io/getlime/core/rest/model/base/response/Response.java +++ b/rest-model-base/src/main/java/io/getlime/core/rest/model/base/response/Response.java @@ -17,12 +17,16 @@ import jakarta.validation.constraints.NotBlank; +import lombok.EqualsAndHashCode; +import lombok.ToString; /** * Simple status only response object. * * @author Petr Dvorak, petr@wultra.com */ +@EqualsAndHashCode +@ToString public class Response { /** @@ -46,7 +50,7 @@ public static class Status { * Response status. */ @NotBlank - protected String status; + private String status; /** * Default constructor.