diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/internal/GrpcUtils.java b/spring-grpc-core/src/main/java/org/springframework/grpc/internal/GrpcUtils.java
index 9f5b164b..e15176d3 100644
--- a/spring-grpc-core/src/main/java/org/springframework/grpc/internal/GrpcUtils.java
+++ b/spring-grpc-core/src/main/java/org/springframework/grpc/internal/GrpcUtils.java
@@ -16,6 +16,12 @@
package org.springframework.grpc.internal;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.util.Objects;
+
+import org.springframework.util.StringUtils;
+
/**
* Provides convenience methods for various gRPC functions.
*
@@ -36,9 +42,21 @@ private GrpcUtils() {
*/
public static int getPort(String address) {
String value = address;
- if (value.contains(":")) {
+ long numberOfColons = countColons(address);
+ if (numberOfColons == 1) {
value = value.substring(value.lastIndexOf(":") + 1);
}
+ else if (numberOfColons > 1) {
+ if (address.startsWith("[")) {
+ var index = address.lastIndexOf("]:");
+ if (index >= 0) {
+ value = address.substring(index + 2);
+ }
+ else {
+ return DEFAULT_PORT;
+ }
+ }
+ }
if (value.contains("/")) {
value = value.substring(0, value.indexOf("/"));
}
@@ -51,4 +69,68 @@ public static int getPort(String address) {
return DEFAULT_PORT;
}
+ public static long countColons(String address) {
+ return address.chars().filter(ch -> ch == ':').count();
+ }
+
+ /**
+ * Gets the hostname from a given address.
+ * @param address a hostname/IPv4/IPv6/empty/* optionally with a port specification
+ * @return the hostname or an empty string
+ * @see https://en.wikipedia.org/wiki/IPv6#Address_representation
+ */
+ public static String getHostName(String address) {
+ String trimmedAddress = address.trim();
+ long numberOfColons = countColons(trimmedAddress);
+
+ if (numberOfColons == 0) {
+ return trimmedAddress;
+ }
+
+ if (numberOfColons == 1) { // An IPv6 address mush have at least 2 colons, so is
+ // {IPv4 or hostname}:{port}
+ return trimmedAddress.split(":")[0].trim();
+ }
+
+ if (numberOfColons > 8 || numberOfColons == 8 && !trimmedAddress.startsWith("[")) {
+ // On an IPv6 address a maximum of 7 colons are allowed + 1 for the port
+ // IPv6 addresses with port should have the format [{address}]:port
+ throw new IllegalArgumentException("Cannot parse address: " + trimmedAddress);
+ }
+
+ if (trimmedAddress.startsWith("[")) {
+ var index = trimmedAddress.lastIndexOf("]");
+ if (index < 0) {
+ throw new IllegalArgumentException("Cannot parse address: " + trimmedAddress);
+ }
+ return trimmedAddress.substring(1, index);
+ }
+
+ return trimmedAddress; // IPv6 Address with no port specified
+ }
+
+ /**
+ * Gets a SocketAddress for the given address.
+ *
+ * If the address part is empty, * or :: a SocketAddress with wildcard address will be
+ * returned. If the port part is empty or missing, a SocketAddress for the gRPC
+ * default port (9090) will be returned.
+ * @param address a hostname/IPv4/IPv6/empty/* optionally with a port specification
+ * @return a SocketAddress representation for the given address
+ */
+ public static SocketAddress getSocketAddress(String address) {
+ if (address.startsWith("unix:")) {
+ throw new UnsupportedOperationException("Unix socket addresses not supported");
+ }
+
+ var host = getHostName(address);
+ if (StringUtils.hasText(host) && !Objects.equals(host, "*") && !Objects.equals(host, "::")) {
+ return new InetSocketAddress(host, getPort(address));
+ }
+ else {
+ return new InetSocketAddress(getPort(address));
+ }
+ }
+
}
diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/server/NettyGrpcServerFactory.java b/spring-grpc-core/src/main/java/org/springframework/grpc/server/NettyGrpcServerFactory.java
index 7562f60e..c68be7dd 100644
--- a/spring-grpc-core/src/main/java/org/springframework/grpc/server/NettyGrpcServerFactory.java
+++ b/spring-grpc-core/src/main/java/org/springframework/grpc/server/NettyGrpcServerFactory.java
@@ -23,6 +23,8 @@
import org.jspecify.annotations.Nullable;
+import org.springframework.grpc.internal.GrpcUtils;
+
import io.grpc.TlsServerCredentials.ClientAuth;
import io.grpc.netty.NettyServerBuilder;
import io.netty.channel.MultiThreadIoEventLoopGroup;
@@ -56,7 +58,7 @@ protected NettyServerBuilder newServerBuilder() {
.bossEventLoopGroup(new MultiThreadIoEventLoopGroup(1, EpollIoHandler.newFactory()))
.workerEventLoopGroup(new MultiThreadIoEventLoopGroup(EpollIoHandler.newFactory()));
}
- return super.newServerBuilder();
+ return NettyServerBuilder.forAddress(GrpcUtils.getSocketAddress(address()), credentials());
}
}
diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/server/ShadedNettyGrpcServerFactory.java b/spring-grpc-core/src/main/java/org/springframework/grpc/server/ShadedNettyGrpcServerFactory.java
index 5a493470..98de3aae 100644
--- a/spring-grpc-core/src/main/java/org/springframework/grpc/server/ShadedNettyGrpcServerFactory.java
+++ b/spring-grpc-core/src/main/java/org/springframework/grpc/server/ShadedNettyGrpcServerFactory.java
@@ -23,6 +23,8 @@
import org.jspecify.annotations.Nullable;
+import org.springframework.grpc.internal.GrpcUtils;
+
import io.grpc.TlsServerCredentials.ClientAuth;
import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder;
import io.grpc.netty.shaded.io.netty.channel.epoll.EpollEventLoopGroup;
@@ -55,7 +57,7 @@ protected NettyServerBuilder newServerBuilder() {
.bossEventLoopGroup(new EpollEventLoopGroup(1))
.workerEventLoopGroup(new EpollEventLoopGroup());
}
- return super.newServerBuilder();
+ return NettyServerBuilder.forAddress(GrpcUtils.getSocketAddress(address()), credentials());
}
}
diff --git a/spring-grpc-core/src/test/java/org/springframework/grpc/internal/GrpcUtilsTests.java b/spring-grpc-core/src/test/java/org/springframework/grpc/internal/GrpcUtilsTests.java
index 43889224..a9e78d23 100644
--- a/spring-grpc-core/src/test/java/org/springframework/grpc/internal/GrpcUtilsTests.java
+++ b/spring-grpc-core/src/test/java/org/springframework/grpc/internal/GrpcUtilsTests.java
@@ -17,8 +17,15 @@
package org.springframework.grpc.internal;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.util.List;
+
+import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestFactory;
class GrpcUtilsTests {
@@ -56,4 +63,48 @@ void testGetInvalidAddress() {
assertThat(GrpcUtils.getPort(address)).isEqualTo(9090); // -1?
}
+ @TestFactory
+ List ipAddress() {
+ return List.of(testIpAddress(":9999", new InetSocketAddress(9999)),
+ testIpAddress("localhost:9999", new InetSocketAddress("localhost", 9999)),
+ testIpAddress("localhost", new InetSocketAddress("localhost", 9090)),
+ testIpAddress("127.0.0.1", new InetSocketAddress("127.0.0.1", 9090)),
+ testIpAddress("127.0.0.1:8888", new InetSocketAddress("127.0.0.1", 8888)),
+ testIpAddress("*", new InetSocketAddress(9090)), testIpAddress("*:8888", new InetSocketAddress(8888)),
+ testIpAddress("", new InetSocketAddress(9090)),
+ // IPv6 cases. See
+ // https://en.wikipedia.org/wiki/IPv6#Address_representation
+ testIpAddress("[::]:8888", new InetSocketAddress(8888)),
+ testIpAddress("::", new InetSocketAddress(9090)), testIpAddress("[::]", new InetSocketAddress(9090)),
+ testIpAddress("::1", new InetSocketAddress("::1", 9090)),
+ testIpAddress("[::1]", new InetSocketAddress("::1", 9090)),
+ testIpAddress("[::1]:9999", new InetSocketAddress("::1", 9999)),
+ testIpAddress("2001:db8::ff00:42:8329", new InetSocketAddress("2001:db8::ff00:42:8329", 9090)),
+ testIpAddress("[2001:db8::ff00:42:8329]", new InetSocketAddress("2001:db8::ff00:42:8329", 9090)),
+ testIpAddress("[2001:db8::ff00:42:8329]:9999", new InetSocketAddress("2001:db8::ff00:42:8329", 9999)),
+ testIpAddress("::ffff:192.0.2.128", new InetSocketAddress("::ffff:192.0.2.128", 9090)),
+ testIpAddress("[::ffff:192.0.2.128]", new InetSocketAddress("::ffff:192.0.2.128", 9090)),
+ testIpAddress("[::ffff:192.0.2.128]:9999", new InetSocketAddress("::ffff:192.0.2.128", 9999)));
+ }
+
+ private DynamicTest testIpAddress(String address, SocketAddress expected) {
+ return DynamicTest.dynamicTest("Socket address: " + address, () -> {
+ assertThat(GrpcUtils.getSocketAddress(address)).isEqualTo(expected);
+ });
+ }
+
+ @TestFactory
+ List unsupportedAddress() {
+ return List.of(testThrows("unix:dummy", UnsupportedOperationException.class),
+ testThrows("0:1:2:3:4:5:6:7:8:9", IllegalArgumentException.class),
+ testThrows("[0:1:2:3:4:5:6:7:8:9]", IllegalArgumentException.class),
+ testThrows("[0:1:2:3:4:5:6:7:8]:9", IllegalArgumentException.class),
+ testThrows("[0:1:2:3:4:5:6:7]:8:9", IllegalArgumentException.class));
+ }
+
+ private DynamicTest testThrows(String address, Class extends Exception> expectedException) {
+ return DynamicTest.dynamicTest("Socket address: " + address, () -> assertThatExceptionOfType(expectedException)
+ .isThrownBy(() -> GrpcUtils.getSocketAddress(address)));
+ }
+
}
diff --git a/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerProperties.java b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerProperties.java
index a6a752f6..6bbfeef1 100644
--- a/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerProperties.java
+++ b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerProperties.java
@@ -99,7 +99,15 @@ public void setAddress(@Nullable String address) {
* @return the address to bind to
*/
public String determineAddress() {
- return (this.address != null) ? this.address : this.host + ":" + this.port;
+ return (this.address != null) ? this.address : getAddressFromHostAndPort();
+ }
+
+ private String getAddressFromHostAndPort() {
+ return isIpV6() ? "[%s]:%d".formatted(this.host, this.port) : this.host + ":" + this.port;
+ }
+
+ private boolean isIpV6() {
+ return GrpcUtils.countColons(this.host) >= 2;
}
public String getHost() {
diff --git a/spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerPropertiesTests.java b/spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerPropertiesTests.java
index 1676ec71..7c32fc01 100644
--- a/spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerPropertiesTests.java
+++ b/spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerPropertiesTests.java
@@ -24,6 +24,8 @@
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.context.properties.source.MapConfigurationPropertySource;
@@ -187,6 +189,28 @@ void addressTakesPrecedenceOverHostAndPort() {
assertThat(properties.getAddress()).isEqualTo("my-server-ip:3130");
}
+ @ParameterizedTest
+ @ValueSource(strings = { "dummy.springframework.org", "127.0.0.1", "0.0.0.0", "192.168.1.2" })
+ void hostnameOrIpv4HostAndPort(String hostName) {
+ Map map = new HashMap<>();
+ map.put("spring.grpc.server.host", hostName);
+ map.put("spring.grpc.server.port", "1234");
+ GrpcServerProperties properties = bindProperties(map);
+ assertThat(properties.getAddress()).isNullOrEmpty();
+ assertThat(properties.determineAddress()).isEqualTo(hostName + ":1234");
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = { "::", "1:2:3:4:5:6:7:8", "::1" })
+ void ipv6HostAndPort(String ipv6Address) {
+ Map map = new HashMap<>();
+ map.put("spring.grpc.server.host", ipv6Address);
+ map.put("spring.grpc.server.port", "1234");
+ GrpcServerProperties properties = bindProperties(map);
+ assertThat(properties.getAddress()).isNullOrEmpty();
+ assertThat(properties.determineAddress()).isEqualTo("[" + ipv6Address + "]:1234");
+ }
+
}
}