diff --git a/api/src/main/java/io/grpc/Uri.java b/api/src/main/java/io/grpc/Uri.java index 612cbbaab99..9f8a5a87848 100644 --- a/api/src/main/java/io/grpc/Uri.java +++ b/api/src/main/java/io/grpc/Uri.java @@ -245,23 +245,7 @@ public static Uri create(String s) { break; } } - String authority = s.substring(authorityStart, i); - - // 3.2.1. UserInfo. Easy, because '@' cannot appear unencoded inside userinfo or host. - int userInfoEnd = authority.indexOf('@'); - if (userInfoEnd >= 0) { - builder.setRawUserInfo(authority.substring(0, userInfoEnd)); - } - - // 3.2.2/3. Host/Port. - int hostStart = userInfoEnd >= 0 ? userInfoEnd + 1 : 0; - int portStartColon = findPortStartColon(authority, hostStart); - if (portStartColon < 0) { - builder.setRawHost(authority.substring(hostStart, authority.length())); - } else { - builder.setRawHost(authority.substring(hostStart, portStartColon)); - builder.setRawPort(authority.substring(portStartColon + 1)); - } + builder.setRawAuthority(s.substring(authorityStart, i)); } // 3.3. Path: Whatever is left before '?' or '#'. @@ -356,6 +340,15 @@ public String getScheme() { /** * Returns the percent-decoded "Authority" component of this URI, or null if not present. * + *
NB: This method's decoding is lossy -- It only exists for compatibility with {@link
+ * java.net.URI}. Prefer {@link #getRawAuthority()} or work instead with authority in terms of its
+ * individual components ({@link #getUserInfo()}, {@link #getHost()} and {@link #getPort()}). The
+ * problem with getAuthority() is that it returns the delimited concatenation of the percent-
+ * decoded userinfo, host and port components. But both userinfo and host can contain the '@'
+ * character, which becomes indistinguishable from the userinfo/host delimiter after decoding. For
+ * example, URIs scheme://x@y%40z and scheme://x%40y@z have different
+ * userinfo and host components but getAuthority() returns "x@y@z" for both of them.
+ *
*
NB: This method assumes the "host" component was encoded as UTF-8, as mandated by RFC 3986. * This method also assumes the "user information" part of authority was encoded as UTF-8, * although RFC 3986 doesn't specify an encoding. @@ -954,6 +947,51 @@ Builder setRawPort(String port) { return this; } + /** + * Specifies the userinfo, host and port URI components all at once using a single string. + * + *
This setter is "raw" in the sense that special characters in userinfo and host must be + * passed in percent-encoded. See RFC 3986 3.2 for the set + * of characters allowed in each component of an authority. + * + *
There's no "cooked" method to set authority like for other URI components because
+ * authority is a *compound* URI component whose userinfo, host and port components are
+ * delimited with special characters '@' and ':'. But the first two of those components can
+ * themselves contain these delimiters so we need percent-encoding to parse them unambiguously.
+ *
+ * @param authority an RFC 3986 authority string that will be used to set userinfo, host and
+ * port, or null to clear all three of those components
+ */
+ @CanIgnoreReturnValue
+ public Builder setRawAuthority(@Nullable String authority) {
+ if (authority == null) {
+ setUserInfo(null);
+ setHost((String) null);
+ setPort(-1);
+ } else {
+ // UserInfo. Easy because '@' cannot appear unencoded inside userinfo or host.
+ int userInfoEnd = authority.indexOf('@');
+ if (userInfoEnd >= 0) {
+ setRawUserInfo(authority.substring(0, userInfoEnd));
+ } else {
+ setUserInfo(null);
+ }
+
+ // Host/Port.
+ int hostStart = userInfoEnd >= 0 ? userInfoEnd + 1 : 0;
+ int portStartColon = findPortStartColon(authority, hostStart);
+ if (portStartColon < 0) {
+ setRawHost(authority.substring(hostStart));
+ setPort(-1);
+ } else {
+ setRawHost(authority.substring(hostStart, portStartColon));
+ setRawPort(authority.substring(portStartColon + 1));
+ }
+ }
+ return this;
+ }
+
/** Builds a new instance of {@link Uri} as specified by the setters. */
public Uri build() {
checkState(scheme != null, "Missing required scheme.");
diff --git a/api/src/test/java/io/grpc/UriTest.java b/api/src/test/java/io/grpc/UriTest.java
index a0f2e96b1cd..a1bd550696f 100644
--- a/api/src/test/java/io/grpc/UriTest.java
+++ b/api/src/test/java/io/grpc/UriTest.java
@@ -627,6 +627,53 @@ public void builder_canClearAllOptionalFields() {
assertThat(uri.toString()).isEqualTo("http:");
}
+ @Test
+ public void builder_canClearAuthorityComponents() {
+ Uri uri = Uri.create("s://user@host:80/path").toBuilder().setRawAuthority(null).build();
+ assertThat(uri.toString()).isEqualTo("s:/path");
+ }
+
+ @Test
+ public void builder_canSetEmptyAuthority() {
+ Uri uri = Uri.create("s://user@host:80/path").toBuilder().setRawAuthority("").build();
+ assertThat(uri.toString()).isEqualTo("s:///path");
+ }
+
+ @Test
+ public void builder_canSetRawAuthority() {
+ Uri uri = Uri.newBuilder().setScheme("http").setRawAuthority("user@host:1234").build();
+ assertThat(uri.getUserInfo()).isEqualTo("user");
+ assertThat(uri.getHost()).isEqualTo("host");
+ assertThat(uri.getPort()).isEqualTo(1234);
+ }
+
+ @Test
+ public void builder_setRawAuthorityPercentDecodes() {
+ Uri uri =
+ Uri.newBuilder()
+ .setScheme("http")
+ .setRawAuthority("user:user%40user@host%40host%3Ahost")
+ .build();
+ assertThat(uri.getUserInfo()).isEqualTo("user:user@user");
+ assertThat(uri.getHost()).isEqualTo("host@host:host");
+ assertThat(uri.getPort()).isEqualTo(-1);
+ }
+
+ @Test
+ public void builder_setRawAuthorityReplacesAllComponents() {
+ Uri uri =
+ Uri.newBuilder()
+ .setScheme("http")
+ .setUserInfo("user")
+ .setHost("host")
+ .setPort(1234)
+ .setRawAuthority("other")
+ .build();
+ assertThat(uri.getUserInfo()).isNull();
+ assertThat(uri.getHost()).isEqualTo("other");
+ assertThat(uri.getPort()).isEqualTo(-1);
+ }
+
@Test
public void toString_percentEncodingMultiChar() throws URISyntaxException {
Uri uri =
diff --git a/googleapis/src/main/java/io/grpc/googleapis/GoogleCloudToProdExperimentalNameResolverProvider.java b/googleapis/src/main/java/io/grpc/googleapis/GoogleCloudToProdExperimentalNameResolverProvider.java
index 349e1c94380..db674aeb2ee 100644
--- a/googleapis/src/main/java/io/grpc/googleapis/GoogleCloudToProdExperimentalNameResolverProvider.java
+++ b/googleapis/src/main/java/io/grpc/googleapis/GoogleCloudToProdExperimentalNameResolverProvider.java
@@ -20,6 +20,7 @@
import io.grpc.NameResolver;
import io.grpc.NameResolver.Args;
import io.grpc.NameResolverProvider;
+import io.grpc.Uri;
import java.net.URI;
/**
@@ -35,6 +36,11 @@ public NameResolver newNameResolver(URI targetUri, Args args) {
return delegate.newNameResolver(targetUri, args);
}
+ @Override
+ public NameResolver newNameResolver(Uri targetUri, Args args) {
+ return delegate.newNameResolver(targetUri, args);
+ }
+
@Override
public String getDefaultScheme() {
return delegate.getDefaultScheme();
diff --git a/googleapis/src/main/java/io/grpc/googleapis/GoogleCloudToProdNameResolver.java b/googleapis/src/main/java/io/grpc/googleapis/GoogleCloudToProdNameResolver.java
index 0aee17b6b9f..427c0658531 100644
--- a/googleapis/src/main/java/io/grpc/googleapis/GoogleCloudToProdNameResolver.java
+++ b/googleapis/src/main/java/io/grpc/googleapis/GoogleCloudToProdNameResolver.java
@@ -29,6 +29,7 @@
import io.grpc.NameResolverRegistry;
import io.grpc.Status;
import io.grpc.SynchronizationContext;
+import io.grpc.Uri;
import io.grpc.alts.InternalCheckGcpEnvironment;
import io.grpc.internal.GrpcUtil;
import io.grpc.internal.SharedResourceHolder;
@@ -49,6 +50,7 @@
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
+import java.util.List;
import java.util.Random;
import java.util.concurrent.Executor;
import java.util.logging.Level;
@@ -114,6 +116,7 @@ private static synchronized BootstrapInfo getBootstrapInfo()
NameResolverRegistry.getDefaultRegistry().asFactory());
}
+ // TODO(jdcormie): Remove after io.grpc.Uri migration.
@VisibleForTesting
GoogleCloudToProdNameResolver(URI targetUri, Args args, Resource