diff --git a/core/app.go b/core/app.go index 11e299a..e9d1ee7 100644 --- a/core/app.go +++ b/core/app.go @@ -37,6 +37,7 @@ var ( systemOnce *SystemSetup proxyOnce *Proxy httpServerOnce *HttpServer + ruleOnce *RuleSet ) func GetApp(assets embed.FS, wjs string) *App { @@ -120,6 +121,7 @@ ILKEQKmPPzKs7kp/7Nz+2cT3 initResource() initHttpServer() initSystem() + initRule() } return appOnce } diff --git a/core/config.go b/core/config.go index 0f752d5..bc5c553 100644 --- a/core/config.go +++ b/core/config.go @@ -37,6 +37,7 @@ type Config struct { UseHeaders string `json:"UseHeaders"` InsertTail bool `json:"InsertTail"` MimeMap map[string]MimeInfo `json:"MimeMap"` + Rule string `json:"Rule"` } var ( @@ -68,6 +69,7 @@ func initConfig() *Config { UseHeaders: "User-Agent,Referer,Authorization,Cookie", InsertTail: true, MimeMap: getDefaultMimeMap(), + Rule: "*", } rawDefaults, err := json.Marshal(defaultConfig) @@ -207,6 +209,7 @@ func getDefaultDownloadDir() string { func (c *Config) setConfig(config Config) { oldProxy := c.UpstreamProxy openProxy := c.OpenProxy + oldRule := c.Rule c.Host = config.Host c.Port = config.Port c.Theme = config.Theme @@ -225,10 +228,18 @@ func (c *Config) setConfig(config Config) { c.WxAction = config.WxAction c.UseHeaders = config.UseHeaders c.InsertTail = config.InsertTail + c.Rule = config.Rule if oldProxy != c.UpstreamProxy || openProxy != c.OpenProxy { proxyOnce.setTransport() } + if oldRule != c.Rule { + err := ruleOnce.Load(c.Rule) + if err != nil { + globalLogger.Esg(err, "set rule failed") + } + } + mimeMux.Lock() c.MimeMap = config.MimeMap mimeMux.Unlock() @@ -281,6 +292,8 @@ func (c *Config) getConfig(key string) interface{} { mimeMux.RLock() defer mimeMux.RUnlock() return c.MimeMap + case "Rule": + return c.Rule default: return nil } diff --git a/core/proxy.go b/core/proxy.go index a329851..6fe953a 100644 --- a/core/proxy.go +++ b/core/proxy.go @@ -80,7 +80,14 @@ func (p *Proxy) Startup() { //p.Proxy.KeepDestinationHeaders = true //p.Proxy.Verbose = false p.setTransport() - p.Proxy.OnRequest().HandleConnect(goproxy.AlwaysMitm) + //p.Proxy.OnRequest().HandleConnect(goproxy.AlwaysMitm) + p.Proxy.OnRequest().HandleConnectFunc(func(host string, ctx *goproxy.ProxyCtx) (*goproxy.ConnectAction, string) { + if ruleOnce.shouldMitm(host) { + return goproxy.MitmConnect, host + } + return goproxy.OkConnect, host + }) + p.Proxy.OnRequest().DoFunc(p.httpRequestEvent) p.Proxy.OnResponse().DoFunc(p.httpResponseEvent) } diff --git a/core/rule.go b/core/rule.go new file mode 100644 index 0000000..9f44ba0 --- /dev/null +++ b/core/rule.go @@ -0,0 +1,118 @@ +package core + +import ( + "bufio" + "net" + "strings" + "sync" +) + +type Rule struct { + raw string + isNeg bool // 是否否定规则(以 ! 开头) + isWildcard bool // 是否为 *.domain 形式 + domain string // 域名部分,不含 "*." +} + +type RuleSet struct { + mu sync.RWMutex + rules []Rule +} + +func initRule() *RuleSet { + if ruleOnce == nil { + ruleOnce = &RuleSet{} + err := ruleOnce.Load(globalConfig.Rule) + if err != nil { + globalLogger.Esg(err, "init rule failed") + return nil + } + } + return ruleOnce +} + +func (r *RuleSet) Load(rs string) error { + reader := strings.NewReader(rs) + scanner := bufio.NewScanner(reader) + + var rules []Rule + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + + isNeg := false + if strings.HasPrefix(line, "!") { + isNeg = true + line = strings.TrimSpace(line[1:]) + if line == "" { + continue + } + } + + isWildcard := false + domain := line + if strings.HasPrefix(line, "*.") { + isWildcard = true + domain = line[2:] + } + + rules = append(rules, Rule{ + raw: line, + isNeg: isNeg, + isWildcard: isWildcard, + domain: strings.ToLower(domain), + }) + } + + if err := scanner.Err(); err != nil { + return err + } + + r.mu.Lock() + r.rules = rules + r.mu.Unlock() + return nil +} + +// shouldMitm: 根据当前规则集判断是否对 host 做 MITM +// host 可能带端口(example.com:443),函数会只匹配 hostname 部分 +// 返回 true => MITM(解密),false => 透传 +func (r *RuleSet) shouldMitm(host string) bool { + h := host + if strings.HasPrefix(h, "[") { + if hostSplitIdx := strings.LastIndex(h, "]"); hostSplitIdx != -1 { + h = h[:hostSplitIdx+1] + } + } + if hp, _, err := net.SplitHostPort(host); err == nil { + h = hp + } + h = strings.ToLower(strings.Trim(h, "[]")) + + r.mu.RLock() + defer r.mu.RUnlock() + + action := false + for _, rule := range r.rules { + if rule.isWildcard { + if h == rule.domain || strings.HasSuffix(h, "."+rule.domain) { + if rule.isNeg { + action = false + } else { + action = true + } + } + } else { + if h == rule.domain { + if rule.isNeg { + action = false + } else { + action = true + } + } + } + } + return action +} diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index a64887a..6eda6f7 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -123,6 +123,8 @@ "use_headers_tip": "Define headers for downloads, comma separated", "mime_map": "Intercept Rules", "mime_map_tip": "JSON format, keep default if unsure, please restart software after modification", + "domain_rule": "Domain Rule", + "domain_rule_tip": "Default * matches all domains, One line for each rule,supports the following: \n*.qq.com\nvideo.qq.com\nexample.com\n\n# Exclude\n!static.qq.com", "port_format_error": "port format error", "host_format_error": "host format error", "basic_setting": "Basic Setting", diff --git a/frontend/src/locales/zh.json b/frontend/src/locales/zh.json index cc601d8..ddf4b41 100644 --- a/frontend/src/locales/zh.json +++ b/frontend/src/locales/zh.json @@ -123,6 +123,8 @@ "use_headers_tip": "定义下载时可使用的header参数,逗号分割", "mime_map": "拦截规则", "mime_map_tip": "json格式,如果不清楚保持默认就行,修改后请重启软件", + "domain_rule": "域名规则", + "domain_rule_tip": "默认*匹配所有域,每个规则一行,支持如下: \n*.qq.com\nvideo.qq.com\nexample.com\n\n# 排除\n!static.qq.com", "port_format_error": "port 格式错误", "host_format_error": "host 格式错误", "basic_setting": "基础设置", diff --git a/frontend/src/stores/index.ts b/frontend/src/stores/index.ts index 215a33d..ed2a435 100644 --- a/frontend/src/stores/index.ts +++ b/frontend/src/stores/index.ts @@ -33,7 +33,8 @@ export const useIndexStore = defineStore("index-store", () => { UserAgent: "", UseHeaders: "", InsertTail: true, - MimeMap: {} + MimeMap: {}, + Rule: "*" }) const envInfo = ref({ diff --git a/frontend/src/types/app.d.ts b/frontend/src/types/app.d.ts index 6686766..6cce76e 100644 --- a/frontend/src/types/app.d.ts +++ b/frontend/src/types/app.d.ts @@ -31,6 +31,7 @@ export namespace appType { UseHeaders: string InsertTail: boolean MimeMap: { [key: string]: MimeMap } + Rule: string } interface MediaInfo { diff --git a/frontend/src/views/setting.vue b/frontend/src/views/setting.vue index 6c673b1..8d34278 100644 --- a/frontend/src/views/setting.vue +++ b/frontend/src/views/setting.vue @@ -197,6 +197,22 @@ + + + + + {{ t("setting.domain_rule_tip") }} + +