lib/proxy: send the decoded user and password in Proxy-Authorization

HTTPConnectDial built the Basic credential from url.Userinfo.String(),
which percent-encodes the user name and password. A proxy URL such as
http://me%40example.com:p%40ss@proxy:3128 sent "me%40example.com:p%40ss"
instead of "me@example.com:p@ss" and the proxy refused it.

Use Username() and Password() like net/http does, which also always
puts the ":" separator in.
This commit is contained in:
kevin authored and Nick Craig-Wood committed 2026-11-05 12:20:41 +00:00
1 parent d1c54c4e5d
commit 5c143c89bf
2 files changed
+55 -1

No files matched your search

+4 -1
View File
@@ -79,7 +79,10 @@ func HTTPConnectDial(network, addr string, proxyURL *url.URL, proxyDialer proxy.
// send CONNECT
user := proxyURL.User
if user != nil {
credential := base64.StdEncoding.EncodeToString([]byte(user.String()))
// Use the decoded user name and password - user.String() would
// send them percent-encoded (eg "me%40example.com").
password, _ := user.Password()
credential := base64.StdEncoding.EncodeToString([]byte(user.Username() + ":" + password))
_, err = fmt.Fprintf(conn, "CONNECT %s HTTP/1.1\r\nHost: %s\r\nProxy-Authorization: Basic %s\r\n\r\n", addr, addr, credential)
} else {
_, err = fmt.Fprintf(conn, "CONNECT %s HTTP/1.1\r\nHost: %s\r\n\r\n", addr, addr)
+51
View File
@@ -2,6 +2,7 @@ package proxy
import (
"bufio"
"encoding/base64"
"net"
"net/url"
"strings"
@@ -118,3 +119,53 @@ func TestHTTPConnectDialNon200(t *testing.T) {
assert.Nil(t, conn)
assert.Contains(t, err.Error(), "403")
}
// Check that the Proxy-Authorization header carries the decoded user
// name and password, not their percent-encoded form.
func TestHTTPConnectDialAuth(t *testing.T) {
for _, test := range []struct {
userInfo string
want string
}{
{userInfo: "user:pass", want: "user:pass"},
{userInfo: "me%40example.com:p%40ss%3Aword", want: "me@example.com:p@ss:word"},
{userInfo: "user", want: "user:"},
} {
t.Run(test.userInfo, func(t *testing.T) {
listener, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
defer func() {
_ = listener.Close()
}()
authHeader := make(chan string, 1)
go func() {
conn, err := listener.Accept()
if err != nil {
return
}
defer func() {
_ = conn.Close()
}()
br := bufio.NewReader(conn)
auth := ""
for {
line, err := br.ReadString('\n')
if err != nil || line == "\r\n" || line == "\n" {
break
}
if value, ok := strings.CutPrefix(line, "Proxy-Authorization: "); ok {
auth = strings.TrimSpace(value)
}
}
authHeader <- auth
_, _ = conn.Write([]byte("HTTP/1.1 200 Connection established\r\n\r\n"))
}()
proxyURL, err := url.Parse("http://" + test.userInfo + "@" + listener.Addr().String())
require.NoError(t, err)
conn, err := HTTPConnectDial("tcp", "example.com:1234", proxyURL, nil)
require.NoError(t, err)
_ = conn.Close()
assert.Equal(t, "Basic "+base64.StdEncoding.EncodeToString([]byte(test.want)), <-authHeader)
})
}
}