15
15
*/
16
16
package io .servicetalk .transport .api ;
17
17
18
+ import javax .annotation .Nullable ;
19
+
20
+ import static io .servicetalk .utils .internal .NetworkUtils .isValidIpV4Address ;
21
+ import static io .servicetalk .utils .internal .NetworkUtils .isValidIpV6Address ;
22
+ import static java .lang .Integer .parseInt ;
18
23
import static java .util .Objects .requireNonNull ;
19
24
20
25
/**
21
26
* A default immutable implementation of {@link HostAndPort}.
22
27
*/
23
28
final class DefaultHostAndPort implements HostAndPort {
29
+ /**
30
+ * {@code xxx.xxx.xxx.xxx:yyyyy}
31
+ */
32
+ private static final int MAX_IPV4_LEN = 21 ;
33
+ /**
34
+ * {@code [xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx]:yyyyy} = 47 chars w/out zone id
35
+ */
36
+ private static final int MAX_IPV6_LEN = 47 + 12 /* some limit for zone id length */ ;
37
+ private static final String STR_IPV6 = "_ipv6_" ;
24
38
private final String hostName ;
39
+ @ Nullable
40
+ private String toString ;
25
41
private final int port ;
26
42
27
43
/**
@@ -30,8 +46,86 @@ final class DefaultHostAndPort implements HostAndPort {
30
46
* @param port the port.
31
47
*/
32
48
DefaultHostAndPort (String hostName , int port ) {
49
+ if (isValidIpV6Address (requireNonNull (hostName ))) { // Normalize ipv6 so equals/hashCode works as expected
50
+ this .hostName = hostName .charAt (0 ) == '[' ?
51
+ compressIPv6 (hostName , 1 , hostName .length () - 1 ) : compressIPv6 (hostName , 0 , hostName .length ());
52
+ this .toString = STR_IPV6 ;
53
+ } else {
54
+ this .hostName = hostName ;
55
+ }
56
+ this .port = port ;
57
+ }
58
+
59
+ DefaultHostAndPort (String hostName , int port , boolean isIPv6 ) {
33
60
this .hostName = requireNonNull (hostName );
34
61
this .port = port ;
62
+ this .toString = isIPv6 ? STR_IPV6 : null ;
63
+ }
64
+
65
+ /**
66
+ * Parse IPv4 {@code xxx.xxx.xxx.xxx:yyyyy} and IPv6 {@code [xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx]:yyyyy} style
67
+ * addresses.
68
+ * @param ipPort An IPv4 {@code xxx.xxx.xxx.xxx:yyyyy} or IPv6
69
+ * {@code [xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx]:yyyyy} addresses.
70
+ * @param startIndex The index at which the address parsing starts.
71
+ * @return A {@link HostAndPort} where the hostname is the IP address and the port is parsed from the string.
72
+ */
73
+ static HostAndPort parseFromIpPort (String ipPort , int startIndex ) {
74
+ String inetAddress ;
75
+ final boolean isv6 ;
76
+ int i ;
77
+ if (ipPort .charAt (startIndex ) == '[' ) { // check if ipv6
78
+ if (ipPort .length () - startIndex > MAX_IPV6_LEN ) {
79
+ throw new IllegalArgumentException ("Invalid IPv6 address: " + ipPort .substring (startIndex ));
80
+ }
81
+ i = ipPort .indexOf (']' );
82
+ if (i <= startIndex ) {
83
+ throw new IllegalArgumentException ("unable to find end ']' of IPv6 address: " +
84
+ ipPort .substring (startIndex ));
85
+ }
86
+ inetAddress = ipPort .substring (startIndex + 1 , i );
87
+ ++i ;
88
+ isv6 = true ;
89
+ if (i >= ipPort .length ()) {
90
+ throw new IllegalArgumentException ("no port found after ']' of IPv6 address: " +
91
+ ipPort .substring (startIndex ));
92
+ } else if (ipPort .charAt (i ) != ':' ) {
93
+ throw new IllegalArgumentException ("':' expected after ']' for IPv6 address: " +
94
+ ipPort .substring (startIndex ));
95
+ }
96
+ } else {
97
+ if (ipPort .length () - startIndex > MAX_IPV4_LEN ) {
98
+ throw new IllegalArgumentException ("Invalid IPv4 address: " + ipPort .substring (startIndex ));
99
+ }
100
+ i = ipPort .lastIndexOf (':' );
101
+ if (i < 0 ) {
102
+ throw new IllegalArgumentException ("no port found: " + ipPort .substring (startIndex ));
103
+ }
104
+ inetAddress = ipPort .substring (startIndex , i );
105
+ isv6 = false ;
106
+ }
107
+
108
+ final int port ;
109
+ try {
110
+ port = parseInt (ipPort .substring (i + 1 ));
111
+ } catch (NumberFormatException e ) {
112
+ throw new IllegalArgumentException ("invalid port " + ipPort .substring (startIndex ), e );
113
+ }
114
+ if (!isValidPort (port ) || ipPort .charAt (i + 1 ) == '+' ) { // parseInt allows '+' but we don't want this
115
+ throw new IllegalArgumentException ("invalid port " + ipPort .substring (startIndex ));
116
+ }
117
+
118
+ if (isv6 ) {
119
+ if (!isValidIpV6Address (inetAddress )) {
120
+ throw new IllegalArgumentException ("Invalid IPv6 address: " + inetAddress );
121
+ }
122
+ inetAddress = compressIPv6 (inetAddress , 0 , inetAddress .length ());
123
+ return new DefaultHostAndPort (inetAddress , port , true );
124
+ }
125
+ if (!isValidIpV4Address (inetAddress )) {
126
+ throw new IllegalArgumentException ("Invalid IPv4 address: " + inetAddress );
127
+ }
128
+ return new DefaultHostAndPort (inetAddress , port , false );
35
129
}
36
130
37
131
@ Override
@@ -46,7 +140,13 @@ public int port() {
46
140
47
141
@ Override
48
142
public String toString () {
49
- return hostName + ':' + port ;
143
+ String str = toString ;
144
+ if (str == null ) {
145
+ toString = str = hostName + ':' + port ;
146
+ } else if (str == STR_IPV6 ) {
147
+ toString = str = '[' + hostName + "]:" + port ;
148
+ }
149
+ return str ;
50
150
}
51
151
52
152
@ Override
@@ -62,4 +162,68 @@ public boolean equals(Object o) {
62
162
public int hashCode () {
63
163
return 31 * (31 + port ) + hostName .hashCode ();
64
164
}
165
+
166
+ private static boolean isValidPort (int port ) {
167
+ return port >= 0 && port <= 65535 ;
168
+ }
169
+
170
+ private static String compressIPv6 (String rawIp , int start , int end ) {
171
+ if (end - start <= 0 ) {
172
+ throw new IllegalArgumentException ("Empty IPv6 address" );
173
+ }
174
+ // https://datatracker.ietf.org/doc/html/rfc5952#section-2
175
+ // JDK doesn't do IPv6 compression, or remove leading 0s. This may lead to inconsistent String representation
176
+ // which will yield different hash-codes and equals comparisons to fail when it shouldn't.
177
+ int longestZerosCount = 0 ;
178
+ int longestZerosBegin = -1 ;
179
+ int longestZerosEnd = -1 ;
180
+ int zerosCount = 0 ;
181
+ int zerosBegin = rawIp .charAt (start ) != '0' ? -1 : 0 ;
182
+ int zerosEnd = -1 ;
183
+ boolean isCompressed = false ;
184
+ char prevChar = '\0' ;
185
+ StringBuilder compressedIPv6Builder = new StringBuilder (end - start );
186
+ for (int i = start ; i < end ; ++i ) {
187
+ final char c = rawIp .charAt (i );
188
+ switch (c ) {
189
+ case '0' :
190
+ if (zerosBegin < 0 || i == end - 1 ) {
191
+ compressedIPv6Builder .append ('0' );
192
+ }
193
+ break ;
194
+ case ':' :
195
+ if (prevChar == ':' ) {
196
+ isCompressed = true ;
197
+ compressedIPv6Builder .append (':' );
198
+ } else if (zerosBegin >= 0 ) {
199
+ ++zerosCount ;
200
+ compressedIPv6Builder .append ("0:" );
201
+ zerosEnd = compressedIPv6Builder .length ();
202
+ } else {
203
+ compressedIPv6Builder .append (':' );
204
+ zerosBegin = compressedIPv6Builder .length ();
205
+ }
206
+ break ;
207
+ default :
208
+ // https://datatracker.ietf.org/doc/html/rfc5952#section-4.2.3
209
+ // if there is a tie in the longest length, we must choose the first to compress.
210
+ if (zerosEnd > 0 && zerosCount > longestZerosCount ) {
211
+ longestZerosCount = zerosCount ;
212
+ longestZerosBegin = zerosBegin ;
213
+ longestZerosEnd = zerosEnd ;
214
+ }
215
+ zerosBegin = zerosEnd = -1 ;
216
+ zerosCount = 0 ;
217
+ compressedIPv6Builder .append (c );
218
+ break ;
219
+ }
220
+ prevChar = c ;
221
+ }
222
+ // https://datatracker.ietf.org/doc/html/rfc5952#section-4.2.2
223
+ // The symbol "::" MUST NOT be used to shorten just one 16-bit 0 field.
224
+ if (!isCompressed && longestZerosBegin >= 0 && longestZerosCount > 1 ) {
225
+ compressedIPv6Builder .replace (longestZerosBegin , longestZerosEnd , longestZerosBegin == 0 ? "::" : ":" );
226
+ }
227
+ return compressedIPv6Builder .toString ();
228
+ }
65
229
}
0 commit comments