Skip to content

Commit ac35ab6

Browse files
authored
xds: Encode the service authority in XdsNameResolver (#10207)
Encode the service authority before passing it into gRPC util in the xDS name resolver to handle xDS requests which might contain multiple slashes. Example: xds:///path/to/service:port. As currently the underlying Java URI library does not break the encoded authority into host/port correctly simplify the check to just look for '@' as we are only interested in checking for user info to validate the authority for HTTP. This change also leads to few changes in unit tests that relied on this check for invalid authorities which now will be considered valid. Just like #9376, depending on Guava packages such as URLEscapers or PercentEscapers leads to internal failures(Ex: Unresolvable reference to com.google.common.escape.Escaper from io.grpc.internal.GrpcUtil). To avoid these issues create an in house version that is heavily inspired by grpc-go/grpc.
1 parent 0aaa2e0 commit ac35ab6

File tree

5 files changed

+146
-19
lines changed

5 files changed

+146
-19
lines changed

core/src/main/java/io/grpc/internal/GrpcUtil.java

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,11 @@
5555
import java.net.URI;
5656
import java.net.URISyntaxException;
5757
import java.nio.charset.Charset;
58+
import java.util.Arrays;
5859
import java.util.Collection;
5960
import java.util.Collections;
6061
import java.util.EnumSet;
62+
import java.util.HashSet;
6163
import java.util.List;
6264
import java.util.Locale;
6365
import java.util.Set;
@@ -526,8 +528,8 @@ public static URI authorityToUri(String authority) {
526528
*/
527529
public static String checkAuthority(String authority) {
528530
URI uri = authorityToUri(authority);
529-
checkArgument(uri.getHost() != null, "No host in authority '%s'", authority);
530-
checkArgument(uri.getUserInfo() == null,
531+
// Verify that the user Info is not provided.
532+
checkArgument(uri.getAuthority().indexOf('@') == -1,
531533
"Userinfo must not be present on authority: '%s'", authority);
532534
return authority;
533535
}
@@ -859,5 +861,92 @@ static <T> boolean iterableContains(Iterable<T> iterable, T item) {
859861
return false;
860862
}
861863

864+
/**
865+
* Percent encode the {@code authority} based on
866+
* https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.
867+
*
868+
* <p>When escaping a String, the following rules apply:
869+
*
870+
* <ul>
871+
* <li>The alphanumeric characters "a" through "z", "A" through "Z" and "0" through "9" remain
872+
* the same.
873+
* <li>The unreserved characters ".", "-", "~", and "_" remain the same.
874+
* <li>The general delimiters for authority, "[", "]", "@" and ":" remain the same.
875+
* <li>The subdelimiters "!", "$", "&amp;", "'", "(", ")", "*", "+", ",", ";", and "=" remain
876+
* the same.
877+
* <li>The space character " " is converted into %20.
878+
* <li>All other characters are converted into one or more bytes using UTF-8 encoding and each
879+
* byte is then represented by the 3-character string "%XY", where "XY" is the two-digit,
880+
* uppercase, hexadecimal representation of the byte value.
881+
* </ul>
882+
*
883+
* <p>This section does not use URLEscapers from Guava Net as its not Android-friendly thus core
884+
* can't depend on it.
885+
*/
886+
public static class AuthorityEscaper {
887+
// Escapers should output upper case hex digits.
888+
private static final char[] UPPER_HEX_DIGITS = "0123456789ABCDEF".toCharArray();
889+
private static final Set<Character> UNRESERVED_CHARACTERS = Collections
890+
.unmodifiableSet(new HashSet<>(Arrays.asList('-', '_', '.', '~')));
891+
private static final Set<Character> SUB_DELIMS = Collections
892+
.unmodifiableSet(new HashSet<>(
893+
Arrays.asList('!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=')));
894+
private static final Set<Character> AUTHORITY_DELIMS = Collections
895+
.unmodifiableSet(new HashSet<>(Arrays.asList(':', '[', ']', '@')));
896+
897+
private static boolean shouldEscape(char c) {
898+
// Only encode ASCII.
899+
if (c > 127) {
900+
return false;
901+
}
902+
// Letters don't need an escape.
903+
if (((c >= 'a') && (c <= 'z')) || ((c >= 'A') && (c <= 'Z'))) {
904+
return false;
905+
}
906+
// Numbers don't need to be escaped.
907+
if ((c >= '0' && c <= '9')) {
908+
return false;
909+
}
910+
// Don't escape allowed characters.
911+
if (UNRESERVED_CHARACTERS.contains(c)
912+
|| SUB_DELIMS.contains(c)
913+
|| AUTHORITY_DELIMS.contains(c)) {
914+
return false;
915+
}
916+
return true;
917+
}
918+
919+
public static String encodeAuthority(String authority) {
920+
Preconditions.checkNotNull(authority, "authority");
921+
int authorityLength = authority.length();
922+
int hexCount = 0;
923+
// Calculate how many characters actually need escaping.
924+
for (int index = 0; index < authorityLength; index++) {
925+
char c = authority.charAt(index);
926+
if (shouldEscape(c)) {
927+
hexCount++;
928+
}
929+
}
930+
// If no char need escaping, just return the original string back.
931+
if (hexCount == 0) {
932+
return authority;
933+
}
934+
935+
// Allocate enough space as encoded characters need 2 extra chars.
936+
StringBuilder encoded_authority = new StringBuilder((2 * hexCount) + authorityLength);
937+
for (int index = 0; index < authorityLength; index++) {
938+
char c = authority.charAt(index);
939+
if (shouldEscape(c)) {
940+
encoded_authority.append('%');
941+
encoded_authority.append(UPPER_HEX_DIGITS[c >>> 4]);
942+
encoded_authority.append(UPPER_HEX_DIGITS[c & 0xF]);
943+
} else {
944+
encoded_authority.append(c);
945+
}
946+
}
947+
return encoded_authority.toString();
948+
}
949+
}
950+
862951
private GrpcUtil() {}
863952
}

core/src/test/java/io/grpc/internal/GrpcUtilTest.java

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,42 @@ public void contentTypeShouldNotBeValid() {
163163
assertFalse(GrpcUtil.isGrpcContentType("application/bad"));
164164
}
165165

166+
@Test
167+
public void urlAuthorityEscape_ipv6Address() {
168+
assertEquals("[::1]", GrpcUtil.AuthorityEscaper.encodeAuthority("[::1]"));
169+
}
170+
171+
@Test
172+
public void urlAuthorityEscape_userInAuthority() {
173+
assertEquals("user@host", GrpcUtil.AuthorityEscaper.encodeAuthority("user@host"));
174+
}
175+
176+
@Test
177+
public void urlAuthorityEscape_slashesAreEncoded() {
178+
assertEquals(
179+
"project%2F123%2Fnetwork%2Fabc%2Fservice",
180+
GrpcUtil.AuthorityEscaper.encodeAuthority("project/123/network/abc/service"));
181+
}
182+
183+
@Test
184+
public void urlAuthorityEscape_allowedCharsAreNotEncoded() {
185+
assertEquals(
186+
"-._~!$&'()*+,;=@:[]", GrpcUtil.AuthorityEscaper.encodeAuthority("-._~!$&'()*+,;=@:[]"));
187+
}
188+
189+
@Test
190+
public void urlAuthorityEscape_allLettersAndNumbers() {
191+
assertEquals(
192+
"abcdefghijklmnopqrstuvwxyz0123456789",
193+
GrpcUtil.AuthorityEscaper.encodeAuthority("abcdefghijklmnopqrstuvwxyz0123456789"));
194+
}
195+
196+
@Test
197+
public void urlAuthorityEscape_unicodeAreNotEncoded() {
198+
assertEquals(
199+
"ö®", GrpcUtil.AuthorityEscaper.encodeAuthority("ö®"));
200+
}
201+
166202
@Test
167203
public void checkAuthority_failsOnNull() {
168204
thrown.expect(NullPointerException.class);
@@ -199,13 +235,6 @@ public void checkAuthority_failsOnInvalidAuthority() {
199235
GrpcUtil.checkAuthority("[ : : 1]");
200236
}
201237

202-
@Test
203-
public void checkAuthority_failsOnInvalidHost() {
204-
thrown.expect(IllegalArgumentException.class);
205-
thrown.expectMessage("No host in authority");
206-
207-
GrpcUtil.checkAuthority("bad_host");
208-
}
209238

210239
@Test
211240
public void checkAuthority_userInfoNotAllowed() {

core/src/test/java/io/grpc/internal/ManagedChannelImplBuilderTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,7 @@ public void overrideAuthority_null() {
368368

369369
@Test(expected = IllegalArgumentException.class)
370370
public void overrideAuthority_invalid() {
371-
builder.overrideAuthority("not_allowed");
371+
builder.overrideAuthority("user@not_allowed");
372372
}
373373

374374
@Test

xds/src/main/java/io/grpc/xds/XdsNameResolver.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,11 @@ final class XdsNameResolver extends NameResolver {
147147
XdsClientPoolFactory xdsClientPoolFactory, ThreadSafeRandom random,
148148
FilterRegistry filterRegistry, @Nullable Map<String, ?> bootstrapOverride) {
149149
this.targetAuthority = targetAuthority;
150-
serviceAuthority = GrpcUtil.checkAuthority(checkNotNull(name, "name"));
150+
151+
// The name might have multiple slashes so encode it before verifying.
152+
String authority = GrpcUtil.AuthorityEscaper.encodeAuthority(checkNotNull(name, "name"));
153+
serviceAuthority = GrpcUtil.checkAuthority(authority);
154+
151155
this.overrideAuthority = overrideAuthority;
152156
this.serviceConfigParser = checkNotNull(serviceConfigParser, "serviceConfigParser");
153157
this.syncContext = checkNotNull(syncContext, "syncContext");

xds/src/test/java/io/grpc/xds/XdsNameResolverProviderTest.java

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -110,14 +110,19 @@ public void validName_noAuthority() {
110110
}
111111

112112
@Test
113-
public void invalidName_hostnameContainsUnderscore() {
114-
URI uri = URI.create("xds:///foo_bar.googleapis.com");
115-
try {
116-
provider.newNameResolver(uri, args);
117-
fail("Expected IllegalArgumentException");
118-
} catch (IllegalArgumentException e) {
119-
// Expected
120-
}
113+
public void validName_urlExtractedAuthorityInvalidWithoutEncoding() {
114+
XdsNameResolver resolver =
115+
provider.newNameResolver(URI.create("xds:///1234/path/foo.googleapis.com:8080"), args);
116+
assertThat(resolver).isNotNull();
117+
assertThat(resolver.getServiceAuthority()).isEqualTo("1234%2Fpath%2Ffoo.googleapis.com:8080");
118+
}
119+
120+
@Test
121+
public void validName_urlwithTargetAuthorityAndExtractedAuthorityInvalidWithoutEncoding() {
122+
XdsNameResolver resolver = provider.newNameResolver(URI.create(
123+
"xds://trafficdirector.google.com/1234/path/foo.googleapis.com:8080"), args);
124+
assertThat(resolver).isNotNull();
125+
assertThat(resolver.getServiceAuthority()).isEqualTo("1234%2Fpath%2Ffoo.googleapis.com:8080");
121126
}
122127

123128
@Test

0 commit comments

Comments
 (0)