Skip to content

Commit ce3696f

Browse files
author
Ivan Lorca
committed
Bind to configured IP address on netty servers
Replace call to super on the `NettyGrpcServer` and `ShaddedNettyGrpcServer` `newServerBuilder()` method, with a call to the `NettyServerBuilder.forAddress` that allows defining the IP and port to be bound to using a `SocketAddress`. The glue logic for getting the `SocketAddress` from the configured address string is in the `DefaultGrpcServerFactory` as is common to both inheritor classes. Signed-off-by: Ivan Lorca <ivanlorcaarago@gmail.com> [resolves #319]
1 parent dc23afd commit ce3696f

File tree

6 files changed

+173
-4
lines changed

6 files changed

+173
-4
lines changed

spring-grpc-core/src/main/java/org/springframework/grpc/internal/GrpcUtils.java

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@
1616

1717
package org.springframework.grpc.internal;
1818

19+
import java.net.InetSocketAddress;
20+
import java.net.SocketAddress;
21+
import java.util.Objects;
22+
23+
import org.springframework.util.StringUtils;
24+
1925
/**
2026
* Provides convenience methods for various gRPC functions.
2127
*
@@ -36,9 +42,21 @@ private GrpcUtils() {
3642
*/
3743
public static int getPort(String address) {
3844
String value = address;
39-
if (value.contains(":")) {
45+
long numberOfColons = countColons(address);
46+
if (numberOfColons == 1) {
4047
value = value.substring(value.lastIndexOf(":") + 1);
4148
}
49+
else if (numberOfColons > 1) {
50+
if (address.startsWith("[")) {
51+
var index = address.lastIndexOf("]:");
52+
if (index >= 0) {
53+
value = address.substring(index + 2);
54+
}
55+
else {
56+
return DEFAULT_PORT;
57+
}
58+
}
59+
}
4260
if (value.contains("/")) {
4361
value = value.substring(0, value.indexOf("/"));
4462
}
@@ -51,4 +69,68 @@ public static int getPort(String address) {
5169
return DEFAULT_PORT;
5270
}
5371

72+
public static long countColons(String address) {
73+
return address.chars().filter(ch -> ch == ':').count();
74+
}
75+
76+
/**
77+
* Gets the hostname from a given address.
78+
* @param address a hostname/IPv4/IPv6/empty/* optionally with a port specification
79+
* @return the hostname or an empty string
80+
* @see <a href=
81+
* "https://en.wikipedia.org/wiki/IPv6#Address_representation">https://en.wikipedia.org/wiki/IPv6#Address_representation</a>
82+
*/
83+
public static String getHostName(String address) {
84+
String trimmedAddress = address.trim();
85+
long numberOfColons = countColons(trimmedAddress);
86+
87+
if (numberOfColons == 0) {
88+
return trimmedAddress;
89+
}
90+
91+
if (numberOfColons == 1) { // An IPv6 address mush have at least 2 colons, so is
92+
// {IPv4 or hostname}:{port}
93+
return trimmedAddress.split(":")[0].trim();
94+
}
95+
96+
if (numberOfColons > 8 || numberOfColons == 8 && !trimmedAddress.startsWith("[")) {
97+
// On an IPv6 address a maximum of 7 colons are allowed + 1 for the port
98+
// IPv6 addresses with port should have the format [{address}]:port
99+
throw new IllegalArgumentException("Cannot parse address: " + trimmedAddress);
100+
}
101+
102+
if (trimmedAddress.startsWith("[")) {
103+
var index = trimmedAddress.lastIndexOf("]");
104+
if (index < 0) {
105+
throw new IllegalArgumentException("Cannot parse address: " + trimmedAddress);
106+
}
107+
return trimmedAddress.substring(1, index);
108+
}
109+
110+
return trimmedAddress; // IPv6 Address with no port specified
111+
}
112+
113+
/**
114+
* Gets a SocketAddress for the given address.
115+
*
116+
* If the address part is empty, * or :: a SocketAddress with wildcard address will be
117+
* returned. If the port part is empty or missing, a SocketAddress for the gRPC
118+
* default port (9090) will be returned.
119+
* @param address a hostname/IPv4/IPv6/empty/* optionally with a port specification
120+
* @return a SocketAddress representation for the given address
121+
*/
122+
public static SocketAddress getSocketAddress(String address) {
123+
if (address.startsWith("unix:")) {
124+
throw new UnsupportedOperationException("Unix socket addresses not supported");
125+
}
126+
127+
var host = getHostName(address);
128+
if (StringUtils.hasText(host) && !Objects.equals(host, "*") && !Objects.equals(host, "::")) {
129+
return new InetSocketAddress(host, getPort(address));
130+
}
131+
else {
132+
return new InetSocketAddress(getPort(address));
133+
}
134+
}
135+
54136
}

spring-grpc-core/src/main/java/org/springframework/grpc/server/NettyGrpcServerFactory.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323

2424
import org.jspecify.annotations.Nullable;
2525

26+
import org.springframework.grpc.internal.GrpcUtils;
27+
2628
import io.grpc.TlsServerCredentials.ClientAuth;
2729
import io.grpc.netty.NettyServerBuilder;
2830
import io.netty.channel.MultiThreadIoEventLoopGroup;
@@ -56,7 +58,7 @@ protected NettyServerBuilder newServerBuilder() {
5658
.bossEventLoopGroup(new MultiThreadIoEventLoopGroup(1, EpollIoHandler.newFactory()))
5759
.workerEventLoopGroup(new MultiThreadIoEventLoopGroup(EpollIoHandler.newFactory()));
5860
}
59-
return super.newServerBuilder();
61+
return NettyServerBuilder.forAddress(GrpcUtils.getSocketAddress(address()), credentials());
6062
}
6163

6264
}

