new: added events.on (and other related commands) to trigger specific actions when an events happens

This commit is contained in:
evilsocket 2019-02-24 20:21:18 +01:00
commit 1492bf5e40
No known key found for this signature in database
GPG key ID: 1564D7F30393A456
24 changed files with 4475 additions and 0 deletions

View file

@ -29,6 +29,7 @@ type EventsStream struct {
output *os.File
rotation rotation
ignoreList *IgnoreList
triggerList *TriggerList
waitFor string
waitChan chan *session.Event
eventListener <-chan session.Event
@ -45,6 +46,7 @@ func NewEventsStream(s *session.Session) *EventsStream {
waitChan: make(chan *session.Event),
waitFor: "",
ignoreList: NewIgnoreList(),
triggerList: NewTriggerList(),
}
mod.AddHandler(session.NewModuleHandler("events.stream on", "",
@ -70,6 +72,38 @@ func NewEventsStream(s *session.Session) *EventsStream {
return mod.Show(limit)
}))
on := session.NewModuleHandler("events.on TAG COMMANDS", `events\.on ([^\s]+) (.+)`,
"Run COMMANDS when an event with the specified TAG is triggered.",
func(args []string) error {
return mod.addTrigger(args[0], args[1])
})
on.Complete("events.on", s.EventsCompleter)
mod.AddHandler(on)
mod.AddHandler(session.NewModuleHandler("events.triggers", "",
"Show the list of event triggers created by the events.on command.",
func(args []string) error {
return mod.showTriggers()
}))
onClear := session.NewModuleHandler("events.trigger.delete TRIGGER_ID", `events\.trigger\.delete ([^\s]+)`,
"Remove an event trigger given its TRIGGER_ID (use events.triggers to see the list of triggers).",
func(args []string) error {
return mod.clearTrigger(args[0])
})
onClear.Complete("events.trigger.delete", mod.triggerList.Completer)
mod.AddHandler(onClear)
mod.AddHandler(session.NewModuleHandler("events.triggers.clear", "",
"Remove all event triggers (use events.triggers to see the list of triggers).",
func(args []string) error {
return mod.clearTrigger("")
}))
mod.AddHandler(session.NewModuleHandler("events.waitfor TAG TIMEOUT?", `events.waitfor ([^\s]+)([\s\d]*)`,
"Wait for an event with the given tag either forever or for a timeout in seconds.",
func(args []string) error {
@ -242,6 +276,10 @@ func (mod *EventsStream) Start() error {
mod.View(e, true)
}
// this could generate sys.log events and lock the whole
// events.stream, make it async
go mod.dispatchTriggers(e)
case <-mod.quit:
return
}

View file

@ -0,0 +1,62 @@
package events_stream
import (
"os"
"github.com/bettercap/bettercap/session"
"github.com/evilsocket/islazy/tui"
)
func (mod *EventsStream) addTrigger(tag string, command string) error {
if err, id := mod.triggerList.Add(tag, command); err != nil {
return err
} else {
mod.Info("trigger for event %s added with identifier '%s'", tui.Green(tag), tui.Bold(id))
}
return nil
}
func (mod *EventsStream) clearTrigger(id string) error {
if err := mod.triggerList.Del(id); err != nil {
return err
}
return nil
}
func (mod *EventsStream) showTriggers() error {
colNames := []string{
"ID",
"Event",
"Action",
}
rows := [][]string{}
mod.triggerList.Each(func(id string, t Trigger) {
rows = append(rows, []string{
tui.Bold(id),
tui.Green(t.For),
t.Action,
})
})
if len(rows) > 0 {
tui.Table(os.Stdout, colNames, rows)
mod.Session.Refresh()
}
return nil
}
func (mod *EventsStream) dispatchTriggers(e session.Event) {
if id, cmds, err, found := mod.triggerList.Dispatch(e); err != nil {
mod.Error("error while dispatching event %s: %v", e.Tag, err)
} else if found {
mod.Debug("running trigger %s (cmds:'%s') for event %v", id, cmds, e)
for _, cmd := range session.ParseCommands(cmds) {
if err := mod.Session.Run(cmd); err != nil {
mod.Error("%s", err.Error())
}
}
}
}

View file

@ -0,0 +1,136 @@
package events_stream
import (
"encoding/json"
"fmt"
"regexp"
"strings"
"sync"
"github.com/bettercap/bettercap/session"
"github.com/antchfx/jsonquery"
"github.com/evilsocket/islazy/str"
"github.com/evilsocket/islazy/tui"
)
var reQueryCapture = regexp.MustCompile(`{{([^}]+)}}`)
type Trigger struct {
For string
Action string
}
type TriggerList struct {
sync.Mutex
triggers map[string]Trigger
}
func NewTriggerList() *TriggerList {
return &TriggerList{
triggers: make(map[string]Trigger),
}
}
func (l *TriggerList) Add(tag string, command string) (error, string) {
l.Lock()
defer l.Unlock()
idNum := 0
command = str.Trim(command)
for id, t := range l.triggers {
if t.For == tag {
if t.Action == command {
return fmt.Errorf("duplicate: trigger '%s' found for action '%s'", tui.Bold(id), command), ""
}
idNum++
}
}
id := fmt.Sprintf("%s-%d", tag, idNum)
l.triggers[id] = Trigger{
For: tag,
Action: command,
}
return nil, id
}
func (l *TriggerList) Del(id string) (err error) {
l.Lock()
defer l.Unlock()
if _, found := l.triggers[id]; found {
delete(l.triggers, id)
} else {
err = fmt.Errorf("trigger '%s' not found", tui.Bold(id))
}
return err
}
func (l *TriggerList) Each(cb func(id string, t Trigger)) {
l.Lock()
defer l.Unlock()
for id, t := range l.triggers {
cb(id, t)
}
}
func (l *TriggerList) Completer(prefix string) []string {
ids := []string{}
l.Each(func(id string, t Trigger) {
if prefix == "" || strings.HasPrefix(id, prefix) {
ids = append(ids, id)
}
})
return ids
}
func (l *TriggerList) Dispatch(e session.Event) (ident string, cmd string, err error, found bool) {
l.Lock()
defer l.Unlock()
for id, t := range l.triggers {
if e.Tag == t.For {
found = true
ident = id
// this is ugly but it's also the only way to allow
// the user to do this easily - since each event Data
// field is an interface and type casting is not possible
// via golang default text/template system, we transform
// the field to JSON, parse it again and then allow the
// user to access it in the command via JSON-Query, example:
//
// events.on wifi.client.new "wifi.deauth {{Client\mac}}"
buf := ([]byte)(nil)
doc := (*jsonquery.Node)(nil)
cmd = t.Action
// parse each {EXPR}
for _, m := range reQueryCapture.FindAllString(t.Action, -1) {
// parse the event Data field as a JSON objects once
if doc == nil {
if buf, err = json.Marshal(e.Data); err != nil {
err = fmt.Errorf("error while encoding event for trigger %s: %v", tui.Bold(id), err)
return
} else if doc, err = jsonquery.Parse(strings.NewReader(string(buf))); err != nil {
err = fmt.Errorf("error while parsing event for trigger %s: %v", tui.Bold(id), err)
return
}
}
// {EXPR} -> EXPR
expr := strings.Trim(m, "{}")
// use EXPR as a JSON query
if node := jsonquery.FindOne(doc, expr); node != nil {
cmd = strings.Replace(cmd, m, node.InnerText(), -1)
} else {
err = fmt.Errorf("error while parsing expressionfor trigger %s: '%s' doesn't resolve any object", tui.Bold(id), expr, expr, err)
return
}
}
return
}
}
return
}