mirror of
https://github.com/anotherhadi/spilltea.git
synced 2026-05-20 17:52:33 +02:00
plugin's config is now in yaml
Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com>
This commit is contained in:
+38
-3
@@ -1,5 +1,7 @@
|
||||
# Plugins
|
||||
|
||||
> **Warning:** Plugins can execute arbitrary shell commands, read and write files via `shell_pipe`, and access all intercepted traffic. Only load plugins you trust and have reviewed. You are solely responsible for the plugins you run.
|
||||
|
||||
Spilltea supports Lua plugins that can intercept, modify, and analyze HTTP traffic.
|
||||
You can found some pre-built plugins [here](../../plugins/).
|
||||
|
||||
@@ -33,7 +35,7 @@ Plugin = {
|
||||
|
||||
| Hook | When called | Sync/async | Return value |
|
||||
| ------------------------- | ------------------------------------- | ------------ | ----------------------------------------------- |
|
||||
| `on_config(config_text)` | At startup and on config save | always sync | ignored |
|
||||
| `on_config()` | At startup and on config save | always sync | ignored |
|
||||
| `on_start()` | Once at startup, after `on_config` | configurable | `false` to self-disable the plugin, otherwise ignored |
|
||||
| `on_quit()` | When the app exits | always sync | ignored |
|
||||
| `on_request(req)` | Every request, before auto-forward | configurable | `"drop"`, `"forward"`, or `nil` (sync only) |
|
||||
@@ -120,6 +122,10 @@ if err then
|
||||
else
|
||||
log("output: " .. out)
|
||||
end
|
||||
|
||||
-- Return the plugin's config section as a Lua table (parsed from YAML).
|
||||
-- Returns an empty table if no config is set.
|
||||
local cfg = get_config()
|
||||
```
|
||||
|
||||
### Finding deduplication
|
||||
@@ -128,9 +134,38 @@ A finding is identified by `(plugin_name, key)`. If a finding with that pair alr
|
||||
|
||||
## Configuration
|
||||
|
||||
Each plugin gets a **config textarea** on the Plugins page. The raw text is passed as-is to `on_config(config_text)`. Parse it however you like (line by line, key=value, JSON, etc.).
|
||||
Plugin configuration is stored in a `plugins.yaml` file alongside the project database. Each plugin has an `enable` toggle and an optional `config` block (arbitrary YAML).
|
||||
|
||||
`on_config` is called once at startup (before `on_start`) and again every time the user saves the config in the UI.
|
||||
```yaml
|
||||
plugins:
|
||||
my_plugin:
|
||||
enable: true
|
||||
config: |
|
||||
some_key: some_value
|
||||
list:
|
||||
- item1
|
||||
- item2
|
||||
other_plugin:
|
||||
enable: false
|
||||
```
|
||||
|
||||
The config block is edited from the **Plugins** page in the TUI. Inside a plugin, call `get_config()` to retrieve the config as a Lua table.
|
||||
|
||||
`on_config()` is called once at startup (before `on_start`) and again every time the user saves the config in the TUI. It is the right place to read `get_config()` and populate local variables.
|
||||
|
||||
```lua
|
||||
local items = {}
|
||||
|
||||
function on_config()
|
||||
items = {}
|
||||
local cfg = get_config()
|
||||
if cfg and cfg.list then
|
||||
for _, v in ipairs(cfg.list) do
|
||||
table.insert(items, v)
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Sync vs async
|
||||
|
||||
|
||||
+1
-6
@@ -72,12 +72,7 @@ CREATE TABLE IF NOT EXISTS replay_entries (
|
||||
status_code INTEGER NOT NULL,
|
||||
error_msg TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS plugins (
|
||||
name TEXT PRIMARY KEY,
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
config_text TEXT NOT NULL DEFAULT ''
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS findings (
|
||||
CREATE TABLE IF NOT EXISTS findings (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
plugin_name TEXT NOT NULL,
|
||||
dedup_key TEXT NOT NULL,
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
package db
|
||||
|
||||
type PluginState struct {
|
||||
Name string
|
||||
Enabled bool
|
||||
ConfigText string
|
||||
}
|
||||
|
||||
func (d *DB) SavePluginState(name string, enabled bool, configText string) error {
|
||||
_, err := d.conn.Exec(
|
||||
`INSERT INTO plugins (name, enabled, config_text) VALUES (?, ?, ?)
|
||||
ON CONFLICT(name) DO UPDATE SET enabled = excluded.enabled, config_text = excluded.config_text`,
|
||||
name, enabled, configText,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *DB) LoadPluginStates() ([]PluginState, error) {
|
||||
rows, err := d.conn.Query(`SELECT name, enabled, config_text FROM plugins`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []PluginState
|
||||
for rows.Next() {
|
||||
var s PluginState
|
||||
var enabled int
|
||||
if err := rows.Scan(&s.Name, &enabled, &s.ConfigText); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.Enabled = enabled != 0
|
||||
out = append(out, s)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/anotherhadi/spilltea/internal/db"
|
||||
goproxy "github.com/lqqyt2423/go-mitmproxy/proxy"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func newLuaState(mgr *Manager, p *Plugin) *lua.LState {
|
||||
@@ -175,6 +176,27 @@ func registerUtilities(L *lua.LState, mgr *Manager, p *Plugin) {
|
||||
return 0
|
||||
}))
|
||||
|
||||
L.SetGlobal("get_config", L.NewFunction(func(L *lua.LState) int {
|
||||
// p.mu is already held by the hook caller - do not lock again.
|
||||
configText := p.ConfigText
|
||||
if configText == "" {
|
||||
L.Push(L.NewTable())
|
||||
return 1
|
||||
}
|
||||
var data interface{}
|
||||
if err := yaml.Unmarshal([]byte(configText), &data); err != nil || data == nil {
|
||||
L.Push(L.NewTable())
|
||||
return 1
|
||||
}
|
||||
lv := goToLuaValue(L, data)
|
||||
if _, ok := lv.(*lua.LTable); !ok {
|
||||
L.Push(L.NewTable())
|
||||
return 1
|
||||
}
|
||||
L.Push(lv)
|
||||
return 1
|
||||
}))
|
||||
|
||||
L.SetGlobal("shell_pipe", L.NewFunction(func(L *lua.LState) int {
|
||||
cmd := L.CheckString(1)
|
||||
input := L.OptString(2, "")
|
||||
@@ -201,6 +223,35 @@ func registerUtilities(L *lua.LState, mgr *Manager, p *Plugin) {
|
||||
}))
|
||||
}
|
||||
|
||||
func goToLuaValue(L *lua.LState, v interface{}) lua.LValue {
|
||||
switch val := v.(type) {
|
||||
case map[string]interface{}:
|
||||
t := L.NewTable()
|
||||
for k, v2 := range val {
|
||||
L.SetField(t, k, goToLuaValue(L, v2))
|
||||
}
|
||||
return t
|
||||
case []interface{}:
|
||||
t := L.NewTable()
|
||||
for i, v2 := range val {
|
||||
L.RawSetInt(t, i+1, goToLuaValue(L, v2))
|
||||
}
|
||||
return t
|
||||
case string:
|
||||
return lua.LString(val)
|
||||
case int:
|
||||
return lua.LNumber(val)
|
||||
case float64:
|
||||
return lua.LNumber(val)
|
||||
case bool:
|
||||
if val {
|
||||
return lua.LTrue
|
||||
}
|
||||
return lua.LFalse
|
||||
}
|
||||
return lua.LNil
|
||||
}
|
||||
|
||||
func luaTableString(t *lua.LTable, key string) string {
|
||||
v := t.RawGetString(key)
|
||||
if s, ok := v.(lua.LString); ok {
|
||||
|
||||
+30
-45
@@ -20,6 +20,7 @@ type Manager struct {
|
||||
plugins []*Plugin
|
||||
|
||||
db *db.DB
|
||||
pluginsFile *PluginsFile
|
||||
broker *intercept.Broker
|
||||
|
||||
Notifs chan PluginNotifMsg
|
||||
@@ -43,6 +44,10 @@ func (m *Manager) SetDB(d *db.DB) {
|
||||
m.db = d
|
||||
}
|
||||
|
||||
func (m *Manager) SetPluginsFile(pf *PluginsFile) {
|
||||
m.pluginsFile = pf
|
||||
}
|
||||
|
||||
func (m *Manager) LoadFromDir(dir string) error {
|
||||
entries, err := os.ReadDir(dir)
|
||||
if os.IsNotExist(err) {
|
||||
@@ -52,17 +57,6 @@ func (m *Manager) LoadFromDir(dir string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
var states map[string]db.PluginState
|
||||
if m.db != nil {
|
||||
list, err := m.db.LoadPluginStates()
|
||||
if err == nil {
|
||||
states = make(map[string]db.PluginState, len(list))
|
||||
for _, s := range list {
|
||||
states[s.Name] = s
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, e := range entries {
|
||||
if e.IsDir() || !strings.HasSuffix(e.Name(), ".lua") {
|
||||
continue
|
||||
@@ -73,9 +67,13 @@ func (m *Manager) LoadFromDir(dir string) error {
|
||||
log.Printf("plugin load error %s: %v", path, err)
|
||||
continue
|
||||
}
|
||||
if s, ok := states[p.Name]; ok {
|
||||
p.Enabled = s.Enabled
|
||||
p.ConfigText = s.ConfigText
|
||||
if m.pluginsFile != nil {
|
||||
if enabled, configText, found := m.pluginsFile.get(p.Name); found {
|
||||
p.Enabled = enabled
|
||||
p.ConfigText = configText
|
||||
} else {
|
||||
m.pluginsFile.register(p.Name, p.Enabled)
|
||||
}
|
||||
}
|
||||
m.mu.Lock()
|
||||
m.plugins = append(m.plugins, p)
|
||||
@@ -131,12 +129,6 @@ func (m *Manager) loadPlugin(path string) (*Plugin, error) {
|
||||
"on_response": false,
|
||||
"on_history_entry": false,
|
||||
}
|
||||
// Fixed-sync hooks: always sync, not configurable.
|
||||
fixedSyncHooks := map[string]struct{}{
|
||||
"on_config": {},
|
||||
"on_quit": {},
|
||||
}
|
||||
|
||||
for hookName, defaultSync := range configurableHooks {
|
||||
if tbl, ok := pluginTable.RawGetString(hookName).(*lua.LTable); ok {
|
||||
p.hooks[hookName] = HookConfig{Sync: tbl.RawGetString("sync") == lua.LTrue}
|
||||
@@ -146,9 +138,9 @@ func (m *Manager) loadPlugin(path string) (*Plugin, error) {
|
||||
p.hooks[hookName] = HookConfig{Sync: defaultSync}
|
||||
}
|
||||
}
|
||||
for hookName := range fixedSyncHooks {
|
||||
if p.L.GetGlobal(hookName) != lua.LNil {
|
||||
p.hooks[hookName] = HookConfig{Sync: true}
|
||||
for _, fixedSync := range []string{"on_config", "on_quit"} {
|
||||
if p.L.GetGlobal(fixedSync) != lua.LNil {
|
||||
p.hooks[fixedSync] = HookConfig{Sync: true}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,10 +171,9 @@ func (m *Manager) TogglePlugin(name string) {
|
||||
found.mu.Lock()
|
||||
found.Enabled = !found.Enabled
|
||||
enabled := found.Enabled
|
||||
configText := found.ConfigText
|
||||
found.mu.Unlock()
|
||||
if m.db != nil {
|
||||
if err := m.db.SavePluginState(name, enabled, configText); err != nil {
|
||||
if m.pluginsFile != nil {
|
||||
if err := m.pluginsFile.setEnabled(name, enabled); err != nil {
|
||||
log.Printf("plugin %s: save state: %v", name, err)
|
||||
}
|
||||
}
|
||||
@@ -196,8 +187,8 @@ func (m *Manager) TogglePlugin(name string) {
|
||||
disableIfFalse := func(p *Plugin, ret lua.LValue) {
|
||||
if ret == lua.LFalse {
|
||||
p.Enabled = false
|
||||
if m.db != nil {
|
||||
if err := m.db.SavePluginState(p.Name, false, p.ConfigText); err != nil {
|
||||
if m.pluginsFile != nil {
|
||||
if err := m.pluginsFile.setEnabled(p.Name, false); err != nil {
|
||||
log.Printf("plugin %s: save state: %v", p.Name, err)
|
||||
}
|
||||
}
|
||||
@@ -241,41 +232,35 @@ func (m *Manager) SaveConfig(name, configText string) {
|
||||
}
|
||||
found.mu.Lock()
|
||||
found.ConfigText = configText
|
||||
enabled := found.Enabled
|
||||
_, hasOnConfig := found.hooks["on_config"]
|
||||
found.mu.Unlock()
|
||||
if m.db != nil {
|
||||
if err := m.db.SavePluginState(name, enabled, configText); err != nil {
|
||||
log.Printf("plugin %s: save state: %v", name, err)
|
||||
if m.pluginsFile != nil {
|
||||
if err := m.pluginsFile.setConfig(name, configText); err != nil {
|
||||
log.Printf("plugin %s: save config: %v", name, err)
|
||||
}
|
||||
}
|
||||
if !hasOnConfig {
|
||||
if _, ok := found.hooks["on_config"]; !ok {
|
||||
return
|
||||
}
|
||||
// on_config is always sync.
|
||||
found.mu.Lock()
|
||||
if _, err := callHook(found, "on_config", lua.LString(configText)); err != nil {
|
||||
log.Printf("plugin %s on_config (config reload): %v", name, err)
|
||||
if _, err := callHook(found, "on_config"); err != nil {
|
||||
log.Printf("plugin %s on_config: %v", name, err)
|
||||
}
|
||||
found.mu.Unlock()
|
||||
}
|
||||
|
||||
func (m *Manager) RunOnStart() {
|
||||
// on_config runs first, always sync, for every enabled plugin that has it.
|
||||
for _, p := range m.GetPlugins() {
|
||||
if !p.Enabled {
|
||||
continue
|
||||
}
|
||||
if _, ok := p.hooks["on_config"]; !ok {
|
||||
continue
|
||||
}
|
||||
if _, ok := p.hooks["on_config"]; ok {
|
||||
p.mu.Lock()
|
||||
if _, err := callHook(p, "on_config", lua.LString(p.ConfigText)); err != nil {
|
||||
if _, err := callHook(p, "on_config"); err != nil {
|
||||
log.Printf("plugin %s on_config: %v", p.Name, err)
|
||||
}
|
||||
p.mu.Unlock()
|
||||
}
|
||||
// on_start runs after, sync or async depending on plugin config.
|
||||
}
|
||||
for _, p := range m.GetPlugins() {
|
||||
if !p.Enabled {
|
||||
continue
|
||||
@@ -287,8 +272,8 @@ func (m *Manager) RunOnStart() {
|
||||
disableIfFalse := func(p *Plugin, ret lua.LValue) {
|
||||
if ret == lua.LFalse {
|
||||
p.Enabled = false
|
||||
if m.db != nil {
|
||||
if err := m.db.SavePluginState(p.Name, false, p.ConfigText); err != nil {
|
||||
if m.pluginsFile != nil {
|
||||
if err := m.pluginsFile.setEnabled(p.Name, false); err != nil {
|
||||
log.Printf("plugin %s: save state: %v", p.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type pluginFileEntry struct {
|
||||
Enable bool `yaml:"enable"`
|
||||
Config string `yaml:"config,omitempty"`
|
||||
}
|
||||
|
||||
type pluginsFileData struct {
|
||||
Plugins map[string]pluginFileEntry `yaml:"plugins"`
|
||||
}
|
||||
|
||||
type PluginsFile struct {
|
||||
path string
|
||||
data pluginsFileData
|
||||
}
|
||||
|
||||
func OpenPluginsFile(dbPath string) (*PluginsFile, error) {
|
||||
path := filepath.Join(filepath.Dir(dbPath), "plugins.yaml")
|
||||
pf := &PluginsFile{
|
||||
path: path,
|
||||
data: pluginsFileData{Plugins: make(map[string]pluginFileEntry)},
|
||||
}
|
||||
raw, err := os.ReadFile(path)
|
||||
if os.IsNotExist(err) {
|
||||
return pf, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := yaml.Unmarshal(raw, &pf.data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if pf.data.Plugins == nil {
|
||||
pf.data.Plugins = make(map[string]pluginFileEntry)
|
||||
}
|
||||
return pf, nil
|
||||
}
|
||||
|
||||
func (pf *PluginsFile) save() error {
|
||||
raw, err := yaml.Marshal(&pf.data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(pf.path, raw, 0o600)
|
||||
}
|
||||
|
||||
func (pf *PluginsFile) get(name string) (enabled bool, config string, found bool) {
|
||||
e, ok := pf.data.Plugins[name]
|
||||
return e.Enable, e.Config, ok
|
||||
}
|
||||
|
||||
func (pf *PluginsFile) register(name string, defaultEnabled bool) {
|
||||
if _, ok := pf.data.Plugins[name]; !ok {
|
||||
pf.data.Plugins[name] = pluginFileEntry{Enable: defaultEnabled}
|
||||
_ = pf.save()
|
||||
}
|
||||
}
|
||||
|
||||
func (pf *PluginsFile) setEnabled(name string, enabled bool) error {
|
||||
e := pf.data.Plugins[name]
|
||||
e.Enable = enabled
|
||||
pf.data.Plugins[name] = e
|
||||
return pf.save()
|
||||
}
|
||||
|
||||
func (pf *PluginsFile) setConfig(name string, configText string) error {
|
||||
e := pf.data.Plugins[name]
|
||||
e.Config = configText
|
||||
pf.data.Plugins[name] = e
|
||||
return pf.save()
|
||||
}
|
||||
@@ -106,6 +106,12 @@ func New(broker *intercept.Broker, name, path string) Model {
|
||||
m.findingsPage.SetDB(d)
|
||||
mgr.SetDB(d)
|
||||
|
||||
pf, err := plugins.OpenPluginsFile(path)
|
||||
if err != nil {
|
||||
log.Printf("plugins file: %v", err)
|
||||
}
|
||||
mgr.SetPluginsFile(pf)
|
||||
|
||||
pluginsDir := config.ExpandPath(cfg.App.PluginsDir)
|
||||
if err := mgr.LoadFromDir(pluginsDir); err != nil {
|
||||
log.Printf("plugins: %v", err)
|
||||
|
||||
@@ -3,22 +3,28 @@ Plugin = {
|
||||
description = [[
|
||||
Inject custom headers into every intercepted request.
|
||||
|
||||
**Config**:
|
||||
- one 'Header-Name: value' per line.
|
||||
**Config** (YAML):
|
||||
```yaml
|
||||
headers:
|
||||
- "X-My-Header: myvalue"
|
||||
```
|
||||
]],
|
||||
on_request = { sync = true },
|
||||
}
|
||||
|
||||
local headers = {}
|
||||
|
||||
function on_config(config_text)
|
||||
function on_config()
|
||||
headers = {}
|
||||
for line in config_text:gmatch("[^\n]+") do
|
||||
local cfg = get_config()
|
||||
if cfg and cfg.headers then
|
||||
for _, line in ipairs(cfg.headers) do
|
||||
local name, value = line:match("^([^:]+):%s*(.+)$")
|
||||
if name and value then
|
||||
table.insert(headers, { name = name, value = value })
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function on_request(req)
|
||||
|
||||
+16
-15
@@ -3,11 +3,13 @@ Plugin = {
|
||||
description = [[
|
||||
Checks that the proxy's outbound IP is in an allowed list on startup.
|
||||
|
||||
**Config**:
|
||||
- one IP per line
|
||||
- prefix with `!` for a blacklist entry (blocked)
|
||||
- prefix with `#` to comment it out (ignored)
|
||||
- if no IPs are configured, the check is skipped
|
||||
**Config** (YAML):
|
||||
```yaml
|
||||
ips:
|
||||
- "1.2.3.4" # whitelist entry
|
||||
- "!5.6.7.8" # blacklist entry (blocked)
|
||||
```
|
||||
- If no IPs are configured, the check is skipped.
|
||||
]],
|
||||
on_start = { sync = false },
|
||||
disable_by_default = true,
|
||||
@@ -16,23 +18,22 @@ Checks that the proxy's outbound IP is in an allowed list on startup.
|
||||
local whitelist = {}
|
||||
local blacklist = {}
|
||||
|
||||
function on_config(config_text)
|
||||
whitelist = {}
|
||||
blacklist = {}
|
||||
|
||||
for line in config_text:gmatch("[^\n]+") do
|
||||
local trimmed = line:match("^%s*(.-)%s*$")
|
||||
if trimmed ~= "" and trimmed:sub(1, 1) ~= "#" then
|
||||
function on_config()
|
||||
whitelist, blacklist = {}, {}
|
||||
local cfg = get_config()
|
||||
if cfg and cfg.ips then
|
||||
for _, entry in ipairs(cfg.ips) do
|
||||
local trimmed = entry:match("^%s*(.-)%s*$")
|
||||
if trimmed ~= "" then
|
||||
if trimmed:sub(1, 1) == "!" then
|
||||
local ip = trimmed:sub(2):match("^%s*(.-)%s*$")
|
||||
if ip ~= "" then
|
||||
table.insert(blacklist, ip)
|
||||
end
|
||||
if ip ~= "" then table.insert(blacklist, ip) end
|
||||
else
|
||||
table.insert(whitelist, trimmed)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function on_start()
|
||||
|
||||
+27
-28
@@ -3,38 +3,35 @@ Plugin = {
|
||||
description = [[
|
||||
Auto-forward requests and exclude them from history based on patterns.
|
||||
|
||||
**Config**:
|
||||
- `pattern` - whitelist: only intercept matching requests/responses and history entries
|
||||
- `!pattern` - blacklist: skip matching requests/responses and history entries
|
||||
- `r:pattern` - whitelist for requests/responses only (history unaffected)
|
||||
- `r:!pattern` - blacklist for requests/responses only
|
||||
- `h:pattern` - whitelist for history entries only (requests unaffected)
|
||||
- `h:!pattern` - blacklist for history entries only
|
||||
- lines starting with `#` are comments
|
||||
**Config** (YAML):
|
||||
```yaml
|
||||
patterns:
|
||||
- "pattern" # whitelist: only intercept matching requests/responses and history
|
||||
- "!pattern" # blacklist: skip matching requests/responses and history
|
||||
- "r:pattern" # whitelist for requests/responses only
|
||||
- "r:!pattern" # blacklist for requests/responses only
|
||||
- "h:pattern" # whitelist for history only
|
||||
- "h:!pattern" # blacklist for history only
|
||||
```
|
||||
|
||||
Example (ignore static assets):
|
||||
```
|
||||
!%.css$
|
||||
!%.js$
|
||||
!%.png$
|
||||
```yaml
|
||||
patterns:
|
||||
- "!%.css$"
|
||||
- "!%.js$"
|
||||
- "!%.png$"
|
||||
```
|
||||
|
||||
Example (focus on mytarget.com, skip everything else):
|
||||
```
|
||||
mytarget%.com/
|
||||
Example (focus on mytarget.com):
|
||||
```yaml
|
||||
patterns:
|
||||
- "mytarget%.com/"
|
||||
```
|
||||
|
||||
Example (intercept mytarget.com except its static assets):
|
||||
```
|
||||
mytarget%.com/
|
||||
!%.css$
|
||||
!%.js$
|
||||
!%.png$
|
||||
```
|
||||
|
||||
Example (disable history: whitelist never matches any real URL):
|
||||
```
|
||||
h:^$
|
||||
Example (disable history):
|
||||
```yaml
|
||||
patterns:
|
||||
- "h:^$"
|
||||
```
|
||||
]],
|
||||
priority = 100,
|
||||
@@ -50,11 +47,13 @@ local whitelist_req = {}
|
||||
local blacklist_hist = {}
|
||||
local whitelist_hist = {}
|
||||
|
||||
function on_config(config_text)
|
||||
function on_config()
|
||||
blacklist, whitelist = {}, {}
|
||||
blacklist_req, whitelist_req = {}, {}
|
||||
blacklist_hist, whitelist_hist = {}, {}
|
||||
for line in config_text:gmatch("[^\n]+") do
|
||||
local cfg = get_config()
|
||||
if not cfg or not cfg.patterns then return end
|
||||
for _, line in ipairs(cfg.patterns) do
|
||||
local trimmed = line:match("^%s*(.-)%s*$")
|
||||
if trimmed ~= "" and trimmed:sub(1, 1) ~= "#" then
|
||||
local scope = trimmed:match("^([rh]):")
|
||||
|
||||
Reference in New Issue
Block a user