First Commit
ci / go (push) Waiting to run
ci / go-db (agent) (push) Waiting to run
ci / go-db (config) (push) Waiting to run
ci / go-db (db) (push) Waiting to run
ci / go-db (evidence) (push) Waiting to run
ci / go-db (llmrec) (push) Waiting to run
ci / go-db (server) (push) Waiting to run
web / web (push) Waiting to run
docs / links (push) Canceled after 0s
detections / detections (push) Canceled after 0s

This commit is contained in:
dela
2026-10-09 08:38:16 +08:00
commit 0335d572de
756 changed files with 201663 additions and 0 deletions
+410
View File
@@ -0,0 +1,410 @@
package traffic
import (
"fmt"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"testing"
mproxy "github.com/lqqyt2423/go-mitmproxy/proxy"
)
func TestRequestHeaderLinesIncludesHost(t *testing.T) {
req := &mproxy.Request{
URL: &url.URL{Host: "target.example:8443"},
Header: http.Header{"Accept": []string{"application/json"}},
}
got := requestHeaderLines(req)
if !strings.Contains(got, "Host: target.example:8443\n") {
t.Fatalf("request headers missing Host: %q", got)
}
if !strings.Contains(got, "Accept: application/json\n") {
t.Fatalf("request headers missing regular header: %q", got)
}
}
// TestDeleteHost verifies the delete contract: rows for hosts containing the
// substring are removed together with their file trees, non-matching hosts are
// untouched, and the count is right.
func TestDeleteHost(t *testing.T) {
dir := t.TempDir()
tr, err := Open(dir, "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer tr.Close()
// Seed two hosts' index rows + trees directly (record() needs a live Flow).
for i, h := range []string{"a.example.com", "b.example.com"} {
id := fmt.Sprintf("1-%04d", i+1)
exDir := filepath.Join(dir, h, "GET", id)
if err := os.MkdirAll(exDir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(exDir, "meta.json"), []byte(fmt.Sprintf(`{"id":%q,"host":%q}`, id, h)), 0o644); err != nil {
t.Fatal(err)
}
if _, err := tr.DB().Exec(`INSERT INTO exchanges(id,ts,host,method,url_template,url,status,content_type,req_len,resp_len,path)
VALUES(?,?,?,?,?,?,?,?,?,?,?)`,
id, i+1, h, "GET", "/", "http://"+h+"/", 200, "text/html", 0, 0, h+"/GET/"+id); err != nil {
t.Fatal(err)
}
}
// Substring: "a.example" matches a.example.com only, leaves b.example.com.
n, err := tr.DeleteHost("a.example")
if err != nil {
t.Fatal(err)
}
if n != 1 {
t.Fatalf("deleted=%d, want 1", n)
}
// Tree removed for the target, intact for the other host.
if _, err := os.Stat(filepath.Join(dir, "a.example.com")); !os.IsNotExist(err) {
t.Fatalf("a.example.com tree still exists (stat err=%v)", err)
}
if _, err := os.Stat(filepath.Join(dir, "b.example.com")); err != nil {
t.Fatalf("b.example.com tree removed: %v", err)
}
// Index reduced to the other host's single row.
var c int
if err := tr.DB().QueryRow(`SELECT COUNT(*) FROM exchanges`).Scan(&c); err != nil {
t.Fatal(err)
}
if c != 1 {
t.Fatalf("rows=%d, want 1", c)
}
// A substring matching nothing is a no-op, not an error.
n, err = tr.DeleteHost("nope.example")
if err != nil || n != 0 {
t.Fatalf("DeleteHost(missing)=%d, err=%v; want 0, nil", n, err)
}
// A broader substring sweeps the remaining host too.
if n, err = tr.DeleteHost("example.com"); err != nil || n != 1 {
t.Fatalf("DeleteHost(example.com)=%d, err=%v; want 1, nil", n, err)
}
c = 0
if err := tr.DB().QueryRow(`SELECT COUNT(*) FROM exchanges`).Scan(&c); err == nil && c != 0 {
t.Fatalf("rows=%d, want 0 after full sweep", c)
}
}
// TestHosts verifies the target picker contract: distinct hosts with counts,
// most recent activity first.
func TestHosts(t *testing.T) {
dir := t.TempDir()
tr, err := Open(dir, "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer tr.Close()
for i, row := range []struct {
host string
ts int64
}{{"old.example.com", 1}, {"new.example.com", 3}, {"old.example.com", 2}} {
id := fmt.Sprintf("1-%04d", i+1)
if _, err := tr.DB().Exec(`INSERT INTO exchanges(id,ts,host,method,url_template,url,status,content_type,req_len,resp_len,path)
VALUES(?,?,?,?,?,?,?,?,?,?,?)`,
id, row.ts, row.host, "GET", "/", "http://"+row.host+"/", 200, "text/html", 0, 0, row.host+"/GET/"+id); err != nil {
t.Fatal(err)
}
}
hosts, err := tr.Hosts()
if err != nil {
t.Fatal(err)
}
if len(hosts) != 2 {
t.Fatalf("hosts=%d, want 2", len(hosts))
}
// newest activity (ts=3) first
if hosts[0].Host != "new.example.com" || hosts[0].Count != 1 {
t.Fatalf("hosts[0]=%+v, want new.example.com/1", hosts[0])
}
if hosts[1].Host != "old.example.com" || hosts[1].Count != 2 {
t.Fatalf("hosts[1]=%+v, want old.example.com/2", hosts[1])
}
}
// TestDeleteHostsExact verifies the batch delete: exact host match only — a
// host whose name contains another as a substring is untouched — duplicates in
// the batch are harmless, and the per-host trees are removed.
func TestDeleteHostsExact(t *testing.T) {
dir := t.TempDir()
tr, err := Open(dir, "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer tr.Close()
seed := func(id, h string) {
exDir := filepath.Join(dir, h, "GET", id)
if err := os.MkdirAll(exDir, 0o755); err != nil {
t.Fatal(err)
}
if _, err := tr.DB().Exec(`INSERT INTO exchanges(id,ts,host,method,url_template,url,status,content_type,req_len,resp_len,path)
VALUES(?,?,?,?,?,?,?,?,?,?,?)`,
id, 1, h, "GET", "/", "http://"+h+"/", 200, "text/html", 0, 0, h+"/GET/"+id); err != nil {
t.Fatal(err)
}
}
// "api.example.com" is a substring of "api.example.com.cn".
seed("1-0001", "api.example.com")
seed("1-0002", "api.example.com.cn")
seed("1-0003", "shop.example.com")
// Duplicate entry in the batch must not double-delete or error.
n, err := tr.DeleteHostsExact([]string{"api.example.com", "api.example.com", "shop.example.com"})
if err != nil {
t.Fatal(err)
}
if n != 2 {
t.Fatalf("deleted=%d, want 2", n)
}
if _, err := os.Stat(filepath.Join(dir, "api.example.com")); !os.IsNotExist(err) {
t.Fatalf("api.example.com tree still exists: %v", err)
}
if _, err := os.Stat(filepath.Join(dir, "shop.example.com")); !os.IsNotExist(err) {
t.Fatalf("shop.example.com tree still exists: %v", err)
}
if _, err := os.Stat(filepath.Join(dir, "api.example.com.cn")); err != nil {
t.Fatalf("api.example.com.cn removed by an exact delete that shouldn't match: %v", err)
}
var c int
if err := tr.DB().QueryRow(`SELECT COUNT(*) FROM exchanges`).Scan(&c); err != nil {
t.Fatal(err)
}
if c != 1 {
t.Fatalf("rows=%d, want 1 (api.example.com.cn only)", c)
}
}
func TestDeleteHostsExactReportsTreeRemovalFailure(t *testing.T) {
dir := t.TempDir()
tr, err := Open(dir, "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer tr.Close()
const host = "api.example.com"
if _, err := tr.DB().Exec(`INSERT INTO exchanges(id,ts,host,method,url_template,url,status,content_type,req_len,resp_len,path)
VALUES(?,?,?,?,?,?,?,?,?,?,?)`,
"1-0001", 1, host, "GET", "/", "http://"+host+"/", 200, "text/html", 0, 0, host+"/GET/1-0001"); err != nil {
t.Fatal(err)
}
notDir := filepath.Join(dir, "not-a-directory")
if err := os.WriteFile(notDir, []byte("x"), 0o644); err != nil {
t.Fatal(err)
}
tr.dir = notDir
n, err := tr.DeleteHostsExact([]string{host})
if err == nil {
t.Fatal("DeleteHostsExact returned nil after traffic tree removal failed")
}
if n != 0 {
t.Fatalf("deleted=%d, want 0 after atomic rollback", n)
}
var count int
if err := tr.DB().QueryRow(`SELECT COUNT(*) FROM exchanges WHERE host=?`, host).Scan(&count); err != nil || count != 1 {
t.Fatalf("rolled-back index count=%d err=%v, want 1", count, err)
}
}
func TestDeleteHostsExactRollsBackWholeIndexBatch(t *testing.T) {
dir := t.TempDir()
tr, err := Open(dir, "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer tr.Close()
for i, host := range []string{"a.example.com", "b.example.com"} {
id := fmt.Sprintf("1-%04d", i+1)
if err := os.MkdirAll(filepath.Join(dir, host, "GET", id), 0o755); err != nil {
t.Fatal(err)
}
if _, err := tr.DB().Exec(`INSERT INTO exchanges(id,ts,host,method,url_template,url,status,content_type,req_len,resp_len,path)
VALUES(?,?,?,?,?,?,?,?,?,?,?)`, id, 1, host, "GET", "/", "http://"+host+"/", 200, "text/html", 0, 0, host+"/GET/"+id); err != nil {
t.Fatal(err)
}
}
if _, err := tr.DB().Exec(`CREATE TRIGGER fail_second_host BEFORE DELETE ON exchanges
WHEN OLD.host='b.example.com' BEGIN SELECT RAISE(ABORT, 'forced delete failure'); END`); err != nil {
t.Fatal(err)
}
if n, err := tr.DeleteHostsExact([]string{"a.example.com", "b.example.com"}); err == nil || n != 0 {
t.Fatalf("DeleteHostsExact failure=(%d,%v), want (0,error)", n, err)
}
var count int
if err := tr.DB().QueryRow(`SELECT COUNT(*) FROM exchanges`).Scan(&count); err != nil || count != 2 {
t.Fatalf("rolled-back index count=%d err=%v, want 2", count, err)
}
for _, host := range []string{"a.example.com", "b.example.com"} {
if _, err := os.Stat(filepath.Join(dir, host)); err != nil {
t.Fatalf("tree %s changed despite index rollback: %v", host, err)
}
}
}
func TestStageDeleteHostsExactRollbackRestoresIndexAndTree(t *testing.T) {
dir := t.TempDir()
tr, err := Open(dir, "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer tr.Close()
const host = "rollback.example.com"
const id = "1-0001"
tree := filepath.Join(dir, host, "GET", id)
if err := os.MkdirAll(tree, 0o755); err != nil {
t.Fatal(err)
}
marker := filepath.Join(tree, "request.http")
if err := os.WriteFile(marker, []byte("original"), 0o644); err != nil {
t.Fatal(err)
}
if _, err := tr.DB().Exec(`INSERT INTO exchanges(id,ts,host,method,url_template,url,status,content_type,req_len,resp_len,path)
VALUES(?,?,?,?,?,?,?,?,?,?,?)`, id, 1, host, "GET", "/", "http://"+host+"/", 200, "text/html", 0, 0, host+"/GET/"+id); err != nil {
t.Fatal(err)
}
stage, err := tr.StageDeleteHostsExact([]string{host})
if err != nil {
t.Fatal(err)
}
if stage.Deleted() != 1 {
t.Fatalf("staged deleted=%d, want 1", stage.Deleted())
}
if _, err := os.Stat(filepath.Join(dir, host)); !os.IsNotExist(err) {
t.Fatalf("host tree was not staged: %v", err)
}
if err := stage.Rollback(); err != nil {
t.Fatal(err)
}
if got, err := os.ReadFile(marker); err != nil || string(got) != "original" {
t.Fatalf("restored tree content=%q err=%v", got, err)
}
var count int
if err := tr.DB().QueryRow(`SELECT COUNT(*) FROM exchanges WHERE host=?`, host).Scan(&count); err != nil || count != 1 {
t.Fatalf("restored index count=%d err=%v, want 1", count, err)
}
}
func TestStageDeleteHostsExactRollbackReportsRestoreFailure(t *testing.T) {
dir := t.TempDir()
tr, err := Open(dir, "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer tr.Close()
const host = "conflict.example.com"
hostDir := filepath.Join(dir, host)
if err := os.MkdirAll(hostDir, 0o755); err != nil {
t.Fatal(err)
}
stage, err := tr.StageDeleteHostsExact([]string{host})
if err != nil {
t.Fatal(err)
}
// Simulate an out-of-band conflicting destination. Rollback must surface the
// failed rename instead of claiming the external data was restored.
if err := os.WriteFile(hostDir, []byte("conflict"), 0o644); err != nil {
t.Fatal(err)
}
if err := stage.Rollback(); err == nil || !strings.Contains(err.Error(), "restore") {
t.Fatalf("rollback err=%v, want restore failure", err)
}
if _, err := os.Stat(stage.stageDir); err != nil {
t.Fatalf("staging was removed after failed restore: %v", err)
}
}
// TestDeleteHostGCBlobs verifies blob garbage collection: after a host's trees
// are removed, blobs referenced by no remaining exchange are deleted, while
// blobs still referenced (including shared ones) survive.
func TestDeleteHostGCBlobs(t *testing.T) {
dir := t.TempDir()
tr, err := Open(dir, "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer tr.Close()
// Two distinct blobs + one shared blob (referenced by two hosts).
blobA := filepath.Join(dir, "_blobs", "sha256", "aa", "aa", strings.Repeat("a", 64)+".bin")
blobB := filepath.Join(dir, "_blobs", "sha256", "bb", "bb", strings.Repeat("b", 64)+".bin")
blobC := filepath.Join(dir, "_blobs", "sha256", "cc", "cc", strings.Repeat("c", 64)+".bin")
for _, b := range []string{blobA, blobB, blobC} {
if err := os.MkdirAll(filepath.Dir(b), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(b, []byte("x"), 0o644); err != nil {
t.Fatal(err)
}
}
seed := func(id, h, ref string) {
exDir := filepath.Join(dir, h, "GET", id)
if err := os.MkdirAll(exDir, 0o755); err != nil {
t.Fatal(err)
}
body := "no blob"
if ref != "" {
body = "@blob sha256:" + ref + " (len=1)"
}
if err := os.WriteFile(filepath.Join(exDir, "request.http"), []byte("GET / HTTP/1.1\n\n"+body), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(exDir, "response.http"), []byte("HTTP 200 OK\n"), 0o644); err != nil {
t.Fatal(err)
}
if _, err := tr.DB().Exec(`INSERT INTO exchanges(id,ts,host,method,url_template,url,status,content_type,req_len,resp_len,path)
VALUES(?,?,?,?,?,?,?,?,?,?,?)`,
id, 1, h, "GET", "/", "http://"+h+"/", 200, "text/html", 0, 0, h+"/GET/"+id); err != nil {
t.Fatal(err)
}
}
ha := strings.Repeat("a", 64)
hb := strings.Repeat("b", 64)
hc := strings.Repeat("c", 64)
seed("1-0001", "a.example.com", ha) // sole reference to blobA
seed("1-0002", "b.example.com", hb) // sole reference to blobB
seed("1-0003", "c.example.com", hc) // shares blobC with d
seed("1-0004", "d.example.com", hc)
// Delete a: blobA orphaned → removed; blobB/blobC still referenced → kept.
if n, err := tr.DeleteHost("a.example"); err != nil || n != 1 {
t.Fatalf("DeleteHost(a.example)=%d, err=%v; want 1, nil", n, err)
}
if _, err := os.Stat(blobA); !os.IsNotExist(err) {
t.Fatalf("orphaned blobA still exists: %v", err)
}
if _, err := os.Stat(blobB); err != nil {
t.Fatalf("referenced blobB removed: %v", err)
}
if _, err := os.Stat(blobC); err != nil {
t.Fatalf("shared blobC removed while d still references it: %v", err)
}
// Delete c (shares blobC with d): blobC must survive.
if n, err := tr.DeleteHost("c.example"); err != nil || n != 1 {
t.Fatalf("DeleteHost(c.example)=%d, err=%v; want 1, nil", n, err)
}
if _, err := os.Stat(blobC); err != nil {
t.Fatalf("shared blobC removed after deleting one sharer: %v", err)
}
// Delete d: last reference gone → blobC collected.
if n, err := tr.DeleteHost("d.example"); err != nil || n != 1 {
t.Fatalf("DeleteHost(d.example)=%d, err=%v; want 1, nil", n, err)
}
if _, err := os.Stat(blobC); !os.IsNotExist(err) {
t.Fatalf("blobC still exists after last reference removed: %v", err)
}
}