Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com>
This commit is contained in:
Hadi
2026-05-12 19:12:29 +02:00
commit e8e64eff12
101 changed files with 10081 additions and 0 deletions
+222
View File
@@ -0,0 +1,222 @@
package intercept
import (
"regexp"
"sync"
"sync/atomic"
"time"
"github.com/anotherhadi/spilltea/internal/config"
"github.com/anotherhadi/spilltea/internal/db"
"github.com/lqqyt2423/go-mitmproxy/proxy"
)
type Decision int
const (
Forward Decision = iota // forward without showing in intercept
Drop // drop the flow
Intercept // pass to the TUI for user decision
)
type PendingRequest struct {
Flow *proxy.Flow
decision chan Decision
ArrivedAt time.Time
}
type PendingResponse struct {
Flow *proxy.Flow
decision chan Decision
ArrivedAt time.Time
}
type Broker struct {
Incoming chan *PendingRequest
IncomingResponse chan *PendingResponse
captureResponse atomic.Bool
dbMu sync.RWMutex
database *db.DB
droppedFlows sync.Map // *proxy.Flow → struct{}
outOfScope sync.Map // *proxy.Flow → struct{}
scopeMu sync.RWMutex
whitelist []*regexp.Regexp
blacklist []*regexp.Regexp
onNewEntry func(db.Entry)
}
func (b *Broker) SetOnNewEntry(cb func(db.Entry)) {
b.onNewEntry = cb
}
// IsInScope reports whether the given target string (host+path) matches the
// current scope rules. Used by the plugin API.
func (b *Broker) IsInScope(target string) bool {
b.scopeMu.RLock()
wl := b.whitelist
bl := b.blacklist
b.scopeMu.RUnlock()
return scopeMatches(wl, bl, target)
}
func NewBroker() *Broker {
return &Broker{
Incoming: make(chan *PendingRequest, 64),
IncomingResponse: make(chan *PendingResponse, 64),
}
}
func (b *Broker) SetCaptureResponse(v bool) {
b.captureResponse.Store(v)
}
// SetScope compiles and stores whitelist/blacklist regex patterns.
// Invalid patterns are silently skipped.
func (b *Broker) SetScope(whitelist, blacklist []string) {
wl := compilePatterns(whitelist)
bl := compilePatterns(blacklist)
b.scopeMu.Lock()
b.whitelist = wl
b.blacklist = bl
b.scopeMu.Unlock()
}
func compilePatterns(patterns []string) []*regexp.Regexp {
out := make([]*regexp.Regexp, 0, len(patterns))
for _, p := range patterns {
if r, err := regexp.Compile(p); err == nil {
out = append(out, r)
}
}
return out
}
func (b *Broker) matchesScope(f *proxy.Flow) bool {
target := f.Request.URL.Host + f.Request.URL.Path
b.scopeMu.RLock()
wl := b.whitelist
bl := b.blacklist
b.scopeMu.RUnlock()
return scopeMatches(wl, bl, target)
}
func scopeMatches(wl, bl []*regexp.Regexp, target string) bool {
if len(wl) > 0 {
matched := false
for _, r := range wl {
if r.MatchString(target) {
matched = true
break
}
}
if !matched {
return false
}
}
for _, r := range bl {
if r.MatchString(target) {
return false
}
}
return true
}
func (b *Broker) SetDB(d *db.DB) {
b.dbMu.Lock()
b.database = d
b.dbMu.Unlock()
}
// Hold is called from the proxy addon: it blocks until a decision is made in the TUI.
func (b *Broker) Hold(f *proxy.Flow) Decision {
if !b.matchesScope(f) {
b.outOfScope.Store(f, struct{}{})
return Forward
}
p := &PendingRequest{
Flow: f,
decision: make(chan Decision, 1),
ArrivedAt: time.Now(),
}
b.Incoming <- p
d := <-p.decision
if d == Drop {
b.droppedFlows.Store(f, struct{}{})
}
return d
}
// HoldResponse is called from the proxy addon after receiving the response headers, but before reading the body.
func (b *Broker) HoldResponse(f *proxy.Flow) Decision {
if _, oos := b.outOfScope.Load(f); oos {
return Forward
}
if !b.captureResponse.Load() {
return Forward
}
p := &PendingResponse{
Flow: f,
decision: make(chan Decision, 1),
ArrivedAt: time.Now(),
}
b.IncomingResponse <- p
return <-p.decision
}
// SaveEntry persists the completed flow to the history DB.
// It must be called after HoldResponse and before modifying f.Response.
// Flows that were dropped at the request phase are silently skipped.
func (b *Broker) SaveEntry(f *proxy.Flow) {
b.dbMu.RLock()
d := b.database
b.dbMu.RUnlock()
if d == nil {
return
}
if _, oos := b.outOfScope.LoadAndDelete(f); oos {
return
}
if _, dropped := b.droppedFlows.LoadAndDelete(f); dropped {
return
}
status := 0
if f.Response != nil {
status = f.Response.StatusCode
}
r := f.Request
path := r.URL.Path
if path == "" {
path = "/"
}
if config.Global.History.SkipDuplicates {
body := string(r.Body)
if dup, _ := d.HasDuplicate(r.Method, r.URL.Host, path, body); dup {
return
}
}
entry, err := d.InsertEntry(db.Entry{
Timestamp: time.Now(),
Method: r.Method,
Host: r.URL.Host,
Path: path,
StatusCode: status,
RequestRaw: FormatRawRequest(f),
ResponseRaw: FormatRawResponse(f),
})
if err == nil {
if cb := b.onNewEntry; cb != nil {
go cb(entry)
}
}
}
func (b *Broker) Decide(p *PendingRequest, d Decision) {
p.decision <- d
}
func (b *Broker) DecideResponse(p *PendingResponse, d Decision) {
p.decision <- d
}
+18
View File
@@ -0,0 +1,18 @@
package intercept
import tea "charm.land/bubbletea/v2"
type RequestArrivedMsg struct{ Req *PendingRequest }
type ResponseArrivedMsg struct{ Resp *PendingResponse }
func WaitForRequest(b *Broker) tea.Cmd {
return func() tea.Msg {
return RequestArrivedMsg{Req: <-b.Incoming}
}
}
func WaitForResponse(b *Broker) tea.Cmd {
return func() tea.Msg {
return ResponseArrivedMsg{Resp: <-b.IncomingResponse}
}
}
+61
View File
@@ -0,0 +1,61 @@
package intercept
import (
"fmt"
"net/http"
"sort"
"strings"
"github.com/lqqyt2423/go-mitmproxy/proxy"
)
// FormatRawRequest serialises a flow's request to a raw HTTP string.
func FormatRawRequest(f *proxy.Flow) string {
r := f.Request
var sb strings.Builder
fmt.Fprintf(&sb, "%s %s %s\n", r.Method, r.URL.RequestURI(), r.Proto)
keys := make([]string, 0, len(r.Header))
for k := range r.Header {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
for _, v := range r.Header[k] {
fmt.Fprintf(&sb, "%s: %s\n", k, v)
}
}
sb.WriteString("\n")
if len(r.Body) > 0 {
sb.Write(r.Body)
}
return sb.String()
}
// FormatRawResponse serialises a flow's response to a raw HTTP string.
func FormatRawResponse(f *proxy.Flow) string {
r := f.Response
if r == nil {
return "(no response)"
}
var sb strings.Builder
proto := f.Request.Proto
if proto == "" {
proto = "HTTP/1.1"
}
fmt.Fprintf(&sb, "%s %d %s\n", proto, r.StatusCode, http.StatusText(r.StatusCode))
keys := make([]string, 0, len(r.Header))
for k := range r.Header {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
for _, v := range r.Header[k] {
fmt.Fprintf(&sb, "%s: %s\n", k, v)
}
}
sb.WriteString("\n")
if len(r.Body) > 0 {
sb.Write(r.Body)
}
return sb.String()
}