Files
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

169 lines
3.8 KiB
Go

package rulematch
import (
"net"
"net/url"
"strings"
"github.com/prism/proxy/internal/country"
"github.com/prism/proxy/internal/store"
)
type Result struct {
Matched bool `json:"matched"`
Host string `json:"host"`
Port string `json:"port,omitempty"`
RuleLine string `json:"rule_line"`
RuleType string `json:"rule_type"`
Payload string `json:"payload"`
Target string `json:"target"`
TargetLabel string `json:"target_label"`
Source string `json:"source"`
Priority int `json:"priority"`
SkippedGeo int `json:"skipped_geo"`
Note string `json:"note,omitempty"`
}
func ParseHost(raw string) (host, port string) {
raw = strings.TrimSpace(raw)
if raw == "" {
return "", ""
}
if !strings.Contains(raw, "://") {
raw = "http://" + raw
}
u, err := url.Parse(raw)
if err != nil {
return strings.TrimSpace(raw), ""
}
host = u.Hostname()
port = u.Port()
if host == "" {
host = strings.TrimSpace(raw)
}
return strings.ToLower(host), port
}
func Match(rules []store.Rule, defaultPolicy, rawURL string, geoReady bool) Result {
host, port := ParseHost(rawURL)
if host == "" {
return Result{Note: "无法解析 URL 或域名"}
}
skippedGeo := 0
for _, r := range rules {
if !r.Enabled {
continue
}
ruleType := strings.ToUpper(strings.TrimSpace(r.RuleType))
if ruleType == "MATCH" {
continue
}
if isGeoRule(ruleType) {
if !geoReady {
skippedGeo++
continue
}
// Geo rules need Mihomo geodata engine; skip in offline matcher.
skippedGeo++
continue
}
if matchesRule(ruleType, r.Payload, host, port) {
line := formatLine(r)
return Result{
Matched: true,
Host: host,
Port: port,
RuleLine: line,
RuleType: ruleType,
Payload: r.Payload,
Target: r.Target,
TargetLabel: targetLabel(r.Target),
Source: r.Source,
Priority: r.Priority,
SkippedGeo: skippedGeo,
}
}
}
policy := defaultPolicy
if policy == "" {
policy = "DIRECT"
}
return Result{
Matched: true,
Host: host,
Port: port,
RuleLine: "MATCH," + policy,
RuleType: "MATCH",
Payload: "",
Target: policy,
TargetLabel: targetLabel(policy),
Source: "system",
Priority: 999999,
SkippedGeo: skippedGeo,
Note: "未命中任何前置规则,使用默认出站",
}
}
func isGeoRule(ruleType string) bool {
switch ruleType {
case "GEOIP", "GEOSITE":
return true
default:
return false
}
}
func matchesRule(ruleType, payload, host, port string) bool {
payload = strings.TrimSpace(payload)
switch ruleType {
case "DOMAIN":
return host == strings.ToLower(payload)
case "DOMAIN-SUFFIX":
suffix := strings.ToLower(payload)
return host == suffix || strings.HasSuffix(host, "."+suffix)
case "DOMAIN-KEYWORD":
return strings.Contains(host, strings.ToLower(payload))
case "IP-CIDR", "IP-CIDR6":
return matchCIDR(host, payload)
case "DST-PORT":
return port != "" && port == strings.TrimSpace(payload)
case "SRC-PORT":
return false
default:
return false
}
}
func matchCIDR(host, cidr string) bool {
ip := net.ParseIP(host)
if ip == nil {
return false
}
_, network, err := net.ParseCIDR(strings.TrimSpace(cidr))
if err != nil {
return false
}
return network.Contains(ip)
}
func formatLine(r store.Rule) string {
if r.Payload == "" {
return r.RuleType + "," + r.Target
}
return r.RuleType + "," + r.Payload + "," + r.Target
}
func targetLabel(target string) string {
target = strings.TrimSpace(target)
if code, ok := country.ParseTargetAlias(target); ok {
return country.Name(code) + "(国家池)"
}
if strings.HasPrefix(target, "country_") {
code := strings.TrimPrefix(target, "country_")
return country.Name(code) + "(国家池)"
}
return target
}