337 lines
8.9 KiB
Go
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
|
|
}
|