spring-grpc-core/src/main/java/org/springframework/grpc/server/ShadedNettyGrpcServerFactory.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323

2424
import org.jspecify.annotations.Nullable;
2525

26+
import org.springframework.grpc.internal.GrpcUtils;
27+
2628
import io.grpc.TlsServerCredentials.ClientAuth;
2729
import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder;
2830
import io.grpc.netty.shaded.io.netty.channel.epoll.EpollEventLoopGroup;
@@ -55,7 +57,7 @@ protected NettyServerBuilder newServerBuilder() {
5557
.bossEventLoopGroup(new EpollEventLoopGroup(1))
5658
.workerEventLoopGroup(new EpollEventLoopGroup());
5759
}
58-
return super.newServerBuilder();
60+
return NettyServerBuilder.forAddress(GrpcUtils.getSocketAddress(address()), credentials());
5961
}
6062

6163
}

spring-grpc-core/src/test/java/org/springframework/grpc/internal/GrpcUtilsTests.java

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,15 @@
1717
package org.springframework.grpc.internal;
1818

1919
import static org.assertj.core.api.Assertions.assertThat;
20+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
2021

22+
import java.net.InetSocketAddress;
23+
import java.net.SocketAddress;
24+
import java.util.List;
25+
26+
import org.junit.jupiter.api.DynamicTest;
2127
import org.junit.jupiter.api.Test;
28+
import org.junit.jupiter.api.TestFactory;
2229

