From 2f07987471c83b53ed2f07561e86eb761391e92e Mon Sep 17 00:00:00 2001 From: ac892247 Date: Wed, 24 Sep 2025 13:42:06 +0200 Subject: [PATCH 01/14] respect encoded slashes in redirect header Signed-off-by: ac892247 --- .../routing/transform/TransformService.java | 24 +++--- .../filters/PageRedirectionFilterFactory.java | 6 +- .../apiml/integration/proxy/RedirectTest.java | 75 +++++++++++++++++++ 3 files changed, 95 insertions(+), 10 deletions(-) create mode 100644 integration-tests/src/test/java/org/zowe/apiml/integration/proxy/RedirectTest.java diff --git a/apiml-common/src/main/java/org/zowe/apiml/product/routing/transform/TransformService.java b/apiml-common/src/main/java/org/zowe/apiml/product/routing/transform/TransformService.java index b92af1420d..9ea4375628 100644 --- a/apiml-common/src/main/java/org/zowe/apiml/product/routing/transform/TransformService.java +++ b/apiml-common/src/main/java/org/zowe/apiml/product/routing/transform/TransformService.java @@ -40,10 +40,10 @@ public class TransformService { /** * Construct the URL using gateway hostname and route * - * @param type the type of the route - * @param serviceId the service id - * @param serviceUrl the service URL - * @param routes the routes + * @param type the type of the route + * @param serviceId the service id + * @param serviceUrl the service URL + * @param routes the routes * @param httpsScheme https scheme flag * @return the new URL * @throws URLTransformationException if the path of the service URL is not valid @@ -71,8 +71,8 @@ public String transformURL(ServiceType type, throw new URLTransformationException(message); } - if (serviceUri.getQuery() != null) { - serviceUriPath += "?" + serviceUri.getQuery(); + if (serviceUri.getRawQuery() != null) { + serviceUriPath += "?" + serviceUri.getRawQuery(); } return transformURL(serviceId, serviceUriPath, route, httpsScheme, serviceUri); @@ -95,13 +95,19 @@ public String transformURL(String serviceId, } ServiceAddress gatewayConfigProperties = gatewayClient.getGatewayConfigProperties(); - + if (originalUri != null && originalUri.toString().startsWith("//")) { + return String.format("//%s/%s%s%s", + gatewayConfigProperties.getHostname(), + serviceId, + StringUtils.isEmpty(route.getGatewayUrl()) ? "" : "/" + route.getGatewayUrl(), + endPoint); + } String scheme = httpsScheme ? "https" : gatewayConfigProperties.getScheme(); - return String.format("%s://%s/%s/%s%s", + return String.format("%s://%s/%s%s%s", scheme, gatewayConfigProperties.getHostname(), serviceId, - route.getGatewayUrl(), + StringUtils.isEmpty(route.getGatewayUrl()) ? "" : "/" + route.getGatewayUrl(), endPoint); } diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactory.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactory.java index 8f28d6b61a..3c5bad3f7e 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactory.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactory.java @@ -24,6 +24,7 @@ import org.springframework.web.server.ServerWebExchange; import org.springframework.web.util.UriComponentsBuilder; import org.zowe.apiml.eurekaservice.client.util.EurekaMetadataParser; +import org.zowe.apiml.product.constants.CoreService; import org.zowe.apiml.product.gateway.GatewayClient; import org.zowe.apiml.product.routing.RoutedService; import org.zowe.apiml.product.routing.ServiceType; @@ -134,6 +135,9 @@ private Mono processNewLocationUrl(ServerWebExchange exchange, Config conf var locationUri = URI.create(location); var targetInstance = getInstance(locationUri, instance); + if (targetInstance.isPresent() && targetInstance.get().getServiceId().equalsIgnoreCase(CoreService.GATEWAY.getServiceId())) { + return Mono.empty(); + } var defaultRoute = config.getRoutedService(); AtomicReference newUrl = new AtomicReference<>(); @@ -142,7 +146,7 @@ private Mono processNewLocationUrl(ServerWebExchange exchange, Config conf try { newUrl.set(transformService.transformURL( StringUtils.toRootLowerCase(config.serviceId), - UriComponentsBuilder.fromPath(locationUri.getPath()).query(locationUri.getQuery()).build().toUri().toString(), + UriComponentsBuilder.fromPath(locationUri.getPath()).query(locationUri.getRawQuery()).build().toUriString(), defaultRoute, false, locationUri diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/RedirectTest.java b/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/RedirectTest.java new file mode 100644 index 0000000000..8df4c3f2eb --- /dev/null +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/RedirectTest.java @@ -0,0 +1,75 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml.integration.proxy; + +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import lombok.Data; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.zowe.apiml.util.categories.DiscoverableClientDependentTest; +import org.zowe.apiml.util.config.ConfigReader; +import org.zowe.apiml.util.config.GatewayServiceConfiguration; +import org.zowe.apiml.util.config.ItSslConfigFactory; +import org.zowe.apiml.util.config.SslContext; + +import java.util.stream.Stream; + +import static io.restassured.RestAssured.given; + +@DiscoverableClientDependentTest +public class RedirectTest { + + GatewayServiceConfiguration gwConf = ConfigReader.environmentConfiguration().getGatewayServiceConfiguration(); + + @BeforeAll + static void init() throws Exception { + RestAssured.useRelaxedHTTPSValidation(); + SslContext.prepareSslAuthentication(ItSslConfigFactory.integrationTests()); + + } + + static Stream headerValues() { + return Stream.of( + Arguments.of("absolut URL with encoding doesn't match service route", "%2Fapi%2Frequest", "%2Fapi%2Frequest"), + Arguments.of("absolut URL doesn't match service route", "/api/request", "/api/request"), + Arguments.of("relative URL", "api/request", "api/request"), + Arguments.of("absolut URL containing encoded characters matches service route", "/discoverableclient/api/v1/login?returnUrl=%2Fapi%2Frequest", "https://localhost:10010/discoverableclient/api/v1/login?returnUrl=%2Fapi%2Frequest"), + Arguments.of("relative URL that contains service ID", "discoverableclient/api/v1/login?returnUrl=%2Fapi%2Frequest", "discoverableclient/api/v1/login?returnUrl=%2Fapi%2Frequest"), + Arguments.of("absolut URL matches service route", "/discoverableclient/api/v1/request", "https://localhost:10010/discoverableclient/api/v1/request"), + Arguments.of("Full URL contains service host and port", "https://localhost:10012/discoverableclient/api/v1/request", "https://localhost:10010/discoverableclient/api/v1/request"), + Arguments.of("Full URL contains gateway host and port", "https://localhost:10010/discoverableclient/api/v3/request", "https://localhost:10010/discoverableclient/api/v3/request"), + Arguments.of("scheme-relative URL contains service host and port", "//localhost:10012/discoverableclient/api/v1/request", "//localhost:10010/discoverableclient/api/v1/request"), + Arguments.of("scheme-relative URL contains gateway host and port", "//localhost:10010/discoverableclient/api/v1/request", "//localhost:10010/discoverableclient/api/v1/request") + ); + } + + @ParameterizedTest(name = "given {0} then Location header value {1} was transform to {2}") + @MethodSource("headerValues") + void giveLocationHeaderFromService(String msg, String original, String translated) { + var baseUrl = String.format("%s://%s:%d", gwConf.getScheme(), gwConf.getHost(), gwConf.getPort()); + var targetUrl = baseUrl + "/discoverableclient/api/v1/redirect"; + given() + .body(new LocationReq(original)) + .contentType(ContentType.JSON) + .post(targetUrl) + .then().log().ifValidationFails() + .header("Location", translated) + .statusCode(307); + } + + @Data + static class LocationReq { + final String location; + } +} From ab587b34220c0376da0dc3035eee903a7ca160fe Mon Sep 17 00:00:00 2001 From: ac892247 Date: Wed, 24 Sep 2025 13:50:10 +0200 Subject: [PATCH 02/14] check for null Signed-off-by: ac892247 --- .../gateway/filters/PageRedirectionFilterFactory.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactory.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactory.java index 3c5bad3f7e..95d8c9fde1 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactory.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactory.java @@ -135,7 +135,7 @@ private Mono processNewLocationUrl(ServerWebExchange exchange, Config conf var locationUri = URI.create(location); var targetInstance = getInstance(locationUri, instance); - if (targetInstance.isPresent() && targetInstance.get().getServiceId().equalsIgnoreCase(CoreService.GATEWAY.getServiceId())) { + if (isGateway(targetInstance)) { return Mono.empty(); } var defaultRoute = config.getRoutedService(); @@ -189,6 +189,12 @@ private Mono processNewLocationUrl(ServerWebExchange exchange, Config conf return Mono.empty(); } + boolean isGateway(Optional targetInstance) { + return targetInstance.isPresent() && + StringUtils.isNoneEmpty(targetInstance.get().getServiceId()) && + targetInstance.get().getServiceId().equalsIgnoreCase(CoreService.GATEWAY.getServiceId()); + } + @Data public static class Config { From 40bbe206a3e3268ba249eef5b8333cd7eccf21a1 Mon Sep 17 00:00:00 2001 From: ac892247 Date: Wed, 24 Sep 2025 14:06:18 +0200 Subject: [PATCH 03/14] code review Signed-off-by: ac892247 --- .../product/routing/transform/TransformService.java | 2 +- .../gateway/filters/PageRedirectionFilterFactory.java | 5 ++--- .../org/zowe/apiml/integration/proxy/RedirectTest.java | 10 +++++----- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/apiml-common/src/main/java/org/zowe/apiml/product/routing/transform/TransformService.java b/apiml-common/src/main/java/org/zowe/apiml/product/routing/transform/TransformService.java index 9ea4375628..d40c4396a7 100644 --- a/apiml-common/src/main/java/org/zowe/apiml/product/routing/transform/TransformService.java +++ b/apiml-common/src/main/java/org/zowe/apiml/product/routing/transform/TransformService.java @@ -71,7 +71,7 @@ public String transformURL(ServiceType type, throw new URLTransformationException(message); } - if (serviceUri.getRawQuery() != null) { + if (StringUtils.isNotBlank(serviceUri.getRawQuery())) { serviceUriPath += "?" + serviceUri.getRawQuery(); } diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactory.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactory.java index 95d8c9fde1..120048ca7f 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactory.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactory.java @@ -190,9 +190,8 @@ private Mono processNewLocationUrl(ServerWebExchange exchange, Config conf } boolean isGateway(Optional targetInstance) { - return targetInstance.isPresent() && - StringUtils.isNoneEmpty(targetInstance.get().getServiceId()) && - targetInstance.get().getServiceId().equalsIgnoreCase(CoreService.GATEWAY.getServiceId()); + return targetInstance.filter(target -> CoreService.GATEWAY.getServiceId().equalsIgnoreCase(target.getServiceId())) + .isPresent(); } @Data diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/RedirectTest.java b/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/RedirectTest.java index 8df4c3f2eb..1fe9baf128 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/RedirectTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/RedirectTest.java @@ -28,7 +28,7 @@ import static io.restassured.RestAssured.given; @DiscoverableClientDependentTest -public class RedirectTest { +class RedirectTest { GatewayServiceConfiguration gwConf = ConfigReader.environmentConfiguration().getGatewayServiceConfiguration(); @@ -41,12 +41,12 @@ static void init() throws Exception { static Stream headerValues() { return Stream.of( - Arguments.of("absolut URL with encoding doesn't match service route", "%2Fapi%2Frequest", "%2Fapi%2Frequest"), - Arguments.of("absolut URL doesn't match service route", "/api/request", "/api/request"), + Arguments.of("absolute URL with encoding doesn't match service route", "%2Fapi%2Frequest", "%2Fapi%2Frequest"), + Arguments.of("absolute URL doesn't match service route", "/api/request", "/api/request"), Arguments.of("relative URL", "api/request", "api/request"), - Arguments.of("absolut URL containing encoded characters matches service route", "/discoverableclient/api/v1/login?returnUrl=%2Fapi%2Frequest", "https://localhost:10010/discoverableclient/api/v1/login?returnUrl=%2Fapi%2Frequest"), + Arguments.of("absolute URL containing encoded characters matches service route", "/discoverableclient/api/v1/login?returnUrl=%2Fapi%2Frequest", "https://localhost:10010/discoverableclient/api/v1/login?returnUrl=%2Fapi%2Frequest"), Arguments.of("relative URL that contains service ID", "discoverableclient/api/v1/login?returnUrl=%2Fapi%2Frequest", "discoverableclient/api/v1/login?returnUrl=%2Fapi%2Frequest"), - Arguments.of("absolut URL matches service route", "/discoverableclient/api/v1/request", "https://localhost:10010/discoverableclient/api/v1/request"), + Arguments.of("absolute URL matches service route", "/discoverableclient/api/v1/request", "https://localhost:10010/discoverableclient/api/v1/request"), Arguments.of("Full URL contains service host and port", "https://localhost:10012/discoverableclient/api/v1/request", "https://localhost:10010/discoverableclient/api/v1/request"), Arguments.of("Full URL contains gateway host and port", "https://localhost:10010/discoverableclient/api/v3/request", "https://localhost:10010/discoverableclient/api/v3/request"), Arguments.of("scheme-relative URL contains service host and port", "//localhost:10012/discoverableclient/api/v1/request", "//localhost:10010/discoverableclient/api/v1/request"), From c2d664e24bbd983a232999501c46d0cbd4eb23c8 Mon Sep 17 00:00:00 2001 From: ac892247 Date: Wed, 24 Sep 2025 16:21:39 +0200 Subject: [PATCH 04/14] do not update absolute paths with gateway host and keep encoding for route matching Signed-off-by: ac892247 --- .../routing/transform/TransformService.java | 22 ++++++++++++++++++- .../filters/PageRedirectionFilterFactory.java | 5 ++--- .../apiml/integration/proxy/RedirectTest.java | 9 ++++---- 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/apiml-common/src/main/java/org/zowe/apiml/product/routing/transform/TransformService.java b/apiml-common/src/main/java/org/zowe/apiml/product/routing/transform/TransformService.java index d40c4396a7..53b9f459f7 100644 --- a/apiml-common/src/main/java/org/zowe/apiml/product/routing/transform/TransformService.java +++ b/apiml-common/src/main/java/org/zowe/apiml/product/routing/transform/TransformService.java @@ -59,7 +59,7 @@ public String transformURL(ServiceType type, } URI serviceUri = URI.create(serviceUrl); - String serviceUriPath = serviceUri.getPath(); + String serviceUriPath = serviceUri.getRawPath(); if (serviceUriPath == null) { String message = String.format("The URI %s is not valid.", serviceUri); throw new URLTransformationException(message); @@ -111,6 +111,26 @@ public String transformURL(String serviceId, endPoint); } + public String transformAbsoluteURL(String serviceId, + String serviceUriPath, + RoutedService route, + URI originalUri + ) throws URLTransformationException { + if (!gatewayClient.isInitialized()) { + apimlLog.log("org.zowe.apiml.common.gatewayNotFoundForTransformRequest"); + throw new URLTransformationException("Gateway not found yet, transform service cannot perform the request"); + } + + String endPoint = getShortEndPoint(route.getServiceUrl(), serviceUriPath); + if (!endPoint.isEmpty() && !endPoint.startsWith("/")) { + throw new URLTransformationException("The path " + originalUri.getPath() + " of the service URL " + originalUri + " is not valid."); + } + return String.format("/%s%s%s", + serviceId, + StringUtils.isEmpty(route.getGatewayUrl()) ? "" : "/" + route.getGatewayUrl(), + endPoint); + } + /** * Construct the API base path using the route * diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactory.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactory.java index 120048ca7f..8b1a8a04f5 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactory.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactory.java @@ -117,7 +117,7 @@ private String normalizePath(String path) { private boolean isMatching(RoutedService route, URI uri) { var servicePath = normalizePath(route.getServiceUrl()); - var locationPath = normalizePath(uri.getPath()); + var locationPath = normalizePath(uri.getRawPath()); return locationPath.startsWith(servicePath); } @@ -144,11 +144,10 @@ private Mono processNewLocationUrl(ServerWebExchange exchange, Config conf if (targetInstance == instance && isMatching(defaultRoute, locationUri)) { // try the preferable route on the same instance (the same as in the original request) try { - newUrl.set(transformService.transformURL( + newUrl.set(transformService.transformAbsoluteURL( StringUtils.toRootLowerCase(config.serviceId), UriComponentsBuilder.fromPath(locationUri.getPath()).query(locationUri.getRawQuery()).build().toUriString(), defaultRoute, - false, locationUri )); } catch (URLTransformationException e) { diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/RedirectTest.java b/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/RedirectTest.java index 1fe9baf128..cddd21edff 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/RedirectTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/RedirectTest.java @@ -42,14 +42,15 @@ static void init() throws Exception { static Stream headerValues() { return Stream.of( Arguments.of("absolute URL with encoding doesn't match service route", "%2Fapi%2Frequest", "%2Fapi%2Frequest"), + Arguments.of("absolute URL with encoding doesn't match service route", "%2Fdiscoverableclient%2Fapi%2Fv1%2Frequest", "%2Fdiscoverableclient%2Fapi%2Fv1%2Frequest"), Arguments.of("absolute URL doesn't match service route", "/api/request", "/api/request"), Arguments.of("relative URL", "api/request", "api/request"), - Arguments.of("absolute URL containing encoded characters matches service route", "/discoverableclient/api/v1/login?returnUrl=%2Fapi%2Frequest", "https://localhost:10010/discoverableclient/api/v1/login?returnUrl=%2Fapi%2Frequest"), + Arguments.of("absolute URL containing encoded characters matches service route", "/discoverableclient/api/v1/login?returnUrl=%2Fapi%2Frequest", "/discoverableclient/api/v1/login?returnUrl=%2Fapi%2Frequest"), Arguments.of("relative URL that contains service ID", "discoverableclient/api/v1/login?returnUrl=%2Fapi%2Frequest", "discoverableclient/api/v1/login?returnUrl=%2Fapi%2Frequest"), - Arguments.of("absolute URL matches service route", "/discoverableclient/api/v1/request", "https://localhost:10010/discoverableclient/api/v1/request"), - Arguments.of("Full URL contains service host and port", "https://localhost:10012/discoverableclient/api/v1/request", "https://localhost:10010/discoverableclient/api/v1/request"), + Arguments.of("absolute URL matches service route", "/discoverableclient/api/v1/request", "/discoverableclient/api/v1/request"), + Arguments.of("Full URL contains service host and port", "https://localhost:10012/discoverableclient/api/v1/request", "/discoverableclient/api/v1/request"), Arguments.of("Full URL contains gateway host and port", "https://localhost:10010/discoverableclient/api/v3/request", "https://localhost:10010/discoverableclient/api/v3/request"), - Arguments.of("scheme-relative URL contains service host and port", "//localhost:10012/discoverableclient/api/v1/request", "//localhost:10010/discoverableclient/api/v1/request"), + Arguments.of("scheme-relative URL contains service host and port", "//localhost:10012/discoverableclient/api/v1/request", "/discoverableclient/api/v1/request"), Arguments.of("scheme-relative URL contains gateway host and port", "//localhost:10010/discoverableclient/api/v1/request", "//localhost:10010/discoverableclient/api/v1/request") ); } From 0010ff1d7c29ec4400743027ed60e6e495fee9b7 Mon Sep 17 00:00:00 2001 From: ac892247 Date: Thu, 25 Sep 2025 10:13:01 +0200 Subject: [PATCH 05/14] read DC url from conf Signed-off-by: ac892247 --- .../apiml/integration/proxy/RedirectTest.java | 74 ++++++++++++++----- 1 file changed, 57 insertions(+), 17 deletions(-) diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/RedirectTest.java b/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/RedirectTest.java index cddd21edff..a23a617892 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/RedirectTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/RedirectTest.java @@ -18,10 +18,7 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.zowe.apiml.util.categories.DiscoverableClientDependentTest; -import org.zowe.apiml.util.config.ConfigReader; -import org.zowe.apiml.util.config.GatewayServiceConfiguration; -import org.zowe.apiml.util.config.ItSslConfigFactory; -import org.zowe.apiml.util.config.SslContext; +import org.zowe.apiml.util.config.*; import java.util.stream.Stream; @@ -30,28 +27,71 @@ @DiscoverableClientDependentTest class RedirectTest { - GatewayServiceConfiguration gwConf = ConfigReader.environmentConfiguration().getGatewayServiceConfiguration(); + static GatewayServiceConfiguration gwConf = ConfigReader.environmentConfiguration().getGatewayServiceConfiguration(); + static DiscoverableClientConfiguration dcConf = ConfigReader.environmentConfiguration().getDiscoverableClientConfiguration(); + static String gatewayUrl; + static String dcUrl; @BeforeAll static void init() throws Exception { RestAssured.useRelaxedHTTPSValidation(); SslContext.prepareSslAuthentication(ItSslConfigFactory.integrationTests()); - + gatewayUrl = String.format("%s:%d", gwConf.getHost(), gwConf.getPort()); + dcUrl = String.format("%s:%d", dcConf.getHost(), dcConf.getPort()); } static Stream headerValues() { return Stream.of( - Arguments.of("absolute URL with encoding doesn't match service route", "%2Fapi%2Frequest", "%2Fapi%2Frequest"), - Arguments.of("absolute URL with encoding doesn't match service route", "%2Fdiscoverableclient%2Fapi%2Fv1%2Frequest", "%2Fdiscoverableclient%2Fapi%2Fv1%2Frequest"), - Arguments.of("absolute URL doesn't match service route", "/api/request", "/api/request"), - Arguments.of("relative URL", "api/request", "api/request"), - Arguments.of("absolute URL containing encoded characters matches service route", "/discoverableclient/api/v1/login?returnUrl=%2Fapi%2Frequest", "/discoverableclient/api/v1/login?returnUrl=%2Fapi%2Frequest"), - Arguments.of("relative URL that contains service ID", "discoverableclient/api/v1/login?returnUrl=%2Fapi%2Frequest", "discoverableclient/api/v1/login?returnUrl=%2Fapi%2Frequest"), - Arguments.of("absolute URL matches service route", "/discoverableclient/api/v1/request", "/discoverableclient/api/v1/request"), - Arguments.of("Full URL contains service host and port", "https://localhost:10012/discoverableclient/api/v1/request", "/discoverableclient/api/v1/request"), - Arguments.of("Full URL contains gateway host and port", "https://localhost:10010/discoverableclient/api/v3/request", "https://localhost:10010/discoverableclient/api/v3/request"), - Arguments.of("scheme-relative URL contains service host and port", "//localhost:10012/discoverableclient/api/v1/request", "/discoverableclient/api/v1/request"), - Arguments.of("scheme-relative URL contains gateway host and port", "//localhost:10010/discoverableclient/api/v1/request", "//localhost:10010/discoverableclient/api/v1/request") + Arguments.of( + "absolute URL with encoding doesn't match service route", + "%2Fapi%2Frequest", + "%2Fapi%2Frequest" + ), + Arguments.of( + "absolute URL with encoding doesn't match service route", + "%2Fdiscoverableclient%2Fapi%2Fv1%2Frequest", + "%2Fdiscoverableclient%2Fapi%2Fv1%2Frequest" + ), + Arguments.of( + "absolute URL doesn't match service route", "/api/request", "/api/request"), + Arguments.of( + "relative URL", + "api/request", + "api/request"), + Arguments.of( + "absolute URL containing encoded characters matches service route", + "/discoverableclient/api/v1/login?returnUrl=%2Fapi%2Frequest", + "/discoverableclient/api/v1/login?returnUrl=%2Fapi%2Frequest" + ), + Arguments.of( + "relative URL that contains service ID", + "discoverableclient/api/v1/login?returnUrl=%2Fapi%2Frequest", + "discoverableclient/api/v1/login?returnUrl=%2Fapi%2Frequest" + ), + Arguments.of( + "absolute URL matches service route", + "/discoverableclient/api/v1/request", + "/discoverableclient/api/v1/request"), + Arguments.of( + "Full URL contains service host and port", + String.format("https://%s/discoverableclient/api/v1/request", dcUrl), + "/discoverableclient/api/v1/request" + ), + Arguments.of( + "Full URL contains gateway host and port", + String.format("https://%s/discoverableclient/api/v3/request", gatewayUrl), + String.format("https://%s/discoverableclient/api/v3/request", gatewayUrl) + ), + Arguments.of( + "scheme-relative URL contains service host and port", + String.format("//%s/discoverableclient/api/v1/request", dcUrl), + "/discoverableclient/api/v1/request" + ), + Arguments.of( + "scheme-relative URL contains gateway host and port", + String.format("//%s/discoverableclient/api/v1/request", gatewayUrl), + String.format("//%s/discoverableclient/api/v1/request", gatewayUrl) + ) ); } From ad7380de0d0bd8cb29491bf677d8c26e8950d7fd Mon Sep 17 00:00:00 2001 From: ac892247 Date: Thu, 25 Sep 2025 10:26:44 +0200 Subject: [PATCH 06/14] update test to expect absolute path Signed-off-by: ac892247 --- .../org/zowe/apiml/functional/gateway/PageRedirectionTest.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/integration-tests/src/test/java/org/zowe/apiml/functional/gateway/PageRedirectionTest.java b/integration-tests/src/test/java/org/zowe/apiml/functional/gateway/PageRedirectionTest.java index e3e6d8f03c..a6d0419331 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/functional/gateway/PageRedirectionTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/functional/gateway/PageRedirectionTest.java @@ -75,7 +75,6 @@ public PageRedirectionTest() throws URISyntaxException { @TestsNotMeantForZowe void apiRouteOfDiscoverableClient() { String location = String.format("%s://%s:%d%s", dcScheme, dcHost, dcPort, DISCOVERABLE_GREET); - String transformedLocation = String.format("%s://%s:%d%s", gatewayScheme, gatewayHost, gatewayPort, STATIC_GREET); RedirectLocation redirectLocation = new RedirectLocation(location); @@ -86,7 +85,7 @@ void apiRouteOfDiscoverableClient() { .post(requestUrl) .then() .statusCode(is(HttpStatus.TEMPORARY_REDIRECT.value())) - .header(LOCATION, transformedLocation); + .header(LOCATION, STATIC_GREET); } /** From c4a7819d35450d6c44b10eaae0ed5854e4c1b525 Mon Sep 17 00:00:00 2001 From: ac892247 Date: Thu, 25 Sep 2025 11:05:50 +0200 Subject: [PATCH 07/14] add service without context path to test redirect Signed-off-by: ac892247 --- config/docker/api-defs/staticclient.yml | 20 ++++++++ config/local/api-defs/staticclient.yml | 18 +++++++ .../apiml/integration/proxy/RedirectTest.java | 23 +++++++++ .../apiml/client/api/RedirectController.java | 50 +++++++++++++++++++ 4 files changed, 111 insertions(+) create mode 100644 mock-services/src/main/java/org/zowe/apiml/client/api/RedirectController.java diff --git a/config/docker/api-defs/staticclient.yml b/config/docker/api-defs/staticclient.yml index 7df25b9083..a478cb9f72 100644 --- a/config/docker/api-defs/staticclient.yml +++ b/config/docker/api-defs/staticclient.yml @@ -216,6 +216,26 @@ services: gatewayUrl: api/v1 version: 1.0.0 + - serviceId: redirectclient # unique lowercase ID of the service + catalogUiTileId: static # ID of the API Catalog UI tile (visual grouping of the services) + title: Redirect client # Title of the service in the API catalog + description: REST API in the mockservices to test redirect on a service without context path. + instanceBaseUrls: # list of base URLs for each instance + - https://mock-services:10013/ # scheme://hostname:port/contextPath + homePageRelativeUrl: /api/v1 # Normally used for informational purposes for other services to use it as a landing page + statusPageRelativeUrl: /application/info # Appended to the instanceBaseUrl + healthCheckRelativeUrl: /application/health # Appended to the instanceBaseUrl + routes: + - gatewayUrl: ui # [api/ui/ws]/v{majorVersion} + serviceRelativeUrl: # relativePath that is added to baseUrl of an instance + authentication: + scheme: zosmf + apiInfo: + - apiId: zowe.apiml.redirectclient + gatewayUrl: api/v1 + version: 1.0.0 + swaggerUrl: https://mock-services:10013/zosmf/api/docs + # Additional metadata that will be added to existing dynamically registered services: additionalServiceMetadata: - serviceId: staticclient # The staticclient service metadata will be extended diff --git a/config/local/api-defs/staticclient.yml b/config/local/api-defs/staticclient.yml index 5f16df1b5f..bb778ce0a8 100644 --- a/config/local/api-defs/staticclient.yml +++ b/config/local/api-defs/staticclient.yml @@ -217,6 +217,24 @@ services: gatewayUrl: api/v1 version: 1.0.0 + - serviceId: redirectclient # unique lowercase ID of the service + catalogUiTileId: static # ID of the API Catalog UI tile (visual grouping of the services) + title: Redirect client # Title of the service in the API catalog + description: REST API in the mockservices to test redirect on a service without context path. + instanceBaseUrls: # list of base URLs for each instance + - https://localhost:10013/ # scheme://hostname:port/contextPath + homePageRelativeUrl: /api/v1 # Normally used for informational purposes for other services to use it as a landing page + statusPageRelativeUrl: /application/info # Appended to the instanceBaseUrl + healthCheckRelativeUrl: /application/health # Appended to the instanceBaseUrl + routes: + - gatewayUrl: ui # [api/ui/ws]/v{majorVersion} + serviceRelativeUrl: # relativePath that is added to baseUrl of an instance + apiInfo: + - apiId: zowe.apiml.redirectclient + gatewayUrl: ui + version: 1.0.0 + swaggerUrl: https://localhost:10013/zosmf/api/docs + # Additional metadata that will be added to existing dynamically registered services: additionalServiceMetadata: - serviceId: staticclient # The staticclient service metadata will be extended diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/RedirectTest.java b/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/RedirectTest.java index a23a617892..3bdfe3b473 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/RedirectTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/RedirectTest.java @@ -109,6 +109,29 @@ void giveLocationHeaderFromService(String msg, String original, String translate .statusCode(307); } + static Stream urlsWithoutContextPath() { + return Stream.of( + Arguments.of( + "absolute URL matches / as a service route ", + "/common/login?returnUrl=%2Fdiscoverableclient%2Fapi%2Fv1%2Frequest", + "/redirectclient/ui/common/login?returnUrl=%2Fdiscoverableclient%2Fapi%2Fv1%2Frequest") + ); + } + + @ParameterizedTest(name = "given {0} then Location header value {1} was transform to {2}") + @MethodSource("urlsWithoutContextPath") + void giveLocationHeaderFromServiceWithoutContextPath(String msg, String original, String translated) { + var baseUrl = String.format("%s://%s:%d", gwConf.getScheme(), gwConf.getHost(), gwConf.getPort()); + var targetUrl = baseUrl + "/redirectclient/ui/api/v1/redirect"; + given() + .body(new LocationReq(original)) + .contentType(ContentType.JSON) + .post(targetUrl) + .then().log().ifValidationFails() + .header("Location", translated) + .statusCode(302); + } + @Data static class LocationReq { final String location; diff --git a/mock-services/src/main/java/org/zowe/apiml/client/api/RedirectController.java b/mock-services/src/main/java/org/zowe/apiml/client/api/RedirectController.java new file mode 100644 index 0000000000..9c541082bc --- /dev/null +++ b/mock-services/src/main/java/org/zowe/apiml/client/api/RedirectController.java @@ -0,0 +1,50 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml.client.api; + +import jakarta.servlet.http.HttpServletResponse; +import lombok.Data; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import static org.springframework.http.HttpHeaders.LOCATION; + +@RestController +public class RedirectController { + + /** + * Get url from POST request body, then set the url to Location response header, and set status code to 307 + * + * @param redirectLocation request body which contains a url + * @param response return the same data as request body + * @return + */ + @PostMapping( + value = "/api/v1/redirect", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE + ) + @ResponseStatus(HttpStatus.FOUND) + public RedirectLocation redirectPage(@RequestBody RedirectLocation redirectLocation, + HttpServletResponse response) { + response.setHeader(LOCATION, redirectLocation.getLocation()); + return redirectLocation; + } + + @Data + static class RedirectLocation { + private String location; + } +} From 8511dc0f1d425250cfe80bfb28e3d1b36358072b Mon Sep 17 00:00:00 2001 From: ac892247 Date: Thu, 25 Sep 2025 13:02:55 +0200 Subject: [PATCH 08/14] new service added to catalog Signed-off-by: ac892247 --- .../cypress/e2e/detail-page/service-version-compare.cy.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api-catalog-ui/frontend/cypress/e2e/detail-page/service-version-compare.cy.js b/api-catalog-ui/frontend/cypress/e2e/detail-page/service-version-compare.cy.js index eb6702362d..b9ecd51175 100644 --- a/api-catalog-ui/frontend/cypress/e2e/detail-page/service-version-compare.cy.js +++ b/api-catalog-ui/frontend/cypress/e2e/detail-page/service-version-compare.cy.js @@ -38,7 +38,7 @@ describe('>>> Service version compare Test', () => { 'exist' ); - const expectedServicesCount = 17; + const expectedServicesCount = 18; cy.get('div.MuiTabs-flexContainer.MuiTabs-flexContainerVertical') // Select the parent div .find('a.MuiTab-root') // Find all the anchor elements within the div From 1546aa24abf8061fbaf548c778b3cbe2ce1cf3a7 Mon Sep 17 00:00:00 2001 From: ac892247 Date: Thu, 25 Sep 2025 14:04:21 +0200 Subject: [PATCH 09/14] set scheme only if already exists Signed-off-by: ac892247 --- .../apiml/gateway/filters/PageRedirectionFilterFactory.java | 2 +- .../gateway/filters/PageRedirectionFilterFactoryTest.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactory.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactory.java index 8b1a8a04f5..371aa83804 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactory.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactory.java @@ -177,7 +177,7 @@ private Mono processNewLocationUrl(ServerWebExchange exchange, Config conf if (newUrl.get() != null) { // if the new URL was defined, decorate (scheme by AT-TLS) and set - if (isServerAttlsEnabled) { + if (isServerAttlsEnabled && newUrl.get().startsWith("http")) { newUrl.set(UriComponentsBuilder.fromUriString(newUrl.get()).scheme("https").build().toUriString()); } diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactoryTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactoryTest.java index f162bdc576..0b6730a211 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactoryTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactoryTest.java @@ -93,7 +93,7 @@ class GivenValidUrl { @Test void whenNoAttls_thenAddRedirectionUrl() { - var expectedUrl = GW_BASE_URL + "/gateway/api/v1/api/v1/redirected_url"; + var expectedUrl = "/gateway/api/v1/api/v1/redirected_url"; var factory = new PageRedirectionFilterFactory(gatewayClient, discoveryClient); var chain = mock(GatewayFilterChain.class); @@ -118,7 +118,7 @@ void whenNoAttls_thenAddRedirectionUrl() { @Test void whenAttls_thenAddRedirectionUrl() { - var expectedUrl = GW_BASE_URL + "/gateway/api/v1/api/v1/redirected_url?arg=1&arg=2"; + var expectedUrl = "/gateway/api/v1/api/v1/redirected_url?arg=1&arg=2"; var factory = new PageRedirectionFilterFactory(gatewayClient, discoveryClient); var chain = mock(GatewayFilterChain.class); var exchange = mock(ServerWebExchange.class); From c8889b01c94f27fd3f5a26da62d858b5926672d9 Mon Sep 17 00:00:00 2001 From: ac892247 Date: Thu, 25 Sep 2025 15:42:13 +0200 Subject: [PATCH 10/14] refactor unit tests Signed-off-by: ac892247 --- .../PageRedirectionFilterFactoryTest.java | 110 ++++++++---------- 1 file changed, 48 insertions(+), 62 deletions(-) diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactoryTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactoryTest.java index 0b6730a211..e49dfed64c 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactoryTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactoryTest.java @@ -14,6 +14,9 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.cloud.gateway.filter.GatewayFilter; @@ -32,6 +35,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; @@ -51,32 +55,40 @@ class PageRedirectionFilterFactoryTest { private final ServiceAddress serviceAddress = ServiceAddress.builder() .scheme(GW_SCHEME).hostname(GW_HOSTNAME + ":" + GW_PORT).build(); + PageRedirectionFilterFactory factory; + GatewayFilterChain chain; + ServerWebExchange exchange; + ServerHttpResponse res; + ServiceInstance serviceInstance; + @BeforeEach void setUp() { gatewayClient = mock(GatewayClient.class); discoveryClient = mock(DiscoveryClient.class); - } + factory = new PageRedirectionFilterFactory(gatewayClient, discoveryClient); - private void commonSetup(PageRedirectionFilterFactory factory, ServerWebExchange exchange, ServerHttpResponse res, GatewayFilterChain chain, boolean isAttlsEnabled) { - ReflectionTestUtils.setField(factory, "isServerAttlsEnabled", isAttlsEnabled); + chain = mock(GatewayFilterChain.class); + exchange = mock(ServerWebExchange.class); + res = mock(ServerHttpResponse.class); + serviceInstance = mock(ServiceInstance.class); + when(gatewayClient.getGatewayConfigProperties()).thenReturn(serviceAddress); + when(gatewayClient.isInitialized()).thenReturn(true); + when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty()); when(res.getStatusCode()).thenReturn(HttpStatusCode.valueOf(HttpStatus.SC_MOVED_PERMANENTLY)); when(exchange.getResponse()).thenReturn(res); - when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty()); - when(gatewayClient.isInitialized()).thenReturn(true); - when(gatewayClient.getGatewayConfigProperties()).thenReturn(serviceAddress); } + private PageRedirectionFilterFactory.Config createConfig() { var config = new PageRedirectionFilterFactory.Config(); - config.setInstanceId("instanceId"); - config.setServiceId("GATEWAY"); + config.setInstanceId("localhost:discoverableclient:10012"); + config.setServiceId("DISCOVERABLECLIENT"); config.setGatewayUrl("api/v1"); - config.setServiceUrl("/"); + config.setServiceUrl("/discoverableclient"); return config; } - private void setupInstanceInfo() { - var serviceInstance = mock(ServiceInstance.class); + private void mockServiceInstance() { Map metadata = new HashMap<>(); metadata.put(ROUTES + ".api-v1." + ROUTES_GATEWAY_URL, "api/v1"); @@ -85,27 +97,33 @@ private void setupInstanceInfo() { when(serviceInstance.getInstanceId()).thenReturn("instanceId"); when(serviceInstance.getHost()).thenReturn("localhost"); when(serviceInstance.getPort()).thenReturn(10010); - when(discoveryClient.getInstances("GATEWAY")).thenReturn(new ArrayList<>(Collections.singletonList(serviceInstance))); + when(discoveryClient.getInstances("DISCOVERABLECLIENT")).thenReturn(new ArrayList<>(Collections.singletonList(serviceInstance))); + } + + void mockLocationHeaderResponse(String url) { + var header = new HttpHeaders(); + header.put(HttpHeaders.LOCATION, Collections.singletonList(url)); + when(res.getHeaders()).thenReturn(header); } @Nested class GivenValidUrl { - @Test - void whenNoAttls_thenAddRedirectionUrl() { - var expectedUrl = "/gateway/api/v1/api/v1/redirected_url"; - var factory = new PageRedirectionFilterFactory(gatewayClient, discoveryClient); + static Stream locationUrls() { + return Stream.of( + Arguments.of("/discoverableclient/api/v1/login?redirected_url=%2Fsome%2Fpath", "https://localhost:10010/discoverableclient/login?redirected_url=%2Fsome%2Fpath", false), + Arguments.of("discoverableclient/api/v1/login?redirected_url=%2Fsome%2Fpath", "discoverableclient/api/v1/login?redirected_url=%2Fsome%2Fpath", false), + Arguments.of("/discoverableclient/api/v1/api/v1/redirected_url?arg=1&arg=2", "http://localhost:10010/discoverableclient/api/v1/redirected_url?arg=1&arg=2", true) - var chain = mock(GatewayFilterChain.class); - var exchange = mock(ServerWebExchange.class); - var res = mock(ServerHttpResponse.class); - var header = new HttpHeaders(); + ); + } - header.put(HttpHeaders.LOCATION, Collections.singletonList("https://localhost:10010/api/v1/redirected_url")); - when(res.getHeaders()).thenReturn(header); + @ParameterizedTest + @MethodSource(value = "locationUrls") + void whenNoAttls_thenAddRedirectionUrl(String expectedUrl, String originalUrl, boolean attlsEnabled) { + ReflectionTestUtils.setField(factory, "isServerAttlsEnabled", attlsEnabled); - commonSetup(factory, exchange, res, chain, false); - setupInstanceInfo(); + mockLocationHeaderResponse(originalUrl); var config = createConfig(); GatewayFilter gatewayFilter = factory.apply(config); @@ -116,24 +134,6 @@ void whenNoAttls_thenAddRedirectionUrl() { assertEquals(expectedUrl, res.getHeaders().getFirst(HttpHeaders.LOCATION)); } - @Test - void whenAttls_thenAddRedirectionUrl() { - var expectedUrl = "/gateway/api/v1/api/v1/redirected_url?arg=1&arg=2"; - var factory = new PageRedirectionFilterFactory(gatewayClient, discoveryClient); - var chain = mock(GatewayFilterChain.class); - var exchange = mock(ServerWebExchange.class); - var res = mock(ServerHttpResponse.class); - var header = new HttpHeaders(); - header.put(HttpHeaders.LOCATION, Collections.singletonList("http://localhost:10010/api/v1/redirected_url?arg=1&arg=2")); - when(res.getHeaders()).thenReturn(header); - - commonSetup(factory, exchange, res, chain, true); - setupInstanceInfo(); - var config = createConfig(); - - StepVerifier.create(factory.apply(config).filter(exchange, chain)).expectComplete().verify(); - assertEquals(expectedUrl, res.getHeaders().getFirst(HttpHeaders.LOCATION)); - } } @Nested @@ -142,15 +142,9 @@ class GivenMissingGwConfig { @Test void thenDoNotTransform() { var expectedUrl = GW_BASE_URL + "/api/v1/redirected_url"; - var factory = new PageRedirectionFilterFactory(gatewayClient, discoveryClient); - var chain = mock(GatewayFilterChain.class); - var exchange = mock(ServerWebExchange.class); - var res = mock(ServerHttpResponse.class); - var header = new HttpHeaders(); - header.put(HttpHeaders.LOCATION, Collections.singletonList(expectedUrl)); - when(res.getHeaders()).thenReturn(header); - commonSetup(factory, exchange, res, chain, false); - setupInstanceInfo(); + + mockLocationHeaderResponse(expectedUrl); + mockServiceInstance(); var config = createConfig(); when(gatewayClient.isInitialized()).thenReturn(false); @@ -163,14 +157,10 @@ void thenDoNotTransform() { class GivenNullUrl { @Test void thenDoNotTransform() { - var factory = new PageRedirectionFilterFactory(gatewayClient, discoveryClient); - var chain = mock(GatewayFilterChain.class); - var exchange = mock(ServerWebExchange.class); - var res = mock(ServerHttpResponse.class); + var header = new HttpHeaders(); header.put(HttpHeaders.LOCATION, Collections.emptyList()); when(res.getHeaders()).thenReturn(header); - commonSetup(factory, exchange, res, chain, false); var config = createConfig(); StepVerifier.create(factory.apply(config).filter(exchange, chain)).expectComplete().verify(); @@ -182,17 +172,13 @@ void thenDoNotTransform() { class GivenDifferentResponseStatusCode { @Test void thenDoNotTransform() { - var factory = new PageRedirectionFilterFactory(gatewayClient, discoveryClient); - var chain = mock(GatewayFilterChain.class); - var exchange = mock(ServerWebExchange.class); - var res = mock(ServerHttpResponse.class); + var header = new HttpHeaders(); header.put(HttpHeaders.LOCATION, Collections.emptyList()); when(res.getHeaders()).thenReturn(header); - commonSetup(factory, exchange, res, chain, false); when(res.getStatusCode()).thenReturn(HttpStatusCode.valueOf(HttpStatus.SC_CONTINUE)); - setupInstanceInfo(); + mockServiceInstance(); var config = createConfig(); StepVerifier.create(factory.apply(config).filter(exchange, chain)).expectComplete().verify(); From 3b2d4282d8bc2d8b23887cab475b18642949aff4 Mon Sep 17 00:00:00 2001 From: ac892247 Date: Thu, 25 Sep 2025 17:23:17 +0200 Subject: [PATCH 11/14] more test cases Signed-off-by: ac892247 --- .../gateway/filters/PageRedirectionFilterFactoryTest.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactoryTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactoryTest.java index e49dfed64c..aa9f367d16 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactoryTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactoryTest.java @@ -113,6 +113,10 @@ static Stream locationUrls() { return Stream.of( Arguments.of("/discoverableclient/api/v1/login?redirected_url=%2Fsome%2Fpath", "https://localhost:10010/discoverableclient/login?redirected_url=%2Fsome%2Fpath", false), Arguments.of("discoverableclient/api/v1/login?redirected_url=%2Fsome%2Fpath", "discoverableclient/api/v1/login?redirected_url=%2Fsome%2Fpath", false), + Arguments.of("http://localhost:10010/api/v1/redirected_url?arg=1&arg=2", "http://localhost:10010/api/v1/redirected_url?arg=1&arg=2", true), + Arguments.of("%2Fdiscoverableclient%2Fapi%2Fv1%2Frequest", "%2Fdiscoverableclient%2Fapi%2Fv1%2Frequest", true), + Arguments.of("api/request", "api/request", true), + Arguments.of("//localhost:10010/api/v1/request", "//localhost:10010/api/v1/request", true), Arguments.of("/discoverableclient/api/v1/api/v1/redirected_url?arg=1&arg=2", "http://localhost:10010/discoverableclient/api/v1/redirected_url?arg=1&arg=2", true) ); From cdc650bde044fc0b5f438b785bc8d88bce7772e0 Mon Sep 17 00:00:00 2001 From: ac892247 Date: Fri, 26 Sep 2025 14:15:21 +0200 Subject: [PATCH 12/14] refactor Signed-off-by: ac892247 --- .../routing/transform/TransformService.java | 25 +- .../transform/TransformServiceTest.java | 372 +++++++----------- .../filters/PageRedirectionFilterFactory.java | 31 +- .../apiml/integration/proxy/RedirectTest.java | 4 +- 4 files changed, 178 insertions(+), 254 deletions(-) diff --git a/apiml-common/src/main/java/org/zowe/apiml/product/routing/transform/TransformService.java b/apiml-common/src/main/java/org/zowe/apiml/product/routing/transform/TransformService.java index 53b9f459f7..5a19532e9c 100644 --- a/apiml-common/src/main/java/org/zowe/apiml/product/routing/transform/TransformService.java +++ b/apiml-common/src/main/java/org/zowe/apiml/product/routing/transform/TransformService.java @@ -89,7 +89,7 @@ public String transformURL(String serviceId, throw new URLTransformationException("Gateway not found yet, transform service cannot perform the request"); } - String endPoint = getShortEndPoint(route.getServiceUrl(), serviceUriPath); + String endPoint = getShortEndpoint(route.getServiceUrl(), serviceUriPath); if (!endPoint.isEmpty() && !endPoint.startsWith("/")) { throw new URLTransformationException("The path " + originalUri.getPath() + " of the service URL " + originalUri + " is not valid."); } @@ -112,23 +112,22 @@ public String transformURL(String serviceId, } public String transformAbsoluteURL(String serviceId, - String serviceUriPath, - RoutedService route, - URI originalUri + String locationUri, + RoutedService route ) throws URLTransformationException { - if (!gatewayClient.isInitialized()) { - apimlLog.log("org.zowe.apiml.common.gatewayNotFoundForTransformRequest"); - throw new URLTransformationException("Gateway not found yet, transform service cannot perform the request"); - } - String endPoint = getShortEndPoint(route.getServiceUrl(), serviceUriPath); - if (!endPoint.isEmpty() && !endPoint.startsWith("/")) { - throw new URLTransformationException("The path " + originalUri.getPath() + " of the service URL " + originalUri + " is not valid."); + String endpoint = getShortEndpoint(route.getServiceUrl(), locationUri); + if (isRelative(endpoint)) { + throw new URLTransformationException("The path " + locationUri + " of the service " + serviceId + " is not valid."); } return String.format("/%s%s%s", serviceId, StringUtils.isEmpty(route.getGatewayUrl()) ? "" : "/" + route.getGatewayUrl(), - endPoint); + endpoint); + } + + boolean isRelative(String endpoint) { + return !endpoint.isEmpty() && !endpoint.startsWith("/"); } /** @@ -172,7 +171,7 @@ public String retrieveApiBasePath(String serviceId, * @param endPoint the endpoint of method * @return short endpoint */ - private String getShortEndPoint(String routeServiceUrl, String endPoint) { + private String getShortEndpoint(String routeServiceUrl, String endPoint) { String shortEndPoint = endPoint; if (!SEPARATOR.equals(routeServiceUrl) && StringUtils.isNotBlank(routeServiceUrl)) { shortEndPoint = shortEndPoint.replaceFirst(UrlUtils.removeLastSlash(routeServiceUrl), ""); diff --git a/apiml-common/src/test/java/org/zowe/apiml/product/routing/transform/TransformServiceTest.java b/apiml-common/src/test/java/org/zowe/apiml/product/routing/transform/TransformServiceTest.java index c98866d014..ad222aa3b4 100644 --- a/apiml-common/src/test/java/org/zowe/apiml/product/routing/transform/TransformServiceTest.java +++ b/apiml-common/src/test/java/org/zowe/apiml/product/routing/transform/TransformServiceTest.java @@ -28,293 +28,182 @@ class TransformServiceTest { private static final String UI_PREFIX = "ui"; private static final String API_PREFIX = "api"; private static final String WS_PREFIX = "ws"; - private static final String SERVICE_ID = "service"; private GatewayClient gatewayClient; + private TransformService transformService; @BeforeEach void setup() { - ServiceAddress gatewayConfigProperties = ServiceAddress.builder() + ServiceAddress gatewayConfig = ServiceAddress.builder() .scheme("https") .hostname("localhost") .build(); - gatewayClient = new GatewayClient(gatewayConfigProperties); + gatewayClient = new GatewayClient(gatewayConfig); + transformService = new TransformService(gatewayClient); } - @Test - void givenHomePageAndUIRoute_whenTransform_thenUseNewUrl() throws URLTransformationException { - String url = "https://localhost:8080/ui"; - + private RoutedServices buildRoutedServices(RoutedService... routes) { RoutedServices routedServices = new RoutedServices(); - RoutedService routedService1 = new RoutedService(SERVICE_ID, UI_PREFIX, "/ui"); - RoutedService routedService2 = new RoutedService(SERVICE_ID, "api/v1", "/"); - routedServices.addRoutedService(routedService1); - routedServices.addRoutedService(routedService2); - - TransformService transformService = new TransformService(gatewayClient); - String actualUrl = transformService.transformURL(ServiceType.UI, SERVICE_ID, url, routedServices, false); + for (RoutedService route : routes) { + routedServices.addRoutedService(route); + } + return routedServices; + } - String expectedUrl = String.format("%s://%s/%s/%s", + private String expectedUrl(String serviceId, String prefix, String path) { + return String.format("%s://%s/%s/%s%s", gatewayClient.getGatewayConfigProperties().getScheme(), gatewayClient.getGatewayConfigProperties().getHostname(), - SERVICE_ID, - UI_PREFIX); - assertEquals(expectedUrl, actualUrl); + serviceId, + prefix, + path == null ? "" : path + ); } - @Test - void givenHomePageWithAttlsEnabled_whenTransform_thenUseNewUrlWithHttps() throws URLTransformationException { - String url = "http://localhost:8080/ui"; - RoutedServices routedServices = new RoutedServices(); - RoutedService routedService1 = new RoutedService(SERVICE_ID, UI_PREFIX, "/ui"); - RoutedService routedService2 = new RoutedService(SERVICE_ID, "api/v1", "/"); - routedServices.addRoutedService(routedService1); - routedServices.addRoutedService(routedService2); - - TransformService transformService = new TransformService(gatewayClient); - String actualUrl = transformService.transformURL(ServiceType.UI, SERVICE_ID, url, routedServices, true); + @ParameterizedTest + @CsvSource({ + "https://localhost:8080/ui,UI,ui,''", + "https://localhost:8080/ws,WS,ws,''", + "https://localhost:8080/api,API,api,''", + "https://locahost:8080/ui/service/login.do?action=secure,UI,ui,'/login.do?action=secure'" + }) + void givenValidRoutes_whenTransform_thenReturnExpectedUrl( + String url, ServiceType type, String prefix, String extraPath + ) throws URLTransformationException { + RoutedServices routes = buildRoutedServices( + new RoutedService(SERVICE_ID, prefix, url.contains("/ui/service") ? "/ui/service" : "/" + prefix), + new RoutedService(SERVICE_ID, "api/v1", "/") + ); - String expectedUrl = String.format("%s://%s/%s/%s", - "https", - gatewayClient.getGatewayConfigProperties().getHostname(), - SERVICE_ID, - UI_PREFIX); - assertEquals(expectedUrl, actualUrl); + String actual = transformService.transformURL(type, SERVICE_ID, url, routes, false); + assertEquals(expectedUrl(SERVICE_ID, prefix, extraPath), actual); } - @Test - void givenHomePage_whenRouteNotFound_thenThrowException() { - String url = "https://localhost:8080/u"; - - RoutedServices routedServices = new RoutedServices(); - RoutedService routedService1 = new RoutedService(SERVICE_ID, UI_PREFIX, "/ui"); - RoutedService routedService2 = new RoutedService(SERVICE_ID, "api/v1", "/"); - routedServices.addRoutedService(routedService1); - routedServices.addRoutedService(routedService2); - - TransformService transformService = new TransformService(gatewayClient); - - Exception exception = assertThrows(URLTransformationException.class, () -> { - transformService.transformURL(ServiceType.UI, SERVICE_ID, url, routedServices, false); - }); - assertEquals("Not able to select route for url https://localhost:8080/u of the service service. Original url used.", exception.getMessage()); + @ParameterizedTest + @CsvSource({ + "https://localhost:8080/,WS,ws,'/'", + "https://locahost:8080/test,UI,ui,'/test'", + }) + void givenMissingRoutes_whenTransform_thenThrow( + String url, ServiceType type, String prefix, String extraPath + ) { + RoutedServices routes = buildRoutedServices( + new RoutedService(SERVICE_ID, prefix, url.contains("/ui/service") ? "/ui/service" : "/" + prefix), + new RoutedService(SERVICE_ID, "api/v1", "/") + ); + assertThrows(URLTransformationException.class, () -> transformService.transformURL(type, SERVICE_ID, url, routes, false)); } @Test - void givenHomePageAndWSRoute_whenTransform_thenUseNewUrl() throws URLTransformationException { - String url = "https://localhost:8080/ws"; - - RoutedServices routedServices = new RoutedServices(); - RoutedService routedService1 = new RoutedService(SERVICE_ID, WS_PREFIX, "/ws"); - RoutedService routedService2 = new RoutedService(SERVICE_ID, "api/v1", "/"); - routedServices.addRoutedService(routedService1); - routedServices.addRoutedService(routedService2); + void givenHttpUrlAndAttlsEnabled_whenTransform_thenForceHttps() throws URLTransformationException { + String url = "http://localhost:8080/ui"; + RoutedServices routes = buildRoutedServices( + new RoutedService(SERVICE_ID, UI_PREFIX, "/ui"), + new RoutedService(SERVICE_ID, "api/v1", "/") + ); - TransformService transformService = new TransformService(gatewayClient); - String actualUrl = transformService.transformURL(ServiceType.WS, SERVICE_ID, url, routedServices, false); + String actual = transformService.transformURL(ServiceType.UI, SERVICE_ID, url, routes, true); - String expectedUrl = String.format("%s://%s/%s/%s", - gatewayClient.getGatewayConfigProperties().getScheme(), - gatewayClient.getGatewayConfigProperties().getHostname(), - SERVICE_ID, - WS_PREFIX); - assertEquals(expectedUrl, actualUrl); + assertEquals("https://localhost/service/ui", actual); } @Test - void givenHomePageAndAPIRoute_whenTransform_thenUseNewUrl() throws URLTransformationException { - String url = "https://localhost:8080/api"; - - RoutedServices routedServices = new RoutedServices(); - RoutedService routedService1 = new RoutedService(SERVICE_ID, API_PREFIX, "/api"); - RoutedService routedService2 = new RoutedService(SERVICE_ID, "api/v1", "/"); - routedServices.addRoutedService(routedService1); - routedServices.addRoutedService(routedService2); + void givenRouteNotFound_whenTransform_thenThrowException() { + String url = "https://localhost:8080/u"; + RoutedServices routes = buildRoutedServices( + new RoutedService(SERVICE_ID, UI_PREFIX, "/ui"), + new RoutedService(SERVICE_ID, "api/v1", "/") + ); - TransformService transformService = new TransformService(gatewayClient); - String actualUrl = transformService.transformURL(ServiceType.API, SERVICE_ID, url, routedServices, false); + URLTransformationException ex = assertThrows(URLTransformationException.class, + () -> transformService.transformURL(ServiceType.UI, SERVICE_ID, url, routes, false)); - String expectedUrl = String.format("%s://%s/%s/%s", - gatewayClient.getGatewayConfigProperties().getScheme(), - gatewayClient.getGatewayConfigProperties().getHostname(), - SERVICE_ID, - API_PREFIX); - assertEquals(expectedUrl, actualUrl); + assertEquals("Not able to select route for url https://localhost:8080/u of the service service. Original url used.", + ex.getMessage()); } - @Test - void givenInvalidHomePage_thenThrowException() { + void givenInvalidUrl_whenTransform_thenThrowException() { String url = "https:localhost:8080/wss"; + TransformService service = new TransformService(gatewayClient); - TransformService transformService = new TransformService(gatewayClient); + URLTransformationException ex = assertThrows(URLTransformationException.class, + () -> service.transformURL(null, null, url, null, false)); - Exception exception = assertThrows(URLTransformationException.class, () -> { - transformService.transformURL(null, null, url, null, false); - }); - assertEquals("The URI " + url + " is not valid.", exception.getMessage()); + assertEquals("The URI " + url + " is not valid.", ex.getMessage()); } @Test - void givenEmptyGatewayClient_thenThrowException() { - String url = "https://localhost:8080/wss"; + void givenEmptyGatewayClient_whenTransform_thenThrowException() { + GatewayClient emptyClient = new GatewayClient(null); + TransformService service = new TransformService(emptyClient); - GatewayClient emptyGatewayClient = new GatewayClient(null); - TransformService transformService = new TransformService(emptyGatewayClient); + URLTransformationException ex = assertThrows(URLTransformationException.class, + () -> service.transformURL(null, null, "https://localhost:8080/wss", null, false)); - Exception exception = assertThrows(URLTransformationException.class, () -> { - transformService.transformURL(null, null, url, null, false); - }); - assertEquals("Gateway not found yet, transform service cannot perform the request", exception.getMessage()); + assertEquals("Gateway not found yet, transform service cannot perform the request", ex.getMessage()); } - @Test - void givenHomePage_whenPathIsNotValid_thenThrowException() { + void givenInvalidPath_whenTransform_thenThrowException() { String url = "https://localhost:8080/wss"; + RoutedServices routes = buildRoutedServices( + new RoutedService(SERVICE_ID, WS_PREFIX, "/ws"), + new RoutedService(SERVICE_ID, "api/v1", "/") + ); - RoutedServices routedServices = new RoutedServices(); - RoutedService routedService1 = new RoutedService(SERVICE_ID, WS_PREFIX, "/ws"); - RoutedService routedService2 = new RoutedService(SERVICE_ID, "api/v1", "/"); - routedServices.addRoutedService(routedService1); - routedServices.addRoutedService(routedService2); - - TransformService transformService = new TransformService(gatewayClient); + URLTransformationException ex = assertThrows(URLTransformationException.class, + () -> transformService.transformURL(ServiceType.WS, SERVICE_ID, url, routes, false)); - Exception exception = assertThrows(URLTransformationException.class, () -> { - transformService.transformURL(ServiceType.WS, SERVICE_ID, url, routedServices, false); - }); - assertEquals("The path /wss of the service URL https://localhost:8080/wss is not valid.", exception.getMessage()); + assertEquals("The path /wss of the service URL https://localhost:8080/wss is not valid.", ex.getMessage()); } - @Test - void givenEmptyPathInHomePage_whenTransform_thenUseNewUrl() throws URLTransformationException { - String url = "https://localhost:8080/"; - - RoutedServices routedServices = new RoutedServices(); - RoutedService routedService1 = new RoutedService(SERVICE_ID, WS_PREFIX, "/"); - RoutedService routedService2 = new RoutedService(SERVICE_ID, "api/v1", "/"); - routedServices.addRoutedService(routedService1); - routedServices.addRoutedService(routedService2); - - TransformService transformService = new TransformService(gatewayClient); - - String actualUrl = transformService.transformURL(ServiceType.WS, SERVICE_ID, url, routedServices, false); - String expectedUrl = String.format("%s://%s/%s/%s%s", - gatewayClient.getGatewayConfigProperties().getScheme(), - gatewayClient.getGatewayConfigProperties().getHostname(), - SERVICE_ID, - WS_PREFIX, - "/"); - assertEquals(expectedUrl, actualUrl); - } - - @Test - void givenServiceUrl_whenItsRoot_thenKeepHomePagePathSame() throws URLTransformationException { - String url = "https://locahost:8080/test"; - RoutedServices routedServices = new RoutedServices(); - RoutedService routedService1 = new RoutedService(SERVICE_ID, UI_PREFIX, "/"); - RoutedService routedService2 = new RoutedService(SERVICE_ID, "api/v1", "/"); - routedServices.addRoutedService(routedService1); - routedServices.addRoutedService(routedService2); - - TransformService transformService = new TransformService(gatewayClient); - - String actualUrl = transformService.transformURL(ServiceType.UI, SERVICE_ID, url, routedServices, false); - String expectedUrl = String.format("%s://%s/%s/%s%s", - gatewayClient.getGatewayConfigProperties().getScheme(), - gatewayClient.getGatewayConfigProperties().getHostname(), - SERVICE_ID, - UI_PREFIX, - "/test"); - assertEquals(expectedUrl, actualUrl); - } - - @Test - void givenUrlContainingPathAndQuery_whenTransform_thenKeepQueryPartInTheNewUrl() throws URLTransformationException { - String url = "https://locahost:8080/ui/service/login.do?action=secure"; - String path = "/login.do?action=secure"; - RoutedServices routedServices = new RoutedServices(); - RoutedService routedService1 = new RoutedService(SERVICE_ID, UI_PREFIX, "/ui/service"); - RoutedService routedService2 = new RoutedService(SERVICE_ID, "api/v1", "/"); - routedServices.addRoutedService(routedService1); - routedServices.addRoutedService(routedService2); - - TransformService transformService = new TransformService(gatewayClient); - - String actualUrl = transformService.transformURL(ServiceType.UI, SERVICE_ID, url, routedServices, false); - String expectedUrl = String.format("%s://%s/%s/%s%s", - gatewayClient.getGatewayConfigProperties().getScheme(), - gatewayClient.getGatewayConfigProperties().getHostname(), - SERVICE_ID, - UI_PREFIX, - path); - assertEquals(expectedUrl, actualUrl); - } - - @Test - void givenServiceAndApiRoute_whenGetApiBasePath_thenReturnApiPath() throws URLTransformationException { - String url = "https://localhost:8080/" + SERVICE_ID + "/" + API_PREFIX + "/v1"; - - String serviceUrl = String.format("/%s/%s", SERVICE_ID, API_PREFIX); - RoutedServices routedServices = new RoutedServices(); - RoutedService routedService = new RoutedService(SERVICE_ID, API_PREFIX, serviceUrl); - routedServices.addRoutedService(routedService); - - TransformService transformService = new TransformService(null); - - String actualPath = transformService.retrieveApiBasePath(SERVICE_ID, url, routedServices); - String expectedPath = String.format("/%s/%s", - SERVICE_ID, - API_PREFIX); - assertEquals(expectedPath, actualPath); - } - - @Test - void givenServiceAndApiRouteWithVersion_whenGetApiBasePath_thenReturnApiPath() throws URLTransformationException { - String url = "https://localhost:8080/" + SERVICE_ID + "/" + API_PREFIX + "/v1"; - - String serviceUrl = String.format("/%s/%s/%s", SERVICE_ID, API_PREFIX, "v1"); - RoutedServices routedServices = new RoutedServices(); - RoutedService routedService = new RoutedService(SERVICE_ID, API_PREFIX + "/v1", serviceUrl); - routedServices.addRoutedService(routedService); + @ParameterizedTest + @CsvSource({ + // url, routePath, expected + "https://localhost:8080/service/api/v1,/service/api,/service/api/{api-version}", + "https://localhost:8080/service/api/v1,/service/api/v1,/service/api/{api-version}" + }) + void givenApiRoute_whenRetrieveApiBasePath_thenReturnExpected( + String url, String routePath, String expected + ) throws URLTransformationException { + RoutedServices routes = buildRoutedServices( + new RoutedService(SERVICE_ID, API_PREFIX + "/v1", routePath) + ); - TransformService transformService = new TransformService(null); + TransformService service = new TransformService(null); + String actual = service.retrieveApiBasePath(SERVICE_ID, url, routes); - String actualPath = transformService.retrieveApiBasePath(SERVICE_ID, url, routedServices); - String expectedPath = String.format("/%s/%s/%s", SERVICE_ID, API_PREFIX, "{api-version}"); - assertEquals(expectedPath, actualPath); + assertEquals(expected, actual); } @Test - void givenInvalidUriPath_whenGetApiBasePath_thenThrowError() { + void givenInvalidUrl_whenRetrieveApiBasePath_thenThrowException() { String url = "https:localhost:8080/wss"; + TransformService service = new TransformService(null); - TransformService transformService = new TransformService(null); + URLTransformationException ex = assertThrows(URLTransformationException.class, + () -> service.retrieveApiBasePath(null, url, null)); - Exception exception = assertThrows(URLTransformationException.class, () -> { - transformService.retrieveApiBasePath(null, url, null); - }); - assertEquals("The URI " + url + " is not valid.", exception.getMessage()); + assertEquals("The URI " + url + " is not valid.", ex.getMessage()); } @Test - void givenNoRoutes_whenGetApiBasePath_thenThrowError() { + void givenNoMatchingRoute_whenRetrieveApiBasePath_thenThrowException() { String url = "https://localhost:8080/u"; + RoutedServices routes = buildRoutedServices( + new RoutedService(SERVICE_ID, UI_PREFIX, "/ui"), + new RoutedService(SERVICE_ID, "api/v1", "/api") + ); - RoutedServices routedServices = new RoutedServices(); - RoutedService routedService1 = new RoutedService(SERVICE_ID, UI_PREFIX, "/ui"); - RoutedService routedService2 = new RoutedService(SERVICE_ID, "api/v1", "/api"); - routedServices.addRoutedService(routedService1); - routedServices.addRoutedService(routedService2); + TransformService service = new TransformService(null); - TransformService transformService = new TransformService(null); + URLTransformationException ex = assertThrows(URLTransformationException.class, + () -> service.retrieveApiBasePath(SERVICE_ID, url, routes)); - Exception exception = assertThrows(URLTransformationException.class, () -> { - transformService.retrieveApiBasePath(SERVICE_ID, url, routedServices); - }); - assertEquals("Not able to select API base path for the service " + SERVICE_ID + ". Original url used.", exception.getMessage()); + assertEquals("Not able to select API base path for the service " + SERVICE_ID + ". Original url used.", + ex.getMessage()); } @ParameterizedTest @@ -324,16 +213,17 @@ void givenNoRoutes_whenGetApiBasePath_thenThrowError() { "srv,wrong/url,api/v1,api/v1,", "srv,apiV1/home/page.html,api/v1,apiV1,/srv/api/{api-version}" }) - void testRetrieveApiBasePath(String serviceId, String url, String gatewayUrl, String serviceUrl, String expectedBasePath) { - RoutedService route = new RoutedService("api", gatewayUrl, serviceUrl); - - RoutedServices routedServices = new RoutedServices(); - routedServices.addRoutedService(route); - - TransformService transformService = new TransformService(null); + void testRetrieveApiBasePathParameterized( + String serviceId, String url, String gatewayUrl, String serviceUrl, String expectedBasePath + ) { + RoutedServices routes = buildRoutedServices( + new RoutedService("api", gatewayUrl, serviceUrl) + ); + + TransformService service = new TransformService(null); String basePath; try { - basePath = transformService.retrieveApiBasePath(serviceId, url, routedServices); + basePath = service.retrieveApiBasePath(serviceId, url, routes); } catch (URLTransformationException e) { basePath = null; } @@ -341,4 +231,24 @@ void testRetrieveApiBasePath(String serviceId, String url, String gatewayUrl, St assertEquals(expectedBasePath, basePath); } + @Test + void givenLocationUriNotMatchingServiceRoutes_whenTransformAbsoluteURL_thenThrowException() { + + RoutedService route = new RoutedService("dcpassticket", "api", "/api"); + + var ex = assertThrows(URLTransformationException.class, () -> + transformService.transformAbsoluteURL("dcpassticket", "/apisome-path", route)); + + assertEquals("The path /apisome-path of the service dcpassticket is not valid.", ex.getMessage()); + } + + @Test + void givenMatchingLocation_whenTransformAbsoluteURL_thenReturnAbsoluteURL() throws URLTransformationException { + + RoutedService route = new RoutedService("dcpassticket", "api", "/api"); + + var result = transformService.transformAbsoluteURL("dcpassticket", "/api/v1/some-path", route); + assertEquals("/dcpassticket/api/v1/some-path", result); + } + } diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactory.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactory.java index 371aa83804..f9659801ea 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactory.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactory.java @@ -74,14 +74,14 @@ public GatewayFilter apply(Config config) { .then(Mono.defer(() -> processNewLocationUrl(exchange, config, instance))); } - private URI getHostUri(ServiceInstance instance) { + private URI getHostAndPortUri(ServiceInstance instance) { return UriComponentsBuilder.newInstance() .host(instance.getHost()) .port(instance.getPort()) .build().toUri(); } - private URI getHostUri(URI uri) { + private URI getHostAndPortUri(URI uri) { return UriComponentsBuilder.newInstance() .host(uri.getHost()) .port(uri.getPort()) @@ -93,18 +93,29 @@ private Optional getInstance(URI locationUri, Optional getHostUri(i).equals(hostUri)).orElse(false)) { + var locationHostAndPortUri = getHostAndPortUri(locationUri); + if (matchesInstance(instance, locationHostAndPortUri)) { return instance; } return discoveryClient.getServices().stream() .map(discoveryClient::getInstances) .flatMap(List::stream) - .filter(i -> hostUri.equals(getHostUri(i))) + .filter(i -> locationHostAndPortUri.equals(getHostAndPortUri(i))) .findFirst(); } + /** + * Compares URI with instance + * + * @param instance + * @param hostAndPortUri + * @return true if URI equals URI from instance + */ + private boolean matchesInstance(Optional instance, URI hostAndPortUri) { + return instance.map(i -> getHostAndPortUri(i).equals(hostAndPortUri)).orElse(false); + } + private String normalizePath(String path) { if (!path.startsWith(SLASH)) { path = SLASH + path; @@ -130,12 +141,14 @@ private Mono processNewLocationUrl(ServerWebExchange exchange, Config conf var location = response.getHeaders().getFirst(HttpHeaders.LOCATION); if (StringUtils.isBlank(location)) { + log.debug("Location header is empty"); return Mono.empty(); } var locationUri = URI.create(location); var targetInstance = getInstance(locationUri, instance); if (isGateway(targetInstance)) { + log.debug("Target instance is Gateway. Location header was not translated. {}", locationUri); return Mono.empty(); } var defaultRoute = config.getRoutedService(); @@ -147,9 +160,9 @@ private Mono processNewLocationUrl(ServerWebExchange exchange, Config conf newUrl.set(transformService.transformAbsoluteURL( StringUtils.toRootLowerCase(config.serviceId), UriComponentsBuilder.fromPath(locationUri.getPath()).query(locationUri.getRawQuery()).build().toUriString(), - defaultRoute, - locationUri + defaultRoute )); + log.debug("Location is matching service URL. New Location header value is: {}", newUrl.get()); } catch (URLTransformationException e) { log.debug("Cannot transform URL on the same route", e); return Mono.empty(); @@ -169,6 +182,7 @@ private Mono processNewLocationUrl(ServerWebExchange exchange, Config conf routes, false )); + log.debug("Target instance: {}. New Location header value is: {}", i.getInstanceId(), newUrl.get()); } catch (URLTransformationException e) { log.debug("Cannot transform URL", e); } @@ -177,8 +191,9 @@ private Mono processNewLocationUrl(ServerWebExchange exchange, Config conf if (newUrl.get() != null) { // if the new URL was defined, decorate (scheme by AT-TLS) and set - if (isServerAttlsEnabled && newUrl.get().startsWith("http")) { + if (isServerAttlsEnabled && newUrl.get().startsWith("http://")) { newUrl.set(UriComponentsBuilder.fromUriString(newUrl.get()).scheme("https").build().toUriString()); + log.debug("AT-TLS server is enabled. Location url was updated with: {}", newUrl.get()); } exchange.getResponse().getHeaders().set(HttpHeaders.LOCATION, newUrl.toString()); diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/RedirectTest.java b/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/RedirectTest.java index 3bdfe3b473..91bcede7f2 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/RedirectTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/RedirectTest.java @@ -43,12 +43,12 @@ static void init() throws Exception { static Stream headerValues() { return Stream.of( Arguments.of( - "absolute URL with encoding doesn't match service route", + "relative URL with encoding", "%2Fapi%2Frequest", "%2Fapi%2Frequest" ), Arguments.of( - "absolute URL with encoding doesn't match service route", + "relative URL with encoding and service route", "%2Fdiscoverableclient%2Fapi%2Fv1%2Frequest", "%2Fdiscoverableclient%2Fapi%2Fv1%2Frequest" ), From a2544ec27e23d077ead3dd26d01f48d7de859298 Mon Sep 17 00:00:00 2001 From: ac892247 Date: Fri, 26 Sep 2025 22:34:39 +0200 Subject: [PATCH 13/14] when serviceID is gateway Signed-off-by: ac892247 --- .../PageRedirectionFilterFactoryTest.java | 41 ++++++++++++++----- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactoryTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactoryTest.java index aa9f367d16..7dfd34785a 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactoryTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactoryTest.java @@ -26,6 +26,7 @@ import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.web.server.ServerWebExchange; +import org.zowe.apiml.product.constants.CoreService; import org.zowe.apiml.product.gateway.GatewayClient; import org.zowe.apiml.product.instance.ServiceAddress; import reactor.core.publisher.Mono; @@ -46,6 +47,8 @@ class PageRedirectionFilterFactoryTest { + public static final String DISCOVERABLECLIENT = "DISCOVERABLECLIENT"; + public static final String GATEWAY = CoreService.GATEWAY.toString(); private GatewayClient gatewayClient; private DiscoveryClient discoveryClient; private static final String GW_HOSTNAME = "gateway"; @@ -79,25 +82,27 @@ void setUp() { } - private PageRedirectionFilterFactory.Config createConfig() { + private PageRedirectionFilterFactory.Config createConfig(String serviceId) { var config = new PageRedirectionFilterFactory.Config(); - config.setInstanceId("localhost:discoverableclient:10012"); - config.setServiceId("DISCOVERABLECLIENT"); + config.setInstanceId("localhost:" + serviceId.toLowerCase() + ":10012"); + config.setServiceId(serviceId); config.setGatewayUrl("api/v1"); config.setServiceUrl("/discoverableclient"); return config; } - private void mockServiceInstance() { + private void mockServiceInstance(String serviceId) { Map metadata = new HashMap<>(); metadata.put(ROUTES + ".api-v1." + ROUTES_GATEWAY_URL, "api/v1"); metadata.put(ROUTES + ".api-v1." + ROUTES_SERVICE_URL, "/"); when(serviceInstance.getMetadata()).thenReturn(metadata); when(serviceInstance.getInstanceId()).thenReturn("instanceId"); + when(serviceInstance.getServiceId()).thenReturn(serviceId); when(serviceInstance.getHost()).thenReturn("localhost"); when(serviceInstance.getPort()).thenReturn(10010); - when(discoveryClient.getInstances("DISCOVERABLECLIENT")).thenReturn(new ArrayList<>(Collections.singletonList(serviceInstance))); + when(discoveryClient.getInstances(serviceId)).thenReturn(new ArrayList<>(Collections.singletonList(serviceInstance))); + when(discoveryClient.getServices()).thenReturn(Collections.singletonList(serviceId)); } void mockLocationHeaderResponse(String url) { @@ -128,7 +133,7 @@ void whenNoAttls_thenAddRedirectionUrl(String expectedUrl, String originalUrl, b ReflectionTestUtils.setField(factory, "isServerAttlsEnabled", attlsEnabled); mockLocationHeaderResponse(originalUrl); - var config = createConfig(); + var config = createConfig(DISCOVERABLECLIENT); GatewayFilter gatewayFilter = factory.apply(config); StepVerifier.create(gatewayFilter.filter(exchange, chain)) @@ -138,6 +143,20 @@ void whenNoAttls_thenAddRedirectionUrl(String expectedUrl, String originalUrl, b assertEquals(expectedUrl, res.getHeaders().getFirst(HttpHeaders.LOCATION)); } + @Test + void whenTargetInstanceIsGateway_thenDoNotUpdate() { + var url = "https://localhost:10010/discoverableclient/api/v1/redirected_url?arg=1&arg=2"; + mockLocationHeaderResponse(url); + var config = createConfig(GATEWAY); + mockServiceInstance(GATEWAY); + GatewayFilter gatewayFilter = factory.apply(config); + StepVerifier.create(gatewayFilter.filter(exchange, chain)) + .thenAwait() + .expectComplete() + .verify(); + assertEquals(url, res.getHeaders().getFirst(HttpHeaders.LOCATION)); + } + } @Nested @@ -148,8 +167,8 @@ void thenDoNotTransform() { var expectedUrl = GW_BASE_URL + "/api/v1/redirected_url"; mockLocationHeaderResponse(expectedUrl); - mockServiceInstance(); - var config = createConfig(); + mockServiceInstance(DISCOVERABLECLIENT); + var config = createConfig(DISCOVERABLECLIENT); when(gatewayClient.isInitialized()).thenReturn(false); StepVerifier.create(factory.apply(config).filter(exchange, chain)).expectComplete().verify(); @@ -165,7 +184,7 @@ void thenDoNotTransform() { var header = new HttpHeaders(); header.put(HttpHeaders.LOCATION, Collections.emptyList()); when(res.getHeaders()).thenReturn(header); - var config = createConfig(); + var config = createConfig(DISCOVERABLECLIENT); StepVerifier.create(factory.apply(config).filter(exchange, chain)).expectComplete().verify(); assertNull(res.getHeaders().getFirst(HttpHeaders.LOCATION)); @@ -182,8 +201,8 @@ void thenDoNotTransform() { when(res.getHeaders()).thenReturn(header); when(res.getStatusCode()).thenReturn(HttpStatusCode.valueOf(HttpStatus.SC_CONTINUE)); - mockServiceInstance(); - var config = createConfig(); + mockServiceInstance(DISCOVERABLECLIENT); + var config = createConfig(DISCOVERABLECLIENT); StepVerifier.create(factory.apply(config).filter(exchange, chain)).expectComplete().verify(); assertNull(res.getHeaders().getFirst(HttpHeaders.LOCATION)); From 5718bcff15c199a332842c19f311c63b46b122b0 Mon Sep 17 00:00:00 2001 From: ac892247 Date: Fri, 26 Sep 2025 22:54:58 +0200 Subject: [PATCH 14/14] test scheme relative transformation Signed-off-by: ac892247 --- .../routing/transform/TransformService.java | 14 +++++--------- .../routing/transform/TransformServiceTest.java | 14 ++++++++++++-- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/apiml-common/src/main/java/org/zowe/apiml/product/routing/transform/TransformService.java b/apiml-common/src/main/java/org/zowe/apiml/product/routing/transform/TransformService.java index 5a19532e9c..848149d9f2 100644 --- a/apiml-common/src/main/java/org/zowe/apiml/product/routing/transform/TransformService.java +++ b/apiml-common/src/main/java/org/zowe/apiml/product/routing/transform/TransformService.java @@ -78,16 +78,12 @@ public String transformURL(ServiceType type, return transformURL(serviceId, serviceUriPath, route, httpsScheme, serviceUri); } - public String transformURL(String serviceId, - String serviceUriPath, - RoutedService route, - boolean httpsScheme, - URI originalUri + private String transformURL(String serviceId, + String serviceUriPath, + RoutedService route, + boolean httpsScheme, + URI originalUri ) throws URLTransformationException { - if (!gatewayClient.isInitialized()) { - apimlLog.log("org.zowe.apiml.common.gatewayNotFoundForTransformRequest"); - throw new URLTransformationException("Gateway not found yet, transform service cannot perform the request"); - } String endPoint = getShortEndpoint(route.getServiceUrl(), serviceUriPath); if (!endPoint.isEmpty() && !endPoint.startsWith("/")) { diff --git a/apiml-common/src/test/java/org/zowe/apiml/product/routing/transform/TransformServiceTest.java b/apiml-common/src/test/java/org/zowe/apiml/product/routing/transform/TransformServiceTest.java index ad222aa3b4..b41ff2c72f 100644 --- a/apiml-common/src/test/java/org/zowe/apiml/product/routing/transform/TransformServiceTest.java +++ b/apiml-common/src/test/java/org/zowe/apiml/product/routing/transform/TransformServiceTest.java @@ -37,7 +37,7 @@ class TransformServiceTest { void setup() { ServiceAddress gatewayConfig = ServiceAddress.builder() .scheme("https") - .hostname("localhost") + .hostname("localhost:10010") .build(); gatewayClient = new GatewayClient(gatewayConfig); transformService = new TransformService(gatewayClient); @@ -80,6 +80,16 @@ void givenValidRoutes_whenTransform_thenReturnExpectedUrl( assertEquals(expectedUrl(SERVICE_ID, prefix, extraPath), actual); } + @Test + void givenSchemeRelativeUrl_whenTransform_thenKeepSchemeRelativeUrl() throws URLTransformationException { + RoutedServices routes = buildRoutedServices( + new RoutedService(SERVICE_ID, "api", "/api"), + new RoutedService(SERVICE_ID, "api/v1", "/") + ); + String actual = transformService.transformURL(ServiceType.API, SERVICE_ID, "//localhost:8080/api", routes, false); + assertEquals("//localhost:10010/service/api", actual); + } + @ParameterizedTest @CsvSource({ "https://localhost:8080/,WS,ws,'/'", @@ -105,7 +115,7 @@ void givenHttpUrlAndAttlsEnabled_whenTransform_thenForceHttps() throws URLTransf String actual = transformService.transformURL(ServiceType.UI, SERVICE_ID, url, routes, true); - assertEquals("https://localhost/service/ui", actual); + assertEquals("https://localhost:10010/service/ui", actual); } @Test