github.com/metacubex/mihomo@v1.18.5/common/convert/converter.go (about)

     1  package convert
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/base64"
     6  	"encoding/json"
     7  	"fmt"
     8  	"net/url"
     9  	"strconv"
    10  	"strings"
    11  
    12  	"github.com/metacubex/mihomo/log"
    13  )
    14  
    15  // ConvertsV2Ray convert V2Ray subscribe proxies data to mihomo proxies config
    16  func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
    17  	data := DecodeBase64(buf)
    18  
    19  	arr := strings.Split(string(data), "\n")
    20  
    21  	proxies := make([]map[string]any, 0, len(arr))
    22  	names := make(map[string]int, 200)
    23  
    24  	for _, line := range arr {
    25  		line = strings.TrimRight(line, " \r")
    26  		if line == "" {
    27  			continue
    28  		}
    29  
    30  		scheme, body, found := strings.Cut(line, "://")
    31  		if !found {
    32  			continue
    33  		}
    34  
    35  		scheme = strings.ToLower(scheme)
    36  		switch scheme {
    37  		case "hysteria":
    38  			urlHysteria, err := url.Parse(line)
    39  			if err != nil {
    40  				continue
    41  			}
    42  
    43  			query := urlHysteria.Query()
    44  			name := uniqueName(names, urlHysteria.Fragment)
    45  			hysteria := make(map[string]any, 20)
    46  
    47  			hysteria["name"] = name
    48  			hysteria["type"] = scheme
    49  			hysteria["server"] = urlHysteria.Hostname()
    50  			hysteria["port"] = urlHysteria.Port()
    51  			hysteria["sni"] = query.Get("peer")
    52  			hysteria["obfs"] = query.Get("obfs")
    53  			if alpn := query.Get("alpn"); alpn != "" {
    54  				hysteria["alpn"] = strings.Split(alpn, ",")
    55  			}
    56  			hysteria["auth_str"] = query.Get("auth")
    57  			hysteria["protocol"] = query.Get("protocol")
    58  			up := query.Get("up")
    59  			down := query.Get("down")
    60  			if up == "" {
    61  				up = query.Get("upmbps")
    62  			}
    63  			if down == "" {
    64  				down = query.Get("downmbps")
    65  			}
    66  			hysteria["down"] = down
    67  			hysteria["up"] = up
    68  			hysteria["skip-cert-verify"], _ = strconv.ParseBool(query.Get("insecure"))
    69  
    70  			proxies = append(proxies, hysteria)
    71  
    72  		case "hysteria2", "hy2":
    73  			urlHysteria2, err := url.Parse(line)
    74  			if err != nil {
    75  				continue
    76  			}
    77  
    78  			query := urlHysteria2.Query()
    79  			name := uniqueName(names, urlHysteria2.Fragment)
    80  			hysteria2 := make(map[string]any, 20)
    81  
    82  			hysteria2["name"] = name
    83  			hysteria2["type"] = "hysteria2"
    84  			hysteria2["server"] = urlHysteria2.Hostname()
    85  			if port := urlHysteria2.Port(); port != "" {
    86  				hysteria2["port"] = port
    87  			} else {
    88  				hysteria2["port"] = "443"
    89  			}
    90  			hysteria2["obfs"] = query.Get("obfs")
    91  			hysteria2["obfs-password"] = query.Get("obfs-password")
    92  			hysteria2["sni"] = query.Get("sni")
    93  			hysteria2["skip-cert-verify"], _ = strconv.ParseBool(query.Get("insecure"))
    94  			if alpn := query.Get("alpn"); alpn != "" {
    95  				hysteria2["alpn"] = strings.Split(alpn, ",")
    96  			}
    97  			if auth := urlHysteria2.User.String(); auth != "" {
    98  				hysteria2["password"] = auth
    99  			}
   100  			hysteria2["fingerprint"] = query.Get("pinSHA256")
   101  			hysteria2["down"] = query.Get("down")
   102  			hysteria2["up"] = query.Get("up")
   103  
   104  			proxies = append(proxies, hysteria2)
   105  
   106  		case "tuic":
   107  			// A temporary unofficial TUIC share link standard
   108  			// Modified from https://github.com/daeuniverse/dae/discussions/182
   109  			// Changes:
   110  			//   1. Support TUICv4, just replace uuid:password with token
   111  			//   2. Remove `allow_insecure` field
   112  			urlTUIC, err := url.Parse(line)
   113  			if err != nil {
   114  				continue
   115  			}
   116  			query := urlTUIC.Query()
   117  
   118  			tuic := make(map[string]any, 20)
   119  			tuic["name"] = uniqueName(names, urlTUIC.Fragment)
   120  			tuic["type"] = scheme
   121  			tuic["server"] = urlTUIC.Hostname()
   122  			tuic["port"] = urlTUIC.Port()
   123  			tuic["udp"] = true
   124  			password, v5 := urlTUIC.User.Password()
   125  			if v5 {
   126  				tuic["uuid"] = urlTUIC.User.Username()
   127  				tuic["password"] = password
   128  			} else {
   129  				tuic["token"] = urlTUIC.User.Username()
   130  			}
   131  			if cc := query.Get("congestion_control"); cc != "" {
   132  				tuic["congestion-controller"] = cc
   133  			}
   134  			if alpn := query.Get("alpn"); alpn != "" {
   135  				tuic["alpn"] = strings.Split(alpn, ",")
   136  			}
   137  			if sni := query.Get("sni"); sni != "" {
   138  				tuic["sni"] = sni
   139  			}
   140  			if query.Get("disable_sni") == "1" {
   141  				tuic["disable-sni"] = true
   142  			}
   143  			if udpRelayMode := query.Get("udp_relay_mode"); udpRelayMode != "" {
   144  				tuic["udp-relay-mode"] = udpRelayMode
   145  			}
   146  
   147  			proxies = append(proxies, tuic)
   148  
   149  		case "trojan":
   150  			urlTrojan, err := url.Parse(line)
   151  			if err != nil {
   152  				continue
   153  			}
   154  
   155  			query := urlTrojan.Query()
   156  
   157  			name := uniqueName(names, urlTrojan.Fragment)
   158  			trojan := make(map[string]any, 20)
   159  
   160  			trojan["name"] = name
   161  			trojan["type"] = scheme
   162  			trojan["server"] = urlTrojan.Hostname()
   163  			trojan["port"] = urlTrojan.Port()
   164  			trojan["password"] = urlTrojan.User.Username()
   165  			trojan["udp"] = true
   166  			trojan["skip-cert-verify"], _ = strconv.ParseBool(query.Get("allowInsecure"))
   167  
   168  			if sni := query.Get("sni"); sni != "" {
   169  				trojan["sni"] = sni
   170  			}
   171  			if alpn := query.Get("alpn"); alpn != "" {
   172  				trojan["alpn"] = strings.Split(alpn, ",")
   173  			}
   174  
   175  			network := strings.ToLower(query.Get("type"))
   176  			if network != "" {
   177  				trojan["network"] = network
   178  			}
   179  
   180  			switch network {
   181  			case "ws":
   182  				headers := make(map[string]any)
   183  				wsOpts := make(map[string]any)
   184  
   185  				headers["User-Agent"] = RandUserAgent()
   186  
   187  				wsOpts["path"] = query.Get("path")
   188  				wsOpts["headers"] = headers
   189  
   190  				trojan["ws-opts"] = wsOpts
   191  
   192  			case "grpc":
   193  				grpcOpts := make(map[string]any)
   194  				grpcOpts["grpc-service-name"] = query.Get("serviceName")
   195  				trojan["grpc-opts"] = grpcOpts
   196  			}
   197  
   198  			if fingerprint := query.Get("fp"); fingerprint == "" {
   199  				trojan["client-fingerprint"] = "chrome"
   200  			} else {
   201  				trojan["client-fingerprint"] = fingerprint
   202  			}
   203  
   204  			proxies = append(proxies, trojan)
   205  
   206  		case "vless":
   207  			urlVLess, err := url.Parse(line)
   208  			if err != nil {
   209  				continue
   210  			}
   211  			query := urlVLess.Query()
   212  			vless := make(map[string]any, 20)
   213  			err = handleVShareLink(names, urlVLess, scheme, vless)
   214  			if err != nil {
   215  				log.Warnln("error:%s line:%s", err.Error(), line)
   216  				continue
   217  			}
   218  			if flow := query.Get("flow"); flow != "" {
   219  				vless["flow"] = strings.ToLower(flow)
   220  			}
   221  			proxies = append(proxies, vless)
   222  
   223  		case "vmess":
   224  			// V2RayN-styled share link
   225  			// https://github.com/2dust/v2rayN/wiki/%E5%88%86%E4%BA%AB%E9%93%BE%E6%8E%A5%E6%A0%BC%E5%BC%8F%E8%AF%B4%E6%98%8E(ver-2)
   226  			dcBuf, err := tryDecodeBase64([]byte(body))
   227  			if err != nil {
   228  				// Xray VMessAEAD share link
   229  				urlVMess, err := url.Parse(line)
   230  				if err != nil {
   231  					continue
   232  				}
   233  				query := urlVMess.Query()
   234  				vmess := make(map[string]any, 20)
   235  				err = handleVShareLink(names, urlVMess, scheme, vmess)
   236  				if err != nil {
   237  					log.Warnln("error:%s line:%s", err.Error(), line)
   238  					continue
   239  				}
   240  				vmess["alterId"] = 0
   241  				vmess["cipher"] = "auto"
   242  				if encryption := query.Get("encryption"); encryption != "" {
   243  					vmess["cipher"] = encryption
   244  				}
   245  				proxies = append(proxies, vmess)
   246  				continue
   247  			}
   248  
   249  			jsonDc := json.NewDecoder(bytes.NewReader(dcBuf))
   250  			values := make(map[string]any, 20)
   251  
   252  			if jsonDc.Decode(&values) != nil {
   253  				continue
   254  			}
   255  			tempName, ok := values["ps"].(string)
   256  			if !ok {
   257  				continue
   258  			}
   259  			name := uniqueName(names, tempName)
   260  			vmess := make(map[string]any, 20)
   261  
   262  			vmess["name"] = name
   263  			vmess["type"] = scheme
   264  			vmess["server"] = values["add"]
   265  			vmess["port"] = values["port"]
   266  			vmess["uuid"] = values["id"]
   267  			if alterId, ok := values["aid"]; ok {
   268  				vmess["alterId"] = alterId
   269  			} else {
   270  				vmess["alterId"] = 0
   271  			}
   272  			vmess["udp"] = true
   273  			vmess["xudp"] = true
   274  			vmess["tls"] = false
   275  			vmess["skip-cert-verify"] = false
   276  
   277  			vmess["cipher"] = "auto"
   278  			if cipher, ok := values["scy"]; ok && cipher != "" {
   279  				vmess["cipher"] = cipher
   280  			}
   281  
   282  			if sni, ok := values["sni"]; ok && sni != "" {
   283  				vmess["servername"] = sni
   284  			}
   285  
   286  			network, _ := values["net"].(string)
   287  			network = strings.ToLower(network)
   288  			if values["type"] == "http" {
   289  				network = "http"
   290  			} else if network == "http" {
   291  				network = "h2"
   292  			}
   293  			vmess["network"] = network
   294  
   295  			tls, ok := values["tls"].(string)
   296  			if ok {
   297  				tls = strings.ToLower(tls)
   298  				if strings.HasSuffix(tls, "tls") {
   299  					vmess["tls"] = true
   300  				}
   301  				if alpn, ok := values["alpn"].(string); ok {
   302  					vmess["alpn"] = strings.Split(alpn, ",")
   303  				}
   304  			}
   305  
   306  			switch network {
   307  			case "http":
   308  				headers := make(map[string]any)
   309  				httpOpts := make(map[string]any)
   310  				if host, ok := values["host"]; ok && host != "" {
   311  					headers["Host"] = []string{host.(string)}
   312  				}
   313  				httpOpts["path"] = []string{"/"}
   314  				if path, ok := values["path"]; ok && path != "" {
   315  					httpOpts["path"] = []string{path.(string)}
   316  				}
   317  				httpOpts["headers"] = headers
   318  
   319  				vmess["http-opts"] = httpOpts
   320  
   321  			case "h2":
   322  				headers := make(map[string]any)
   323  				h2Opts := make(map[string]any)
   324  				if host, ok := values["host"]; ok && host != "" {
   325  					headers["Host"] = []string{host.(string)}
   326  				}
   327  
   328  				h2Opts["path"] = values["path"]
   329  				h2Opts["headers"] = headers
   330  
   331  				vmess["h2-opts"] = h2Opts
   332  
   333  			case "ws", "httpupgrade":
   334  				headers := make(map[string]any)
   335  				wsOpts := make(map[string]any)
   336  				wsOpts["path"] = []string{"/"}
   337  				if host, ok := values["host"]; ok && host != "" {
   338  					headers["Host"] = host.(string)
   339  				}
   340  				if path, ok := values["path"]; ok && path != "" {
   341  					path := path.(string)
   342  					pathURL, err := url.Parse(path)
   343  					if err == nil {
   344  						query := pathURL.Query()
   345  						if earlyData := query.Get("ed"); earlyData != "" {
   346  							med, err := strconv.Atoi(earlyData)
   347  							if err == nil {
   348  								switch network {
   349  								case "ws":
   350  									wsOpts["max-early-data"] = med
   351  									wsOpts["early-data-header-name"] = "Sec-WebSocket-Protocol"
   352  								case "httpupgrade":
   353  									wsOpts["v2ray-http-upgrade-fast-open"] = true
   354  								}
   355  								query.Del("ed")
   356  								pathURL.RawQuery = query.Encode()
   357  								path = pathURL.String()
   358  							}
   359  						}
   360  						if earlyDataHeader := query.Get("eh"); earlyDataHeader != "" {
   361  							wsOpts["early-data-header-name"] = earlyDataHeader
   362  						}
   363  					}
   364  					wsOpts["path"] = path
   365  				}
   366  				wsOpts["headers"] = headers
   367  				vmess["ws-opts"] = wsOpts
   368  
   369  			case "grpc":
   370  				grpcOpts := make(map[string]any)
   371  				grpcOpts["grpc-service-name"] = values["path"]
   372  				vmess["grpc-opts"] = grpcOpts
   373  			}
   374  
   375  			proxies = append(proxies, vmess)
   376  
   377  		case "ss":
   378  			urlSS, err := url.Parse(line)
   379  			if err != nil {
   380  				continue
   381  			}
   382  
   383  			name := uniqueName(names, urlSS.Fragment)
   384  			port := urlSS.Port()
   385  
   386  			if port == "" {
   387  				dcBuf, err := encRaw.DecodeString(urlSS.Host)
   388  				if err != nil {
   389  					continue
   390  				}
   391  
   392  				urlSS, err = url.Parse("ss://" + string(dcBuf))
   393  				if err != nil {
   394  					continue
   395  				}
   396  			}
   397  
   398  			var (
   399  				cipherRaw = urlSS.User.Username()
   400  				cipher    string
   401  				password  string
   402  			)
   403  			cipher = cipherRaw
   404  			if password, found = urlSS.User.Password(); !found {
   405  				dcBuf, err := base64.RawURLEncoding.DecodeString(cipherRaw)
   406  				if err != nil {
   407  					dcBuf, _ = enc.DecodeString(cipherRaw)
   408  				}
   409  				cipher, password, found = strings.Cut(string(dcBuf), ":")
   410  				if !found {
   411  					continue
   412  				}
   413  				err = VerifyMethod(cipher, password)
   414  				if err != nil {
   415  					dcBuf, _ = encRaw.DecodeString(cipherRaw)
   416  					cipher, password, found = strings.Cut(string(dcBuf), ":")
   417  				}
   418  			}
   419  
   420  			ss := make(map[string]any, 10)
   421  
   422  			ss["name"] = name
   423  			ss["type"] = scheme
   424  			ss["server"] = urlSS.Hostname()
   425  			ss["port"] = urlSS.Port()
   426  			ss["cipher"] = cipher
   427  			ss["password"] = password
   428  			query := urlSS.Query()
   429  			ss["udp"] = true
   430  			if query.Get("udp-over-tcp") == "true" || query.Get("uot") == "1" {
   431  				ss["udp-over-tcp"] = true
   432  			}
   433  			plugin := query.Get("plugin")
   434  			if strings.Contains(plugin, ";") {
   435  				pluginInfo, _ := url.ParseQuery("pluginName=" + strings.ReplaceAll(plugin, ";", "&"))
   436  				pluginName := pluginInfo.Get("pluginName")
   437  				if strings.Contains(pluginName, "obfs") {
   438  					ss["plugin"] = "obfs"
   439  					ss["plugin-opts"] = map[string]any{
   440  						"mode": pluginInfo.Get("obfs"),
   441  						"host": pluginInfo.Get("obfs-host"),
   442  					}
   443  				} else if strings.Contains(pluginName, "v2ray-plugin") {
   444  					ss["plugin"] = "v2ray-plugin"
   445  					ss["plugin-opts"] = map[string]any{
   446  						"mode": pluginInfo.Get("mode"),
   447  						"host": pluginInfo.Get("host"),
   448  						"path": pluginInfo.Get("path"),
   449  						"tls":  strings.Contains(plugin, "tls"),
   450  					}
   451  				}
   452  			}
   453  
   454  			proxies = append(proxies, ss)
   455  
   456  		case "ssr":
   457  			dcBuf, err := encRaw.DecodeString(body)
   458  			if err != nil {
   459  				continue
   460  			}
   461  
   462  			// ssr://host:port:protocol:method:obfs:urlsafebase64pass/?obfsparam=urlsafebase64&protoparam=&remarks=urlsafebase64&group=urlsafebase64&udpport=0&uot=1
   463  
   464  			before, after, ok := strings.Cut(string(dcBuf), "/?")
   465  			if !ok {
   466  				continue
   467  			}
   468  
   469  			beforeArr := strings.Split(before, ":")
   470  
   471  			if len(beforeArr) != 6 {
   472  				continue
   473  			}
   474  
   475  			host := beforeArr[0]
   476  			port := beforeArr[1]
   477  			protocol := beforeArr[2]
   478  			method := beforeArr[3]
   479  			obfs := beforeArr[4]
   480  			password := decodeUrlSafe(urlSafe(beforeArr[5]))
   481  
   482  			query, err := url.ParseQuery(urlSafe(after))
   483  			if err != nil {
   484  				continue
   485  			}
   486  
   487  			remarks := decodeUrlSafe(query.Get("remarks"))
   488  			name := uniqueName(names, remarks)
   489  
   490  			obfsParam := decodeUrlSafe(query.Get("obfsparam"))
   491  			protocolParam := query.Get("protoparam")
   492  
   493  			ssr := make(map[string]any, 20)
   494  
   495  			ssr["name"] = name
   496  			ssr["type"] = scheme
   497  			ssr["server"] = host
   498  			ssr["port"] = port
   499  			ssr["cipher"] = method
   500  			ssr["password"] = password
   501  			ssr["obfs"] = obfs
   502  			ssr["protocol"] = protocol
   503  			ssr["udp"] = true
   504  
   505  			if obfsParam != "" {
   506  				ssr["obfs-param"] = obfsParam
   507  			}
   508  
   509  			if protocolParam != "" {
   510  				ssr["protocol-param"] = protocolParam
   511  			}
   512  
   513  			proxies = append(proxies, ssr)
   514  		}
   515  	}
   516  
   517  	if len(proxies) == 0 {
   518  		return nil, fmt.Errorf("convert v2ray subscribe error: format invalid")
   519  	}
   520  
   521  	return proxies, nil
   522  }
   523  
   524  func uniqueName(names map[string]int, name string) string {
   525  	if index, ok := names[name]; ok {
   526  		index++
   527  		names[name] = index
   528  		name = fmt.Sprintf("%s-%02d", name, index)
   529  	} else {
   530  		index = 0
   531  		names[name] = index
   532  	}
   533  	return name
   534  }