diff --git a/api/handler/default.go b/api/handler/default.go index f6686eb..86eb736 100644 --- a/api/handler/default.go +++ b/api/handler/default.go @@ -76,7 +76,7 @@ func BuildSub(clashType model.ClashType, query validator.SubValidator, template err = yaml.Unmarshal(data, &sub) var newProxies []model.Proxy if err != nil { - reg, _ := regexp.Compile("(ssr|ss|vmess|trojan|vless|hysteria)://") + reg, _ := regexp.Compile("(ssr|ss|vmess|trojan|vless|hysteria|hy2|hysteria2)://") if reg.Match(data) { p := utils.ParseProxy(strings.Split(string(data), "\n")...) newProxies = p diff --git a/constant/prefix.go b/constant/prefix.go index 9a27b85..e629cf1 100644 --- a/constant/prefix.go +++ b/constant/prefix.go @@ -1,11 +1,12 @@ package constant const ( - HysteriaPrefix string = "hysteria://" - Hysteria2Prefix1 string = "hysteria2://" - Hysteria2Prefix2 string = "hy2://" - ShadowsocksPrefix string = "ss://" - TrojanPrefix string = "trojan://" - VLESSPrefix string = "vless://" - VMessPrefix string = "vmess://" + HysteriaPrefix string = "hysteria://" + Hysteria2Prefix1 string = "hysteria2://" + Hysteria2Prefix2 string = "hy2://" + ShadowsocksPrefix string = "ss://" + ShadowsocksRPrefix string = "ssr://" + TrojanPrefix string = "trojan://" + VLESSPrefix string = "vless://" + VMessPrefix string = "vmess://" ) diff --git a/parser/base64.go b/parser/base64.go index b14d423..1a775fa 100644 --- a/parser/base64.go +++ b/parser/base64.go @@ -7,6 +7,11 @@ import ( func DecodeBase64(s string) (string, error) { s = strings.TrimSpace(s) + // url safe + if strings.Contains(s, "-") || strings.Contains(s, "_") { + s = strings.Replace(s, "-", "+", -1) + s = strings.Replace(s, "_", "/", -1) + } if len(s)%4 != 0 { s += strings.Repeat("=", 4-len(s)%4) } diff --git a/parser/hysteria.go b/parser/hysteria.go index c7c9942..0a19fb7 100644 --- a/parser/hysteria.go +++ b/parser/hysteria.go @@ -1,79 +1,86 @@ package parser import ( - "errors" "net/url" "strconv" "strings" + "sub2clash/constant" "sub2clash/model" ) -//hysteria://host:port?protocol=udp&auth=123456&peer=sni.domain&insecure=1&upmbps=100&downmbps=100&alpn=hysteria&obfs=xplus&obfsParam=123456#remarks -// -//- host: hostname or IP address of the server to connect to (required) -//- port: port of the server to connect to (required) -//- protocol: protocol to use ("udp", "wechat-video", "faketcp") (optional, default: "udp") -//- auth: authentication payload (string) (optional) -//- peer: SNI for TLS (optional) -//- insecure: ignore certificate errors (optional) -//- upmbps: upstream bandwidth in Mbps (required) -//- downmbps: downstream bandwidth in Mbps (required) -//- alpn: QUIC ALPN (optional) -//- obfs: Obfuscation mode (optional, empty or "xplus") -//- obfsParam: Obfuscation password (optional) -//- remarks: remarks (optional) - func ParseHysteria(proxy string) (model.Proxy, error) { - // 判断是否以 hysteria:// 开头 - if !strings.HasPrefix(proxy, "hysteria://") { - return model.Proxy{}, errors.New("invalid hysteria Url") + if !strings.HasPrefix(proxy, constant.HysteriaPrefix) { + return model.Proxy{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy} } - // 分割 - parts := strings.SplitN(strings.TrimPrefix(proxy, "hysteria://"), "?", 2) - serverInfo := strings.SplitN(parts[0], ":", 2) + + proxy = strings.TrimPrefix(proxy, constant.HysteriaPrefix) + urlParts := strings.SplitN(proxy, "?", 2) + if len(urlParts) != 2 { + return model.Proxy{}, &ParseError{ + Type: ErrInvalidStruct, + Message: "missing character '?' in url", + Raw: proxy, + } + } + + serverInfo := strings.SplitN(urlParts[0], ":", 2) if len(serverInfo) != 2 { - return model.Proxy{}, errors.New("invalid hysteria Url") + return model.Proxy{}, &ParseError{ + Type: ErrInvalidStruct, + Message: "missing server host or port", + Raw: proxy, + } } - params, err := url.ParseQuery(parts[1]) + server, portStr := serverInfo[0], serverInfo[1] + + port, err := ParsePort(portStr) if err != nil { - return model.Proxy{}, errors.New("invalid hysteria Url") + return model.Proxy{}, &ParseError{ + Type: ErrInvalidPort, + Message: err.Error(), + Raw: proxy, + } } - host := serverInfo[0] - port, err := strconv.Atoi(serverInfo[1]) + + params, err := url.ParseQuery(urlParts[1]) if err != nil { - return model.Proxy{}, errors.New("invalid hysteria Url") + return model.Proxy{}, &ParseError{ + Type: ErrCannotParseParams, + Raw: proxy, + Message: err.Error(), + } } - protocol := params.Get("protocol") - auth := params.Get("auth") - peer := params.Get("peer") - insecure := params.Get("insecure") - upmbps := params.Get("upmbps") - downmbps := params.Get("downmbps") - alpn := params.Get("alpn") - obfs := params.Get("obfs") - obfsParam := params.Get("obfsParam") - remarks := "" - if strings.Contains(parts[1], "#") { - r := strings.Split(parts[1], "#") - remarks = r[len(r)-1] - } else { - remarks = serverInfo[0] + ":" + serverInfo[1] + + protocol, auth, insecure, upmbps, downmbps, obfs, alpnStr := params.Get("protocol"), params.Get("auth"), params.Get("insecure"), params.Get("upmbps"), params.Get("downmbps"), params.Get("obfs"), params.Get("alpn") + insecureBool, err := strconv.ParseBool(insecure) + if err != nil { + insecureBool = false } - // 返回结果 + + var alpn []string + alpnStr = strings.TrimSpace(alpnStr) + if alpnStr != "" { + alpn = strings.Split(alpnStr, ",") + } + + remarks := server + ":" + portStr + if params.Get("remarks") != "" { + remarks = params.Get("remarks") + } + result := model.Proxy{ Type: "hysteria", Name: remarks, - Server: host, + Server: server, Port: port, Up: upmbps, Down: downmbps, Auth: auth, Obfs: obfs, - Sni: peer, SkipCertVerify: insecure == "1", - Alpn: strings.Split(alpn, ","), - ObfsParam: obfsParam, + Alpn: alpn, Protocol: protocol, + AllowInsecure: insecureBool, } return result, nil } diff --git a/parser/hysteria2.go b/parser/hysteria2.go index 39343e6..2eb07ae 100644 --- a/parser/hysteria2.go +++ b/parser/hysteria2.go @@ -1,57 +1,89 @@ package parser import ( - "errors" "net/url" - "strconv" "strings" + "sub2clash/constant" "sub2clash/model" ) -// hysteria2://letmein@example.com/?insecure=1&obfs=salamander&obfs-password=gawrgura&pinSHA256=deadbeef&sni=real.example.com#name - func ParseHysteria2(proxy string) (model.Proxy, error) { - // 判断是否以 hysteria2:// 开头 - if !strings.HasPrefix(proxy, "hysteria2://") && !strings.HasPrefix(proxy, "hy2://") { - return model.Proxy{}, errors.New("invalid hysteria2 Url") + if !strings.HasPrefix(proxy, constant.Hysteria2Prefix1) && + !strings.HasPrefix(proxy, constant.Hysteria2Prefix2) { + return model.Proxy{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy} } - // 分割 - parts := strings.SplitN(strings.TrimPrefix(proxy, "hysteria2://"), "@", 2) - // 分割 - serverInfo := strings.SplitN(parts[1], "/?", 2) + + proxy = strings.TrimPrefix(proxy, constant.Hysteria2Prefix1) + proxy = strings.TrimPrefix(proxy, constant.Hysteria2Prefix2) + urlParts := strings.SplitN(proxy, "@", 2) + if len(urlParts) != 2 { + return model.Proxy{}, &ParseError{ + Type: ErrInvalidStruct, + Message: "missing character '@' in url", + Raw: proxy, + } + } + password := urlParts[0] + + serverInfo := strings.SplitN(urlParts[1], "/?", 2) + if len(serverInfo) != 2 { + return model.Proxy{}, &ParseError{ + Type: ErrInvalidStruct, + Message: "missing params in url", + Raw: proxy, + } + } + paramStr := serverInfo[1] + serverAndPort := strings.SplitN(serverInfo[0], ":", 2) + var server string + var portStr string if len(serverAndPort) == 1 { - serverAndPort = append(serverAndPort, "443") - } else if len(serverAndPort) != 2 { - return model.Proxy{}, errors.New("invalid hysteria2 Url") - } - params, err := url.ParseQuery(serverInfo[1]) - if err != nil { - return model.Proxy{}, errors.New("invalid hysteria2 Url") - } - // 获取端口 - port, err := strconv.Atoi(serverAndPort[1]) - if err != nil { - return model.Proxy{}, errors.New("invalid hysteria2 Url") - } - name := "" - if strings.Contains(proxy, "#") { - splitResult := strings.Split(proxy, "#") - name, _ = url.QueryUnescape(splitResult[len(splitResult)-1]) + portStr = "443" + } else if len(serverAndPort) == 2 { + server, portStr = serverAndPort[0], serverAndPort[1] } else { - name = strings.Join(serverAndPort, ":") + return model.Proxy{}, &ParseError{ + Type: ErrInvalidStruct, + Message: "missing server host or port", + Raw: proxy, + } } - // 返回结果 + + port, err := ParsePort(portStr) + if err != nil { + return model.Proxy{}, &ParseError{ + Type: ErrInvalidPort, + Message: err.Error(), + Raw: proxy, + } + } + + params, err := url.ParseQuery(paramStr) + if err != nil { + return model.Proxy{}, &ParseError{ + Type: ErrCannotParseParams, + Raw: proxy, + Message: err.Error(), + } + } + + remarks, network, obfs, obfsPassword, pinSHA256, insecure, sni := params.Get("name"), params.Get("network"), params.Get("obfs"), params.Get("obfs-password"), params.Get("pinSHA256"), params.Get("insecure"), params.Get("sni") + enableTLS := pinSHA256 != "" + insecureBool := insecure == "1" + result := model.Proxy{ Type: "hysteria2", - Name: name, - Server: serverAndPort[0], + Name: remarks, + Server: server, Port: port, - Password: parts[0], - Obfs: params.Get("obfs"), - ObfsParam: params.Get("obfs-password"), - Sni: params.Get("sni"), - SkipCertVerify: params.Get("insecure") == "1", + Password: password, + Obfs: obfs, + ObfsParam: obfsPassword, + Sni: sni, + SkipCertVerify: insecureBool, + TLS: enableTLS, + Network: network, } return result, nil } diff --git a/parser/port.go b/parser/port.go index 585b64c..eda7351 100644 --- a/parser/port.go +++ b/parser/port.go @@ -1,6 +1,7 @@ package parser import ( + "errors" "strconv" ) @@ -8,16 +9,10 @@ func ParsePort(portStr string) (int, error) { port, err := strconv.Atoi(portStr) if err != nil { - return 0, &ParseError{ - Type: ErrInvalidPort, - Message: portStr, - } + return 0, err } if port < 1 || port > 65535 { - return 0, &ParseError{ - Type: ErrInvalidPort, - Message: portStr, - } + return 0, errors.New("invaild port range") } return port, nil } diff --git a/parser/shadowsocks.go b/parser/shadowsocks.go new file mode 100644 index 0000000..6143f30 --- /dev/null +++ b/parser/shadowsocks.go @@ -0,0 +1,91 @@ +package parser + +import ( + "net/url" + "strings" + "sub2clash/constant" + "sub2clash/model" +) + +func ParseShadowsocks(proxy string) (model.Proxy, error) { + if !strings.HasPrefix(proxy, constant.ShadowsocksPrefix) { + return model.Proxy{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy} + } + + proxy = strings.TrimPrefix(proxy, constant.ShadowsocksPrefix) + urlParts := strings.SplitN(proxy, "@", 2) + if len(urlParts) != 2 { + return model.Proxy{}, &ParseError{ + Type: ErrInvalidStruct, + Message: "missing character '@' in url", + Raw: proxy, + } + } + + var serverAndPort []string + if !strings.Contains(urlParts[0], ":") { + decoded, err := DecodeBase64(urlParts[0]) + if err != nil { + return model.Proxy{}, &ParseError{ + Type: ErrInvalidStruct, + Message: "invalid base64 encoded", + Raw: proxy, + } + } + urlParts[0] = decoded + } + credentials := strings.SplitN(urlParts[0], ":", 2) + if len(credentials) != 2 { + return model.Proxy{}, &ParseError{ + Type: ErrInvalidStruct, + Message: "missing server host or port", + Raw: proxy, + } + } + method, password := credentials[0], credentials[1] + + serverInfo := strings.SplitN(urlParts[1], "#", 2) + serverAndPort = strings.SplitN(serverInfo[0], ":", 2) + server, portStr := serverAndPort[0], serverAndPort[1] + if len(serverInfo) != 2 { + return model.Proxy{}, &ParseError{ + Type: ErrInvalidStruct, + Message: "missing server host or port", + Raw: proxy, + } + } + port, err := ParsePort(portStr) + if err != nil { + return model.Proxy{}, &ParseError{ + Type: ErrInvalidPort, + Message: err.Error(), + Raw: proxy, + } + } + + var remarks string + if len(serverInfo) == 2 { + unescape, err := url.QueryUnescape(serverInfo[1]) + if err != nil { + return model.Proxy{}, &ParseError{ + Type: ErrInvalidStruct, + Message: "cannot unescape remarks", + Raw: proxy, + } + } + remarks = strings.TrimSpace(unescape) + } else { + remarks = strings.TrimSpace(server + ":" + portStr) + } + + result := model.Proxy{ + Type: "ss", + Cipher: method, + Password: password, + Server: server, + Port: port, + Name: remarks, + } + + return result, nil +} diff --git a/parser/shadowsocksr.go b/parser/shadowsocksr.go new file mode 100644 index 0000000..68c9e06 --- /dev/null +++ b/parser/shadowsocksr.go @@ -0,0 +1,86 @@ +package parser + +import ( + "net/url" + "strconv" + "strings" + "sub2clash/constant" + "sub2clash/model" +) + +func ParseShadowsocksR(proxy string) (model.Proxy, error) { + if !strings.HasPrefix(proxy, constant.ShadowsocksRPrefix) { + return model.Proxy{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy} + } + + proxy = strings.TrimPrefix(proxy, constant.ShadowsocksRPrefix) + proxy, err := DecodeBase64(proxy) + if err != nil { + return model.Proxy{}, &ParseError{ + Type: ErrInvalidBase64, + Raw: proxy, + } + } + serverInfoAndParams := strings.SplitN(proxy, "/?", 2) + parts := strings.Split(serverInfoAndParams[0], ":") + server := parts[0] + protocol := parts[2] + method := parts[3] + obfs := parts[4] + password := parts[5] + port, err := ParsePort(parts[1]) + if err != nil { + return model.Proxy{}, &ParseError{ + Type: ErrInvalidPort, + Message: err.Error(), + Raw: proxy, + } + } + + var obfsParam string + var protoParam string + var remarks string + if len(serverInfoAndParams) == 2 { + params, err := url.ParseQuery(serverInfoAndParams[1]) + if err != nil { + return model.Proxy{}, &ParseError{ + Type: ErrCannotParseParams, + Raw: proxy, + Message: err.Error(), + } + } + if params.Get("obfsparam") != "" { + obfsParam, err = DecodeBase64(params.Get("obfsparam")) + } + if params.Get("protoparam") != "" { + protoParam, err = DecodeBase64(params.Get("protoparam")) + } + if params.Get("remarks") != "" { + remarks, err = DecodeBase64(params.Get("remarks")) + } else { + remarks = server + ":" + strconv.Itoa(port) + } + if err != nil { + return model.Proxy{}, &ParseError{ + Type: ErrInvalidStruct, + Raw: proxy, + Message: err.Error(), + } + } + } + + result := model.Proxy{ + Name: remarks, + Type: "ssr", + Server: server, + Port: port, + Protocol: protocol, + Cipher: method, + Obfs: obfs, + Password: password, + ObfsParam: obfsParam, + ProtocolParam: protoParam, + } + + return result, nil +} diff --git a/parser/ss.go b/parser/ss.go deleted file mode 100644 index 11eceb2..0000000 --- a/parser/ss.go +++ /dev/null @@ -1,67 +0,0 @@ -package parser - -import ( - "errors" - "net/url" - "strconv" - "strings" - "sub2clash/model" -) - -// ParseSS 解析 SS(Shadowsocks)Url -func ParseSS(proxy string) (model.Proxy, error) { - // 判断是否以 ss:// 开头 - if !strings.HasPrefix(proxy, "ss://") { - return model.Proxy{}, errors.New("invalid ss Url") - } - // 分割 - parts := strings.SplitN(strings.TrimPrefix(proxy, "ss://"), "@", 2) - if len(parts) != 2 { - return model.Proxy{}, errors.New("invalid ss Url") - } - if !strings.Contains(parts[0], ":") { - // 解码 - decoded, err := DecodeBase64(parts[0]) - if err != nil { - return model.Proxy{}, errors.New("invalid ss Url" + err.Error()) - } - parts[0] = decoded - } - credentials := strings.SplitN(parts[0], ":", 2) - if len(credentials) != 2 { - return model.Proxy{}, errors.New("invalid ss Url") - } - // 分割 - serverInfo := strings.SplitN(parts[1], "#", 2) - serverAndPort := strings.SplitN(serverInfo[0], ":", 2) - if len(serverAndPort) != 2 { - return model.Proxy{}, errors.New("invalid ss Url") - } - // 转换端口字符串为数字 - port, err := strconv.Atoi(strings.TrimSpace(serverAndPort[1])) - if err != nil { - return model.Proxy{}, errors.New("invalid ss Url" + err.Error()) - } - // 返回结果 - result := model.Proxy{ - Type: "ss", - Cipher: strings.TrimSpace(credentials[0]), - Password: strings.TrimSpace(credentials[1]), - Server: strings.TrimSpace(serverAndPort[0]), - Port: port, - UDP: true, - Name: serverAndPort[0], - } - // 如果有节点名称 - if len(serverInfo) == 2 { - unescape, err := url.QueryUnescape(serverInfo[1]) - if err != nil { - return model.Proxy{}, errors.New("invalid ss Url" + err.Error()) - } - result.Name = strings.TrimSpace(unescape) - } else { - result.Name = strings.TrimSpace(serverAndPort[0]) - } - - return result, nil -} diff --git a/parser/ssr.go b/parser/ssr.go deleted file mode 100644 index a6cde05..0000000 --- a/parser/ssr.go +++ /dev/null @@ -1,71 +0,0 @@ -package parser - -import ( - "fmt" - "net/url" - "strconv" - "strings" - "sub2clash/model" -) - -func ParseShadowsocksR(proxy string) (model.Proxy, error) { - // 判断是否以 ssr:// 开头 - if !strings.HasPrefix(proxy, "ssr://") { - return model.Proxy{}, fmt.Errorf("invalid ssr Url") - } - var err error - proxy = strings.TrimPrefix(proxy, "ssr://") - if !strings.Contains(proxy, ":") { - proxy, err = DecodeBase64(strings.TrimPrefix(proxy, "ssr://")) - if err != nil { - return model.Proxy{}, err - } - } - // 分割 - detailsAndParams := strings.SplitN(proxy, "/?", 2) - parts := strings.Split(detailsAndParams[0], ":") - params, err := url.ParseQuery(detailsAndParams[1]) - if err != nil { - return model.Proxy{}, err - } - // 处理端口 - port, err := strconv.Atoi(parts[1]) - if err != nil { - return model.Proxy{}, err - } - var obfsParam string - var protoParam string - var remarks string - if params.Get("obfsparam") != "" { - obfsParam, err = DecodeBase64(params.Get("obfsparam")) - } - if params.Get("protoparam") != "" { - protoParam, err = DecodeBase64(params.Get("protoparam")) - } - if params.Get("remarks") != "" { - remarks, err = DecodeBase64(params.Get("remarks")) - } - - if err != nil { - return model.Proxy{}, err - } - - result := model.Proxy{ - Name: remarks, - Type: "ssr", - Server: parts[0], - Port: port, - Protocol: parts[2], - Cipher: parts[3], - Obfs: parts[4], - Password: parts[5], - ObfsParam: obfsParam, - ProtocolParam: protoParam, - } - - if result.Name == "" { - result.Name = result.Server - } - - return result, nil -} diff --git a/parser/trojan.go b/parser/trojan.go index cf37971..ee43bc4 100644 --- a/parser/trojan.go +++ b/parser/trojan.go @@ -55,7 +55,11 @@ func ParseTrojan(proxy string) (model.Proxy, error) { port, err := ParsePort(portStr) if err != nil { - return model.Proxy{}, err + return model.Proxy{}, &ParseError{ + Type: ErrInvalidPort, + Message: err.Error(), + Raw: proxy, + } } remarks := "" @@ -112,16 +116,13 @@ func ParseTrojan(proxy string) (model.Proxy, error) { } } - if network == "http" { - result.HTTP2Opts = model.HTTP2Options{ - Host: []string{host}, - Path: path, - } - } + // if network == "http" { + // // 未查到相关支持文档 + // } - if network == "quic" { - // 未查到相关支持文档 - } + // if network == "quic" { + // // 未查到相关支持文档 + // } if network == "grpc" { result.GrpcOpts = model.GrpcOptions{ diff --git a/parser/vless.go b/parser/vless.go index fa108a7..b74c9f1 100644 --- a/parser/vless.go +++ b/parser/vless.go @@ -1,92 +1,156 @@ package parser import ( - "fmt" "net/url" - "strconv" "strings" + "sub2clash/constant" "sub2clash/model" ) func ParseVless(proxy string) (model.Proxy, error) { - // 判断是否以 vless:// 开头 - if !strings.HasPrefix(proxy, "vless://") { - return model.Proxy{}, fmt.Errorf("invalid vless Url") + if !strings.HasPrefix(proxy, constant.VLESSPrefix) { + return model.Proxy{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy} } - // 分割 - parts := strings.SplitN(strings.TrimPrefix(proxy, "vless://"), "@", 2) - if len(parts) != 2 { - return model.Proxy{}, fmt.Errorf("invalid vless Url") + + urlParts := strings.SplitN(strings.TrimPrefix(proxy, constant.VLESSPrefix), "@", 2) + if len(urlParts) != 2 { + return model.Proxy{}, &ParseError{ + Type: ErrInvalidStruct, + Message: "missing character '@' in url", + Raw: proxy, + } } - // 分割 - serverInfo := strings.SplitN(parts[1], "#", 2) + + serverInfo := strings.SplitN(urlParts[1], "#", 2) serverAndPortAndParams := strings.SplitN(serverInfo[0], "?", 2) + if len(serverAndPortAndParams) != 2 { + return model.Proxy{}, &ParseError{ + Type: ErrInvalidStruct, + Message: "missing character '?' in url", + Raw: proxy, + } + } + serverAndPort := strings.SplitN(serverAndPortAndParams[0], ":", 2) + if len(serverAndPort) != 2 { + return model.Proxy{}, &ParseError{ + Type: ErrInvalidStruct, + Message: "missing server host or port", + Raw: proxy, + } + } + server, portStr := serverAndPort[0], serverAndPort[1] + port, err := ParsePort(portStr) + if err != nil { + return model.Proxy{}, &ParseError{ + Type: ErrInvalidPort, + Message: err.Error(), + Raw: proxy, + } + } + params, err := url.ParseQuery(serverAndPortAndParams[1]) if err != nil { - return model.Proxy{}, err - } - if len(serverAndPort) != 2 { - return model.Proxy{}, fmt.Errorf("invalid vless") - } - // 处理端口 - port, err := strconv.Atoi(strings.TrimSpace(serverAndPort[1])) - if err != nil { - return model.Proxy{}, err - } - // 返回结果 - result := model.Proxy{ - Type: "vless", - Server: strings.TrimSpace(serverAndPort[0]), - Port: port, - UUID: strings.TrimSpace(parts[0]), - UDP: true, - Sni: params.Get("sni"), - Network: params.Get("type"), - Flow: params.Get("flow"), - ClientFingerprint: params.Get("fp"), - Servername: params.Get("sni"), - } - if params.Get("alpn") != "" { - result.Alpn = strings.Split(params.Get("alpn"), ",") - } - if params.Get("security") == "reality" { - result.TLS = true - result.RealityOpts = model.RealityOptions{ - PublicKey: params.Get("pbk"), - ShortID: params.Get("sid"), + return model.Proxy{}, &ParseError{ + Type: ErrCannotParseParams, + Raw: proxy, + Message: err.Error(), } } - if params.Get("type") == "ws" { - result.TLS = true - result.WSOpts = model.WSOptions{ - Path: params.Get("path"), - Headers: map[string]string{ - "Host": params.Get("host"), - }, - } - } - if params.Get("type") == "grpc" { - result.TLS = true - result.GrpcOpts = model.GrpcOptions{ - GrpcServiceName: params.Get("serviceName"), - } - } - // 如果有节点名称 + + remarks := "" if len(serverInfo) == 2 { if strings.Contains(serverInfo[1], "|") { - result.Name = strings.SplitN(serverInfo[1], "|", 2)[1] + remarks = strings.SplitN(serverInfo[1], "|", 2)[1] } else { - result.Name, err = url.QueryUnescape(serverInfo[1]) + remarks, err = url.QueryUnescape(serverInfo[1]) if err != nil { - return model.Proxy{}, err + return model.Proxy{}, &ParseError{ + Type: ErrCannotParseParams, + Raw: proxy, + Message: err.Error(), + } } } } else { - result.Name, err = url.QueryUnescape(serverAndPort[0]) + remarks, err = url.QueryUnescape(server) if err != nil { return model.Proxy{}, err } } + + uuid := strings.TrimSpace(urlParts[0]) + flow, security, alpnStr, sni, insecure, fp, pbk, sid, path, host, serviceName, _type := params.Get("flow"), params.Get("security"), params.Get("alpn"), params.Get("sni"), params.Get("allowInsecure"), params.Get("fp"), params.Get("pbk"), params.Get("sid"), params.Get("path"), params.Get("host"), params.Get("serviceName"), params.Get("type") + + // enableUTLS := fp != "" + insecureBool := insecure == "1" + var alpn []string + if strings.Contains(alpnStr, ",") { + alpn = strings.Split(alpnStr, ",") + } else { + alpn = nil + } + + result := model.Proxy{ + Type: "vless", + Server: server, + Name: remarks, + Port: port, + UUID: uuid, + Flow: flow, + } + + if security == "tls" { + result.TLS = true + result.Alpn = alpn + result.Sni = sni + result.AllowInsecure = insecureBool + result.Fingerprint = fp + } + + if security == "reality" { + result.TLS = true + result.RealityOpts = model.RealityOptions{ + PublicKey: pbk, + ShortID: sid, + } + } + + if _type == "ws" { + result.Network = "ws" + result.WSOpts = model.WSOptions{ + Path: path, + } + if host != "" { + result.WSOpts.Headers = make(map[string]string) + result.WSOpts.Headers["Host"] = host + } + } + + // if _type == "quic" { + // // 未查到相关支持文档 + // } + + if _type == "grpc" { + result.Network = "grpc" + result.Servername = serviceName + } + + if _type == "http" { + hosts, err := url.QueryUnescape(host) + if err != nil { + return model.Proxy{}, &ParseError{ + Type: ErrCannotParseParams, + Raw: proxy, + Message: err.Error(), + } + } + result.Network = "http" + result.HTTPOpts = model.HTTPOptions{ + Headers: map[string][]string{"Host": strings.Split(hosts, ",")}, + } + + } + return result, nil } diff --git a/parser/vmess.go b/parser/vmess.go index 6250799..5ab3186 100644 --- a/parser/vmess.go +++ b/parser/vmess.go @@ -2,83 +2,96 @@ package parser import ( "encoding/json" - "errors" "net/url" "strconv" "strings" + "sub2clash/constant" "sub2clash/model" ) func ParseVmess(proxy string) (model.Proxy, error) { - // 判断是否以 vmess:// 开头 - if !strings.HasPrefix(proxy, "vmess://") { - return model.Proxy{}, errors.New("invalid vmess url") + if !strings.HasPrefix(proxy, constant.VMessPrefix) { + return model.Proxy{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy} } - // 解码 - base64, err := DecodeBase64(strings.TrimPrefix(proxy, "vmess://")) + + proxy = strings.TrimPrefix(proxy, constant.VMessPrefix) + base64, err := DecodeBase64(proxy) if err != nil { - return model.Proxy{}, errors.New("invalid vmess url" + err.Error()) + return model.Proxy{}, &ParseError{Type: ErrInvalidBase64, Raw: proxy, Message: err.Error()} } - // 解析 + var vmess model.VmessJson err = json.Unmarshal([]byte(base64), &vmess) if err != nil { - return model.Proxy{}, errors.New("invalid vmess url" + err.Error()) + return model.Proxy{}, &ParseError{Type: ErrInvalidStruct, Raw: proxy, Message: err.Error()} } - // 解析端口 - port := 0 + + var port int switch vmess.Port.(type) { case string: - port, err = strconv.Atoi(vmess.Port.(string)) + port, err = ParsePort(vmess.Port.(string)) if err != nil { - return model.Proxy{}, errors.New("invalid vmess url" + err.Error()) + return model.Proxy{}, &ParseError{ + Type: ErrInvalidPort, + Message: err.Error(), + Raw: proxy, + } } case float64: port = int(vmess.Port.(float64)) } - // 解析Aid + aid := 0 switch vmess.Aid.(type) { case string: aid, err = strconv.Atoi(vmess.Aid.(string)) if err != nil { - return model.Proxy{}, errors.New("invalid vmess url" + err.Error()) + return model.Proxy{}, &ParseError{Type: ErrInvalidStruct, Raw: proxy, Message: err.Error()} } case float64: aid = int(vmess.Aid.(float64)) } - // 设置默认值 + if vmess.Scy == "" { vmess.Scy = "auto" } - if vmess.Net == "ws" && vmess.Path == "" { - vmess.Path = "/" - } - if vmess.Net == "ws" && vmess.Host == "" { - vmess.Host = vmess.Add - } + name, err := url.QueryUnescape(vmess.Ps) if err != nil { name = vmess.Ps } - // 返回结果 + result := model.Proxy{ - Name: name, - Type: "vmess", - Server: vmess.Add, - Port: port, - UUID: vmess.Id, - AlterID: aid, - Cipher: vmess.Scy, - UDP: true, - TLS: vmess.Tls == "tls", - Fingerprint: vmess.Fp, - ClientFingerprint: "chrome", - SkipCertVerify: true, - Servername: vmess.Add, - Network: vmess.Net, + Name: name, + Type: "vmess", + Server: vmess.Add, + Port: port, + UUID: vmess.Id, + AlterID: aid, + Cipher: vmess.Scy, } + + if vmess.Tls == "tls" { + var alpn []string + if strings.Contains(vmess.Alpn, ",") { + alpn = strings.Split(vmess.Alpn, ",") + } else { + alpn = nil + } + result.TLS = true + result.Fingerprint = vmess.Fp + result.Alpn = alpn + result.Servername = vmess.Sni + } + if vmess.Net == "ws" { + if vmess.Path == "" { + vmess.Path = "/" + } + if vmess.Host == "" { + vmess.Host = vmess.Add + } + result.Network = "ws" result.WSOpts = model.WSOptions{ Path: vmess.Path, Headers: map[string]string{ @@ -86,5 +99,25 @@ func ParseVmess(proxy string) (model.Proxy, error) { }, } } + + // if vmess.Net == "quic" { + // // 未查到相关支持文档 + // } + + if vmess.Net == "grpc" { + result.GrpcOpts = model.GrpcOptions{ + GrpcServiceName: vmess.Path, + } + result.Network = "grpc" + } + + if vmess.Net == "h2" { + result.HTTP2Opts = model.HTTP2Options{ + Host: strings.Split(vmess.Host, ","), + Path: vmess.Path, + } + result.Network = "h2" + } + return result, nil } diff --git a/test/parser_test.go b/test/parser_test.go index 9d85217..a14f891 100644 --- a/test/parser_test.go +++ b/test/parser_test.go @@ -1,22 +1,22 @@ package test import ( - "sub2clash/parser" + "strings" "testing" - - "gopkg.in/yaml.v3" ) -func TestHy2Parser(t *testing.T) { - res, err := parser.ParseTrojan("trojan://Abse64hhjewrs@test.com:8443?type=ws&path=%2Fx&host=test.com&security=tls&fp=&alpn=http%2F1.1&sni=test.com#test") - if err != nil { - t.Log(err.Error()) - t.Fail() - } - bytes, err := yaml.Marshal(res) - if err != nil { - t.Log(err.Error()) - t.Fail() - } - t.Log(string(bytes)) +func TestParser(t *testing.T) { + // res, err := parser.ParseTrojan("trojan://Abse64hhjewrs@test.com:8443?type=ws&path=%2Fx&host=test.com&security=tls&fp=&alpn=http%2F1.1&sni=test.com#test") + // if err != nil { + // t.Log(err.Error()) + // t.Fail() + // } + // bytes, err := yaml.Marshal(res) + // if err != nil { + // t.Log(err.Error()) + // t.Fail() + // } + // t.Log(string(bytes)) + t.Log(strings.SplitN("123456", "/?", 2)) + } diff --git a/utils/proxy.go b/utils/proxy.go index e483f10..d17fca7 100644 --- a/utils/proxy.go +++ b/utils/proxy.go @@ -2,6 +2,7 @@ package utils import ( "strings" + "sub2clash/constant" "sub2clash/logger" "sub2clash/model" "sub2clash/parser" @@ -106,25 +107,25 @@ func ParseProxy(proxies ...string) []model.Proxy { var proxyItem model.Proxy var err error // 解析节点 - if strings.HasPrefix(proxy, "ss://") { - proxyItem, err = parser.ParseSS(proxy) + if strings.HasPrefix(proxy, constant.ShadowsocksPrefix) { + proxyItem, err = parser.ParseShadowsocks(proxy) } - if strings.HasPrefix(proxy, "trojan://") { + if strings.HasPrefix(proxy, constant.TrojanPrefix) { proxyItem, err = parser.ParseTrojan(proxy) } - if strings.HasPrefix(proxy, "vmess://") { + if strings.HasPrefix(proxy, constant.VMessPrefix) { proxyItem, err = parser.ParseVmess(proxy) } - if strings.HasPrefix(proxy, "vless://") { + if strings.HasPrefix(proxy, constant.VLESSPrefix) { proxyItem, err = parser.ParseVless(proxy) } - if strings.HasPrefix(proxy, "ssr://") { + if strings.HasPrefix(proxy, constant.ShadowsocksRPrefix) { proxyItem, err = parser.ParseShadowsocksR(proxy) } - if strings.HasPrefix(proxy, "hysteria2://") || strings.HasPrefix(proxy, "hy2://") { + if strings.HasPrefix(proxy, constant.Hysteria2Prefix1) || strings.HasPrefix(proxy, constant.Hysteria2Prefix2) { proxyItem, err = parser.ParseHysteria2(proxy) } - if strings.HasPrefix(proxy, "hysteria://") { + if strings.HasPrefix(proxy, constant.HysteriaPrefix) { proxyItem, err = parser.ParseHysteria(proxy) } if err == nil {