From a66a89308af728f68f712e71df5a42bd8071687c Mon Sep 17 00:00:00 2001 From: jebbs Date: Mon, 17 Oct 2022 12:26:43 +0800 Subject: [PATCH] shadowsocks SIP002 --- common/link/{ss.go => shadowsocks.go} | 83 ++++++++++++++++----------- common/link/shadowsocks_test.go | 75 ++++++++++++++++++++++++ 2 files changed, 125 insertions(+), 33 deletions(-) rename common/link/{ss.go => shadowsocks.go} (53%) create mode 100644 common/link/shadowsocks_test.go diff --git a/common/link/ss.go b/common/link/shadowsocks.go similarity index 53% rename from common/link/ss.go rename to common/link/shadowsocks.go index 20abf517..1f5d7a05 100644 --- a/common/link/ss.go +++ b/common/link/shadowsocks.go @@ -1,7 +1,6 @@ package link import ( - "fmt" "net/url" "strconv" "strings" @@ -11,21 +10,52 @@ import ( E "github.com/sagernet/sing/common/exceptions" ) -var _ Link = (*SSLink)(nil) +var _ Link = (*ShadowSocks)(nil) func init() { common.Must(RegisterParser(&Parser{ Name: "Shadowsocks", Scheme: []string{"ss"}, Parse: func(u *url.URL) (Link, error) { - link := &SSLink{} + link := &ShadowSocks{} return link, link.Parse(u) }, })) } +// ShadowSocks represents a parsed shadowsocks link +type ShadowSocks struct { + Method string `json:"method,omitempty"` + Password string `json:"password,omitempty"` + Address string `json:"address,omitempty"` + Port uint16 `json:"port,omitempty"` + Ps string `json:"ps,omitempty"` + Plugin string `json:"plugin,omitempty"` + PluginOpts string `json:"plugin-opts,omitempty"` +} + +// Options implements Link +func (l *ShadowSocks) Options() *option.Outbound { + return &option.Outbound{ + Type: "shadowsocks", + Tag: l.Ps, + ShadowsocksOptions: option.ShadowsocksOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: l.Address, + ServerPort: l.Port, + }, + Method: l.Method, + Password: l.Password, + Plugin: l.Plugin, + PluginOptions: l.PluginOpts, + }, + } +} + // Parse implements Link -func (l *SSLink) Parse(u *url.URL) error { +// +// https://github.com/shadowsocks/shadowsocks-org/wiki/SIP002-URI-Scheme +func (l *ShadowSocks) Parse(u *url.URL) error { if u.Scheme != "ss" { return E.New("not a ss link") } @@ -39,14 +69,26 @@ func (l *SSLink) Parse(u *url.URL) error { queries := u.Query() for key, values := range queries { switch key { - default: - return fmt.Errorf("unsupported shadowsocks parameter: %s=%v", key, values) + case "plugin": + parts := strings.SplitN(values[0], ";", 2) + l.Plugin = parts[0] + if len(parts) == 2 { + l.PluginOpts = parts[1] + } } } if uname := u.User.Username(); uname != "" { if pass, ok := u.User.Password(); ok { - l.Method = uname - l.Password = pass + method, err := url.QueryUnescape(uname) + if err != nil { + return err + } + password, err := url.QueryUnescape(pass) + if err != nil { + return err + } + l.Method = method + l.Password = password } else { dec, err := base64Decode(uname) if err != nil { @@ -61,28 +103,3 @@ func (l *SSLink) Parse(u *url.URL) error { } return nil } - -// SSLink represents a parsed shadowsocks link -type SSLink struct { - Method string `json:"method,omitempty"` - Password string `json:"password,omitempty"` - Address string `json:"address,omitempty"` - Port uint16 `json:"port,omitempty"` - Ps string `json:"ps,omitempty"` -} - -// Options implements Link -func (l *SSLink) Options() *option.Outbound { - return &option.Outbound{ - Type: "shadowsocks", - Tag: l.Ps, - ShadowsocksOptions: option.ShadowsocksOutboundOptions{ - ServerOptions: option.ServerOptions{ - Server: l.Address, - ServerPort: l.Port, - }, - Method: l.Method, - Password: l.Password, - }, - } -} diff --git a/common/link/shadowsocks_test.go b/common/link/shadowsocks_test.go new file mode 100644 index 00000000..0054fbd0 --- /dev/null +++ b/common/link/shadowsocks_test.go @@ -0,0 +1,75 @@ +package link_test + +import ( + "net/url" + "testing" + + "github.com/sagernet/sing-box/common/link" +) + +func TestShadowSocks(t *testing.T) { + tests := []struct { + link string + want link.ShadowSocks + }{ + { + link: "ss://YWVzLTEyOC1nY206dGVzdA@192.168.100.1:8888#Example1", + want: link.ShadowSocks{ + Address: "192.168.100.1", + Port: 8888, + Ps: "Example1", + Method: "aes-128-gcm", + Password: "test", + }, + }, + { + link: "ss://cmM0LW1kNTpwYXNzd2Q@192.168.100.1:8888/?plugin=obfs-local%3Bobfs%3Dhttp%3Bobfs-host=abc.com#Example2", + want: link.ShadowSocks{ + Address: "192.168.100.1", + Port: 8888, + Ps: "Example2", + Method: "rc4-md5", + Password: "passwd", + Plugin: "obfs-local", + PluginOpts: "obfs=http;obfs-host=abc.com", + }, + }, + { + link: "ss://2022-blake3-aes-256-gcm:YctPZ6U7xPPcU%2Bgp3u%2B0tx%2FtRizJN9K8y%2BuKlW2qjlI%3D@192.168.100.1:8888#Example3", + want: link.ShadowSocks{ + Address: "192.168.100.1", + Port: 8888, + Ps: "Example3", + Method: "2022-blake3-aes-256-gcm", + Password: "YctPZ6U7xPPcU gp3u 0tx/tRizJN9K8y uKlW2qjlI=", + }, + }, + { + link: "ss://2022-blake3-aes-256-gcm:YctPZ6U7xPPcU%2Bgp3u%2B0tx%2FtRizJN9K8y%2BuKlW2qjlI%3D@192.168.100.1:8888/?plugin=v2ray-plugin%3Bserver&unsupported-arguments=should-be-ignored#Example3", + want: link.ShadowSocks{ + Address: "192.168.100.1", + Port: 8888, + Ps: "Example3", + Method: "2022-blake3-aes-256-gcm", + Password: "YctPZ6U7xPPcU gp3u 0tx/tRizJN9K8y uKlW2qjlI=", + Plugin: "v2ray-plugin", + PluginOpts: "server", + }, + }, + } + for _, tt := range tests { + u, err := url.Parse(tt.link) + if err != nil { + t.Fatal(err) + } + link := link.ShadowSocks{} + err = link.Parse(u) + if err != nil { + t.Error(err) + return + } + if link != tt.want { + t.Errorf("want %v, got %v", tt.want, link) + } + } +}