Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion api/src/main/java/io/grpc/Uri.java
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,12 @@
*
* <p>{@link java.net.URI} and {@link Uri} both support IPv6 literals in square brackets as defined
* by RFC 2732.
*
* <p>{@link java.net.URI} supports IPv6 scope IDs but accepts and emits a non-standard syntax.
* {@link Uri} implements the newer RFC 6874, which percent encodes scope IDs and the % delimiter
* itself. RFC 9844 claims to obsolete RFC 6874 because web browsers would not support it. This
* class implements RFC 6874 anyway, mostly to avoid creating a barrier to migration away from
* {@link java.net.URI}.
*/
@Internal
public final class Uri {
Expand Down Expand Up @@ -825,12 +831,32 @@ public Builder setHost(@Nullable String regName) {
*/
@CanIgnoreReturnValue
public Builder setHost(@Nullable InetAddress addr) {
this.host = addr != null ? InetAddresses.toUriString(addr) : null;
this.host = addr != null ? toUriString(addr) : null;
return this;
}

private static String toUriString(InetAddress addr) {
// InetAddresses.toUriString(addr) is almost enough but neglects RFC 6874 percent encoding.
String inetAddrStr = InetAddresses.toUriString(addr);
int percentIndex = inetAddrStr.indexOf('%');
if (percentIndex < 0) {
return inetAddrStr;
}

String scope = inetAddrStr.substring(percentIndex, inetAddrStr.length() - 1);
return inetAddrStr.substring(0, percentIndex) + percentEncode(scope, unreservedChars) + "]";
}

@CanIgnoreReturnValue
Builder setRawHost(String host) {
if (host.startsWith("[") && host.endsWith("]")) {
// IP-literal: Guava's isUriInetAddress() is almost enough but it doesn't check the scope.
int percentIndex = host.indexOf('%');
if (percentIndex > 0) {
String scope = host.substring(percentIndex, host.length() - 1);
checkPercentEncodedArg(scope, "scope", unreservedChars);
}
}
// IP-literal validation is complicated so we delegate it to Guava. We use this particular
// method of InetAddresses because it doesn't try to match interfaces on the local machine.
// (The validity of a URI should be the same no matter which machine does the parsing.)
Expand Down
65 changes: 65 additions & 0 deletions api/src/test/java/io/grpc/UriTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@

import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import static org.junit.Assume.assumeNoException;

import com.google.common.net.InetAddresses;
import com.google.common.testing.EqualsTester;
import java.net.Inet6Address;
import java.net.URISyntaxException;
import java.net.UnknownHostException;
import java.util.BitSet;
import org.junit.Test;
import org.junit.runner.RunWith;
Expand Down Expand Up @@ -77,6 +80,20 @@ public void parse_ipv6Literal_noPort() throws URISyntaxException {
assertThat(uri.getPort()).isLessThan(0);
}

@Test
public void parse_ipv6ScopedLiteral() throws URISyntaxException {
Uri uri = Uri.parse("http://[fe80::1%25eth0]");
assertThat(uri.getRawHost()).isEqualTo("[fe80::1%25eth0]");
assertThat(uri.getHost()).isEqualTo("[fe80::1%eth0]");
}

@Test
public void parse_ipv6ScopedPercentEncodedLiteral() throws URISyntaxException {
Uri uri = Uri.parse("http://[fe80::1%25foo-bar%2Fblah]");
assertThat(uri.getRawHost()).isEqualTo("[fe80::1%25foo-bar%2Fblah]");
assertThat(uri.getHost()).isEqualTo("[fe80::1%foo-bar/blah]");
}

@Test
public void parse_noQuery() throws URISyntaxException {
Uri uri = Uri.parse("scheme://authority/path#fragment");
Expand Down Expand Up @@ -203,6 +220,13 @@ public void parse_invalidBackslashInHost_throws() {
assertThat(e).hasMessageThat().contains("Invalid character in host");
}

@Test
public void parse_invalidBackslashScope_throws() {
URISyntaxException e =
assertThrows(URISyntaxException.class, () -> Uri.parse("http://[::1%25foo\\bar]"));
assertThat(e).hasMessageThat().contains("Invalid character in scope");
}

@Test
public void parse_emptyPort_throws() {
URISyntaxException e =
Expand Down Expand Up @@ -397,6 +421,47 @@ public void builder_ipv6Literal() throws URISyntaxException {
assertThat(uri.toString()).isEqualTo("scheme://[2001:4860:4860::8844]");
}

@Test
public void builder_ipv6ScopedLiteral_numeric() throws UnknownHostException {
Uri uri =
Uri.newBuilder()
.setScheme("http")
// Create an address with a numeric scope_id, which should always be valid.
.setHost(
Inet6Address.getByAddress(null, InetAddresses.forString("fe80::1").getAddress(), 1))
.build();

// We expect the scope ID to be percent encoded.
assertThat(uri.getRawHost()).isEqualTo("[fe80::1%251]");
assertThat(uri.getHost()).isEqualTo("[fe80::1%1]");
}

@Test
public void builder_ipv6ScopedLiteral_named() throws UnknownHostException {
// Unfortunately, there's no Java API to create an Inet6Address with an arbitrary interface-
// scoped name. There's actually no way to hermetically create an Inet6Address with a scope name
// at all! The following address/interface is likely to be present on Linux test runners.
Inet6Address address;
try {
address = (Inet6Address) InetAddresses.forString("::1%lo");
} catch (IllegalArgumentException e) {
assumeNoException(e);
return; // Not reached.
}
Uri uri = Uri.newBuilder().setScheme("http").setHost(address).build();

// We expect the scope ID to be percent encoded.
assertThat(uri.getRawHost()).isEqualTo("[::1%25lo]");
assertThat(uri.getHost()).isEqualTo("[::1%lo]");
}

@Test
public void builder_ipv6PercentEncodedScopedLiteral() {
Uri uri = Uri.newBuilder().setScheme("http").setRawHost("[fe80::1%25foo%2Dbar%2Fblah]").build();
assertThat(uri.getRawHost()).isEqualTo("[fe80::1%25foo%2Dbar%2Fblah]");
assertThat(uri.getHost()).isEqualTo("[fe80::1%foo-bar/blah]");
}

@Test
public void builder_encodingWithAllowedReservedChars() throws URISyntaxException {
Uri uri =
Expand Down