Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
c702446
IPv6 host support
hrishikesh-nalawade Oct 29, 2025
623960f
Merge branch 'v3.x.x' into hrishikesh-nalawade/GH4318/IPv6-host-forma…
hrishikesh-nalawade Oct 30, 2025
99e1065
IPv6 host support enhancements
hrishikesh-nalawade Nov 5, 2025
93d577c
Merge remote-tracking branch 'origin/hrishikesh-nalawade/GH4318/IPv6-…
hrishikesh-nalawade Nov 5, 2025
0ca2efa
corrections
hrishikesh-nalawade Nov 5, 2025
b1307ad
Merge branch 'v3.x.x' into hrishikesh-nalawade/GH4318/IPv6-host-forma…
hrishikesh-nalawade Nov 5, 2025
170d4e5
test modifications
hrishikesh-nalawade Nov 7, 2025
a473483
Merge branch 'v3.x.x' into hrishikesh-nalawade/GH4318/IPv6-host-forma…
hrishikesh-nalawade Nov 7, 2025
cb213d6
test modifications
hrishikesh-nalawade Nov 7, 2025
a3279b3
Merge remote-tracking branch 'origin/hrishikesh-nalawade/GH4318/IPv6-…
hrishikesh-nalawade Nov 7, 2025
f739842
temporarily removing test
hrishikesh-nalawade Nov 7, 2025
2907323
temporarily removing test
hrishikesh-nalawade Nov 7, 2025
7ffa42f
reverting test changes
hrishikesh-nalawade Nov 7, 2025
c01baff
Merge branch 'v3.x.x' into hrishikesh-nalawade/GH4318/IPv6-host-forma…
hrishikesh-nalawade Nov 10, 2025
a0dc139
code changes
hrishikesh-nalawade Nov 17, 2025
2a5ef3b
added debug logs
hrishikesh-nalawade Nov 17, 2025
f4ad343
Merge branch 'v3.x.x' into hrishikesh-nalawade/GH4318/IPv6-host-forma…
hrishikesh-nalawade Nov 18, 2025
83d1253
Merge branch 'v3.x.x' into hrishikesh-nalawade/GH4318/IPv6-host-forma…
hrishikesh-nalawade Nov 20, 2025
a378cb8
Merge branch 'v3.x.x' into hrishikesh-nalawade/GH4318/IPv6-host-forma…
hrishikesh-nalawade Nov 21, 2025
52580af
Test Changes
hrishikesh-nalawade Nov 27, 2025
c253933
Merge branch 'v3.x.x' into hrishikesh-nalawade/GH4318/IPv6-host-forma…
hrishikesh-nalawade Nov 27, 2025
fe7c7b9
Removing deprecated code from test
hrishikesh-nalawade Nov 27, 2025
f480f23
Merge remote-tracking branch 'origin/hrishikesh-nalawade/GH4318/IPv6-…
hrishikesh-nalawade Nov 27, 2025
a00a7df
test fix
hrishikesh-nalawade Nov 27, 2025
98d6b2c
Removing strict validation which is causing starUpCheck failure
hrishikesh-nalawade Nov 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.*;
Expand Down Expand Up @@ -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;
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getHostname() returns host+port, which contains ":" even if there is no IPv6 address. This will probably create an incorrect URL

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've considered the possibility that getHostname() might be returning the host along with the port, and I've added handling for that here. But, I suspect the issue might be related to the formatHostnameForUrl() method. I'll take a closer look to better understand what's going wrong.

}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<OpenAPI> openApiHolder = new AtomicReference<>();
Expand All @@ -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))
Expand Down
124 changes: 124 additions & 0 deletions common-service-core/src/main/java/org/zowe/apiml/util/UrlUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,128 @@ 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;
}

// Check for hostname:port format by looking at the last colon
// We check this BEFORE checking if the entire string is IPv6 because:
// 1. "2001:db8::1:8080" could be ambiguous - is 8080 part of IPv6 or a port?
// 2. If the last segment after colon is a valid port number, we should treat it as such
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;
}
// If the full string is NOT IPv6, return as-is (hostname:port or IPv4:port)
if (!isIPv6Address(hostname)) {
return hostname;
}
// Edge case: If full hostname IS IPv6 but possibleHost is not,
// fall through to check if it's a plain IPv6 address without port
}
}

// No valid port found, check if it's a plain IPv6 address
if (isIPv6Address(hostname)) {
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it possible that there will be scheme if the parameter is called hostWithPort?

Copy link
Member Author

@hrishikesh-nalawade hrishikesh-nalawade Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have added this just to safeguard us if any caller passes value which may contain scheme, also as it's a single regex check hence, computationally cheap for us and makes sure correct host/host+port is sent.

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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<Void> create(ApiKeyValue keyValue) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -21,7 +22,9 @@

import java.net.URI;
import java.net.URISyntaxException;
import org.zowe.apiml.util.UrlUtils;

@Slf4j
@Configuration
public class RegistryConfig {

Expand All @@ -42,15 +45,29 @@ ServiceAddress gatewayServiceAddress(
) throws URISyntaxException {
if (externalUrl != null) {
URI uri = new URI(externalUrl);
return ServiceAddress.builder()
.scheme(clientAttlsEnabled ? "http" : uri.getScheme())
.hostname(uri.getHost() + ":" + uri.getPort())
.build();
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();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.*;
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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();
Expand All @@ -53,14 +56,57 @@
* @param serviceInstance instance of service to process
* @return URL for loadbalancer (without path)
*/
protected String getHostname(ServiceInstance serviceInstance) {

Check failure on line 59 in gateway-service/src/main/java/org/zowe/apiml/gateway/service/routing/RouteDefinitionProducer.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 19 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=zowe_api-layer&issues=AZrGcyLAEWXyDk6PYoXa&open=AZrGcyLAEWXyDk6PYoXa&pullRequest=4367
String output = null;
Map<String, String> 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;
}
Expand All @@ -83,7 +129,9 @@
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()));
Expand Down
Loading
Loading