199 lines
5.4 KiB
Go
199 lines
5.4 KiB
Go
package model
|
|
|
|
// SubscriptionTarget constants.
|
|
const (
|
|
TargetMihomo = "mihomo"
|
|
TargetStash = "stash"
|
|
TargetSurge = "surge"
|
|
TargetSurgeMac = "surge-mac"
|
|
TargetSurfboard = "surfboard"
|
|
TargetLoon = "loon"
|
|
TargetEgern = "egern"
|
|
TargetShadowrocket = "shadowrocket"
|
|
TargetQX = "qx"
|
|
TargetSingBox = "sing-box"
|
|
TargetV2ray = "v2ray"
|
|
TargetURI = "uri"
|
|
TargetJSON = "json"
|
|
)
|
|
|
|
// FrontendVersion per review-resolution #39.
|
|
const FrontendVersion = "1.0.0"
|
|
|
|
// TestURL per review-resolution #40.
|
|
const TestURL = "https://www.gstatic.com/generate_204"
|
|
|
|
// targetAliases maps alias strings to canonical target names.
|
|
var targetAliases = map[string]string{
|
|
"clash": TargetMihomo,
|
|
"clashmeta": TargetMihomo,
|
|
"clash-meta": TargetMihomo,
|
|
"meta": TargetMihomo,
|
|
"mihomo": TargetMihomo,
|
|
"stash": TargetStash,
|
|
"surge": TargetSurge,
|
|
"surgemac": TargetSurgeMac,
|
|
"surge-mac": TargetSurgeMac,
|
|
"surge_mac": TargetSurgeMac,
|
|
"surfboard": TargetSurfboard,
|
|
"loon": TargetLoon,
|
|
"egern": TargetEgern,
|
|
"shadowrocket": TargetShadowrocket,
|
|
"sr": TargetShadowrocket,
|
|
"qx": TargetQX,
|
|
"quanx": TargetQX,
|
|
"quantumult": TargetQX,
|
|
"quantumultx": TargetQX,
|
|
"quantumult-x": TargetQX,
|
|
"singbox": TargetSingBox,
|
|
"sing-box": TargetSingBox,
|
|
"sfa": TargetSingBox,
|
|
"karing": TargetSingBox,
|
|
"v2ray": TargetV2ray,
|
|
"v2rayn": TargetV2ray,
|
|
"v2rayng": TargetV2ray,
|
|
"base64": TargetV2ray,
|
|
"uri": TargetURI,
|
|
"uris": TargetURI,
|
|
"plain": TargetURI,
|
|
"text": TargetURI,
|
|
"json": TargetJSON,
|
|
"raw": TargetJSON,
|
|
}
|
|
|
|
// NormalizeTargetAlias returns the canonical target for an alias, or "" if unknown.
|
|
func NormalizeTargetAlias(input any) string {
|
|
if input == nil {
|
|
return ""
|
|
}
|
|
s := toString(input)
|
|
if s == "" {
|
|
return ""
|
|
}
|
|
if t, ok := targetAliases[s]; ok {
|
|
return t
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// NormalizeTarget resolves a target from explicit input or User-Agent inference.
|
|
// Per review-resolution #7 — full UA keyword mapping with priority order.
|
|
func NormalizeTarget(target, ua string) string {
|
|
v := toLowerString(target)
|
|
if t, ok := targetAliases[v]; ok {
|
|
return t
|
|
}
|
|
u := toLowerString(ua)
|
|
if contains(u, "sing-box") || contains(u, "singbox") {
|
|
return TargetSingBox
|
|
}
|
|
if contains(u, "v2ray") {
|
|
return TargetV2ray
|
|
}
|
|
if contains(u, "surge") {
|
|
if contains(u, "mac") {
|
|
return TargetSurgeMac
|
|
}
|
|
return TargetSurge
|
|
}
|
|
if contains(u, "loon") {
|
|
return TargetLoon
|
|
}
|
|
if contains(u, "egern") {
|
|
return TargetEgern
|
|
}
|
|
if contains(u, "shadowrocket") {
|
|
return TargetShadowrocket
|
|
}
|
|
if contains(u, "quantumult") {
|
|
return TargetQX
|
|
}
|
|
if contains(u, "stash") {
|
|
return TargetStash
|
|
}
|
|
return TargetMihomo
|
|
}
|
|
|
|
// IsTargetCompatible checks whether a proxy type can be rendered for the given target.
|
|
func IsTargetCompatible(proxyType, target string) bool {
|
|
if target == TargetMihomo || target == TargetStash || target == TargetJSON {
|
|
return true
|
|
}
|
|
commonUri := []string{"ss", "ssr", "vmess", "vless", "trojan", "hysteria", "hysteria2", "tuic", "anytls", "http", "socks5", "wireguard"}
|
|
switch target {
|
|
case TargetURI, TargetV2ray, TargetShadowrocket:
|
|
return containsAny(proxyType, commonUri)
|
|
case TargetSingBox:
|
|
return containsAny(proxyType, []string{"ss", "vmess", "vless", "trojan", "hysteria", "hysteria2", "tuic", "anytls", "http", "socks5", "wireguard"})
|
|
case TargetSurge:
|
|
return containsAny(proxyType, []string{"ss", "vmess", "trojan", "http", "socks5", "hysteria2", "tuic", "anytls", "snell"})
|
|
case TargetSurgeMac:
|
|
return containsAny(proxyType, []string{"ss", "vmess", "trojan", "http", "socks5", "hysteria2", "tuic", "anytls", "snell", "ssh", "h2-connect"})
|
|
case TargetSurfboard:
|
|
return containsAny(proxyType, []string{"ss", "vmess", "trojan", "http", "socks5"})
|
|
case TargetLoon:
|
|
return containsAny(proxyType, []string{"ss", "ssr", "vmess", "vless", "trojan", "http", "socks5", "hysteria2", "tuic", "anytls", "wireguard"})
|
|
case TargetQX:
|
|
return containsAny(proxyType, []string{"ss", "ssr", "vmess", "vless", "trojan", "http", "socks5", "anytls"})
|
|
case TargetEgern:
|
|
return containsAny(proxyType, []string{"ss", "vmess", "trojan", "http", "socks5", "hysteria2", "tuic", "anytls"})
|
|
}
|
|
return false
|
|
}
|
|
|
|
// GetTargetContentType returns the Content-Type for a target.
|
|
func GetTargetContentType(target string) string {
|
|
switch target {
|
|
case TargetSingBox, TargetJSON:
|
|
return "application/json; charset=utf-8"
|
|
case TargetV2ray, TargetURI, TargetSurge, TargetSurgeMac, TargetSurfboard, TargetLoon, TargetShadowrocket, TargetQX:
|
|
return "text/plain; charset=utf-8"
|
|
default:
|
|
return "text/yaml; charset=utf-8"
|
|
}
|
|
}
|
|
|
|
// --- helpers ---
|
|
|
|
func toString(v any) string {
|
|
if s, ok := v.(string); ok {
|
|
return s
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func toLowerString(v any) string {
|
|
s := toString(v)
|
|
b := make([]byte, len(s))
|
|
for i := 0; i < len(s); i++ {
|
|
c := s[i]
|
|
if c >= 'A' && c <= 'Z' {
|
|
c += 32
|
|
}
|
|
b[i] = c
|
|
}
|
|
return string(b)
|
|
}
|
|
|
|
func contains(s, substr string) bool {
|
|
return len(s) >= len(substr) && (s == substr || indexOf(s, substr) >= 0)
|
|
}
|
|
|
|
func indexOf(s, substr string) int {
|
|
for i := 0; i <= len(s)-len(substr); i++ {
|
|
if s[i:i+len(substr)] == substr {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
func containsAny(s string, list []string) bool {
|
|
for _, item := range list {
|
|
if s == item {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|