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  }