From 61cd0b119bae02851f2165549a15e5d8bd4dce55 Mon Sep 17 00:00:00 2001 From: John Judd Date: Tue, 18 Jun 2024 09:58:14 -0500 Subject: [PATCH] Add uri authority encode utils 191 (#192) * Add a util functions to encode authority strictly following RFC 3986. Fixes #191 * update signature vars to use "raw" prefix --- .gvmrc | 2 +- CHANGELOG.md | 6 +++- utils/authority.go | 64 ++++++++++++++++++++++++++++++++++ utils/authority_test.go | 76 ++++++++++++++++++++++++++++++++++++++++- 4 files changed, 145 insertions(+), 3 deletions(-) diff --git a/.gvmrc b/.gvmrc index 3c9a034b..5e7f5b7a 100644 --- a/.gvmrc +++ b/.gvmrc @@ -1 +1 @@ -go1.22.0 +go1.22.4 diff --git a/CHANGELOG.md b/CHANGELOG.md index 02d1abdc..10f3aab2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] -## [6.14.3] - 2024-05-30 +## [6.15.0] - 2024-06-18 +### Added +- Fixed #191 - Add a util functions to encode authority strictly following RFC 3986. + +## [6.14.3] - 2024-06-11 ### Fixed - Fixed #189 - Update utils authority package to handle proper encoding/decoding of uri with reserved characters. diff --git a/utils/authority.go b/utils/authority.go index 1e6c712b..18eda876 100644 --- a/utils/authority.go +++ b/utils/authority.go @@ -159,3 +159,67 @@ func validOptionalPort(port string) bool { } return true } + +// EncodeUserInfo takes an unencoded URI authority userinfo string and encodes it +func EncodeUserInfo(rawUserInfo string) string { + parts := strings.SplitN(rawUserInfo, ":", 2) + encodedParts := make([]string, len(parts)) + for i, part := range parts { + encoded := url.QueryEscape(part) + decoded := strings.NewReplacer( + "%21", "!", "%24", "$", "%26", "&", "%27", "'", + "%28", "(", "%29", ")", "%2A", "*", "%2B", "+", + "%2C", ",", "%3B", ";", "%3D", "=", + ).Replace(encoded) + encodedParts[i] = decoded + } + return strings.Join(encodedParts, ":") +} + +// EncodeAuthority takes an unencoded URI authority string and encodes it +func EncodeAuthority(rawAuthority string) (string, error) { + var userInfo, hostPort string + + // Split the authority into user info and hostPort + atIndex := strings.LastIndex(rawAuthority, "@") + if atIndex != -1 { + userInfo = rawAuthority[:atIndex] + hostPort = rawAuthority[atIndex+1:] + } else { + hostPort = rawAuthority + } + + // Encode userInfo if present + if userInfo != "" { + userInfo = EncodeUserInfo(userInfo) + } + + // Split host and port + var host, port string + hostPortSplit := strings.SplitN(hostPort, ":", 2) + if len(hostPortSplit) > 0 { + host = hostPortSplit[0] + } + if len(hostPortSplit) > 1 { + port = hostPortSplit[1] + } + + // Encode host and port + encodedHost := url.QueryEscape(host) + var encodedPort string + if port != "" { + encodedPort = url.QueryEscape(port) + } + + // Reconstruct the encoded authority string + var encodedAuthority string + if userInfo != "" { + encodedAuthority = userInfo + "@" + } + encodedAuthority += encodedHost + if encodedPort != "" { + encodedAuthority += ":" + encodedPort + } + + return encodedAuthority, nil +} diff --git a/utils/authority_test.go b/utils/authority_test.go index d72a7255..f55e2435 100644 --- a/utils/authority_test.go +++ b/utils/authority_test.go @@ -24,7 +24,7 @@ type authorityTest struct { } func (a *authoritySuite) TestAuthority() { - tests := []*authorityTest{ + tests := []authorityTest{ { authorityString: "", host: "", @@ -308,6 +308,80 @@ func (a *authoritySuite) TestAuthority() { } } +type encodeAuthorityTest struct { + rawAuthority string + expectedEncoded string + hasError bool + errMessage string + message string +} + +func (a *authoritySuite) TestEncodeAuthority() { + tests := []encodeAuthorityTest{ + { + rawAuthority: "domain.com\\user@someserver.com:22", + expectedEncoded: "domain.com%5Cuser@someserver.com:22", + hasError: false, + errMessage: "", + message: "basic encoding", + }, + { + rawAuthority: "example.com:80", + expectedEncoded: "example.com:80", + hasError: false, + errMessage: "", + message: "no user info", + }, + { + rawAuthority: "user:password@host.com", + expectedEncoded: "user:password@host.com", + hasError: false, + errMessage: "", + message: "username and password", + }, + { + rawAuthority: "!username@host.com:22", + expectedEncoded: "!username@host.com:22", + hasError: false, + errMessage: "", + message: "exclamation point in username (remains unencoded)", + }, + { + rawAuthority: "user@host.com", + expectedEncoded: "user@host.com", + hasError: false, + errMessage: "", + message: "username only", + }, + { + rawAuthority: "host.com:8080", + expectedEncoded: "host.com:8080", + hasError: false, + errMessage: "", + message: "host and port only", + }, + { + rawAuthority: "@host.com", + expectedEncoded: "host.com", + hasError: false, + errMessage: "", + message: "empty user info", + }, + } + + for _, t := range tests { + a.Run(t.message, func() { + actual, err := EncodeAuthority(t.rawAuthority) + if t.hasError { + a.ErrorContains(err, t.errMessage, t.message) + } else { + a.NoError(err, t.message) + a.Equal(t.expectedEncoded, actual, t.message) + } + }) + } +} + func TestAuthority(t *testing.T) { suite.Run(t, new(authoritySuite)) }