Files
Prism/internal/proxyio/proxyio.go
T
renjue fe8ea784d0
CI / docker (push) Successful in 2m54s
feat: Prism HTTP/SOCKS5 代理网关及管理台
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-23 15:58:28 +08:00

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