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
127 lines
3.2 KiB
Go
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
|
|
}
|
|
}
|
|
}
|