add link package

This commit is contained in:
jebbs 2022-10-14 18:05:30 +08:00
parent c1eb9e807a
commit fd9184bbde
7 changed files with 572 additions and 0 deletions

34
common/link/link.go Normal file
View File

@ -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...)
}

52
common/link/ng.go Normal file
View File

@ -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
}

53
common/link/parsers.go Normal file
View File

@ -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
}

116
common/link/ss.go Normal file
View File

@ -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
}

70
common/link/vmess.go Normal file
View File

@ -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
}

View File

@ -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
}

125
common/link/vmess_rocket.go Normal file
View File

@ -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]
}