4200f90159
Go 控制面(SQLite + REST API)嵌入 Mihomo 数据面,Vue 3 多语言 Web 管理台。 含订阅/节点/规则/出站/监控/日志、OpenAPI、API Key、Gitea Actions CI(runs-on: docker) 与开源文档;镜像发布至 git.rc707blog.top Container Registry。 Co-authored-by: Cursor <cursoragent@cursor.com>
270 lines
6.6 KiB
Go
270 lines
6.6 KiB
Go
package proxyio
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"github.com/prism/proxy/internal/country"
|
|
"github.com/prism/proxy/internal/store"
|
|
"github.com/prism/proxy/internal/subscription"
|
|
)
|
|
|
|
const ExportVersion = 1
|
|
|
|
type ExportDoc struct {
|
|
Version int `json:"version"`
|
|
Nodes []ExportNode `json:"nodes"`
|
|
}
|
|
|
|
type ExportNode struct {
|
|
Name string `json:"name"`
|
|
Type string `json:"type"`
|
|
RawConfig json.RawMessage `json:"raw_config"`
|
|
Country string `json:"country,omitempty"`
|
|
Enabled *bool `json:"enabled,omitempty"`
|
|
}
|
|
|
|
func nodeEnabled(n ExportNode) bool {
|
|
if n.Enabled == nil {
|
|
return true
|
|
}
|
|
return *n.Enabled
|
|
}
|
|
|
|
func ToExportNodes(nodes []store.ProxyNode) []ExportNode {
|
|
out := make([]ExportNode, 0, len(nodes))
|
|
for _, n := range nodes {
|
|
enabled := n.Enabled
|
|
raw := json.RawMessage(n.RawConfig)
|
|
if !json.Valid(raw) {
|
|
raw = json.RawMessage("{}")
|
|
}
|
|
out = append(out, ExportNode{
|
|
Name: n.Name,
|
|
Type: n.Type,
|
|
RawConfig: raw,
|
|
Country: n.Country,
|
|
Enabled: &enabled,
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func ExportJSON(nodes []store.ProxyNode) ([]byte, error) {
|
|
doc := ExportDoc{Version: ExportVersion, Nodes: ToExportNodes(nodes)}
|
|
return json.MarshalIndent(doc, "", " ")
|
|
}
|
|
|
|
func ExportLinks(nodes []store.ProxyNode) string {
|
|
var b strings.Builder
|
|
for _, n := range nodes {
|
|
if !n.Enabled {
|
|
continue
|
|
}
|
|
link, err := EncodeLink(n)
|
|
if err != nil {
|
|
b.WriteString("# ")
|
|
b.WriteString(n.Name)
|
|
b.WriteString(": ")
|
|
b.WriteString(err.Error())
|
|
b.WriteByte('\n')
|
|
continue
|
|
}
|
|
b.WriteString(link)
|
|
b.WriteByte('\n')
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func ParseJSON(data []byte) ([]ExportNode, error) {
|
|
var doc ExportDoc
|
|
if err := json.Unmarshal(data, &doc); err == nil && (len(doc.Nodes) > 0 || doc.Version > 0) {
|
|
return doc.Nodes, nil
|
|
}
|
|
var nodes []ExportNode
|
|
if err := json.Unmarshal(data, &nodes); err != nil {
|
|
return nil, fmt.Errorf("invalid JSON: %w", err)
|
|
}
|
|
return nodes, nil
|
|
}
|
|
|
|
func ParseText(text string) ([]store.ProxyNode, []string) {
|
|
text = strings.TrimSpace(text)
|
|
if text == "" {
|
|
return nil, nil
|
|
}
|
|
if strings.HasPrefix(text, "{") || strings.HasPrefix(text, "[") {
|
|
items, err := ParseJSON([]byte(text))
|
|
if err == nil && len(items) > 0 {
|
|
nodes, errs := ToStoreNodes(items)
|
|
return nodes, errs
|
|
}
|
|
}
|
|
return subscription.ParseLinkLines(text)
|
|
}
|
|
|
|
func ToStoreNodes(items []ExportNode) ([]store.ProxyNode, []string) {
|
|
out := make([]store.ProxyNode, 0, len(items))
|
|
var errs []string
|
|
for i, item := range items {
|
|
name := strings.TrimSpace(item.Name)
|
|
typ := strings.TrimSpace(strings.ToLower(item.Type))
|
|
raw := normalizeRawConfig(item.RawConfig)
|
|
if name == "" {
|
|
errs = append(errs, fmt.Sprintf("节点 %d: 缺少名称", i+1))
|
|
continue
|
|
}
|
|
if typ == "" {
|
|
errs = append(errs, fmt.Sprintf("节点 %d: 缺少类型", i+1))
|
|
continue
|
|
}
|
|
if raw == "" || raw == "{}" {
|
|
errs = append(errs, fmt.Sprintf("节点 %d: 缺少配置", i+1))
|
|
continue
|
|
}
|
|
code := strings.TrimSpace(item.Country)
|
|
if code == "" {
|
|
code = country.Detect(name)
|
|
}
|
|
out = append(out, store.ProxyNode{
|
|
Name: name,
|
|
RuntimeName: store.RuntimeName("manual", 0, name),
|
|
Type: typ,
|
|
RawConfig: raw,
|
|
Source: "manual",
|
|
Country: code,
|
|
Enabled: nodeEnabled(item),
|
|
})
|
|
}
|
|
return out, errs
|
|
}
|
|
|
|
func normalizeRawConfig(raw json.RawMessage) string {
|
|
if len(raw) == 0 {
|
|
return ""
|
|
}
|
|
if raw[0] == '{' || raw[0] == '[' {
|
|
if !json.Valid(raw) {
|
|
return ""
|
|
}
|
|
return string(raw)
|
|
}
|
|
var s string
|
|
if err := json.Unmarshal(raw, &s); err == nil {
|
|
return strings.TrimSpace(s)
|
|
}
|
|
return strings.TrimSpace(string(raw))
|
|
}
|
|
|
|
func AssignUniqueNames(nodes []store.ProxyNode, reserved map[string]bool) {
|
|
if reserved == nil {
|
|
reserved = map[string]bool{}
|
|
}
|
|
for i := range nodes {
|
|
name := strings.TrimSpace(nodes[i].Name)
|
|
if name == "" {
|
|
name = "node"
|
|
}
|
|
base := name
|
|
candidate := base
|
|
for n := 2; reserved[candidate]; n++ {
|
|
candidate = fmt.Sprintf("%s_%d", base, n)
|
|
}
|
|
reserved[candidate] = true
|
|
nodes[i].Name = candidate
|
|
nodes[i].RuntimeName = store.RuntimeName("manual", 0, candidate)
|
|
if nodes[i].Country == "" {
|
|
nodes[i].Country = country.Detect(candidate)
|
|
}
|
|
}
|
|
}
|
|
|
|
func ReservedNames(nodes []store.ProxyNode) map[string]bool {
|
|
m := make(map[string]bool, len(nodes))
|
|
for _, n := range nodes {
|
|
m[n.Name] = true
|
|
}
|
|
return m
|
|
}
|
|
|
|
func EncodeLink(n store.ProxyNode) (string, error) {
|
|
var raw map[string]any
|
|
if err := json.Unmarshal([]byte(n.RawConfig), &raw); err != nil {
|
|
return "", err
|
|
}
|
|
fragment := url.QueryEscape(n.Name)
|
|
typ := strings.ToLower(n.Type)
|
|
switch typ {
|
|
case "socks5":
|
|
return encodeSocks5(raw, fragment)
|
|
case "http", "https":
|
|
return encodeHTTP(raw, fragment, typ == "https")
|
|
case "trojan":
|
|
return encodeTrojan(raw, fragment)
|
|
default:
|
|
return "", fmt.Errorf("类型 %s 暂不支持导出为分享链接", n.Type)
|
|
}
|
|
}
|
|
|
|
func encodeSocks5(raw map[string]any, fragment string) (string, error) {
|
|
server, _ := raw["server"].(string)
|
|
port := intFromAny(raw["port"])
|
|
if server == "" || port <= 0 {
|
|
return "", fmt.Errorf("socks5 配置不完整")
|
|
}
|
|
host := fmt.Sprintf("%s:%d", server, port)
|
|
user, _ := raw["username"].(string)
|
|
pass, _ := raw["password"].(string)
|
|
if user != "" {
|
|
return fmt.Sprintf("socks5://%s:%s@%s#%s", url.QueryEscape(user), url.QueryEscape(pass), host, fragment), nil
|
|
}
|
|
return fmt.Sprintf("socks5://%s#%s", host, fragment), nil
|
|
}
|
|
|
|
func encodeHTTP(raw map[string]any, fragment string, tls bool) (string, error) {
|
|
server, _ := raw["server"].(string)
|
|
port := intFromAny(raw["port"])
|
|
if server == "" || port <= 0 {
|
|
return "", fmt.Errorf("http 配置不完整")
|
|
}
|
|
scheme := "http"
|
|
if tls {
|
|
scheme = "https"
|
|
}
|
|
host := fmt.Sprintf("%s:%d", server, port)
|
|
user, _ := raw["username"].(string)
|
|
pass, _ := raw["password"].(string)
|
|
if user != "" {
|
|
return fmt.Sprintf("%s://%s:%s@%s#%s", scheme, url.QueryEscape(user), url.QueryEscape(pass), host, fragment), nil
|
|
}
|
|
return fmt.Sprintf("%s://%s#%s", scheme, host, fragment), nil
|
|
}
|
|
|
|
func encodeTrojan(raw map[string]any, fragment string) (string, error) {
|
|
server, _ := raw["server"].(string)
|
|
port := intFromAny(raw["port"])
|
|
pass, _ := raw["password"].(string)
|
|
if server == "" || port <= 0 || pass == "" {
|
|
return "", fmt.Errorf("trojan 配置不完整")
|
|
}
|
|
return fmt.Sprintf("trojan://%s@%s:%d#%s", url.QueryEscape(pass), server, port, fragment), nil
|
|
}
|
|
|
|
func intFromAny(v any) int {
|
|
switch n := v.(type) {
|
|
case float64:
|
|
return int(n)
|
|
case int:
|
|
return n
|
|
case int64:
|
|
return int(n)
|
|
case json.Number:
|
|
i, _ := n.Int64()
|
|
return int(i)
|
|
default:
|
|
return 0
|
|
}
|
|
}
|