fix(bridge): normalize legacy vmess links
This commit is contained in:
@@ -6,41 +6,11 @@ package main
|
||||
import "C"
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
"strings"
|
||||
"unsafe"
|
||||
|
||||
"github.com/metacubex/mihomo/common/convert"
|
||||
)
|
||||
|
||||
// preprocessSubscription fixes URL encoding issues in subscription links
|
||||
// Decodes the entire URL line to ensure Mihomo parser receives properly unencoded links
|
||||
func preprocessSubscription(subscription string) string {
|
||||
lines := strings.Split(subscription, "\n")
|
||||
var result []string
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimRight(line, " \r")
|
||||
if line == "" {
|
||||
result = append(result, line)
|
||||
continue
|
||||
}
|
||||
|
||||
// Decode the entire URL line
|
||||
// This fixes issues like v2rayN's uuid%3Apassword encoding
|
||||
// Safe for all protocols: url.QueryUnescape only decodes %XX patterns
|
||||
// and leaves structural characters (://, @, ?, #) intact
|
||||
if decoded, err := url.QueryUnescape(line); err == nil {
|
||||
line = decoded
|
||||
}
|
||||
// If decoding fails (malformed %), keep original line
|
||||
|
||||
result = append(result, line)
|
||||
}
|
||||
|
||||
return strings.Join(result, "\n")
|
||||
}
|
||||
|
||||
// ConvertSubscription converts V2Ray subscription links to mihomo proxy configs
|
||||
//
|
||||
//export ConvertSubscription
|
||||
|
||||
181
bridge/preprocess.go
Normal file
181
bridge/preprocess.go
Normal file
@@ -0,0 +1,181 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// preprocessSubscription fixes URL encoding issues and legacy share links before
|
||||
// the subscription is handed to mihomo's parser.
|
||||
func preprocessSubscription(subscription string) string {
|
||||
lines := strings.Split(subscription, "\n")
|
||||
result := make([]string, 0, len(lines))
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimRight(line, " \r")
|
||||
if line == "" {
|
||||
result = append(result, line)
|
||||
continue
|
||||
}
|
||||
|
||||
// Decode the entire URL line. This fixes inputs such as v2rayN's
|
||||
// uuid%3Apassword encoding and keeps malformed percent escapes unchanged.
|
||||
if decoded, err := url.QueryUnescape(line); err == nil {
|
||||
line = decoded
|
||||
}
|
||||
|
||||
line = normalizeLegacyShadowrocketVMess(line)
|
||||
result = append(result, line)
|
||||
}
|
||||
|
||||
return strings.Join(result, "\n")
|
||||
}
|
||||
|
||||
func normalizeLegacyShadowrocketVMess(line string) string {
|
||||
const prefix = "vmess://"
|
||||
if !strings.HasPrefix(line, prefix) {
|
||||
return line
|
||||
}
|
||||
|
||||
body := strings.TrimPrefix(line, prefix)
|
||||
queryStart := strings.IndexByte(body, '?')
|
||||
if queryStart < 0 {
|
||||
return line
|
||||
}
|
||||
|
||||
encoded := body[:queryStart]
|
||||
rawQuery := body[queryStart+1:]
|
||||
if encoded == "" || rawQuery == "" {
|
||||
return line
|
||||
}
|
||||
|
||||
decoded, ok := decodeLooseBase64(encoded)
|
||||
if !ok {
|
||||
return line
|
||||
}
|
||||
|
||||
cipher, remainder, ok := strings.Cut(decoded, ":")
|
||||
if !ok {
|
||||
return line
|
||||
}
|
||||
uuid, serverPort, ok := strings.Cut(remainder, "@")
|
||||
if !ok {
|
||||
return line
|
||||
}
|
||||
server, port, ok := splitHostPortLoose(serverPort)
|
||||
if !ok || uuid == "" || server == "" || port == "" {
|
||||
return line
|
||||
}
|
||||
if _, err := strconv.Atoi(port); err != nil {
|
||||
return line
|
||||
}
|
||||
|
||||
query, err := url.ParseQuery(rawQuery)
|
||||
if err != nil {
|
||||
return line
|
||||
}
|
||||
|
||||
aid := firstQueryValue(query, "aid", "alterId")
|
||||
if aid == "" {
|
||||
aid = "0"
|
||||
}
|
||||
|
||||
network := "tcp"
|
||||
headerType := "none"
|
||||
host := ""
|
||||
path := ""
|
||||
obfs := strings.ToLower(firstQueryValue(query, "obfs"))
|
||||
switch obfs {
|
||||
case "websocket", "ws":
|
||||
network = "ws"
|
||||
host = firstQueryValue(query, "obfsParam", "host", "wsHost")
|
||||
path = firstQueryValue(query, "path", "wspath")
|
||||
case "":
|
||||
if value := firstQueryValue(query, "network", "net", "type"); value != "" {
|
||||
network = strings.ToLower(value)
|
||||
}
|
||||
host = firstQueryValue(query, "wsHost", "host")
|
||||
path = firstQueryValue(query, "wspath", "path")
|
||||
if value := firstQueryValue(query, "headerType"); value != "" {
|
||||
headerType = value
|
||||
}
|
||||
case "none":
|
||||
headerType = "none"
|
||||
default:
|
||||
headerType = obfs
|
||||
}
|
||||
|
||||
tls := ""
|
||||
switch strings.ToLower(firstQueryValue(query, "tls", "security")) {
|
||||
case "1", "true", "tls":
|
||||
tls = "tls"
|
||||
}
|
||||
|
||||
remarks := firstQueryValue(query, "remarks", "remark", "name")
|
||||
if remarks == "" {
|
||||
remarks = server + ":" + port
|
||||
}
|
||||
|
||||
values := map[string]string{
|
||||
"v": "2",
|
||||
"ps": remarks,
|
||||
"add": server,
|
||||
"port": port,
|
||||
"id": uuid,
|
||||
"aid": aid,
|
||||
"scy": cipher,
|
||||
"net": network,
|
||||
"type": headerType,
|
||||
"host": host,
|
||||
"path": path,
|
||||
"tls": tls,
|
||||
}
|
||||
if sni := firstQueryValue(query, "sni", "peer"); sni != "" {
|
||||
values["sni"] = sni
|
||||
}
|
||||
if alpn := firstQueryValue(query, "alpn"); alpn != "" {
|
||||
values["alpn"] = alpn
|
||||
}
|
||||
|
||||
jsonBytes, err := json.Marshal(values)
|
||||
if err != nil {
|
||||
return line
|
||||
}
|
||||
return prefix + base64.StdEncoding.EncodeToString(jsonBytes)
|
||||
}
|
||||
|
||||
func decodeLooseBase64(value string) (string, bool) {
|
||||
encodings := []*base64.Encoding{
|
||||
base64.RawURLEncoding,
|
||||
base64.URLEncoding,
|
||||
base64.RawStdEncoding,
|
||||
base64.StdEncoding,
|
||||
}
|
||||
for _, encoding := range encodings {
|
||||
decoded, err := encoding.DecodeString(value)
|
||||
if err == nil {
|
||||
return string(decoded), true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func splitHostPortLoose(value string) (string, string, bool) {
|
||||
colon := strings.LastIndexByte(value, ':')
|
||||
if colon <= 0 || colon == len(value)-1 {
|
||||
return "", "", false
|
||||
}
|
||||
return strings.Trim(value[:colon], "[]"), value[colon+1:], true
|
||||
}
|
||||
|
||||
func firstQueryValue(values url.Values, keys ...string) string {
|
||||
for _, key := range keys {
|
||||
if value := values.Get(key); value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
55
bridge/preprocess_test.go
Normal file
55
bridge/preprocess_test.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/metacubex/mihomo/common/convert"
|
||||
)
|
||||
|
||||
func TestPreprocessLegacyShadowrocketVmess(t *testing.T) {
|
||||
input := strings.Join([]string{
|
||||
"vmess://YXV0bzowNmNlNzU4Yy1iNTNkLTQ2NzQtOTdhNy01M2U4YmFhOGQwMjlAMjE2LjE0NC4yMjQuNjk6MzMwNg?remarks=%E7%BE%8E%E5%9B%BD%E8%87%AA%E5%BB%BA&path=/&obfs=none&alterId=0",
|
||||
"vmess://YXV0bzpmMmJiMmE4ZC02YWM0LTQ3NGYtYjJlYS1lMjJjNzhlYjkwMGZAMTI5LjE1MS4yNS4xOjE4MjU?remarks=US-O&udp=1&alterId=0",
|
||||
}, "\n")
|
||||
|
||||
proxies, err := convert.ConvertsV2Ray([]byte(preprocessSubscription(input)))
|
||||
if err != nil {
|
||||
t.Fatalf("convert error: %v", err)
|
||||
}
|
||||
if len(proxies) != 2 {
|
||||
t.Fatalf("got %d proxies: %#v", len(proxies), proxies)
|
||||
}
|
||||
|
||||
first := proxies[0]
|
||||
if first["type"] != "vmess" {
|
||||
t.Fatalf("unexpected first type: %#v", first["type"])
|
||||
}
|
||||
if first["name"] != "美国自建" {
|
||||
t.Fatalf("unexpected first name: %#v", first["name"])
|
||||
}
|
||||
if first["server"] != "216.144.224.69" {
|
||||
t.Fatalf("unexpected first server: %#v", first["server"])
|
||||
}
|
||||
if first["port"] != "3306" {
|
||||
t.Fatalf("unexpected first port: %#v", first["port"])
|
||||
}
|
||||
if first["uuid"] != "06ce758c-b53d-4674-97a7-53e8baa8d029" {
|
||||
t.Fatalf("unexpected first uuid: %#v", first["uuid"])
|
||||
}
|
||||
|
||||
second := proxies[1]
|
||||
if second["server"] != "129.151.25.1" {
|
||||
t.Fatalf("unexpected second server: %#v", second["server"])
|
||||
}
|
||||
if second["port"] != "1825" {
|
||||
t.Fatalf("unexpected second port: %#v", second["port"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestPreprocessKeepsStandardVmess(t *testing.T) {
|
||||
input := "vmess://uuid@example.com:443?encryption=auto#name"
|
||||
if got := preprocessSubscription(input); got != input {
|
||||
t.Fatalf("standard vmess changed:\nwant %q\n got %q", input, got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user