Files
CN-JS-HuiBai cafab67dcc
Some checks failed
build / build (api, amd64, linux) (push) Failing after -51s
build / build (api, arm64, linux) (push) Failing after -51s
build / build (api.exe, amd64, windows) (push) Failing after -51s
持续集成修复订阅错误的问题
2026-04-18 00:08:19 +08:00

465 lines
11 KiB
Go

package protocol
import (
"fmt"
"strings"
"xboard-go/internal/model"
"xboard-go/internal/service"
"github.com/goccy/go-yaml"
)
func GenerateClash(servers []model.Server, user model.User) (string, error) {
return GenerateClashWithTemplate("clash", servers, user)
}
func GenerateClashWithTemplate(templateName string, servers []model.Server, user model.User) (string, error) {
template := strings.TrimSpace(service.GetSubscribeTemplate(templateName))
if template == "" {
return generateClashFallback(servers, user), nil
}
var config map[string]any
if err := yaml.Unmarshal([]byte(template), &config); err != nil {
return generateClashFallback(servers, user), nil
}
proxies := make([]any, 0, len(servers))
proxyNames := make([]string, 0, len(servers))
for _, s := range servers {
conf := service.BuildNodeConfig(&s)
password := service.GenerateServerPassword(&s, &user)
proxy := buildClashProxy(templateName, conf, password)
if proxy == nil {
continue
}
proxies = append(proxies, proxy)
proxyNames = append(proxyNames, conf.Name)
}
config["proxies"] = append(anySlice(config["proxies"]), proxies...)
config["proxy-groups"] = mergeClashProxyGroups(anySlice(config["proxy-groups"]), proxyNames)
if _, ok := config["rules"]; !ok {
config["rules"] = []any{"MATCH,Proxy"}
}
data, err := yaml.Marshal(config)
if err != nil {
return "", err
}
output := strings.ReplaceAll(string(data), "$app_name", service.MustGetString("app_name", "XBoard"))
return output, nil
}
func buildClashProxy(templateName string, conf service.NodeServerConfig, password string) map[string]any {
switch conf.Protocol {
case "shadowsocks":
cipher, _ := conf.Cipher.(string)
if templateName == "clash" {
switch cipher {
case "aes-128-gcm", "aes-192-gcm", "aes-256-gcm", "chacha20-ietf-poly1305":
default:
return nil
}
}
return map[string]any{
"name": conf.Name,
"type": "ss",
"server": conf.RawHost,
"port": conf.Port,
"cipher": cipher,
"password": password,
"udp": true,
}
case "vmess":
return map[string]any{
"name": conf.Name,
"type": "vmess",
"server": conf.RawHost,
"port": conf.Port,
"uuid": password,
"alterId": 0,
"cipher": "auto",
"udp": true,
}
case "vless":
if templateName != "clashmeta" {
return nil
}
proxy := map[string]any{
"name": conf.Name,
"type": "vless",
"server": conf.RawHost,
"port": conf.Port,
"uuid": password,
"alterId": 0,
"cipher": "auto",
"udp": true,
"flow": toClashString(conf.Flow),
"encryption": "none",
"tls": false,
}
switch toClashInt(conf.Tls) {
case 1:
proxy["tls"] = true
if fp := tlsFingerprint(conf.UTLS); fp != "" {
proxy["client-fingerprint"] = fp
}
if tlsSettings, ok := conf.TlsSettings.(map[string]any); ok {
proxy["skip-cert-verify"] = toClashBool(tlsSettings["allow_insecure"])
if serverName := toClashString(tlsSettings["server_name"]); serverName != "" {
proxy["servername"] = serverName
}
}
case 2:
proxy["tls"] = true
if fp := tlsFingerprint(conf.UTLS); fp != "" {
proxy["client-fingerprint"] = fp
}
if tlsSettings, ok := conf.TlsSettings.(map[string]any); ok {
proxy["skip-cert-verify"] = toClashBool(tlsSettings["allow_insecure"])
if serverName := toClashString(tlsSettings["server_name"]); serverName != "" {
proxy["servername"] = serverName
}
proxy["reality-opts"] = map[string]any{
"public-key": toClashString(tlsSettings["public_key"]),
"short-id": toClashString(tlsSettings["short_id"]),
}
}
}
proxy["network"] = "tcp"
if settings, ok := conf.NetworkSettings.(map[string]any); ok {
switch toClashString(conf.Network) {
case "", "tcp":
if header, ok := settings["header"].(map[string]any); ok && toClashString(header["type"]) == "http" {
proxy["network"] = "http"
httpOpts := map[string]any{}
if request, ok := header["request"].(map[string]any); ok {
if headers, ok := request["headers"].(map[string]any); ok && len(headers) > 0 {
httpOpts["headers"] = headers
}
if paths := clashPathList(request["path"]); len(paths) > 0 {
httpOpts["path"] = paths
}
}
if len(httpOpts) > 0 {
proxy["http-opts"] = httpOpts
}
}
case "ws":
proxy["network"] = "ws"
wsOpts := map[string]any{}
if path := toClashString(settings["path"]); path != "" {
wsOpts["path"] = path
}
if headers, ok := settings["headers"].(map[string]any); ok {
if host := toClashString(headers["Host"]); host != "" {
wsOpts["headers"] = map[string]any{"Host": host}
}
}
if len(wsOpts) > 0 {
proxy["ws-opts"] = wsOpts
}
case "grpc":
proxy["network"] = "grpc"
if serviceName := toClashString(settings["serviceName"]); serviceName != "" {
proxy["grpc-opts"] = map[string]any{"grpc-service-name": serviceName}
}
case "h2":
proxy["network"] = "h2"
h2Opts := map[string]any{}
if path := toClashString(settings["path"]); path != "" {
h2Opts["path"] = path
}
if hosts := clashHostList(settings["host"]); len(hosts) > 0 {
h2Opts["host"] = hosts
}
if len(h2Opts) > 0 {
proxy["h2-opts"] = h2Opts
}
case "httpupgrade":
proxy["network"] = "ws"
wsOpts := map[string]any{
"v2ray-http-upgrade": true,
}
if path := toClashString(settings["path"]); path != "" {
wsOpts["path"] = path
}
host := toClashString(settings["host"])
if host == "" {
host = conf.RawHost
}
if host != "" {
wsOpts["headers"] = map[string]any{"Host": host}
}
proxy["ws-opts"] = wsOpts
default:
if network := toClashString(conf.Network); network != "" {
proxy["network"] = network
}
}
} else if network := toClashString(conf.Network); network != "" {
proxy["network"] = network
}
if smux := buildClashSmux(conf.Multiplex); smux != nil {
proxy["smux"] = smux
}
return proxy
case "trojan":
return map[string]any{
"name": conf.Name,
"type": "trojan",
"server": conf.RawHost,
"port": conf.Port,
"password": password,
"udp": true,
}
default:
return nil
}
}
func mergeClashProxyGroups(groups []any, proxyNames []string) []any {
if len(groups) == 0 {
return []any{
map[string]any{
"name": "Proxy",
"type": "select",
"proxies": append([]any{"DIRECT"}, stringsToAny(proxyNames)...),
},
}
}
for index, item := range groups {
group, ok := item.(map[string]any)
if !ok {
continue
}
group["proxies"] = appendUniqueAny(anySlice(group["proxies"]), stringsToAny(proxyNames)...)
groups[index] = group
}
return groups
}
func anySlice(value any) []any {
switch typed := value.(type) {
case nil:
return []any{}
case []any:
return append([]any{}, typed...)
case []string:
result := make([]any, 0, len(typed))
for _, item := range typed {
result = append(result, item)
}
return result
default:
return []any{}
}
}
func stringsToAny(values []string) []any {
result := make([]any, 0, len(values))
for _, value := range values {
result = append(result, value)
}
return result
}
func appendUniqueAny(base []any, values ...any) []any {
existing := make(map[string]struct{}, len(base))
for _, item := range base {
existing[fmt.Sprint(item)] = struct{}{}
}
for _, item := range values {
key := fmt.Sprint(item)
if _, ok := existing[key]; ok {
continue
}
base = append(base, item)
existing[key] = struct{}{}
}
return base
}
func clashPathList(value any) []string {
switch typed := value.(type) {
case []string:
return append([]string{}, typed...)
case []any:
result := make([]string, 0, len(typed))
for _, item := range typed {
text := toClashString(item)
if text != "" {
result = append(result, text)
}
}
return result
case string:
if typed == "" {
return nil
}
return []string{typed}
default:
return nil
}
}
func clashHostList(value any) []string {
switch typed := value.(type) {
case []string:
return append([]string{}, typed...)
case []any:
result := make([]string, 0, len(typed))
for _, item := range typed {
text := toClashString(item)
if text != "" {
result = append(result, text)
}
}
return result
case string:
if typed == "" {
return nil
}
return []string{typed}
default:
return nil
}
}
func buildClashSmux(value any) map[string]any {
settings, ok := value.(map[string]any)
if !ok || !toClashBool(settings["enabled"]) {
return nil
}
smux := map[string]any{
"enabled": true,
}
if protocol := toClashString(settings["protocol"]); protocol != "" {
smux["protocol"] = protocol
}
if maxConnections := toClashInt(settings["max_connections"]); maxConnections > 0 {
smux["max-connections"] = maxConnections
}
if toClashBool(settings["padding"]) {
smux["padding"] = true
}
if brutal, ok := settings["brutal"].(map[string]any); ok && toClashBool(brutal["enabled"]) {
smux["brutal-opts"] = map[string]any{
"enabled": true,
"up": toClashInt(brutal["up_mbps"]),
"down": toClashInt(brutal["down_mbps"]),
}
}
return smux
}
func toClashString(value any) string {
switch typed := value.(type) {
case string:
return typed
case nil:
return ""
default:
return fmt.Sprint(typed)
}
}
func toClashInt(value any) int {
switch typed := value.(type) {
case int:
return typed
case int64:
return int(typed)
case float64:
return int(typed)
case string:
if typed == "" {
return 0
}
var result int
fmt.Sscanf(typed, "%d", &result)
return result
default:
return 0
}
}
func toClashBool(value any) bool {
switch typed := value.(type) {
case bool:
return typed
case int:
return typed != 0
case int64:
return typed != 0
case float64:
return typed != 0
case string:
return typed == "1" || strings.EqualFold(typed, "true")
default:
return false
}
}
func generateClashFallback(servers []model.Server, user model.User) string {
var builder strings.Builder
builder.WriteString("proxies:\n")
var proxyNames []string
for _, s := range servers {
conf := service.BuildNodeConfig(&s)
password := service.GenerateServerPassword(&s, &user)
proxy := buildClashProxy("clash", conf, password)
if proxy == nil {
continue
}
switch proxy["type"] {
case "ss":
builder.WriteString(fmt.Sprintf(
" - name: \"%s\"\n type: ss\n server: %s\n port: %d\n cipher: %s\n password: %s\n",
conf.Name,
conf.RawHost,
conf.ServerPort,
fmt.Sprint(proxy["cipher"]),
fmt.Sprint(proxy["password"]),
))
case "vmess":
builder.WriteString(fmt.Sprintf(
" - name: \"%s\"\n type: vmess\n server: %s\n port: %d\n uuid: %s\n alterId: 0\n cipher: auto\n udp: true\n",
conf.Name,
conf.RawHost,
conf.ServerPort,
fmt.Sprint(proxy["uuid"]),
))
case "trojan":
builder.WriteString(fmt.Sprintf(
" - name: \"%s\"\n type: trojan\n server: %s\n port: %d\n password: %s\n udp: true\n",
conf.Name,
conf.RawHost,
conf.ServerPort,
fmt.Sprint(proxy["password"]),
))
}
proxyNames = append(proxyNames, fmt.Sprintf("\"%s\"", conf.Name))
}
builder.WriteString("\nproxy-groups:\n")
builder.WriteString(" - name: Proxy\n type: select\n proxies:\n - DIRECT\n")
for _, name := range proxyNames {
builder.WriteString(" - " + name + "\n")
}
builder.WriteString("\nrules:\n")
builder.WriteString(" - MATCH,Proxy\n")
return builder.String()
}