Skip to content

Commit 7e34bb9

Browse files
committed
fix(proxy): encode special chars in proxy credentials
1 parent f42c8f2 commit 7e34bb9

2 files changed

Lines changed: 104 additions & 3 deletions

File tree

backend/internal/service/proxy.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package service
22

33
import (
4-
"fmt"
4+
"net"
5+
"net/url"
6+
"strconv"
57
"time"
68
)
79

@@ -23,10 +25,14 @@ func (p *Proxy) IsActive() bool {
2325
}
2426

2527
func (p *Proxy) URL() string {
28+
u := &url.URL{
29+
Scheme: p.Protocol,
30+
Host: net.JoinHostPort(p.Host, strconv.Itoa(p.Port)),
31+
}
2632
if p.Username != "" && p.Password != "" {
27-
return fmt.Sprintf("%s://%s:%s@%s:%d", p.Protocol, p.Username, p.Password, p.Host, p.Port)
33+
u.User = url.UserPassword(p.Username, p.Password)
2834
}
29-
return fmt.Sprintf("%s://%s:%d", p.Protocol, p.Host, p.Port)
35+
return u.String()
3036
}
3137

3238
type ProxyWithAccountCount struct {
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package service
2+
3+
import (
4+
"net/url"
5+
"testing"
6+
)
7+
8+
func TestProxyURL(t *testing.T) {
9+
t.Parallel()
10+
11+
tests := []struct {
12+
name string
13+
proxy Proxy
14+
want string
15+
}{
16+
{
17+
name: "without auth",
18+
proxy: Proxy{
19+
Protocol: "http",
20+
Host: "proxy.example.com",
21+
Port: 8080,
22+
},
23+
want: "http://proxy.example.com:8080",
24+
},
25+
{
26+
name: "with auth",
27+
proxy: Proxy{
28+
Protocol: "socks5",
29+
Host: "socks.example.com",
30+
Port: 1080,
31+
Username: "user",
32+
Password: "pass",
33+
},
34+
want: "socks5://user:pass@socks.example.com:1080",
35+
},
36+
{
37+
name: "username only keeps no auth for compatibility",
38+
proxy: Proxy{
39+
Protocol: "http",
40+
Host: "proxy.example.com",
41+
Port: 8080,
42+
Username: "user-only",
43+
},
44+
want: "http://proxy.example.com:8080",
45+
},
46+
{
47+
name: "with special characters in credentials",
48+
proxy: Proxy{
49+
Protocol: "http",
50+
Host: "proxy.example.com",
51+
Port: 3128,
52+
Username: "first last@corp",
53+
Password: "p@ ss:#word",
54+
},
55+
want: "http://first%20last%40corp:p%40%20ss%3A%23word@proxy.example.com:3128",
56+
},
57+
}
58+
59+
for _, tc := range tests {
60+
tc := tc
61+
t.Run(tc.name, func(t *testing.T) {
62+
t.Parallel()
63+
if got := tc.proxy.URL(); got != tc.want {
64+
t.Fatalf("Proxy.URL() mismatch: got=%q want=%q", got, tc.want)
65+
}
66+
})
67+
}
68+
}
69+
70+
func TestProxyURL_SpecialCharactersRoundTrip(t *testing.T) {
71+
t.Parallel()
72+
73+
proxy := Proxy{
74+
Protocol: "http",
75+
Host: "proxy.example.com",
76+
Port: 3128,
77+
Username: "first last@corp",
78+
Password: "p@ ss:#word",
79+
}
80+
81+
parsed, err := url.Parse(proxy.URL())
82+
if err != nil {
83+
t.Fatalf("parse proxy URL failed: %v", err)
84+
}
85+
if got := parsed.User.Username(); got != proxy.Username {
86+
t.Fatalf("username mismatch after parse: got=%q want=%q", got, proxy.Username)
87+
}
88+
pass, ok := parsed.User.Password()
89+
if !ok {
90+
t.Fatal("password missing after parse")
91+
}
92+
if pass != proxy.Password {
93+
t.Fatalf("password mismatch after parse: got=%q want=%q", pass, proxy.Password)
94+
}
95+
}

0 commit comments

Comments
 (0)