Skip to content

Commit

Permalink
client: encode the authority by default (#6428)
Browse files Browse the repository at this point in the history
  • Loading branch information
anicr7 authored Jul 10, 2023
1 parent 11feb0a commit fc0aa46
Show file tree
Hide file tree
Showing 3 changed files with 98 additions and 2 deletions.
61 changes: 60 additions & 1 deletion clientconn.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
37 changes: 37 additions & 0 deletions clientconn_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
})
}
}
2 changes: 1 addition & 1 deletion test/authority_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
{
Expand Down

0 comments on commit fc0aa46

Please sign in to comment.