From a7ceb4ecb0d8a005f1a8cccc705cc3e7b10b7254 Mon Sep 17 00:00:00 2001 From: nite07 Date: Fri, 22 Mar 2024 16:10:15 +0800 Subject: [PATCH] =?UTF-8?q?=E6=95=B4=E7=90=86=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 - api/handler/convert.go | 4 +- cmd/convert.go | 7 +- common/convert.go | 2 +- constant/prefix.go | 11 ++ model/config.go | 236 ++--------------------------------------- parser/error.go | 24 +++++ parser/hysteria.go | 103 +++++++++--------- parser/hysteria2.go | 84 ++++++++++----- parser/parsers_map.go | 15 +-- parser/port.go | 23 ++++ parser/shadowsocks.go | 81 +++++++++----- parser/trojan.go | 105 ++++++++++++------ parser/vless.go | 133 +++++++++++++++-------- parser/vmess.go | 30 +++--- test/country_test.go | 11 ++ 16 files changed, 428 insertions(+), 442 deletions(-) create mode 100644 constant/prefix.go create mode 100644 parser/error.go create mode 100644 parser/port.go create mode 100644 test/country_test.go diff --git a/.gitignore b/.gitignore index 8e47708..d9addfc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ .vscode/launch.json .vscode/settings.json dist -*test.go template.json .idea \ No newline at end of file diff --git a/api/handler/convert.go b/api/handler/convert.go index 3e6dac3..8e7d970 100644 --- a/api/handler/convert.go +++ b/api/handler/convert.go @@ -4,7 +4,7 @@ import ( "encoding/json" "sub2sing-box/api/model" "sub2sing-box/common" - putil "sub2sing-box/util" + "sub2sing-box/util" "github.com/gin-gonic/gin" ) @@ -17,7 +17,7 @@ func Convert(c *gin.Context) { }) return } - j, err := putil.DecodeBase64(c.Query("data")) + j, err := util.DecodeBase64(c.Query("data")) if err != nil { c.JSON(400, gin.H{ "error": "Invalid data", diff --git a/cmd/convert.go b/cmd/convert.go index 82d8942..9bf8bf1 100644 --- a/cmd/convert.go +++ b/cmd/convert.go @@ -2,9 +2,10 @@ package cmd import ( "fmt" - "github.com/spf13/cobra" "os" - . "sub2sing-box/common" + "sub2sing-box/common" + + "github.com/spf13/cobra" ) var subscriptions []string @@ -39,7 +40,7 @@ var convertCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { result := "" var err error - result, err = Convert( + result, err = common.Convert( subscriptions, proxies, template, diff --git a/common/convert.go b/common/convert.go index e975560..432fee3 100644 --- a/common/convert.go +++ b/common/convert.go @@ -233,7 +233,7 @@ func ConvertCProxyToSProxy(proxy string) (model.Outbound, error) { return proxy, nil } } - return model.Outbound{}, errors.New("Unknown proxy format") + return model.Outbound{}, errors.New("unknown proxy format") } func ConvertCProxyToJson(proxy string) (string, error) { diff --git a/constant/prefix.go b/constant/prefix.go new file mode 100644 index 0000000..9a27b85 --- /dev/null +++ b/constant/prefix.go @@ -0,0 +1,11 @@ +package constant + +const ( + HysteriaPrefix string = "hysteria://" + Hysteria2Prefix1 string = "hysteria2://" + Hysteria2Prefix2 string = "hy2://" + ShadowsocksPrefix string = "ss://" + TrojanPrefix string = "trojan://" + VLESSPrefix string = "vless://" + VMessPrefix string = "vmess://" +) diff --git a/model/config.go b/model/config.go index 052c607..3cdaec4 100644 --- a/model/config.go +++ b/model/config.go @@ -18,189 +18,14 @@ func (l *Listable[T]) UnmarshalJSON(data []byte) error { return nil } -type Obfs struct { - Str string - Obfs *Hysteria2Obfs - IsStr bool -} - -func (o *Obfs) UnmarshalJSON(data []byte) error { - var str string - if err := json.Unmarshal(data, &str); err == nil { - o.Str = str - o.IsStr = true - return nil - } - var obfs Hysteria2Obfs - if err := json.Unmarshal(data, &obfs); err == nil { - o.IsStr = false - o.Obfs = &obfs - return nil - } - return nil -} - -func (o Obfs) MarshalJSON() ([]byte, error) { - if o.IsStr { - return json.Marshal(o.Str) - } - return json.Marshal(o.Obfs) -} - type Config struct { - Log *LogOptions `json:"log,omitempty"` - DNS *DNSOptions `json:"dns,omitempty"` - NTP *NTPOptions `json:"ntp,omitempty"` - Inbounds []Inbound `json:"inbounds,omitempty"` - Outbounds []Outbound `json:"outbounds,omitempty"` - Route *RouteOptions `json:"route,omitempty"` - Experimental *ExperimentalOptions `json:"experimental,omitempty"` -} - -type LogOptions struct { - Disabled bool `json:"disabled,omitempty"` - Level string `json:"level,omitempty"` - Output string `json:"output,omitempty"` - Timestamp bool `json:"timestamp,omitempty"` -} - -type DNSOptions struct { - Servers Listable[DNSServerOptions] `json:"servers,omitempty"` - Rules Listable[DNSRule] `json:"rules,omitempty"` - Final string `json:"final,omitempty"` - ReverseMapping bool `json:"reverse_mapping,omitempty"` - FakeIP *DNSFakeIPOptions `json:"fakeip,omitempty"` - Strategy string `json:"strategy,omitempty"` - DisableCache bool `json:"disable_cache,omitempty"` - DisableExpire bool `json:"disable_expire,omitempty"` - IndependentCache bool `json:"independent_cache,omitempty"` - ClientSubnet string `json:"client_subnet,omitempty"` -} - -type DNSServerOptions struct { - Tag string `json:"tag,omitempty"` - Address string `json:"address"` - AddressResolver string `json:"address_resolver,omitempty"` - AddressStrategy string `json:"address_strategy,omitempty"` - Strategy string `json:"strategy,omitempty"` - Detour string `json:"detour,omitempty"` - ClientSubnet string `json:"client_subnet,omitempty"` -} - -type DNSRule struct { - Type string `json:"type,omitempty"` - Inbound Listable[string] `json:"inbound,omitempty"` - IPVersion int `json:"ip_version,omitempty"` - QueryType Listable[string] `json:"query_type,omitempty"` - Network Listable[string] `json:"network,omitempty"` - AuthUser Listable[string] `json:"auth_user,omitempty"` - Protocol Listable[string] `json:"protocol,omitempty"` - Domain Listable[string] `json:"domain,omitempty"` - DomainSuffix Listable[string] `json:"domain_suffix,omitempty"` - DomainKeyword Listable[string] `json:"domain_keyword,omitempty"` - DomainRegex Listable[string] `json:"domain_regex,omitempty"` - Geosite Listable[string] `json:"geosite,omitempty"` - SourceGeoIP Listable[string] `json:"source_geoip,omitempty"` - GeoIP Listable[string] `json:"geoip,omitempty"` - IPCIDR Listable[string] `json:"ip_cidr,omitempty"` - IPIsPrivate bool `json:"ip_is_private,omitempty"` - SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"` - SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"` - SourcePort Listable[uint16] `json:"source_port,omitempty"` - SourcePortRange Listable[string] `json:"source_port_range,omitempty"` - Port Listable[uint16] `json:"port,omitempty"` - PortRange Listable[string] `json:"port_range,omitempty"` - ProcessName Listable[string] `json:"process_name,omitempty"` - ProcessPath Listable[string] `json:"process_path,omitempty"` - PackageName Listable[string] `json:"package_name,omitempty"` - User Listable[string] `json:"user,omitempty"` - UserID Listable[int32] `json:"user_id,omitempty"` - Outbound Listable[string] `json:"outbound,omitempty"` - ClashMode string `json:"clash_mode,omitempty"` - WIFISSID Listable[string] `json:"wifi_ssid,omitempty"` - WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"` - RuleSet Listable[string] `json:"rule_set,omitempty"` - RuleSetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty"` - Invert bool `json:"invert,omitempty"` - Server string `json:"server,omitempty"` - DisableCache bool `json:"disable_cache,omitempty"` - RewriteTTL uint32 `json:"rewrite_ttl,omitempty"` - ClientSubnet string `json:"client_subnet,omitempty"` - Mode string `json:"mode,omitempty"` - Rules Listable[DNSRule] `json:"rules,omitempty"` -} - -type DNSFakeIPOptions struct { - Enabled bool `json:"enabled,omitempty"` - Inet4Range string `json:"inet4_range,omitempty"` - Inet6Range string `json:"inet6_range,omitempty"` -} - -type NTPOptions struct { - Enabled bool `json:"enabled,omitempty"` - Server string `json:"server,omitempty"` - ServerPort uint16 `json:"server_port,omitempty"` - Interval string `json:"interval,omitempty"` - WriteToSystem bool `json:"write_to_system,omitempty"` - Detour string `json:"detour,omitempty"` - BindInterface string `json:"bind_interface,omitempty"` - Inet4BindAddress string `json:"inet4_bind_address,omitempty"` - Inet6BindAddress string `json:"inet6_bind_address,omitempty"` - ProtectPath string `json:"protect_path,omitempty"` - RoutingMark int `json:"routing_mark,omitempty"` - ReuseAddr bool `json:"reuse_addr,omitempty"` - ConnectTimeout string `json:"connect_timeout,omitempty"` - TCPFastOpen bool `json:"tcp_fast_open,omitempty"` - TCPMultiPath bool `json:"tcp_multi_path,omitempty"` - UDPFragment bool `json:"udp_fragment,omitempty"` - DomainStrategy string `json:"domain_strategy,omitempty"` - FallbackDelay string `json:"fallback_delay,omitempty"` -} - -type Inbound struct { - Type string `json:"type"` - Tag string `json:"tag,omitempty"` - InterfaceName string `json:"interface_name,omitempty"` - MTU uint32 `json:"mtu,omitempty"` - GSO bool `json:"gso,omitempty"` - Inet4Address Listable[string] `json:"inet4_address,omitempty"` - Inet6Address Listable[string] `json:"inet6_address,omitempty"` - AutoRoute bool `json:"auto_route,omitempty"` - StrictRoute bool `json:"strict_route,omitempty"` - Inet4RouteAddress Listable[string] `json:"inet4_route_address,omitempty"` - Inet6RouteAddress Listable[string] `json:"inet6_route_address,omitempty"` - Inet4RouteExcludeAddress Listable[string] `json:"inet4_route_exclude_address,omitempty"` - Inet6RouteExcludeAddress Listable[string] `json:"inet6_route_exclude_address,omitempty"` - IncludeInterface Listable[string] `json:"include_interface,omitempty"` - ExcludeInterface Listable[string] `json:"exclude_interface,omitempty"` - IncludeUID Listable[uint32] `json:"include_uid,omitempty"` - IncludeUIDRange Listable[string] `json:"include_uid_range,omitempty"` - ExcludeUID Listable[uint32] `json:"exclude_uid,omitempty"` - ExcludeUIDRange Listable[string] `json:"exclude_uid_range,omitempty"` - IncludeAndroidUser Listable[int] `json:"include_android_user,omitempty"` - IncludePackage Listable[string] `json:"include_package,omitempty"` - ExcludePackage Listable[string] `json:"exclude_package,omitempty"` - EndpointIndependentNat bool `json:"endpoint_independent_nat,omitempty"` - UDPTimeout string `json:"udp_timeout,omitempty"` - Stack string `json:"stack,omitempty"` - Platform *TunPlatformOptions `json:"platform,omitempty"` - SniffEnabled bool `json:"sniff,omitempty"` - SniffOverrideDestination bool `json:"sniff_override_destination,omitempty"` - SniffTimeout string `json:"sniff_timeout,omitempty"` - DomainStrategy string `json:"domain_strategy,omitempty"` - UDPDisableDomainUnmapping bool `json:"udp_disable_domain_unmapping,omitempty"` -} - -type TunPlatformOptions struct { - HTTPProxy *HTTPProxyOptions `json:"http_proxy,omitempty"` -} - -type HTTPProxyOptions struct { - Enabled bool `json:"enabled,omitempty"` - Server string `json:"server"` - ServerPort uint16 `json:"server_port"` - BypassDomain Listable[string] `json:"bypass_domain,omitempty"` - MatchDomain Listable[string] `json:"match_domain,omitempty"` + Log any `json:"log,omitempty"` + DNS any `json:"dns,omitempty"` + NTP any `json:"ntp,omitempty"` + Inbounds any `json:"inbounds,omitempty"` + Outbounds []Outbound `json:"outbounds,omitempty"` + Route *RouteOptions `json:"route,omitempty"` + Experimental any `json:"experimental,omitempty"` } type RouteOptions struct { @@ -275,50 +100,3 @@ type RuleSet struct { DownloadDetour string `json:"download_detour,omitempty"` UpdateInterval string `json:"update_interval,omitempty"` } - -type ExperimentalOptions struct { - CacheFile *CacheFileOptions `json:"cache_file,omitempty"` - ClashAPI *ClashAPIOptions `json:"clash_api,omitempty"` - V2RayAPI *V2RayAPIOptions `json:"v2ray_api,omitempty"` -} - -type CacheFileOptions struct { - Enabled bool `json:"enabled,omitempty"` - Path string `json:"path,omitempty"` - CacheID string `json:"cache_id,omitempty"` - StoreFakeIP bool `json:"store_fakeip,omitempty"` - StoreRDRC bool `json:"store_rdrc,omitempty"` - RDRCTimeout string `json:"rdrc_timeout,omitempty"` -} - -type ClashAPIOptions struct { - ExternalController string `json:"external_controller,omitempty"` - ExternalUI string `json:"external_ui,omitempty"` - ExternalUIDownloadURL string `json:"external_ui_download_url,omitempty"` - ExternalUIDownloadDetour string `json:"external_ui_download_detour,omitempty"` - Secret string `json:"secret,omitempty"` - DefaultMode string `json:"default_mode,omitempty"` - - // Deprecated: migrated to global cache file - CacheFile string `json:"cache_file,omitempty"` - // Deprecated: migrated to global cache file - CacheID string `json:"cache_id,omitempty"` - // Deprecated: migrated to global cache file - StoreMode bool `json:"store_mode,omitempty"` - // Deprecated: migrated to global cache file - StoreSelected bool `json:"store_selected,omitempty"` - // Deprecated: migrated to global cache file - StoreFakeIP bool `json:"store_fakeip,omitempty"` -} - -type V2RayAPIOptions struct { - Listen string `json:"listen,omitempty"` - Stats *V2RayStatsServiceOptions `json:"stats,omitempty"` -} - -type V2RayStatsServiceOptions struct { - Enabled bool `json:"enabled,omitempty"` - Inbounds Listable[string] `json:"inbounds,omitempty"` - Outbounds Listable[string] `json:"outbounds,omitempty"` - Users Listable[string] `json:"users,omitempty"` -} diff --git a/parser/error.go b/parser/error.go new file mode 100644 index 0000000..af95f0b --- /dev/null +++ b/parser/error.go @@ -0,0 +1,24 @@ +package parser + +type ParseError struct { + Type ParseErrorType + Message string + Raw string +} + +type ParseErrorType string + +const ( + ErrInvalidPrefix ParseErrorType = "invalid url prefix" + ErrInvalidStruct ParseErrorType = "invalid struct" + ErrInvalidPort ParseErrorType = "invalid port number" + ErrCannotParseParams ParseErrorType = "cannot parse query parameters" + ErrInvalidBase64 ParseErrorType = "invalid base64" +) + +func (e *ParseError) Error() string { + if e.Message != "" { + return string(e.Type) + ": " + e.Message + " \"" + e.Raw + "\"" + } + return string(e.Type) +} diff --git a/parser/hysteria.go b/parser/hysteria.go index 854e910..3fc487e 100644 --- a/parser/hysteria.go +++ b/parser/hysteria.go @@ -1,78 +1,76 @@ package parser import ( - "errors" "net/url" "strconv" "strings" + "sub2sing-box/constant" "sub2sing-box/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.Outbound, error) { - if !strings.HasPrefix(proxy, "hysteria://") { - return model.Outbound{}, errors.New("invalid hysteria Url") + if !strings.HasPrefix(proxy, constant.HysteriaPrefix) { + return model.Outbound{}, &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.Outbound{}, &ParseError{ + Type: ErrInvalidStruct, + Message: "missing character '?' in url", + Raw: proxy, + } + } + + serverInfo := strings.SplitN(urlParts[0], ":", 2) if len(serverInfo) != 2 { - return model.Outbound{}, errors.New("invalid hysteria Url") + return model.Outbound{}, &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.Outbound{}, errors.New("invalid hysteria Url") + return model.Outbound{}, err } - host := serverInfo[0] - port, err := strconv.Atoi(serverInfo[1]) + + params, err := url.ParseQuery(urlParts[1]) if err != nil { - return model.Outbound{}, errors.New("invalid hysteria Url") - } - protocol := params.Get("protocol") - auth := params.Get("auth") - // peer := params.Get("peer") - insecure := params.Get("insecure") - upmbps := params.Get("upmbps") - downmbps := params.Get("downmbps") - obfs := params.Get("obfs") - // obfsParam := params.Get("obfsParam") - var alpn []string - if params.Get("alpn") != "" { - alpn = strings.Split(params.Get("alpn"), ",") - } else { - alpn = nil - } - remarks := "" - if strings.Contains(parts[1], "#") { - r := strings.Split(parts[1], "#") - remarks = r[len(r)-1] - } else { - remarks = serverInfo[0] + ":" + serverInfo[1] + return model.Outbound{}, &ParseError{ + Type: ErrCannotParseParams, + Raw: proxy, + Message: err.Error(), + } } + + 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 { - return model.Outbound{}, errors.New("invalid hysteria Url") + insecureBool = false } - result := model.Outbound{ + + var alpn []string + alpnStr = strings.TrimSpace(alpnStr) + if alpnStr != "" { + alpn = strings.Split(alpnStr, ",") + } + + remarks := server + ":" + portStr + if params.Get("remarks") != "" { + remarks = params.Get("remarks") + } + + return model.Outbound{ Type: "hysteria", Tag: remarks, HysteriaOptions: model.HysteriaOutboundOptions{ ServerOptions: model.ServerOptions{ - Server: host, - ServerPort: uint16(port), + Server: server, + ServerPort: port, }, Up: upmbps, Down: downmbps, @@ -87,6 +85,5 @@ func ParseHysteria(proxy string) (model.Outbound, error) { }, }, }, - } - return result, nil + }, nil } diff --git a/parser/hysteria2.go b/parser/hysteria2.go index 88b97c0..3afcff5 100644 --- a/parser/hysteria2.go +++ b/parser/hysteria2.go @@ -1,39 +1,73 @@ package parser import ( - "errors" "net/url" - "strconv" "strings" + "sub2sing-box/constant" "sub2sing-box/model" ) -// hysteria2://letmein@example.com/?insecure=1&obfs=salamander&obfs-password=gawrgura&pinSHA256=deadbeef&sni=real.example.com - func ParseHysteria2(proxy string) (model.Outbound, error) { - if !strings.HasPrefix(proxy, "hysteria2://") && !strings.HasPrefix(proxy, "hy2://") { - return model.Outbound{}, errors.New("invalid hysteria2 Url") + if !strings.HasPrefix(proxy, constant.Hysteria2Prefix1) && + !strings.HasPrefix(proxy, constant.Hysteria2Prefix2) { + return model.Outbound{}, &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.Outbound{}, &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.Outbound{}, &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.Outbound{}, errors.New("invalid hysteria2 Url") + portStr = "443" + } else if len(serverAndPort) == 2 { + server, portStr = serverAndPort[0], serverAndPort[1] + } else { + return model.Outbound{}, &ParseError{ + Type: ErrInvalidStruct, + Message: "missing server host or port", + Raw: proxy, + } } - params, err := url.ParseQuery(serverInfo[1]) + + port, err := ParsePort(portStr) if err != nil { - return model.Outbound{}, errors.New("invalid hysteria2 Url") + return model.Outbound{}, err } - port, err := strconv.Atoi(serverAndPort[1]) + + params, err := url.ParseQuery(paramStr) if err != nil { - return model.Outbound{}, errors.New("invalid hysteria2 Url") + return model.Outbound{}, &ParseError{ + Type: ErrCannotParseParams, + Raw: proxy, + Message: err.Error(), + } } - remarks := params.Get("name") - server := serverAndPort[0] - password := parts[0] - network := params.Get("network") + + 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.Outbound{ Type: "hysteria2", Tag: remarks, @@ -44,14 +78,14 @@ func ParseHysteria2(proxy string) (model.Outbound, error) { }, Password: password, Obfs: &model.Hysteria2Obfs{ - Type: params.Get("obfs"), - Password: params.Get("obfs-password"), + Type: obfs, + Password: obfsPassword, }, OutboundTLSOptionsContainer: model.OutboundTLSOptionsContainer{ - TLS: &model.OutboundTLSOptions{Enabled: params.Get("pinSHA256") != "", - Insecure: params.Get("insecure") == "1", - ServerName: params.Get("sni"), - Certificate: []string{params.Get("pinSHA256")}}, + TLS: &model.OutboundTLSOptions{Enabled: enableTLS, + Insecure: insecureBool, + ServerName: sni, + Certificate: []string{pinSHA256}}, }, Network: network, }, diff --git a/parser/parsers_map.go b/parser/parsers_map.go index 6837179..dd8e9ac 100644 --- a/parser/parsers_map.go +++ b/parser/parsers_map.go @@ -1,15 +1,16 @@ package parser import ( + "sub2sing-box/constant" "sub2sing-box/model" ) var ParserMap map[string]func(string) (model.Outbound, error) = map[string]func(string) (model.Outbound, error){ - "ss://": ParseShadowsocks, - "vmess://": ParseVmess, - "trojan://": ParseTrojan, - "vless://": ParseVless, - "hysteria://": ParseHysteria, - "hy2://": ParseHysteria2, - "hysteria2://": ParseHysteria2, + constant.ShadowsocksPrefix: ParseShadowsocks, + constant.VMessPrefix: ParseVmess, + constant.TrojanPrefix: ParseTrojan, + constant.VLESSPrefix: ParseVless, + constant.HysteriaPrefix: ParseHysteria, + constant.Hysteria2Prefix1: ParseHysteria2, + constant.Hysteria2Prefix2: ParseHysteria2, } diff --git a/parser/port.go b/parser/port.go new file mode 100644 index 0000000..88e0e1e --- /dev/null +++ b/parser/port.go @@ -0,0 +1,23 @@ +package parser + +import ( + "strconv" +) + +func ParsePort(portStr string) (uint16, error) { + port, err := strconv.Atoi(portStr) + + if err != nil { + return 0, &ParseError{ + Type: ErrInvalidPort, + Message: portStr, + } + } + if port < 1 || port > 65535 { + return 0, &ParseError{ + Type: ErrInvalidPort, + Message: portStr, + } + } + return uint16(port), nil +} diff --git a/parser/shadowsocks.go b/parser/shadowsocks.go index 2ce68d9..e2bb7aa 100644 --- a/parser/shadowsocks.go +++ b/parser/shadowsocks.go @@ -1,62 +1,87 @@ package parser import ( - "errors" "net/url" - "strconv" "strings" + "sub2sing-box/constant" "sub2sing-box/model" "sub2sing-box/util" ) func ParseShadowsocks(proxy string) (model.Outbound, error) { - if !strings.HasPrefix(proxy, "ss://") { - return model.Outbound{}, errors.New("invalid ss Url") + if !strings.HasPrefix(proxy, constant.ShadowsocksPrefix) { + return model.Outbound{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy} } - parts := strings.SplitN(strings.TrimPrefix(proxy, "ss://"), "@", 2) - if len(parts) != 2 { - return model.Outbound{}, errors.New("invalid ss Url") - } - if !strings.Contains(parts[0], ":") { - decoded, err := util.DecodeBase64(parts[0]) - if err != nil { - return model.Outbound{}, errors.New("invalid ss Url" + err.Error()) + + proxy = strings.TrimPrefix(proxy, constant.ShadowsocksPrefix) + urlParts := strings.SplitN(proxy, "@", 2) + if len(urlParts) != 2 { + return model.Outbound{}, &ParseError{ + Type: ErrInvalidStruct, + Message: "missing character '@' in url", + Raw: proxy, } - parts[0] = decoded } - credentials := strings.SplitN(parts[0], ":", 2) + + var serverAndPort []string + if !strings.Contains(urlParts[0], ":") { + decoded, err := util.DecodeBase64(urlParts[0]) + if err != nil { + return model.Outbound{}, &ParseError{ + Type: ErrInvalidStruct, + Message: "invalid base64 encoded", + Raw: proxy, + } + } + urlParts[0] = decoded + } + credentials := strings.SplitN(urlParts[0], ":", 2) if len(credentials) != 2 { - return model.Outbound{}, errors.New("invalid ss Url") + return model.Outbound{}, &ParseError{ + Type: ErrInvalidStruct, + Message: "missing server host or port", + Raw: proxy, + } } - serverInfo := strings.SplitN(parts[1], "#", 2) - serverAndPort := strings.SplitN(serverInfo[0], ":", 2) - if len(serverAndPort) != 2 { - return model.Outbound{}, errors.New("invalid ss Url") + 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.Outbound{}, &ParseError{ + Type: ErrInvalidStruct, + Message: "missing server host or port", + Raw: proxy, + } } - port, err := strconv.Atoi(strings.TrimSpace(serverAndPort[1])) + port, err := ParsePort(portStr) if err != nil { - return model.Outbound{}, errors.New("invalid ss Url" + err.Error()) + return model.Outbound{}, err } - remarks := "" + + var remarks string if len(serverInfo) == 2 { unescape, err := url.QueryUnescape(serverInfo[1]) if err != nil { - return model.Outbound{}, errors.New("invalid ss Url" + err.Error()) + return model.Outbound{}, &ParseError{ + Type: ErrInvalidStruct, + Message: "cannot unescape remarks", + Raw: proxy, + } } remarks = strings.TrimSpace(unescape) } else { - remarks = strings.TrimSpace(serverAndPort[0]) + remarks = strings.TrimSpace(server + ":" + portStr) } - method := credentials[0] - password := credentials[1] - server := strings.TrimSpace(serverAndPort[0]) + result := model.Outbound{ Type: "shadowsocks", Tag: remarks, ShadowsocksOptions: model.ShadowsocksOutboundOptions{ ServerOptions: model.ServerOptions{ Server: server, - ServerPort: uint16(port), + ServerPort: port, }, Method: method, Password: password, diff --git a/parser/trojan.go b/parser/trojan.go index d838dd2..dc09e97 100644 --- a/parser/trojan.go +++ b/parser/trojan.go @@ -1,118 +1,155 @@ package parser import ( - "errors" "net/url" - "strconv" "strings" + "sub2sing-box/constant" "sub2sing-box/model" ) func ParseTrojan(proxy string) (model.Outbound, error) { - if !strings.HasPrefix(proxy, "trojan://") { - return model.Outbound{}, errors.New("invalid trojan Url") + if !strings.HasPrefix(proxy, constant.TrojanPrefix) { + return model.Outbound{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy} } - parts := strings.SplitN(strings.TrimPrefix(proxy, "trojan://"), "@", 2) - if len(parts) != 2 { - return model.Outbound{}, errors.New("invalid trojan Url") + + proxy = strings.TrimPrefix(proxy, constant.TrojanPrefix) + urlParts := strings.SplitN(proxy, "@", 2) + if len(urlParts) != 2 { + return model.Outbound{}, &ParseError{ + Type: ErrInvalidStruct, + Message: "missing character '@' in url", + Raw: proxy, + } } - serverInfo := strings.SplitN(parts[1], "#", 2) + password := strings.TrimSpace(urlParts[0]) + + serverInfo := strings.SplitN(urlParts[1], "#", 2) serverAndPortAndParams := strings.SplitN(serverInfo[0], "?", 2) + if len(serverAndPortAndParams) != 2 { + return model.Outbound{}, &ParseError{ + Type: ErrInvalidStruct, + Message: "missing character '?' in url", + Raw: proxy, + } + } + serverAndPort := strings.SplitN(serverAndPortAndParams[0], ":", 2) + if len(serverAndPort) != 2 { + return model.Outbound{}, &ParseError{ + Type: ErrInvalidStruct, + Message: "missing server host or port", + Raw: proxy, + } + } + server, portStr := serverAndPort[0], serverAndPort[1] + params, err := url.ParseQuery(serverAndPortAndParams[1]) if err != nil { - return model.Outbound{}, err + return model.Outbound{}, &ParseError{ + Type: ErrCannotParseParams, + Raw: proxy, + Message: err.Error(), + } } - if len(serverAndPort) != 2 { - return model.Outbound{}, errors.New("invalid trojan Url") - } - port, err := strconv.Atoi(strings.TrimSpace(serverAndPort[1])) + + port, err := ParsePort(portStr) if err != nil { return model.Outbound{}, err } + remarks := "" if len(serverInfo) == 2 { remarks, _ = url.QueryUnescape(strings.TrimSpace(serverInfo[1])) } else { remarks = serverAndPort[0] } - server := strings.TrimSpace(serverAndPort[0]) - password := strings.TrimSpace(parts[0]) + + network, security, alpnStr, sni, pbk, sid, fp, path, host, serviceName := params.Get("type"), params.Get("security"), params.Get("alpn"), params.Get("sni"), params.Get("pbk"), params.Get("sid"), params.Get("fp"), params.Get("path"), params.Get("host"), params.Get("serviceName") + + var alpn []string + if strings.Contains(alpnStr, ",") { + alpn = strings.Split(alpnStr, ",") + } else { + alpn = nil + } + + enableUTLS := fp != "" + result := model.Outbound{ Type: "trojan", Tag: remarks, TrojanOptions: model.TrojanOutboundOptions{ ServerOptions: model.ServerOptions{ Server: server, - ServerPort: uint16(port), + ServerPort: port, }, Password: password, - Network: params.Get("type"), + Network: network, }, } - if params.Get("security") == "xtls" || params.Get("security") == "tls" { - var alpn []string - if strings.Contains(params.Get("alpn"), ",") { - alpn = strings.Split(params.Get("alpn"), ",") - } else { - alpn = nil - } + + if security == "xtls" || security == "tls" { result.TrojanOptions.OutboundTLSOptionsContainer = model.OutboundTLSOptionsContainer{ TLS: &model.OutboundTLSOptions{ Enabled: true, ALPN: alpn, - ServerName: params.Get("sni"), + ServerName: sni, }, } } + if params.Get("security") == "reality" { result.TrojanOptions.OutboundTLSOptionsContainer = model.OutboundTLSOptionsContainer{ TLS: &model.OutboundTLSOptions{ Enabled: true, - ServerName: params.Get("sni"), + ServerName: sni, Reality: &model.OutboundRealityOptions{ Enabled: true, - PublicKey: params.Get("pbk"), - ShortID: params.Get("sid"), + PublicKey: pbk, + ShortID: sid, }, UTLS: &model.OutboundUTLSOptions{ - Enabled: params.Get("fp") != "", - Fingerprint: params.Get("fp"), + Enabled: enableUTLS, + Fingerprint: fp, }, }, } } + if params.Get("type") == "ws" { result.TrojanOptions.Transport = &model.V2RayTransportOptions{ Type: "ws", WebsocketOptions: model.V2RayWebsocketOptions{ - Path: params.Get("path"), + Path: path, Headers: map[string]string{ - "Host": params.Get("host"), + "Host": host, }, }, } } + if params.Get("type") == "http" { result.TrojanOptions.Transport = &model.V2RayTransportOptions{ Type: "http", HTTPOptions: model.V2RayHTTPOptions{ - Host: []string{params.Get("host")}, + Host: []string{host}, Path: params.Get("path"), }, } } + if params.Get("type") == "quic" { result.TrojanOptions.Transport = &model.V2RayTransportOptions{ Type: "quic", QUICOptions: model.V2RayQUICOptions{}, } } + if params.Get("type") == "grpc" { result.TrojanOptions.Transport = &model.V2RayTransportOptions{ Type: "grpc", GRPCOptions: model.V2RayGRPCOptions{ - ServiceName: params.Get("serviceName"), + ServiceName: serviceName, }, } } diff --git a/parser/vless.go b/parser/vless.go index 1fc12c6..987de8e 100644 --- a/parser/vless.go +++ b/parser/vless.go @@ -1,35 +1,59 @@ package parser import ( - "errors" "net/url" - "strconv" "strings" + "sub2sing-box/constant" "sub2sing-box/model" ) func ParseVless(proxy string) (model.Outbound, error) { - if !strings.HasPrefix(proxy, "vless://") { - return model.Outbound{}, errors.New("invalid vless Url") + if !strings.HasPrefix(proxy, constant.VLESSPrefix) { + return model.Outbound{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy} } - parts := strings.SplitN(strings.TrimPrefix(proxy, "vless://"), "@", 2) - if len(parts) != 2 { - return model.Outbound{}, errors.New("invalid vless Url") + + urlParts := strings.SplitN(strings.TrimPrefix(proxy, constant.VLESSPrefix), "@", 2) + if len(urlParts) != 2 { + return model.Outbound{}, &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.Outbound{}, &ParseError{ + Type: ErrInvalidStruct, + Message: "missing character '?' in url", + Raw: proxy, + } + } + serverAndPort := strings.SplitN(serverAndPortAndParams[0], ":", 2) + if len(serverAndPort) != 2 { + return model.Outbound{}, &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.Outbound{}, err + } + params, err := url.ParseQuery(serverAndPortAndParams[1]) if err != nil { - return model.Outbound{}, err - } - if len(serverAndPort) != 2 { - return model.Outbound{}, errors.New("invalid vless Url") - } - port, err := strconv.Atoi(strings.TrimSpace(serverAndPort[1])) - if err != nil { - return model.Outbound{}, err + return model.Outbound{}, &ParseError{ + Type: ErrCannotParseParams, + Raw: proxy, + Message: err.Error(), + } } + remarks := "" if len(serverInfo) == 2 { if strings.Contains(serverInfo[1], "|") { @@ -37,92 +61,107 @@ func ParseVless(proxy string) (model.Outbound, error) { } else { remarks, err = url.QueryUnescape(serverInfo[1]) if err != nil { - return model.Outbound{}, err + return model.Outbound{}, &ParseError{ + Type: ErrCannotParseParams, + Raw: proxy, + Message: err.Error(), + } } } } else { - remarks, err = url.QueryUnescape(serverAndPort[0]) + remarks, err = url.QueryUnescape(server) if err != nil { return model.Outbound{}, err } } - server := strings.TrimSpace(serverAndPort[0]) - uuid := strings.TrimSpace(parts[0]) + + uuid := strings.TrimSpace(urlParts[0]) + flow, security, alpnStr, sni, insecure, fp, pbk, sid, path, host, serviceName := 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") + + enableUTLS := fp != "" + insecureBool := insecure == "1" + var alpn []string + if strings.Contains(alpnStr, ",") { + alpn = strings.Split(alpnStr, ",") + } else { + alpn = nil + } + result := model.Outbound{ Type: "vless", Tag: remarks, VLESSOptions: model.VLESSOutboundOptions{ ServerOptions: model.ServerOptions{ Server: server, - ServerPort: uint16(port), + ServerPort: port, }, UUID: uuid, - Flow: params.Get("flow"), + Flow: flow, }, } - if params.Get("security") == "tls" { - var alpn []string - if strings.Contains(params.Get("alpn"), ",") { - alpn = strings.Split(params.Get("alpn"), ",") - } else { - alpn = nil - } + + if security == "tls" { result.VLESSOptions.OutboundTLSOptionsContainer = model.OutboundTLSOptionsContainer{ TLS: &model.OutboundTLSOptions{ Enabled: true, ALPN: alpn, - ServerName: params.Get("sni"), - Insecure: params.Get("allowInsecure") == "1", + ServerName: sni, + Insecure: insecureBool, }, } - if params.Get("fp") != "" { - result.VLESSOptions.OutboundTLSOptionsContainer.TLS.UTLS = &model.OutboundUTLSOptions{ - Enabled: true, - Fingerprint: params.Get("fp"), - } + result.VLESSOptions.OutboundTLSOptionsContainer.TLS.UTLS = &model.OutboundUTLSOptions{ + Enabled: enableUTLS, + Fingerprint: fp, } } - if params.Get("security") == "reality" { + + if security == "reality" { result.VLESSOptions.OutboundTLSOptionsContainer.TLS.Reality = &model.OutboundRealityOptions{ Enabled: true, - PublicKey: params.Get("pbk"), - ShortID: params.Get("sid"), + PublicKey: pbk, + ShortID: sid, } } + if params.Get("type") == "ws" { result.VLESSOptions.Transport = &model.V2RayTransportOptions{ Type: "ws", WebsocketOptions: model.V2RayWebsocketOptions{ - Path: params.Get("path"), + Path: path, }, } - if params.Get("host") != "" { - result.VLESSOptions.Transport.WebsocketOptions.Headers["Host"] = params.Get("host") - } + result.VLESSOptions.Transport.WebsocketOptions.Headers["Host"] = host } + if params.Get("type") == "quic" { result.VLESSOptions.Transport = &model.V2RayTransportOptions{ Type: "quic", QUICOptions: model.V2RayQUICOptions{}, } } + if params.Get("type") == "grpc" { result.VLESSOptions.Transport = &model.V2RayTransportOptions{ Type: "grpc", GRPCOptions: model.V2RayGRPCOptions{ - ServiceName: params.Get("serviceName"), + ServiceName: serviceName, }, } } + if params.Get("type") == "http" { - host, err := url.QueryUnescape(params.Get("host")) + hosts, err := url.QueryUnescape(host) if err != nil { - return model.Outbound{}, err + return model.Outbound{}, &ParseError{ + Type: ErrCannotParseParams, + Raw: proxy, + Message: err.Error(), + } } result.VLESSOptions.Transport = &model.V2RayTransportOptions{ Type: "http", HTTPOptions: model.V2RayHTTPOptions{ - Host: strings.Split(host, ","), + Host: strings.Split(hosts, ","), }, } } diff --git a/parser/vmess.go b/parser/vmess.go index 262d871..a290fd6 100644 --- a/parser/vmess.go +++ b/parser/vmess.go @@ -2,47 +2,53 @@ package parser import ( "encoding/json" - "errors" "net/url" "strconv" "strings" + "sub2sing-box/constant" "sub2sing-box/model" "sub2sing-box/util" ) func ParseVmess(proxy string) (model.Outbound, error) { - if !strings.HasPrefix(proxy, "vmess://") { - return model.Outbound{}, errors.New("invalid vmess url") + if !strings.HasPrefix(proxy, constant.VMessPrefix) { + return model.Outbound{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy} } - base64, err := util.DecodeBase64(strings.TrimPrefix(proxy, "vmess://")) + + proxy = strings.TrimPrefix(proxy, constant.VMessPrefix) + base64, err := util.DecodeBase64(proxy) if err != nil { - return model.Outbound{}, errors.New("invalid vmess url" + err.Error()) + return model.Outbound{}, &ParseError{Type: ErrInvalidBase64, Raw: proxy, Message: err.Error()} } + var vmess model.VmessJson err = json.Unmarshal([]byte(base64), &vmess) if err != nil { - return model.Outbound{}, errors.New("invalid vmess url" + err.Error()) + return model.Outbound{}, &ParseError{Type: ErrInvalidStruct, Raw: proxy, Message: err.Error()} } - port := 0 + + var port uint16 switch vmess.Port.(type) { case string: - port, err = strconv.Atoi(vmess.Port.(string)) + port, err = ParsePort(vmess.Port.(string)) if err != nil { - return model.Outbound{}, errors.New("invalid vmess url" + err.Error()) + return model.Outbound{}, err } case float64: - port = int(vmess.Port.(float64)) + port = uint16(vmess.Port.(float64)) } + aid := 0 switch vmess.Aid.(type) { case string: aid, err = strconv.Atoi(vmess.Aid.(string)) if err != nil { - return model.Outbound{}, errors.New("invalid vmess url" + err.Error()) + return model.Outbound{}, &ParseError{Type: ErrInvalidStruct, Raw: proxy, Message: err.Error()} } case float64: aid = int(vmess.Aid.(float64)) } + if vmess.Scy == "" { vmess.Scy = "auto" } @@ -58,7 +64,7 @@ func ParseVmess(proxy string) (model.Outbound, error) { VMessOptions: model.VMessOutboundOptions{ ServerOptions: model.ServerOptions{ Server: vmess.Add, - ServerPort: uint16(port), + ServerPort: port, }, UUID: vmess.Id, AlterId: aid, diff --git a/test/country_test.go b/test/country_test.go new file mode 100644 index 0000000..a6a5fed --- /dev/null +++ b/test/country_test.go @@ -0,0 +1,11 @@ +package model + +import ( + "log" + "sub2sing-box/model" + "testing" +) + +func TestCountry(t *testing.T) { + log.Println(model.GetContryName("US 节点")) +}