client: encode the authority by default (#6428)

This commit is contained in:
Anirudh Ramachandra 2023-07-10 14:48:27 -07:00 committed by GitHub
parent 11feb0a9af
commit fc0aa4689c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 98 additions and 2 deletions

View File

@ -1818,6 +1818,61 @@ func parseTarget(target string) (resolver.Target, error) {
return resolver.Target{URL: *u}, nil
}
func encodeAuthority(authority string) string {
const upperhex = "0123456789ABCDEF"
// Return for characters that must be escaped as per
// Valid chars are mentioned here:
// https://datatracker.ietf.org/doc/html/rfc3986#section-3.2
shouldEscape := func(c byte) bool {
// Alphanum are always allowed.
if 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || '0' <= c && c <= '9' {
return false
}
switch c {
case '-', '_', '.', '~': // Unreserved characters
return false
case '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=': // Subdelim characters
return false
case ':', '[', ']', '@': // Authority related delimeters
return false
}
// Everything else must be escaped.
return true
}
hexCount := 0
for i := 0; i < len(authority); i++ {
c := authority[i]
if shouldEscape(c) {
hexCount++
}
}
if hexCount == 0 {
return authority
}
required := len(authority) + 2*hexCount
t := make([]byte, required)
j := 0
// This logic is a barebones version of escape in the go net/url library.
for i := 0; i < len(authority); i++ {
switch c := authority[i]; {
case shouldEscape(c):
t[j] = '%'
t[j+1] = upperhex[c>>4]
t[j+2] = upperhex[c&15]
j += 3
default:
t[j] = authority[i]
j++
}
}
return string(t)
}
// Determine channel authority. The order of precedence is as follows:
// - user specified authority override using `WithAuthority` dial option
// - creds' notion of server name for the authentication handshake
@ -1868,7 +1923,11 @@ func (cc *ClientConn) determineAuthority() error {
// the channel authority given the user's dial target. For resolvers
// which don't implement this interface, we will use the endpoint from
// "scheme://authority/endpoint" as the default authority.
cc.authority = endpoint
// Escape the endpoint to handle use cases where the endpoint
// might not be a valid authority by default.
// For example an endpoint which has multiple paths like
// 'a/b/c', which is not a valid authority by default.
cc.authority = encodeAuthority(endpoint)
}
channelz.Infof(logger, cc.channelzID, "Channel authority set to %q", cc.authority)
return nil

View File

@ -1221,3 +1221,40 @@ func stayConnected(cc *ClientConn) {
}
}
}
func (s) TestURLAuthorityEscape(t *testing.T) {
tests := []struct {
name string
authority string
want string
}{
{
name: "ipv6_authority",
authority: "[::1]",
want: "[::1]",
},
{
name: "with_user_and_host",
authority: "userinfo@host:10001",
want: "userinfo@host:10001",
},
{
name: "with_multiple_slashes",
authority: "projects/123/network/abc/service",
want: "projects%2F123%2Fnetwork%2Fabc%2Fservice",
},
{
name: "all_possible_allowed_chars",
authority: "abc123-._~!$&'()*+,;=@:[]",
want: "abc123-._~!$&'()*+,;=@:[]",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if got, want := encodeAuthority(test.authority), test.want; got != want {
t.Errorf("encodeAuthority(%s) = %s, want %s", test.authority, got, test.want)
}
})
}
}

View File

@ -126,7 +126,7 @@ var authorityTests = []authorityTest{
name: "UnixPassthrough",
address: "/tmp/sock.sock",
target: "passthrough:///unix:///tmp/sock.sock",
authority: "unix:///tmp/sock.sock",
authority: "unix:%2F%2F%2Ftmp%2Fsock.sock",
dialTargetWant: "unix:///tmp/sock.sock",
},
{