commit c75126e36804dc3a17842d7e92766cccd482b64b Author: nite07 Date: Mon Mar 11 03:13:42 2024 +0800 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e56a728 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.vscode +dist \ No newline at end of file diff --git a/cmd/url.go b/cmd/url.go new file mode 100644 index 0000000..5fd9db7 --- /dev/null +++ b/cmd/url.go @@ -0,0 +1,87 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "sub2sing-box/model" + "sub2sing-box/parser" + . "sub2sing-box/util" + + "github.com/spf13/cobra" +) + +func Url(cmd *cobra.Command, args []string) { + proxyList := make([]model.Proxy, 0) + if cmd.Flag("url").Changed { + urls, _ := cmd.Flags().GetStringSlice("url") + for _, url := range urls { + resp, err := http.Get(url) + if err != nil { + fmt.Println(err) + return + } + data, err := io.ReadAll(resp.Body) + if err != nil { + fmt.Println(err) + return + } + proxy, err := DecodeBase64(string(data)) + if err != nil { + fmt.Println(err) + return + } + proxies := strings.Split(proxy, "\n") + for _, p := range proxies { + if strings.HasPrefix(p, "ss://") { + proxy, err := parser.ParseShadowsocks(p) + if err != nil { + fmt.Println(proxy) + } + proxyList = append(proxyList, proxy) + } else if strings.HasPrefix(p, "vmess://") { + proxy, err := parser.ParseVmess(p) + if err != nil { + fmt.Println(proxy) + } + proxyList = append(proxyList, proxy) + } else if strings.HasPrefix(p, "trojan://") { + proxy, err := parser.ParseTrojan(p) + if err != nil { + fmt.Println(proxy) + } + proxyList = append(proxyList, proxy) + } else if strings.HasPrefix(p, "vless://") { + proxy, err := parser.ParseVless(p) + if err != nil { + fmt.Println(proxy) + } + proxyList = append(proxyList, proxy) + } else if strings.HasPrefix(p, "hysteria://") { + proxy, err := parser.ParseHysteria(p) + if err != nil { + fmt.Println(proxy) + } + proxyList = append(proxyList, proxy) + } else if strings.HasPrefix(p, "hy2://") || strings.HasPrefix(p, "hysteria2://") { + proxy, err := parser.ParseHysteria2(p) + if err != nil { + fmt.Println(proxy) + } + proxyList = append(proxyList, proxy) + } + } + } + result, err := json.Marshal(proxyList) + if err != nil { + fmt.Println(err) + return + } else { + fmt.Println(string(result)) + } + } else { + fmt.Println("No URLs provided") + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..4754fd4 --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module sub2sing-box + +go 1.21.5 + +require github.com/spf13/cobra v1.8.0 + +require ( + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d0e8c2c --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..98cfc41 --- /dev/null +++ b/main.go @@ -0,0 +1,23 @@ +package main + +import ( + "fmt" + "sub2sing-box/cmd" + + "github.com/spf13/cobra" +) + +var rootCmd = &cobra.Command{ + Use: "process", + Run: cmd.Url, +} + +func init() { + rootCmd.Flags().StringSliceP("url", "u", []string{}, "URLs to process") +} + +func main() { + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + } +} diff --git a/model/hysteria.go b/model/hysteria.go new file mode 100644 index 0000000..3e93205 --- /dev/null +++ b/model/hysteria.go @@ -0,0 +1,20 @@ +package model + +type Hysteria struct { + Type string `json:"type"` + Tag string `json:"tag,omitempty"` + Server string `json:"server"` + ServerPort uint16 `json:"server_port"` + Up string `json:"up,omitempty"` + UpMbps int `json:"up_mbps,omitempty"` + Down string `json:"down,omitempty"` + DownMbps int `json:"down_mbps,omitempty"` + Obfs string `json:"obfs,omitempty"` + Auth []byte `json:"auth,omitempty"` + AuthString string `json:"auth_str,omitempty"` + ReceiveWindowConn uint64 `json:"recv_window_conn,omitempty"` + ReceiveWindow uint64 `json:"recv_window,omitempty"` + DisableMTUDiscovery bool `json:"disable_mtu_discovery,omitempty"` + Network string `json:"network,omitempty"` + TLS *OutboundTLSOptions `json:"tls,omitempty"` +} diff --git a/model/hysteria2.go b/model/hysteria2.go new file mode 100644 index 0000000..b57f894 --- /dev/null +++ b/model/hysteria2.go @@ -0,0 +1,46 @@ +package model + +type Hysteria2Obfs struct { + Type string `json:"type,omitempty"` + Password string `json:"password,omitempty"` +} + +type Hysteria2 struct { + Type string `json:"type"` + Tag string `json:"tag,omitempty"` + Server string `json:"server"` + ServerPort uint16 `json:"server_port"` + UpMbps int `json:"up_mbps,omitempty"` + DownMbps int `json:"down_mbps,omitempty"` + Obfs *Hysteria2Obfs `json:"obfs,omitempty"` + Password string `json:"password,omitempty"` + Network string `json:"network,omitempty"` + TLS *OutboundTLSOptions `json:"tls,omitempty"` + BrutalDebug bool `json:"brutal_debug,omitempty"` +} + +// func (h *Hysteria2OutboundOptions) MarshalJSON() ([]byte, error) { +// val := reflect.ValueOf(h) +// out := make(map[string]interface{}) +// typ := val.Type() +// for i := 0; i < val.NumField(); i++ { +// field := val.Field(i) +// fieldType := typ.Field(i) +// if field.Kind() == reflect.Struct { +// for j := 0; j < field.NumField(); j++ { +// subField := field.Field(j) +// subFieldType := fieldType.Type.Field(j) +// jsonTag := subFieldType.Tag.Get("json") +// if jsonTag != "" && jsonTag != "-" { +// out[jsonTag] = subField.Interface() +// } +// } +// } else { +// jsonTag := fieldType.Tag.Get("json") +// if jsonTag != "" && jsonTag != "-" { +// out[jsonTag] = field.Interface() +// } +// } +// } +// return json.Marshal(out) +// } diff --git a/model/multiplex.go b/model/multiplex.go new file mode 100644 index 0000000..e7f6332 --- /dev/null +++ b/model/multiplex.go @@ -0,0 +1,17 @@ +package model + +type OutboundMultiplexOptions struct { + Enabled bool `json:"enabled,omitempty"` + Protocol string `json:"protocol,omitempty"` + MaxConnections int `json:"max_connections,omitempty"` + MinStreams int `json:"min_streams,omitempty"` + MaxStreams int `json:"max_streams,omitempty"` + Padding bool `json:"padding,omitempty"` + Brutal *BrutalOptions `json:"brutal,omitempty"` +} + +type BrutalOptions struct { + Enabled bool `json:"enabled,omitempty"` + UpMbps int `json:"up_mbps,omitempty"` + DownMbps int `json:"down_mbps,omitempty"` +} diff --git a/model/proxy.go b/model/proxy.go new file mode 100644 index 0000000..2418131 --- /dev/null +++ b/model/proxy.go @@ -0,0 +1,79 @@ +package model + +import ( + "encoding/json" +) + +type Proxy struct { + Type string `json:"type"` + Shadowsocks `json:"-"` + VMess `json:"-"` + VLESS `json:"-"` + Trojan `json:"-"` + TUIC `json:"-"` + Hysteria `json:"-"` + Hysteria2 `json:"-"` +} + +func (p *Proxy) MarshalJSON() ([]byte, error) { + switch p.Type { + case "shadowsocks": + return json.Marshal(&struct { + Type string `json:"type"` + Shadowsocks + }{ + Type: p.Type, + Shadowsocks: p.Shadowsocks, + }) + case "vmess": + return json.Marshal(&struct { + Type string `json:"type"` + VMess + }{ + Type: p.Type, + VMess: p.VMess, + }) + case "vless": + return json.Marshal(&struct { + Type string `json:"type"` + VLESS + }{ + Type: p.Type, + VLESS: p.VLESS, + }) + case "trojan": + return json.Marshal(&struct { + Type string `json:"type"` + Trojan + }{ + Type: p.Type, + Trojan: p.Trojan, + }) + case "tuic": + return json.Marshal(&struct { + Type string `json:"type"` + TUIC + }{ + Type: p.Type, + TUIC: p.TUIC, + }) + case "hysteria": + return json.Marshal(&struct { + Type string `json:"type"` + Hysteria + }{ + Type: p.Type, + Hysteria: p.Hysteria, + }) + case "hysteria2": + return json.Marshal(&struct { + Type string `json:"type"` + Hysteria2 + }{ + Type: p.Type, + Hysteria2: p.Hysteria2, + }) + default: + return json.Marshal(p) + } +} diff --git a/model/shadowsocks.go b/model/shadowsocks.go new file mode 100644 index 0000000..6cf6824 --- /dev/null +++ b/model/shadowsocks.go @@ -0,0 +1,16 @@ +package model + +type NetworkList string + +type Shadowsocks struct { + Tag string `json:"tag,omitempty"` + Server string `json:"server"` + ServerPort uint16 `json:"server_port"` + Method string `json:"method"` + Password string `json:"password"` + Plugin string `json:"plugin,omitempty"` + PluginOptions string `json:"plugin_opts,omitempty"` + Network string `json:"network,omitempty"` + UDPOverTCP *UDPOverTCPOptions `json:"udp_over_tcp,omitempty"` + Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"` +} diff --git a/model/tls.go b/model/tls.go new file mode 100644 index 0000000..50e5f78 --- /dev/null +++ b/model/tls.go @@ -0,0 +1,36 @@ +package model + +type OutboundTLSOptions struct { + Enabled bool `json:"enabled,omitempty"` + DisableSNI bool `json:"disable_sni,omitempty"` + ServerName string `json:"server_name,omitempty"` + Insecure bool `json:"insecure,omitempty"` + ALPN []string `json:"alpn,omitempty"` + MinVersion string `json:"min_version,omitempty"` + MaxVersion string `json:"max_version,omitempty"` + CipherSuites []string `json:"cipher_suites,omitempty"` + Certificate []string `json:"certificate,omitempty"` + CertificatePath string `json:"certificate_path,omitempty"` + ECH *OutboundECHOptions `json:"ech,omitempty"` + UTLS *OutboundUTLSOptions `json:"utls,omitempty"` + Reality *OutboundRealityOptions `json:"reality,omitempty"` +} + +type OutboundECHOptions struct { + Enabled bool `json:"enabled,omitempty"` + PQSignatureSchemesEnabled bool `json:"pq_signature_schemes_enabled,omitempty"` + DynamicRecordSizingDisabled bool `json:"dynamic_record_sizing_disabled,omitempty"` + Config []string `json:"config,omitempty"` + ConfigPath string `json:"config_path,omitempty"` +} + +type OutboundUTLSOptions struct { + Enabled bool `json:"enabled,omitempty"` + Fingerprint string `json:"fingerprint,omitempty"` +} + +type OutboundRealityOptions struct { + Enabled bool `json:"enabled,omitempty"` + PublicKey string `json:"public_key,omitempty"` + ShortID string `json:"short_id,omitempty"` +} diff --git a/model/trojan.go b/model/trojan.go new file mode 100644 index 0000000..5571aba --- /dev/null +++ b/model/trojan.go @@ -0,0 +1,13 @@ +package model + +type Trojan struct { + Type string `json:"type"` + Tag string `json:"tag,omitempty"` + Server string `json:"server"` + ServerPort uint16 `json:"server_port"` + Password string `json:"password"` + Network string `json:"network,omitempty"` + TLS *OutboundTLSOptions `json:"tls,omitempty"` + Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"` + Transport *V2RayTransportOptions `json:"transport,omitempty"` +} diff --git a/model/tuic.go b/model/tuic.go new file mode 100644 index 0000000..5fcf8b4 --- /dev/null +++ b/model/tuic.go @@ -0,0 +1,17 @@ +package model + +type TUIC struct { + Type string `json:"type"` + Tag string `json:"tag,omitempty"` + Server string `json:"server"` + ServerPort uint16 `json:"server_port"` + UUID string `json:"uuid,omitempty"` + Password string `json:"password,omitempty"` + CongestionControl string `json:"congestion_control,omitempty"` + UDPRelayMode string `json:"udp_relay_mode,omitempty"` + UDPOverStream bool `json:"udp_over_stream,omitempty"` + ZeroRTTHandshake bool `json:"zero_rtt_handshake,omitempty"` + Heartbeat Duration `json:"heartbeat,omitempty"` + Network string `json:"network,omitempty"` + TLS *OutboundTLSOptions `json:"tls,omitempty"` +} diff --git a/model/types.go b/model/types.go new file mode 100644 index 0000000..6f36009 --- /dev/null +++ b/model/types.go @@ -0,0 +1,5 @@ +package model + +import "time" + +type Duration time.Duration diff --git a/model/udp_over_tcp.go b/model/udp_over_tcp.go new file mode 100644 index 0000000..86dbf6b --- /dev/null +++ b/model/udp_over_tcp.go @@ -0,0 +1,6 @@ +package model + +type UDPOverTCPOptions struct { + Enabled bool `json:"enabled,omitempty"` + Version uint8 `json:"version,omitempty"` +} diff --git a/model/v2ray_transport.go b/model/v2ray_transport.go new file mode 100644 index 0000000..3488e8c --- /dev/null +++ b/model/v2ray_transport.go @@ -0,0 +1,85 @@ +package model + +import ( + "encoding/json" +) + +type V2RayTransportOptions struct { + Type string `json:"type"` + HTTPOptions V2RayHTTPOptions `json:"-"` + WebsocketOptions V2RayWebsocketOptions `json:"-"` + QUICOptions V2RayQUICOptions `json:"-"` + GRPCOptions V2RayGRPCOptions `json:"-"` + HTTPUpgradeOptions V2RayHTTPUpgradeOptions `json:"-"` +} + +func (o *V2RayTransportOptions) MarshalJSON() ([]byte, error) { + switch o.Type { + case "ws": + return json.Marshal(&struct { + Type string `json:"type"` + *V2RayWebsocketOptions + }{ + Type: o.Type, + V2RayWebsocketOptions: &o.WebsocketOptions, + }) + case "quic": + return json.Marshal(&struct { + Type string `json:"type"` + *V2RayQUICOptions + }{ + Type: o.Type, + V2RayQUICOptions: &o.QUICOptions, + }) + case "grpc": + return json.Marshal(&struct { + Type string `json:"type"` + *V2RayGRPCOptions + }{ + Type: o.Type, + V2RayGRPCOptions: &o.GRPCOptions, + }) + case "http": + return json.Marshal(&struct { + Type string `json:"type"` + *V2RayHTTPOptions + }{ + Type: o.Type, + V2RayHTTPOptions: &o.HTTPOptions, + }) + default: + return json.Marshal(&struct{}{}) + } +} + +type V2RayHTTPOptions struct { + Host []string `json:"host,omitempty"` + Path string `json:"path,omitempty"` + Method string `json:"method,omitempty"` + Headers map[string]string `json:"headers,omitempty"` + IdleTimeout Duration `json:"idle_timeout,omitempty"` + PingTimeout Duration `json:"ping_timeout,omitempty"` +} + +type V2RayWebsocketOptions struct { + Path string `json:"path,omitempty"` + Headers map[string]string `json:"headers,omitempty"` + MaxEarlyData uint32 `json:"max_early_data,omitempty"` + EarlyDataHeaderName string `json:"early_data_header_name,omitempty"` +} + +type V2RayQUICOptions struct{} + +type V2RayGRPCOptions struct { + ServiceName string `json:"service_name,omitempty"` + IdleTimeout Duration `json:"idle_timeout,omitempty"` + PingTimeout Duration `json:"ping_timeout,omitempty"` + PermitWithoutStream bool `json:"permit_without_stream,omitempty"` + ForceLite bool `json:"-"` // for test +} + +type V2RayHTTPUpgradeOptions struct { + Host string `json:"host,omitempty"` + Path string `json:"path,omitempty"` + Headers map[string]string `json:"headers,omitempty"` +} diff --git a/model/vless.go b/model/vless.go new file mode 100644 index 0000000..95b825a --- /dev/null +++ b/model/vless.go @@ -0,0 +1,15 @@ +package model + +type VLESS struct { + Type string `json:"type"` + Tag string `json:"tag,omitempty"` + Server string `json:"server"` + ServerPort uint16 `json:"server_port"` + UUID string `json:"uuid"` + Flow string `json:"flow,omitempty"` + Network string `json:"network,omitempty"` + TLS *OutboundTLSOptions `json:"tls,omitempty"` + Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"` + Transport *V2RayTransportOptions `json:"transport,omitempty"` + PacketEncoding *string `json:"packet_encoding,omitempty"` +} diff --git a/model/vmess.go b/model/vmess.go new file mode 100644 index 0000000..a8f0b1e --- /dev/null +++ b/model/vmess.go @@ -0,0 +1,36 @@ +package model + +type VmessJson struct { + V string `json:"v"` + Ps string `json:"ps"` + Add string `json:"add"` + Port interface{} `json:"port"` + Id string `json:"id"` + Aid interface{} `json:"aid"` + Scy string `json:"scy"` + Net string `json:"net"` + Type string `json:"type"` + Host string `json:"host"` + Path string `json:"path"` + Tls string `json:"tls"` + Sni string `json:"sni"` + Alpn string `json:"alpn"` + Fp string `json:"fp"` +} + +type VMess struct { + Type string `json:"type"` + Tag string `json:"tag,omitempty"` + Server string `json:"server"` + ServerPort uint16 `json:"server_port"` + UUID string `json:"uuid"` + Security string `json:"security"` + AlterId int `json:"alter_id,omitempty"` + GlobalPadding bool `json:"global_padding,omitempty"` + AuthenticatedLength bool `json:"authenticated_length,omitempty"` + Network string `json:"network,omitempty"` + TLS *OutboundTLSOptions `json:"tls,omitempty"` + PacketEncoding string `json:"packet_encoding,omitempty"` + Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"` + Transport *V2RayTransportOptions `json:"transport,omitempty"` +} diff --git a/parser/hysteria.go b/parser/hysteria.go new file mode 100644 index 0000000..d270bcf --- /dev/null +++ b/parser/hysteria.go @@ -0,0 +1,83 @@ +package parser + +import ( + "errors" + "net/url" + "strconv" + "strings" + "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.Proxy, error) { + if !strings.HasPrefix(proxy, "hysteria://") { + return model.Proxy{}, errors.New("invalid hysteria Url") + } + parts := strings.SplitN(strings.TrimPrefix(proxy, "hysteria://"), "?", 2) + serverInfo := strings.SplitN(parts[0], ":", 2) + if len(serverInfo) != 2 { + return model.Proxy{}, errors.New("invalid hysteria Url") + } + params, err := url.ParseQuery(parts[1]) + if err != nil { + return model.Proxy{}, errors.New("invalid hysteria Url") + } + host := serverInfo[0] + port, err := strconv.Atoi(serverInfo[1]) + if err != nil { + return model.Proxy{}, 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") + 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] + } + insecureBool, err := strconv.ParseBool(insecure) + if err != nil { + return model.Proxy{}, errors.New("invalid hysteria Url") + } + result := model.Proxy{ + Type: "hysteria", + Hysteria: model.Hysteria{ + Tag: remarks, + Server: host, + ServerPort: uint16(port), + Up: upmbps, + Down: downmbps, + Auth: []byte(auth), + Obfs: obfs, + Network: protocol, + TLS: &model.OutboundTLSOptions{ + Enabled: true, + Insecure: insecureBool, + ALPN: strings.Split(alpn, ","), + }, + }, + } + return result, nil +} diff --git a/parser/hysteria2.go b/parser/hysteria2.go new file mode 100644 index 0000000..1c0a7fb --- /dev/null +++ b/parser/hysteria2.go @@ -0,0 +1,62 @@ +package parser + +import ( + "errors" + "net/url" + "strconv" + "strings" + "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.Proxy, error) { + if !strings.HasPrefix(proxy, "hysteria2://") && !strings.HasPrefix(proxy, "hy2://") { + return model.Proxy{}, errors.New("invalid hysteria2 Url") + } + parts := strings.SplitN(strings.TrimPrefix(proxy, "hysteria2://"), "@", 2) + serverInfo := strings.SplitN(parts[1], "/?", 2) + serverAndPort := strings.SplitN(serverInfo[0], ":", 2) + 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") + } + remarks := params.Get("name") + certificate := make([]string, 0) + if params.Get("pinSHA256") != "" { + certificate = append(certificate, params.Get("pinSHA256")) + } + server := serverAndPort[0] + password := parts[0] + network := params.Get("network") + result := model.Proxy{ + Type: "hysteria2", + Hysteria2: model.Hysteria2{ + Tag: remarks, + Server: server, + ServerPort: uint16(port), + Password: password, + Obfs: &model.Hysteria2Obfs{ + Type: params.Get("obfs"), + Password: params.Get("obfs-password"), + }, + TLS: &model.OutboundTLSOptions{ + Enabled: params.Get("pinSHA256") != "", + Insecure: params.Get("insecure") == "1", + ServerName: params.Get("sni"), + Certificate: certificate, + }, + Network: network, + }, + } + return result, nil +} diff --git a/parser/shadowsocks.go b/parser/shadowsocks.go new file mode 100644 index 0000000..e43a476 --- /dev/null +++ b/parser/shadowsocks.go @@ -0,0 +1,64 @@ +package parser + +import ( + "errors" + "net/url" + "strconv" + "strings" + "sub2sing-box/model" + . "sub2sing-box/util" +) + +func ParseShadowsocks(proxy string) (model.Proxy, error) { + 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()) + } + remarks := "" + if len(serverInfo) == 2 { + unescape, err := url.QueryUnescape(serverInfo[1]) + if err != nil { + return model.Proxy{}, errors.New("invalid ss Url" + err.Error()) + } + remarks = strings.TrimSpace(unescape) + } else { + remarks = strings.TrimSpace(serverAndPort[0]) + } + method := credentials[0] + password := credentials[1] + server := strings.TrimSpace(serverAndPort[0]) + result := model.Proxy{ + Type: "shadowsocks", + Shadowsocks: model.Shadowsocks{ + Tag: remarks, + Method: method, + Password: password, + Server: server, + ServerPort: uint16(port), + }, + } + return result, nil +} diff --git a/parser/trojan.go b/parser/trojan.go new file mode 100644 index 0000000..fdbfe0f --- /dev/null +++ b/parser/trojan.go @@ -0,0 +1,56 @@ +package parser + +import ( + "fmt" + "net/url" + "strconv" + "strings" + "sub2sing-box/model" +) + +func ParseTrojan(proxy string) (model.Proxy, error) { + if !strings.HasPrefix(proxy, "trojan://") { + return model.Proxy{}, fmt.Errorf("invalid trojan Url") + } + parts := strings.SplitN(strings.TrimPrefix(proxy, "trojan://"), "@", 2) + if len(parts) != 2 { + return model.Proxy{}, fmt.Errorf("invalid trojan Url") + } + serverInfo := strings.SplitN(parts[1], "#", 2) + serverAndPortAndParams := strings.SplitN(serverInfo[0], "?", 2) + serverAndPort := strings.SplitN(serverAndPortAndParams[0], ":", 2) + params, err := url.ParseQuery(serverAndPortAndParams[1]) + if err != nil { + return model.Proxy{}, err + } + if len(serverAndPort) != 2 { + return model.Proxy{}, fmt.Errorf("invalid trojan") + } + port, err := strconv.Atoi(strings.TrimSpace(serverAndPort[1])) + if err != nil { + return model.Proxy{}, 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]) + result := model.Proxy{ + Type: "trojan", + Trojan: model.Trojan{ + Tag: remarks, + Server: server, + ServerPort: uint16(port), + TLS: &model.OutboundTLSOptions{ + Enabled: true, + ServerName: params.Get("sni"), + }, + Password: password, + Network: "tcp", + }, + } + return result, nil +} diff --git a/parser/vless.go b/parser/vless.go new file mode 100644 index 0000000..f6a5c7a --- /dev/null +++ b/parser/vless.go @@ -0,0 +1,89 @@ +package parser + +import ( + "fmt" + "net/url" + "strconv" + "strings" + "sub2sing-box/model" +) + +func ParseVless(proxy string) (model.Proxy, error) { + if !strings.HasPrefix(proxy, "vless://") { + return model.Proxy{}, fmt.Errorf("invalid vless Url") + } + parts := strings.SplitN(strings.TrimPrefix(proxy, "vless://"), "@", 2) + if len(parts) != 2 { + return model.Proxy{}, fmt.Errorf("invalid vless Url") + } + serverInfo := strings.SplitN(parts[1], "#", 2) + serverAndPortAndParams := strings.SplitN(serverInfo[0], "?", 2) + serverAndPort := strings.SplitN(serverAndPortAndParams[0], ":", 2) + 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 + } + remarks := "" + if len(serverInfo) == 2 { + if strings.Contains(serverInfo[1], "|") { + remarks = strings.SplitN(serverInfo[1], "|", 2)[1] + } else { + remarks, err = url.QueryUnescape(serverInfo[1]) + if err != nil { + return model.Proxy{}, err + } + } + } else { + remarks, err = url.QueryUnescape(serverAndPort[0]) + if err != nil { + return model.Proxy{}, err + } + } + server := strings.TrimSpace(serverAndPort[0]) + uuid := strings.TrimSpace(parts[0]) + network := params.Get("type") + result := model.Proxy{ + Type: "vless", + VLESS: model.VLESS{ + Tag: remarks, + Server: server, + ServerPort: uint16(port), + UUID: uuid, + Network: network, + TLS: &model.OutboundTLSOptions{ + Enabled: params.Get("security") == "reality", + ServerName: params.Get("sni"), + UTLS: &model.OutboundUTLSOptions{ + Enabled: params.Get("fp") != "", + Fingerprint: params.Get("fp"), + }, + Reality: &model.OutboundRealityOptions{ + Enabled: params.Get("pbk") != "", + PublicKey: params.Get("pbk"), + ShortID: params.Get("sid"), + }, + ALPN: strings.Split(params.Get("alpn"), ","), + }, + Transport: &model.V2RayTransportOptions{ + WebsocketOptions: model.V2RayWebsocketOptions{ + Path: params.Get("path"), + Headers: map[string]string{ + "Host": params.Get("host"), + }, + }, + GRPCOptions: model.V2RayGRPCOptions{ + ServiceName: params.Get("serviceName"), + }, + }, + Flow: params.Get("flow"), + }, + } + return result, nil +} diff --git a/parser/vmess.go b/parser/vmess.go new file mode 100644 index 0000000..d7afde0 --- /dev/null +++ b/parser/vmess.go @@ -0,0 +1,133 @@ +package parser + +import ( + "encoding/json" + "errors" + "net/url" + "strconv" + "strings" + "sub2sing-box/model" + . "sub2sing-box/util" +) + +func ParseVmess(proxy string) (model.Proxy, error) { + if !strings.HasPrefix(proxy, "vmess://") { + return model.Proxy{}, errors.New("invalid vmess url") + } + base64, err := DecodeBase64(strings.TrimPrefix(proxy, "vmess://")) + if err != nil { + return model.Proxy{}, errors.New("invalid vmess url" + 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()) + } + port := 0 + switch vmess.Port.(type) { + case string: + port, err = strconv.Atoi(vmess.Port.(string)) + if err != nil { + return model.Proxy{}, errors.New("invalid vmess url" + err.Error()) + } + case float64: + port = int(vmess.Port.(float64)) + } + 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()) + } + case float64: + aid = int(vmess.Aid.(float64)) + } + if vmess.Scy == "" { + vmess.Scy = "auto" + } + + name, err := url.QueryUnescape(vmess.Ps) + if err != nil { + name = vmess.Ps + } + + result := model.Proxy{ + Type: "vmess", + VMess: model.VMess{ + Tag: name, + Server: vmess.Add, + ServerPort: uint16(port), + UUID: vmess.Id, + AlterId: aid, + Security: vmess.Scy, + Network: vmess.Net, + }, + } + + if vmess.Tls == "tls" { + tls := model.OutboundTLSOptions{ + Enabled: true, + UTLS: &model.OutboundUTLSOptions{ + Fingerprint: vmess.Fp, + }, + ALPN: strings.Split(vmess.Alpn, ","), + } + result.VMess.TLS = &tls + } + + if vmess.Net == "ws" { + if vmess.Path == "" { + vmess.Path = "/" + } + if vmess.Host == "" { + vmess.Host = vmess.Add + } + ws := model.V2RayWebsocketOptions{ + Path: vmess.Path, + Headers: map[string]string{ + "Host": vmess.Host, + }, + } + transport := model.V2RayTransportOptions{ + Type: "ws", + WebsocketOptions: ws, + } + result.VMess.Transport = &transport + } + + if vmess.Net == "quic" { + quic := model.V2RayQUICOptions{} + transport := model.V2RayTransportOptions{ + Type: "quic", + QUICOptions: quic, + } + result.VMess.Transport = &transport + } + + if vmess.Net == "grpc" { + grpc := model.V2RayGRPCOptions{ + ServiceName: vmess.Path, + PermitWithoutStream: true, + } + transport := model.V2RayTransportOptions{ + Type: "grpc", + GRPCOptions: grpc, + } + result.VMess.Transport = &transport + } + + if vmess.Net == "h2" { + httpOps := model.V2RayHTTPOptions{ + Host: strings.Split(vmess.Host, ","), + Path: vmess.Path, + } + transport := model.V2RayTransportOptions{ + Type: "http", + HTTPOptions: httpOps, + } + result.VMess.Transport = &transport + } + + return result, nil +} diff --git a/util/base64.go b/util/base64.go new file mode 100644 index 0000000..d22bfb2 --- /dev/null +++ b/util/base64.go @@ -0,0 +1,18 @@ +package util + +import ( + "encoding/base64" + "strings" +) + +func DecodeBase64(s string) (string, error) { + s = strings.TrimSpace(s) + if len(s)%4 != 0 { + s += strings.Repeat("=", 4-len(s)%4) + } + decodeStr, err := base64.StdEncoding.DecodeString(s) + if err != nil { + return "", err + } + return string(decodeStr), nil +}