mirror of
https://github.com/anotherhadi/spilltea.git
synced 2026-05-20 01:32:33 +02:00
QOL & Security improvement
Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com>
This commit is contained in:
+2
-6
@@ -4,9 +4,7 @@ The History page has a built-in search bar with two modes:
|
|||||||
|
|
||||||
**Fulltext search**: press `/` to open it. Results filter in real time as you type across all fields: method, host, path, and the raw request/response bodies.
|
**Fulltext search**: press `/` to open it. Results filter in real time as you type across all fields: method, host, path, and the raw request/response bodies.
|
||||||
|
|
||||||
**SQL mode**: press `:` to open it, then `Enter` to run. You can write either a WHERE expression or a full SELECT query against the `entries` table.
|
**SQL mode**: press `:` to open it, then `Enter` to run. Type a WHERE expression: the full `SELECT … FROM entries WHERE` is added automatically.
|
||||||
|
|
||||||
WHERE expression (the `SELECT` is added automatically):
|
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
status_code = 404
|
status_code = 404
|
||||||
@@ -16,10 +14,8 @@ status_code = 404
|
|||||||
host LIKE '%.api.%' AND method = 'POST'
|
host LIKE '%.api.%' AND method = 'POST'
|
||||||
```
|
```
|
||||||
|
|
||||||
Full SELECT query:
|
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
SELECT * FROM entries WHERE response_raw LIKE '%password%' ORDER BY timestamp DESC LIMIT 20
|
response_raw LIKE '%password%' ORDER BY timestamp DESC LIMIT 20
|
||||||
```
|
```
|
||||||
|
|
||||||
The `entries` table has the following columns: `id`, `timestamp`, `method`, `host`, `path`, `status_code`, `request_raw`, `response_raw`.
|
The `entries` table has the following columns: `id`, `timestamp`, `method`, `host`, `path`, `status_code`, `request_raw`, `response_raw`.
|
||||||
|
|||||||
+12
-12
@@ -30,14 +30,14 @@ Plugin = {
|
|||||||
|
|
||||||
### Hook reference
|
### Hook reference
|
||||||
|
|
||||||
| Hook | When called | Sync/async | Return value (sync only) |
|
| Hook | When called | Sync/async | Return value (sync only) |
|
||||||
| ------------------------- | ------------------------------------ | ------------- | ----------------------------------------------------- |
|
| ------------------------- | ------------------------------------- | ------------ | ----------------------------------------------- |
|
||||||
| `on_config(config_text)` | At startup and on config save | always sync | ignored |
|
| `on_config(config_text)` | At startup and on config save | always sync | ignored |
|
||||||
| `on_start()` | Once at startup, after `on_config` | configurable | ignored |
|
| `on_start()` | Once at startup, after `on_config` | configurable | ignored |
|
||||||
| `on_quit()` | When the app exits | always sync | ignored |
|
| `on_quit()` | When the app exits | always sync | ignored |
|
||||||
| `on_request(req)` | Every request, before auto-forward | configurable | `"drop"`, `"forward"`, or `nil` |
|
| `on_request(req)` | Every request, before auto-forward | configurable | `"drop"`, `"forward"`, or `nil` |
|
||||||
| `on_response(req, res)` | Every response | configurable | `"drop"`, `"forward"`, or `nil` |
|
| `on_response(req, res)` | Every response | configurable | `"drop"`, `"forward"`, or `nil` |
|
||||||
| `on_history_entry(entry)` | Sync: before DB insert / Async: after | configurable | `"skip"` (don't save), `"keep"` or `nil` (save) |
|
| `on_history_entry(entry)` | Sync: before DB insert / Async: after | configurable | `"skip"` (don't save), `"keep"` or `nil` (save) |
|
||||||
|
|
||||||
## Request and response objects
|
## Request and response objects
|
||||||
|
|
||||||
@@ -140,10 +140,10 @@ Each plugin gets a **config textarea** on the Plugins page. The raw text is pass
|
|||||||
|
|
||||||
**`on_history_entry` (sync only):**
|
**`on_history_entry` (sync only):**
|
||||||
|
|
||||||
| Return value | Effect |
|
| Return value | Effect |
|
||||||
| ------------------- | -------------------------------------- |
|
| ----------------- | --------------------------------- |
|
||||||
| `"skip"` | The entry is not saved to the DB. |
|
| `"skip"` | The entry is not saved to the DB. |
|
||||||
| `"keep"` or `nil` | The entry is saved normally. |
|
| `"keep"` or `nil` | The entry is saved normally. |
|
||||||
|
|
||||||
Sync `on_history_entry` runs **before** the DB insert, so it can prevent an entry from ever appearing in history. Async `on_history_entry` runs **after** the insert and cannot affect it.
|
Sync `on_history_entry` runs **before** the DB insert, so it can prevent an entry from ever appearing in history. Async `on_history_entry` runs **after** the insert and cannot affect it.
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ type Config struct {
|
|||||||
ProjectDir string `mapstructure:"project_dir"`
|
ProjectDir string `mapstructure:"project_dir"`
|
||||||
PluginsDir string `mapstructure:"plugins_dir"`
|
PluginsDir string `mapstructure:"plugins_dir"`
|
||||||
UpstreamProxy string `mapstructure:"upstream_proxy"`
|
UpstreamProxy string `mapstructure:"upstream_proxy"`
|
||||||
|
MaxBodySizeMB int `mapstructure:"max_body_size_mb"`
|
||||||
} `mapstructure:"app"`
|
} `mapstructure:"app"`
|
||||||
|
|
||||||
TUI struct {
|
TUI struct {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ app:
|
|||||||
project_dir: ~/.local/share/spilltea
|
project_dir: ~/.local/share/spilltea
|
||||||
plugins_dir: ~/.config/spilltea/plugins
|
plugins_dir: ~/.config/spilltea/plugins
|
||||||
upstream_proxy: "" # e.g. http://corporate-proxy:8888 or http://user:pass@host:8888
|
upstream_proxy: "" # e.g. http://corporate-proxy:8888 or http://user:pass@host:8888
|
||||||
|
max_body_size_mb: 50 # max response body size read into memory for large streamed responses (MB)
|
||||||
|
|
||||||
intercept:
|
intercept:
|
||||||
default_intercept_enabled: true
|
default_intercept_enabled: true
|
||||||
|
|||||||
+9
-2
@@ -2,12 +2,14 @@ package db
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"sync"
|
||||||
|
|
||||||
_ "modernc.org/sqlite"
|
_ "modernc.org/sqlite"
|
||||||
)
|
)
|
||||||
|
|
||||||
type DB struct {
|
type DB struct {
|
||||||
conn *sql.DB
|
conn *sql.DB
|
||||||
|
dedupMu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func Open(path string) (*DB, error) {
|
func Open(path string) (*DB, error) {
|
||||||
@@ -33,7 +35,8 @@ func (d *DB) migrate() error {
|
|||||||
path TEXT NOT NULL,
|
path TEXT NOT NULL,
|
||||||
status_code INTEGER NOT NULL,
|
status_code INTEGER NOT NULL,
|
||||||
request_raw TEXT NOT NULL,
|
request_raw TEXT NOT NULL,
|
||||||
response_raw TEXT NOT NULL
|
response_raw TEXT NOT NULL,
|
||||||
|
body_hash TEXT NOT NULL DEFAULT ''
|
||||||
);
|
);
|
||||||
CREATE TABLE IF NOT EXISTS replay_entries (
|
CREATE TABLE IF NOT EXISTS replay_entries (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
@@ -65,6 +68,10 @@ CREATE TABLE IF NOT EXISTS replay_entries (
|
|||||||
UNIQUE(plugin_name, dedup_key)
|
UNIQUE(plugin_name, dedup_key)
|
||||||
);
|
);
|
||||||
`)
|
`)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = d.conn.Exec(`CREATE INDEX IF NOT EXISTS idx_entries_dedup ON entries(method, host, path, body_hash)`)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+40
-40
@@ -1,6 +1,7 @@
|
|||||||
package db
|
package db
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -18,40 +19,45 @@ type Entry struct {
|
|||||||
ResponseRaw string
|
ResponseRaw string
|
||||||
}
|
}
|
||||||
|
|
||||||
// HasDuplicate returns true if an entry with the same method, host, path and
|
func bodyHash(body string) string {
|
||||||
// request body already exists. Used to implement skip_duplicates filtering.
|
sum := sha256.Sum256([]byte(body))
|
||||||
func (d *DB) HasDuplicate(method, host, path, body string) (bool, error) {
|
return fmt.Sprintf("%x", sum)
|
||||||
rows, err := d.conn.Query(
|
|
||||||
`SELECT request_raw FROM entries WHERE method = ? AND host = ? AND path = ?`,
|
|
||||||
method, host, path,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
for rows.Next() {
|
|
||||||
var raw string
|
|
||||||
if err := rows.Scan(&raw); err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
parts := strings.SplitN(raw, "\n\n", 2)
|
|
||||||
entryBody := ""
|
|
||||||
if len(parts) == 2 {
|
|
||||||
entryBody = parts[1]
|
|
||||||
}
|
|
||||||
if entryBody == body {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false, rows.Err()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DB) InsertEntry(e Entry) (Entry, error) {
|
// HasDuplicate returns true if an entry with the same method, host, path and
|
||||||
|
// request body hash already exists.
|
||||||
|
func (d *DB) HasDuplicate(method, host, path, body string) (bool, error) {
|
||||||
|
hash := bodyHash(body)
|
||||||
|
var exists int
|
||||||
|
err := d.conn.QueryRow(
|
||||||
|
`SELECT 1 FROM entries WHERE method = ? AND host = ? AND path = ? AND body_hash = ? LIMIT 1`,
|
||||||
|
method, host, path, hash,
|
||||||
|
).Scan(&exists)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return err == nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// InsertIfNotDuplicate atomically checks for a duplicate and inserts if none
|
||||||
|
// exists. Returns (entry, isDuplicate, error).
|
||||||
|
func (d *DB) InsertIfNotDuplicate(e Entry, body string) (Entry, bool, error) {
|
||||||
|
d.dedupMu.Lock()
|
||||||
|
defer d.dedupMu.Unlock()
|
||||||
|
dup, err := d.HasDuplicate(e.Method, e.Host, e.Path, body)
|
||||||
|
if err != nil || dup {
|
||||||
|
return e, dup, err
|
||||||
|
}
|
||||||
|
e, err = d.InsertEntry(e, body)
|
||||||
|
return e, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) InsertEntry(e Entry, body string) (Entry, error) {
|
||||||
res, err := d.conn.Exec(
|
res, err := d.conn.Exec(
|
||||||
`INSERT INTO entries (timestamp, method, host, path, status_code, request_raw, response_raw)
|
`INSERT INTO entries (timestamp, method, host, path, status_code, request_raw, response_raw, body_hash)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
e.Timestamp.UTC().Format(time.RFC3339),
|
e.Timestamp.UTC().Format(time.RFC3339),
|
||||||
e.Method, e.Host, e.Path, e.StatusCode, e.RequestRaw, e.ResponseRaw,
|
e.Method, e.Host, e.Path, e.StatusCode, e.RequestRaw, e.ResponseRaw, bodyHash(body),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return e, err
|
return e, err
|
||||||
@@ -102,16 +108,10 @@ func (d *DB) SearchEntries(term string) ([]Entry, error) {
|
|||||||
return scanEntries(rows)
|
return scanEntries(rows)
|
||||||
}
|
}
|
||||||
|
|
||||||
// QueryEntries executes a user-supplied query against the entries table.
|
// QueryEntries runs a WHERE expression supplied by the user against the entries
|
||||||
// If the query does not start with SELECT, it is treated as a WHERE expression
|
// table (e.g. "status_code = 404" or "host LIKE '%example.com%'").
|
||||||
// and wrapped automatically (e.g. "status_code = 404" becomes a full SELECT).
|
func (d *DB) QueryEntries(where string) ([]Entry, error) {
|
||||||
func (d *DB) QueryEntries(rawSQL string) ([]Entry, error) {
|
q := "SELECT id, timestamp, method, host, path, status_code, request_raw, response_raw FROM entries WHERE " + strings.TrimSpace(where)
|
||||||
q := strings.TrimSpace(rawSQL)
|
|
||||||
if !strings.HasPrefix(strings.ToUpper(q), "SELECT") {
|
|
||||||
q = "SELECT id, timestamp, method, host, path, status_code, request_raw, response_raw FROM entries WHERE " + q
|
|
||||||
} else if strings.ContainsAny(strings.ToUpper(q), "INSERTDELETEUPDATEDROP") {
|
|
||||||
return nil, fmt.Errorf("only SELECT queries are allowed")
|
|
||||||
}
|
|
||||||
rows, err := d.conn.Query(q)
|
rows, err := d.conn.Query(q)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package intercept
|
package intercept
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log"
|
||||||
"regexp"
|
"regexp"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
@@ -75,9 +76,12 @@ func (b *Broker) SetCaptureResponse(v bool) {
|
|||||||
func (b *Broker) SetAutoForwardRegex(patterns []string) {
|
func (b *Broker) SetAutoForwardRegex(patterns []string) {
|
||||||
compiled := make([]*regexp.Regexp, 0, len(patterns))
|
compiled := make([]*regexp.Regexp, 0, len(patterns))
|
||||||
for _, p := range patterns {
|
for _, p := range patterns {
|
||||||
if r, err := regexp.Compile(p); err == nil {
|
r, err := regexp.Compile(p)
|
||||||
compiled = append(compiled, r)
|
if err != nil {
|
||||||
|
log.Printf("intercept: invalid auto_forward_regex %q: %v", p, err)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
compiled = append(compiled, r)
|
||||||
}
|
}
|
||||||
b.autoFwdMu.Lock()
|
b.autoFwdMu.Lock()
|
||||||
b.autoFwdRegexes = compiled
|
b.autoFwdRegexes = compiled
|
||||||
@@ -164,19 +168,14 @@ func (b *Broker) SaveEntry(f *proxy.Flow) {
|
|||||||
if path == "" {
|
if path == "" {
|
||||||
path = "/"
|
path = "/"
|
||||||
}
|
}
|
||||||
if config.Global.History.SkipDuplicates {
|
body := string(r.Body)
|
||||||
body := string(r.Body)
|
|
||||||
if dup, _ := d.HasDuplicate(r.Method, r.URL.Host, path, body); dup {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pending := db.Entry{
|
pending := db.Entry{
|
||||||
Timestamp: time.Now(),
|
Timestamp: time.Now(),
|
||||||
Method: r.Method,
|
Method: r.Method,
|
||||||
Host: r.URL.Host,
|
Host: r.URL.Host,
|
||||||
Path: path,
|
Path: path,
|
||||||
StatusCode: status,
|
StatusCode: status,
|
||||||
RequestRaw: FormatRawRequest(f),
|
RequestRaw: FormatRawRequest(f),
|
||||||
ResponseRaw: func() string {
|
ResponseRaw: func() string {
|
||||||
if config.Global.History.KeepResponses {
|
if config.Global.History.KeepResponses {
|
||||||
return FormatRawResponse(f)
|
return FormatRawResponse(f)
|
||||||
@@ -189,7 +188,19 @@ func (b *Broker) SaveEntry(f *proxy.Flow) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
entry, err := d.InsertEntry(pending)
|
var (
|
||||||
|
entry db.Entry
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if config.Global.History.SkipDuplicates {
|
||||||
|
var dup bool
|
||||||
|
entry, dup, err = d.InsertIfNotDuplicate(pending, body)
|
||||||
|
if dup || err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
entry, err = d.InsertEntry(pending, body)
|
||||||
|
}
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if cb := b.onNewEntry; cb != nil {
|
if cb := b.onNewEntry; cb != nil {
|
||||||
go cb(entry)
|
go cb(entry)
|
||||||
|
|||||||
+16
-1
@@ -11,7 +11,22 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func newLuaState(mgr *Manager, p *Plugin) *lua.LState {
|
func newLuaState(mgr *Manager, p *Plugin) *lua.LState {
|
||||||
L := lua.NewState()
|
L := lua.NewState(lua.Options{SkipOpenLibs: true})
|
||||||
|
for _, lib := range []struct {
|
||||||
|
name string
|
||||||
|
fn lua.LGFunction
|
||||||
|
}{
|
||||||
|
{lua.LoadLibName, lua.OpenPackage},
|
||||||
|
{lua.BaseLibName, lua.OpenBase},
|
||||||
|
{lua.TabLibName, lua.OpenTable},
|
||||||
|
{lua.StringLibName, lua.OpenString},
|
||||||
|
{lua.MathLibName, lua.OpenMath},
|
||||||
|
{lua.CoroutineLibName, lua.OpenCoroutine},
|
||||||
|
} {
|
||||||
|
L.Push(L.NewFunction(lib.fn))
|
||||||
|
L.Push(lua.LString(lib.name))
|
||||||
|
L.Call(1, 0)
|
||||||
|
}
|
||||||
registerUtilities(L, mgr, p)
|
registerUtilities(L, mgr, p)
|
||||||
return L
|
return L
|
||||||
}
|
}
|
||||||
|
|||||||
+45
-75
@@ -270,20 +270,22 @@ func (m *Manager) RunOnQuit() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) RunSyncOnRequest(f *goproxy.Flow) intercept.Decision {
|
// runSyncDecisionForPlugins runs hookName synchronously for all enabled plugins
|
||||||
|
// that registered it as sync, and returns the first non-Intercept decision.
|
||||||
|
func (m *Manager) runSyncDecisionForPlugins(hookName string, argsFor func(*Plugin) []lua.LValue) intercept.Decision {
|
||||||
for _, p := range m.GetPlugins() {
|
for _, p := range m.GetPlugins() {
|
||||||
if !p.Enabled {
|
if !p.Enabled {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
hc, ok := p.hooks["on_request"]
|
hc, ok := p.hooks[hookName]
|
||||||
if !ok || !hc.Sync {
|
if !ok || !hc.Sync {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
result, err := callHook(p, "on_request", pushRequest(p.L, f))
|
result, err := callHook(p, hookName, argsFor(p)...)
|
||||||
p.mu.Unlock()
|
p.mu.Unlock()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("plugin %s on_request: %v", p.Name, err)
|
log.Printf("plugin %s %s: %v", p.Name, hookName, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
switch result {
|
switch result {
|
||||||
@@ -296,68 +298,49 @@ func (m *Manager) RunSyncOnRequest(f *goproxy.Flow) intercept.Decision {
|
|||||||
return intercept.Intercept
|
return intercept.Intercept
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// runAsyncForPlugins fires hookName asynchronously for all enabled plugins
|
||||||
|
// that registered it as async.
|
||||||
|
func (m *Manager) runAsyncForPlugins(hookName string, argsFor func(*Plugin) []lua.LValue) {
|
||||||
|
for _, p := range m.GetPlugins() {
|
||||||
|
if !p.Enabled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
hc, ok := p.hooks[hookName]
|
||||||
|
if !ok || hc.Sync {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
go func(p *Plugin) {
|
||||||
|
p.mu.Lock()
|
||||||
|
if _, err := callHook(p, hookName, argsFor(p)...); err != nil {
|
||||||
|
log.Printf("plugin %s %s: %v", p.Name, hookName, err)
|
||||||
|
}
|
||||||
|
p.mu.Unlock()
|
||||||
|
}(p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) RunSyncOnRequest(f *goproxy.Flow) intercept.Decision {
|
||||||
|
return m.runSyncDecisionForPlugins("on_request", func(p *Plugin) []lua.LValue {
|
||||||
|
return []lua.LValue{pushRequest(p.L, f)}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Manager) RunAsyncOnRequest(f *goproxy.Flow) {
|
func (m *Manager) RunAsyncOnRequest(f *goproxy.Flow) {
|
||||||
for _, p := range m.GetPlugins() {
|
m.runAsyncForPlugins("on_request", func(p *Plugin) []lua.LValue {
|
||||||
if !p.Enabled {
|
return []lua.LValue{pushRequest(p.L, f)}
|
||||||
continue
|
})
|
||||||
}
|
|
||||||
hc, ok := p.hooks["on_request"]
|
|
||||||
if !ok || hc.Sync {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
go func(p *Plugin) {
|
|
||||||
p.mu.Lock()
|
|
||||||
if _, err := callHook(p, "on_request", pushRequest(p.L, f)); err != nil {
|
|
||||||
log.Printf("plugin %s on_request: %v", p.Name, err)
|
|
||||||
}
|
|
||||||
p.mu.Unlock()
|
|
||||||
}(p)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) RunSyncOnResponse(f *goproxy.Flow) intercept.Decision {
|
func (m *Manager) RunSyncOnResponse(f *goproxy.Flow) intercept.Decision {
|
||||||
for _, p := range m.GetPlugins() {
|
return m.runSyncDecisionForPlugins("on_response", func(p *Plugin) []lua.LValue {
|
||||||
if !p.Enabled {
|
return []lua.LValue{pushRequest(p.L, f), pushResponse(p.L, f)}
|
||||||
continue
|
})
|
||||||
}
|
|
||||||
hc, ok := p.hooks["on_response"]
|
|
||||||
if !ok || !hc.Sync {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
p.mu.Lock()
|
|
||||||
result, err := callHook(p, "on_response", pushRequest(p.L, f), pushResponse(p.L, f))
|
|
||||||
p.mu.Unlock()
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("plugin %s on_response: %v", p.Name, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
switch result {
|
|
||||||
case "drop":
|
|
||||||
return intercept.Drop
|
|
||||||
case "forward":
|
|
||||||
return intercept.Forward
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return intercept.Intercept
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) RunAsyncOnResponse(f *goproxy.Flow) {
|
func (m *Manager) RunAsyncOnResponse(f *goproxy.Flow) {
|
||||||
for _, p := range m.GetPlugins() {
|
m.runAsyncForPlugins("on_response", func(p *Plugin) []lua.LValue {
|
||||||
if !p.Enabled {
|
return []lua.LValue{pushRequest(p.L, f), pushResponse(p.L, f)}
|
||||||
continue
|
})
|
||||||
}
|
|
||||||
hc, ok := p.hooks["on_response"]
|
|
||||||
if !ok || hc.Sync {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
go func(p *Plugin) {
|
|
||||||
p.mu.Lock()
|
|
||||||
if _, err := callHook(p, "on_response", pushRequest(p.L, f), pushResponse(p.L, f)); err != nil {
|
|
||||||
log.Printf("plugin %s on_response: %v", p.Name, err)
|
|
||||||
}
|
|
||||||
p.mu.Unlock()
|
|
||||||
}(p)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunSyncOnHistoryEntry is called before DB insert; returns false to skip saving.
|
// RunSyncOnHistoryEntry is called before DB insert; returns false to skip saving.
|
||||||
@@ -385,20 +368,7 @@ func (m *Manager) RunSyncOnHistoryEntry(e db.Entry) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) RunAsyncOnHistoryEntry(e db.Entry) {
|
func (m *Manager) RunAsyncOnHistoryEntry(e db.Entry) {
|
||||||
for _, p := range m.GetPlugins() {
|
m.runAsyncForPlugins("on_history_entry", func(p *Plugin) []lua.LValue {
|
||||||
if !p.Enabled {
|
return []lua.LValue{pushEntry(p.L, e)}
|
||||||
continue
|
})
|
||||||
}
|
|
||||||
hc, ok := p.hooks["on_history_entry"]
|
|
||||||
if !ok || hc.Sync {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
go func(p *Plugin) {
|
|
||||||
p.mu.Lock()
|
|
||||||
if _, err := callHook(p, "on_history_entry", pushEntry(p.L, e)); err != nil {
|
|
||||||
log.Printf("plugin %s on_history_entry: %v", p.Name, err)
|
|
||||||
}
|
|
||||||
p.mu.Unlock()
|
|
||||||
}(p)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package proxy
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
@@ -63,7 +64,11 @@ func (a *interceptAddon) Request(f *goproxy.Flow) {
|
|||||||
func (a *interceptAddon) Response(f *goproxy.Flow) {
|
func (a *interceptAddon) Response(f *goproxy.Flow) {
|
||||||
if f.Response != nil {
|
if f.Response != nil {
|
||||||
if len(f.Response.Body) == 0 && f.Response.BodyReader != nil {
|
if len(f.Response.Body) == 0 && f.Response.BodyReader != nil {
|
||||||
body, _ := io.ReadAll(f.Response.BodyReader)
|
limit := int64(config.Global.App.MaxBodySizeMB) * 1024 * 1024
|
||||||
|
body, err := io.ReadAll(io.LimitReader(f.Response.BodyReader, limit))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("proxy: reading response body: %v", err)
|
||||||
|
}
|
||||||
f.Response.Body = body
|
f.Response.Body = body
|
||||||
f.Response.BodyReader = nil
|
f.Response.BodyReader = nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -237,7 +237,7 @@ func toHAR(pr parsedRequest) string {
|
|||||||
} `json:"timings"`
|
} `json:"timings"`
|
||||||
}
|
}
|
||||||
type harLog struct {
|
type harLog struct {
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
Creator struct {
|
Creator struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
|
|||||||
+1
-1
@@ -32,7 +32,7 @@ mytarget%.com/
|
|||||||
!%.png$
|
!%.png$
|
||||||
```
|
```
|
||||||
|
|
||||||
Example (disable history — h: whitelist never matches any real URL):
|
Example (disable history: whitelist never matches any real URL):
|
||||||
```
|
```
|
||||||
h:^$
|
h:^$
|
||||||
```
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user