Files
Prism/internal/configbuilder/builder.go
T
rose_cat707 024c15a836
CI / test (push) Has been cancelled
CI / docker (push) Has been cancelled
feat: Prism HTTP/SOCKS5 代理网关及管理台
Go 控制面(SQLite + REST API)嵌入 Mihomo 数据面,Vue 3 多语言 Web 管理台。
含订阅/节点/规则/出站/监控/日志、OpenAPI、API Key、Gitea Actions CI 与开源文档。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-23 12:18:58 +08:00

368 lines
8.7 KiB
Go

package configbuilder
import (
"context"
"encoding/json"
"fmt"
"sort"
"strings"
"github.com/prism/proxy/internal/country"
"github.com/prism/proxy/internal/geodata"
"github.com/prism/proxy/internal/store"
"gopkg.in/yaml.v3"
)
type Builder struct {
store *store.Store
dataDir string
}
func New(s *store.Store, dataDir string) *Builder {
return &Builder{store: s, dataDir: dataDir}
}
func (b *Builder) Build(ctx context.Context) ([]byte, error) {
return b.build(ctx, false)
}
// BuildMinimal loads nodes/groups with MATCH-only rules when full config fails.
func (b *Builder) BuildMinimal(ctx context.Context) ([]byte, error) {
return b.build(ctx, true)
}
func (b *Builder) build(ctx context.Context, minimalRules bool) ([]byte, error) {
settings, err := b.store.GetSettings(ctx)
if err != nil {
return nil, err
}
httpPort := settings["http_port"]
socksPort := settings["socks_port"]
logLevel := settings["log_level"]
if logLevel == "" {
logLevel = "info"
}
defaultPolicy := settings["default_policy"]
if defaultPolicy == "" {
defaultPolicy = "DIRECT"
}
nodes, err := b.store.ListProxyNodes(ctx, "", nil)
if err != nil {
return nil, err
}
groups, err := b.store.ListProxyGroups(ctx)
if err != nil {
return nil, err
}
mergedRules, err := b.store.ListMergedRules(ctx, defaultPolicy)
if err != nil {
return nil, err
}
if minimalRules {
mergedRules = nil
}
cfg := map[string]any{
"port": atoi(httpPort, 7890),
"socks-port": atoi(socksPort, 7891),
"allow-lan": true,
"mode": "rule",
"log-level": logLevel,
"proxies": []any{},
"proxy-groups": []any{
map[string]any{
"name": "PROXY",
"type": "select",
"proxies": []string{"DIRECT"},
},
},
"rules": []string{"MATCH," + defaultPolicy},
}
applyInboundAuth(cfg, settings)
var proxies []any
proxyNames := []string{}
for _, n := range nodes {
if !n.Enabled {
continue
}
var proxy map[string]any
if err := json.Unmarshal([]byte(n.RawConfig), &proxy); err != nil {
continue
}
proxy["name"] = n.RuntimeName
proxies = append(proxies, proxy)
proxyNames = append(proxyNames, n.RuntimeName)
}
cfg["proxies"] = proxies
var proxyGroups []any
hasGroups := false
for _, g := range groups {
if !g.Enabled {
continue
}
hasGroups = true
var members []string
_ = json.Unmarshal([]byte(g.Proxies), &members)
remapped := remapMembers(members, nodes, groups)
proxyGroups = append(proxyGroups, buildProxyGroup(g.RuntimeName, g.Type, remapped))
}
if !hasGroups && len(proxyNames) > 0 {
proxyGroups = []any{
map[string]any{
"name": "PROXY",
"type": "select",
"proxies": append(proxyNames, "DIRECT"),
},
}
} else if len(proxyGroups) > 0 {
cfg["proxy-groups"] = proxyGroups
} else {
cfg["proxy-groups"] = []any{
map[string]any{
"name": "PROXY",
"type": "select",
"proxies": []string{"DIRECT"},
},
}
}
if len(proxyGroups) > 0 {
cfg["proxy-groups"] = proxyGroups
}
proxyGroups = appendCountryProxyGroups(proxyGroups, nodes)
cfg["proxy-groups"] = proxyGroups
nameMap := buildNameMap(nodes, groups)
countryNameMap, countryValid := countryMaps(nodes)
for k, v := range countryNameMap {
nameMap[k] = v
}
valid := buildValidTargets(nodes, groups)
for k := range countryValid {
valid[k] = true
}
defaultPolicy = resolvePolicy(defaultPolicy, nameMap, valid, groups)
cfg["rules"] = sanitizeRules(mergedRules, nameMap, valid, defaultPolicy, geodata.Ready(b.dataDir))
out, err := yaml.Marshal(cfg)
if err != nil {
return nil, err
}
return out, nil
}
func buildNameMap(nodes []store.ProxyNode, groups []store.ProxyGroup) map[string]string {
nameMap := make(map[string]string)
for _, n := range nodes {
nameMap[n.Name] = n.RuntimeName
nameMap[n.RuntimeName] = n.RuntimeName
}
for _, g := range groups {
nameMap[g.Name] = g.RuntimeName
nameMap[g.RuntimeName] = g.RuntimeName
}
return nameMap
}
func remapMembers(members []string, nodes []store.ProxyNode, groups []store.ProxyGroup) []string {
nameMap := buildNameMap(nodes, groups)
out := make([]string, 0, len(members))
for _, name := range members {
if name == "DIRECT" || name == "REJECT" {
out = append(out, name)
continue
}
if rn, ok := nameMap[name]; ok {
out = append(out, rn)
}
}
return out
}
func remapRuleTarget(rule string, nameMap map[string]string) string {
parts := strings.Split(rule, ",")
if len(parts) < 2 {
return rule
}
target := parts[len(parts)-1]
if code, ok := country.ParseTargetAlias(target); ok {
parts[len(parts)-1] = country.GroupRuntime(code)
return strings.Join(parts, ",")
}
if mapped, ok := nameMap[target]; ok {
parts[len(parts)-1] = mapped
return strings.Join(parts, ",")
}
return rule
}
func remapProxyNames(names []string, nodes []store.ProxyNode) []string {
return remapMembers(names, nodes, nil)
}
func buildProxyGroup(name, gtype string, members []string) map[string]any {
group := map[string]any{
"name": name,
"type": gtype,
"proxies": appendGroupMembers(gtype, members),
}
switch gtype {
case "url-test", "fallback":
group["url"] = "http://www.gstatic.com/generate_204"
group["interval"] = 300
}
return group
}
func appendGroupMembers(gtype string, members []string) []string {
switch strings.ToLower(gtype) {
case "url-test", "fallback":
return members
default:
return append(append([]string{}, members...), "DIRECT")
}
}
func buildValidTargets(nodes []store.ProxyNode, groups []store.ProxyGroup) map[string]bool {
valid := map[string]bool{
"DIRECT": true,
"REJECT": true,
}
for _, n := range nodes {
if n.Enabled {
valid[n.RuntimeName] = true
}
}
for _, g := range groups {
if g.Enabled {
valid[g.RuntimeName] = true
}
}
return valid
}
func resolvePolicy(policy string, nameMap map[string]string, valid map[string]bool, groups []store.ProxyGroup) string {
if code, ok := country.ParseTargetAlias(policy); ok {
rn := country.GroupRuntime(code)
if valid[rn] {
return rn
}
}
if mapped, ok := nameMap[policy]; ok && valid[mapped] {
return mapped
}
if valid[policy] {
return policy
}
if policy == "PROXY" || policy == "GLOBAL" {
for _, g := range groups {
if g.Enabled && valid[g.RuntimeName] {
return g.RuntimeName
}
}
}
return "DIRECT"
}
func sanitizeRules(merged []store.MergedRule, nameMap map[string]string, valid map[string]bool, defaultPolicy string, geoReady bool) []string {
var rules []string
hasMatch := false
for _, r := range merged {
line := remapRuleTarget(r.Rule, nameMap)
if !geoReady && needsGeoData(line) {
continue
}
if strings.HasPrefix(line, "MATCH,") {
line = "MATCH," + defaultPolicy
hasMatch = true
}
target := ruleTarget(line)
if target == "" || !valid[target] {
continue
}
rules = append(rules, line)
}
if !hasMatch {
rules = append(rules, "MATCH,"+defaultPolicy)
}
return rules
}
func needsGeoData(rule string) bool {
upper := strings.ToUpper(rule)
if strings.HasPrefix(upper, "GEOIP,") || strings.HasPrefix(upper, "GEOSITE,") {
return true
}
return strings.Contains(strings.ToLower(rule), "geoip:")
}
func ruleTarget(line string) string {
parts := strings.Split(line, ",")
if len(parts) < 2 {
return ""
}
return parts[len(parts)-1]
}
func applyInboundAuth(cfg map[string]any, settings map[string]string) {
user := strings.TrimSpace(settings["proxy_auth_user"])
pass := settings["proxy_auth_pass"]
if user == "" || pass == "" {
return
}
cfg["authentication"] = []string{user + ":" + pass}
cfg["skip-auth-prefixes"] = []string{"127.0.0.1/8", "::1/128"}
}
func atoi(s string, def int) int {
var v int
if _, err := fmt.Sscanf(strings.TrimSpace(s), "%d", &v); err != nil || v <= 0 {
return def
}
return v
}
func appendCountryProxyGroups(groups []any, nodes []store.ProxyNode) []any {
byCountry := map[string][]string{}
for _, n := range nodes {
if !n.Enabled || n.Country == "" {
continue
}
byCountry[n.Country] = append(byCountry[n.Country], n.RuntimeName)
}
codes := make([]string, 0, len(byCountry))
for code := range byCountry {
codes = append(codes, code)
}
sort.Strings(codes)
for _, code := range codes {
members := byCountry[code]
if len(members) == 0 {
continue
}
groups = append(groups, buildProxyGroup(country.GroupRuntime(code), "url-test", members))
}
return groups
}
func countryMaps(nodes []store.ProxyNode) (map[string]string, map[string]bool) {
nameMap := map[string]string{}
valid := map[string]bool{}
seen := map[string]bool{}
for _, n := range nodes {
if !n.Enabled || n.Country == "" || seen[n.Country] {
continue
}
seen[n.Country] = true
rn := country.GroupRuntime(n.Country)
nameMap[country.TargetAlias(n.Country)] = rn
nameMap[country.Name(n.Country)] = rn
nameMap[rn] = rn
valid[rn] = true
}
return nameMap, valid
}