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

337 lines
8.9 KiB
Go

package protocol
import (
"encoding/base64"
"encoding/json"
"fmt"
"net/url"
"strings"
"xboard-go/internal/model"
"xboard-go/internal/service"
)
func GenerateGeneralLinks(servers []model.Server, user model.User) string {
var links []string
for _, s := range servers {
conf := service.BuildNodeConfig(&s)
password := service.GenerateServerPassword(&s, &user)
link := BuildLink(conf, password)
if link != "" {
links = append(links, link)
}
}
return strings.Join(links, "\n")
}
func BuildLink(c service.NodeServerConfig, password string) string {
switch c.Protocol {
case "shadowsocks":
return buildShadowsocks(c, password)
case "vmess":
return buildVmess(c, password)
case "vless":
return buildVless(c, password)
case "trojan":
return buildTrojan(c, password)
case "hysteria", "hysteria2":
return buildHysteria(c, password)
case "tuic":
return buildTuic(c, password)
case "anytls":
return buildAnyTLS(c, password)
case "socks":
return buildSocks(c, password)
case "http":
return buildHttp(c, password)
}
return ""
}
func buildShadowsocks(c service.NodeServerConfig, password string) string {
cipher := toString(c.Cipher)
userInfo := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", cipher, password)))
userInfo = strings.TrimRight(strings.ReplaceAll(strings.ReplaceAll(userInfo, "+", "-"), "/", "_"), "=")
link := fmt.Sprintf("ss://%s@%s:%d", userInfo, wrapIPv6(c.RawHost), c.Port)
params := url.Values{}
if plugin := toString(c.Plugin); plugin != "" {
opts := toString(c.PluginOpts)
params.Set("plugin", plugin+";"+opts)
}
if q := params.Encode(); q != "" {
link += "/?" + q
}
return link + "#" + url.PathEscape(c.Name)
}
func buildVmess(c service.NodeServerConfig, password string) string {
m := map[string]any{
"v": "2",
"ps": c.Name,
"add": c.RawHost,
"port": fmt.Sprintf("%d", c.Port),
"id": password,
"aid": "0",
"net": toString(c.Network),
"type": "none",
"host": "",
"path": "",
"tls": "",
}
if c.Tls != nil && toInt(c.Tls) > 0 {
m["tls"] = "tls"
}
if settings, ok := c.NetworkSettings.(map[string]any); ok {
switch toString(c.Network) {
case "ws":
m["path"] = toString(settings["path"])
if headers, ok := settings["headers"].(map[string]any); ok {
m["host"] = toString(headers["Host"])
}
case "grpc":
m["path"] = toString(settings["serviceName"])
case "h2":
m["path"] = toString(settings["path"])
m["host"] = toString(settings["host"])
}
}
if tlsSettings, ok := c.TlsSettings.(map[string]any); ok {
m["sni"] = toString(tlsSettings["server_name"])
}
b, _ := json.Marshal(m)
return "vmess://" + base64.StdEncoding.EncodeToString(b)
}
func buildVless(c service.NodeServerConfig, password string) string {
params := url.Values{}
params.Set("mode", "multi")
params.Set("encryption", "none")
if c.Flow != nil {
params.Set("flow", toString(c.Flow))
}
security := "none"
switch toInt(c.Tls) {
case 1:
security = "tls"
if fp := tlsFingerprint(c.UTLS); fp != "" {
params.Set("fp", fp)
}
if tlsSettings, ok := c.TlsSettings.(map[string]any); ok {
params.Set("sni", toString(tlsSettings["server_name"]))
if truthy(tlsSettings["allow_insecure"]) {
params.Set("allowInsecure", "1")
}
}
case 2:
security = "reality"
if fp := tlsFingerprint(c.UTLS); fp != "" {
params.Set("fp", fp)
}
if tlsSettings, ok := c.TlsSettings.(map[string]any); ok {
params.Set("pbk", toString(tlsSettings["public_key"]))
params.Set("sid", toString(tlsSettings["short_id"]))
params.Set("sni", toString(tlsSettings["server_name"]))
params.Set("servername", toString(tlsSettings["server_name"]))
params.Set("spx", "/")
}
}
params.Set("security", security)
params.Set("type", toString(c.Network))
if settings, ok := c.NetworkSettings.(map[string]any); ok {
switch toString(c.Network) {
case "ws":
params.Set("path", toString(settings["path"]))
if headers, ok := settings["headers"].(map[string]any); ok {
params.Set("host", toString(headers["Host"]))
}
case "grpc":
params.Set("serviceName", toString(settings["serviceName"]))
case "h2":
params.Set("type", "http")
params.Set("path", toString(settings["path"]))
if host := joinConfigValue(settings["host"]); host != "" {
params.Set("host", host)
}
case "httpupgrade":
params.Set("path", toString(settings["path"]))
host := toString(settings["host"])
if host == "" {
host = c.RawHost
}
params.Set("host", host)
}
}
return fmt.Sprintf("vless://%s@%s:%d?%s#%s", password, wrapIPv6(c.RawHost), c.Port, params.Encode(), url.PathEscape(c.Name))
}
func buildTrojan(c service.NodeServerConfig, password string) string {
params := url.Values{}
security := "tls"
if toInt(c.Tls) == 2 {
security = "reality"
if tlsSettings, ok := c.TlsSettings.(map[string]any); ok {
params.Set("pbk", toString(tlsSettings["public_key"]))
params.Set("sid", toString(tlsSettings["short_id"]))
params.Set("sni", toString(tlsSettings["server_name"]))
}
} else {
if tlsSettings, ok := c.TlsSettings.(map[string]any); ok {
params.Set("sni", toString(tlsSettings["server_name"]))
}
}
params.Set("security", security)
if settings, ok := c.NetworkSettings.(map[string]any); ok {
switch toString(c.Network) {
case "ws":
params.Set("type", "ws")
params.Set("path", toString(settings["path"]))
case "grpc":
params.Set("type", "grpc")
params.Set("serviceName", toString(settings["serviceName"]))
}
}
return fmt.Sprintf("trojan://%s@%s:%d?%s#%s", password, wrapIPv6(c.RawHost), c.Port, params.Encode(), url.PathEscape(c.Name))
}
func buildHysteria(c service.NodeServerConfig, password string) string {
params := url.Values{}
params.Set("sni", toString(c.ServerName))
if c.Protocol == "hysteria2" || toInt(c.Version) == 2 {
if obfs := toString(c.Obfs); obfs != "" {
params.Set("obfs", "salamander")
params.Set("obfs-password", toString(c.ObfsPassword))
}
if c.Ports != "" {
params.Set("mport", c.Ports)
}
return fmt.Sprintf("hysteria2://%s@%s:%d?%s#%s", password, wrapIPv6(c.RawHost), c.Port, params.Encode(), url.PathEscape(c.Name))
}
params.Set("protocol", "udp")
params.Set("auth", password)
if c.UpMbps != nil {
params.Set("upmbps", fmt.Sprintf("%v", c.UpMbps))
}
if c.DownMbps != nil {
params.Set("downmbps", fmt.Sprintf("%v", c.DownMbps))
}
return fmt.Sprintf("hysteria://%s:%d?%s#%s", wrapIPv6(c.RawHost), c.Port, params.Encode(), url.PathEscape(c.Name))
}
func buildTuic(c service.NodeServerConfig, password string) string {
params := url.Values{}
params.Set("sni", toString(c.ServerName))
params.Set("congestion_control", toString(c.CongestionControl))
params.Set("udp-relay-mode", "native")
return fmt.Sprintf("tuic://%s:%s@%s:%d?%s#%s", password, password, wrapIPv6(c.RawHost), c.Port, params.Encode(), url.PathEscape(c.Name))
}
func buildAnyTLS(c service.NodeServerConfig, password string) string {
params := url.Values{}
params.Set("sni", toString(c.ServerName))
return fmt.Sprintf("anytls://%s@%s:%d?%s#%s", password, wrapIPv6(c.RawHost), c.Port, params.Encode(), url.PathEscape(c.Name))
}
func buildSocks(c service.NodeServerConfig, password string) string {
auth := base64.StdEncoding.EncodeToString([]byte(password + ":" + password))
return fmt.Sprintf("socks://%s@%s:%d#%s", auth, wrapIPv6(c.RawHost), c.Port, url.PathEscape(c.Name))
}
func buildHttp(c service.NodeServerConfig, password string) string {
auth := base64.StdEncoding.EncodeToString([]byte(password + ":" + password))
link := fmt.Sprintf("http://%s@%s:%d", auth, wrapIPv6(c.RawHost), c.Port)
if toInt(c.Tls) > 0 {
params := url.Values{}
params.Set("security", "tls")
link += "?" + params.Encode()
}
return link + "#" + url.PathEscape(c.Name)
}
func wrapIPv6(host string) string {
if strings.Contains(host, ":") && !strings.HasPrefix(host, "[") {
return "[" + host + "]"
}
return host
}
func tlsFingerprint(value any) string {
settings, ok := value.(map[string]any)
if !ok || !truthy(settings["enabled"]) {
return ""
}
return toString(settings["fingerprint"])
}
func joinConfigValue(value any) string {
switch typed := value.(type) {
case string:
return typed
case []string:
return strings.Join(typed, ",")
case []any:
result := make([]string, 0, len(typed))
for _, item := range typed {
text := toString(item)
if text != "" {
result = append(result, text)
}
}
return strings.Join(result, ",")
default:
return ""
}
}
func toString(v any) string {
if s, ok := v.(string); ok {
return s
}
if v == nil {
return ""
}
return fmt.Sprintf("%v", v)
}
func toInt(v any) int {
switch typed := v.(type) {
case int:
return typed
case int64:
return int(typed)
case float64:
return int(typed)
}
return 0
}
func truthy(v any) bool {
switch typed := v.(type) {
case bool:
return typed
case int:
return typed != 0
case int64:
return typed != 0
case float64:
return typed != 0
case string:
switch strings.ToLower(strings.TrimSpace(typed)) {
case "1", "true", "yes", "on":
return true
}
}
return false
}