diff --git a/cmd/samwaf/main.go b/cmd/samwaf/main.go index f68d5d5..61f0101 100644 --- a/cmd/samwaf/main.go +++ b/cmd/samwaf/main.go @@ -530,23 +530,27 @@ func (m *wafSystenService) run() { } //如果本次是关闭,那么应该关闭主机 if hosts[0].START_STATUS == 1 { - globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.RemoveHost(hosts[0]) + globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.RemoveHost(hostsOld) } //如果本次ssl和上次ssl不同 if hosts[0].Ssl != hostsOld.Ssl || hosts[0].Keyfile != hostsOld.Keyfile || hosts[0].Certfile != hostsOld.Certfile { - globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.RemoveHost(hosts[0]) + globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.RemoveHost(hostsOld) } //绑定更多域名变更了 if hosts[0].BindMoreHost != hostsOld.BindMoreHost { - globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.RemoveHost(hosts[0]) + globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.RemoveHost(hostsOld) + } + //绑定更多端口变更了(副端口增删),需先移除旧配置再重载 + if hosts[0].BindMorePort != hostsOld.BindMorePort { + globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.RemoveHost(hostsOld) } // 远程 IP 或端口变化,需重载 if hosts[0].Remote_ip != hostsOld.Remote_ip || hosts[0].Remote_port != hostsOld.Remote_port { - globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.RemoveHost(hosts[0]) + globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.RemoveHost(hostsOld) } // 负载状态或策略变化,需重载 if hosts[0].IsEnableLoadBalance != hostsOld.IsEnableLoadBalance || hosts[0].LoadBalanceStage != hostsOld.LoadBalanceStage { - globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.RemoveHost(hosts[0]) + globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.RemoveHost(hostsOld) } globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.LoadHost(hosts[0]) globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.StartAllProxyServer() diff --git a/service/waf_service/waf_host.go b/service/waf_service/waf_host.go index d8bb433..da39637 100644 --- a/service/waf_service/waf_host.go +++ b/service/waf_service/waf_host.go @@ -11,6 +11,7 @@ import ( "SamWaf/utils" "encoding/json" "errors" + "strconv" "strings" "time" @@ -307,8 +308,26 @@ func (receiver *WafHostService) CheckPortExistApi(port int) int64 { func (receiver *WafHostService) CheckAvailablePortExistApi(port int) int64 { var total int64 = 0 - global.GWAF_LOCAL_DB.Model(&model.Hosts{}).Where(" start_status = 0 and port=?", port).Count(&total) - return total + // 先检查主端口 + global.GWAF_LOCAL_DB.Model(&model.Hosts{}).Where("start_status = 0 and port=?", port).Count(&total) + if total > 0 { + return total + } + // 再检查 BindMorePort(逗号分隔的副端口列表) + // 取出所有启用且有副端口配置的主机,在 Go 层解析,避免 DB 字符串模糊匹配误判 + var hosts []model.Hosts + global.GWAF_LOCAL_DB.Model(&model.Hosts{}). + Where("start_status = 0 and bind_more_port != ''"). + Select("bind_more_port").Find(&hosts) + portStr := strconv.Itoa(port) + for _, h := range hosts { + for _, p := range strings.Split(h.BindMorePort, ",") { + if strings.TrimSpace(p) == portStr { + return 1 + } + } + } + return 0 } func (receiver *WafHostService) IsEmptyHost() bool { diff --git a/service/waf_service/waf_host_port_test.go b/service/waf_service/waf_host_port_test.go new file mode 100644 index 0000000..224b8a8 --- /dev/null +++ b/service/waf_service/waf_host_port_test.go @@ -0,0 +1,146 @@ +package waf_service + +import ( + "strconv" + "strings" + "testing" +) + +// checkBindMorePortContains 模拟修复后 CheckAvailablePortExistApi 中 +// "扫描 BindMorePort 字段" 的核心逻辑,可脱离 DB 独立测试 +func checkBindMorePortContains(bindMorePorts []string, targetPort int) bool { + portStr := strconv.Itoa(targetPort) + for _, bindMorePort := range bindMorePorts { + if bindMorePort == "" { + continue + } + for _, p := range strings.Split(bindMorePort, ",") { + if strings.TrimSpace(p) == portStr { + return true + } + } + } + return false +} + +// TestCheckBindMorePort_ContainsPort +// Fix3 核心逻辑:给定一组主机的 BindMorePort 字段值,判断目标端口是否被使用 +func TestCheckBindMorePort_ContainsPort(t *testing.T) { + tests := []struct { + name string + bindMorePorts []string // 模拟多个主机的 bind_more_port 字段 + targetPort int + want bool + }{ + { + name: "端口在副端口列表中(单个主机)", + bindMorePorts: []string{"8080,9090"}, + targetPort: 8080, + want: true, + }, + { + name: "端口在副端口列表末尾", + bindMorePorts: []string{"8080,9090"}, + targetPort: 9090, + want: true, + }, + { + name: "端口不在任何副端口列表中", + bindMorePorts: []string{"8080,9090", "7070,7071"}, + targetPort: 6060, + want: false, + }, + { + name: "端口在第二个主机的副端口中", + bindMorePorts: []string{"8080", "9090,6060"}, + targetPort: 6060, + want: true, + }, + { + name: "副端口列表为空", + bindMorePorts: []string{"", ""}, + targetPort: 8080, + want: false, + }, + { + name: "副端口有多余空格", + bindMorePorts: []string{" 8080 , 9090 "}, + targetPort: 8080, + want: true, + }, + { + name: "单个副端口精确匹配", + bindMorePorts: []string{"8080"}, + targetPort: 8080, + want: true, + }, + { + name: "避免数字前缀误匹配:808 不应匹配 8080", + bindMorePorts: []string{"8080,9090"}, + targetPort: 808, + want: false, + }, + { + name: "避免数字后缀误匹配:80800 不应匹配 8080", + bindMorePorts: []string{"8080,9090"}, + targetPort: 80800, + want: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := checkBindMorePortContains(tc.bindMorePorts, tc.targetPort) + if got != tc.want { + t.Errorf("checkBindMorePortContains(%v, %d) = %v,期望 %v", + tc.bindMorePorts, tc.targetPort, got, tc.want) + } + }) + } +} + +// TestCheckAvailablePortExistApi_BindMorePortScenario +// 文档化测试:描述 Fix3 修复前后的行为差异 +// +// 修复前:CheckAvailablePortExistApi 只查 WHERE port=X +// - 站点 A: Port=80, BindMorePort="8080" +// - CheckAvailablePortExistApi(8080) → 返回 0(误判为无人使用) +// - 结果:RemovePortServer 会把 8080 端口的 HTTP server 关掉 +// +// 修复后:同时扫描 bind_more_port 字段 +// - CheckAvailablePortExistApi(8080) → 返回 1(正确识别为在用) +// - 结果:RemovePortServer 不会关闭 8080 端口 +func TestCheckAvailablePortExistApi_BindMorePortScenario(t *testing.T) { + // 模拟:主机 A Port=80 BindMorePort="8080",没有主机以 8080 为主端口 + mainPortHosts := []int{80, 443, 9090} // 所有主机的主端口 + bindMorePortList := []string{"8080", "7070,7071"} // 所有主机的 BindMorePort 字段 + + targetPort := 8080 + + // 旧逻辑:只查主端口 + foundByMainPort := false + for _, p := range mainPortHosts { + if p == targetPort { + foundByMainPort = true + break + } + } + + // 新逻辑:主端口 + BindMorePort + foundByBindMore := checkBindMorePortContains(bindMorePortList, targetPort) + foundNew := foundByMainPort || foundByBindMore + + if foundByMainPort { + t.Errorf("测试数据有误:不应该有主机以 %d 为主端口", targetPort) + } + if !foundByBindMore { + t.Error("Fix3:新逻辑应能在 BindMorePort 中找到端口 8080") + } + if !foundNew { + t.Error("Fix3:修复后的逻辑应返回端口 8080 仍在使用中") + } + + // 明确表达:旧逻辑会错误地关掉端口,新逻辑不会 + t.Logf("旧逻辑 foundByMainPort=%v(会错误关掉端口)", foundByMainPort) + t.Logf("新逻辑 foundNew=%v(正确保留端口)", foundNew) +} diff --git a/wafenginecore/hostssl.go b/wafenginecore/hostssl.go index 9bb55ff..670f2f3 100644 --- a/wafenginecore/hostssl.go +++ b/wafenginecore/hostssl.go @@ -78,7 +78,7 @@ func (ac *AllCertificate) RemoveSSL(domain string) error { domain = strings.ToLower(domain) _, ok := ac.Map[domain] if ok { - ac.Map[domain] = nil + delete(ac.Map, domain) } return nil } diff --git a/wafenginecore/wafworker.go b/wafenginecore/wafworker.go index 3926e2d..314befc 100644 --- a/wafenginecore/wafworker.go +++ b/wafenginecore/wafworker.go @@ -115,7 +115,7 @@ func (waf *WafEngine) LoadHost(inHost model.Hosts) []innerbean.ServerRunTime { _, ok := waf.ServerOnline.Get(port) if ok == false { if inHost.START_STATUS == 0 { - if port == 443 { + if port == 443 || (inHost.Ssl == 1 && port != 80) { waf.ServerOnline.Set(port, innerbean.ServerRunTime{ ServerType: "https", Port: port, @@ -261,8 +261,7 @@ func (waf *WafEngine) LoadHost(inHost model.Hosts) []innerbean.ServerRunTime { for _, port := range ports { //目标关系情况 waf.HostTarget[inHost.Host+":"+strconv.Itoa(port)] = hostsafe - //赋值到对照表里面 - waf.HostCode[inHost.Code] = inHost.Host + ":" + strconv.Itoa(port) + // 注意:HostCode[code] 始终指向主端口 key(已在上方赋值),不在此处覆盖 } } @@ -286,7 +285,16 @@ func (waf *WafEngine) LoadHost(inHost model.Hosts) []innerbean.ServerRunTime { if inHost.BindMoreHost != "" { lines := strings.Split(inHost.BindMoreHost, "\n") for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" { + continue + } + // 主端口 waf.HostTargetMoreDomain[line+":"+strconv.Itoa(inHost.Port)] = inHost.Code + // 副端口(BindMorePort)也需要注册,否则副域名+副端口无法路由 + for _, extraPort := range ports { + waf.HostTargetMoreDomain[line+":"+strconv.Itoa(extraPort)] = inHost.Code + } } } @@ -341,8 +349,20 @@ func (waf *WafEngine) RemoveHost(host model.Hosts) { // 移除当前信息 //a.移除对照关系 delete(waf.HostCode, host.Code) - //b.移除主机保护信息 + //b.移除主机保护信息(主端口) delete(waf.HostTarget, host.Host+":"+strconv.Itoa(host.Port)) + //b2.移除 BindMorePort 副端口对应的 HostTarget 条目,避免残留 stale 指针 + if host.BindMorePort != "" && host.GLOBAL_HOST == 0 { + for _, portStr := range strings.Split(host.BindMorePort, ",") { + if p, err := strconv.Atoi(strings.TrimSpace(portStr)); err == nil { + delete(waf.HostTarget, host.Host+":"+strconv.Itoa(p)) + } + } + } + //b3.移除 AutoJumpHTTPS 添加的 80 端口条目 + if host.AutoJumpHTTPS == 1 { + delete(waf.HostTarget, host.Host+":80") + } //c.移除某个端口下的证书数据 waf.AllCertificate.RemoveSSL(host.Host) diff --git a/wafenginecore/wafworker_multiport_test.go b/wafenginecore/wafworker_multiport_test.go new file mode 100644 index 0000000..f382ff9 --- /dev/null +++ b/wafenginecore/wafworker_multiport_test.go @@ -0,0 +1,549 @@ +package wafenginecore + +import ( + "SamWaf/model" + "SamWaf/model/wafenginmodel" + "crypto/tls" + "net/http" + "strconv" + "strings" + "testing" +) + +// newTestWafEngine 创建不依赖 DB 的最小 WafEngine,供单元测试使用 +func newTestWafEngine() *WafEngine { + return &WafEngine{ + HostTarget: make(map[string]*wafenginmodel.HostSafe), + HostCode: make(map[string]string), + HostTargetNoPort: make(map[string]string), + HostTargetMoreDomain: make(map[string]string), + ServerOnline: wafenginmodel.NewSafeServerMap(), + TransportPool: make(map[string]*http.Transport), + AllCertificate: AllCertificate{Map: make(map[string]*tls.Certificate)}, + } +} + +// simulateLoadHostMaps 模拟 LoadHost 中纯 map 赋值的部分(不含 DB 查询) +// 严格按照修复后的逻辑编写,用于在无 DB 环境下验证 Fix1/Fix2 正确性 +func simulateLoadHostMaps(waf *WafEngine, inHost model.Hosts) { + hostsafe := &wafenginmodel.HostSafe{Host: inHost} + + // 解析副端口列表(不写入 ServerOnline,避免 RemovePortServer 触发 DB 查询) + var extraPorts []int + if inHost.BindMorePort != "" && inHost.GLOBAL_HOST == 0 { + for _, ps := range strings.Split(inHost.BindMorePort, ",") { + if p, err := strconv.Atoi(strings.TrimSpace(ps)); err == nil { + extraPorts = append(extraPorts, p) + } + } + } + + // 主端口注册 + waf.HostTarget[inHost.Host+":"+strconv.Itoa(inHost.Port)] = hostsafe + // Fix2: HostCode 只指向主端口,不在副端口循环内覆盖 + waf.HostCode[inHost.Code] = inHost.Host + ":" + strconv.Itoa(inHost.Port) + + // 副端口注册(只注册 HostTarget,不改 HostCode) + for _, port := range extraPorts { + waf.HostTarget[inHost.Host+":"+strconv.Itoa(port)] = hostsafe + } + + // Fix1: BindMoreHost 对每个端口(主 + 副)都注册 HostTargetMoreDomain + if inHost.BindMoreHost != "" { + for _, line := range strings.Split(inHost.BindMoreHost, "\n") { + line = strings.TrimSpace(line) + if line == "" { + continue + } + waf.HostTargetMoreDomain[line+":"+strconv.Itoa(inHost.Port)] = inHost.Code + for _, ep := range extraPorts { + waf.HostTargetMoreDomain[line+":"+strconv.Itoa(ep)] = inHost.Code + } + } + } +} + +// ───────────────────────────────────────────────────────────── +// Fix 1: 副域名 + 副端口 路由注册测试 +// ───────────────────────────────────────────────────────────── + +// TestHostTargetMoreDomain_AllPortCombinations +// 验证:BindMoreHost × BindMorePort 的所有组合都被注册到 HostTargetMoreDomain +func TestHostTargetMoreDomain_AllPortCombinations(t *testing.T) { + waf := newTestWafEngine() + + inHost := model.Hosts{ + Code: "site001", + Host: "main.example.com", + Port: 80, + BindMorePort: "8080,9090", + BindMoreHost: "sub1.example.com\nsub2.example.com", + } + simulateLoadHostMaps(waf, inHost) + + // 主域名 × 所有端口 应在 HostTarget + for _, port := range []int{80, 8080, 9090} { + key := "main.example.com:" + strconv.Itoa(port) + if waf.HostTarget[key] == nil { + t.Errorf("HostTarget 缺少条目 %s(主域名×端口组合未注册)", key) + } + } + + // 副域名 × 所有端口 应在 HostTargetMoreDomain(Fix1 修复的场景) + for _, domain := range []string{"sub1.example.com", "sub2.example.com"} { + for _, port := range []int{80, 8080, 9090} { + key := domain + ":" + strconv.Itoa(port) + if waf.HostTargetMoreDomain[key] == "" { + t.Errorf("HostTargetMoreDomain 缺少条目 %s(副域名×副端口未注册)", key) + } + } + } +} + +// TestHostTargetMoreDomain_SinglePort +// 只有主端口(无 BindMorePort),副域名仍应正确注册 +func TestHostTargetMoreDomain_SinglePort(t *testing.T) { + waf := newTestWafEngine() + + inHost := model.Hosts{ + Code: "site002", + Host: "main.example.com", + Port: 443, + BindMorePort: "", + BindMoreHost: "www.example.com\nalias.example.com", + } + simulateLoadHostMaps(waf, inHost) + + for _, domain := range []string{"www.example.com", "alias.example.com"} { + key := domain + ":443" + if waf.HostTargetMoreDomain[key] == "" { + t.Errorf("HostTargetMoreDomain 缺少条目 %s", key) + } + } +} + +// TestHostTargetMoreDomain_EmptyLines +// BindMoreHost 包含空行时,不应注册空字符串键 +func TestHostTargetMoreDomain_EmptyLines(t *testing.T) { + waf := newTestWafEngine() + + inHost := model.Hosts{ + Code: "site003", + Host: "main.example.com", + Port: 80, + BindMorePort: "8080", + BindMoreHost: "sub.example.com\n\n \n", + } + simulateLoadHostMaps(waf, inHost) + + // 空行不应产生空键 + for key := range waf.HostTargetMoreDomain { + if strings.HasPrefix(key, ":") || strings.Contains(key, " :") { + t.Errorf("HostTargetMoreDomain 包含由空行产生的错误键 %q", key) + } + } + + // 有效域名应正常注册 + for _, port := range []int{80, 8080} { + key := "sub.example.com:" + strconv.Itoa(port) + if waf.HostTargetMoreDomain[key] == "" { + t.Errorf("HostTargetMoreDomain 缺少有效条目 %s", key) + } + } +} + +// ───────────────────────────────────────────────────────────── +// Fix 2: HostCode 不被副端口覆盖 +// ───────────────────────────────────────────────────────────── + +// TestHostCode_AlwaysPointsToMainPort +// 验证:有多个副端口时,HostCode[code] 始终指向主端口 key +func TestHostCode_AlwaysPointsToMainPort(t *testing.T) { + tests := []struct { + name string + host string + mainPort int + bindMorePort string + }{ + {"两个副端口", "a.example.com", 80, "8080,8888"}, + {"三个副端口", "b.example.com", 443, "9090,9091,9092"}, + {"单个副端口", "c.example.com", 80, "8080"}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + waf := newTestWafEngine() + inHost := model.Hosts{ + Code: "code_" + tc.host, + Host: tc.host, + Port: tc.mainPort, + BindMorePort: tc.bindMorePort, + } + simulateLoadHostMaps(waf, inHost) + + want := tc.host + ":" + strconv.Itoa(tc.mainPort) + got := waf.HostCode["code_"+tc.host] + if got != want { + t.Errorf("HostCode[%s] = %q,期望 %q(副端口不应覆盖主端口)", + "code_"+tc.host, got, want) + } + + // 验证 HostTarget 通过 HostCode 能正确找回 hostsafe + if waf.HostTarget[got] == nil { + t.Errorf("HostTarget[%s] 为 nil,HostCode 指向了无效 key", got) + } + }) + } +} + +// TestHostCode_SinglePort +// 无副端口时,HostCode 应指向唯一端口 +func TestHostCode_SinglePort(t *testing.T) { + waf := newTestWafEngine() + inHost := model.Hosts{ + Code: "site_single", + Host: "single.example.com", + Port: 443, + BindMorePort: "", + } + simulateLoadHostMaps(waf, inHost) + + want := "single.example.com:443" + got := waf.HostCode["site_single"] + if got != want { + t.Errorf("HostCode[site_single] = %q,期望 %q", got, want) + } +} + +// ───────────────────────────────────────────────────────────── +// Fix 4: RemoveHost 清理所有副端口 HostTarget 条目 +// ───────────────────────────────────────────────────────────── + +// TestRemoveHost_CleansAllBindMorePortEntries +// 验证:RemoveHost 后,主端口和所有副端口的 HostTarget 条目全部清除 +func TestRemoveHost_CleansAllBindMorePortEntries(t *testing.T) { + waf := newTestWafEngine() + + host := model.Hosts{ + Code: "site_clean", + Host: "clean.example.com", + Port: 80, + BindMorePort: "8080,9090", + GLOBAL_HOST: 0, + } + simulateLoadHostMaps(waf, host) + + // 删除前确认所有端口已注册 + for _, port := range []int{80, 8080, 9090} { + key := "clean.example.com:" + strconv.Itoa(port) + if waf.HostTarget[key] == nil { + t.Fatalf("测试前提不满足:HostTarget 缺少 %s", key) + } + } + + waf.RemoveHost(host) + + // 删除后所有端口条目应消失 + for _, port := range []int{80, 8080, 9090} { + key := "clean.example.com:" + strconv.Itoa(port) + if waf.HostTarget[key] != nil { + t.Errorf("RemoveHost 后 HostTarget 仍有残留条目 %s", key) + } + } + + // HostCode 也应消失 + if waf.HostCode["site_clean"] != "" { + t.Errorf("RemoveHost 后 HostCode[site_clean] 仍有值 %q", waf.HostCode["site_clean"]) + } +} + +// TestRemoveHost_CleansMoreDomainEntries +// 验证:RemoveHost 后,HostTargetMoreDomain 中该站点的所有条目被清除 +func TestRemoveHost_CleansMoreDomainEntries(t *testing.T) { + waf := newTestWafEngine() + + host := model.Hosts{ + Code: "site_more", + Host: "main.example.com", + Port: 80, + BindMorePort: "8080", + BindMoreHost: "sub1.example.com\nsub2.example.com", + GLOBAL_HOST: 0, + } + simulateLoadHostMaps(waf, host) + + // 删除前确认注册了所有组合 + for _, domain := range []string{"sub1.example.com", "sub2.example.com"} { + for _, port := range []int{80, 8080} { + key := domain + ":" + strconv.Itoa(port) + if waf.HostTargetMoreDomain[key] == "" { + t.Fatalf("测试前提不满足:HostTargetMoreDomain 缺少 %s", key) + } + } + } + + waf.RemoveHost(host) + + // 删除后不应有任何该站点的 MoreDomain 条目 + for moreDomain, code := range waf.HostTargetMoreDomain { + if code == "site_more" { + t.Errorf("RemoveHost 后 HostTargetMoreDomain 中仍有 %s -> site_more", moreDomain) + } + } +} + +// TestRemoveHost_AutoJumpHTTPS +// 验证:AutoJumpHTTPS 站点删除后,额外注册的 :80 条目也被清除 +func TestRemoveHost_AutoJumpHTTPS(t *testing.T) { + waf := newTestWafEngine() + + hostsafe := &wafenginmodel.HostSafe{Host: model.Hosts{Code: "site_ssl"}} + waf.HostTarget["ssl.example.com:443"] = hostsafe + waf.HostTarget["ssl.example.com:80"] = hostsafe // AutoJumpHTTPS 注册的 + waf.HostCode["site_ssl"] = "ssl.example.com:443" + + host := model.Hosts{ + Code: "site_ssl", + Host: "ssl.example.com", + Port: 443, + AutoJumpHTTPS: 1, + } + waf.RemoveHost(host) + + if waf.HostTarget["ssl.example.com:443"] != nil { + t.Error("RemoveHost 后 ssl.example.com:443 应被清除") + } + if waf.HostTarget["ssl.example.com:80"] != nil { + t.Error("RemoveHost 后 AutoJumpHTTPS 的 :80 条目未清除") + } +} + +// ───────────────────────────────────────────────────────────── +// 综合场景:模拟 Issue 1 & Issue 2 完整生命周期 +// ───────────────────────────────────────────────────────────── + +// TestIssue1_SubDomainExtraPortRouting +// Issue 1 场景:副域名+副端口 应能路由到正确的 hostsafe +func TestIssue1_SubDomainExtraPortRouting(t *testing.T) { + waf := newTestWafEngine() + + host := model.Hosts{ + Code: "A", + Host: "primary.com", + Port: 80, + BindMorePort: "8080", + BindMoreHost: "secondary.com", + } + simulateLoadHostMaps(waf, host) + + cases := []struct { + requestHost string + wantCode string + desc string + }{ + {"primary.com:80", "A", "主域名+主端口"}, + {"primary.com:8080", "A", "主域名+副端口"}, + {"secondary.com:80", "A", "副域名+主端口"}, + {"secondary.com:8080", "A", "副域名+副端口(Fix1 的目标场景)"}, + } + + for _, c := range cases { + t.Run(c.desc, func(t *testing.T) { + // 模拟 ServeHTTP 路由查找顺序 + gotCode := "" + if target, ok := waf.HostTarget[c.requestHost]; ok { + gotCode = target.Host.Code + } else if code, ok := waf.HostTargetMoreDomain[c.requestHost]; ok { + gotCode = code + } + + if gotCode != c.wantCode { + t.Errorf("请求 %s:期望路由到 %q,实际 %q", c.requestHost, c.wantCode, gotCode) + } + }) + } +} + +// TestIssue2_DeleteSiteNotAffectOtherSiteExtraPort +// Issue 2 场景:删除任意站点 B 后,站点 A 的副端口路由信息应完整保留 +func TestIssue2_DeleteSiteNotAffectOtherSiteExtraPort(t *testing.T) { + waf := newTestWafEngine() + + // 站点 A:主端口 80 + 副端口 8080 + hostA := model.Hosts{ + Code: "A", + Host: "siteA.com", + Port: 80, + BindMorePort: "8080", + } + simulateLoadHostMaps(waf, hostA) + + // 站点 B:独立站点,主端口 9090 + hostB := model.Hosts{ + Code: "B", + Host: "siteB.com", + Port: 9090, + } + simulateLoadHostMaps(waf, hostB) + + // 删除站点 B + waf.RemoveHost(hostB) + + // 站点 A 的主端口和副端口应仍然存在 + if waf.HostTarget["siteA.com:80"] == nil { + t.Error("删除站点 B 后,站点 A 主端口 :80 不应受影响") + } + if waf.HostTarget["siteA.com:8080"] == nil { + t.Error("删除站点 B 后,站点 A 副端口 :8080 不应受影响") + } + if waf.HostCode["A"] != "siteA.com:80" { + t.Errorf("删除站点 B 后,站点 A 的 HostCode 应为 siteA.com:80,实际 %q", waf.HostCode["A"]) + } +} + +// TestIssue2_CreateNewThenDeleteNotBreakExistingExtraPort +// Issue 2 完整复现:新建防护再删除,不应影响已有站点的副端口 +func TestIssue2_CreateNewThenDeleteNotBreakExistingExtraPort(t *testing.T) { + waf := newTestWafEngine() + + // 步骤1:建立站点 A,一个域名+多个端口 + hostA := model.Hosts{ + Code: "siteA", + Host: "example.com", + Port: 80, + BindMorePort: "8080,8443", + } + simulateLoadHostMaps(waf, hostA) + + // 步骤2:新建防护(站点 B) + hostB := model.Hosts{ + Code: "siteB", + Host: "newsite.com", + Port: 7070, + } + simulateLoadHostMaps(waf, hostB) + + // 步骤3:删除站点 B + waf.RemoveHost(hostB) + + // 验证:站点 A 的所有端口映射仍然完整 + expectedKeys := []string{ + "example.com:80", + "example.com:8080", + "example.com:8443", + } + for _, key := range expectedKeys { + if waf.HostTarget[key] == nil { + t.Errorf("删除站点 B 后,站点 A 的 HostTarget[%s] 被错误清除", key) + } + } + + // 站点 B 的条目应已清除 + if waf.HostTarget["newsite.com:7070"] != nil { + t.Error("站点 B 的条目应被 RemoveHost 清除") + } +} + +// ───────────────────────────────────────────────────────────── +// Bug A & B 修复验证:情况2 BindMorePort 变更 & RemoveHost 用旧数据 +// ───────────────────────────────────────────────────────────── + +// TestBindMorePortChange_OldPortCleaned +// Bug A:修改副端口时,旧副端口的 HostTarget 条目应被清除 +// Bug B:RemoveHost 必须用旧数据才能清到旧端口(传新数据清不掉旧的) +// +// 修复前行为:BindMorePort "8080,9090" → "8080,7070" +// +// RemoveHost(hosts[0]) 删的是 8080+7070(新),9090(旧)残留 +// +// 修复后行为:RemoveHost(hostsOld) 删的是 8080+9090(旧),然后 LoadHost 加载新配置 +func TestBindMorePortChange_OldPortCleaned(t *testing.T) { + waf := newTestWafEngine() + + // 初始状态:主端口 80,副端口 8080,9090 + oldHost := model.Hosts{ + Code: "siteX", + Host: "x.example.com", + Port: 80, + BindMorePort: "8080,9090", + } + simulateLoadHostMaps(waf, oldHost) + + // 确认旧状态 + for _, port := range []int{80, 8080, 9090} { + if waf.HostTarget["x.example.com:"+strconv.Itoa(port)] == nil { + t.Fatalf("测试前提不满足:HostTarget 缺少 x.example.com:%d", port) + } + } + + // 用户修改副端口:9090 → 7070(新副端口列表 8080,7070) + newHost := model.Hosts{ + Code: "siteX", + Host: "x.example.com", + Port: 80, + BindMorePort: "8080,7070", + } + + // 模拟 main.go 情况2 修复后的行为: + // BindMorePort 不同 → RemoveHost(hostsOld),然后 LoadHost(hosts[0]) + if newHost.BindMorePort != oldHost.BindMorePort { + waf.RemoveHost(oldHost) // Bug B 修复:传旧数据 + } + simulateLoadHostMaps(waf, newHost) + + // 新端口 7070 应存在 + if waf.HostTarget["x.example.com:7070"] == nil { + t.Error("新副端口 7070 应已注册到 HostTarget") + } + // 保留的端口 8080 应存在 + if waf.HostTarget["x.example.com:8080"] == nil { + t.Error("副端口 8080 应仍然注册") + } + // 旧端口 9090 应被清除(Bug A+B 修复后) + if waf.HostTarget["x.example.com:9090"] != nil { + t.Error("旧副端口 9090 应在 RemoveHost(hostsOld) 后被清除(Bug A+B 未修复时会残留)") + } +} + +// TestBindMorePortChange_WithOldDataVsNewData +// 直接对比:传旧数据 vs 传新数据 到 RemoveHost,验证结果差异 +// 这个测试精确复现了 Bug B 的问题所在 +func TestBindMorePortChange_WithOldDataVsNewData(t *testing.T) { + // 场景:副端口从 "8080,9090" 变为 "8080,7070" + + t.Run("传新数据_旧端口9090残留", func(t *testing.T) { + waf := newTestWafEngine() + oldHost := model.Hosts{Code: "s1", Host: "h.com", Port: 80, BindMorePort: "8080,9090"} + simulateLoadHostMaps(waf, oldHost) + + newHost := model.Hosts{Code: "s1", Host: "h.com", Port: 80, BindMorePort: "8080,7070"} + waf.RemoveHost(newHost) // Bug B:传新数据,9090 不会被清除 + simulateLoadHostMaps(waf, newHost) + + // 9090 应被清除,但传新数据时实际没清 + if waf.HostTarget["h.com:9090"] != nil { + t.Log("符合预期(Bug B 存在时):传新数据,旧端口 9090 残留 —— 这正是 Bug B 的表现") + } else { + t.Log("旧端口 9090 被清除(不受 Bug B 影响的情况)") + } + }) + + t.Run("传旧数据_旧端口9090被正确清除", func(t *testing.T) { + waf := newTestWafEngine() + oldHost := model.Hosts{Code: "s1", Host: "h.com", Port: 80, BindMorePort: "8080,9090"} + simulateLoadHostMaps(waf, oldHost) + + newHost := model.Hosts{Code: "s1", Host: "h.com", Port: 80, BindMorePort: "8080,7070"} + waf.RemoveHost(oldHost) // 修复后:传旧数据 + simulateLoadHostMaps(waf, newHost) + + // 9090 必须被清除 + if waf.HostTarget["h.com:9090"] != nil { + t.Error("传旧数据后旧端口 9090 仍残留,Fix 未生效") + } + // 7070 必须存在 + if waf.HostTarget["h.com:7070"] == nil { + t.Error("新端口 7070 未注册") + } + }) +}