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 } }