From 7e34bb946f6870658c3882afc49b869c3d09794b Mon Sep 17 00:00:00 2001 From: luxiang Date: Tue, 17 Mar 2026 08:40:08 +0800 Subject: [PATCH] fix(proxy): encode special chars in proxy credentials --- backend/internal/service/proxy.go | 14 ++-- backend/internal/service/proxy_test.go | 95 ++++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 4 deletions(-) create mode 100644 backend/internal/service/proxy_test.go diff --git a/backend/internal/service/proxy.go b/backend/internal/service/proxy.go index fc449091..a2896d6c 100644 --- a/backend/internal/service/proxy.go +++ b/backend/internal/service/proxy.go @@ -1,7 +1,9 @@ package service import ( - "fmt" + "net" + "net/url" + "strconv" "time" ) @@ -23,10 +25,14 @@ func (p *Proxy) IsActive() bool { } func (p *Proxy) URL() string { - if p.Username != "" && p.Password != "" { - return fmt.Sprintf("%s://%s:%s@%s:%d", p.Protocol, p.Username, p.Password, p.Host, p.Port) + u := &url.URL{ + Scheme: p.Protocol, + Host: net.JoinHostPort(p.Host, strconv.Itoa(p.Port)), } - return fmt.Sprintf("%s://%s:%d", p.Protocol, p.Host, p.Port) + if p.Username != "" && p.Password != "" { + u.User = url.UserPassword(p.Username, p.Password) + } + return u.String() } type ProxyWithAccountCount struct { diff --git a/backend/internal/service/proxy_test.go b/backend/internal/service/proxy_test.go new file mode 100644 index 00000000..da6d1236 --- /dev/null +++ b/backend/internal/service/proxy_test.go @@ -0,0 +1,95 @@ +package service + +import ( + "net/url" + "testing" +) + +func TestProxyURL(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + proxy Proxy + want string + }{ + { + name: "without auth", + proxy: Proxy{ + Protocol: "http", + Host: "proxy.example.com", + Port: 8080, + }, + want: "http://proxy.example.com:8080", + }, + { + name: "with auth", + proxy: Proxy{ + Protocol: "socks5", + Host: "socks.example.com", + Port: 1080, + Username: "user", + Password: "pass", + }, + want: "socks5://user:pass@socks.example.com:1080", + }, + { + name: "username only keeps no auth for compatibility", + proxy: Proxy{ + Protocol: "http", + Host: "proxy.example.com", + Port: 8080, + Username: "user-only", + }, + want: "http://proxy.example.com:8080", + }, + { + name: "with special characters in credentials", + proxy: Proxy{ + Protocol: "http", + Host: "proxy.example.com", + Port: 3128, + Username: "first last@corp", + Password: "p@ ss:#word", + }, + want: "http://first%20last%40corp:p%40%20ss%3A%23word@proxy.example.com:3128", + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + if got := tc.proxy.URL(); got != tc.want { + t.Fatalf("Proxy.URL() mismatch: got=%q want=%q", got, tc.want) + } + }) + } +} + +func TestProxyURL_SpecialCharactersRoundTrip(t *testing.T) { + t.Parallel() + + proxy := Proxy{ + Protocol: "http", + Host: "proxy.example.com", + Port: 3128, + Username: "first last@corp", + Password: "p@ ss:#word", + } + + parsed, err := url.Parse(proxy.URL()) + if err != nil { + t.Fatalf("parse proxy URL failed: %v", err) + } + if got := parsed.User.Username(); got != proxy.Username { + t.Fatalf("username mismatch after parse: got=%q want=%q", got, proxy.Username) + } + pass, ok := parsed.User.Password() + if !ok { + t.Fatal("password missing after parse") + } + if pass != proxy.Password { + t.Fatalf("password mismatch after parse: got=%q want=%q", pass, proxy.Password) + } +}