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 }