Skip to content

Commit ddf093c

Browse files
committed
RFC6874 zone IDs with minimal parsing Bracket/encode only; treat IPv6 literal opaquely, decode/validate ZoneID; keep colon-count heuristic.
1 parent b40a633 commit ddf093c

File tree

6 files changed

+419
-15
lines changed

6 files changed

+419
-15
lines changed

httpcore5/src/main/java/org/apache/hc/core5/http/HttpHost.java

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import org.apache.hc.core5.net.Host;
3939
import org.apache.hc.core5.net.NamedEndpoint;
4040
import org.apache.hc.core5.net.URIAuthority;
41+
import org.apache.hc.core5.net.ZoneIdSupport;
4142
import org.apache.hc.core5.util.Args;
4243
import org.apache.hc.core5.util.LangUtils;
4344
import org.apache.hc.core5.util.TextUtils;
@@ -303,13 +304,24 @@ public InetAddress getAddress() {
303304
*/
304305
public String toURI() {
305306
final StringBuilder buffer = new StringBuilder();
306-
buffer.append(this.schemeName);
307-
buffer.append("://");
308-
buffer.append(this.host.toString());
307+
buffer.append(this.schemeName).append("://");
308+
309+
final String hostname = this.host.getHostName();
310+
final int port = this.host.getPort();
311+
312+
// Bracket only real IPv6 literals; decide using the address part only (ignore zone)
313+
if (ZoneIdSupport.looksLikeIPv6AddressPart(hostname)) {
314+
ZoneIdSupport.appendBracketedIPv6(buffer, hostname);
315+
if (port >= 0) {
316+
buffer.append(':').append(port);
317+
}
318+
} else {
319+
// reg-name / IPv4 / special forms like "host:80" for CONNECT
320+
buffer.append(this.host);
321+
}
309322
return buffer.toString();
310323
}
311324

312-
313325
/**
314326
* Obtains the host string, without scheme prefix.
315327
*

httpcore5/src/main/java/org/apache/hc/core5/net/URIAuthority.java

Lines changed: 104 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,19 @@ public final class URIAuthority implements NamedEndpoint, Serializable {
5151
private final String userInfo;
5252
private final Host host;
5353

54+
static URIAuthority parse(final CharSequence s) throws URISyntaxException {
55+
if (TextUtils.isBlank(s)) {
56+
return null;
57+
}
58+
final Tokenizer.Cursor cursor = new Tokenizer.Cursor(0, s.length());
59+
return parse(s, cursor); // intentionally no cursor.atEnd() check
60+
}
61+
5462
static URIAuthority parse(final CharSequence s, final Tokenizer.Cursor cursor) throws URISyntaxException {
5563
final Tokenizer tokenizer = Tokenizer.INSTANCE;
5664
String userInfo = null;
65+
66+
// optional userinfo@
5767
final int initPos = cursor.getPos();
5868
final String token = tokenizer.parseContent(s, cursor, URISupport.HOST_DELIMITERS);
5969
if (!cursor.atEnd() && s.charAt(cursor.getPos()) == '@') {
@@ -62,26 +72,112 @@ static URIAuthority parse(final CharSequence s, final Tokenizer.Cursor cursor) t
6272
userInfo = token;
6373
}
6474
} else {
65-
//Rewind
6675
cursor.updatePos(initPos);
6776
}
77+
78+
if (!cursor.atEnd() && s.charAt(cursor.getPos()) == '[') {
79+
final int lb = cursor.getPos();
80+
final int upper = cursor.getUpperBound();
81+
int rb = -1;
82+
for (int i = lb + 1; i < upper; i++) {
83+
if (s.charAt(i) == ']') {
84+
rb = i;
85+
break;
86+
}
87+
}
88+
if (rb < 0) {
89+
throw URISupport.createException(s.toString(), cursor, "Expected closing bracket for IPv6 address");
90+
}
91+
92+
final String literal = s.subSequence(lb + 1, rb).toString();
93+
final int zoneMark = literal.indexOf("%25");
94+
final String addrPart = zoneMark >= 0 ? literal.substring(0, zoneMark) : literal;
95+
96+
int colons = 0;
97+
for (int i = 0; i < addrPart.length(); i++) {
98+
if (addrPart.charAt(i) == ':') {
99+
if (++colons >= 2) {
100+
break;
101+
}
102+
}
103+
}
104+
if (colons < 2) {
105+
throw URISupport.createException(s.toString(), cursor, "Expected an IPv6 address");
106+
}
107+
108+
if (zoneMark >= 0) {
109+
final String zoneEnc = literal.substring(zoneMark + 3);
110+
ZoneIdSupport.validateZoneIdEncoded(zoneEnc);
111+
}
112+
// Store host in friendly form: "...%<decoded-zone>" (or literal as-is if no zone)
113+
final String hostName = ZoneIdSupport.decodeZoneId(literal);
114+
115+
// optional :port
116+
int pos = rb + 1;
117+
int port = -1;
118+
if (pos < upper && s.charAt(pos) == ':') {
119+
pos++;
120+
if (pos >= upper || !Character.isDigit(s.charAt(pos))) {
121+
throw URISupport.createException(s.toString(), cursor, "Invalid port");
122+
}
123+
long acc = 0;
124+
while (pos < upper && Character.isDigit(s.charAt(pos))) {
125+
acc = acc * 10 + (s.charAt(pos) - '0');
126+
if (acc > 65535) {
127+
throw URISupport.createException(s.toString(), cursor, "Port out of range");
128+
}
129+
pos++;
130+
}
131+
port = (int) acc;
132+
}
133+
cursor.updatePos(pos);
134+
return new URIAuthority(userInfo, hostName, port);
135+
}
136+
137+
{
138+
final int start = cursor.getPos();
139+
final int upper = cursor.getUpperBound();
140+
int i = start;
141+
int colonCount = 0;
142+
while (i < upper) {
143+
final char ch = s.charAt(i);
144+
if (ch == '/' || ch == '?' || ch == '#') {
145+
break; // end of authority
146+
}
147+
if (ch == ']') {
148+
break; // safety
149+
}
150+
if (ch == ':') {
151+
if (++colonCount > 1) {
152+
throw URISupport.createException(s.toString(), cursor, "Expected an IPv6 address");
153+
}
154+
}
155+
i++;
156+
}
157+
}
158+
68159
final Host host = Host.parse(s, cursor);
69160
return new URIAuthority(userInfo, host);
70161
}
71162

72-
static URIAuthority parse(final CharSequence s) throws URISyntaxException {
73-
final Tokenizer.Cursor cursor = new Tokenizer.Cursor(0, s.length());
74-
return parse(s, cursor);
75-
}
76163

77164
static void format(final StringBuilder buf, final URIAuthority uriAuthority) {
78165
if (uriAuthority.getUserInfo() != null) {
79-
buf.append(uriAuthority.getUserInfo());
80-
buf.append("@");
166+
buf.append(uriAuthority.getUserInfo()).append("@");
167+
}
168+
final String hostName = uriAuthority.getHostName();
169+
final int port = uriAuthority.getPort();
170+
171+
if (ZoneIdSupport.appendBracketedIPv6(buf, hostName)) {
172+
if (port >= 0) {
173+
buf.append(':').append(port);
174+
}
175+
} else {
176+
Host.format(buf, uriAuthority);
81177
}
82-
Host.format(buf, uriAuthority);
83178
}
84179

180+
85181
static String format(final URIAuthority uriAuthority) {
86182
final StringBuilder buf = new StringBuilder();
87183
format(buf, uriAuthority);

httpcore5/src/main/java/org/apache/hc/core5/net/URIBuilder.java

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -424,14 +424,16 @@ private String buildString() {
424424
}
425425
sb.append("@");
426426
}
427-
if (InetAddressUtils.isIPv6(this.host)) {
428-
sb.append("[").append(this.host).append("]");
427+
428+
// Bracket only true IPv6 hosts; decide based on address part only (ignore zone)
429+
if (ZoneIdSupport.appendBracketedIPv6(sb, this.host)) {
430+
// wrote [IPv6%25zone]
429431
} else {
430432
PercentCodec.encode(sb, this.host, this.charset,
431433
encodingPolicy == EncodingPolicy.ALL_RESERVED ? PercentCodec.UNRESERVED : PercentCodec.REG_NAME, false);
432434
}
433435
if (this.port >= 0) {
434-
sb.append(":").append(this.port);
436+
sb.append(':').append(this.port);
435437
}
436438
authoritySpecified = true;
437439
} else {
@@ -478,6 +480,10 @@ private void digestURI(final URI uri, final Charset charset) {
478480
this.host = uriHost != null && InetAddressUtils.isIPv6URLBracketed(uriHost)
479481
? uriHost.substring(1, uriHost.length() - 1)
480482
: uriHost;
483+
484+
// Normalize zone-id to user-friendly form: "...%25zone" -> "...%zone" (and decode %HH in zone)
485+
this.host = ZoneIdSupport.decodeZoneId(this.host);
486+
481487
this.port = uri.getPort();
482488
this.encodedUserInfo = uri.getRawUserInfo();
483489
this.userInfo = uri.getUserInfo();

0 commit comments

Comments
 (0)