Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user