2330
class GrpcUtilsTests {
2431

@@ -56,4 +63,48 @@ void testGetInvalidAddress() {
5663
assertThat(GrpcUtils.getPort(address)).isEqualTo(9090); // -1?
5764
}
5865

66+
@TestFactory
67+
List<DynamicTest> ipAddress() {
68+
return List.of(testIpAddress(":9999", new InetSocketAddress(9999)),
69+
testIpAddress("localhost:9999", new InetSocketAddress("localhost", 9999)),
70+
testIpAddress("localhost", new InetSocketAddress("localhost", 9090)),
71+
testIpAddress("127.0.0.1", new InetSocketAddress("127.0.0.1", 9090)),
72+
testIpAddress("127.0.0.1:8888", new InetSocketAddress("127.0.0.1", 8888)),
73+
testIpAddress("*", new InetSocketAddress(9090)), testIpAddress("*:8888", new InetSocketAddress(8888)),
74+
testIpAddress("", new InetSocketAddress(9090)),
75+
// IPv6 cases. See
76+
// https://en.wikipedia.org/wiki/IPv6#Address_representation
77+
testIpAddress("[::]:8888", new InetSocketAddress(8888)),
78+
testIpAddress("::", new InetSocketAddress(9090)), testIpAddress("[::]", new InetSocketAddress(9090)),
79+
testIpAddress("::1", new InetSocketAddress("::1", 9090)),
80+
testIpAddress("[::1]", new InetSocketAddress("::1", 9090)),
81+
testIpAddress("[::1]:9999", new InetSocketAddress("::1", 9999)),
82+
testIpAddress("2001:db8::ff00:42:8329", new InetSocketAddress("2001:db8::ff00:42:8329", 9090)),
83+
testIpAddress("[2001:db8::ff00:42:8329]", new InetSocketAddress("2001:db8::ff00:42:8329", 9090)),
84+
testIpAddress("[2001:db8::ff00:42:8329]:9999", new InetSocketAddress("2001:db8::ff00:42:8329", 9999)),
85+
testIpAddress("::ffff:192.0.2.128", new InetSocketAddress("::ffff:192.0.2.128", 9090)),
86+
testIpAddress("[::ffff:192.0.2.128]", new InetSocketAddress("::ffff:192.0.2.128", 9090)),
87+
testIpAddress("[::ffff:192.0.2.128]:9999", new InetSocketAddress("::ffff:192.0.2.128", 9999)));
88+
}
89+
90+
private DynamicTest testIpAddress(String address, SocketAddress expected) {
91+
return DynamicTest.dynamicTest("Socket address: " + address, () -> {
92+
assertThat(GrpcUtils.getSocketAddress(address)).isEqualTo(expected);
93+
});
94+
}
95+
96+
@TestFactory
97+
List<DynamicTest> unsupportedAddress() {
98+
return List.of(testThrows("unix:dummy", UnsupportedOperationException.class),
99+
testThrows("0:1:2:3:4:5:6:7:8:9", IllegalArgumentException.class),
100+
testThrows("[0:1:2:3:4:5:6:7:8:9]", IllegalArgumentException.class),
101+
testThrows("[0:1:2:3:4:5:6:7:8]:9", IllegalArgumentException.class),
102+
testThrows("[0:1:2:3:4:5:6:7]:8:9", IllegalArgumentException.class));
103+
}
104+
105+
private DynamicTest testThrows(String address, Class<? extends Exception> expectedException) {
106+
return DynamicTest.dynamicTest("Socket address: " + address, () -> assertThatExceptionOfType(expectedException)
107+
.isThrownBy(() -> GrpcUtils.getSocketAddress(address)));
108+
}
109+
59110
}

spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerProperties.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,15 @@ public void setAddress(@Nullable String address) {
9999
* @return the address to bind to
100100
*/
101101
public String determineAddress() {
102-
return (this.address != null) ? this.address : this.host + ":" + this.port;
102+
return (this.address != null) ? this.address : getAddressFromHostAndPort();
103+
}
104+
105+
private String getAddressFromHostAndPort() {
106+
return isIpV6() ? "[%s]:%d".formatted(this.host, this.port) : this.host + ":" + this.port;
107+
}
108+
109+
private boolean isIpV6() {
110+
return GrpcUtils.countColons(this.host) >= 2;
103111
}
104112

105113
public String getHost() {

spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerPropertiesTests.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424

2525
import org.junit.jupiter.api.Nested;
2626
import org.junit.jupiter.api.Test;
27+
import org.junit.jupiter.params.ParameterizedTest;
28+
import org.junit.jupiter.params.provider.ValueSource;
2729

2830
import org.springframework.boot.context.properties.bind.Binder;
2931
import org.springframework.boot.context.properties.source.MapConfigurationPropertySource;
@@ -187,6 +189,28 @@ void addressTakesPrecedenceOverHostAndPort() {
187189
assertThat(properties.getAddress()).isEqualTo("my-server-ip:3130");
188190
}
189191

192+
@ParameterizedTest
193+
@ValueSource(strings = { "dummy.springframework.org", "127.0.0.1", "0.0.0.0", "192.168.1.2" })
194+
void hostnameOrIpv4HostAndPort(String hostName) {
195+
Map<String, String> map = new HashMap<>();
196+
map.put("spring.grpc.server.host", hostName);
197+
map.put("spring.grpc.server.port", "1234");
198+
GrpcServerProperties properties = bindProperties(map);
199+
assertThat(properties.getAddress()).isNullOrEmpty();
200+
assertThat(properties.determineAddress()).isEqualTo(hostName + ":1234");
201+
}
202+
203+
@ParameterizedTest
204+
@ValueSource(strings = { "::", "1:2:3:4:5:6:7:8", "::1" })
205+
void ipv6HostAndPort(String ipv6Address) {
206+
Map<String, String> map = new HashMap<>();
207+
map.put("spring.grpc.server.host", ipv6Address);
208+
map.put("spring.grpc.server.port", "1234");
209+
GrpcServerProperties properties = bindProperties(map);
210+
assertThat(properties.getAddress()).isNullOrEmpty();
211+
assertThat(properties.determineAddress()).isEqualTo("[" + ipv6Address + "]:1234");
212+
}
213+
190214
}
191215

192216
}

0 commit comments

Comments
 (0)