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
63 lines
1.7 KiB
Go
63 lines
1.7 KiB
Go
package server
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
|
|
"github.com/Autumn-27/artex/traffic"
|
|
)
|
|
|
|
// Ingest caps: a browser tab can produce a burst of entries, but the ingest
|
|
// path is meant for deliberate operator captures, not bulk sync. Bodies are
|
|
// truncated server-side as a second line of defense behind the extension's
|
|
// own caps.
|
|
const (
|
|
maxIngestBatch = 50
|
|
maxIngestBodySize = 256 << 10 // 256 KiB per body
|
|
)
|
|
|
|
// ingestTraffic accepts exchanges captured outside the MITM proxy (currently
|
|
// the browser extension) into the global traffic store, so operator-driven
|
|
// authenticated browsing becomes evidence the same way proxy traffic is.
|
|
func (s *Server) ingestTraffic(w http.ResponseWriter, r *http.Request) {
|
|
tr := s.m.Traffic()
|
|
if tr == nil {
|
|
writeErr(w, 503, "traffic store is not available")
|
|
return
|
|
}
|
|
var req struct {
|
|
Exchanges []traffic.ExternalExchange `json:"exchanges"`
|
|
}
|
|
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 32<<20)).Decode(&req); err != nil {
|
|
writeErr(w, 400, "bad json: "+err.Error())
|
|
return
|
|
}
|
|
if len(req.Exchanges) == 0 {
|
|
writeErr(w, 400, "empty exchanges")
|
|
return
|
|
}
|
|
if len(req.Exchanges) > maxIngestBatch {
|
|
writeErr(w, 400, "too many exchanges in one batch")
|
|
return
|
|
}
|
|
|
|
ingested, failed := 0, 0
|
|
for _, e := range req.Exchanges {
|
|
e.RequestBody = truncateBody(e.RequestBody)
|
|
e.ResponseBody = truncateBody(e.ResponseBody)
|
|
if err := tr.RecordExternal(e); err != nil {
|
|
failed++
|
|
continue
|
|
}
|
|
ingested++
|
|
}
|
|
writeJSON(w, 200, map[string]any{"ingested": ingested, "failed": failed})
|
|
}
|
|
|
|
func truncateBody(s string) string {
|
|
if len(s) <= maxIngestBodySize {
|
|
return s
|
|
}
|
|
return s[:maxIngestBodySize] + "\n... [truncated by ingest]"
|
|
}
|