diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/ApiDocRetrievalServiceLocal.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/ApiDocRetrievalServiceLocal.java index d7e4f7e312..c04753dcca 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/ApiDocRetrievalServiceLocal.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/ApiDocRetrievalServiceLocal.java @@ -33,6 +33,7 @@ import org.zowe.apiml.apicatalog.model.ApiDocInfo; import org.zowe.apiml.config.ApiInfo; import org.zowe.apiml.product.gateway.GatewayClient; +import org.zowe.apiml.util.UrlUtils; import reactor.core.publisher.Mono; import java.util.*; @@ -78,7 +79,7 @@ springDocProviders, new SpringDocCustomizers(Optional.of(openApiCustomizers), Op @Override protected String getServerUrl(ServerHttpRequest serverHttpRequest, String apiDocsUrl) { var gw = gatewayClient.getGatewayConfigProperties(); - return String.format("%s://%s%s", gw.getScheme(), gw.getHostname(), apiDocsUrl); + return UrlUtils.getUrl(gw.getScheme(), gw.getHostname()) + apiDocsUrl; } }; diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/api/ApiDocV3Service.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/api/ApiDocV3Service.java index 5471cb6711..f4b145691c 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/api/ApiDocV3Service.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/api/ApiDocV3Service.java @@ -41,6 +41,7 @@ import org.zowe.apiml.product.gateway.GatewayClient; import org.zowe.apiml.product.instance.ServiceAddress; import org.zowe.apiml.product.routing.RoutedService; +import org.zowe.apiml.util.UrlUtils; import java.net.URI; import java.util.Collections; @@ -106,7 +107,7 @@ private void updateServer(OpenAPI openAPI) { if (openAPI.getServers() != null) { openAPI.getServers() .forEach(server -> server.setUrl( - String.format("%s://%s/%s", scheme, getHostname(), server.getUrl()))); + UrlUtils.getUrl(scheme, UrlUtils.formatHostnameForUrl(getHostname())) + "/" + server.getUrl())); } } diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/swagger/api/ApiDocV3ServiceTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/swagger/api/ApiDocV3ServiceTest.java index de5b4b66e5..37f5d69e2e 100644 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/swagger/api/ApiDocV3ServiceTest.java +++ b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/swagger/api/ApiDocV3ServiceTest.java @@ -250,7 +250,10 @@ private void verifyOpenApi3(OpenAPI openAPI) { @Test void givenInputFile_thenParseItCorrectly() throws IOException { - ServiceAddress gatewayConfigProperties = ServiceAddress.builder().scheme("https").hostname("localhost").build(); + ServiceAddress gatewayConfigProperties = ServiceAddress.builder() + .scheme("https") + .hostname("localhost:10010") + .build(); gatewayClient.setGatewayConfigProperties(gatewayConfigProperties); AtomicReference openApiHolder = new AtomicReference<>(); @@ -261,6 +264,9 @@ protected void updateExternalDoc(OpenAPI openAPI, ApiDocInfo apiDocInfo) { openApiHolder.set(openAPI); } }; + // Set the scheme field for the new ApiDocV3Service instance + ReflectionTestUtils.setField(apiDocV3Service, "scheme", "https"); + String transformed = apiDocV3Service.transformApiDoc("serviceId", ApiDocInfo.builder() .apiInfo(mock(ApiInfo.class)) .apiDocContent(IOUtils.toString(new ClassPathResource("swagger/openapi3.json").getInputStream(), StandardCharsets.UTF_8)) diff --git a/apiml/src/main/java/org/zowe/apiml/ModulithConfig.java b/apiml/src/main/java/org/zowe/apiml/ModulithConfig.java index ce304f571b..fd803de122 100644 --- a/apiml/src/main/java/org/zowe/apiml/ModulithConfig.java +++ b/apiml/src/main/java/org/zowe/apiml/ModulithConfig.java @@ -32,6 +32,7 @@ import org.apache.catalina.connector.Connector; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.InitializingBean; +import org.zowe.apiml.util.UrlUtils; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.event.ApplicationReadyEvent; @@ -172,11 +173,14 @@ private InstanceInfo getInstanceInfo(String serviceId) { scheme = "http"; } + // Format hostname for IPv6 addresses (use brackets) + String formattedHostname = UrlUtils.formatHostnameForUrl(hostname); + return InstanceInfo.Builder.newBuilder() - .setInstanceId(String.format("%s:%s:%d", hostname, serviceId, port)) + .setInstanceId(String.format("%s:%s:%d", formattedHostname, serviceId, port)) .setAppName(serviceId) .setHostName(hostname) - .setHomePageUrl(null, String.format("%s://%s:%d%s", scheme, hostname, port, homePagePath)) + .setHomePageUrl(null, String.format("%s://%s:%d%s", scheme, formattedHostname, port, homePagePath)) .setStatus(InstanceInfo.InstanceStatus.UP) .setIPAddr(ipAddress) .setPort(port) diff --git a/common-service-core/src/main/java/org/zowe/apiml/util/EurekaUtils.java b/common-service-core/src/main/java/org/zowe/apiml/util/EurekaUtils.java index b460c19806..d7f78a9a25 100644 --- a/common-service-core/src/main/java/org/zowe/apiml/util/EurekaUtils.java +++ b/common-service-core/src/main/java/org/zowe/apiml/util/EurekaUtils.java @@ -34,25 +34,56 @@ public class EurekaUtils { public static final Pattern SERVICE_ID_PATTERN = Pattern.compile("^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$"); /** - * Extract serviceId from instanceId - * @param instanceId input, instanceId in format "host:service:random number to unique instanceId" - * @return second part, it means serviceId. If it doesn't exist return null; + * Extract serviceId from instanceId. + * The instanceId format is "hostname:serviceId:port". + * For IPv6 addresses (which contain colons), the hostname may be bracketed like "[2001:db8::1]". + * @param instanceId input, instanceId in format "host:service:port" or "[ipv6]:service:port" + * @return the serviceId part. If it doesn't exist or format is invalid, return null. */ public String getServiceIdFromInstanceId(String instanceId) { if (StringUtils.isBlank(instanceId)) { return null; } - String[] parts = instanceId.split(":"); - if (parts.length != 3) { + + if (instanceId.startsWith("[")) { + int closingBracket = instanceId.indexOf("]"); + if (closingBracket > 0 && closingBracket < instanceId.length() - 1) { + // After the closing bracket, we expect :serviceId:port + String afterBracket = instanceId.substring(closingBracket + 1); + if (afterBracket.startsWith(":")) { + String[] remainingParts = afterBracket.substring(1).split(":"); + if (remainingParts.length == 2) { + String serviceId = remainingParts[0].trim(); + return serviceId.isEmpty() ? null : serviceId; + } + } + } + return null; + } + + // For non-bracketed addresses, we need to handle both: + // 1. hostname:serviceId:port (3 parts) + // 2. ipv6Address:serviceId:port (many colons from IPv6) + // Parse from the end: last part is port, second-to-last is serviceId + int lastColon = instanceId.lastIndexOf(':'); + if (lastColon <= 0) { + return null; + } + + // Validate that port part is not empty + String portPart = instanceId.substring(lastColon + 1).trim(); + if (portPart.isEmpty()) { return null; } - String serviceId = parts[1].trim(); - if (serviceId.isEmpty()) { + String beforeLastColon = instanceId.substring(0, lastColon); + int secondLastColon = beforeLastColon.lastIndexOf(':'); + if (secondLastColon < 0) { return null; } - return serviceId; + String serviceId = beforeLastColon.substring(secondLastColon + 1).trim(); + return serviceId.isEmpty() ? null : serviceId; } /** @@ -74,15 +105,17 @@ public void validateServiceId(String serviceId) { } /** - * Construct base URL for specific InstanceInfo + * Construct base URL for specific InstanceInfo. + * Handles IPv6 addresses by formatting the hostname with brackets if needed. * @param instanceInfo Instance of service, for which we want to get an URL * @return URL to the instance */ public String getUrl(InstanceInfo instanceInfo) { + String formattedHostname = UrlUtils.formatHostnameForUrl(instanceInfo.getHostName()); if (instanceInfo.getSecurePort() == 0 || !instanceInfo.isPortEnabled(InstanceInfo.PortType.SECURE)) { - return "http://" + instanceInfo.getHostName() + ":" + instanceInfo.getPort(); + return "http://" + formattedHostname + ":" + instanceInfo.getPort(); } else { - return "https://" + instanceInfo.getHostName() + ":" + instanceInfo.getSecurePort(); + return "https://" + formattedHostname + ":" + instanceInfo.getSecurePort(); } } diff --git a/common-service-core/src/main/java/org/zowe/apiml/util/UrlUtils.java b/common-service-core/src/main/java/org/zowe/apiml/util/UrlUtils.java index 307b1aa7db..977f3b9e07 100644 --- a/common-service-core/src/main/java/org/zowe/apiml/util/UrlUtils.java +++ b/common-service-core/src/main/java/org/zowe/apiml/util/UrlUtils.java @@ -112,4 +112,187 @@ public boolean isValidUrl(String urlString) { return false; } } + + /** + * Determines if a given string is an IPv6 address. + * + * @param address The string to check + * @return true if the address is an IPv6 address, false otherwise + */ + private boolean isIPv6Address(String address) { + try { + return InetAddress.getByName(address) instanceof Inet6Address; + } catch (UnknownHostException e) { + return false; + } + } + + /** + * Validates if a string represents a valid port number. + * + * @param port The string to validate as a port number + * @return true if the string represents a valid port, false otherwise + */ + private boolean isValidPort(String port) { + + if (port == null || port.isEmpty()) { + return false; + } + + try { + int portNum = Integer.parseInt(port); + return portNum >= 0 && portNum <= 65535; + } catch (NumberFormatException e) { + return false; + } + } + + /** + * Formats a hostname properly, ensuring IPv6 addresses are enclosed in square brackets. + * If the input is already a properly formatted IPv6 address (with brackets), it remains unchanged. + * Handles both IPv6 addresses and hostname:port combinations. + * + * @param hostname The hostname or IP address to format + * @return Properly formatted hostname, with IPv6 addresses enclosed in square brackets + */ + public String formatHostnameForUrl(String hostname) { + if (hostname == null || hostname.isEmpty()) { + return hostname; + } + + // If already properly formatted with brackets, return as is + if (hostname.startsWith("[") && hostname.contains("]")) { + return hostname; + } + + // FIRST: Check if the ENTIRE string is a valid IPv6 address + // This must be done BEFORE attempting to split by colon + // because IPv6 addresses contain colons as part of the address + if (isIPv6Address(hostname)) { + return "[" + hostname + "]"; + } + + // Not a pure IPv6 address - check for hostname:port format + // by looking at the last colon + int lastColonIndex = hostname.lastIndexOf(':'); + if (lastColonIndex > -1) { + String possibleHost = hostname.substring(0, lastColonIndex); + String possiblePort = hostname.substring(lastColonIndex + 1); + + // Check if what follows the last colon is a valid port number + if (isValidPort(possiblePort)) { + // If we have a valid port, check if the host part is IPv6 + if (isIPv6Address(possibleHost)) { + return "[" + possibleHost + "]:" + possiblePort; + } + // Not IPv6, return as-is (hostname:port or IPv4:port) + return hostname; + } + } + + return hostname; + } + + /** + * Creates a proper URL string with scheme, hostname, and port, + * handling IPv6 addresses correctly. + * + * @param scheme The URL scheme (http, https, etc.) + * @param hostname The hostname or IP address + * @param port The port number + * @return A properly formatted URL string with IPv6 address handling + */ + public String getUrl(String scheme, String hostname, int port) { + String formattedHostname = formatHostnameForUrl(hostname); + return String.format("%s://%s:%d", scheme, formattedHostname, port); + } + + /** + * Creates a proper URL string with scheme and host (which may include port), + * handling IPv6 addresses correctly. + * + * @param scheme The URL scheme (http, https, etc.) + * @param hostWithPort The hostname or IP address, possibly including a port + * @return A properly formatted URL string with IPv6 address handling + */ + public String getUrl(String scheme, String hostWithPort) { + if (scheme == null || scheme.isEmpty()) { + throw new IllegalArgumentException("Scheme cannot be null or empty"); + } + + if (hostWithPort == null || hostWithPort.isEmpty()) { + throw new IllegalArgumentException("Host cannot be null or empty"); + } + + // Remove any existing scheme if present + String cleanHostWithPort = hostWithPort.replaceFirst("^[a-zA-Z][a-zA-Z0-9+.-]*://", ""); + + // Format the hostname part properly + String formattedHost = formatHostnameForUrl(cleanHostWithPort); + + return String.format("%s://%s", scheme, formattedHost); + } + + /** + * Formats a URL string to ensure IPv6 addresses are properly bracketed. + *

+ * For example, if a URL is constructed as "https://2001:db8::1:8080/path", + * this method will convert it to "https://[2001:db8::1]:8080/path". + * + * @param urlString The URL string that may contain an un-bracketed IPv6 address + * @return A properly formatted URL with IPv6 addresses enclosed in brackets, + * or the original string if it's not a valid URL or doesn't need formatting + */ + public String formatUrlWithIPv6Support(String urlString) { + if (urlString == null || urlString.isEmpty()) { + return urlString; + } + + if (urlString.contains("[") && urlString.contains("]")) { + return urlString; + } + + int schemeEnd = urlString.indexOf("://"); + if (schemeEnd == -1) { + return urlString; // Not a URL with scheme + } + + String scheme = urlString.substring(0, schemeEnd); + String rest = urlString.substring(schemeEnd + 3); // Skip "://" + + // Find where the host:port ends (first slash or end of string) + int pathStart = rest.indexOf('/'); + String hostPort; + String pathAndQuery; + if (pathStart == -1) { + hostPort = rest; + pathAndQuery = ""; + } else { + hostPort = rest.substring(0, pathStart); + pathAndQuery = rest.substring(pathStart); + } + + // Check if hostPort contains an IPv6 address (multiple colons without brackets) + long colonCount = hostPort.chars().filter(ch -> ch == ':').count(); + if (colonCount > 1) { + // Find the port by looking for the last segment that's a valid port number + int lastColon = hostPort.lastIndexOf(':'); + if (lastColon > 0) { + String possiblePort = hostPort.substring(lastColon + 1); + if (isValidPort(possiblePort)) { + // Everything before the last colon is the IPv6 address + String ipv6Address = hostPort.substring(0, lastColon); + if (isIPv6Address(ipv6Address)) { + return scheme + "://[" + ipv6Address + "]:" + possiblePort + pathAndQuery; + } + } + } + // If no valid port found, the entire hostPort might be an IPv6 address + if (isIPv6Address(hostPort)) { + return scheme + "://[" + hostPort + "]" + pathAndQuery; + } + } + + return urlString; + } } diff --git a/common-service-core/src/test/java/org/zowe/apiml/util/EurekaUtilsTest.java b/common-service-core/src/test/java/org/zowe/apiml/util/EurekaUtilsTest.java index 64070e7002..35fe3c2d98 100644 --- a/common-service-core/src/test/java/org/zowe/apiml/util/EurekaUtilsTest.java +++ b/common-service-core/src/test/java/org/zowe/apiml/util/EurekaUtilsTest.java @@ -35,15 +35,77 @@ class EurekaUtilsTest { - @Test - void test() { - assertEquals("abc", EurekaUtils.getServiceIdFromInstanceId("123:abc:def")); - assertNull(EurekaUtils.getServiceIdFromInstanceId("123:abc:def:::::xyz")); - assertNull(EurekaUtils.getServiceIdFromInstanceId("hostname:123:")); - assertNull(EurekaUtils.getServiceIdFromInstanceId("::")); - assertNull(EurekaUtils.getServiceIdFromInstanceId("123::def")); - assertNull(EurekaUtils.getServiceIdFromInstanceId(":")); - assertNull(EurekaUtils.getServiceIdFromInstanceId("")); + @Nested + class GetServiceIdFromInstanceIdTests { + + @Test + void givenStandardInstanceId_thenExtractServiceId() { + assertEquals("abc", EurekaUtils.getServiceIdFromInstanceId("hostname:abc:123")); + assertEquals("service", EurekaUtils.getServiceIdFromInstanceId("host:service:8080")); + assertEquals("my-service", EurekaUtils.getServiceIdFromInstanceId("my.host.com:my-service:443")); + } + + @Test + void givenIPv4InstanceId_thenExtractServiceId() { + assertEquals("gateway", EurekaUtils.getServiceIdFromInstanceId("192.168.1.1:gateway:8080")); + assertEquals("api", EurekaUtils.getServiceIdFromInstanceId("10.0.0.1:api:443")); + assertEquals("service", EurekaUtils.getServiceIdFromInstanceId("127.0.0.1:service:80")); + } + + @Test + void givenBracketedIPv6InstanceId_thenExtractServiceId() { + // IPv6 address with brackets: [ipv6]:serviceId:port + assertEquals("gateway", EurekaUtils.getServiceIdFromInstanceId("[2620:117:10:4300::55:28]:gateway:12314")); + assertEquals("discovery", EurekaUtils.getServiceIdFromInstanceId("[::1]:discovery:8080")); + assertEquals("service", EurekaUtils.getServiceIdFromInstanceId("[2001:db8::1]:service:443")); + assertEquals("api", EurekaUtils.getServiceIdFromInstanceId("[fe80::1]:api:8080")); + assertEquals("svc", EurekaUtils.getServiceIdFromInstanceId("[::]:svc:80")); // unspecified address + } + + @Test + void givenUnbracketedIPv6InstanceId_thenExtractServiceIdFromEnd() { + // Unbracketed IPv6 address: parse from the end (port is last, serviceId is second-to-last) + assertEquals("gateway", EurekaUtils.getServiceIdFromInstanceId("2620:117:10:4300::55:28:gateway:12314")); + assertEquals("apicatalog", EurekaUtils.getServiceIdFromInstanceId("2620:117:10:4300::55:28:apicatalog:12312")); + assertEquals("discovery", EurekaUtils.getServiceIdFromInstanceId("::1:discovery:8080")); // loopback + assertEquals("service", EurekaUtils.getServiceIdFromInstanceId("2001:db8::1:service:443")); + assertEquals("api", EurekaUtils.getServiceIdFromInstanceId("fe80::1:api:8080")); // link-local + } + + @Test + void givenInvalidInstanceId_thenReturnNull() { + assertNull(EurekaUtils.getServiceIdFromInstanceId("hostname:123:")); // empty port + assertNull(EurekaUtils.getServiceIdFromInstanceId("::")); + assertNull(EurekaUtils.getServiceIdFromInstanceId(":")); + assertNull(EurekaUtils.getServiceIdFromInstanceId("")); + assertNull(EurekaUtils.getServiceIdFromInstanceId(null)); + assertNull(EurekaUtils.getServiceIdFromInstanceId("onlyhostname")); + assertNull(EurekaUtils.getServiceIdFromInstanceId("hostname:service")); // only 2 parts + } + + @Test + void givenEmptyServiceId_thenReturnNull() { + assertNull(EurekaUtils.getServiceIdFromInstanceId("hostname::123")); // empty serviceId + assertNull(EurekaUtils.getServiceIdFromInstanceId("192.168.1.1::8080")); // IPv4 with empty serviceId + } + + @Test + void givenMalformedBracketedIPv6_thenReturnNull() { + assertNull(EurekaUtils.getServiceIdFromInstanceId("[2001:db8::1]")); // no serviceId/port after brackets + assertNull(EurekaUtils.getServiceIdFromInstanceId("[2001:db8::1]:")); // missing serviceId and port + assertNull(EurekaUtils.getServiceIdFromInstanceId("[2001:db8::1]:service")); // missing port + assertNull(EurekaUtils.getServiceIdFromInstanceId("[2001:db8::1]service:8080")); // missing colon after bracket + assertNull(EurekaUtils.getServiceIdFromInstanceId("[2001:db8::1]:svc:extra:8080")); // extra colons + assertNull(EurekaUtils.getServiceIdFromInstanceId("[]::8080")); // empty brackets + } + + @Test + void givenEdgeCaseHostnames_thenExtractServiceId() { + // Empty hostname is technically allowed (extracts serviceId correctly) + assertEquals("service", EurekaUtils.getServiceIdFromInstanceId(":service:8080")); + // Very long serviceId + assertEquals("a".repeat(63), EurekaUtils.getServiceIdFromInstanceId("host:" + "a".repeat(63) + ":8080")); + } } private InstanceInfo createInstanceInfo(String host, int port, int securePort, boolean isSecureEnabled) { @@ -58,10 +120,22 @@ private InstanceInfo createInstanceInfo(String host, int port, int securePort, b @Test void testGetUrl() { InstanceInfo ii1 = createInstanceInfo("hostname1", 80, 0, false); - InstanceInfo ii2 = createInstanceInfo("locahost", 80, 443, true); + InstanceInfo ii2 = createInstanceInfo("localhost", 80, 443, true); assertEquals("http://hostname1:80", EurekaUtils.getUrl(ii1)); - assertEquals("https://locahost:443", EurekaUtils.getUrl(ii2)); + assertEquals("https://localhost:443", EurekaUtils.getUrl(ii2)); + } + + @Test + void testGetUrlWithIPv6() { + // IPv6 addresses should be wrapped in brackets in URLs + InstanceInfo iiIPv6Http = createInstanceInfo("2620:117:10:4300::55:28", 8080, 0, false); + InstanceInfo iiIPv6Https = createInstanceInfo("2001:db8::1", 80, 443, true); + InstanceInfo iiIPv6Loopback = createInstanceInfo("::1", 8080, 0, false); + + assertEquals("http://[2620:117:10:4300::55:28]:8080", EurekaUtils.getUrl(iiIPv6Http)); + assertEquals("https://[2001:db8::1]:443", EurekaUtils.getUrl(iiIPv6Https)); + assertEquals("http://[::1]:8080", EurekaUtils.getUrl(iiIPv6Loopback)); } @Nested diff --git a/discovery-service/src/main/java/org/zowe/apiml/discovery/ApimlInstanceRegistry.java b/discovery-service/src/main/java/org/zowe/apiml/discovery/ApimlInstanceRegistry.java index accb1f0ab3..9bdb68814d 100644 --- a/discovery-service/src/main/java/org/zowe/apiml/discovery/ApimlInstanceRegistry.java +++ b/discovery-service/src/main/java/org/zowe/apiml/discovery/ApimlInstanceRegistry.java @@ -258,7 +258,8 @@ public void register(InstanceInfo info, final boolean isReplication) { * Only lowercase letters, digits, and hyphens allowed, must not start or end with a hyphen, and must not exceed 63 characters. * Unfortunately the java enabler converts the appName to uppercase when sending the registration request. * Therefore, the validation is case-insensitive. - * The instanceId must follow the format 'hostname:serviceId:port'. + * The instanceId must follow the format 'hostname:serviceId:port' for IPv4/hostnames, + * or '[ipv6address]:serviceId:port' for IPv6 addresses (with brackets around the IPv6 address). * The serviceId extracted from the instanceId must match the appName, the check is again case-insensitive for the reason * described above. For backwards compatibility the validation prints warnings only for non-conformant values. * @param info the instance info @@ -277,7 +278,7 @@ private void validateInstanceInfo(InstanceInfo info) { try { EurekaUtils.validateServiceId(serviceId); } catch (MetadataValidationException e) { - log.warn("Conformance criteria violation in serviceId or instanceId, instanceId expected format 'hostname:serviceid:port' but is '{}' in instanceInfo: {}. Cause: {}", + log.warn("Conformance criteria violation in serviceId or instanceId, instanceId expected format 'hostname:serviceid:port' or '[ipv6]:serviceid:port' but is '{}' in instanceInfo: {}. Cause: {}", info.getInstanceId(), info, e.getMessage()); } diff --git a/discovery-service/src/main/java/org/zowe/apiml/discovery/eureka/RefreshablePeerEurekaNodes.java b/discovery-service/src/main/java/org/zowe/apiml/discovery/eureka/RefreshablePeerEurekaNodes.java index f0a3576efb..91aa5e2e45 100644 --- a/discovery-service/src/main/java/org/zowe/apiml/discovery/eureka/RefreshablePeerEurekaNodes.java +++ b/discovery-service/src/main/java/org/zowe/apiml/discovery/eureka/RefreshablePeerEurekaNodes.java @@ -44,6 +44,7 @@ import org.springframework.cloud.context.environment.EnvironmentChangeEvent; import org.springframework.context.ApplicationListener; import org.zowe.apiml.product.eureka.client.ApimlPeerEurekaNode; +import org.zowe.apiml.util.UrlUtils; import javax.net.ssl.SSLContext; import java.net.InetAddress; @@ -98,8 +99,10 @@ private Jersey3ReplicationClient createReplicationClient(EurekaServerConfig conf EurekaJersey3Client jerseyClient; try { String hostname; + // Format the service URL to properly handle IPv6 addresses + String formattedServiceUrl = UrlUtils.formatUrlWithIPv6Support(serviceUrl); try { - hostname = new URL(serviceUrl).getHost(); + hostname = new URL(formattedServiceUrl).getHost(); } catch (MalformedURLException e) { hostname = serviceUrl; } diff --git a/discovery-service/src/main/java/org/zowe/apiml/discovery/staticdef/ServiceDefinitionProcessor.java b/discovery-service/src/main/java/org/zowe/apiml/discovery/staticdef/ServiceDefinitionProcessor.java index a6cf018b53..9a2894065c 100644 --- a/discovery-service/src/main/java/org/zowe/apiml/discovery/staticdef/ServiceDefinitionProcessor.java +++ b/discovery-service/src/main/java/org/zowe/apiml/discovery/staticdef/ServiceDefinitionProcessor.java @@ -242,7 +242,9 @@ private InstanceInfo buildInstanceInfo(StaticRegistrationResult context, String serviceId = service.getServiceId(); try { - URL url = new URL(instanceBaseUrl); + // Format URL to handle IPv6 addresses properly (add brackets if needed) + String formattedInstanceBaseUrl = UrlUtils.formatUrlWithIPv6Support(instanceBaseUrl); + URL url = new URL(formattedInstanceBaseUrl); if (url.getHost().isEmpty()) { throw new ServiceDefinitionException(String.format("The URL %s does not contain a hostname. The instance of %s will not be created", instanceBaseUrl, serviceId)); diff --git a/discovery-service/src/test/java/org/zowe/apiml/discovery/ApimlInstanceRegistryTest.java b/discovery-service/src/test/java/org/zowe/apiml/discovery/ApimlInstanceRegistryTest.java index 8da07bd686..5da473ee6d 100644 --- a/discovery-service/src/test/java/org/zowe/apiml/discovery/ApimlInstanceRegistryTest.java +++ b/discovery-service/src/test/java/org/zowe/apiml/discovery/ApimlInstanceRegistryTest.java @@ -97,7 +97,10 @@ private static Stream instanceIds() { Arguments.of( "hostname:-serviceclient:10010", "-serviceclient"), Arguments.of( "hostname:serviceclient-:10010", "serviceclient-"), Arguments.of( "hostname:invalidserviceidididididididididididdididididididididididdidididididididididid:10010", "invalidserviceidididididididididididdididididididididididdidididididididididid"), - Arguments.of( null, "service") + Arguments.of( null, "service"), + // IPv6 format test cases (with brackets) + Arguments.of( "[2620:117:10:4300::55:28]:service_client:12314", "service_client"), + Arguments.of( "[::1]:service-client-:10010", "service-client-") ); } diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/caching/CachingServiceClientRest.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/caching/CachingServiceClientRest.java index 34d425939a..d53b0158f5 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/caching/CachingServiceClientRest.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/caching/CachingServiceClientRest.java @@ -19,6 +19,7 @@ import org.springframework.util.MultiValueMap; import org.springframework.web.reactive.function.client.WebClient; import org.zowe.apiml.product.gateway.GatewayClient; +import org.zowe.apiml.util.UrlUtils; import reactor.core.publisher.Mono; import static reactor.core.publisher.Mono.empty; @@ -55,7 +56,10 @@ public CachingServiceClientRest( void updateUrl() { // Lazy initialization of GatewayClient's ServerAddress may bring invalid URL during initialization - this.cachingBalancerUrl = String.format("%s://%s/%s", gatewayClient.getGatewayConfigProperties().getScheme(), gatewayClient.getGatewayConfigProperties().getHostname(), CACHING_API_PATH); + this.cachingBalancerUrl = UrlUtils.getUrl( + gatewayClient.getGatewayConfigProperties().getScheme(), + gatewayClient.getGatewayConfigProperties().getHostname() + ) + "/" + CACHING_API_PATH; } public Mono create(ApiKeyValue keyValue) { diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/ConnectionsConfig.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/ConnectionsConfig.java index e9da616a8f..8e82aebcd3 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/ConnectionsConfig.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/ConnectionsConfig.java @@ -62,6 +62,7 @@ import org.zowe.apiml.security.HttpsConfigError; import org.zowe.apiml.security.common.util.ConnectionUtil; import org.zowe.apiml.util.CorsUtils; +import org.zowe.apiml.util.UrlUtils; import reactor.netty.http.client.HttpClient; import java.net.MalformedURLException; @@ -293,11 +294,16 @@ public InstanceInfo create(EurekaInstanceConfig config) { if (!namespace.endsWith(".")) { namespace = namespace + "."; } + + // This ensures IPv6 addresses like "https://2001:db8::1:8080" become "https://[2001:db8::1]:8080" + String formattedExternalUrl = UrlUtils.formatUrlWithIPv6Support(externalUrl); + URL url; try { - url = new URL(externalUrl); + url = new URL(formattedExternalUrl); } catch (MalformedURLException e) { - throw new RuntimeException(e); + log.error("Failed to parse external URL '{}' (formatted: '{}'): {}", externalUrl, formattedExternalUrl, e.getMessage()); + throw new RuntimeException("Invalid external URL configuration: " + externalUrl, e); } builder @@ -314,8 +320,8 @@ public InstanceInfo create(EurekaInstanceConfig config) { .enablePort(InstanceInfo.PortType.SECURE, config.getSecurePortEnabled()) .setVIPAddress(config.getVirtualHostName()) .setSecureVIPAddress(config.getSecureVirtualHostName()) - .setHomePageUrl(null, UriComponentsBuilder.fromUriString(externalUrl).path(config.getHomePageUrlPath()).toUriString()) - .setStatusPageUrl(null, UriComponentsBuilder.fromUriString(externalUrl).path(config.getStatusPageUrlPath()).toUriString()) + .setHomePageUrl(null, UriComponentsBuilder.fromUriString(formattedExternalUrl).path(config.getHomePageUrlPath()).toUriString()) + .setStatusPageUrl(null, UriComponentsBuilder.fromUriString(formattedExternalUrl).path(config.getStatusPageUrlPath()).toUriString()) .setHealthCheckUrls(config.getHealthCheckUrlPath(), null, null) .setASGName(config.getASGName()); @@ -336,7 +342,7 @@ public InstanceInfo create(EurekaInstanceConfig config) { // Add any user-specific metadata information var fromUrl = UriComponentsBuilder.fromUriString(config.getHomePageUrl()).path("/").toUriString(); - var toUrl = UriComponentsBuilder.fromUriString(externalUrl).path("/").toUriString(); + var toUrl = UriComponentsBuilder.fromUriString(formattedExternalUrl).path("/").toUriString(); for (Map.Entry mapEntry : config.getMetadataMap().entrySet()) { String key = mapEntry.getKey(); String value = mapEntry.getValue(); diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/RegistryConfig.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/RegistryConfig.java index 834b163994..e575156d37 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/RegistryConfig.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/RegistryConfig.java @@ -11,6 +11,7 @@ package org.zowe.apiml.gateway.config; import com.netflix.discovery.EurekaClient; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; @@ -21,7 +22,9 @@ import java.net.URI; import java.net.URISyntaxException; +import org.zowe.apiml.util.UrlUtils; +@Slf4j @Configuration public class RegistryConfig { @@ -41,16 +44,32 @@ ServiceAddress gatewayServiceAddress( @Value("${server.port}") int port ) throws URISyntaxException { if (externalUrl != null) { - URI uri = new URI(externalUrl); - return ServiceAddress.builder() - .scheme(clientAttlsEnabled ? "http" : uri.getScheme()) - .hostname(uri.getHost() + ":" + uri.getPort()) - .build(); + // Format the URL to handle IPv6 addresses properly (add brackets if needed) + String formattedExternalUrl = UrlUtils.formatUrlWithIPv6Support(externalUrl); + URI uri = new URI(formattedExternalUrl); + String host = uri.getHost(); + + // Validate that the external URL has a valid host component + if (host == null || host.trim().isEmpty()) { + log.warn("Invalid external URL '{}' has no valid host component. Falling back to default configuration (hostname:port).", externalUrl); + // Fall through to use the default hostname and port configuration below + } else { + // Handle IPv6 address format using UrlUtils + host = UrlUtils.formatHostnameForUrl(host); + + return ServiceAddress.builder() + .scheme(clientAttlsEnabled ? "http" : uri.getScheme()) + .hostname(host + ":" + uri.getPort()) + .build(); + } } + // Handle IPv6 address format using UrlUtils + String formattedHostname = UrlUtils.formatHostnameForUrl(hostname); + return ServiceAddress.builder() .scheme(determineScheme(serverAttlsEnabled, clientAttlsEnabled, sslEnabled)) - .hostname(hostname + ":" + port) + .hostname(formattedHostname + ":" + port) .build(); } diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/ZaasSchemeTransformRest.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/ZaasSchemeTransformRest.java index c75e4256d7..bd7e9910ff 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/ZaasSchemeTransformRest.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/ZaasSchemeTransformRest.java @@ -28,6 +28,7 @@ import org.zowe.apiml.security.common.error.ServiceNotAccessibleException; import org.zowe.apiml.ticket.TicketRequest; import org.zowe.apiml.ticket.TicketResponse; +import org.zowe.apiml.util.UrlUtils; import org.zowe.apiml.zaas.ZaasTokenResponse; import reactor.core.publisher.Mono; @@ -129,7 +130,8 @@ private WebClient.RequestHeadersSpec createRequest(RequestCredentials request } private String getUrl(String pattern, ServiceInstance instance) { - return String.format(pattern, instance.getScheme(), instance.getHost(), instance.getPort(), instance.getServiceId().toLowerCase()); + String host = UrlUtils.formatHostnameForUrl(instance.getHost()); + return String.format(pattern, instance.getScheme(), host, instance.getPort(), instance.getServiceId().toLowerCase()); } @Override diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/service/GatewayIndexService.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/service/GatewayIndexService.java index 79ff31fa5d..411ada0c31 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/service/GatewayIndexService.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/service/GatewayIndexService.java @@ -29,6 +29,7 @@ import org.zowe.apiml.message.log.ApimlLogger; import org.zowe.apiml.message.yaml.YamlMessageServiceInstance; import org.zowe.apiml.services.ServiceInfo; +import org.zowe.apiml.util.UrlUtils; import reactor.core.publisher.Mono; import java.util.*; @@ -67,7 +68,7 @@ public GatewayIndexService( } private WebClient buildWebClient(ServiceInstance registration) { - final String baseUrl = String.format("%s://%s:%d", registration.getScheme(), registration.getHost(), registration.getPort()); + final String baseUrl = UrlUtils.getUrl(registration.getScheme(), registration.getHost(), registration.getPort()); return webClient.mutate() .baseUrl(baseUrl) diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/service/routing/RouteDefinitionProducer.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/service/routing/RouteDefinitionProducer.java index 3e8e6992aa..20c63a3db2 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/service/routing/RouteDefinitionProducer.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/service/routing/RouteDefinitionProducer.java @@ -12,16 +12,18 @@ import lombok.RequiredArgsConstructor; import lombok.experimental.Delegate; +import lombok.extern.slf4j.Slf4j; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.gateway.discovery.DiscoveryLocatorProperties; import org.springframework.cloud.gateway.route.RouteDefinition; import org.springframework.expression.Expression; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.SimpleEvaluationContext; +import java.net.URI; +import java.net.URISyntaxException; +import org.zowe.apiml.util.UrlUtils; import org.springframework.util.StringUtils; import org.zowe.apiml.product.routing.RoutedService; - -import java.net.URI; import java.util.LinkedHashMap; import java.util.Map; @@ -33,6 +35,7 @@ * * The producers define an order ({@link #getOrder()}). It allows to create multiple rules with a prioritization. */ +@Slf4j public abstract class RouteDefinitionProducer { protected final SimpleEvaluationContext evalCtxt = SimpleEvaluationContext.forReadOnlyDataBinding().withInstanceMethods().build(); @@ -58,9 +61,52 @@ protected String getHostname(ServiceInstance serviceInstance) { Map metadata = serviceInstance.getMetadata(); if (metadata != null) { output = metadata.get(SERVICE_EXTERNAL_URL); + + // If we have an external URL and it's not a load balancer URL, format it + if (output != null && !output.startsWith("lb://")) { + try { + URI uri = new URI(output); + String formattedHost = UrlUtils.formatHostnameForUrl(uri.getHost()); + if (formattedHost != null) { + URI newUri = new URI( + uri.getScheme(), + uri.getUserInfo(), + formattedHost, + uri.getPort(), + uri.getPath(), + uri.getQuery(), + uri.getFragment() + ); + output = newUri.toString(); + } + } catch (URISyntaxException e) { + log.error("Error while formatting URI: {}", output, e); + } + } } if (output == null) { - output = evalHostname(serviceInstance); + String evalHost = evalHostname(serviceInstance); + // Return load balancer URL as is, format others + if (!evalHost.startsWith("lb://")) { + try { + URI uri = new URI(evalHost); + String formattedHost = UrlUtils.formatHostnameForUrl(uri.getHost()); + if (formattedHost != null) { + evalHost = new URI( + uri.getScheme(), + uri.getUserInfo(), + formattedHost, + uri.getPort(), + uri.getPath(), + uri.getQuery(), + uri.getFragment() + ).toString(); + } + } catch (URISyntaxException e) { + log.error("Error while formatting URI: {}", evalHost, e); + } + } + output = evalHost; } return output; } @@ -83,7 +129,9 @@ protected RouteDefinition buildRouteDefinition(ServiceInstance serviceInstance, RouteDefinition routeDefinition = new RouteDefinition(); routeDefinition.setId(serviceInstance.getInstanceId() + ":" + routeId); routeDefinition.setOrder(getOrder()); - routeDefinition.setUri(URI.create(getHostname(serviceInstance))); + String hostname = getHostname(serviceInstance); + + routeDefinition.setUri(URI.create(hostname)); // add instance metadata routeDefinition.setMetadata(new LinkedHashMap<>(serviceInstance.getMetadata())); diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/services/ServicesInfoService.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/services/ServicesInfoService.java index 85c9adfc6a..41be4d9257 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/services/ServicesInfoService.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/services/ServicesInfoService.java @@ -28,6 +28,7 @@ import org.zowe.apiml.product.routing.ServiceType; import org.zowe.apiml.product.routing.transform.TransformService; import org.zowe.apiml.product.routing.transform.URLTransformationException; +import org.zowe.apiml.util.UrlUtils; import org.zowe.apiml.services.ServiceInfo; import org.zowe.apiml.services.ServiceInfoUtils; @@ -92,8 +93,8 @@ public ServiceInfo getServiceInfo(String serviceId) { private String getBaseUrl(ApiInfo apiInfo, InstanceInfo instanceInfo) { ServiceAddress gatewayAddress = gatewayClient.getGatewayConfigProperties(); - return String.format("%s://%s%s", - gatewayAddress.getScheme(), gatewayAddress.getHostname(), getBasePath(apiInfo, instanceInfo)); + return UrlUtils.getUrl(gatewayAddress.getScheme(), gatewayAddress.getHostname()) + + getBasePath(apiInfo, instanceInfo); } static List getPrimaryInstances(Application application) { diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/config/DiscoveryClientTestConfig.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/config/DiscoveryClientTestConfig.java index 1228c536db..667cb0bc61 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/config/DiscoveryClientTestConfig.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/config/DiscoveryClientTestConfig.java @@ -21,8 +21,6 @@ import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient; import org.springframework.cloud.context.config.annotation.RefreshScope; -import org.springframework.cloud.netflix.eureka.RestClientTimeoutProperties; -import org.springframework.cloud.netflix.eureka.http.DefaultEurekaClientHttpRequestFactorySupplier; import org.springframework.cloud.netflix.eureka.http.RestClientDiscoveryClientOptionalArgs; import org.springframework.cloud.netflix.eureka.http.RestClientTransportClientFactories; import org.springframework.cloud.util.ProxyUtils; @@ -94,9 +92,10 @@ ApimlDiscoveryClientStub eurekaClient(ApplicationInfoManager manager, appManager = manager; } - - var factorySupplier = new DefaultEurekaClientHttpRequestFactorySupplier(new RestClientTimeoutProperties()); - var args1 = new RestClientDiscoveryClientOptionalArgs(factorySupplier, RestClient::builder); + // Use RestClientDiscoveryClientOptionalArgs with default RestClient builder + // The DefaultEurekaClientHttpRequestFactorySupplier constructors are deprecated, + // so we pass null for the supplier and let Spring Cloud use its defaults + var args1 = new RestClientDiscoveryClientOptionalArgs(null, RestClient::builder); var factories = new RestClientTransportClientFactories(args1); final var discoveryClient = new ApimlDiscoveryClientStub(appManager, config, this.context, applicationRegistry, factories, args1); discoveryClient.registerHealthCheck(healthCheckHandler); diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/caching/CachingServiceClientRestTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/caching/CachingServiceClientRestTest.java index 376dfe9286..ec31739c19 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/caching/CachingServiceClientRestTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/caching/CachingServiceClientRestTest.java @@ -54,7 +54,10 @@ class CachingServiceClientRestTest { @BeforeEach void setUp() { webClient = spy(WebClient.builder().exchangeFunction(exchangeFunction).build()); - client = new CachingServiceClientRest(webClient, new GatewayClient(ServiceAddress.builder().build())); + client = new CachingServiceClientRest(webClient, new GatewayClient(ServiceAddress.builder() + .scheme("https") + .hostname("localhost:10011") + .build())); lenient().when(clientResponse.releaseBody()).thenReturn(empty()); } diff --git a/onboarding-enabler-java/src/main/java/org/zowe/apiml/eurekaservice/client/util/EurekaInstanceConfigCreator.java b/onboarding-enabler-java/src/main/java/org/zowe/apiml/eurekaservice/client/util/EurekaInstanceConfigCreator.java index b4d76cc5fd..b486cacbce 100644 --- a/onboarding-enabler-java/src/main/java/org/zowe/apiml/eurekaservice/client/util/EurekaInstanceConfigCreator.java +++ b/onboarding-enabler-java/src/main/java/org/zowe/apiml/eurekaservice/client/util/EurekaInstanceConfigCreator.java @@ -37,7 +37,9 @@ public EurekaInstanceConfig createEurekaInstanceConfig(ApiMediationServiceConfig URL baseUrl; try { - baseUrl = new URL(config.getBaseUrl()); + // Format URL to handle IPv6 addresses properly (add brackets if needed) + String formattedBaseUrl = UrlUtils.formatUrlWithIPv6Support(config.getBaseUrl()); + baseUrl = new URL(formattedBaseUrl); hostname = baseUrl.getHost(); port = baseUrl.getPort(); } catch (MalformedURLException e) { @@ -46,10 +48,12 @@ public EurekaInstanceConfig createEurekaInstanceConfig(ApiMediationServiceConfig } if (config.isPreferIpAddress()) { hostname = config.getServiceIpAddress(); - config.setBaseUrl(baseUrl.getProtocol() + "://" + hostname + ":" + port); + config.setBaseUrl(baseUrl.getProtocol() + "://" + UrlUtils.formatHostnameForUrl(hostname) + ":" + port); } - result.setInstanceId(String.format("%s:%s:%s", hostname, config.getServiceId(), port)); + // Format hostname for IPv6 when constructing instanceId (use brackets for IPv6 addresses) + String formattedHostnameForInstanceId = UrlUtils.formatHostnameForUrl(hostname); + result.setInstanceId(String.format("%s:%s:%s", formattedHostnameForInstanceId, config.getServiceId(), port)); result.setAppname(config.getServiceId()); result.setAppGroupName(config.getServiceId()); result.setHostName(hostname); diff --git a/security-service-client-spring/src/main/java/org/zowe/apiml/security/client/service/GatewaySecurityService.java b/security-service-client-spring/src/main/java/org/zowe/apiml/security/client/service/GatewaySecurityService.java index 9709a06cd7..24d8766d2d 100644 --- a/security-service-client-spring/src/main/java/org/zowe/apiml/security/client/service/GatewaySecurityService.java +++ b/security-service-client-spring/src/main/java/org/zowe/apiml/security/client/service/GatewaySecurityService.java @@ -29,6 +29,7 @@ import org.zowe.apiml.product.gateway.GatewayClient; import org.zowe.apiml.product.instance.ServiceAddress; import org.zowe.apiml.security.client.handler.RestResponseHandler; +import org.zowe.apiml.util.UrlUtils; import org.zowe.apiml.security.common.config.AuthConfigurationProperties; import org.zowe.apiml.security.common.error.ErrorType; import org.zowe.apiml.security.common.login.LoginRequest; @@ -58,8 +59,8 @@ public class GatewaySecurityService implements GatewaySecurity { @Override public Optional login(String username, char[] password, char[] newPassword) { ServiceAddress gatewayConfigProperties = gatewayClient.getGatewayConfigProperties(); - String uri = String.format("%s://%s%s", gatewayConfigProperties.getScheme(), - gatewayConfigProperties.getHostname(), authConfigurationProperties.getGatewayLoginEndpoint()); + String uri = UrlUtils.getUrl(gatewayConfigProperties.getScheme(), gatewayConfigProperties.getHostname()) + + authConfigurationProperties.getGatewayLoginEndpoint(); LoginRequest loginRequest = new LoginRequest(username, password); if (!ArrayUtils.isEmpty(newPassword)) { @@ -95,8 +96,8 @@ public Optional login(String username, char[] password, char[] newPasswo @Override public QueryResponse query(String token) { ServiceAddress gatewayConfigProperties = gatewayClient.getGatewayConfigProperties(); - String uri = String.format("%s://%s%s", gatewayConfigProperties.getScheme(), - gatewayConfigProperties.getHostname(), authConfigurationProperties.getGatewayQueryEndpoint()); + String uri = UrlUtils.getUrl(gatewayConfigProperties.getScheme(), gatewayConfigProperties.getHostname()) + + authConfigurationProperties.getGatewayQueryEndpoint(); String cookie = String.format("%s=%s", authConfigurationProperties.getCookieProperties().getCookieName(), token); try { @@ -126,8 +127,8 @@ public QueryResponse query(String token) { @Override public QueryResponse verifyOidc(String token) { ServiceAddress gatewayConfigProperties = gatewayClient.getGatewayConfigProperties(); - String uri = String.format("%s://%s%s", gatewayConfigProperties.getScheme(), - gatewayConfigProperties.getHostname(), authConfigurationProperties.getGatewayOidcValidateEndpoint()); + String uri = UrlUtils.getUrl(gatewayConfigProperties.getScheme(), gatewayConfigProperties.getHostname()) + + authConfigurationProperties.getGatewayOidcValidateEndpoint(); try { HttpPost post = new HttpPost(uri); diff --git a/zaas-service/src/main/java/org/zowe/apiml/zaas/cache/CachingServiceClient.java b/zaas-service/src/main/java/org/zowe/apiml/zaas/cache/CachingServiceClient.java index 91339e0ff6..779664d55f 100644 --- a/zaas-service/src/main/java/org/zowe/apiml/zaas/cache/CachingServiceClient.java +++ b/zaas-service/src/main/java/org/zowe/apiml/zaas/cache/CachingServiceClient.java @@ -25,6 +25,7 @@ import org.springframework.web.client.RestTemplate; import org.zowe.apiml.product.gateway.GatewayClient; import org.zowe.apiml.product.instance.ServiceAddress; +import org.zowe.apiml.util.UrlUtils; import java.util.Map; @@ -67,7 +68,7 @@ private String getGatewayAddress() { if (gatewayAddress.getScheme() == null || gatewayAddress.getHostname() == null) { throw new IllegalStateException("zaasProtocolHostPort has to have value in format ://: and not be null"); } - return String.format("%s://%s", gatewayAddress.getScheme(), gatewayAddress.getHostname()); + return UrlUtils.getUrl(gatewayAddress.getScheme(), gatewayAddress.getHostname()); } /**