Skip to content
Open
Show file tree
Hide file tree
Changes from 13 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
120 changes: 120 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,124 @@ 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
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 port, check if the host part is IPv6
if (isIPv6Address(possibleHost)) {
return "[" + possibleHost + "]:" + possiblePort;
}
// If the full string is NOT IPv6, return as-is
if (!isIPv6Address(hostname)) {
return hostname; // Regular hostname:port or IPv4:port
}
// Otherwise, fall through to check if full hostname is IPv6
}
}

// No port number, check if it's a plain IPv6 address
if (isIPv6Address(hostname)) {
return "[" + hostname + "]";
}

Comment on lines 167 to 196
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// Check for hostname:port format
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 port, check if the host part is IPv6
if (isIPv6Address(possibleHost)) {
return "[" + possibleHost + "]:" + possiblePort;
}
// If the full string is NOT IPv6, return as-is
if (!isIPv6Address(hostname)) {
return hostname; // Regular hostname:port or IPv4:port
}
// Otherwise, fall through to check if full hostname is IPv6
}
}
// No port number, check if it's a plain IPv6 address
if (isIPv6Address(hostname)) {
return "[" + hostname + "]";
}
// SECURITY: Check if the entire string is a valid IPv6 address first
// This prevents incorrectly splitting IPv6 addresses that might have
// numeric suffixes that look like ports
if (isIPv6Address(hostname)) {
return "[" + hostname + "]";
}
// Check for hostname:port format
// Only split if we're sure it's not an IPv6 address
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)) {
// SECURITY: Verify the host part is NOT an IPv6 address
// If it is, we need to bracket just the IPv6 part, not the entire string
if (isIPv6Address(possibleHost)) {
// This is an unbracketed IPv6 address with port - format correctly
return "[" + possibleHost + "]:" + possiblePort;
}
// If the host part is not IPv6, it's safe to treat as hostname:port
return hostname; // Regular hostname:port or IPv4:port
}
}

It is better to check for IPv6 address before looking for port number

Copy link
Member Author

Choose a reason for hiding this comment

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

Hello @achmelo ,
The problem with checking the entire string as IPv6 before is that, in case where port is present with host (example: - 2001:db8::1:8080), The InetAddress verifies this as a IPv6 address and will return true. Hence resulting into incorrect formatting of complete string [2001:db8::1:8080].
Though your concern "This prevents incorrectly splitting IPv6 addresses that might have numeric suffixes that look like ports" is very much valid and I have handled these scenarios where I have made sure that IPv6 string is returned properly.

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("^\\w+://", "");
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
String cleanHostWithPort = hostWithPort.replaceFirst("^\\w+://", "");
String cleanHostWithPort = hostWithPort.replaceFirst("^[a-zA-Z][a-zA-Z0-9+.-]*://", "");

is probably more accurate according to the RFC 3986

Copy link
Member Author

Choose a reason for hiding this comment

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

Thank You, I have applied your suggestion.


// 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 @@ -21,6 +21,7 @@

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

@Configuration
public class RegistryConfig {
Expand All @@ -42,15 +43,23 @@ ServiceAddress gatewayServiceAddress(
) throws URISyntaxException {
if (externalUrl != null) {
URI uri = new URI(externalUrl);
String host = uri.getHost();
// Handle IPv6 address format using UrlUtils
if (host != null) {
host = UrlUtils.formatHostnameForUrl(host);
}
Copy link
Member

Choose a reason for hiding this comment

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

if host == null, is it ok to continue? it will probably throw some error in the builder

Copy link
Member Author

Choose a reason for hiding this comment

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

Thank you for pointing that out, I have corrected the above

return ServiceAddress.builder()
.scheme(clientAttlsEnabled ? "http" : uri.getScheme())
.hostname(uri.getHost() + ":" + uri.getPort())
.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 @@ -18,10 +18,11 @@
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 Down Expand Up @@ -58,9 +59,52 @@ protected String getHostname(ServiceInstance serviceInstance) {
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) {
// If there's an error parsing the URI, keeping the original URL
Copy link
Member

Choose a reason for hiding this comment

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

some debug log would be good here

Copy link
Member Author

Choose a reason for hiding this comment

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

added, Thank You

}
}
}
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) {
// Keep original if URI parsing fails
Copy link
Member

Choose a reason for hiding this comment

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

debug log would be good

Copy link
Member Author

Choose a reason for hiding this comment

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

added, Thank You

}
}
output = evalHost;
}
return output;
}
Expand All @@ -83,7 +127,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()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<InstanceInfo> getPrimaryInstances(Application application) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -58,8 +59,8 @@ public class GatewaySecurityService implements GatewaySecurity {
@Override
public Optional<String> 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)) {
Expand Down Expand Up @@ -95,8 +96,8 @@ public Optional<String> 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 {
Expand Down Expand Up @@ -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);
Expand Down
Loading
Loading