feat: Go port of the panel in panel-go/ — drop-in replacement for server.js
Pure-stdlib Go implementation of the whole control plane: same nexusgate.json schema (unknown fields preserved verbatim, explicit null/false tri-states modeled), same HTTP API and error strings, embedded frontend (NG_PUBLIC_DIR override), self-implemented scrypt auth, X25519 Reality keys, panel-hosted keyStore and heartbeat reconciliation included. Version stays 0.6.8. Verified: Go tests 33/33 (scrypt + Reality vectors generated from the Node implementation, subscription goldens byte-identical), Node suite 52/52 untouched, differential E2E against the Node panel 50/50 including two-way DB swap. See panel-go/PARITY.md and panel-go/INSTALL-GO.md.
This commit is contained in:
@@ -0,0 +1,703 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ---------- ordered map (JS object insertion order for Clash proxies) ----------
|
||||
|
||||
type OrderedKV struct {
|
||||
Key string
|
||||
Value any
|
||||
}
|
||||
|
||||
type OrderedMap []OrderedKV
|
||||
|
||||
func (m OrderedMap) Get(key string) (any, bool) {
|
||||
for _, kv := range m {
|
||||
if kv.Key == key {
|
||||
return kv.Value, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (m *OrderedMap) Set(key string, value any) {
|
||||
for i := range *m {
|
||||
if (*m)[i].Key == key {
|
||||
(*m)[i].Value = value
|
||||
return
|
||||
}
|
||||
}
|
||||
*m = append(*m, OrderedKV{Key: key, Value: value})
|
||||
}
|
||||
|
||||
func (m OrderedMap) Str(key string) string {
|
||||
v, ok := m.Get(key)
|
||||
if !ok || v == nil {
|
||||
return ""
|
||||
}
|
||||
s, _ := v.(string)
|
||||
return s
|
||||
}
|
||||
|
||||
func (m OrderedMap) Bool(key string) bool {
|
||||
v, _ := m.Get(key)
|
||||
b, _ := v.(bool)
|
||||
return b
|
||||
}
|
||||
|
||||
// ---------- entries ----------
|
||||
|
||||
type SubEntry struct {
|
||||
NodeID string `json:"nodeId"`
|
||||
ClientURI string `json:"clientUri"`
|
||||
}
|
||||
|
||||
type SubResult struct {
|
||||
Body string
|
||||
ContentType string
|
||||
}
|
||||
|
||||
func decodeURIComponent(s string) string {
|
||||
out, err := url.PathUnescape(s)
|
||||
if err != nil {
|
||||
return s
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func NodeClientURI(d *Data, nc *NodeClient) string {
|
||||
var node *Node
|
||||
for _, item := range d.Nodes {
|
||||
if item.ID == nc.NodeID {
|
||||
node = item
|
||||
break
|
||||
}
|
||||
}
|
||||
if node == nil || node.Status != "active" {
|
||||
return ""
|
||||
}
|
||||
var server *Server
|
||||
for _, item := range d.Servers {
|
||||
if item.ID == node.ServerID {
|
||||
server = item
|
||||
break
|
||||
}
|
||||
}
|
||||
if server == nil {
|
||||
return ""
|
||||
}
|
||||
publicKey := ""
|
||||
if raw, ok := node.Artifacts["realityPublicKey"]; ok && len(raw) > 0 {
|
||||
var s string
|
||||
if err := json.Unmarshal(raw, &s); err == nil {
|
||||
publicKey = s
|
||||
}
|
||||
}
|
||||
networkMode := node.NetworkMode
|
||||
if networkMode == "" {
|
||||
networkMode = "ipv4"
|
||||
}
|
||||
uri, ok := BuildClientURI(BuildClientURIParams{
|
||||
Protocol: node.Protocol,
|
||||
RelayServer: server,
|
||||
RelayPort: node.Port,
|
||||
ClientID: nc.Credentials.ClientID,
|
||||
RelayPassword: nc.Credentials.RelayPassword,
|
||||
ShortID: nc.Credentials.ShortID,
|
||||
WSPath: xbDeref(node.WSPath),
|
||||
SocksUser: NodeEmail(nc.CustomerID),
|
||||
Reality: node.Reality,
|
||||
PublicKey: publicKey,
|
||||
Name: node.Name,
|
||||
NetworkMode: networkMode,
|
||||
TLSDomain: server.TLSDomain,
|
||||
SSServerKey: xbDeref(node.SSServerKey),
|
||||
})
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return uri
|
||||
}
|
||||
|
||||
func Entries(d *Data, customerID string) []SubEntry {
|
||||
seen := map[string]int{}
|
||||
items := []SubEntry{}
|
||||
for _, nc := range d.NodeClients {
|
||||
if nc.CustomerID != customerID || nc.Status != "active" {
|
||||
continue
|
||||
}
|
||||
uri := NodeClientURI(d, nc)
|
||||
if uri == "" {
|
||||
continue
|
||||
}
|
||||
nodeName := nc.NodeID
|
||||
for _, item := range d.Nodes {
|
||||
if item.ID == nc.NodeID {
|
||||
nodeName = item.Name
|
||||
break
|
||||
}
|
||||
}
|
||||
index := seen[nodeName] + 1
|
||||
seen[nodeName] = index
|
||||
name := nodeName
|
||||
if index != 1 {
|
||||
name = fmt.Sprintf("%s (%d)", nodeName, index)
|
||||
}
|
||||
separator := strings.LastIndex(uri, "#")
|
||||
prefix := uri
|
||||
if separator >= 0 {
|
||||
prefix = uri[:separator]
|
||||
}
|
||||
items = append(items, SubEntry{
|
||||
NodeID: nc.NodeID,
|
||||
ClientURI: prefix + "#" + encodeURIComponent(name),
|
||||
})
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
// ---------- Clash ----------
|
||||
|
||||
func q(value any) string {
|
||||
// JSON.stringify(String(value)); String(null) is "null".
|
||||
b, err := marshalJSON(toJSString(value))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func toJSString(value any) string {
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
return v
|
||||
case bool:
|
||||
if v {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
case int:
|
||||
return strconv.Itoa(v)
|
||||
case int64:
|
||||
return strconv.FormatInt(v, 10)
|
||||
case float64:
|
||||
return strconv.FormatFloat(v, 'g', -1, 64)
|
||||
case nil:
|
||||
return "null"
|
||||
default:
|
||||
return fmt.Sprintf("%v", v)
|
||||
}
|
||||
}
|
||||
|
||||
func ClashProxy(item SubEntry) (OrderedMap, error) {
|
||||
uri := item.ClientURI
|
||||
hashIdx := strings.LastIndex(uri, "#")
|
||||
name := decodeURIComponent(uri[hashIdx+1:])
|
||||
u, err := url.Parse(uri)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
port := 0
|
||||
if p := u.Port(); p != "" {
|
||||
if n, err := strconv.Atoi(p); err == nil {
|
||||
port = n
|
||||
}
|
||||
}
|
||||
base := OrderedMap{
|
||||
{Key: "name", Value: name},
|
||||
{Key: "server", Value: u.Hostname()},
|
||||
{Key: "port", Value: port},
|
||||
}
|
||||
query := u.Query()
|
||||
switch u.Scheme + ":" {
|
||||
case "hysteria2:":
|
||||
password := ""
|
||||
if u.User != nil {
|
||||
password = u.User.Username()
|
||||
}
|
||||
out := append(OrderedMap{}, base...)
|
||||
out = append(out,
|
||||
OrderedKV{Key: "type", Value: "hysteria2"},
|
||||
OrderedKV{Key: "password", Value: password},
|
||||
OrderedKV{Key: "sni", Value: queryGetOrNil(query, "sni")},
|
||||
OrderedKV{Key: "alpn", Value: []string{"h3"}},
|
||||
)
|
||||
return out, nil
|
||||
case "anytls:":
|
||||
password := ""
|
||||
if u.User != nil {
|
||||
password = u.User.Username()
|
||||
}
|
||||
out := append(OrderedMap{}, base...)
|
||||
out = append(out,
|
||||
OrderedKV{Key: "type", Value: "anytls"},
|
||||
OrderedKV{Key: "password", Value: password},
|
||||
OrderedKV{Key: "sni", Value: queryGetOrNil(query, "sni")},
|
||||
OrderedKV{Key: "udp", Value: true},
|
||||
)
|
||||
return out, nil
|
||||
case "vless:":
|
||||
security := query.Get("security")
|
||||
uuid := ""
|
||||
if u.User != nil {
|
||||
uuid = u.User.Username()
|
||||
}
|
||||
isTLS := security == "reality" || security == "tls"
|
||||
network := "tcp"
|
||||
if query.Get("type") == "ws" {
|
||||
network = "ws"
|
||||
}
|
||||
out := append(OrderedMap{}, base...)
|
||||
out = append(out,
|
||||
OrderedKV{Key: "type", Value: "vless"},
|
||||
OrderedKV{Key: "uuid", Value: uuid},
|
||||
OrderedKV{Key: "tls", Value: isTLS},
|
||||
OrderedKV{Key: "network", Value: network},
|
||||
)
|
||||
if network == "ws" {
|
||||
path := query.Get("path")
|
||||
if path == "" {
|
||||
path = "/"
|
||||
}
|
||||
out = append(out, OrderedKV{Key: "ws-opts", Value: OrderedMap{{Key: "path", Value: path}}})
|
||||
}
|
||||
if isTLS {
|
||||
out = append(out, OrderedKV{Key: "servername", Value: queryGetOrNil(query, "sni")})
|
||||
if security == "reality" {
|
||||
fp := query.Get("fp")
|
||||
if fp == "" {
|
||||
fp = "chrome"
|
||||
}
|
||||
out = append(out,
|
||||
OrderedKV{Key: "client-fingerprint", Value: fp},
|
||||
OrderedKV{Key: "reality-opts", Value: OrderedMap{
|
||||
{Key: "public-key", Value: queryGetOrNil(query, "pbk")},
|
||||
{Key: "short-id", Value: queryGetOrNil(query, "sid")},
|
||||
}},
|
||||
)
|
||||
}
|
||||
if _, has := query["flow"]; has {
|
||||
out = append(out, OrderedKV{Key: "flow", Value: query.Get("flow")})
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
case "ss:":
|
||||
username := ""
|
||||
if u.User != nil {
|
||||
username = u.User.Username()
|
||||
}
|
||||
raw, err := decodeBase64URL(username)
|
||||
if err != nil {
|
||||
return nil, errors.New("Invalid Shadowsocks URI")
|
||||
}
|
||||
decoded := string(raw)
|
||||
separator := strings.Index(decoded, ":")
|
||||
if separator < 0 {
|
||||
return nil, errors.New("Invalid Shadowsocks URI")
|
||||
}
|
||||
out := append(OrderedMap{}, base...)
|
||||
out = append(out,
|
||||
OrderedKV{Key: "type", Value: "ss"},
|
||||
OrderedKV{Key: "cipher", Value: decoded[:separator]},
|
||||
OrderedKV{Key: "password", Value: decoded[separator+1:]},
|
||||
)
|
||||
return out, nil
|
||||
case "socks5:":
|
||||
username, password := "", ""
|
||||
if u.User != nil {
|
||||
username = u.User.Username()
|
||||
password, _ = u.User.Password()
|
||||
}
|
||||
out := append(OrderedMap{}, base...)
|
||||
out = append(out,
|
||||
OrderedKV{Key: "type", Value: "socks5"},
|
||||
OrderedKV{Key: "username", Value: username},
|
||||
OrderedKV{Key: "password", Value: password},
|
||||
)
|
||||
return out, nil
|
||||
}
|
||||
return nil, errors.New("Unsupported client URI")
|
||||
}
|
||||
|
||||
func queryGetOrNil(query url.Values, key string) any {
|
||||
vals, ok := query[key]
|
||||
if !ok || len(vals) == 0 {
|
||||
return nil
|
||||
}
|
||||
return vals[0]
|
||||
}
|
||||
|
||||
func decodeBase64URL(s string) ([]byte, error) {
|
||||
if raw, err := base64.RawURLEncoding.DecodeString(s); err == nil {
|
||||
return raw, nil
|
||||
}
|
||||
padded := s
|
||||
for len(padded)%4 != 0 {
|
||||
padded += "="
|
||||
}
|
||||
return base64.URLEncoding.DecodeString(padded)
|
||||
}
|
||||
|
||||
func yamlValue(value any, indent int) string {
|
||||
switch v := value.(type) {
|
||||
case []string:
|
||||
raw, err := marshalJSON(v)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return string(raw)
|
||||
case []any:
|
||||
raw, err := marshalJSON(v)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return string(raw)
|
||||
case OrderedMap:
|
||||
parts := make([]string, 0, len(v))
|
||||
pad := strings.Repeat(" ", indent)
|
||||
for _, kv := range v {
|
||||
if child, isObj := kv.Value.(OrderedMap); isObj {
|
||||
parts = append(parts, pad+kv.Key+": "+"\n"+yamlValue(child, indent+2))
|
||||
} else {
|
||||
parts = append(parts, pad+kv.Key+": "+yamlValue(kv.Value, 0))
|
||||
}
|
||||
}
|
||||
return strings.Join(parts, "\n")
|
||||
case bool:
|
||||
if v {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
case int:
|
||||
return strconv.Itoa(v)
|
||||
case int64:
|
||||
return strconv.FormatInt(v, 10)
|
||||
case float64:
|
||||
return strconv.FormatFloat(v, 'g', -1, 64)
|
||||
case json.Number:
|
||||
return v.String()
|
||||
default:
|
||||
return q(value)
|
||||
}
|
||||
}
|
||||
|
||||
func MakeClash(items []SubEntry, smart bool) string {
|
||||
reserved := []string{"节点选择", "DIRECT", "REJECT"}
|
||||
if smart {
|
||||
reserved = []string{"节点选择", "自动选择", "流媒体", "AI 服务", "Meta 服务", "DIRECT", "REJECT"}
|
||||
}
|
||||
used := map[string]bool{}
|
||||
for _, r := range reserved {
|
||||
used[r] = true
|
||||
}
|
||||
proxies := make([]OrderedMap, 0, len(items))
|
||||
for _, item := range items {
|
||||
proxy, err := ClashProxy(item)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
name := proxy.Str("name")
|
||||
finalName := name
|
||||
if used[finalName] {
|
||||
index := 1
|
||||
for {
|
||||
suffix := ""
|
||||
if index != 1 {
|
||||
suffix = fmt.Sprintf(" %d", index)
|
||||
}
|
||||
finalName = fmt.Sprintf("%s (节点%s)", name, suffix)
|
||||
index++
|
||||
if !used[finalName] {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
used[finalName] = true
|
||||
proxy.Set("name", finalName)
|
||||
proxies = append(proxies, proxy)
|
||||
}
|
||||
names := make([]string, 0, len(proxies))
|
||||
yamlParts := make([]string, 0, len(proxies))
|
||||
for _, proxy := range proxies {
|
||||
names = append(names, proxy.Str("name"))
|
||||
y := yamlValue(proxy, 4)
|
||||
if strings.HasPrefix(y, " ") {
|
||||
y = " - " + y[4:]
|
||||
}
|
||||
yamlParts = append(yamlParts, y)
|
||||
}
|
||||
yaml := strings.Join(yamlParts, "\n")
|
||||
group := func(name, typ string, members []string) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString(" - name: " + q(name) + "\n")
|
||||
sb.WriteString(" type: " + typ + "\n")
|
||||
if typ == "url-test" {
|
||||
sb.WriteString(" url: \"https://www.gstatic.com/generate_204\"\n interval: 300\n tolerance: 80\n")
|
||||
}
|
||||
sb.WriteString(" proxies:\n")
|
||||
lines := make([]string, len(members))
|
||||
for i, m := range members {
|
||||
lines[i] = " - " + q(m)
|
||||
}
|
||||
sb.WriteString(strings.Join(lines, "\n"))
|
||||
sb.WriteString("\n")
|
||||
return sb.String()
|
||||
}
|
||||
var groups string
|
||||
if smart {
|
||||
groups = group("节点选择", "select", append([]string{"自动选择"}, append(append([]string{}, names...), "DIRECT")...)) +
|
||||
group("自动选择", "url-test", names) +
|
||||
group("流媒体", "select", append([]string{"节点选择", "自动选择"}, names...)) +
|
||||
group("AI 服务", "select", append([]string{"节点选择", "自动选择"}, names...)) +
|
||||
group("Meta 服务", "select", append([]string{"节点选择", "自动选择"}, names...))
|
||||
} else {
|
||||
groups = group("节点选择", "select", names)
|
||||
}
|
||||
rules := []string{"MATCH,节点选择"}
|
||||
if smart {
|
||||
rules = []string{
|
||||
"GEOSITE,category-ads-all,REJECT", "GEOSITE,private,DIRECT",
|
||||
"GEOSITE,openai,AI 服务", "GEOSITE,netflix,流媒体",
|
||||
"GEOSITE,facebook,Meta 服务", "DOMAIN-SUFFIX,fbcdn.net,Meta 服务",
|
||||
"DOMAIN-SUFFIX,instagram.com,Meta 服务", "DOMAIN-SUFFIX,whatsapp.net,Meta 服务",
|
||||
"GEOSITE,geolocation-!cn,节点选择", "GEOSITE,cn,DIRECT",
|
||||
"GEOIP,CN,DIRECT", "MATCH,节点选择",
|
||||
}
|
||||
}
|
||||
ruleLines := make([]string, len(rules))
|
||||
for i, r := range rules {
|
||||
ruleLines[i] = " - " + q(r)
|
||||
}
|
||||
return "mixed-port: 7890\nallow-lan: false\nmode: rule\nproxies:\n" + yaml +
|
||||
"\nproxy-groups:\n" + groups + "rules:\n" + strings.Join(ruleLines, "\n") + "\n"
|
||||
}
|
||||
|
||||
// ---------- Surge ----------
|
||||
|
||||
// MakeSurge mirrors makeSurge; ok=false mirrors JS null.
|
||||
func MakeSurge(items []SubEntry) (string, bool) {
|
||||
proxies := []OrderedMap{}
|
||||
for _, item := range items {
|
||||
proxy, err := ClashProxy(item)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
typ := proxy.Str("type")
|
||||
if typ == "ss" || typ == "socks5" {
|
||||
proxies = append(proxies, proxy)
|
||||
}
|
||||
}
|
||||
if len(proxies) == 0 {
|
||||
return "", false
|
||||
}
|
||||
safeReplacer := strings.NewReplacer("\r", "_", "\n", "_", ",", "_", "=", "_")
|
||||
safe := func(v string) string { return safeReplacer.Replace(v) }
|
||||
policies := make([]string, 0, len(proxies))
|
||||
names := make([]string, 0, len(proxies))
|
||||
for _, proxy := range proxies {
|
||||
prefix := safe(proxy.Str("name")) + " = "
|
||||
server, _ := proxy.Get("server")
|
||||
port, _ := proxy.Get("port")
|
||||
if proxy.Str("type") == "ss" {
|
||||
policies = append(policies, fmt.Sprintf("%sss, %s, %s, encrypt-method=%s, password=%s",
|
||||
prefix, toJSString(server), toJSString(port), proxy.Str("cipher"), safe(proxy.Str("password"))))
|
||||
} else {
|
||||
policies = append(policies, fmt.Sprintf("%ssocks5, %s, %s, %s, %s",
|
||||
prefix, toJSString(server), toJSString(port), safe(proxy.Str("username")), safe(proxy.Str("password"))))
|
||||
}
|
||||
names = append(names, safe(proxy.Str("name")))
|
||||
}
|
||||
joined := strings.Join(names, ", ")
|
||||
body := "[General]\nloglevel = notify\n[Proxy]\n" + strings.Join(policies, "\n") +
|
||||
"\n[Proxy Group]\n节点选择 = select, " + joined + ", DIRECT\n自动选择 = url-test, " + joined +
|
||||
", url=http://www.gstatic.com/generate_204, interval=600\n[Rule]\nGEOIP,CN,DIRECT\nFINAL,节点选择\n"
|
||||
return body, true
|
||||
}
|
||||
|
||||
// ---------- sing-box ----------
|
||||
|
||||
type sbTransport struct {
|
||||
Type string `json:"type"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
type sbReality struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
PublicKey string `json:"public_key"`
|
||||
ShortID string `json:"short_id"`
|
||||
}
|
||||
|
||||
type sbTLS struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
ServerName string `json:"server_name"`
|
||||
Alpn []string `json:"alpn,omitempty"`
|
||||
Reality *sbReality `json:"reality,omitempty"`
|
||||
}
|
||||
|
||||
type sbOutbound struct {
|
||||
Type string `json:"type"`
|
||||
Tag string `json:"tag"`
|
||||
Server string `json:"server,omitempty"`
|
||||
ServerPort int `json:"server_port,omitempty"`
|
||||
Method string `json:"method,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
UUID string `json:"uuid,omitempty"`
|
||||
Flow string `json:"flow,omitempty"`
|
||||
Transport *sbTransport `json:"transport,omitempty"`
|
||||
TLS *sbTLS `json:"tls,omitempty"`
|
||||
Outbounds []string `json:"outbounds,omitempty"`
|
||||
}
|
||||
|
||||
type sbInbound struct {
|
||||
Type string `json:"type"`
|
||||
Tag string `json:"tag"`
|
||||
Listen string `json:"listen"`
|
||||
ListenPort int `json:"listen_port"`
|
||||
}
|
||||
|
||||
type sbConfig struct {
|
||||
Log struct {
|
||||
Level string `json:"level"`
|
||||
} `json:"log"`
|
||||
Inbounds []sbInbound `json:"inbounds"`
|
||||
Outbounds []sbOutbound `json:"outbounds"`
|
||||
Route struct {
|
||||
Final string `json:"final"`
|
||||
} `json:"route"`
|
||||
}
|
||||
|
||||
func subMarshalIndentJSON(v any) string {
|
||||
var buf bytes.Buffer
|
||||
enc := json.NewEncoder(&buf)
|
||||
enc.SetEscapeHTML(false)
|
||||
enc.SetIndent("", " ")
|
||||
if err := enc.Encode(v); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func MakeSingBox(items []SubEntry) string {
|
||||
proxies := make([]sbOutbound, 0, len(items))
|
||||
tags := make([]string, 0, len(items))
|
||||
for i, item := range items {
|
||||
proxy, err := ClashProxy(item)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
tag := fmt.Sprintf("node-%d", i+1)
|
||||
tags = append(tags, tag)
|
||||
server := proxy.Str("server")
|
||||
portVal, _ := proxy.Get("port")
|
||||
port, _ := portVal.(int)
|
||||
base := sbOutbound{Tag: tag, Server: server, ServerPort: port}
|
||||
switch proxy.Str("type") {
|
||||
case "ss":
|
||||
base.Type = "shadowsocks"
|
||||
base.Method = proxy.Str("cipher")
|
||||
base.Password = proxy.Str("password")
|
||||
case "hysteria2":
|
||||
base.Type = "hysteria2"
|
||||
base.Password = proxy.Str("password")
|
||||
base.TLS = &sbTLS{Enabled: true, ServerName: proxy.Str("sni"), Alpn: []string{"h3"}}
|
||||
case "anytls":
|
||||
base.Type = "anytls"
|
||||
base.Password = proxy.Str("password")
|
||||
base.TLS = &sbTLS{Enabled: true, ServerName: proxy.Str("sni")}
|
||||
case "socks5":
|
||||
base.Type = "socks"
|
||||
base.Version = "5"
|
||||
base.Username = proxy.Str("username")
|
||||
base.Password = proxy.Str("password")
|
||||
default: // vless
|
||||
base.Type = "vless"
|
||||
base.UUID = proxy.Str("uuid")
|
||||
if flow := proxy.Str("flow"); flow != "" {
|
||||
base.Flow = flow
|
||||
}
|
||||
if proxy.Str("network") == "ws" {
|
||||
path := "/"
|
||||
if wsOpts, ok := proxy.Get("ws-opts"); ok {
|
||||
if om, isOM := wsOpts.(OrderedMap); isOM {
|
||||
if p := om.Str("path"); p != "" {
|
||||
path = p
|
||||
}
|
||||
}
|
||||
}
|
||||
base.Transport = &sbTransport{Type: "ws", Path: path}
|
||||
}
|
||||
if proxy.Bool("tls") {
|
||||
tls := &sbTLS{Enabled: true, ServerName: proxy.Str("servername")}
|
||||
if ro, ok := proxy.Get("reality-opts"); ok && ro != nil {
|
||||
if om, isOM := ro.(OrderedMap); isOM {
|
||||
tls.Reality = &sbReality{Enabled: true, PublicKey: om.Str("public-key"), ShortID: om.Str("short-id")}
|
||||
}
|
||||
}
|
||||
base.TLS = tls
|
||||
}
|
||||
}
|
||||
proxies = append(proxies, base)
|
||||
}
|
||||
cfg := sbConfig{}
|
||||
cfg.Log.Level = "warn"
|
||||
cfg.Inbounds = []sbInbound{{Type: "mixed", Tag: "local", Listen: "127.0.0.1", ListenPort: 2080}}
|
||||
cfg.Outbounds = append([]sbOutbound{{Type: "selector", Tag: "proxy", Outbounds: tags}}, proxies...)
|
||||
cfg.Outbounds = append(cfg.Outbounds, sbOutbound{Type: "direct", Tag: "direct"})
|
||||
cfg.Route.Final = "proxy"
|
||||
return subMarshalIndentJSON(cfg)
|
||||
}
|
||||
|
||||
// ---------- format dispatch ----------
|
||||
|
||||
var clashUARe = regexp.MustCompile(`(?i)clash|mihomo`)
|
||||
|
||||
func FormatSubscription(d *Data, customer *Customer, format, userAgent string) (*SubResult, bool) {
|
||||
items := Entries(d, customer.ID)
|
||||
if len(items) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
selected := format
|
||||
if format == "auto" {
|
||||
if clashUARe.MatchString(userAgent) {
|
||||
selected = "clash"
|
||||
} else {
|
||||
selected = "base64"
|
||||
}
|
||||
}
|
||||
joined := func() string {
|
||||
uris := make([]string, len(items))
|
||||
for i, item := range items {
|
||||
uris[i] = item.ClientURI
|
||||
}
|
||||
return strings.Join(uris, "\n") + "\n"
|
||||
}
|
||||
switch selected {
|
||||
case "raw":
|
||||
return &SubResult{Body: joined(), ContentType: "text/plain; charset=utf-8"}, true
|
||||
case "base64":
|
||||
return &SubResult{Body: base64.StdEncoding.EncodeToString([]byte(joined())), ContentType: "text/plain; charset=utf-8"}, true
|
||||
case "clash", "clash-smart":
|
||||
return &SubResult{Body: MakeClash(items, selected == "clash-smart"), ContentType: "text/yaml; charset=utf-8"}, true
|
||||
case "singbox":
|
||||
return &SubResult{Body: MakeSingBox(items), ContentType: "application/json; charset=utf-8"}, true
|
||||
case "surge":
|
||||
body, ok := MakeSurge(items)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
return &SubResult{Body: body, ContentType: "text/plain; charset=utf-8"}, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
Reference in New Issue
Block a user