From fd9184bbdec44e6e94f6a519826c0fe805ee7313 Mon Sep 17 00:00:00 2001 From: jebbs Date: Fri, 14 Oct 2022 18:05:30 +0800 Subject: [PATCH] add link package --- common/link/link.go | 34 +++++++++ common/link/ng.go | 52 +++++++++++++ common/link/parsers.go | 53 ++++++++++++++ common/link/ss.go | 116 +++++++++++++++++++++++++++++ common/link/vmess.go | 70 ++++++++++++++++++ common/link/vmess_quantumult.go | 122 +++++++++++++++++++++++++++++++ common/link/vmess_rocket.go | 125 ++++++++++++++++++++++++++++++++ 7 files changed, 572 insertions(+) create mode 100644 common/link/link.go create mode 100644 common/link/ng.go create mode 100644 common/link/parsers.go create mode 100644 common/link/ss.go create mode 100644 common/link/vmess.go create mode 100644 common/link/vmess_quantumult.go create mode 100644 common/link/vmess_rocket.go diff --git a/common/link/link.go b/common/link/link.go new file mode 100644 index 00000000..0648db37 --- /dev/null +++ b/common/link/link.go @@ -0,0 +1,34 @@ +package link + +import ( + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" +) + +// Link is the interface for v2ray links +type Link interface { + // Detail returns human readable string + Options() *option.Outbound + // String unmarshals Link to string + String() string +} + +// Parse parses a link string to Link +func Parse(arg string) (Link, error) { + ps, err := getParsers(arg) + if err != nil { + return nil, err + } + errs := make([]error, 0, len(ps)) + for _, p := range ps { + lk, err := p.Parse(arg) + if err == nil { + return lk, nil + } + errs = append(errs, err) + } + if len(errs) == 1 { + return nil, errs[0] + } + return nil, E.Errors(errs...) +} diff --git a/common/link/ng.go b/common/link/ng.go new file mode 100644 index 00000000..f98e32b3 --- /dev/null +++ b/common/link/ng.go @@ -0,0 +1,52 @@ +package link + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "strings" + + "github.com/sagernet/sing/common" +) + +func init() { + common.Must(RegisterParser(&Parser{ + Name: "V2RayNG", + Scheme: []string{"vmess"}, + Parse: func(input string) (Link, error) { + return ParseVMessV2RayNG(input) + }, + })) +} + +// VMessV2RayNG is the vmess link of V2RayNG +type VMessV2RayNG struct { + vmess +} + +// String implements Link +func (v VMessV2RayNG) String() string { + b, _ := json.Marshal(v) + return "vmess://" + base64.StdEncoding.EncodeToString(b) +} + +// ParseVMessV2RayNG parses V2RayN vemss link +func ParseVMessV2RayNG(vmess string) (*VMessV2RayNG, error) { + if !strings.HasPrefix(vmess, "vmess://") { + return nil, fmt.Errorf("vmess unreconized: %s", vmess) + } + + b64 := vmess[8:] + b, err := base64Decode(b64) + if err != nil { + return nil, err + } + + v := &VMessV2RayNG{} + if err := json.Unmarshal(b, v); err != nil { + return nil, err + } + v.OrigLink = vmess + + return v, nil +} diff --git a/common/link/parsers.go b/common/link/parsers.go new file mode 100644 index 00000000..a75ed6a0 --- /dev/null +++ b/common/link/parsers.go @@ -0,0 +1,53 @@ +package link + +import ( + "fmt" + "net/url" + "strings" + + E "github.com/sagernet/sing/common/exceptions" +) + +// ParseFunc is parser function to load links, like "vmess://..." +type ParseFunc func(input string) (Link, error) + +// Parser is parser load v2ray links with specified schemes +type Parser struct { + Name string + Scheme []string + Parse ParseFunc +} + +var ( + parsers = make(map[string][]*Parser) +) + +// RegisterParser add a new link parser. +func RegisterParser(parser *Parser) error { + for _, scheme := range parser.Scheme { + s := strings.ToLower(scheme) + ps := parsers[s] + if len(ps) == 0 { + ps = make([]*Parser, 0) + } + parsers[s] = append(ps, parser) + } + + return nil +} + +func getParsers(link string) ([]*Parser, error) { + u, err := url.Parse(link) + if err != nil { + return nil, err + } + if u.Scheme == "" { + return nil, E.New("invalid link") + } + s := strings.ToLower(u.Scheme) + ps := parsers[s] + if len(ps) == 0 { + return nil, fmt.Errorf("unsupported link scheme: %s", u.Scheme) + } + return ps, nil +} diff --git a/common/link/ss.go b/common/link/ss.go new file mode 100644 index 00000000..c5d26112 --- /dev/null +++ b/common/link/ss.go @@ -0,0 +1,116 @@ +package link + +import ( + "encoding/base64" + "fmt" + "net/url" + "strconv" + "strings" + + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" +) + +var _ Link = (*SSLink)(nil) + +func init() { + common.Must(RegisterParser(&Parser{ + Name: "Shadowsocks", + Scheme: []string{"ss"}, + Parse: func(input string) (Link, error) { + return ParseShadowSocks(input) + }, + })) +} + +// ParseShadowSocks parses official vemss link to Link +func ParseShadowSocks(ss string) (*SSLink, error) { + url, err := url.Parse(ss) + if err != nil { + return nil, err + } + if url.Scheme != "ss" { + return nil, E.New("not a ss:// link") + } + port, err := strconv.ParseUint(url.Port(), 10, 16) + if err != nil { + return nil, E.Cause(err, "invalid port") + } + link := &SSLink{ + OrigLink: ss, + Address: url.Hostname(), + Port: uint16(port), + Ps: url.Fragment, + } + queries := url.Query() + for key, values := range queries { + switch key { + default: + return nil, fmt.Errorf("unsupported shadowsocks parameter: %s=%v", key, values) + } + } + if uname := url.User.Username(); uname != "" { + if pass, ok := url.User.Password(); ok { + link.Method = uname + link.Password = pass + } else { + dec, err := base64Decode(uname) + if err != nil { + return nil, err + } + parts := strings.Split(string(dec), ":") + link.Method = parts[0] + if len(parts) > 1 { + link.Password = parts[1] + } + } + } + return link, 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"` + + OrigLink string `json:"-,omitempty"` +} + +// String implements Link +func (v SSLink) String() string { + return v.OrigLink +} + +// Options implements Link +func (v *SSLink) Options() *option.Outbound { + return &option.Outbound{ + Type: "shadowsocks", + Tag: v.Ps, + ShadowsocksOptions: option.ShadowsocksOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: v.Address, + ServerPort: v.Port, + }, + Method: v.Method, + Password: v.Password, + }, + } +} + +func base64Decode(b64 string) ([]byte, error) { + b64 = strings.TrimSpace(b64) + stdb64 := b64 + if pad := len(b64) % 4; pad != 0 { + stdb64 += strings.Repeat("=", 4-pad) + } + + b, err := base64.StdEncoding.DecodeString(stdb64) + if err != nil { + return base64.URLEncoding.DecodeString(b64) + } + return b, nil +} diff --git a/common/link/vmess.go b/common/link/vmess.go new file mode 100644 index 00000000..c8f83424 --- /dev/null +++ b/common/link/vmess.go @@ -0,0 +1,70 @@ +package link + +import ( + "strings" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" +) + +type vmess struct { + Ver string `json:"v,omitempty"` + Add string `json:"add,omitempty"` + Aid int `json:"aid,omitempty"` + Host string `json:"host,omitempty"` + ID string `json:"id,omitempty"` + Net string `json:"net,omitempty"` + Path string `json:"path,omitempty"` + Port uint16 `json:"port,omitempty"` + Ps string `json:"ps,omitempty"` + TLS string `json:"tls,omitempty"` + Type string `json:"type,omitempty"` + OrigLink string `json:"-,omitempty"` +} + +// Options implements Link +func (v *vmess) Options() *option.Outbound { + out := &option.Outbound{ + Type: "vmess", + Tag: v.Ps, + VMessOptions: option.VMessOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: v.Add, + ServerPort: v.Port, + }, + UUID: v.ID, + AlterId: v.Aid, + Security: "auto", + }, + } + + opt := &option.V2RayTransportOptions{} + + switch v.Net { + case "": + opt = nil + case "tcp", "h2", "http": + opt.Type = C.V2RayTransportTypeHTTP + opt.HTTPOptions.Path = strings.Split(v.Path, ",")[0] + if v.Host != "" { + opt.HTTPOptions.Host = strings.Split(v.Host, ",") + opt.HTTPOptions.Headers["Host"] = opt.HTTPOptions.Host[0] + } + case "ws": + opt.Type = C.V2RayTransportTypeWebsocket + opt.WebsocketOptions.Path = v.Path + opt.WebsocketOptions.Headers = map[string]string{ + "Host": v.Host, + } + } + + if v.TLS == "tls" { + out.VMessOptions.TLS = &option.OutboundTLSOptions{ + Insecure: true, + ServerName: v.Host, + } + } + + out.VMessOptions.Transport = opt + return out +} diff --git a/common/link/vmess_quantumult.go b/common/link/vmess_quantumult.go new file mode 100644 index 00000000..5ec084c0 --- /dev/null +++ b/common/link/vmess_quantumult.go @@ -0,0 +1,122 @@ +package link + +import ( + "encoding/base64" + "fmt" + "strconv" + "strings" + + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" +) + +func init() { + common.Must(RegisterParser(&Parser{ + Name: "Quantumult", + Scheme: []string{"vmess"}, + Parse: func(input string) (Link, error) { + return ParseVMessQuantumult(input) + }, + })) +} + +// VMessQuantumult is the vmess link of Quantumult +type VMessQuantumult struct { + vmess +} + +// String implements Link +func (v VMessQuantumult) String() string { + /* + let obfs = `,obfs=${jsonConf.net === 'ws' ? 'ws' : 'http'},obfs-path="${jsonConf.path || '/'}",obfs-header="Host:${jsonConf.host || jsonConf.add}[Rr][Nn]User-Agent:${ua}"` + let quanVmess = `${jsonConf.ps} = vmess,${jsonConf.add},${jsonConf.port},${method},"${jsonConf.id}",over-tls=${jsonConf.tls === 'tls' ? 'true' : 'false'},certificate=1${jsonConf.type === 'none' && jsonConf.net !== 'ws' ? '' : obfs},group=${group}` + */ + + method := "aes-128-gcm" + vbase := fmt.Sprintf("%s = vmess,%s,%d,%s,\"%s\",over-tls=%v,certificate=1", v.Ps, v.Add, v.Port, method, v.ID, v.TLS == "tls") + + var obfs string + if (v.Net == "ws" || v.Net == "http") && (v.Type == "none" || v.Type == "") { + if v.Path == "" { + v.Path = "/" + } + if v.Host == "" { + v.Host = v.Add + } + obfs = fmt.Sprintf(`,obfs=ws,obfs-path="%s",obfs-header="Host:%s[Rr][Nn]User-Agent:Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/16A5366a"`, v.Path, v.Host) + } + + vbase += obfs + vbase += ",group=Fndroid" + return "vmess://" + base64.URLEncoding.EncodeToString([]byte(vbase)) +} + +// ParseVMessQuantumult parses Quantumult vemss link +func ParseVMessQuantumult(vmess string) (*VMessQuantumult, error) { + if !strings.HasPrefix(vmess, "vmess://") { + return nil, fmt.Errorf("vmess unreconized: %s", vmess) + } + b64 := vmess[8:] + b, err := base64Decode(b64) + if err != nil { + return nil, err + } + + info := string(b) + v := &VMessQuantumult{} + v.OrigLink = vmess + v.Ver = "2" + + psn := strings.SplitN(info, " = ", 2) + if len(psn) != 2 { + return nil, fmt.Errorf("part error: %s", info) + } + + v.Ps = psn[0] + params := strings.Split(psn[1], ",") + port, err := strconv.ParseUint(params[2], 10, 16) + if err != nil { + return nil, E.Cause(err, "invalid port") + } + v.Add = params[1] + v.Port = uint16(port) + v.ID = strings.Trim(params[4], "\"") + v.Aid = 0 + v.Net = "tcp" + v.Type = "none" + + if len(params) > 4 { + for _, pkv := range params[5:] { + kvp := strings.SplitN(pkv, "=", 2) + switch kvp[0] { + case "over-tls": + if kvp[1] == "true" { + v.TLS = "tls" + } + case "obfs": + switch kvp[1] { + case "ws", "http": + v.Net = kvp[1] + default: + return nil, fmt.Errorf("unsupported quantumult vmess obfs parameter: %s", kvp[1]) + } + case "obfs-path": + v.Path = strings.Trim(kvp[1], "\"") + case "obfs-header": + hd := strings.Trim(kvp[1], "\"") + for _, hl := range strings.Split(hd, "[Rr][Nn]") { + if strings.HasPrefix(hl, "Host:") { + host := hl[5:] + if host != v.Add { + v.Host = host + } + break + } + } + default: + return nil, fmt.Errorf("unsupported quantumult vmess parameter: %s", pkv) + } + } + } + return v, nil +} diff --git a/common/link/vmess_rocket.go b/common/link/vmess_rocket.go new file mode 100644 index 00000000..d9f52a6a --- /dev/null +++ b/common/link/vmess_rocket.go @@ -0,0 +1,125 @@ +package link + +import ( + "encoding/base64" + "fmt" + "net/url" + "strconv" + "strings" + + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" +) + +var _ Link = (*VMessRocket)(nil) + +func init() { + common.Must(RegisterParser(&Parser{ + Name: "VMess ShadowRocket", + Scheme: []string{"vmess"}, + Parse: func(input string) (Link, error) { + return ParseVMessRocket(input) + }, + })) +} + +// VMessRocket is the vmess link of ShadowRocket +type VMessRocket struct { + vmess +} + +// String implements Link +func (v VMessRocket) String() string { + mhp := fmt.Sprintf("%s:%s@%s:%d", v.Type, v.ID, v.Add, v.Port) + qs := url.Values{} + qs.Add("remarks", v.Ps) + if v.Net == "ws" { + qs.Add("obfs", "websocket") + } + if v.Host != "" { + qs.Add("obfsParam", v.Host) + } + if v.Path != "" { + qs.Add("path", v.Host) + } + if v.TLS == "tls" { + qs.Add("tls", "1") + } + + url := url.URL{ + Scheme: "vmess", + Host: base64.URLEncoding.EncodeToString([]byte(mhp)), + RawQuery: qs.Encode(), + } + + return url.String() +} + +// ParseVMessRocket parses ShadowRocket vemss link string to VMessRocket +func ParseVMessRocket(vmess string) (*VMessRocket, error) { + url, err := url.Parse(vmess) + if err != nil { + return nil, err + } + if url.Scheme != "vmess" { + return nil, E.New("not a vmess:// link") + } + link := &VMessRocket{} + link.Ver = "2" + link.OrigLink = vmess + + b64 := url.Host + b, err := base64Decode(b64) + if err != nil { + return nil, err + } + + mhp := strings.SplitN(string(b), ":", 3) + if len(mhp) != 3 { + return nil, fmt.Errorf("vmess unreconized: method:host:port -- %v", mhp) + } + port, err := strconv.ParseUint(mhp[2], 10, 16) + if err != nil { + return nil, E.Cause(err, "invalid port") + } + // mhp[0] is the encryption method + link.Port = uint16(port) + idadd := strings.SplitN(mhp[1], "@", 2) + if len(idadd) != 2 { + return nil, fmt.Errorf("vmess unreconized: id@addr -- %v", idadd) + } + link.ID = idadd[0] + link.Add = idadd[1] + link.Aid = 0 + + for key, values := range url.Query() { + switch key { + case "remarks": + link.Ps = firstValueOf(values) + case "path": + link.Path = firstValueOf(values) + case "tls": + link.TLS = firstValueOf(values) + case "obfs": + v := firstValueOf(values) + switch v { + case "websocket": + link.Net = "ws" + case "none": + link.Net = "" + } + case "obfsParam": + link.Host = firstValueOf(values) + default: + return nil, fmt.Errorf("unsupported shadowrocket vmess parameter: %s=%v", key, values) + } + } + return link, nil +} + +func firstValueOf(values []string) string { + if len(values) == 0 { + return "" + } + return values[0] +}