github.com/chainreactors/fingers@v1.2.1/fingers/realsite_bench_test.go (about) 1 package fingers 2 3 import ( 4 "crypto/tls" 5 "fmt" 6 "io" 7 "net" 8 "net/http" 9 "os" 10 "path/filepath" 11 "strings" 12 "sync" 13 "testing" 14 "time" 15 16 "github.com/chainreactors/fingers/resources" 17 ) 18 19 var ( 20 cachedResponses map[string][]byte 21 cachedResponsesOnce sync.Once 22 ) 23 24 func fetchAndCache(targets []string) map[string][]byte { 25 cachedResponsesOnce.Do(func() { 26 cachedResponses = make(map[string][]byte) 27 cacheDir := filepath.Join(os.TempDir(), "fingers_bench_cache") 28 os.MkdirAll(cacheDir, 0755) 29 30 client := &http.Client{ 31 Timeout: 5 * time.Second, 32 Transport: &http.Transport{ 33 TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 34 DisableKeepAlives: true, 35 }, 36 CheckRedirect: func(req *http.Request, via []*http.Request) error { 37 return http.ErrUseLastResponse 38 }, 39 } 40 41 var mu sync.Mutex 42 var wg sync.WaitGroup 43 sem := make(chan struct{}, 20) 44 45 for _, target := range targets { 46 wg.Add(1) 47 go func(t string) { 48 defer wg.Done() 49 sem <- struct{}{} 50 defer func() { <-sem }() 51 52 cacheKey := strings.ReplaceAll(strings.ReplaceAll(t, "://", "_"), "/", "_") 53 cacheFile := filepath.Join(cacheDir, cacheKey) 54 55 if data, err := os.ReadFile(cacheFile); err == nil && len(data) > 0 { 56 mu.Lock() 57 cachedResponses[t] = data 58 mu.Unlock() 59 return 60 } 61 62 resp, err := client.Get(t) 63 if err != nil { 64 return 65 } 66 defer resp.Body.Close() 67 body, err := io.ReadAll(io.LimitReader(resp.Body, 512*1024)) 68 if err != nil || len(body) == 0 { 69 return 70 } 71 72 var sb strings.Builder 73 sb.WriteString(fmt.Sprintf("HTTP/%d.%d %s\r\n", resp.ProtoMajor, resp.ProtoMinor, resp.Status)) 74 for k, vs := range resp.Header { 75 for _, v := range vs { 76 sb.WriteString(fmt.Sprintf("%s: %s\r\n", k, v)) 77 } 78 } 79 sb.WriteString("\r\n") 80 sb.Write(body) 81 82 raw := []byte(sb.String()) 83 os.WriteFile(cacheFile, raw, 0644) 84 mu.Lock() 85 cachedResponses[t] = raw 86 mu.Unlock() 87 }(target) 88 } 89 wg.Wait() 90 }) 91 return cachedResponses 92 } 93 94 func expandCIDR(cidr string, ports []int) []string { 95 ip, ipnet, err := net.ParseCIDR(cidr) 96 if err != nil { 97 return nil 98 } 99 var targets []string 100 for ip := ip.Mask(ipnet.Mask); ipnet.Contains(ip); incIP(ip) { 101 for _, port := range ports { 102 scheme := "http" 103 if port == 443 || port == 8443 { 104 scheme = "https" 105 } 106 targets = append(targets, fmt.Sprintf("%s://%s:%d/", scheme, ip.String(), port)) 107 } 108 } 109 return targets 110 } 111 112 func incIP(ip net.IP) { 113 for j := len(ip) - 1; j >= 0; j-- { 114 ip[j]++ 115 if ip[j] > 0 { 116 break 117 } 118 } 119 } 120 121 func newRealSiteEngine(tb testing.TB) *FingersEngine { 122 tb.Helper() 123 engine, err := NewFingersEngine(resources.FingersHTTPData, resources.FingersSocketData, resources.PortData) 124 if err != nil { 125 tb.Fatalf("create engine: %v", err) 126 } 127 return engine 128 } 129 130 // BenchmarkRealSites compares AC-accelerated matching vs brute-force PassiveMatch 131 // on real HTTP responses from a /26 CIDR range. 132 func BenchmarkRealSites(b *testing.B) { 133 if testing.Short() { 134 b.Skip("skipping live network benchmark in short mode") 135 } 136 targets := expandCIDR("101.132.149.35/26", []int{80, 443}) 137 responses := fetchAndCache(targets) 138 if len(responses) == 0 { 139 b.Skip("no responses fetched") 140 } 141 142 engine := newRealSiteEngine(b) 143 144 type testCase struct { 145 url string 146 data []byte 147 content *Content 148 } 149 var cases []testCase 150 for url, data := range responses { 151 cases = append(cases, testCase{ 152 url: url, 153 data: data, 154 content: NewContent(data, "", true), 155 }) 156 } 157 158 totalBytes := 0 159 for _, c := range cases { 160 totalBytes += len(c.data) 161 } 162 b.Logf("%d responses, %d bytes total, %d fingerprint rules", 163 len(cases), totalBytes, len(engine.HTTPFingers)) 164 165 // With AC pre-filtering (current optimized path) 166 b.Run("WithAC", func(b *testing.B) { 167 b.ReportAllocs() 168 b.SetBytes(int64(totalBytes)) 169 for i := 0; i < b.N; i++ { 170 for _, c := range cases { 171 engine.HTTPFingers.ACPassiveMatch(c.content, engine.httpKeywordIndex, false) 172 } 173 } 174 }) 175 176 // Without AC - brute force all fingerprints 177 b.Run("WithoutAC", func(b *testing.B) { 178 b.ReportAllocs() 179 b.SetBytes(int64(totalBytes)) 180 for i := 0; i < b.N; i++ { 181 for _, c := range cases { 182 engine.HTTPFingers.PassiveMatch(c.content, false) 183 } 184 } 185 }) 186 187 // AC MatchCandidates only (pure AC cost) 188 b.Run("ACOnly", func(b *testing.B) { 189 b.ReportAllocs() 190 b.SetBytes(int64(totalBytes)) 191 for i := 0; i < b.N; i++ { 192 for _, c := range cases { 193 engine.httpKeywordIndex.MatchCandidates(c.content.Header, c.content.Body) 194 } 195 } 196 }) 197 198 // Full HTTPMatch (includes Content parsing overhead) 199 b.Run("FullHTTPMatch", func(b *testing.B) { 200 b.ReportAllocs() 201 b.SetBytes(int64(totalBytes)) 202 for i := 0; i < b.N; i++ { 203 for _, c := range cases { 204 engine.HTTPMatch(c.data, "") 205 } 206 } 207 }) 208 } 209 210 func TestRealSites_Correctness(t *testing.T) { 211 if testing.Short() { 212 t.Skip("skipping live network test in short mode") 213 } 214 targets := expandCIDR("101.132.149.35/26", []int{80, 443}) 215 responses := fetchAndCache(targets) 216 if len(responses) == 0 { 217 t.Skip("no responses fetched") 218 } 219 220 engine := newRealSiteEngine(t) 221 222 for url, data := range responses { 223 content := NewContent(data, "", true) 224 withAC, _ := engine.HTTPFingers.ACPassiveMatch(content, engine.httpKeywordIndex, false) 225 withoutAC, _ := engine.HTTPFingers.PassiveMatch(content, false) 226 227 acNames := make([]string, 0) 228 for _, f := range withAC { 229 acNames = append(acNames, f.Name) 230 } 231 noACNames := make([]string, 0) 232 for _, f := range withoutAC { 233 noACNames = append(noACNames, f.Name) 234 } 235 236 if len(acNames) > 0 || len(noACNames) > 0 { 237 t.Logf("%-40s AC:[%s] NoAC:[%s]", url, 238 strings.Join(acNames, ","), strings.Join(noACNames, ",")) 239 } 240 241 // Verify AC doesn't miss any matches 242 noACSet := make(map[string]bool) 243 for _, n := range noACNames { 244 noACSet[n] = true 245 } 246 for _, n := range acNames { 247 if !noACSet[n] { 248 // AC found something PassiveMatch didn't - that's fine (AC includes nonKeyword fingers) 249 } 250 } 251 acSet := make(map[string]bool) 252 for _, n := range acNames { 253 acSet[n] = true 254 } 255 for _, n := range noACNames { 256 if !acSet[n] { 257 t.Errorf("%s: AC missed fingerprint %q found by PassiveMatch", url, n) 258 } 259 } 260 } 261 }