mirror of
https://github.com/anotherhadi/spilltea.git
synced 2026-05-20 01:32:33 +02:00
Remove scope page
Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com>
This commit is contained in:
@@ -1,19 +0,0 @@
|
|||||||
## Scopes
|
|
||||||
|
|
||||||
Scopes let you control which requests Spilltea intercepts. Patterns are Go regular expressions matched against `host/path` (e.g. `api.example.com/v1/users`).
|
|
||||||
|
|
||||||
- **Whitelist**: if non-empty, only matching requests are intercepted.
|
|
||||||
- **Blacklist**: matching requests are always ignored, even if whitelisted.
|
|
||||||
|
|
||||||
When both lists are set, a request must pass the whitelist _and_ not be in the blacklist.
|
|
||||||
|
|
||||||
### Examples
|
|
||||||
|
|
||||||
| Pattern | Matches |
|
|
||||||
| -------------------------- | ----------------------------------- |
|
|
||||||
| `example\.com` | any request to `example.com` |
|
|
||||||
| `^api\.example\.com` | only the `api` subdomain |
|
|
||||||
| `example\.com/api/v2` | a specific path prefix |
|
|
||||||
| `\.(js\|css\|png\|woff2?)` | static assets (useful in blacklist) |
|
|
||||||
| `googleapis\.com` | all Google API traffic |
|
|
||||||
| `/graphql$` | any host with a `/graphql` endpoint |
|
|
||||||
@@ -25,7 +25,6 @@ It is intentionally minimal. No Electron, no browser, no bloat. Just a fast, key
|
|||||||
- **Intercept**: Pause requests and responses in-flight. Inspect and modify them (even with your favorite editor) before forwarding.
|
- **Intercept**: Pause requests and responses in-flight. Inspect and modify them (even with your favorite editor) before forwarding.
|
||||||
- **HTTP History**: Every request that passes through the proxy is stored. Browse, search and filter your full session history.
|
- **HTTP History**: Every request that passes through the proxy is stored. Browse, search and filter your full session history.
|
||||||
- **Replay**: Pick any request from the history, modify it if needed, and send it again. Useful for manual testing and quick iteration
|
- **Replay**: Pick any request from the history, modify it if needed, and send it again. Useful for manual testing and quick iteration
|
||||||
- **Scopes**: Keep your history clean by white/blacklisting domains or specific paths.
|
|
||||||
- **HTTPS Support** (using go-mitmproxy under the hood)
|
- **HTTPS Support** (using go-mitmproxy under the hood)
|
||||||
- Built-in Integrations:
|
- Built-in Integrations:
|
||||||
- **FFuf Export**: Generate a ffuf command or configuration directly from a request to start fuzzing instantly.
|
- **FFuf Export**: Generate a ffuf command or configuration directly from a request to start fuzzing instantly.
|
||||||
|
|||||||
@@ -33,8 +33,9 @@ type Config struct {
|
|||||||
} `mapstructure:"tui"`
|
} `mapstructure:"tui"`
|
||||||
|
|
||||||
Intercept struct {
|
Intercept struct {
|
||||||
DefaultAutoForward bool `mapstructure:"default_auto_forward"`
|
DefaultAutoForward bool `mapstructure:"default_auto_forward"`
|
||||||
DefaultCaptureResponse bool `mapstructure:"default_capture_response"`
|
DefaultCaptureResponse bool `mapstructure:"default_capture_response"`
|
||||||
|
AutoForwardRegex []string `mapstructure:"auto_forward_regex"`
|
||||||
} `mapstructure:"intercept"`
|
} `mapstructure:"intercept"`
|
||||||
|
|
||||||
Replay struct {
|
Replay struct {
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ app:
|
|||||||
intercept:
|
intercept:
|
||||||
default_auto_forward: false
|
default_auto_forward: false
|
||||||
default_capture_response: false
|
default_capture_response: false
|
||||||
|
auto_forward_regex:
|
||||||
|
- '\.(js|css|png|gif|ico|woff2?|ttf|svg)(\?.*)?$'
|
||||||
|
|
||||||
replay:
|
replay:
|
||||||
switch_to_page_on_send: false
|
switch_to_page_on_send: false
|
||||||
|
|||||||
+1
-9
@@ -35,12 +35,7 @@ func (d *DB) migrate() error {
|
|||||||
request_raw TEXT NOT NULL,
|
request_raw TEXT NOT NULL,
|
||||||
response_raw TEXT NOT NULL
|
response_raw TEXT NOT NULL
|
||||||
);
|
);
|
||||||
CREATE TABLE IF NOT EXISTS scope (
|
CREATE TABLE IF NOT EXISTS replay_entries (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
kind TEXT NOT NULL CHECK(kind IN ('whitelist','blacklist')),
|
|
||||||
pattern TEXT NOT NULL
|
|
||||||
);
|
|
||||||
CREATE TABLE IF NOT EXISTS replay_entries (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
timestamp DATETIME NOT NULL,
|
timestamp DATETIME NOT NULL,
|
||||||
scheme TEXT NOT NULL,
|
scheme TEXT NOT NULL,
|
||||||
@@ -69,9 +64,6 @@ func (d *DB) migrate() error {
|
|||||||
created_at DATETIME NOT NULL,
|
created_at DATETIME NOT NULL,
|
||||||
UNIQUE(plugin_name, dedup_key)
|
UNIQUE(plugin_name, dedup_key)
|
||||||
);
|
);
|
||||||
INSERT INTO scope (kind, pattern)
|
|
||||||
SELECT 'blacklist', '\.(js|css|png|gif|ico|woff2?|ttf|svg)(\?.*)?$'
|
|
||||||
WHERE NOT EXISTS (SELECT 1 FROM scope);
|
|
||||||
`)
|
`)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
package db
|
|
||||||
|
|
||||||
func (d *DB) SaveScope(whitelist, blacklist []string) error {
|
|
||||||
tx, err := d.conn.Begin()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if _, err := tx.Exec(`DELETE FROM scope`); err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, p := range whitelist {
|
|
||||||
if _, err := tx.Exec(`INSERT INTO scope (kind, pattern) VALUES ('whitelist', ?)`, p); err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, p := range blacklist {
|
|
||||||
if _, err := tx.Exec(`INSERT INTO scope (kind, pattern) VALUES ('blacklist', ?)`, p); err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tx.Commit()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DB) LoadScope() (whitelist, blacklist []string, err error) {
|
|
||||||
rows, err := d.conn.Query(`SELECT kind, pattern FROM scope`)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
for rows.Next() {
|
|
||||||
var kind, pattern string
|
|
||||||
if err := rows.Scan(&kind, &pattern); err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
if kind == "whitelist" {
|
|
||||||
whitelist = append(whitelist, pattern)
|
|
||||||
} else {
|
|
||||||
blacklist = append(blacklist, pattern)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return whitelist, blacklist, rows.Err()
|
|
||||||
}
|
|
||||||
@@ -41,9 +41,8 @@ type Broker struct {
|
|||||||
droppedFlows sync.Map // *proxy.Flow → struct{}
|
droppedFlows sync.Map // *proxy.Flow → struct{}
|
||||||
outOfScope sync.Map // *proxy.Flow → struct{}
|
outOfScope sync.Map // *proxy.Flow → struct{}
|
||||||
|
|
||||||
scopeMu sync.RWMutex
|
autoFwdMu sync.RWMutex
|
||||||
whitelist []*regexp.Regexp
|
autoFwdRegexes []*regexp.Regexp
|
||||||
blacklist []*regexp.Regexp
|
|
||||||
|
|
||||||
onNewEntry func(db.Entry)
|
onNewEntry func(db.Entry)
|
||||||
}
|
}
|
||||||
@@ -52,76 +51,44 @@ func (b *Broker) SetOnNewEntry(cb func(db.Entry)) {
|
|||||||
b.onNewEntry = cb
|
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 {
|
func NewBroker() *Broker {
|
||||||
return &Broker{
|
b := &Broker{
|
||||||
Incoming: make(chan *PendingRequest, 64),
|
Incoming: make(chan *PendingRequest, 64),
|
||||||
IncomingResponse: make(chan *PendingResponse, 64),
|
IncomingResponse: make(chan *PendingResponse, 64),
|
||||||
}
|
}
|
||||||
|
b.SetAutoForwardRegex(config.Global.Intercept.AutoForwardRegex)
|
||||||
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Broker) SetCaptureResponse(v bool) {
|
func (b *Broker) SetCaptureResponse(v bool) {
|
||||||
b.captureResponse.Store(v)
|
b.captureResponse.Store(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetScope compiles and stores whitelist/blacklist regex patterns.
|
// SetAutoForwardRegex compiles and stores patterns for requests that should
|
||||||
|
// be forwarded automatically without interception or history logging.
|
||||||
// Invalid patterns are silently skipped.
|
// Invalid patterns are silently skipped.
|
||||||
func (b *Broker) SetScope(whitelist, blacklist []string) {
|
func (b *Broker) SetAutoForwardRegex(patterns []string) {
|
||||||
wl := compilePatterns(whitelist)
|
compiled := make([]*regexp.Regexp, 0, len(patterns))
|
||||||
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 {
|
for _, p := range patterns {
|
||||||
if r, err := regexp.Compile(p); err == nil {
|
if r, err := regexp.Compile(p); err == nil {
|
||||||
out = append(out, r)
|
compiled = append(compiled, r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return out
|
b.autoFwdMu.Lock()
|
||||||
|
b.autoFwdRegexes = compiled
|
||||||
|
b.autoFwdMu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Broker) matchesScope(f *proxy.Flow) bool {
|
func (b *Broker) isAutoForwarded(target string) bool {
|
||||||
target := f.Request.URL.Host + f.Request.URL.Path
|
b.autoFwdMu.RLock()
|
||||||
b.scopeMu.RLock()
|
regexes := b.autoFwdRegexes
|
||||||
wl := b.whitelist
|
b.autoFwdMu.RUnlock()
|
||||||
bl := b.blacklist
|
for _, r := range regexes {
|
||||||
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) {
|
if r.MatchString(target) {
|
||||||
return false
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Broker) SetDB(d *db.DB) {
|
func (b *Broker) SetDB(d *db.DB) {
|
||||||
@@ -132,7 +99,8 @@ func (b *Broker) SetDB(d *db.DB) {
|
|||||||
|
|
||||||
// Hold is called from the proxy addon: it blocks until a decision is made in the TUI.
|
// 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 {
|
func (b *Broker) Hold(f *proxy.Flow) Decision {
|
||||||
if !b.matchesScope(f) {
|
target := f.Request.URL.Host + f.Request.URL.Path
|
||||||
|
if b.isAutoForwarded(target) {
|
||||||
b.outOfScope.Store(f, struct{}{})
|
b.outOfScope.Store(f, struct{}{})
|
||||||
return Forward
|
return Forward
|
||||||
}
|
}
|
||||||
@@ -168,7 +136,7 @@ func (b *Broker) HoldResponse(f *proxy.Flow) Decision {
|
|||||||
|
|
||||||
// SaveEntry persists the completed flow to the history DB.
|
// SaveEntry persists the completed flow to the history DB.
|
||||||
// It must be called after HoldResponse and before modifying f.Response.
|
// It must be called after HoldResponse and before modifying f.Response.
|
||||||
// Flows that were dropped at the request phase are silently skipped.
|
// Flows that were dropped or auto-forwarded are silently skipped.
|
||||||
func (b *Broker) SaveEntry(f *proxy.Flow) {
|
func (b *Broker) SaveEntry(f *proxy.Flow) {
|
||||||
b.dbMu.RLock()
|
b.dbMu.RLock()
|
||||||
d := b.database
|
d := b.database
|
||||||
|
|||||||
+1
-21
@@ -2,7 +2,6 @@ package plugins
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
"net/url"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -65,26 +64,7 @@ func registerUtilities(L *lua.LState, mgr *Manager, p *Plugin) {
|
|||||||
return 0
|
return 0
|
||||||
}))
|
}))
|
||||||
|
|
||||||
L.SetGlobal("is_in_scope", L.NewFunction(func(L *lua.LState) int {
|
L.SetGlobal("quit", L.NewFunction(func(L *lua.LState) int {
|
||||||
raw := L.CheckString(1)
|
|
||||||
if mgr.broker == nil {
|
|
||||||
L.Push(lua.LTrue)
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
u, err := url.Parse(raw)
|
|
||||||
if err != nil {
|
|
||||||
L.Push(lua.LFalse)
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
path := u.Path
|
|
||||||
if path == "" {
|
|
||||||
path = "/"
|
|
||||||
}
|
|
||||||
L.Push(lua.LBool(mgr.broker.IsInScope(u.Host + path)))
|
|
||||||
return 1
|
|
||||||
}))
|
|
||||||
|
|
||||||
L.SetGlobal("quit", L.NewFunction(func(L *lua.LState) int {
|
|
||||||
reason := L.OptString(1, "plugin requested quit")
|
reason := L.OptString(1, "plugin requested quit")
|
||||||
select {
|
select {
|
||||||
case mgr.Quit <- reason:
|
case mgr.Quit <- reason:
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import (
|
|||||||
interceptUI "github.com/anotherhadi/spilltea/internal/ui/intercept"
|
interceptUI "github.com/anotherhadi/spilltea/internal/ui/intercept"
|
||||||
pluginsUI "github.com/anotherhadi/spilltea/internal/ui/plugins"
|
pluginsUI "github.com/anotherhadi/spilltea/internal/ui/plugins"
|
||||||
replayUI "github.com/anotherhadi/spilltea/internal/ui/replay"
|
replayUI "github.com/anotherhadi/spilltea/internal/ui/replay"
|
||||||
scopeUI "github.com/anotherhadi/spilltea/internal/ui/scope"
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -64,7 +63,6 @@ type Model struct {
|
|||||||
replay replayUI.Model
|
replay replayUI.Model
|
||||||
diff diffUI.Model
|
diff diffUI.Model
|
||||||
docs docsUI.Model
|
docs docsUI.Model
|
||||||
scope scopeUI.Model
|
|
||||||
pluginsPage pluginsUI.Model
|
pluginsPage pluginsUI.Model
|
||||||
findingsPage findingsUI.Model
|
findingsPage findingsUI.Model
|
||||||
copyAs copyasUI.Model
|
copyAs copyasUI.Model
|
||||||
@@ -86,7 +84,6 @@ func New(broker *intercept.Broker, name, path string) Model {
|
|||||||
replay: replayUI.New(),
|
replay: replayUI.New(),
|
||||||
diff: diffUI.New(),
|
diff: diffUI.New(),
|
||||||
docs: docsUI.New(),
|
docs: docsUI.New(),
|
||||||
scope: scopeUI.New(name, path),
|
|
||||||
pluginsPage: pluginsUI.New(mgr),
|
pluginsPage: pluginsUI.New(mgr),
|
||||||
findingsPage: findingsUI.New(),
|
findingsPage: findingsUI.New(),
|
||||||
copyAs: copyasUI.New(),
|
copyAs: copyasUI.New(),
|
||||||
@@ -101,10 +98,6 @@ func New(broker *intercept.Broker, name, path string) Model {
|
|||||||
m.replay.SetDB(d)
|
m.replay.SetDB(d)
|
||||||
m.findingsPage.SetDB(d)
|
m.findingsPage.SetDB(d)
|
||||||
mgr.SetDB(d)
|
mgr.SetDB(d)
|
||||||
if wl, bl, err := d.LoadScope(); err == nil {
|
|
||||||
broker.SetScope(wl, bl)
|
|
||||||
m.scope.SetScope(wl, bl)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pluginsDir := config.ExpandPath(cfg.App.PluginsDir)
|
pluginsDir := config.ExpandPath(cfg.App.PluginsDir)
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
interceptUI "github.com/anotherhadi/spilltea/internal/ui/intercept"
|
interceptUI "github.com/anotherhadi/spilltea/internal/ui/intercept"
|
||||||
pluginsUI "github.com/anotherhadi/spilltea/internal/ui/plugins"
|
pluginsUI "github.com/anotherhadi/spilltea/internal/ui/plugins"
|
||||||
replayUI "github.com/anotherhadi/spilltea/internal/ui/replay"
|
replayUI "github.com/anotherhadi/spilltea/internal/ui/replay"
|
||||||
scopeUI "github.com/anotherhadi/spilltea/internal/ui/scope"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type page string
|
type page string
|
||||||
@@ -20,7 +19,6 @@ const (
|
|||||||
pageHistory page = "History"
|
pageHistory page = "History"
|
||||||
pageReplay page = "Replay"
|
pageReplay page = "Replay"
|
||||||
pageDiff page = "Diff"
|
pageDiff page = "Diff"
|
||||||
pageScopes page = "Scopes"
|
|
||||||
pagePlugins page = "Plugins"
|
pagePlugins page = "Plugins"
|
||||||
pageFindings page = "Findings"
|
pageFindings page = "Findings"
|
||||||
pageDocs page = "Docs"
|
pageDocs page = "Docs"
|
||||||
@@ -93,19 +91,6 @@ var pageRegistry = []pageEntry{
|
|||||||
},
|
},
|
||||||
resize: func(m *Model, w, h int) { m.diff.SetSize(w, h) },
|
resize: func(m *Model, w, h int) { m.diff.SetSize(w, h) },
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: pageScopes,
|
|
||||||
icon: func() string { return icons.I.Scope },
|
|
||||||
|
|
||||||
render: func(m *Model) string { return m.scope.View().Content },
|
|
||||||
update: func(m *Model, msg tea.Msg) tea.Cmd {
|
|
||||||
updated, cmd := m.scope.Update(msg)
|
|
||||||
m.scope = updated.(scopeUI.Model)
|
|
||||||
return cmd
|
|
||||||
},
|
|
||||||
isEditing: func(m *Model) bool { return m.scope.IsEditing() },
|
|
||||||
resize: func(m *Model, w, h int) { m.scope.SetSize(w, h) },
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: pagePlugins,
|
id: pagePlugins,
|
||||||
icon: func() string { return icons.I.Plugin },
|
icon: func() string { return icons.I.Plugin },
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import (
|
|||||||
historyUI "github.com/anotherhadi/spilltea/internal/ui/history"
|
historyUI "github.com/anotherhadi/spilltea/internal/ui/history"
|
||||||
interceptUI "github.com/anotherhadi/spilltea/internal/ui/intercept"
|
interceptUI "github.com/anotherhadi/spilltea/internal/ui/intercept"
|
||||||
replayUI "github.com/anotherhadi/spilltea/internal/ui/replay"
|
replayUI "github.com/anotherhadi/spilltea/internal/ui/replay"
|
||||||
scopeUI "github.com/anotherhadi/spilltea/internal/ui/scope"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
@@ -79,15 +78,6 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.height = msg.Height
|
m.height = msg.Height
|
||||||
m.resizeChildren()
|
m.resizeChildren()
|
||||||
|
|
||||||
case scopeUI.ScopeChangedMsg:
|
|
||||||
m.broker.SetScope(msg.Whitelist, msg.Blacklist)
|
|
||||||
if m.database != nil {
|
|
||||||
if err := m.database.SaveScope(msg.Whitelist, msg.Blacklist); err != nil {
|
|
||||||
log.Printf("failed to persist scope: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
|
|
||||||
case proxyPkg.ErrMsg:
|
case proxyPkg.ErrMsg:
|
||||||
if msg.Err != nil {
|
if msg.Err != nil {
|
||||||
log.Printf("proxy error: %v", msg.Err)
|
log.Printf("proxy error: %v", msg.Err)
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ var contentMarkdown = strings.Join([]string{
|
|||||||
readDoc("proxy.md"),
|
readDoc("proxy.md"),
|
||||||
readDoc("certificate.md"),
|
readDoc("certificate.md"),
|
||||||
readDoc("history.md"),
|
readDoc("history.md"),
|
||||||
readDoc("scopes.md"),
|
|
||||||
}, "\n")
|
}, "\n")
|
||||||
|
|
||||||
type Model struct {
|
type Model struct {
|
||||||
|
|||||||
@@ -1,150 +0,0 @@
|
|||||||
package scope
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"charm.land/bubbles/v2/help"
|
|
||||||
"charm.land/bubbles/v2/key"
|
|
||||||
"charm.land/bubbles/v2/textarea"
|
|
||||||
tea "charm.land/bubbletea/v2"
|
|
||||||
"charm.land/lipgloss/v2"
|
|
||||||
"github.com/anotherhadi/spilltea/internal/keys"
|
|
||||||
"github.com/anotherhadi/spilltea/internal/style"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
fieldNone = -1
|
|
||||||
fieldWhitelist = 0
|
|
||||||
fieldBlacklist = 1
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
minTaH = 3
|
|
||||||
maxTaH = 12
|
|
||||||
fixedH = 8 // (blank + label + desc + blank) x2
|
|
||||||
)
|
|
||||||
|
|
||||||
type ScopeChangedMsg struct {
|
|
||||||
Whitelist []string
|
|
||||||
Blacklist []string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Model struct {
|
|
||||||
focusIdx int
|
|
||||||
|
|
||||||
wlTextarea textarea.Model
|
|
||||||
blTextarea textarea.Model
|
|
||||||
|
|
||||||
innerH int
|
|
||||||
width int
|
|
||||||
height int
|
|
||||||
|
|
||||||
help help.Model
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(name, path string) Model {
|
|
||||||
wl := style.NewTextarea(true)
|
|
||||||
wl.Placeholder = "one pattern per line..."
|
|
||||||
|
|
||||||
bl := style.NewTextarea(true)
|
|
||||||
bl.Placeholder = "one pattern per line..."
|
|
||||||
bl.Blur()
|
|
||||||
|
|
||||||
return Model{
|
|
||||||
focusIdx: fieldNone,
|
|
||||||
wlTextarea: wl,
|
|
||||||
blTextarea: bl,
|
|
||||||
help: style.NewHelp(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) Init() tea.Cmd { return nil }
|
|
||||||
|
|
||||||
func (m *Model) SetScope(whitelist, blacklist []string) {
|
|
||||||
m.wlTextarea.SetValue(strings.Join(whitelist, "\n"))
|
|
||||||
m.blTextarea.SetValue(strings.Join(blacklist, "\n"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) SetSize(w, h int) {
|
|
||||||
m.width = w
|
|
||||||
m.height = h
|
|
||||||
m.syncLayout()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) syncLayout() {
|
|
||||||
if m.width == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
m.help.SetWidth(m.width - 2)
|
|
||||||
|
|
||||||
statusH := strings.Count(m.renderStatusBar(), "\n") + 1
|
|
||||||
panelH := m.height - statusH
|
|
||||||
m.innerH = max(1, style.PanelContentH(panelH))
|
|
||||||
|
|
||||||
taH := (m.innerH - fixedH) / 2
|
|
||||||
if taH < minTaH {
|
|
||||||
taH = minTaH
|
|
||||||
}
|
|
||||||
if taH > maxTaH {
|
|
||||||
taH = maxTaH
|
|
||||||
}
|
|
||||||
// width - 2 (panel border) - 1 (leading space in view) - 3 (right margin + cursor)
|
|
||||||
taW := max(1, m.width-6)
|
|
||||||
m.wlTextarea.SetWidth(taW)
|
|
||||||
m.wlTextarea.SetHeight(taH)
|
|
||||||
m.blTextarea.SetWidth(taW)
|
|
||||||
m.blTextarea.SetHeight(taH)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) IsEditing() bool {
|
|
||||||
return m.focusIdx == fieldWhitelist || m.focusIdx == fieldBlacklist
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) scopeChangedCmd() tea.Cmd {
|
|
||||||
wl := parseLines(m.wlTextarea.Value())
|
|
||||||
bl := parseLines(m.blTextarea.Value())
|
|
||||||
return func() tea.Msg {
|
|
||||||
return ScopeChangedMsg{Whitelist: wl, Blacklist: bl}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseLines(s string) []string {
|
|
||||||
var out []string
|
|
||||||
for _, line := range strings.Split(s, "\n") {
|
|
||||||
if t := strings.TrimSpace(line); t != "" {
|
|
||||||
out = append(out, t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) renderStatusBar() string {
|
|
||||||
return lipgloss.NewStyle().Padding(0, 1).Render(
|
|
||||||
m.help.View(formKeyMap{focusIdx: m.focusIdx}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
type formKeyMap struct {
|
|
||||||
focusIdx int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (k formKeyMap) ShortHelp() []key.Binding {
|
|
||||||
cycle := keys.Keys.Global.CycleFocus
|
|
||||||
hlp := keys.Keys.Global.Help
|
|
||||||
|
|
||||||
switch k.focusIdx {
|
|
||||||
case fieldWhitelist, fieldBlacklist:
|
|
||||||
esc := keys.Keys.Global.Escape
|
|
||||||
escBinding := key.NewBinding(key.WithKeys(esc.Keys()...), key.WithHelp(esc.Help().Key, "unfocus"))
|
|
||||||
return []key.Binding{
|
|
||||||
key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "new line")),
|
|
||||||
escBinding,
|
|
||||||
cycle,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return []key.Binding{cycle, hlp}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (k formKeyMap) FullHelp() [][]key.Binding {
|
|
||||||
return [][]key.Binding{k.ShortHelp()}
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
package scope
|
|
||||||
|
|
||||||
import (
|
|
||||||
"charm.land/bubbles/v2/key"
|
|
||||||
tea "charm.land/bubbletea/v2"
|
|
||||||
"github.com/anotherhadi/spilltea/internal/keys"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
||||||
kp, isKey := msg.(tea.KeyPressMsg)
|
|
||||||
if !isKey {
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if key.Matches(kp, keys.Keys.Global.CycleFocus) {
|
|
||||||
return m.cycleFocus()
|
|
||||||
}
|
|
||||||
|
|
||||||
if key.Matches(kp, keys.Keys.Global.Help) && !m.IsEditing() {
|
|
||||||
m.help.ShowAll = !m.help.ShowAll
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
switch m.focusIdx {
|
|
||||||
case fieldWhitelist:
|
|
||||||
if key.Matches(kp, keys.Keys.Global.Escape) {
|
|
||||||
return m.blurAll()
|
|
||||||
}
|
|
||||||
var cmd tea.Cmd
|
|
||||||
m.wlTextarea, cmd = m.wlTextarea.Update(kp)
|
|
||||||
return m, cmd
|
|
||||||
|
|
||||||
case fieldBlacklist:
|
|
||||||
if key.Matches(kp, keys.Keys.Global.Escape) {
|
|
||||||
return m.blurAll()
|
|
||||||
}
|
|
||||||
var cmd tea.Cmd
|
|
||||||
m.blTextarea, cmd = m.blTextarea.Update(kp)
|
|
||||||
return m, cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) blurAll() (tea.Model, tea.Cmd) {
|
|
||||||
m.wlTextarea.Blur()
|
|
||||||
m.blTextarea.Blur()
|
|
||||||
m.focusIdx = fieldNone
|
|
||||||
m.syncLayout()
|
|
||||||
return m, m.scopeChangedCmd()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) cycleFocus() (tea.Model, tea.Cmd) {
|
|
||||||
scopeCmd := m.scopeChangedCmd()
|
|
||||||
|
|
||||||
var focusCmd tea.Cmd
|
|
||||||
switch m.focusIdx {
|
|
||||||
case fieldNone, fieldBlacklist:
|
|
||||||
m.blTextarea.Blur()
|
|
||||||
m.focusIdx = fieldWhitelist
|
|
||||||
focusCmd = m.wlTextarea.Focus()
|
|
||||||
case fieldWhitelist:
|
|
||||||
m.wlTextarea.Blur()
|
|
||||||
m.focusIdx = fieldBlacklist
|
|
||||||
focusCmd = m.blTextarea.Focus()
|
|
||||||
}
|
|
||||||
|
|
||||||
m.syncLayout()
|
|
||||||
return m, tea.Batch(focusCmd, scopeCmd)
|
|
||||||
}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
package scope
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
tea "charm.land/bubbletea/v2"
|
|
||||||
"charm.land/lipgloss/v2"
|
|
||||||
"github.com/anotherhadi/spilltea/internal/icons"
|
|
||||||
"github.com/anotherhadi/spilltea/internal/style"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (m Model) View() tea.View {
|
|
||||||
if m.width == 0 {
|
|
||||||
return tea.NewView("")
|
|
||||||
}
|
|
||||||
|
|
||||||
s := style.S
|
|
||||||
|
|
||||||
statusBar := m.renderStatusBar()
|
|
||||||
statusH := strings.Count(statusBar, "\n") + 1
|
|
||||||
panelH := m.height - statusH
|
|
||||||
innerH := max(1, style.PanelContentH(panelH))
|
|
||||||
|
|
||||||
taH := (innerH - fixedH) / 2
|
|
||||||
if taH < minTaH {
|
|
||||||
taH = minTaH
|
|
||||||
}
|
|
||||||
if taH > maxTaH {
|
|
||||||
taH = maxTaH
|
|
||||||
}
|
|
||||||
|
|
||||||
var lines []string
|
|
||||||
add := func(l string) { lines = append(lines, l) }
|
|
||||||
|
|
||||||
add("")
|
|
||||||
add(fieldLabel("Whitelist", m.focusIdx == fieldWhitelist))
|
|
||||||
add(" " + s.Faint.Render("If non-empty, only matching requests are intercepted."))
|
|
||||||
add("")
|
|
||||||
wlContentLines := strings.Count(m.wlTextarea.Value(), "\n") + 1
|
|
||||||
for _, l := range taLines(m.wlTextarea.View(), taH, wlContentLines) {
|
|
||||||
add(" " + l)
|
|
||||||
}
|
|
||||||
|
|
||||||
add("")
|
|
||||||
add(fieldLabel("Blacklist", m.focusIdx == fieldBlacklist))
|
|
||||||
add(" " + s.Faint.Render("Matching requests are always excluded from history."))
|
|
||||||
add("")
|
|
||||||
blContentLines := strings.Count(m.blTextarea.Value(), "\n") + 1
|
|
||||||
for _, l := range taLines(m.blTextarea.View(), taH, blContentLines) {
|
|
||||||
add(" " + l)
|
|
||||||
}
|
|
||||||
|
|
||||||
for len(lines) < innerH {
|
|
||||||
lines = append(lines, "")
|
|
||||||
}
|
|
||||||
content := strings.Join(lines[:innerH], "\n")
|
|
||||||
|
|
||||||
panel := style.RenderWithTitle(s.PanelFocused, icons.I.Scope+"Scopes", content, m.width, panelH)
|
|
||||||
return tea.NewView(lipgloss.JoinVertical(lipgloss.Left, panel, statusBar))
|
|
||||||
}
|
|
||||||
|
|
||||||
func fieldLabel(name string, focused bool) string {
|
|
||||||
s := style.S
|
|
||||||
c := s.MutedFg
|
|
||||||
if focused {
|
|
||||||
c = s.Primary
|
|
||||||
}
|
|
||||||
return " " + lipgloss.NewStyle().Foreground(c).Bold(focused).Render(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func taLines(view string, h int, contentLines int) []string {
|
|
||||||
raw := strings.Split(strings.TrimRight(view, "\n"), "\n")
|
|
||||||
tilde := style.S.Faint.Render("~")
|
|
||||||
for len(raw) < h {
|
|
||||||
raw = append(raw, tilde)
|
|
||||||
}
|
|
||||||
if len(raw) > h {
|
|
||||||
raw = raw[:h]
|
|
||||||
}
|
|
||||||
for i := contentLines; i < len(raw); i++ {
|
|
||||||
raw[i] = tilde
|
|
||||||
}
|
|
||||||
return raw
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user