mirror of
https://github.com/BaaRai/ARTEX.git
synced 2026-11-13 11:40:53 +08:00
Comments, doc comments, UI labels, log and error messages, LLM prompt strings, markdown docs, test data and assertions. Identifiers, enum keys, routes, placeholders and code structure are unchanged. Notes: - docs/漏洞流量证据.md renamed to docs/vulnerability-traffic-evidence.md - tests that exercised CJK specific handling (GBK zip names, Chinese full text search) now use ASCII equivalents; go build passes and the test suite matches the pre translation baseline (remaining failures need a PostgreSQL DSN and fail identically on the original) - frontend mention syntax now uses English kinds (finding, asset, ...) matching the backend, chat mentions tests pass
165 lines
5.7 KiB
Go
165 lines
5.7 KiB
Go
package traffic
|
|
|
|
import (
|
|
"database/sql"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
// openLegacyIndex builds the index exactly as the pre-reclamation Open did: a
|
|
// plain-path DSN, pragmas via the pool, and auto_vacuum left at its default 0.
|
|
func openLegacyIndex(t *testing.T, dir string) *sql.DB {
|
|
t.Helper()
|
|
if err := os.MkdirAll(filepath.Join(dir, "_index"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
old, err := sql.Open("sqlite", filepath.Join(dir, "_index", "index.sqlite"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, p := range []string{"PRAGMA journal_mode=WAL", "PRAGMA busy_timeout=5000"} {
|
|
if _, err := old.Exec(p); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if _, err := old.Exec(indexSchema); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return old
|
|
}
|
|
|
|
// TestUpgradeFromOldInstall guards the upgrade path. Open now names the database
|
|
// through a file: URI so per-connection pragmas can ride in the DSN, and a
|
|
// driver that did not treat that as a URI would quietly open a file literally
|
|
// named "file:/…" — an empty index, with every recorded exchange apparently
|
|
// gone. The assertions below are what prove that does not happen.
|
|
func TestUpgradeFromOldInstall(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "_index", "index.sqlite")
|
|
old := openLegacyIndex(t, dir)
|
|
if _, err := old.Exec(ftsSchema); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// three historical flows, including one row with legacy path<>''
|
|
for i, row := range [][]any{
|
|
{"1700000000-0001", "old.example.com", ""},
|
|
{"1700000000-0002", "old.example.com", ""},
|
|
{"1700000000-0003", "legacy.example.com", "legacy.example.com/GET/x"},
|
|
} {
|
|
if _, err := old.Exec(`INSERT INTO exchanges(id,ts,host,method,url_template,url,status,content_type,req_len,resp_len,path)
|
|
VALUES(?,?,?,'GET','/x','http://x/x',200,'text/html',0,9,?)`, row[0], 1700000000+i, row[1], row[2]); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := old.Exec(`INSERT INTO exchange_bodies(id,req_head,req_body,resp_head,resp_body)
|
|
VALUES(?,'GET /x','','HTTP 200','legacy body text')`, row[0]); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := old.Exec(`INSERT INTO ex_fts(rowid,content) VALUES(?,?)`, i+1, "legacy body text secret-token"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if err := old.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
stat, err := os.Stat(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// ---- new version takes over
|
|
tr, err := Open(dir, "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("new version cannot open the old database: %v", err)
|
|
}
|
|
defer tr.Close()
|
|
|
|
// 1. must be the same file — no silently opening a new empty database
|
|
if st2, err := os.Stat(path); err != nil || st2.Size() == 0 {
|
|
t.Fatalf("original index file abnormal: size=%v err=%v", st2, err)
|
|
}
|
|
if entries, _ := os.ReadDir(filepath.Join(dir, "_index")); len(entries) > 3 {
|
|
for _, e := range entries {
|
|
t.Logf("under _index: %s", e.Name())
|
|
}
|
|
t.Fatal("unexpected files under _index; the DSN may point to a different database")
|
|
}
|
|
t.Logf("old database %d bytes, still the same file after the new version takes over", stat.Size())
|
|
|
|
// 2. all historical data is visible
|
|
n, err := tr.Count()
|
|
if err != nil || n != 3 {
|
|
t.Fatalf("Count=(%d,%v), want (3,nil) — historical traffic lost", n, err)
|
|
}
|
|
// 3. historical full-text index is still searchable
|
|
if tr.fts {
|
|
rows, err := tr.query("old.example.com", "", "secret-token", 0, 10)
|
|
if err != nil {
|
|
t.Fatalf("historical full-text search failed: %v", err)
|
|
}
|
|
if len(rows) != 2 {
|
|
t.Fatalf("historical full-text search hit %d rows, want 2", len(rows))
|
|
}
|
|
}
|
|
// 4. historical bodies are still readable
|
|
if _, resp, err := tr.Get("1700000000-0001"); err != nil {
|
|
t.Fatalf("failed to read historical body: %v", err)
|
|
} else if resp == "" {
|
|
t.Fatal("historical response is empty")
|
|
}
|
|
// 5. the old database must not be misdetected as incremental-vacuum-enabled
|
|
if tr.incrementalVacuum {
|
|
t.Fatal("old database misdetected as incremental-vacuum-enabled")
|
|
}
|
|
// 6. deletion still works, and reclamation converges on the old database
|
|
deleted, err := tr.DeleteHostsExact([]string{"old.example.com"})
|
|
if err != nil || deleted != 2 {
|
|
t.Fatalf("DeleteHostsExact=(%d,%v), want (2,nil)", deleted, err)
|
|
}
|
|
tr.reaping.Wait()
|
|
if n, err := tr.Count(); err != nil || n != 1 {
|
|
t.Fatalf("after delete Count=(%d,%v), want (1,nil)", n, err)
|
|
}
|
|
// 7. the legacy path<>'' row is not affected
|
|
var legacyPath string
|
|
if err := tr.DB().QueryRow(`SELECT path FROM exchanges`).Scan(&legacyPath); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if legacyPath == "" {
|
|
t.Fatal("legacy row's path was cleared")
|
|
}
|
|
}
|
|
|
|
// TestDowngradeToOldBinary covers a rollback: a database created with
|
|
// auto_vacuum=incremental must stay readable and writable by a build that knows
|
|
// nothing about it. auto_vacuum only changes where SQLite tracks free pages, so
|
|
// the old binary simply goes back to never returning them.
|
|
func TestDowngradeToOldBinary(t *testing.T) {
|
|
dir := t.TempDir()
|
|
tr, err := Open(dir, "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !tr.incrementalVacuum {
|
|
t.Fatal("new database should enable incremental vacuum")
|
|
}
|
|
bulkRecord(tr, "keep.example.com", 5, 100*1024)
|
|
if err := tr.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
old := openLegacyIndex(t, dir) // old binary takes over
|
|
defer old.Close()
|
|
var n int
|
|
if err := old.QueryRow(`SELECT COUNT(*) FROM exchanges`).Scan(&n); err != nil || n != 5 {
|
|
t.Fatalf("old version read (%d,%v), want (5,nil)", n, err)
|
|
}
|
|
if _, err := old.Exec(`INSERT INTO exchanges(id,ts,host,method,url_template,url,status,content_type,req_len,resp_len,path)
|
|
VALUES('x',1,'new.example.com','GET','/x','http://x/x',200,'',0,0,'')`); err != nil {
|
|
t.Fatalf("old version write failed: %v", err)
|
|
}
|
|
if _, err := old.Exec(`DELETE FROM exchanges WHERE host='keep.example.com'`); err != nil {
|
|
t.Fatalf("old version delete failed: %v", err)
|
|
}
|
|
}
|