mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-16 07:16:38 +00:00
212 lines
4.8 KiB
Go
212 lines
4.8 KiB
Go
//go:build js
|
|
|
|
// Package capture bridges the util/capture package to JavaScript via syscall/js.
|
|
package capture
|
|
|
|
import (
|
|
"strings"
|
|
"sync"
|
|
"syscall/js"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
netbird "github.com/netbirdio/netbird/client/embed"
|
|
"github.com/netbirdio/netbird/util/capture"
|
|
)
|
|
|
|
// Handle holds a running capture session and the embedded client reference
|
|
// so it can be stopped later.
|
|
type Handle struct {
|
|
client *netbird.Client
|
|
sess *capture.Session
|
|
stopFn js.Func
|
|
stopped bool
|
|
}
|
|
|
|
// Stop ends the capture and returns stats.
|
|
func (h *Handle) Stop() capture.Stats {
|
|
if h.stopped {
|
|
return h.sess.Stats()
|
|
}
|
|
h.stopped = true
|
|
h.stopFn.Release()
|
|
|
|
if err := h.client.SetCapture(nil); err != nil {
|
|
log.Debugf("clear capture: %v", err)
|
|
}
|
|
h.sess.Stop()
|
|
return h.sess.Stats()
|
|
}
|
|
|
|
func statsToJS(s capture.Stats) js.Value {
|
|
obj := js.Global().Get("Object").Call("create", js.Null())
|
|
obj.Set("packets", js.ValueOf(s.Packets))
|
|
obj.Set("bytes", js.ValueOf(s.Bytes))
|
|
obj.Set("dropped", js.ValueOf(s.Dropped))
|
|
return obj
|
|
}
|
|
|
|
// parseOpts extracts filter/verbose/ascii from a JS options value.
|
|
func parseOpts(jsOpts js.Value) (filter string, verbose, ascii bool) {
|
|
if jsOpts.IsNull() || jsOpts.IsUndefined() {
|
|
return
|
|
}
|
|
if jsOpts.Type() == js.TypeString {
|
|
filter = jsOpts.String()
|
|
return
|
|
}
|
|
if jsOpts.Type() != js.TypeObject {
|
|
return
|
|
}
|
|
if f := jsOpts.Get("filter"); !f.IsUndefined() && !f.IsNull() {
|
|
filter = f.String()
|
|
}
|
|
if v := jsOpts.Get("verbose"); !v.IsUndefined() {
|
|
verbose = v.Truthy()
|
|
}
|
|
if a := jsOpts.Get("ascii"); !a.IsUndefined() {
|
|
ascii = a.Truthy()
|
|
}
|
|
return
|
|
}
|
|
|
|
func buildMatcher(filter string) (capture.Matcher, error) {
|
|
if filter == "" {
|
|
return nil, nil
|
|
}
|
|
return capture.ParseFilter(filter)
|
|
}
|
|
|
|
// Start creates a capture session and returns a JS interface for streaming text
|
|
// output. The returned object exposes:
|
|
//
|
|
// onpacket(callback) - set callback(string) for each text line
|
|
// stop() - stop capture and return stats { packets, bytes, dropped }
|
|
//
|
|
// Options: { filter: string, verbose: bool, ascii: bool } or just a filter string.
|
|
func Start(client *netbird.Client, jsOpts js.Value) (js.Value, error) {
|
|
filter, verbose, ascii := parseOpts(jsOpts)
|
|
|
|
matcher, err := buildMatcher(filter)
|
|
if err != nil {
|
|
return js.Undefined(), err
|
|
}
|
|
|
|
cb := &jsCallbackWriter{}
|
|
|
|
sess, err := capture.NewSession(capture.Options{
|
|
TextOutput: cb,
|
|
Matcher: matcher,
|
|
Verbose: verbose,
|
|
ASCII: ascii,
|
|
})
|
|
if err != nil {
|
|
return js.Undefined(), err
|
|
}
|
|
|
|
if err := client.SetCapture(sess); err != nil {
|
|
sess.Stop()
|
|
return js.Undefined(), err
|
|
}
|
|
|
|
handle := &Handle{client: client, sess: sess}
|
|
|
|
iface := js.Global().Get("Object").Call("create", js.Null())
|
|
handle.stopFn = js.FuncOf(func(_ js.Value, _ []js.Value) any {
|
|
return statsToJS(handle.Stop())
|
|
})
|
|
iface.Set("stop", handle.stopFn)
|
|
iface.Set("onpacket", js.Undefined())
|
|
cb.setInterface(iface)
|
|
|
|
return iface, nil
|
|
}
|
|
|
|
// StartConsole starts a capture that logs every packet line to console.log.
|
|
// Returns a Handle so the caller can stop it later.
|
|
func StartConsole(client *netbird.Client, jsOpts js.Value) (*Handle, error) {
|
|
filter, verbose, ascii := parseOpts(jsOpts)
|
|
|
|
matcher, err := buildMatcher(filter)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cb := &jsCallbackWriter{}
|
|
|
|
sess, err := capture.NewSession(capture.Options{
|
|
TextOutput: cb,
|
|
Matcher: matcher,
|
|
Verbose: verbose,
|
|
ASCII: ascii,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := client.SetCapture(sess); err != nil {
|
|
sess.Stop()
|
|
return nil, err
|
|
}
|
|
|
|
handle := &Handle{client: client, sess: sess}
|
|
handle.stopFn = js.FuncOf(func(_ js.Value, _ []js.Value) any {
|
|
return statsToJS(handle.Stop())
|
|
})
|
|
|
|
iface := js.Global().Get("Object").Call("create", js.Null())
|
|
console := js.Global().Get("console")
|
|
iface.Set("onpacket", console.Get("log").Call("bind", console, js.ValueOf("[capture]")))
|
|
cb.setInterface(iface)
|
|
|
|
return handle, nil
|
|
}
|
|
|
|
// jsCallbackWriter is an io.Writer that buffers text until a newline, then
|
|
// invokes the JS onpacket callback with each complete line.
|
|
type jsCallbackWriter struct {
|
|
mu sync.Mutex
|
|
iface js.Value
|
|
buf strings.Builder
|
|
}
|
|
|
|
func (w *jsCallbackWriter) setInterface(iface js.Value) {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
w.iface = iface
|
|
}
|
|
|
|
func (w *jsCallbackWriter) Write(p []byte) (int, error) {
|
|
w.mu.Lock()
|
|
w.buf.Write(p)
|
|
|
|
var lines []string
|
|
for {
|
|
str := w.buf.String()
|
|
idx := strings.IndexByte(str, '\n')
|
|
if idx < 0 {
|
|
break
|
|
}
|
|
lines = append(lines, str[:idx])
|
|
w.buf.Reset()
|
|
if idx+1 < len(str) {
|
|
w.buf.WriteString(str[idx+1:])
|
|
}
|
|
}
|
|
|
|
iface := w.iface
|
|
w.mu.Unlock()
|
|
|
|
if iface.IsUndefined() {
|
|
return len(p), nil
|
|
}
|
|
cb := iface.Get("onpacket")
|
|
if cb.IsUndefined() || cb.IsNull() {
|
|
return len(p), nil
|
|
}
|
|
for _, line := range lines {
|
|
cb.Invoke(js.ValueOf(line))
|
|
}
|
|
return len(p), nil
|
|
}
|