Skip to content

Commit b7287c5

Browse files
committed
Properly parse IPv6 host and port into RedisNode.
Closes #2418
1 parent 01aa6c4 commit b7287c5

File tree

5 files changed

+126
-22
lines changed

5 files changed

+126
-22
lines changed

src/main/java/org/springframework/data/redis/connection/RedisClusterConfiguration.java

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ public RedisClusterConfiguration clusterNode(String host, Integer port) {
178178
private void appendClusterNodes(Set<String> hostAndPorts) {
179179

180180
for (String hostAndPort : hostAndPorts) {
181-
addClusterNode(readHostAndPortFromString(hostAndPort));
181+
addClusterNode(RedisNode.fromString(hostAndPort));
182182
}
183183
}
184184

@@ -260,15 +260,6 @@ public int hashCode() {
260260
return result;
261261
}
262262

263-
private RedisNode readHostAndPortFromString(String hostAndPort) {
264-
265-
String[] args = split(hostAndPort, ":");
266-
267-
Assert.notNull(args, "HostAndPort need to be seperated by ':'.");
268-
Assert.isTrue(args.length == 2, "Host and Port String needs to specified as host:port");
269-
return new RedisNode(args[0], Integer.valueOf(args[1]));
270-
}
271-
272263
/**
273264
* @param clusterHostAndPorts must not be {@literal null} or empty.
274265
* @param redirects the max number of redirects to follow.
@@ -288,4 +279,5 @@ private static Map<String, Object> asMap(Collection<String> clusterHostAndPorts,
288279

289280
return map;
290281
}
282+
291283
}

src/main/java/org/springframework/data/redis/connection/RedisNode.java

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,95 @@ private RedisNode(RedisNode redisNode) {
6161
this.masterId = redisNode.masterId;
6262
}
6363

64+
/**
65+
* Parse a {@code hostAndPort} string into {@link RedisNode}. Supports IPv4, IPv6, and hostname notations including
66+
* the port. For example:
67+
*
68+
* <pre class="code">
69+
* RedisNode.fromString("127.0.0.1:6379");
70+
* RedisNode.fromString("[aaaa:bbbb::dddd:eeee]:6379");
71+
* RedisNode.fromString("my.redis.server:6379");
72+
* </pre>
73+
*
74+
* @param hostPortString must not be {@literal null} or empty.
75+
* @return the parsed {@link RedisNode}.
76+
* @since 2.6.8
77+
*/
78+
public static RedisNode fromString(String hostPortString) {
79+
80+
Assert.notNull(hostPortString, "HostAndPort must not be null");
81+
82+
String host;
83+
String portString = null;
84+
85+
if (hostPortString.startsWith("[")) {
86+
String[] hostAndPort = getHostAndPortFromBracketedHost(hostPortString);
87+
host = hostAndPort[0];
88+
portString = hostAndPort[1];
89+
} else {
90+
int colonPos = hostPortString.indexOf(':');
91+
if (colonPos >= 0 && hostPortString.indexOf(':', colonPos + 1) == -1) {
92+
// Exactly 1 colon. Split into host:port.
93+
host = hostPortString.substring(0, colonPos);
94+
portString = hostPortString.substring(colonPos + 1);
95+
} else {
96+
// 0 or 2+ colons. Bare hostname or IPv6 literal.
97+
host = hostPortString;
98+
}
99+
}
100+
101+
int port = -1;
102+
try {
103+
port = Integer.parseInt(portString);
104+
} catch (RuntimeException e) {
105+
throw new IllegalArgumentException(String.format("Unparseable port number: %s", hostPortString));
106+
}
107+
108+
if (!isValidPort(port)) {
109+
throw new IllegalArgumentException(String.format("Port number out of range: %s", hostPortString));
110+
}
111+
112+
return new RedisNode(host, port);
113+
}
114+
115+
/**
116+
* Parses a bracketed host-port string, throwing IllegalArgumentException if parsing fails.
117+
*
118+
* @param hostPortString the full bracketed host-port specification. Post might not be specified.
119+
* @return an array with 2 strings: host and port, in that order.
120+
* @throws IllegalArgumentException if parsing the bracketed host-port string fails.
121+
*/
122+
private static String[] getHostAndPortFromBracketedHost(String hostPortString) {
123+
124+
if (hostPortString.charAt(0) != '[') {
125+
throw new IllegalArgumentException(
126+
String.format("Bracketed host-port string must start with a bracket: %s", hostPortString));
127+
}
128+
129+
int colonIndex = hostPortString.indexOf(':');
130+
int closeBracketIndex = hostPortString.lastIndexOf(']');
131+
132+
if (!(colonIndex > -1 && closeBracketIndex > colonIndex)) {
133+
throw new IllegalArgumentException(String.format("Invalid bracketed host/port: %s", hostPortString));
134+
}
135+
136+
String host = hostPortString.substring(1, closeBracketIndex);
137+
if (closeBracketIndex + 1 == hostPortString.length()) {
138+
return new String[] { host, "" };
139+
} else {
140+
if (!(hostPortString.charAt(closeBracketIndex + 1) == ':')) {
141+
throw new IllegalArgumentException(
142+
String.format("Only a colon may follow a close bracket: %s", hostPortString));
143+
}
144+
for (int i = closeBracketIndex + 2; i < hostPortString.length(); ++i) {
145+
if (!Character.isDigit(hostPortString.charAt(i))) {
146+
throw new IllegalArgumentException(String.format("Port must be numeric: %s", hostPortString));
147+
}
148+
}
149+
return new String[] { host, hostPortString.substring(closeBracketIndex + 2) };
150+
}
151+
}
152+
64153
/**
65154
* @return can be {@literal null}.
66155
*/
@@ -86,6 +175,11 @@ public Integer getPort() {
86175
}
87176

88177
public String asString() {
178+
179+
if (host != null && host.contains(":")) {
180+
return "[" + host + "]:" + port;
181+
}
182+
89183
return host + ":" + port;
90184
}
91185

@@ -313,4 +407,8 @@ public RedisNode build() {
313407
}
314408
}
315409

