Files
artex/traffic/evidence.go
T
dela 0335d572de
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
First Commit
2026-10-09 08:38:16 +08:00

127 lines
3.2 KiB
Go

package traffic
import (
"bufio"
"bytes"
"context"
"database/sql"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
)
// EvidenceExchange exposes complete captured bodies only during the callback.
// Readers are closed before releasing the writer lock, so host deletion cannot
// remove a blob between its index lookup and its evidence copy.
type EvidenceExchange struct {
ID, URL, Method, ContentType string
TS int64
Status int
ReqHead, RespHead string
ReqLen, RespLen int64
ReqHash, RespHash string
Request, Response io.Reader
}
func (t *Traffic) ReadEvidence(ctx context.Context, ids []string, consume func(EvidenceExchange) error) error {
if t == nil {
return errors.New("流量录制存储不可用")
}
t.wmu.Lock()
defer t.wmu.Unlock()
for _, id := range ids {
if err := ctx.Err(); err != nil {
return err
}
if err := t.readEvidence(id, consume); err != nil {
return fmt.Errorf("流量 %s: %w", id, err)
}
}
return nil
}
func (t *Traffic) readEvidence(id string, consume func(EvidenceExchange) error) error {
e := EvidenceExchange{ID: id}
var legacy string
if err := t.db.QueryRow(`SELECT ts,url,method,status,content_type,req_len,resp_len,path FROM exchanges WHERE id=?`, id).
Scan(&e.TS, &e.URL, &e.Method, &e.Status, &e.ContentType, &e.ReqLen, &e.RespLen, &legacy); err != nil {
return err
}
var req, resp []byte
var reqBlob, respBlob sql.NullString
err := t.db.QueryRow(`SELECT req_head,req_body,req_blob,resp_head,resp_body,resp_blob FROM exchange_bodies WHERE id=?`, id).
Scan(&e.ReqHead, &req, &reqBlob, &e.RespHead, &resp, &respBlob)
if errors.Is(err, sql.ErrNoRows) && legacy != "" {
if !filepath.IsLocal(legacy) {
return errors.New("旧流量路径无效")
}
r, err := os.Open(filepath.Join(t.dir, legacy, "request.http"))
if err != nil {
return err
}
defer r.Close()
p, err := os.Open(filepath.Join(t.dir, legacy, "response.http"))
if err != nil {
return err
}
defer p.Close()
e.ReqHead, e.Request, err = splitLegacyEvidence(r)
if err != nil {
return err
}
e.RespHead, e.Response, err = splitLegacyEvidence(p)
if err != nil {
return err
}
return consume(e)
}
if err != nil {
return err
}
e.Request = bytes.NewReader(req)
e.Response = bytes.NewReader(resp)
if reqBlob.Valid && reqBlob.String != "" {
f, _, err := t.Blob(reqBlob.String)
if err != nil {
return err
}
defer f.Close()
e.Request = f
e.ReqHash = reqBlob.String
}
if respBlob.Valid && respBlob.String != "" {
f, _, err := t.Blob(respBlob.String)
if err != nil {
return err
}
defer f.Close()
e.Response = f
e.RespHash = respBlob.String
}
return consume(e)
}
func splitLegacyEvidence(r io.Reader) (string, io.Reader, error) {
b := bufio.NewReader(r)
var head strings.Builder
for {
line, err := b.ReadString('\n')
if line == "\n" || line == "\r\n" {
return head.String(), b, nil
}
head.WriteString(line)
if head.Len() > 1<<20 {
return "", nil, errors.New("旧流量报文头过大")
}
if errors.Is(err, io.EOF) {
return head.String(), b, nil
}
if err != nil {
return "", nil, err
}
}
}