mirror of
https://github.com/BaaRai/ARTEX.git
synced 2026-11-13 11:40:53 +08:00
Closes #33. The MITM proxy only sees the agent's own actions, so anything behind a human gate (signup captcha, email OTP, 2FA, SSO) left the authenticated surface dark. The operator's browser becomes a capture source instead: - traffic: exported RecordExternal storing an externally captured exchange through the same spill/blob/FTS path as proxy traffic (insert logic shared with record()); rejects unparsable URLs and empty methods - server: POST /api/traffic/ingest inside the authenticated API group, batch capped at 50 entries, bodies truncated at 256 KiB, per-entry ingested/failed counts - extension/: Manifest V3 Chrome/Edge extension driving the chrome.debugger Network domain on the chosen tab, batching text exchanges to the ingest endpoint with the operator's URL + JWT in chrome.storage.local; binary bodies skipped; flush on batch/stop - tests for both layers, README section
78 lines
2.3 KiB
Go
78 lines
2.3 KiB
Go
package traffic
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestRecordExternalStoresLikeProxyTraffic(t *testing.T) {
|
|
tr, _ := openTraffic(t)
|
|
|
|
big := strings.Repeat("A", 2<<20) // spills to the blob store
|
|
err := tr.RecordExternal(ExternalExchange{
|
|
Method: "POST",
|
|
URL: "https://api.example.com/v1/login",
|
|
Status: 200,
|
|
RequestHeaders: map[string]string{"content-type": "application/json", "authorization": "Bearer xyz"},
|
|
RequestBody: `{"user":"u1","password":"p1"}`,
|
|
ResponseHeaders: map[string]string{"content-type": "application/json"},
|
|
ResponseBody: `{"token":"tok"}`,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("RecordExternal small: %v", err)
|
|
}
|
|
if err := tr.RecordExternal(ExternalExchange{
|
|
Method: "GET",
|
|
URL: "https://api.example.com/v1/items?page=1",
|
|
Status: 404,
|
|
ResponseHeaders: map[string]string{"content-type": "text/html"},
|
|
ResponseBody: big,
|
|
}); err != nil {
|
|
t.Fatalf("RecordExternal spilled: %v", err)
|
|
}
|
|
|
|
if n, _ := tr.Count(); n != 2 {
|
|
t.Fatalf("count = %d, want 2", n)
|
|
}
|
|
rows, total, err := tr.Page(PageQuery{Query: "login", RespMin: -1, RespMax: -1}, 0, 10)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if total != 1 || len(rows) != 1 {
|
|
t.Fatalf("filtered page = %d rows (total %d), want 1", len(rows), total)
|
|
}
|
|
req, resp, err := tr.Get(rows[0].ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(req, "POST /v1/login") || !strings.Contains(req, "Bearer xyz") || !strings.Contains(req, `"user":"u1"`) {
|
|
t.Fatalf("request side incomplete: %q", req)
|
|
}
|
|
if !strings.Contains(resp, `{"token":"tok"}`) {
|
|
t.Fatalf("response side incomplete: %q", resp)
|
|
}
|
|
|
|
// The spilled entry must be FTS-searchable by its body content too.
|
|
rows, total, err = tr.Page(PageQuery{Body: strings.Repeat("A", 64), RespMin: -1, RespMax: -1}, 0, 10)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if total != 1 || len(rows) != 1 {
|
|
t.Fatalf("spilled-body FTS search = %d rows (total %d), want 1", len(rows), total)
|
|
}
|
|
}
|
|
|
|
func TestRecordExternalRejectsBadInput(t *testing.T) {
|
|
tr, _ := openTraffic(t)
|
|
cases := []ExternalExchange{
|
|
{Method: "GET", URL: "not a url"},
|
|
{Method: "GET", URL: ""},
|
|
{Method: "", URL: "https://api.example.com/"},
|
|
}
|
|
for i, c := range cases {
|
|
if err := tr.RecordExternal(c); err == nil {
|
|
t.Fatalf("case %d: expected an error for %+v", i, c)
|
|
}
|
|
}
|
|
}
|