410+
private static boolean isValidPort(int port) {
411+
return port >= 0 && port <= 65535;
412+
}
413+
316414
}

src/main/java/org/springframework/data/redis/connection/RedisSentinelConfiguration.java

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ public RedisSentinelConfiguration sentinel(String host, Integer port) {
205205
private void appendSentinels(Set<String> hostAndPorts) {
206206

207207
for (String hostAndPort : hostAndPorts) {
208-
addSentinel(readHostAndPortFromString(hostAndPort));
208+
addSentinel(RedisNode.fromString(hostAndPort));
209209
}
210210
}
211211

@@ -335,15 +335,6 @@ public int hashCode() {
335335
return result;
336336
}
337337

338-
private RedisNode readHostAndPortFromString(String hostAndPort) {
339-
340-
String[] args = split(hostAndPort, ":");
341-
342-
Assert.notNull(args, "HostAndPort need to be seperated by ':'.");
343-
Assert.isTrue(args.length == 2, "Host and Port String needs to specified as host:port");
344-
return new RedisNode(args[0], Integer.valueOf(args[1]).intValue());
345-
}
346-
347338
/**
348339
* @param master must not be {@literal null} or empty.
349340
* @param sentinelHostAndPorts must not be {@literal null}.

src/test/java/org/springframework/data/redis/connection/RedisClusterConfigurationUnitTests.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
import org.springframework.util.StringUtils;
2929

3030
/**
31+
* Unit tests for {@link RedisClusterConfiguration}.
32+
*
3133
* @author Christoph Strobl
3234
* @author Mark Paluch
3335
*/
@@ -48,12 +50,22 @@ void shouldCreateRedisClusterConfigurationCorrectly() {
4850
assertThat(config.getMaxRedirects()).isNull();
4951
}
5052

53+
@Test // GH-2418
54+
void shouldCreateRedisClusterConfigurationForIPV6Correctly() {
55+
56+
RedisClusterConfiguration config = new RedisClusterConfiguration(Collections.singleton("[aaa:bbb:ccc::dd1]:123"));
57+
58+
assertThat(config.getClusterNodes().size()).isEqualTo(1);
59+
assertThat(config.getClusterNodes()).contains(new RedisNode("aaa:bbb:ccc::dd1", 123));
60+
assertThat(config.getClusterNodes()).first().hasToString("[aaa:bbb:ccc::dd1]:123");
61+
assertThat(config.getMaxRedirects()).isNull();
62+
}
63+
5164
@Test // DATAREDIS-315
5265
void shouldCreateRedisClusterConfigurationCorrectlyGivenMultipleHostAndPortStrings() {
5366

5467
RedisClusterConfiguration config = new RedisClusterConfiguration(
55-
new HashSet<>(Arrays.asList(HOST_AND_PORT_1,
56-
HOST_AND_PORT_2, HOST_AND_PORT_3)));
68+
new HashSet<>(Arrays.asList(HOST_AND_PORT_1, HOST_AND_PORT_2, HOST_AND_PORT_3)));
5769

5870
assertThat(config.getClusterNodes().size()).isEqualTo(3);
5971
assertThat(config.getClusterNodes()).contains(new RedisNode("127.0.0.1", 123), new RedisNode("localhost", 456),

src/test/java/org/springframework/data/redis/connection/RedisSentinelConfigurationUnitTests.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929

3030
/**
3131
* @author Christoph Strobl
32+
* @author Mark Paluch
3233
*/
3334
class RedisSentinelConfigurationUnitTests {
3435

@@ -47,6 +48,16 @@ void shouldCreateRedisSentinelConfigurationCorrectlyGivenMasterAndSingleHostAndP
4748
assertThat(config.getSentinels()).contains(new RedisNode("127.0.0.1", 123));
4849
}
4950

51+
@Test // GH-2418
52+
void shouldCreateRedisSentinelConfigurationCorrectlyGivenMasterAndSingleIPV6HostAndPortString() {
53+
54+
RedisSentinelConfiguration config = new RedisSentinelConfiguration("mymaster",
55+
Collections.singleton("[ca:fee::1]:123"));
56+
57+
assertThat(config.getSentinels()).hasSize(1);
58+
assertThat(config.getSentinels()).contains(new RedisNode("ca:fee::1", 123));
59+
}
60+
5061
@Test // DATAREDIS-372
5162
void shouldCreateRedisSentinelConfigurationCorrectlyGivenMasterAndMultipleHostAndPortStrings() {
5263

0 commit comments

Comments
 (0)