Files

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
}