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 }