From b6b2c35461d9910710c8ef03f95f21a879240d2b Mon Sep 17 00:00:00 2001 From: Graham Clark Date: Wed, 17 Apr 2019 22:38:02 -0400 Subject: [PATCH] Ready for pushing to github! --- .gitignore | 1 + .goreleaser.yml | 50 + README.md | 68 + cmd/termshark/termshark.go | 3129 ++++++++++++++++++++++ confwatcher.go | 78 + copycommand.go | 9 + copycommand_android.go | 7 + copycommand_darwin.go | 7 + copycommand_windows.go | 7 + docs/FAQ.md | 122 + docs/UserGuide.md | 185 ++ fdinfo.go | 67 + fields.go | 177 ++ fields_test.go | 35 + go.mod | 19 + go.sum | 108 + have_fdinfo.go | 15 + have_fdinfo_linux.go | 13 + modeswap/modeswap.go | 42 + noroot.go | 42 + pcap/cmds.go | 194 ++ pcap/loader.go | 1632 +++++++++++ pcap/loader_test.go | 644 +++++ pcap/loader_tshark_test.go | 566 ++++ pcap/testdata/1.pcap | Bin 0 -> 2360 bytes pcap/testdata/1.pdml | 2113 +++++++++++++++ pcap/testdata/1.psml | 193 ++ pcap/testdata/2.pcap-body | Bin 0 -> 93 bytes pcap/testdata/2.pcap-footer | 0 pcap/testdata/2.pcap-header | Bin 0 -> 24 bytes pcap/testdata/2.pdml-body | 103 + pcap/testdata/2.pdml-footer | 3 + pcap/testdata/2.pdml-header | 4 + pcap/testdata/2.psml-body | 9 + pcap/testdata/2.psml-footer | 2 + pcap/testdata/2.psml-header | 12 + pdmltree/pdmltree.go | 237 ++ pdmltree/pdmltree_test.go | 210 ++ psmltable/model.go | 152 ++ utils.go | 448 ++++ utils_test.go | 95 + version.go | 14 + widgets/appkeys/appkeys.go | 187 ++ widgets/copymodetree/copymodetree.go | 155 ++ widgets/enableselected/enableselected.go | 54 + widgets/expander/expander.go | 68 + widgets/filter/filter.go | 507 ++++ widgets/hexdumper/hexdumper.go | 619 +++++ widgets/hexdumper/hexdumper_test.go | 40 + widgets/ifwidget/ifwidget.go | 97 + widgets/renderfocused/renderfocused.go | 56 + widgets/resizable/resizable.go | 246 ++ widgets/resizable/resizable_test.go | 37 + widgets/withscrollbar/withscrollbar.go | 149 ++ 54 files changed, 13027 insertions(+) create mode 100644 .gitignore create mode 100644 .goreleaser.yml create mode 100644 README.md create mode 100644 cmd/termshark/termshark.go create mode 100644 confwatcher.go create mode 100644 copycommand.go create mode 100644 copycommand_android.go create mode 100644 copycommand_darwin.go create mode 100644 copycommand_windows.go create mode 100644 docs/FAQ.md create mode 100644 docs/UserGuide.md create mode 100644 fdinfo.go create mode 100644 fields.go create mode 100644 fields_test.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 have_fdinfo.go create mode 100644 have_fdinfo_linux.go create mode 100644 modeswap/modeswap.go create mode 100644 noroot.go create mode 100644 pcap/cmds.go create mode 100644 pcap/loader.go create mode 100644 pcap/loader_test.go create mode 100644 pcap/loader_tshark_test.go create mode 100644 pcap/testdata/1.pcap create mode 100644 pcap/testdata/1.pdml create mode 100644 pcap/testdata/1.psml create mode 100644 pcap/testdata/2.pcap-body create mode 100644 pcap/testdata/2.pcap-footer create mode 100644 pcap/testdata/2.pcap-header create mode 100644 pcap/testdata/2.pdml-body create mode 100644 pcap/testdata/2.pdml-footer create mode 100644 pcap/testdata/2.pdml-header create mode 100644 pcap/testdata/2.psml-body create mode 100644 pcap/testdata/2.psml-footer create mode 100644 pcap/testdata/2.psml-header create mode 100644 pdmltree/pdmltree.go create mode 100644 pdmltree/pdmltree_test.go create mode 100644 psmltable/model.go create mode 100644 utils.go create mode 100644 utils_test.go create mode 100644 version.go create mode 100644 widgets/appkeys/appkeys.go create mode 100644 widgets/copymodetree/copymodetree.go create mode 100644 widgets/enableselected/enableselected.go create mode 100644 widgets/expander/expander.go create mode 100644 widgets/filter/filter.go create mode 100644 widgets/hexdumper/hexdumper.go create mode 100644 widgets/hexdumper/hexdumper_test.go create mode 100644 widgets/ifwidget/ifwidget.go create mode 100644 widgets/renderfocused/renderfocused.go create mode 100644 widgets/resizable/resizable.go create mode 100644 widgets/resizable/resizable_test.go create mode 100644 widgets/withscrollbar/withscrollbar.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..849ddff --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +dist/ diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..b9f02cd --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,50 @@ +# This is an example goreleaser.yaml file with some sane defaults. +# Make sure to check the documentation at http://goreleaser.com +before: + hooks: +builds: +- env: + - CGO_ENABLED=0 + - GO111MODULE=on + main: ./cmd/termshark/termshark.go + goos: + - freebsd + - windows + - linux + - darwin + goarch: + - arm + - amd64 + ignore: + - goos: darwin + goarch: arm + - goos: freebsd + goarch: arm + - goos: windows + goarch: arm + ldflags: + - -X github.com/gcla/termshark.Version={{.Version}} +archives: +- replacements: + darwin: macOS + linux: linux + windows: windows + amd64: x64 + wrap_in_directory: true + format_overrides: + - goos: windows + format: zip + files: + - none* +sign: + artifacts: checksum +checksum: + name_template: 'checksums.txt' +snapshot: + name_template: "{{ .Tag }}-next" +changelog: + sort: asc + filters: + exclude: + - '^docs:' + - '^test:' diff --git a/README.md b/README.md new file mode 100644 index 0000000..142c938 --- /dev/null +++ b/README.md @@ -0,0 +1,68 @@ +# Termshark +A terminal user-interface for tshark, inspired by Wireshark. + +![demo1](https://drive.google.com/uc?export=view&id=1vDecxjqwJrtMGJjOObL-LLvi-1pBVByt) + +If you're debugging on a remote machine with a large pcap and no desire to scp it back to your desktop, termshark can help! + +## Features + +- Read pcap files or sniff live interfaces (where tshark is permitted). +- Inspect each packet using familiar Wireshark-inspired views +- Filter pcaps or live captures using Wireshark's display filters +- Copy ranges of packets to the clipboard from the terminal +- Written in Golang, compiles to a single executable on each platform - downloads available for Linux (+termux), macOS, FreeBSD, and Windows + +## Building + +Termshark uses Go modules, so it's best to compile with Go 1.11 or higher. Set `GO111MODULE=on` then run: + +```bash +go get github.com/gcla/termshark/cmd/termshark +``` +Then add ```~/go/bin/``` to your ```PATH```. + +For all packet analysis, termshark depends on tshark from the Wireshark project. Make sure ```tshark``` is in your ```PATH```. + +## Quick Start + +Inspect a local pcap: + +```bash +termshark -r test.pcap +``` + +Capture ping packets on interface ```eth0```: + +```bash +termshark -i eth0 icmp +``` + +Run ```termshark -h``` for options. + +## Downloads + +Pre-compiled executables are available via [Github releases](https://github.com/gcla/termshark/releases) + +## User Guide + +See the [termshark user guide](docs/UserGuide.md) (and my best guess at some [FAQs](docs/FAQ.md)) + +## Dependencies + +Termshark depends on these open-source packages: + +- [tshark](https://www.wireshark.org/docs/man-pages/tshark.html) - command-line network protocol analyzer, part of [Wireshark](https://wireshark.org) +- [tcell](https://github.com/gdamore/tcell) - a cell based terminal handling package, inspired by termbox +- [gowid](https://github.com/gcla/gowid) - compositional terminal UI widgets, inspired by [urwid](http://urwid.org), built on [tcell](https://github.com/gdamore/tcell) + +Note that tshark is a run-time dependency, and must be in your ```PATH``` for termshark to function. Version 1.10.2 or higher is required (approx 2013). + +## Contact + +- The author - Graham Clark (grclark@gmail.com) + +## License + +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + diff --git a/cmd/termshark/termshark.go b/cmd/termshark/termshark.go new file mode 100644 index 0000000..16ed5d2 --- /dev/null +++ b/cmd/termshark/termshark.go @@ -0,0 +1,3129 @@ +// Copyright 2019 Graham Clark. All rights reserved. Use of this source +// code is governed by the MIT license that can be found in the LICENSE +// file. + +package main + +import ( + "bytes" + "encoding/json" + "encoding/xml" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "runtime" + "strconv" + "strings" + "sync" + "syscall" + "text/template" + "time" + + "github.com/blang/semver" + "github.com/gcla/deep" + "github.com/gcla/gowid" + "github.com/gcla/gowid/widgets/button" + "github.com/gcla/gowid/widgets/cellmod" + "github.com/gcla/gowid/widgets/columns" + "github.com/gcla/gowid/widgets/dialog" + "github.com/gcla/gowid/widgets/disable" + "github.com/gcla/gowid/widgets/divider" + "github.com/gcla/gowid/widgets/fill" + "github.com/gcla/gowid/widgets/framed" + "github.com/gcla/gowid/widgets/holder" + "github.com/gcla/gowid/widgets/hpadding" + "github.com/gcla/gowid/widgets/isselected" + "github.com/gcla/gowid/widgets/keypress" + "github.com/gcla/gowid/widgets/list" + "github.com/gcla/gowid/widgets/menu" + "github.com/gcla/gowid/widgets/null" + "github.com/gcla/gowid/widgets/pile" + "github.com/gcla/gowid/widgets/progress" + "github.com/gcla/gowid/widgets/selectable" + "github.com/gcla/gowid/widgets/spinner" + "github.com/gcla/gowid/widgets/styled" + "github.com/gcla/gowid/widgets/table" + "github.com/gcla/gowid/widgets/text" + "github.com/gcla/gowid/widgets/tree" + "github.com/gcla/gowid/widgets/vpadding" + "github.com/gcla/termshark" + "github.com/gcla/termshark/modeswap" + "github.com/gcla/termshark/pcap" + "github.com/gcla/termshark/pdmltree" + "github.com/gcla/termshark/psmltable" + "github.com/gcla/termshark/widgets/appkeys" + "github.com/gcla/termshark/widgets/copymodetree" + "github.com/gcla/termshark/widgets/enableselected" + "github.com/gcla/termshark/widgets/expander" + "github.com/gcla/termshark/widgets/filter" + "github.com/gcla/termshark/widgets/hexdumper" + "github.com/gcla/termshark/widgets/ifwidget" + "github.com/gcla/termshark/widgets/resizable" + "github.com/gcla/termshark/widgets/withscrollbar" + "github.com/gdamore/tcell" + lru "github.com/hashicorp/golang-lru" + flags "github.com/jessevdk/go-flags" + isatty "github.com/mattn/go-isatty" + "github.com/pkg/errors" + "github.com/shibukawa/configdir" + log "github.com/sirupsen/logrus" + "github.com/spf13/viper" +) + +// TODO - just for debugging +var ensureGoroutinesStopWG sync.WaitGroup + +// Global so that we can change the displayed packet in the struct view, etc +// test +var topview *holder.Widget +var yesno *dialog.Widget +var pleaseWait *dialog.Widget +var pleaseWaitSpinner *spinner.Widget +var mainviewRs *resizable.PileWidget +var mainview gowid.IWidget +var altviewRs *resizable.PileWidget +var altview gowid.IWidget +var altviewpile *resizable.PileWidget +var altviewcols *resizable.ColumnsWidget +var viewOnlyPacketList *pile.Widget +var viewOnlyPacketStructure *pile.Widget +var viewOnlyPacketHex *pile.Widget +var filterCols *columns.Widget +var progWidgetIdx int +var mainViewPaths [][]interface{} +var altViewPaths [][]interface{} +var maxViewPath []interface{} +var filterPathMain []interface{} +var filterPathAlt []interface{} +var filterPathMax []interface{} +var view1idx int +var view2idx int +var menu1 *menu.Widget +var savedMenu *menu.Widget +var filterWidget *filter.Widget +var btnSite *menu.SiteWidget +var packetListViewHolder *holder.Widget +var packetListTable *table.BoundedWidget +var packetStructureViewHolder *holder.Widget +var packetHexViewHolder *holder.Widget +var progressHolder *holder.Widget +var loadProgress *progress.Widget +var loadSpinner *spinner.Widget + +var nullw *null.Widget +var loadingw gowid.IWidget +var structmsgHolder *holder.Widget +var missingMsgw gowid.IWidget +var fillSpace *fill.Widget +var fillVBar *fill.Widget +var colSpace *gowid.ContainerWidget + +var packetStructWidgets *lru.Cache +var packetHexWidgets *lru.Cache +var packetListView *rowFocusTableWidget + +var cacheRequests []pcap.LoadPcapSlice +var cacheRequestsChan chan struct{} // false means started, true means finished +var quitRequestedChan chan struct{} +var loader *pcap.Loader +var scheduler *pcap.Scheduler +var captureFilter string // global for now, might make it possible to change in app at some point +var tmplData map[string]interface{} + +var fixed gowid.RenderFixed +var flow gowid.RenderFlow +var hmiddle gowid.HAlignMiddle +var vmiddle gowid.VAlignMiddle + +var ( + lightGray gowid.GrayColor = gowid.MakeGrayColor("g74") + mediumGray gowid.GrayColor = gowid.MakeGrayColor("g50") + darkGray gowid.GrayColor = gowid.MakeGrayColor("g35") + brightBlue gowid.RGBColor = gowid.MakeRGBColor("#08f") + brightGreen gowid.RGBColor = gowid.MakeRGBColor("#6f2") + + // 256 color < 256 color + pktListRowSelectedBg *modeswap.Color = modeswap.New(mediumGray, gowid.ColorBlack) + pktListRowFocusBg *modeswap.Color = modeswap.New(brightBlue, gowid.ColorBlue) + pktListCellSelectedBg *modeswap.Color = modeswap.New(darkGray, gowid.ColorBlack) + pktStructSelectedBg *modeswap.Color = modeswap.New(mediumGray, gowid.ColorBlack) + pktStructFocusBg *modeswap.Color = modeswap.New(brightBlue, gowid.ColorBlue) + hexTopUnselectedFg *modeswap.Color = modeswap.New(gowid.ColorWhite, gowid.ColorWhite) + hexTopUnselectedBg *modeswap.Color = modeswap.New(mediumGray, gowid.ColorBlack) + hexTopSelectedFg *modeswap.Color = modeswap.New(gowid.ColorWhite, gowid.ColorWhite) + hexTopSelectedBg *modeswap.Color = modeswap.New(brightBlue, gowid.ColorBlue) + hexBottomUnselectedFg *modeswap.Color = modeswap.New(gowid.ColorBlack, gowid.ColorWhite) + hexBottomUnselectedBg *modeswap.Color = modeswap.New(lightGray, gowid.ColorBlack) + hexBottomSelectedFg *modeswap.Color = modeswap.New(gowid.ColorBlack, gowid.ColorWhite) + hexBottomSelectedBg *modeswap.Color = modeswap.New(lightGray, gowid.ColorBlack) + hexCurUnselectedFg *modeswap.Color = modeswap.New(gowid.ColorWhite, gowid.ColorBlack) + hexCurUnselectedBg *modeswap.Color = modeswap.New(gowid.ColorBlack, gowid.ColorWhite) + hexLineFg *modeswap.Color = modeswap.New(gowid.ColorBlack, gowid.ColorWhite) + hexLineBg *modeswap.Color = modeswap.New(lightGray, gowid.ColorBlack) + filterValidBg *modeswap.Color = modeswap.New(brightGreen, gowid.ColorGreen) + + palette gowid.Palette = gowid.Palette{ + "default": gowid.MakePaletteEntry(gowid.ColorBlack, gowid.ColorWhite), + "title": gowid.MakeForeground(gowid.ColorDarkRed), + "pkt-struct-focus": gowid.MakePaletteEntry(gowid.ColorWhite, pktStructFocusBg), + "pkt-struct-selected": gowid.MakePaletteEntry(gowid.ColorWhite, pktStructSelectedBg), + "pkt-list-row-focus": gowid.MakePaletteEntry(gowid.ColorWhite, pktListRowFocusBg), + "pkt-list-cell-focus": gowid.MakePaletteEntry(gowid.ColorWhite, gowid.ColorPurple), + "pkt-list-row-selected": gowid.MakePaletteEntry(gowid.ColorWhite, pktListRowSelectedBg), + "pkt-list-cell-selected": gowid.MakePaletteEntry(gowid.ColorWhite, pktListCellSelectedBg), + "filter-menu-focus": gowid.MakeStyledPaletteEntry(gowid.ColorBlack, gowid.ColorWhite, gowid.StyleBold), + "filter-valid": gowid.MakePaletteEntry(gowid.ColorBlack, filterValidBg), + "filter-invalid": gowid.MakePaletteEntry(gowid.ColorBlack, gowid.ColorRed), + "filter-intermediate": gowid.MakePaletteEntry(gowid.ColorBlack, gowid.ColorOrange), + "dialog": gowid.MakePaletteEntry(gowid.ColorBlack, gowid.ColorYellow), + "dialog-buttons": gowid.MakePaletteEntry(gowid.ColorYellow, gowid.ColorBlack), + "stop-load-button": gowid.MakeForeground(gowid.ColorMagenta), + "stop-load-button-focus": gowid.MakePaletteEntry(gowid.ColorWhite, gowid.ColorDarkBlue), + "menu-button": gowid.MakeForeground(gowid.ColorMagenta), + "menu-button-focus": gowid.MakePaletteEntry(gowid.ColorWhite, gowid.ColorDarkBlue), + "saved-button": gowid.MakeForeground(gowid.ColorMagenta), + "saved-button-focus": gowid.MakePaletteEntry(gowid.ColorWhite, gowid.ColorDarkBlue), + "apply-button": gowid.MakeForeground(gowid.ColorMagenta), + "apply-button-focus": gowid.MakePaletteEntry(gowid.ColorWhite, gowid.ColorDarkBlue), + "progress-default": gowid.MakeStyledPaletteEntry(gowid.ColorWhite, gowid.ColorBlack, gowid.StyleBold), + "progress-complete": gowid.MakeStyleMod(gowid.MakePaletteRef("progress-default"), gowid.MakeBackground(gowid.ColorMagenta)), + "progress-spinner": gowid.MakePaletteEntry(gowid.ColorYellow, gowid.ColorBlack), + "hex-cur-selected": gowid.MakePaletteEntry(gowid.ColorWhite, gowid.ColorMagenta), + "hex-cur-unselected": gowid.MakePaletteEntry(hexCurUnselectedFg, hexCurUnselectedBg), + "hex-top-selected": gowid.MakePaletteEntry(hexTopSelectedFg, hexTopSelectedBg), + "hex-top-unselected": gowid.MakePaletteEntry(hexTopUnselectedFg, hexTopUnselectedBg), + "hex-bottom-selected": gowid.MakePaletteEntry(hexBottomSelectedFg, hexBottomSelectedBg), + "hex-bottom-unselected": gowid.MakePaletteEntry(hexBottomUnselectedFg, hexBottomUnselectedBg), + "hexln-selected": gowid.MakePaletteEntry(hexLineFg, hexLineBg), + "hexln-unselected": gowid.MakePaletteEntry(hexLineFg, hexLineBg), + "copy-mode-indicator": gowid.MakePaletteEntry(gowid.ColorWhite, gowid.ColorDarkRed), + "copy-mode": gowid.MakePaletteEntry(gowid.ColorBlack, gowid.ColorYellow), + } + + helpTmpl = template.Must(template.New("Help").Parse(` +{{define "NameVer"}}termshark v{{.Version}}{{end}} + +{{define "OneLine"}}A wireshark-inspired terminal user interface for tshark. Analyze network traffic interactively from your terminal.{{end}} + +{{define "Header"}}{{template "NameVer" .}} + +{{template "OneLine"}} +See https://github.com/gcla/termshark for more information.{{end}} + +{{define "Footer"}} +If --pass-thru is true (or auto, and stdout is not a tty), tshark will be +executed with the supplied command- line flags. You can provide +tshark-specific flags and they will be passed through to tshark (-n, -d, -T, +etc). For example: + +$ termshark -r file.pcap -T psml -n | less{{end}} + +{{define "UIHelp"}}{{template "NameVer" .}} + +A wireshark-inspired tui for tshark. Analyze network traffic interactively from your terminal. + +'/' - Go to display filter +'q' - Quit +'tab' - Switch panes +'c' - Switch to copy-mode +'|' - Cycle through pane layouts +'\' - Toggle pane zoom +'esc' - Activate menu +'t' - In bytes view, switch hex ⟷ ascii +'+/-' - Adjust horizontal split +'' - Adjust vertical split +'?' - Display help + +In the filter, type a wireshark display filter expression. + +Most terminals will support using the mouse! Try clicking the Close button. + +Use shift-left-mouse to copy and shift-right-mouse to paste.{{end}} + +{{define "CopyModeHelp"}}{{template "NameVer" .}} + +termshark is in copy-mode. You can press: + +'q', 'c' - Exit copy-mode +ctrl-c - Copy from selected widget +left - Select next outer-most widget +right - Select next inner-most widget{{end}} +'?' - Display copy-mode help +`)) + + // Used to determine if we should run tshark instead e.g. stdout is not a tty + tsopts struct { + PassThru string `long:"pass-thru" default:"auto" optional:"true" optional-value:"true" choice:"yes" choice:"no" choice:"auto" choice:"true" choice:"false" description:"Run tshark instead (auto => if stdout is not a tty)."` + } + + // Termshark's own command line arguments. Used if we don't pass through to tshark. + opts struct { + Iface string `value-name:"" short:"i" description:"Interface to read."` + Pcap flags.Filename `value-name:"" short:"r" description:"Pcap file to read."` + DecodeAs []string `short:"d" description:"Specify dissection of layer type." value-name:"==,"` + DisplayFilter string `short:"Y" description:"Apply display filter." value-name:""` + CaptureFilter string `short:"f" description:"Apply capture filter." value-name:""` + PassThru string `long:"pass-thru" default:"auto" optional:"true" optional-value:"true" choice:"yes" choice:"no" choice:"auto" choice:"true" choice:"false" description:"Run tshark instead (auto => if stdout is not a tty)."` + LogTty string `long:"log-tty" default:"false" optional:"true" optional-value:"true" choice:"yes" choice:"no" choice:"true" choice:"false" description:"Log to the terminal.."` + Help bool `long:"help" short:"h" optional:"true" optional-value:"true" description:"Show this help message."` + Version bool `long:"version" short:"v" optional:"true" optional-value:"true" description:"Show version information."` + + Args struct { + FilterOrFile string `value-name:"" description:"Filter (capture for iface, display for pcap), or pcap file to read."` + } `positional-args:"yes"` + } + + // If args are passed through to tshark (e.g. stdout not a tty), then + // strip these out so tshark doesn't fail. + termsharkOnly = []string{"--pass-thru", "--log-tty"} +) + +func flagIsTrue(val string) bool { + return val == "true" || val == "yes" +} + +//====================================================================== + +func init() { + tmplData = map[string]interface{}{ + "Version": termshark.Version, + } + quitRequestedChan = make(chan struct{}, 1) // buffered because send happens from ui goroutine, which runs global select + cacheRequestsChan = make(chan struct{}, 1000) + cacheRequests = make([]pcap.LoadPcapSlice, 0) +} + +//====================================================================== + +func writeHelp(p *flags.Parser, w io.Writer) { + if err := helpTmpl.ExecuteTemplate(w, "Header", tmplData); err != nil { + log.Fatal(err) + } + + fmt.Fprintln(w) + fmt.Fprintln(w) + p.WriteHelp(w) + + if err := helpTmpl.ExecuteTemplate(w, "Footer", tmplData); err != nil { + log.Fatal(err) + } + fmt.Fprintln(w) + fmt.Fprintln(w) +} + +func writeVersion(p *flags.Parser, w io.Writer) { + if err := helpTmpl.ExecuteTemplate(w, "NameVer", tmplData); err != nil { + log.Fatal(err) + } + + fmt.Fprintln(w) +} + +//====================================================================== + +func updateProgressBarForInterface(c *pcap.Loader, app gowid.IApp) { + setProgressIndeterminate(app) + switch loader.State() { + case 0: + app.Run(gowid.RunFunction(func(app gowid.IApp) { + clearProgressWidget(app) + })) + default: + app.Run(gowid.RunFunction(func(app gowid.IApp) { + loadSpinner.Update() + setProgressWidget(app) + })) + } +} + +func updateProgressBarForFile(c *pcap.Loader, prevRatio float64, app gowid.IApp) float64 { + setProgressDeterminate(app) + + psmlProg := Prog{100, 100} + pdmlPacketProg := Prog{0, 100} + pdmlIdxProg := Prog{0, 100} + pcapPacketProg := Prog{0, 100} + pcapIdxProg := Prog{0, 100} + curRowProg := Prog{100, 100} + + var err error + var c2 int64 + var m int64 + var x int + + // This shows where we are in the packet list. We want progress to be active only + // as long as our view has missing widgets. So this can help predict when our little + // view into the list of packets will be populated. + currentRow := -1 + var currentRowMod int64 = -1 + var currentRowDiv int = -1 + if packetListView != nil { + if fxy, err := packetListView.FocusXY(); err == nil { + foo, ok := packetListView.Model().RowIdentifier(fxy.Row) + if ok { + currentRow = int(foo) + currentRowMod = int64(currentRow % 1000) + currentRowDiv = (currentRow / 1000) * 1000 + c.Lock() + curRowProg.cur, curRowProg.max = int64(currentRow), int64(len(c.PacketPsmlData)) + c.Unlock() + } + } + } + + // Progress determined by how many of the (up to) 1000 pdml packets are read + // If it's not the same chunk of rows, assume it won't affect our view, so no progress needed + if c.State()&pcap.LoadingPdml != 0 { + if c.RowCurrentlyLoading == currentRowDiv { + if x, err = c.LengthOfPdmlCacheEntry(c.RowCurrentlyLoading); err == nil { + pdmlPacketProg.cur = int64(x) + pdmlPacketProg.max = int64(c.KillAfterReadingThisMany) + if currentRow != -1 && currentRowMod < pdmlPacketProg.max { + pdmlPacketProg.max = currentRowMod + 1 // zero-based + } + } + + // Progress determined by how far through the pcap the pdml reader is. + c.Lock() + c2, m, err = termshark.ProcessProgress(termshark.SafePid(c.PdmlCmd), c.PcapPdml) + c.Unlock() + if err == nil { + pdmlIdxProg.cur, pdmlIdxProg.max = c2, m + if currentRow != -1 { + // Only need to look this far into the psml file before my view is populated + m = m * (curRowProg.cur / curRowProg.max) + } + } + + // Progress determined by how many of the (up to) 1000 pcap packets are read + if x, err = c.LengthOfPcapCacheEntry(c.RowCurrentlyLoading); err == nil { + pcapPacketProg.cur = int64(x) + pcapPacketProg.max = int64(c.KillAfterReadingThisMany) + if currentRow != -1 && currentRowMod < pcapPacketProg.max { + pcapPacketProg.max = currentRowMod + 1 // zero-based + } + } + + // Progress determined by how far through the pcap the pcap reader is. + c.Lock() + c2, m, err = termshark.ProcessProgress(termshark.SafePid(c.PcapCmd), c.PcapPcap) + c.Unlock() + if err == nil { + pcapIdxProg.cur, pcapIdxProg.max = c2, m + if currentRow != -1 { + // Only need to look this far into the psml file before my view is populated + m = m * (curRowProg.cur / curRowProg.max) + } + } + } + } + + if psml, ok := c.PcapPsml.(string); ok && c.State()&pcap.LoadingPsml != 0 { + c.Lock() + c2, m, err = termshark.ProcessProgress(termshark.SafePid(c.PsmlCmd), psml) + c.Unlock() + if err == nil { + psmlProg.cur, psmlProg.max = c2, m + } + } + + var prog Prog + + // state is guaranteed not to include pcap.Loadingiface if we showing a determinate progress bar + switch c.State() { + case pcap.LoadingPsml: + prog = psmlProg + select { + case <-c.StartStage2Chan: + default: + prog.cur = prog.cur / 2 // temporarily divide in 2. Leave original for case above - so that the 50% + } + case pcap.LoadingPdml: + prog = progMin( + progMax(pcapPacketProg, pcapIdxProg), // max because the fastest will win and cancel the other + progMax(pdmlPacketProg, pdmlIdxProg), + ) + case pcap.LoadingPsml | pcap.LoadingPdml: + select { + case <-c.StartStage2Chan: + prog = progMin( // min because all of these have to complete, so the slowest determines progress + psmlProg, + progMin( + progMax(pcapPacketProg, pcapIdxProg), // max because the fastest will win and cancel the other + progMax(pdmlPacketProg, pdmlIdxProg), + ), + ) + default: + prog = psmlProg + prog.cur = prog.cur / 2 // temporarily divide in 2. Leave original for case above - so that the 50% + } + } + + curRatio := float64(prog.cur) / float64(prog.max) + if prog.Complete() { + if prevRatio < 1.0 { + app.Run(gowid.RunFunction(func(app gowid.IApp) { + clearProgressWidget(app) + })) + } + } else { + if prevRatio < curRatio { + app.Run(gowid.RunFunction(func(app gowid.IApp) { + loadProgress.SetTarget(app, int(prog.max)) + loadProgress.SetProgress(app, int(prog.cur)) + setProgressWidget(app) + })) + } + } + return curRatio +} + +//====================================================================== + +type RenderWeightUpTo struct { + gowid.RenderWithWeight + max int +} + +func (s RenderWeightUpTo) MaxUnits() int { + return s.max +} + +func weightupto(w int, max int) RenderWeightUpTo { + return RenderWeightUpTo{gowid.RenderWithWeight{W: w}, max} +} + +func units(n int) gowid.RenderWithUnits { + return gowid.RenderWithUnits{U: n} +} + +func weight(n int) gowid.RenderWithWeight { + return gowid.RenderWithWeight{W: n} +} + +func ratio(r float64) gowid.RenderWithRatio { + return gowid.RenderWithRatio{R: r} +} + +//====================================================================== + +func swallowMovementKeys(ev *tcell.EventKey, app gowid.IApp) bool { + res := false + switch ev.Key() { + case tcell.KeyDown, tcell.KeyCtrlN, tcell.KeyUp, tcell.KeyCtrlP, tcell.KeyRight, tcell.KeyCtrlF, tcell.KeyLeft, tcell.KeyCtrlB: + res = true + } + return res +} + +func swallowMouseScroll(ev *tcell.EventMouse, app gowid.IApp) bool { + res := false + switch ev.Buttons() { + case tcell.WheelDown: + res = true + case tcell.WheelUp: + res = true + } + return res +} + +// run in app goroutine +func clearPacketViews(app gowid.IApp) { + packetStructWidgets.Purge() + packetHexWidgets.Purge() + + packetListViewHolder.SetSubWidget(nullw, app) + packetStructureViewHolder.SetSubWidget(nullw, app) + packetHexViewHolder.SetSubWidget(nullw, app) +} + +//====================================================================== + +// Construct decoration around the tree node widget - a button to collapse, etc. +func makeStructNodeDecoration(pos tree.IPos, tr tree.IModel, wmaker tree.IWidgetMaker) gowid.IWidget { + var res gowid.IWidget + if tr == nil { + return nil + } + // Note that level should never end up < 0 + + // We know ou tree widget will never display the root node, so everything will be indented at + // least one level. So we know this will never end up negative. + level := -2 + for cur := pos; cur != nil; cur = tree.ParentPosition(cur) { + level += 1 + } + if level < 0 { + panic(errors.WithStack(gowid.WithKVs(termshark.BadState, map[string]interface{}{"level": level}))) + } + + pad := strings.Repeat(" ", level*2) + cwidgets := make([]gowid.IContainerWidget, 0) + cwidgets = append(cwidgets, + &gowid.ContainerWidget{ + IWidget: text.New(pad), + D: units(len(pad)), + }, + ) + + ct, ok := tr.(*pdmltree.Model) + if !ok { + panic(errors.WithStack(gowid.WithKVs(termshark.BadState, map[string]interface{}{"tree": tr}))) + } + + inner := wmaker.MakeWidget(pos, tr) + if ct.HasChildren() { + + var bn *button.Widget + if ct.IsCollapsed() { + bn = button.NewAlt(text.New("+")) + } else { + bn = button.NewAlt(text.New("-")) + } + + // If I use one button with conditional logic in the callback, rather than make + // a separate button depending on whether or not the tree is collapsed, it will + // correctly work when the DecoratorMaker is caching the widgets i.e. it will + // collapse or expand even when the widget is rendered from the cache + bn.OnClick(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, w gowid.IWidget) { + // Run this outside current event loop because we are implicitly + // adjusting the data structure behind the list walker, and it's + // not prepared to handle that in the same pass of processing + // UserInput. TODO. + app.Run(gowid.RunFunction(func(app gowid.IApp) { + ct.SetCollapsed(app, !ct.IsCollapsed()) + })) + })) + + cwidgets = append(cwidgets, + &gowid.ContainerWidget{ + IWidget: bn, + D: fixed, + }, + &gowid.ContainerWidget{ + IWidget: fillSpace, + D: units(1), + }, + ) + } else { + // Lines without an expander are just text - so you can't cursor down on to them unless you + // make them selectable (because the list will jump over them) + inner = selectable.New(inner) + + cwidgets = append(cwidgets, + &gowid.ContainerWidget{ + IWidget: fillSpace, + D: units(4), + }, + ) + + } + + cwidgets = append(cwidgets, &gowid.ContainerWidget{ + IWidget: inner, + D: weight(1), + }) + + res = columns.New(cwidgets) + + res = expander.New( + isselected.New( + res, + styled.New(res, gowid.MakePaletteRef("pkt-struct-selected")), + styled.New(res, gowid.MakePaletteRef("pkt-struct-focus")), + ), + ) + + return res +} + +// The widget representing the data at this level in the tree. Simply use what we extract from +// the PDML. +func makeStructNodeWidget(pos tree.IPos, tr tree.IModel) gowid.IWidget { + return text.New(tr.Leaf()) +} + +//====================================================================== + +// I want to have prefered position work on this, but you have to choose a subwidget +// to navigate to. We have three. I know that my use of them is very similar, so I'll +// just pick the first +type selectedComposite struct { + *isselected.Widget +} + +var _ gowid.IComposite = (*selectedComposite)(nil) + +func (w *selectedComposite) SubWidget() gowid.IWidget { + return w.Not +} + +//====================================================================== + +// rowFocusTableWidget provides a table that highlights the selected row or +// focused row. +type rowFocusTableWidget struct { + *table.BoundedWidget +} + +var _ gowid.IWidget = (*rowFocusTableWidget)(nil) +var _ gowid.IComposite = (*rowFocusTableWidget)(nil) + +func (t *rowFocusTableWidget) SubWidget() gowid.IWidget { + return t.BoundedWidget +} + +func (t *rowFocusTableWidget) Rows() int { + return t.Widget.Model().(table.IBoundedModel).Rows() +} + +func (t *rowFocusTableWidget) Up(lines int, size gowid.IRenderSize, app gowid.IApp) { + for i := 0; i < lines; i++ { + gowid.UserInput(t.Widget, tcell.NewEventKey(tcell.KeyUp, ' ', tcell.ModNone), size, gowid.Focused, app) + } +} + +func (t *rowFocusTableWidget) Down(lines int, size gowid.IRenderSize, app gowid.IApp) { + for i := 0; i < lines; i++ { + gowid.UserInput(t.Widget, tcell.NewEventKey(tcell.KeyDown, ' ', tcell.ModNone), size, gowid.Focused, app) + } +} + +func (t *rowFocusTableWidget) UpPage(num int, size gowid.IRenderSize, app gowid.IApp) { + for i := 0; i < num; i++ { + gowid.UserInput(t.Widget, tcell.NewEventKey(tcell.KeyPgUp, ' ', tcell.ModNone), size, gowid.Focused, app) + } +} + +func (t *rowFocusTableWidget) DownPage(num int, size gowid.IRenderSize, app gowid.IApp) { + for i := 0; i < num; i++ { + gowid.UserInput(t.Widget, tcell.NewEventKey(tcell.KeyPgDn, ' ', tcell.ModNone), size, gowid.Focused, app) + } +} + +// list.IWalker +func (t *rowFocusTableWidget) At(lpos list.IWalkerPosition) gowid.IWidget { + pos := int(lpos.(table.Position)) + w := t.Widget.AtRow(pos) + if w == nil { + return nil + } + + // Composite so it passes through prefered column + return &selectedComposite{ + Widget: isselected.New(w, + styled.New(w, gowid.MakePaletteRef("pkt-list-row-selected")), + styled.New(w, gowid.MakePaletteRef("pkt-list-row-focus")), + ), + } +} + +// Needed for WidgetAt above to work - otherwise t.Table.Focus() is called, table is the receiver, +// then it calls WidgetAt so ours is not used. +func (t *rowFocusTableWidget) Focus() list.IWalkerPosition { + return table.Focus(t) +} + +//====================================================================== + +func openError(msgt string, app gowid.IApp) { + // the same, for now + openMessage(msgt, app) +} + +func openMessage(msgt string, app gowid.IApp) { + maximizer := &dialog.Maximizer{} + + var al gowid.IHAlignment = hmiddle + if strings.Count(msgt, "\n") > 0 { + al = gowid.HAlignLeft{} + } + + var view gowid.IWidget = text.New(msgt, text.Options{ + Align: al, + }) + + view = hpadding.New( + view, + hmiddle, + gowid.RenderFixed{}, + ) + + view = framed.NewSpace(view) + + view = appkeys.New( + view, + func(ev *tcell.EventKey, app gowid.IApp) bool { + if ev.Rune() == 'z' { // maximize/unmaximize + if maximizer.Maxed { + maximizer.Unmaximize(yesno, app) + } else { + maximizer.Maximize(yesno, app) + } + return true + } + return false + }, + appkeys.Options{ + ApplyBefore: true, + }, + ) + + yesno = dialog.New( + view, + dialog.Options{ + Buttons: dialog.CloseOnly, + NoShadow: true, + BackgroundStyle: gowid.MakePaletteRef("dialog"), + ButtonStyle: gowid.MakePaletteRef("dialog-buttons"), + }, + ) + + dialog.OpenExt(yesno, topview, fixed, fixed, app) +} + +func openHelp(tmplName string, app gowid.IApp) { + yesno = dialog.New(framed.NewSpace(text.New(termshark.TemplateToString(helpTmpl, tmplName, tmplData))), + dialog.Options{ + Buttons: dialog.CloseOnly, + NoShadow: true, + BackgroundStyle: gowid.MakePaletteRef("dialog"), + ButtonStyle: gowid.MakePaletteRef("dialog-buttons"), + }, + ) + yesno.Open(topview, ratio(0.5), app) +} + +func openPleaseWait(app gowid.IApp) { + pleaseWait.Open(topview, fixed, app) +} + +func openCopyChoices(app gowid.IApp) { + var cc *dialog.Widget + maximizer := &dialog.Maximizer{} + + clips := app.Clips() + + cws := make([]gowid.IWidget, 0, len(clips)) + + copyCmd := termshark.ConfStringSlice( + "main.copy-command", + termshark.CopyToClipboard, + ) + + if len(copyCmd) == 0 { + openError("Config file has an invalid copy-command entry! Please remove it.", app) + return + } + + for _, clip := range clips { + c2 := clip + lbl := text.New(clip.ClipName() + ":") + btn := button.NewBare(text.New(clip.ClipValue(), text.Options{ + Wrap: text.WrapClip, + ClipIndicator: "...", + })) + + btn.OnClick(gowid.MakeWidgetCallback("cb", gowid.WidgetChangedFunction(func(app gowid.IApp, w gowid.IWidget) { + cmd := exec.Command(copyCmd[0], copyCmd[1:]...) + cmd.Stdin = strings.NewReader(c2.ClipValue()) + outBuf := bytes.Buffer{} + cmd.Stdout = &outBuf + + cc.Close(app) + app.InCopyMode(false) + + cmdTimeout := termshark.ConfInt("main.copy-command-timeout", 5) + if err := cmd.Start(); err != nil { + openError(fmt.Sprintf("Copy command \"%s\" failed: %v", strings.Join(copyCmd, " "), err), app) + return + } + + go func() { + closed := true + closeme := func() { + if !closed { + pleaseWait.Close(app) + closed = true + } + } + defer app.Run(gowid.RunFunction(func(app gowid.IApp) { + closeme() + })) + + done := make(chan error, 1) + go func() { + done <- cmd.Wait() + }() + + tick := time.NewTicker(time.Duration(200) * time.Millisecond) + defer tick.Stop() + tchan := time.After(time.Duration(cmdTimeout) * time.Second) + + Loop: + for { + select { + case <-tick.C: + app.Run(gowid.RunFunction(func(app gowid.IApp) { + pleaseWaitSpinner.Update() + if closed { + openPleaseWait(app) + closed = false + } + })) + + case <-tchan: + if err := cmd.Process.Kill(); err != nil { + app.Run(gowid.RunFunction(func(app gowid.IApp) { + closeme() + openError(fmt.Sprintf("Timed out, but could not kill copy command: %v", err), app) + })) + } else { + app.Run(gowid.RunFunction(func(app gowid.IApp) { + closeme() + openError(fmt.Sprintf("Copy command \"%v\" timed out", strings.Join(copyCmd, " ")), app) + })) + } + break Loop + + case err := <-done: + if err != nil { + app.Run(gowid.RunFunction(func(app gowid.IApp) { + closeme() + openError(fmt.Sprintf("Copy command \"%v\" failed: %v", strings.Join(copyCmd, " "), err), app) + })) + } else { + outStr := outBuf.String() + if len(outStr) == 0 { + app.Run(gowid.RunFunction(func(app gowid.IApp) { + closeme() + openMessage(" Copied! ", app) + })) + } else { + app.Run(gowid.RunFunction(func(app gowid.IApp) { + closeme() + openMessage(fmt.Sprintf("Copied! Output was:\n%s\n", outStr), app) + })) + } + } + break Loop + } + } + + }() + + }))) + + btn2 := styled.NewFocus(btn, gowid.MakeStyledAs(gowid.StyleReverse)) + tog := pile.NewFlow(lbl, btn2, divider.NewUnicode()) + cws = append(cws, tog) + } + + walker := list.NewSimpleListWalker(cws) + clipList := list.New(walker) + + // Do this so the list box scrolls inside the dialog + view2 := &gowid.ContainerWidget{ + IWidget: clipList, + D: weight(1), + } + + var view1 gowid.IWidget = pile.NewFlow(text.New("Select option to copy:"), divider.NewUnicode(), view2) + + view1 = appkeys.New( + view1, + func(ev *tcell.EventKey, app gowid.IApp) bool { + if ev.Rune() == 'z' { // maximize/unmaximize + if maximizer.Maxed { + maximizer.Unmaximize(cc, app) + } else { + maximizer.Maximize(cc, app) + } + return true + } + return false + }, + ) + + cc = dialog.New(view1, + dialog.Options{ + Buttons: dialog.CloseOnly, + NoShadow: true, + BackgroundStyle: gowid.MakePaletteRef("dialog"), + ButtonStyle: gowid.MakePaletteRef("dialog-buttons"), + }, + ) + + cc.OnOpenClose(gowid.MakeWidgetCallback("cb", gowid.WidgetChangedFunction(func(app gowid.IApp, w gowid.IWidget) { + if !cc.IsOpen() { + app.InCopyMode(false) + } + }))) + + dialog.OpenExt(cc, topview, ratio(0.5), ratio(0.8), app) +} + +func reallyQuit(app gowid.IApp) { + msgt := "Do you want to quit?" + msg := text.New(msgt) + yesno = dialog.New( + framed.NewSpace(hpadding.New(msg, hmiddle, fixed)), + dialog.Options{ + Buttons: []dialog.Button{ + dialog.Button{ + Msg: "Ok", + Action: func(app gowid.IApp, widget gowid.IWidget) { + quitRequestedChan <- struct{}{} + }, + }, + dialog.Cancel, + }, + NoShadow: true, + BackgroundStyle: gowid.MakePaletteRef("dialog"), + ButtonStyle: gowid.MakePaletteRef("dialog-buttons"), + }, + ) + yesno.Open(topview, units(len(msgt)+20), app) +} + +//====================================================================== + +type stateHandler struct { + sc *pcap.Scheduler +} + +func (s stateHandler) EnableOperations() { + s.sc.Enable() +} + +//====================================================================== + +type updatePacketViews struct { + ld *pcap.Scheduler + app gowid.IApp + stateHandler // send idle and iface state changes to global channels +} + +var _ pcap.IOnError = updatePacketViews{} +var _ pcap.IClear = updatePacketViews{} +var _ pcap.IBeforeBegin = updatePacketViews{} +var _ pcap.IAfterEnd = updatePacketViews{} + +func makePacketViewUpdater(app gowid.IApp) updatePacketViews { + res := updatePacketViews{} + res.app = app + res.ld = scheduler + return res +} + +func (t updatePacketViews) EnableOperations() { + t.ld.Enable() +} + +func (t updatePacketViews) OnClear(closeMe chan<- struct{}) { + close(closeMe) + t.app.Run(gowid.RunFunction(func(app gowid.IApp) { + clearPacketViews(app) + })) +} + +func (t updatePacketViews) BeforeBegin(ch chan<- struct{}) { + ch2 := loader.PsmlFinishedChan + + t.app.Run(gowid.RunFunction(func(app gowid.IApp) { + clearPacketViews(app) + t.ld.Lock() + defer t.ld.Unlock() + setPacketListWidgets(t.ld.PacketPsmlHeaders, t.ld.PacketPsmlData, app) + setProgressWidget(app) + + // Start this after widgets have been cleared, to get focus change + termshark.TrackedGo(func() { + fn2 := func() { + app.Run(gowid.RunFunction(func(app gowid.IApp) { + loader.Lock() + defer loader.Unlock() + updatePacketListWithData(loader.PacketPsmlHeaders, loader.PacketPsmlData, app) + })) + } + + termshark.RunOnDoubleTicker(ch2, fn2, + time.Duration(100)*time.Millisecond, + time.Duration(2000)*time.Millisecond, + 10) + }) + + close(ch) + })) +} + +func (t updatePacketViews) AfterEnd(ch chan<- struct{}) { + close(ch) + t.app.Run(gowid.RunFunction(func(app gowid.IApp) { + t.ld.Lock() + defer t.ld.Unlock() + updatePacketListWithData(t.ld.PacketPsmlHeaders, t.ld.PacketPsmlData, app) + })) +} + +func (t updatePacketViews) OnError(err error, closeMe chan<- struct{}) { + close(closeMe) + log.Error(err) + t.app.Run(gowid.RunFunction(func(app gowid.IApp) { + openError(fmt.Sprintf("%v", err), app) + })) +} + +//====================================================================== + +func reallyClear(app gowid.IApp) { + msgt := "Do you want to clear current capture?" + msg := text.New(msgt) + yesno = dialog.New( + framed.NewSpace(hpadding.New(msg, hmiddle, fixed)), + dialog.Options{ + Buttons: []dialog.Button{ + dialog.Button{ + Msg: "Ok", + Action: func(app gowid.IApp, w gowid.IWidget) { + yesno.Close(app) + scheduler.RequestClearPcap(makePacketViewUpdater(app)) + }, + }, + dialog.Cancel, + }, + NoShadow: true, + BackgroundStyle: gowid.MakePaletteRef("dialog"), + ButtonStyle: gowid.MakePaletteRef("dialog-buttons"), + }, + ) + yesno.Open(topview, units(len(msgt)+28), app) +} + +//====================================================================== + +type simpleMenuItem struct { + Txt string + Key gowid.Key + CB gowid.WidgetChangedFunction +} + +func makeRecentMenu(items []simpleMenuItem) gowid.IWidget { + menu1Widgets := make([]gowid.IWidget, 0) + menu1HotKeys := make([]gowid.IWidget, 0) + + max := 0 + for _, w := range items { + k := fmt.Sprintf("%v", w.Key) + if len(k) > max { + max = len(k) + } + } + + for _, w := range items { + load1B := button.NewBare(text.New(w.Txt)) + load1K := button.NewBare(text.New(fmt.Sprintf("%v", w.Key))) + load1CB := gowid.MakeWidgetCallback("cb", w.CB) + load1B.OnClick(load1CB) + if w.Key != gowid.MakeKey(' ') { + load1K.OnClick(load1CB) + } + menu1Widgets = append(menu1Widgets, load1B) + menu1HotKeys = append(menu1HotKeys, load1K) + } + for i, w := range menu1Widgets { + menu1Widgets[i] = styled.NewInvertedFocus(selectable.New(w), gowid.MakePaletteRef("default")) + } + for i, w := range menu1HotKeys { + menu1HotKeys[i] = styled.NewInvertedFocus(w, gowid.MakePaletteRef("default")) + } + + menu1Widgets2 := make([]*columns.Widget, len(menu1Widgets)) + for i, w := range menu1Widgets { + menu1Widgets2[i] = columns.New( + []gowid.IContainerWidget{ + &gowid.ContainerWidget{ + IWidget: hpadding.New( + // size is translated from flowwith{20} to fixed; fixed gives size 6, flowwith aligns right to 12 + hpadding.New( + menu1HotKeys[i], + gowid.HAlignRight{}, + fixed, + ), + gowid.HAlignLeft{}, + gowid.RenderFlowWith{C: max}, + ), + D: fixed, + }, + &gowid.ContainerWidget{ + IWidget: text.New("| "), + D: fixed, + }, + &gowid.ContainerWidget{ + IWidget: w, + D: fixed, + }, + }, + columns.Options{ + StartColumn: 2, + }, + ) + } + + menu1cwidgets := make([]gowid.IContainerWidget, len(menu1Widgets2)) + for i, w := range menu1Widgets2 { + menu1cwidgets[i] = &gowid.ContainerWidget{ + IWidget: w, + D: fixed, + } + } + + keys := make([]gowid.IKey, 0) + for _, i := range items { + if i.Key != gowid.MakeKey(' ') { + keys = append(keys, i.Key) + } + } + + menuListBox1 := keypress.New( + cellmod.Opaque( + styled.New( + framed.NewUnicode( + pile.New(menu1cwidgets, pile.Options{ + Wrap: true, + }), + ), + gowid.MakePaletteRef("default"), + ), + ), + keypress.Options{ + Keys: keys, + }, + ) + + menuListBox1.OnKeyPress(keypress.MakeCallback("key1", func(app gowid.IApp, w gowid.IWidget, k gowid.IKey) { + for _, r := range items { + if gowid.KeysEqual(k, r.Key) && r.Key != gowid.MakeKey(' ') { + r.CB(app, w) + break + } + } + })) + + return menuListBox1 +} + +//====================================================================== + +func appKeysResize1(evk *tcell.EventKey, app gowid.IApp) bool { + handled := true + if evk.Rune() == '+' { + mainviewRs.AdjustOffset(2, 6, resizable.Add1, app) + } else if evk.Rune() == '-' { + mainviewRs.AdjustOffset(2, 6, resizable.Subtract1, app) + } else { + handled = false + } + return handled +} + +func appKeysResize2(evk *tcell.EventKey, app gowid.IApp) bool { + handled := true + if evk.Rune() == '+' { + mainviewRs.AdjustOffset(4, 6, resizable.Add1, app) + } else if evk.Rune() == '-' { + mainviewRs.AdjustOffset(4, 6, resizable.Subtract1, app) + } else { + handled = false + } + return handled +} + +func viewcolsaKeyPress(evk *tcell.EventKey, app gowid.IApp) bool { + handled := true + if evk.Rune() == '>' { + altviewcols.AdjustOffset(0, 2, resizable.Add1, app) + } else if evk.Rune() == '<' { + altviewcols.AdjustOffset(0, 2, resizable.Subtract1, app) + } else { + handled = false + } + return handled +} + +func viewpilebKeyPress(evk *tcell.EventKey, app gowid.IApp) bool { + handled := true + if evk.Rune() == '+' { + altviewpile.AdjustOffset(0, 2, resizable.Add1, app) + } else if evk.Rune() == '-' { + altviewpile.AdjustOffset(0, 2, resizable.Subtract1, app) + } else { + handled = false + } + return handled +} + +func copyModeKeys(evk *tcell.EventKey, app gowid.IApp) bool { + handled := false + if app.InCopyMode() { + handled = true + + switch evk.Key() { + case tcell.KeyRune: + switch evk.Rune() { + case 'q', 'c': + app.InCopyMode(false) + case '?': + openHelp("CopyModeHelp", app) + } + case tcell.KeyEscape: + app.InCopyMode(false) + case tcell.KeyCtrlC: + openCopyChoices(app) + case tcell.KeyRight: + cl := app.CopyModeClaimedAt() + app.CopyModeClaimedAt(cl + 1) + app.RefreshCopyMode() + case tcell.KeyLeft: + cl := app.CopyModeClaimedAt() + if cl > 0 { + app.CopyModeClaimedAt(cl - 1) + app.RefreshCopyMode() + } + } + } else { + switch evk.Key() { + case tcell.KeyRune: + switch evk.Rune() { + case 'c': + app.InCopyMode(true) + handled = true + } + } + } + return handled +} + +func appKeyPress(evk *tcell.EventKey, app gowid.IApp) bool { + handled := true + if evk.Key() == tcell.KeyCtrlC { + if loader.State()&pcap.LoadingPsml != 0 { + scheduler.RequestStopLoad(stateHandler{}) // iface and psml + } else { + reallyQuit(app) + } + } else if evk.Key() == tcell.KeyCtrlL { + app.Sync() + } else if evk.Rune() == 'q' || evk.Rune() == 'Q' { + reallyQuit(app) + } else if evk.Key() == tcell.KeyTAB { + if topview.SubWidget() == viewOnlyPacketList { + topview.SetSubWidget(viewOnlyPacketStructure, app) + } else if topview.SubWidget() == viewOnlyPacketStructure { + topview.SetSubWidget(viewOnlyPacketHex, app) + } else if topview.SubWidget() == viewOnlyPacketHex { + topview.SetSubWidget(viewOnlyPacketList, app) + } + + gowid.SetFocusPath(viewOnlyPacketList, maxViewPath, app) + gowid.SetFocusPath(viewOnlyPacketStructure, maxViewPath, app) + gowid.SetFocusPath(viewOnlyPacketHex, maxViewPath, app) + + if packetStructureViewHolder.SubWidget() == missingMsgw { + gowid.SetFocusPath(mainview, mainViewPaths[0], app) + gowid.SetFocusPath(altview, altViewPaths[0], app) + } else { + newidx := -1 + if topview.SubWidget() == mainview { + v1p := gowid.FocusPath(mainview) + if deep.Equal(v1p, mainViewPaths[0]) == nil { + newidx = 1 + } else if deep.Equal(v1p, mainViewPaths[1]) == nil { + newidx = 2 + } else { + newidx = 0 + } + } else if topview.SubWidget() == altview { + v2p := gowid.FocusPath(altview) + if deep.Equal(v2p, altViewPaths[0]) == nil { + newidx = 1 + } else if deep.Equal(v2p, altViewPaths[1]) == nil { + newidx = 2 + } else { + newidx = 0 + } + } + + if newidx != -1 { + // Keep the views in sync + gowid.SetFocusPath(mainview, mainViewPaths[newidx], app) + gowid.SetFocusPath(altview, altViewPaths[newidx], app) + } + } + + } else if evk.Key() == tcell.KeyEscape { + menu1.Open(btnSite, app) + } else if evk.Rune() == '|' { + if topview.SubWidget() == mainview { + topview.SetSubWidget(altview, app) + } else { + topview.SetSubWidget(mainview, app) + } + } else if evk.Rune() == '\\' { + w := topview.SubWidget() + fp := gowid.FocusPath(w) + if w == viewOnlyPacketList || w == viewOnlyPacketStructure || w == viewOnlyPacketHex { + topview.SetSubWidget(mainview, app) + if deep.Equal(fp, maxViewPath) == nil { + switch w { + case viewOnlyPacketList: + gowid.SetFocusPath(mainview, mainViewPaths[0], app) + case viewOnlyPacketStructure: + gowid.SetFocusPath(mainview, mainViewPaths[1], app) + case viewOnlyPacketHex: + gowid.SetFocusPath(mainview, mainViewPaths[2], app) + } + } + } else { + topview.SetSubWidget(viewOnlyPacketList, app) + if deep.Equal(fp, maxViewPath) == nil { + gowid.SetFocusPath(viewOnlyPacketList, maxViewPath, app) + } + } + } else if evk.Rune() == '/' { + gowid.SetFocusPath(mainview, filterPathMain, app) + gowid.SetFocusPath(altview, filterPathAlt, app) + gowid.SetFocusPath(viewOnlyPacketList, filterPathMax, app) + gowid.SetFocusPath(viewOnlyPacketStructure, filterPathMax, app) + gowid.SetFocusPath(viewOnlyPacketHex, filterPathMax, app) + } else if evk.Rune() == '?' { + openHelp("UIHelp", app) + } else { + handled = false + } + return handled +} + +type LoadResult struct { + packetTree []*pdmltree.Model + headers []string + packetList [][]string +} + +func isProgressIndeterminate() bool { + return progressHolder.SubWidget() == loadSpinner +} + +func setProgressDeterminate(app gowid.IApp) { + progressHolder.SetSubWidget(loadProgress, app) +} + +func setProgressIndeterminate(app gowid.IApp) { + progressHolder.SetSubWidget(loadSpinner, app) +} + +func clearProgressWidget(app gowid.IApp) { + ds := filterCols.Dimensions() + sw := filterCols.SubWidgets() + sw[progWidgetIdx] = nullw + ds[progWidgetIdx] = fixed + filterCols.SetSubWidgets(sw, app) + filterCols.SetDimensions(ds, app) +} + +func setProgressWidget(app gowid.IApp) { + stop := button.New(text.New("Stop")) + stop2 := styled.NewExt(stop, gowid.MakePaletteRef("stop-load-button"), gowid.MakePaletteRef("stop-load-button-focus")) + + stop.OnClick(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, w gowid.IWidget) { + scheduler.RequestStopLoad(stateHandler{}) + })) + + prog := vpadding.New(progressHolder, gowid.VAlignTop{}, flow) + prog2 := columns.New([]gowid.IContainerWidget{ + &gowid.ContainerWidget{ + IWidget: prog, + D: weight(1), + }, + colSpace, + &gowid.ContainerWidget{ + IWidget: stop2, + D: fixed, + }, + }) + + ds := filterCols.Dimensions() + sw := filterCols.SubWidgets() + sw[progWidgetIdx] = prog2 + ds[progWidgetIdx] = weight(33) + filterCols.SetSubWidgets(sw, app) + filterCols.SetDimensions(ds, app) +} + +func setLowerWidgets(app gowid.IApp) { + var sw1 gowid.IWidget = missingMsgw + var sw2 gowid.IWidget = missingMsgw + if packetListView != nil { + if fxy, err := packetListView.FocusXY(); err == nil { + row2 := fxy.Row + row3, _ := packetListView.Model().RowIdentifier(row2) + row := int(row3) + + hex := getHexWidgetToDisplay(row) + if hex == nil { + sw1 = missingMsgw + } else { + // The 't' key will switch from hex <-> ascii + sw1 = enableselected.New(appkeys.New( + hex, + hex.OnKey(func(ev *tcell.EventKey) bool { + return ev.Rune() == 't' + }).SwitchView, + )) + } + //str := getStructWidgetToDisplay(row, hex) + str := getStructWidgetToDisplay(row, app) + if str == nil { + sw2 = missingMsgw + } else { + sw2 = enableselected.New(str) + } + } + } + packetHexViewHolder.SetSubWidget(sw1, app) + packetStructureViewHolder.SetSubWidget(sw2, app) +} + +func makePacketListModel(packetPsmlHeaders []string, packetPsmlData [][]string, app gowid.IApp) *psmltable.Model { + packetPsmlTableModel := table.NewSimpleModel( + packetPsmlHeaders, + packetPsmlData, + table.SimpleOptions{ + Style: table.StyleOptions{ + VerticalSeparator: fill.New(' '), + HeaderStyleProvided: true, + HeaderStyleFocus: gowid.MakePaletteRef("pkt-list-cell-focus"), + CellStyleProvided: true, + CellStyleSelected: gowid.MakePaletteRef("pkt-list-cell-selected"), + CellStyleFocus: gowid.MakePaletteRef("pkt-list-cell-focus"), + }, + Layout: table.LayoutOptions{ + Widths: []gowid.IWidgetDimension{ + weightupto(6, 10), + weightupto(10, 14), + weightupto(14, 32), + weightupto(14, 32), + weightupto(12, 32), + weightupto(8, 8), + weight(40), + }, + }, + }, + ) + + expandingModel := psmltable.New(packetPsmlTableModel, gowid.MakePaletteRef("pkt-list-row-focus")) + if len(expandingModel.Comparators) > 0 { + expandingModel.Comparators[0] = table.IntCompare{} + expandingModel.Comparators[5] = table.IntCompare{} + } + + return expandingModel +} + +func updatePacketListWithData(packetPsmlHeaders []string, packetPsmlData [][]string, app gowid.IApp) { + model := makePacketListModel(packetPsmlHeaders, packetPsmlData, app) + packetListTable.SetModel(model, app) +} + +func setPacketListWidgets(packetPsmlHeaders []string, packetPsmlData [][]string, app gowid.IApp) { + expandingModel := makePacketListModel(packetPsmlHeaders, packetPsmlData, app) + + packetListTable = &table.BoundedWidget{Widget: table.New(expandingModel)} + packetListView = &rowFocusTableWidget{packetListTable} + + packetListView.Lower().IWidget = list.NewBounded(packetListView) + packetListView.OnFocusChanged(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, w gowid.IWidget) { + fxy, err := packetListView.FocusXY() + if err != nil { + return + } + row2 := fxy.Row + row3, gotrow := packetListView.Model().RowIdentifier(row2) + row := int(row3) + + if gotrow && row >= 0 { + + rowm := row % 1000 + + cacheRequests = cacheRequests[:0] + + cacheRequests = append(cacheRequests, pcap.LoadPcapSlice{ + Row: (row / 1000) * 1000, + Cancel: true, + }) + if rowm > 500 { + cacheRequests = append(cacheRequests, pcap.LoadPcapSlice{ + Row: ((row / 1000) + 1) * 1000, + }) + } else { + row2 := ((row / 1000) - 1) * 1000 + if row2 < 0 { + row2 = 0 + } + cacheRequests = append(cacheRequests, pcap.LoadPcapSlice{ + Row: row2, + }) + } + + cacheRequestsChan <- struct{}{} + + setLowerWidgets(app) + } + })) + + withScrollbar := withscrollbar.New(packetListView) + packetListViewHolder.SetSubWidget(enableselected.New(withScrollbar), app) +} + +func expandStructWidgetAtPosition(row int, pos int, app gowid.IApp) { + if val, ok := packetStructWidgets.Get(row); ok { + trw := val.(*copymodetree.Widget) + + walker := trw.Walker().(*termshark.NoRootWalker) + curTree := walker.Tree().(*pdmltree.Model) + + finalPos := make([]int, 0) + + // hack accounts for the fact we always skip the first two nodes in the pdml tree but + // only at the first level + hack := 1 + Out: + for { + chosenIdx := -1 + var chosenTree *pdmltree.Model + for i, ch := range curTree.Children_[hack:] { + // Save the current best one - but keep going. The pdml does not necessarily present them sorted + // by position. So we might need to skip one to find the best fit. + if ch.Pos <= pos && pos < ch.Pos+ch.Size { + chosenTree = ch + chosenIdx = i + } + } + if chosenTree != nil { + chosenTree.Expanded = true + finalPos = append(finalPos, chosenIdx+hack) + curTree = chosenTree + hack = 0 + } else { + // didn't find any + break Out + } + } + if len(finalPos) > 0 { + tp := tree.NewPosExt(finalPos) + // this is to account for the fact that noRootWalker returns the next widget + // in the tree. Whatever position we find, we need to go back one to make up for this. + walker.SetFocus(tp, app) + trw.GoToMiddle(app) + } + } +} + +func getLayersFromStructWidget(row int, pos int) []hexdumper.LayerStyler { + layers := make([]hexdumper.LayerStyler, 0) + + row2 := (row / 1000) * 1000 + if ws, ok := loader.PacketCache.Get(row2); ok { + srcb2 := ws.(pcap.CacheEntry).Pdml + if row%1000 < len(srcb2) { + data, err := xml.Marshal(srcb2[row%1000]) + if err != nil { + log.Fatal(err) + } + + tr := pdmltree.DecodePacket(data) + tr.Expanded = true + + layers = tr.HexLayers(pos, false) + } + } + + return layers +} + +func getHexWidgetKey(row int) []byte { + return []byte(fmt.Sprintf("p%d", row)) +} + +// Can return nil +func getHexWidgetToDisplay(row int) *hexdumper.Widget { + var res2 *hexdumper.Widget + + if val, ok := packetHexWidgets.Get(row); ok { + res2 = val.(*hexdumper.Widget) + } else { + row2 := (row / 1000) * 1000 + if ws, ok := loader.PacketCache.Get(row2); ok { + srca := ws.(pcap.CacheEntry).Pcap + if len(srca) > row%1000 { + src := srca[row%1000] + b := make([]byte, len(src)) + copy(b, src) + + layers := getLayersFromStructWidget(row, 0) + res2 = hexdumper.New(b, layers, + "hex-cur-unselected", "hex-cur-selected", + "hexln-unselected", "hexln-selected", + "copy-mode", + ) + + // If the user moves the cursor in the hexdump, this callback will adjust the corresponding + // pdml tree/struct widget's currently selected layer. That in turn will result in a callback + // to the hex widget to set the active layers. + res2.OnPositionChanged(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, target gowid.IWidget) { + + // If we're not focused on hex, then don't expand the struct widget. That's because if + // we're focused on struct, then changing the struct position causes a callback to the + // hex to update layers - which can update the hex position - which invokes a callback + // to change the struct again. So ultimately, moving the struct moves the hex position + // which moves the struct and causes the struct to jump around. I need to check + // the alt view too because the user can click with the mouse and in one view have + // struct selected but in the other view have hex selected. + if topview.SubWidget() == mainview { + v1p := gowid.FocusPath(mainview) + if deep.Equal(v1p, mainViewPaths[2]) != nil { // it's not hex + return + } + } else { + v2p := gowid.FocusPath(altview) + if deep.Equal(v2p, altViewPaths[2]) != nil { // it's not hex + return + } + } + + expandStructWidgetAtPosition(row, res2.Position(), app) + })) + + packetHexWidgets.Add(row, res2) + } + } + } + return res2 +} + +//====================================================================== + +func getStructWidgetKey(row int) []byte { + return []byte(fmt.Sprintf("s%d", row)) +} + +// Note - hex can be nil +func getStructWidgetToDisplay(row int, app gowid.IApp) gowid.IWidget { + var res gowid.IWidget = missingMsgw + + if val, ok := packetStructWidgets.Get(row); ok { + res = val.(gowid.IWidget) + } else { + row2 := (row / 1000) * 1000 + if ws, ok := loader.PacketCache.Get(row2); ok { + srca := ws.(pcap.CacheEntry).Pdml + if len(srca) > row%1000 { + data, err := xml.Marshal(srca[row%1000]) + if err != nil { + log.Fatal(err) + } + + tr := pdmltree.DecodePacket(data) + tr.Expanded = true + + var pos tree.IPos = tree.NewPos() + pos = tree.NextPosition(pos, tr) // Start ahead by one, then never go back + + // Without the caching layer, clicking on a button has no effect + walker := termshark.NewNoRootWalker(tree.NewWalker(tr, pos, + tree.NewCachingMaker(tree.WidgetMakerFunction(makeStructNodeWidget)), + tree.NewCachingDecorator(tree.DecoratorFunction(makeStructNodeDecoration)))) + + // Send the layers represents the tree expansion to hex. + // This could be the user clicking inside the tree. Or it might be the position changing + // in the hex widget, resulting in a callback to programmatically change the tree expansion, + // which then calls back to the hex + updateHex := func(app gowid.IApp, twalker tree.ITreeWalker) { + newhex := getHexWidgetToDisplay(row) + if newhex != nil { + + newtree := twalker.Tree().(*pdmltree.Model) + newpos := twalker.Focus().(tree.IPos) + + leaf := newpos.GetSubStructure(twalker.Tree()).(*pdmltree.Model) + + coverWholePacket := false + + // This skips the "frame" node in the pdml that covers the entire range of bytes. If newpos + // is [0] then the user has chosen that node by interacting with the struct view (the hex view + // can't choose any position that maps to the first pdml child node) - so in this case, we + // send back a layer spanning the entire packet. Otherwise we don't want to send back that + // packet-spanning layer because it will always be the layer returned, meaning the hexdumper + // will always show the entire packet highlighted. + if newpos.Equal(tree.NewPosExt([]int{0})) { + coverWholePacket = true + } + + newlayers := newtree.HexLayers(leaf.Pos, coverWholePacket) + if len(newlayers) > 0 { + newhex.SetLayers(newlayers, app) + + curhexpos := newhex.Position() + smallestlayer := newlayers[len(newlayers)-1] + + if !(smallestlayer.Start <= curhexpos && curhexpos < smallestlayer.End) { + // This might trigger a callback from the hex layer since the position is set. Which will call + // back into here. But then this logic should not be triggered because the new pos will be + // inside the smallest layer + newhex.SetPosition(smallestlayer.Start, app) + } + } + } + + } + + walker.OnFocusChanged(tree.MakeCallback("cb", func(app gowid.IApp, twalker tree.ITreeWalker) { + updateHex(app, twalker) + })) + + updateHex(app, walker) + + tb := copymodetree.New(tree.New(walker), copyModePalette{}) + res = tb + packetStructWidgets.Add(row, res) + } + } + } + return res +} + +//====================================================================== + +type copyModePalette struct{} + +var _ gowid.IClipboardSelected = copyModePalette{} + +func (r copyModePalette) AlterWidget(w gowid.IWidget, app gowid.IApp) gowid.IWidget { + return styled.New(w, gowid.MakePaletteRef("copy-mode"), + styled.Options{ + OverWrite: true, + }, + ) +} + +//====================================================================== + +type saveRecents struct { + updatePacketViews + pcap string + filter string +} + +var _ pcap.IAfterEnd = saveRecents{} + +func (t saveRecents) AfterEnd(closeMe chan<- struct{}) { + t.updatePacketViews.AfterEnd(closeMe) + if t.pcap != "" { + addToRecentFiles(t.pcap) + } + if t.filter != "" { + addToRecentFilters(t.filter) + } +} + +// Call from app goroutine context +func requestLoadPcapWithCheck(pcap string, displayFilter string, app gowid.IApp) { + if _, err := os.Stat(pcap); os.IsNotExist(err) { + openError(fmt.Sprintf("File %s not found.", pcap), app) + } else { + scheduler.RequestLoadPcap(pcap, displayFilter, saveRecents{makePacketViewUpdater(app), pcap, displayFilter}) + } +} + +//====================================================================== + +// Prog hold a progress model - a current value on the way up to the max value +type Prog struct { + cur int64 + max int64 +} + +func (p Prog) Complete() bool { + return p.cur >= p.max +} + +func (p Prog) String() string { + return fmt.Sprintf("cur=%d max=%d", p.cur, p.max) +} + +func progMin(x, y Prog) Prog { + if float64(x.cur)/float64(x.max) < float64(y.cur)/float64(y.max) { + return x + } else { + return y + } +} + +func progMax(x, y Prog) Prog { + if float64(x.cur)/float64(x.max) > float64(y.cur)/float64(y.max) { + return x + } else { + return y + } +} + +//====================================================================== + +type configError struct { + Name string + Msg string +} + +var _ error = configError{} + +func (e configError) Error() string { + return fmt.Sprintf("Config error for key %s: %s", e.Name, e.Msg) +} + +//====================================================================== + +func loadOffsetFromConfig(name string) ([]resizable.Offset, error) { + offsStr := viper.GetString("main." + name) + if offsStr == "" { + return nil, errors.WithStack(configError{Name: name, Msg: "No offsets found"}) + } + res := make([]resizable.Offset, 0) + err := json.Unmarshal([]byte(offsStr), &res) + if err != nil { + return nil, errors.WithStack(configError{Name: name, Msg: "Could not unmarshal offsets"}) + } + return res, nil +} + +func saveOffsetToConfig(name string, offsets2 []resizable.Offset) { + offsets := make([]resizable.Offset, 0) + for _, off := range offsets2 { + if off.Adjust != 0 { + offsets = append(offsets, off) + } + } + if len(offsets) == 0 { + delete(viper.Get("main").(map[string]interface{}), name) + } else { + offs, err := json.Marshal(offsets) + if err != nil { + log.Fatal(err) + } + viper.Set("main."+name, string(offs)) + } + // Hack to make viper save if I only deleted from the map + viper.Set("main.lastupdate", time.Now().String()) + viper.WriteConfig() +} + +func addToRecentFiles(pcap string) { + comps := viper.GetStringSlice("main.recent-files") + if len(comps) == 0 || comps[0] != pcap { + comps = termshark.RemoveFromStringSlice(pcap, comps) + if len(comps) > 16 { + comps = comps[0 : 16-1] + } + viper.Set("main.recent-files", comps) + viper.WriteConfig() + } +} + +func addToRecentFilters(val string) { + comps := viper.GetStringSlice("main.recent-filters") + if (len(comps) == 0 || comps[0] != val) && strings.TrimSpace(val) != "" { + comps = termshark.RemoveFromStringSlice(val, comps) + if len(comps) > 64 { + comps = comps[0 : 64-1] + } + viper.Set("main.recent-filters", comps) + viper.WriteConfig() + } +} + +func makeRecentMenuWidget() gowid.IWidget { + savedItems := make([]simpleMenuItem, 0) + cfiles := termshark.ConfStringSlice("main.recent-files", []string{}) + if cfiles != nil { + for i, s := range cfiles { + scopy := s + savedItems = append(savedItems, + simpleMenuItem{ + Txt: s, + Key: gowid.MakeKey('a' + rune(i)), + CB: func(app gowid.IApp, w gowid.IWidget) { + savedMenu.Close(app) + // capFilter global, set up in cmain() + requestLoadPcapWithCheck(scopy, filterWidget.Value(), app) + }, + }, + ) + } + } + savedListBox := makeRecentMenu(savedItems) + + return savedListBox +} + +//====================================================================== + +type savedCompleterCallback struct { + prefix string + comp termshark.IPrefixCompleterCallback +} + +var _ termshark.IPrefixCompleterCallback = (*savedCompleterCallback)(nil) + +func (s *savedCompleterCallback) Call(orig []string) { + if s.prefix == "" { + comps := viper.GetStringSlice("main.recent-filters") + if len(comps) == 0 { + comps = orig + } + s.comp.Call(comps) + } else { + s.comp.Call(orig) + } +} + +type savedCompleter struct { + def termshark.IPrefixCompleter +} + +var _ termshark.IPrefixCompleter = (*savedCompleter)(nil) + +func (s savedCompleter) Completions(prefix string, cb termshark.IPrefixCompleterCallback) { + ncomp := &savedCompleterCallback{ + prefix: prefix, + comp: cb, + } + s.def.Completions(prefix, ncomp) +} + +//====================================================================== + +type setStructWidgets struct { + ld *pcap.Loader + app gowid.IApp +} + +var _ pcap.IOnError = setStructWidgets{} +var _ pcap.IClear = setStructWidgets{} +var _ pcap.IBeforeBegin = setStructWidgets{} +var _ pcap.IAfterEnd = setStructWidgets{} + +func (s setStructWidgets) OnClear(closeMe chan<- struct{}) { + close(closeMe) +} + +func (s setStructWidgets) BeforeBegin(ch chan<- struct{}) { + s2ch := loader.Stage2FinishedChan + + s.app.Run(gowid.RunFunction(func(app gowid.IApp) { + structmsgHolder.SetSubWidget(loadingw, s.app) + })) + + termshark.TrackedGo(func() { + fn2 := func() { + s.app.Run(gowid.RunFunction(func(app gowid.IApp) { + setLowerWidgets(app) + })) + } + + termshark.RunOnDoubleTicker(s2ch, fn2, + time.Duration(100)*time.Millisecond, + time.Duration(2000)*time.Millisecond, + 10) + }) + + close(ch) +} + +// Close the channel before the callback. When the global loader state is idle, +// app.Quit() will stop accepting app callbacks, so the goroutine that waits +// for ch to be closed will never terminate. +func (s setStructWidgets) AfterEnd(ch chan<- struct{}) { + close(ch) + s.app.Run(gowid.RunFunction(func(app gowid.IApp) { + setLowerWidgets(app) + structmsgHolder.SetSubWidget(nullw, app) + })) +} + +func (s setStructWidgets) OnError(err error, closeMe chan<- struct{}) { + close(closeMe) + log.Error(err) + s.app.Run(gowid.RunFunction(func(app gowid.IApp) { + openError(fmt.Sprintf("%v", err), app) + })) +} + +//====================================================================== + +type setNewPdmlRequests struct { + *pcap.Scheduler +} + +var _ pcap.ICacheUpdater = setNewPdmlRequests{} + +func (u setNewPdmlRequests) WhenLoadingPdml() { + u.When(func() bool { + return u.State()&pcap.LoadingPdml == pcap.LoadingPdml + }, func() { + cacheRequestsChan <- struct{}{} + }) +} + +func (u setNewPdmlRequests) WhenNotLoadingPdml() { + u.When(func() bool { + return u.State()&pcap.LoadingPdml == 0 + }, func() { + cacheRequestsChan <- struct{}{} + }) +} + +//====================================================================== + +// Run cmain() and afterwards make sure all goroutines stop, then exit with +// the correct exit code. Go's main() prototype does not provide for returning +// a value. +func main() { + // TODO - fix this later. goroutinewg is used every time a + // goroutine is started, to ensure we don't terminate until all are + // stopped. Any exception is a bug. + filter.Goroutinewg = &ensureGoroutinesStopWG + termshark.Goroutinewg = &ensureGoroutinesStopWG + pcap.Goroutinewg = &ensureGoroutinesStopWG + + res := cmain() + ensureGoroutinesStopWG.Wait() + os.Exit(res) +} + +func cmain() int { + viper.SetConfigName("termshark") // no need to include file extension - looks for file called termshark.ini for example + + stdConf := configdir.New("", "termshark") + dirs := stdConf.QueryFolders(configdir.Cache) + if err := dirs[0].CreateParentDir("dummy"); err != nil { + fmt.Printf("Warning: could not create cache dir: %v\n", err) + } + dirs = stdConf.QueryFolders(configdir.Global) + if err := dirs[0].CreateParentDir("dummy"); err != nil { + fmt.Printf("Warning: could not create config dir: %v\n", err) + } + viper.AddConfigPath(dirs[0].Path) + + if f, err := os.OpenFile(filepath.Join(dirs[0].Path, "termshark.toml"), os.O_RDONLY|os.O_CREATE, 0666); err != nil { + fmt.Printf("Warning: could not create initial config file: %v\n", err) + } else { + f.Close() + } + + err := viper.ReadInConfig() + if err != nil { + fmt.Println("Config file not found...") + } + + tsharkBin := termshark.TSharkBin() + + // Add help flag. This is no use for the user and we don't want to display + // help for this dummy set of flags designed to check for pass-thru to tshark - but + // if help is on, then we'll detect it, parse the flags as termshark, then + // display the intended help. + tsFlags := flags.NewParser(&tsopts, flags.IgnoreUnknown|flags.HelpFlag) + _, err = tsFlags.ParseArgs(os.Args) + + passthru := true + + if err != nil { + // If it's because of --help, then skip the tty check, and display termshark's help. This + // ensures we don't display a useless help, and further that you can pipe termshark's help + // into PAGER without invoking tshark. + if ferr, ok := err.(*flags.Error); ok && ferr.Type == flags.ErrHelp { + passthru = false + } else { + return 1 + } + } + + // Run after accessing the config so I can use the configured tshark binary, if there is one. I need that + // binary in the case that termshark is run where stdout is not a tty, in which case I exec tshark - but + // it makes sense to use the one in termshark.toml + if passthru && (flagIsTrue(tsopts.PassThru) || (tsopts.PassThru == "auto" && !isatty.IsTerminal(os.Stdout.Fd()))) { + bin, err := exec.LookPath(tsharkBin) + if err != nil { + fmt.Fprintf(os.Stderr, "Error looking up tshark binary: %v\n", err) + return 1 + } + args := []string{} + for _, arg := range os.Args[1:] { + if !termshark.StringInSlice(arg, termsharkOnly) && !termshark.StringIsArgPrefixOf(arg, termsharkOnly) { + args = append(args, arg) + } + } + args = append([]string{bin}, args...) + + if runtime.GOOS != "windows" { + err = syscall.Exec(bin, args, os.Environ()) + if err != nil { + fmt.Fprintf(os.Stderr, "Error execing tshark binary: %v\n", err) + return 1 + } + } else { + // No exec() on windows + c := exec.Command(args[0], args[1:]...) + c.Stdout = os.Stdout + c.Stderr = os.Stderr + + err = c.Start() + if err != nil { + fmt.Fprintf(os.Stderr, "Error starting tshark: %v\n", err) + return 1 + } + + err = c.Wait() + if err != nil { + fmt.Fprintf(os.Stderr, "Error waiting for tshark: %v\n", err) + return 1 + } + + return 0 + } + } + + // Parse the args now as intended for termshark + tmFlags := flags.NewParser(&opts, flags.PassDoubleDash) + var filterArgs []string + filterArgs, err = tmFlags.Parse() + + if err != nil { + fmt.Printf("Command-line error: %v\n\n", err) + writeHelp(tmFlags, os.Stderr) + return 1 + } + + if opts.Help { + writeHelp(tmFlags, os.Stdout) + return 0 + } + + if opts.Version { + writeVersion(tmFlags, os.Stdout) + return 0 + } + + pcapf := string(opts.Pcap) + + // If no interface specified, and no pcap specified via -r, then we assume the first + // argument is a pcap file e.g. termshark foo.pcap + if pcapf == "" && opts.Iface == "" { + pcapf = string(opts.Args.FilterOrFile) + } else { + // Add it to filter args. Figure out later if they're capture or display. + filterArgs = append(filterArgs, opts.Args.FilterOrFile) + } + + if pcapf != "" && opts.Iface != "" { + fmt.Fprintf(os.Stderr, "Please supply either a pcap or an interface.\n") + return 1 + } + + // go-flags returns [""] when no extra args are provided, so I can't just + // test the length of this slice + argsFilter := strings.Join(filterArgs, " ") + + // Work out capture filter afterwards because we need to determine first + // whether any potential first argument is intended as a pcap file instead of + // a capture filter. + captureFilter = opts.CaptureFilter + + if opts.Iface != "" && argsFilter != "" { + if opts.CaptureFilter != "" { + fmt.Fprintf(os.Stderr, "Two capture filters provided - '%s' and '%s' - please supply one only.\n", opts.CaptureFilter, argsFilter) + return 1 + } + captureFilter = argsFilter + } + + displayFilter := opts.DisplayFilter + + if pcapf != "" { + if captureFilter != "" { + fmt.Fprintf(os.Stderr, "Cannot use a capture filter when reading from a pcap file - '%s' and '%s'.\n", captureFilter, pcapf) + return 1 + } + if argsFilter != "" { + if opts.DisplayFilter != "" { + fmt.Fprintf(os.Stderr, "Two display filters provided - '%s' and '%s' - please supply one only.\n", opts.DisplayFilter, argsFilter) + return 1 + } + displayFilter = argsFilter + } + } + + // Better to do a command-line error if file supplied at command-line is not found. + if pcapf != "" { + if _, err := os.Stat(pcapf); err != nil { + fmt.Fprintf(os.Stderr, "Error reading file %s: %v.\n", pcapf, err) + return 1 + } + } + + // Helpful to use logging when enumerating interfaces below, so do it first + if !flagIsTrue(opts.LogTty) { + logfile := termshark.CacheFile("termshark.log") + logfd, err := os.OpenFile(logfile, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0666) + if err != nil { + fmt.Fprintf(os.Stderr, "Could not create log file %s: %v\n", logfile, err) + return 1 + } + // Don't close it - just let the descriptor be closed at exit. logrus is used + // in many places, some outside of this main function, and closing results in + // an error often on freebsd. + //defer logfd.Close() + log.SetOutput(logfd) + } + + foundTshark := false + if viper.Get("tshark") != nil { + if _, err = os.Stat(tsharkBin); err == nil { + foundTshark = true + } else if termshark.IsCommandInPath(tsharkBin) { + foundTshark = true + } + if !foundTshark { + fmt.Fprintf(os.Stderr, "Could not run tshark binary '%s'. The tshark binary is required to run termshark.\n", tsharkBin) + fmt.Fprintf(os.Stderr, "Check your config file %s\n", termshark.ConfFile("termshark.toml")) + return 1 + } + } else { + if !termshark.IsCommandInPath(tsharkBin) { + fmt.Fprintf(os.Stderr, "Could not find tshark in your PATH. The tshark binary is required to run termshark.\n") + if termshark.IsCommandInPath("apt") { + fmt.Fprintf(os.Stderr, "Try installing with: apt install tshark") + } else if termshark.IsCommandInPath("apt-get") { + fmt.Fprintf(os.Stderr, "Try installing with: apt-get install tshark") + } else if termshark.IsCommandInPath("yum") { + fmt.Fprintf(os.Stderr, "Try installing with: yum install wireshark") + } else if termshark.IsCommandInPath("brew") { + fmt.Fprintf(os.Stderr, "Try installing with: brew install wireshark") + } else { + fmt.Fprintln(os.Stderr, "") + } + fmt.Fprintln(os.Stderr, "") + return 1 + } + tsharkBin = termshark.DirOfPathCommandUnsafe(tsharkBin) + } + + valids := viper.GetStringSlice("main.validated-tsharks") + + if !termshark.StringInSlice(tsharkBin, valids) { + tver, err := termshark.TSharkVersion(tsharkBin) + if err != nil { + fmt.Fprintf(os.Stderr, "Could not determine tshark version: %v\n", err) + return 1 + } + // This is the earliest version I could determine gives reliable results in termshark. + // tshark compiled against tag v1.10.1 doesn't populate the hex view. + mver, _ := semver.Make("1.10.2") + if tver.LTE(mver) { + fmt.Fprintf(os.Stderr, "termshark will not operate correctly with a tshark older than %v (found %v)\n", mver, tver) + return 1 + } + + valids = append(valids, tsharkBin) + viper.Set("main.validated-tsharks", valids) + viper.WriteConfig() + } + + cacheDir := termshark.CacheDir() + if _, err = os.Stat(cacheDir); os.IsNotExist(err) { + err = os.Mkdir(cacheDir, 0777) + if err != nil { + fmt.Fprintf(os.Stderr, "Unexpected error making cache dir %s: %v", cacheDir, err) + return 1 + } + } + + emptyPcap := termshark.CacheFile("empty.pcap") + if _, err := os.Stat(emptyPcap); os.IsNotExist(err) { + err = termshark.WriteEmptyPcap(emptyPcap) + if err != nil { + fmt.Fprintf(os.Stderr, "Could not create dummy pcap %s: %v", emptyPcap, err) + return 1 + } + } + + // If opts.Iface is provided as a number, it's meant as the index of the interfaces as + // per the order returned by the OS. useIface will always be the name of the interface. + useIface := opts.Iface + + if opts.Iface != "" { + //ifaces, err := net.Interfaces() + ifaces, err := termshark.Interfaces() + if err != nil { + fmt.Fprintf(os.Stderr, "Could not enumerate network interfaces: %v\n", err) + return 1 + } + gotit := false + + // Check if opts.Iface was provided as a number + ifaceIdx, err := strconv.Atoi(opts.Iface) + if err != nil { + ifaceIdx = -1 + } + + for n, i := range ifaces { + if i == opts.Iface || n+1 == ifaceIdx { + gotit = true + useIface = i + break + } + } + if !gotit { + fmt.Fprintf(os.Stderr, "Could not find network interface %s\n", opts.Iface) + return 1 + } + } + + watcher, err := termshark.NewConfigWatcher() + if err != nil { + fmt.Fprintf(os.Stderr, "Problem constructing config file watcher: %v", err) + return 1 + } + defer watcher.Close() + + //====================================================================== + + startedWithIface := false + + defer func() { + if startedWithIface && loader != nil { + fmt.Printf("Packets read from interface %s have been saved in %s\n", loader.Interface(), loader.InterfaceFile()) + } + }() + + //====================================================================== + + ifaceExitCode := 0 + var ifaceErr error + + // This is deferred until after the app is Closed - otherwise messages written to stdout/stderr are + // swallowed by tcell. + defer func() { + if ifaceExitCode != 0 { + fmt.Printf("Cannot capture on interface %s", useIface) + if ifaceErr != nil { + fmt.Printf(": %v", ifaceErr) + } + fmt.Printf(" (exit code %d)\n", ifaceExitCode) + fmt.Printf("See https://wiki.wireshark.org/CaptureSetup/CapturePrivileges for more info.\n") + } + }() + + //====================================================================== + // + // Build the UI + + var app *gowid.App + + widgetCacheSize := termshark.ConfInt("main.ui-cache-size", 1000) + if widgetCacheSize < 64 { + widgetCacheSize = 64 + } + packetStructWidgets, err = lru.New(widgetCacheSize) + if err != nil { + fmt.Printf("Internal error: %v\n", err) + return 1 + } + packetHexWidgets, err = lru.New(widgetCacheSize) + if err != nil { + fmt.Printf("Internal error: %v\n", err) + return 1 + } + + nullw = null.New() + + loadingw = text.New("Loading, please wait...") + structmsgHolder = holder.New(loadingw) + fillSpace = fill.New(' ') + if runtime.GOOS == "windows" { + fillVBar = fill.New('|') + } else { + fillVBar = fill.New('┃') + } + + colSpace = &gowid.ContainerWidget{ + IWidget: fillSpace, + D: units(1), + } + + missingMsgw = vpadding.New( // centred + hpadding.New(structmsgHolder, hmiddle, fixed), + vmiddle, + flow, + ) + + pleaseWaitSpinner = spinner.New(spinner.Options{ + Styler: gowid.MakePaletteRef("progress-spinner"), + }) + + pleaseWait = dialog.New(framed.NewSpace( + pile.NewFlow( + &gowid.ContainerWidget{ + IWidget: text.New(" Please wait... "), + D: gowid.RenderFixed{}, + }, + fillSpace, + pleaseWaitSpinner, + )), + dialog.Options{ + Buttons: dialog.NoButtons, + NoShadow: true, + BackgroundStyle: gowid.MakePaletteRef("dialog"), + ButtonStyle: gowid.MakePaletteRef("dialog-buttons"), + }, + ) + + openMenu := button.New(text.New("Menu")) + openMenu2 := styled.NewExt(openMenu, gowid.MakePaletteRef("menu-button"), gowid.MakePaletteRef("menu-button-focus")) + + btnSite = menu.NewSite(menu.SiteOptions{YOffset: 1}) + openMenu.OnClick(gowid.MakeWidgetCallback(gowid.ClickCB{}, func(app gowid.IApp, target gowid.IWidget) { + menu1.Open(btnSite, app) + })) + + title := styled.New(text.New(termshark.TemplateToString(helpTmpl, "NameVer", tmplData)), gowid.MakePaletteRef("title")) + + copyMode := styled.New( + ifwidget.New( + text.New(" COPY-MODE "), + null.New(), + func() bool { + return app != nil && app.InCopyMode() + }, + ), + gowid.MakePaletteRef("copy-mode-indicator"), + ) + + menu1items := []simpleMenuItem{ + simpleMenuItem{ + Txt: "Help", + Key: gowid.MakeKey('?'), + CB: func(app gowid.IApp, w gowid.IWidget) { + menu1.Close(app) + openHelp("UIHelp", app) + }, + }, + simpleMenuItem{ + Txt: "Clear Packets", + Key: gowid.MakeKeyExt2(0, tcell.KeyCtrlW, ' '), + CB: func(app gowid.IApp, w gowid.IWidget) { + menu1.Close(app) + reallyClear(app) + }, + }, + simpleMenuItem{ + Txt: "Refresh Screen", + Key: gowid.MakeKeyExt2(0, tcell.KeyCtrlL, ' '), + CB: func(app gowid.IApp, w gowid.IWidget) { + menu1.Close(app) + app.Sync() + }, + }, + simpleMenuItem{ + Txt: "Quit", + Key: gowid.MakeKey('q'), + CB: func(app gowid.IApp, w gowid.IWidget) { + menu1.Close(app) + reallyQuit(app) + }, + }, + } + + menuListBox1 := makeRecentMenu(menu1items) + menu1 = menu.New("main", menuListBox1, fixed, menu.Options{ + Modal: true, + CloseKeysProvided: true, + CloseKeys: []gowid.IKey{ + gowid.MakeKeyExt(tcell.KeyLeft), + gowid.MakeKeyExt(tcell.KeyEscape), + gowid.MakeKeyExt(tcell.KeyCtrlC), + }, + }) + + loadProgress = progress.New(progress.Options{ + Normal: gowid.MakePaletteRef("progress-default"), + Complete: gowid.MakePaletteRef("progress-complete"), + }) + + loadSpinner = spinner.New(spinner.Options{ + Styler: gowid.MakePaletteRef("progress-spinner"), + }) + + savedListBox := makeRecentMenuWidget() + savedListBoxWidgetHolder := holder.New(savedListBox) + + savedMenu = menu.New("saved", savedListBoxWidgetHolder, fixed, menu.Options{ + Modal: true, + CloseKeysProvided: true, + CloseKeys: []gowid.IKey{ + gowid.MakeKeyExt(tcell.KeyLeft), + gowid.MakeKeyExt(tcell.KeyEscape), + gowid.MakeKeyExt(tcell.KeyCtrlC), + }, + }) + + titleView := columns.New([]gowid.IContainerWidget{ + &gowid.ContainerWidget{ + IWidget: title, + D: fixed, + }, + &gowid.ContainerWidget{ + IWidget: fill.New(' '), + D: weight(1), + }, + &gowid.ContainerWidget{ + IWidget: copyMode, + D: fixed, + }, + &gowid.ContainerWidget{ + IWidget: fill.New(' '), + D: weight(1), + }, + &gowid.ContainerWidget{ + IWidget: btnSite, + D: fixed, + }, + &gowid.ContainerWidget{ + IWidget: openMenu2, + D: fixed, + }, + }) + + packetListViewHolder = holder.New(nullw) + packetStructureViewHolder = holder.New(nullw) + packetHexViewHolder = holder.New(nullw) + + progressHolder = holder.New(nullw) + + applyw := button.New(text.New("Apply")) + applyWidget2 := styled.NewExt(applyw, gowid.MakePaletteRef("apply-button"), gowid.MakePaletteRef("apply-button-focus")) + applyWidget := disable.NewEnabled(applyWidget2) + + filterWidget = filter.New(filter.Options{ + Completer: savedCompleter{def: termshark.NewFields()}, + }) + + defer filterWidget.Close() + + applyw.OnClick(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, w gowid.IWidget) { + scheduler.RequestNewFilter(filterWidget.Value(), makePacketViewUpdater(app)) + })) + + filterWidget.OnValid(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, w gowid.IWidget) { + applyWidget.Enable() + })) + filterWidget.OnInvalid(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, w gowid.IWidget) { + applyWidget.Disable() + })) + filterLabel := text.New("Filter: ") + + savedw := button.New(text.New("Recent")) + savedWidget := styled.NewExt(savedw, gowid.MakePaletteRef("saved-button"), gowid.MakePaletteRef("saved-button-focus")) + savedBtnSite := menu.NewSite(menu.SiteOptions{YOffset: 1}) + savedw.OnClick(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, w gowid.IWidget) { + savedMenu.Open(savedBtnSite, app) + })) + + progWidgetIdx = 7 // adjust this if nullw moves position in filterCols + filterCols = columns.NewFixed(filterLabel, + &gowid.ContainerWidget{ + IWidget: filterWidget, + D: weight(100), + }, + applyWidget, colSpace, savedBtnSite, savedWidget, colSpace, nullw) + + filterView := framed.NewUnicode(filterCols) + + // swallowMovementKeys will prevent cursor movement that is not accepted + // by the main views (column or pile) to change focus e.g. moving from the + // packet structure view to the packet list view. Often you'd want this + // movement to be possible, but in termshark it's more often annoying - + // you navigate to the top of the packet structure, hit up one more time + // and you're in the packet list view accidentally, hit down instinctively + // to go back and you change the selected packet. + packetListViewWithKeys := appkeys.NewMouse( + appkeys.New( + appkeys.New( + packetListViewHolder, + appKeysResize1, + ), + swallowMovementKeys, + ), + swallowMouseScroll, + ) + + packetStructureViewWithKeys := appkeys.New( + appkeys.NewMouse( + appkeys.New( + appkeys.New( + packetStructureViewHolder, + appKeysResize2, + ), + swallowMovementKeys, + ), + swallowMouseScroll, + ), + copyModeKeys, appkeys.Options{ + ApplyBefore: true, + }, + ) + + packetHexViewHolderWithKeys := appkeys.New( + appkeys.NewMouse( + appkeys.New( + packetHexViewHolder, + swallowMovementKeys, + ), + swallowMouseScroll, + ), + copyModeKeys, appkeys.Options{ + ApplyBefore: true, + }, + ) + + mainviewRs = resizable.NewPile([]gowid.IContainerWidget{ + &gowid.ContainerWidget{ + IWidget: titleView, + D: units(1), + }, + &gowid.ContainerWidget{ + IWidget: filterView, + D: units(3), + }, + &gowid.ContainerWidget{ + IWidget: packetListViewWithKeys, + D: weight(1), + }, + &gowid.ContainerWidget{ + IWidget: divider.NewUnicode(), + D: flow, + }, + &gowid.ContainerWidget{ + IWidget: packetStructureViewWithKeys, + D: weight(1), + }, + &gowid.ContainerWidget{ + IWidget: divider.NewUnicode(), + D: flow, + }, + &gowid.ContainerWidget{ + IWidget: packetHexViewHolderWithKeys, + D: weight(1), + }, + }) + + mainviewRs.OnOffsetsSet(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, w gowid.IWidget) { + saveOffsetToConfig("mainview", mainviewRs.GetOffsets()) + })) + + viewOnlyPacketList = pile.New([]gowid.IContainerWidget{ + &gowid.ContainerWidget{ + IWidget: titleView, + D: units(1), + }, + &gowid.ContainerWidget{ + IWidget: filterView, + D: units(3), + }, + &gowid.ContainerWidget{ + IWidget: packetListViewHolder, + D: weight(1), + }, + }) + + viewOnlyPacketStructure = pile.New([]gowid.IContainerWidget{ + &gowid.ContainerWidget{ + IWidget: titleView, + D: units(1), + }, + &gowid.ContainerWidget{ + IWidget: filterView, + D: units(3), + }, + &gowid.ContainerWidget{ + IWidget: packetStructureViewHolder, + D: weight(1), + }, + }) + + viewOnlyPacketHex = pile.New([]gowid.IContainerWidget{ + &gowid.ContainerWidget{ + IWidget: titleView, + D: units(1), + }, + &gowid.ContainerWidget{ + IWidget: filterView, + D: units(3), + }, + &gowid.ContainerWidget{ + IWidget: packetHexViewHolder, + D: weight(1), + }, + }) + + altviewpile = resizable.NewPile([]gowid.IContainerWidget{ + &gowid.ContainerWidget{ + IWidget: packetListViewHolder, + D: weight(1), + }, + &gowid.ContainerWidget{ + IWidget: divider.NewUnicode(), + D: flow, + }, + &gowid.ContainerWidget{ + IWidget: packetStructureViewHolder, + D: weight(1), + }, + }) + + altviewpile.OnOffsetsSet(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, w gowid.IWidget) { + saveOffsetToConfig("altviewleft", altviewpile.GetOffsets()) + })) + + viewpilebkeys := appkeys.New(altviewpile, viewpilebKeyPress) + + altviewcols = resizable.NewColumns([]gowid.IContainerWidget{ + &gowid.ContainerWidget{ + IWidget: viewpilebkeys, + D: weight(1), + }, + &gowid.ContainerWidget{ + IWidget: fillVBar, + D: units(1), + }, + &gowid.ContainerWidget{ + IWidget: packetHexViewHolder, + D: weight(1), + }, + }) + + altviewcols.OnOffsetsSet(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, w gowid.IWidget) { + saveOffsetToConfig("altviewright", altviewcols.GetOffsets()) + })) + + viewcolsakeys := appkeys.New(altviewcols, viewcolsaKeyPress) + + altviewRs = resizable.NewPile([]gowid.IContainerWidget{ + &gowid.ContainerWidget{ + IWidget: titleView, + D: units(1), + }, + &gowid.ContainerWidget{ + IWidget: filterView, + D: units(3), + }, + &gowid.ContainerWidget{ + IWidget: viewcolsakeys, + D: weight(1), + }, + }) + + maxViewPath = []interface{}{2, 0} // list, structure or hex - whichever one is selected + + mainViewPaths = [][]interface{}{ + {2, 0}, // packet list + {4}, // packet structure + {6}, // packet hex + } + + altViewPaths = [][]interface{}{ + {2, 0, 0, 0}, // packet list + {2, 0, 2}, // packet structure + {2, 2}, // packet hex + } + + filterPathMain = []interface{}{1, 1} + filterPathAlt = []interface{}{1, 1} + filterPathMax = []interface{}{1, 1} + + mainview = mainviewRs + altview = altviewRs + + topview = holder.New(mainview) + + keylayer := appkeys.New(topview, appKeyPress) + + app, err = gowid.NewApp(gowid.AppArgs{ + View: keylayer, + Palette: &palette, + Log: log.StandardLogger(), + }) + if err != nil { + fmt.Printf("Error: %v\n", err) + return 1 + } + defer app.Close() + + for _, m := range filterWidget.Menus() { + app.RegisterMenu(m) + } + app.RegisterMenu(savedMenu) + app.RegisterMenu(menu1) + + // Populate the filter widget initially - runs asynchronously + go filterWidget.UpdateCompletions(app) + + gowid.SetFocusPath(mainview, mainViewPaths[0], app) + gowid.SetFocusPath(altview, altViewPaths[0], app) + + if offs, err := loadOffsetFromConfig("mainview"); err == nil { + mainviewRs.SetOffsets(offs, app) + } + if offs, err := loadOffsetFromConfig("altviewleft"); err == nil { + altviewpile.SetOffsets(offs, app) + } + if offs, err := loadOffsetFromConfig("altviewright"); err == nil { + altviewcols.SetOffsets(offs, app) + } + + // Set them up here so they have access to any command-line flags that + // need to be passed to the tshark commands used + pdmlArgs := termshark.ConfStringSlice("main.pdml-args", []string{}) + psmlArgs := termshark.ConfStringSlice("main.psml-args", []string{}) + tsharkArgs := termshark.ConfStringSlice("main.tshark-args", []string{}) + cacheSize := termshark.ConfInt("main.pcap-cache-size", 64) + scheduler = pcap.NewScheduler( + pcap.MakeCommands(opts.DecodeAs, tsharkArgs, pdmlArgs, psmlArgs), + pcap.Options{ + CacheSize: cacheSize, + }, + ) + loader = scheduler.Loader + + validator := filter.Validator{ + Invalid: &filter.ValidateCB{ + App: app, + Fn: func(app gowid.IApp) { + app.Run(gowid.RunFunction(func(app gowid.IApp) { + openError(fmt.Sprintf("Invalid filter: %s", displayFilter), app) + })) + }, + }, + } + + if pcapf != "" { + if pcapf, err = filepath.Abs(pcapf); err != nil { + fmt.Printf("Could not determine working directory: %v\n", err) + return 1 + } else { + doit := func(app gowid.IApp) { + app.Run(gowid.RunFunction(func(app gowid.IApp) { + filterWidget.SetValue(displayFilter, app) + })) + requestLoadPcapWithCheck(pcapf, displayFilter, app) + } + validator.Valid = &filter.ValidateCB{Fn: doit, App: app} + validator.Validate(displayFilter) + } + } else if useIface != "" { + + // Verifies whether or not we will be able to read from the interface (hopefully) + ifaceExitCode = -1 + if ifaceExitCode, ifaceErr = termshark.RunForExitCode("dumpcap", "-i", useIface, "-a", "duration:1", "-w", os.DevNull); ifaceExitCode != 0 { + return 1 + } + + doit := func(app gowid.IApp) { + app.Run(gowid.RunFunction(func(app gowid.IApp) { + filterWidget.SetValue(displayFilter, app) + })) + scheduler.RequestLoadInterface(useIface, captureFilter, displayFilter, saveRecents{makePacketViewUpdater(app), "", displayFilter}) + startedWithIface = true + } + validator.Valid = &filter.ValidateCB{Fn: doit, App: app} + validator.Validate(displayFilter) + } + + // Do this to make sure the program quits quickly if quit is invoked + // mid-load. It's safe to call this if a pcap isn't being loaded. + // + // The regular stopLoadPcap will send a signal to pcapChan. But if qpp.quit + // is called, the main select{} loop will be broken, and nothing will listen + // to that channel. As a result, nothing stops a pcap load. This calls the + // context cancellation function right away + defer func() { + loader.Close() + }() + + st := app.Runner() + st.Start() + defer st.Stop() + + configChangedFn := func(app gowid.IApp) { + savedListBox = makeRecentMenuWidget() + savedListBoxWidgetHolder.SetSubWidget(savedListBox, app) + } + + quitRequested := false + prevstate := loader.State() + var prev float64 + + progTicker := time.NewTicker(time.Duration(200) * time.Millisecond) + + loaderPsmlFinChan := loader.PsmlFinishedChan + loaderIfaceFinChan := loader.IfaceFinishedChan + loaderPdmlFinChan := loader.Stage2FinishedChan + +Loop: + for { + var opsChan <-chan pcap.RunFn + var tickChan <-chan time.Time + var psmlFinChan <-chan struct{} + var ifaceFinChan <-chan struct{} + var pdmlFinChan <-chan struct{} + + if loader.State() == 0 { + if loader.State() != prevstate { + if quitRequested { + app.Quit() + } + app.Run(gowid.RunFunction(func(app gowid.IApp) { + clearProgressWidget(app) + setProgressDeterminate(app) // always switch back - for pdml (partial) loads of later data. + })) + // When the progress bar is enabled, track the previous percentage reached. This + // is so that I don't go "backwards" if I generate a progress value less than the last + // one, using the current algorithm (because it would be confusing to see it go backwards) + prev = 0.0 + } + } + + if loader.State()&(pcap.LoadingPdml|pcap.LoadingPsml) != 0 { + tickChan = progTicker.C // progress is only enabled when a pcap may be loading + } + + if loader.State()&pcap.LoadingPdml != 0 { + pdmlFinChan = loaderPdmlFinChan + } + + if loader.State()&pcap.LoadingPsml != 0 { + psmlFinChan = loaderPsmlFinChan + } + + if loader.State()&pcap.LoadingIface != 0 { + ifaceFinChan = loaderIfaceFinChan + } + + // (User) operations are enabled by default (the test predicate is nil), or if the predicate returns true + // meaning the operation has reached its desired state. Only one operation can be in progress at a time. + if scheduler.IsEnabled() { + opsChan = scheduler.OperationsChan + } + + prevstate = loader.State() + + select { + case <-quitRequestedChan: + if loader.State() == 0 { + app.Quit() + } else { + quitRequested = true + // We know we're not idle, so stop any load so the quit op happens quickly for the user. + scheduler.RequestStopLoad(stateHandler{}) + } + + case fn := <-opsChan: + // We run the requested operation - because operations are now enabled, since this channel + // is listening - and the result tells us when operations can be re-enabled (i.e. the target + // state of the operation just started, for example). This means we can let an operation + // "complete", moving through a sequence of states to the final state, befpre accepting + // another request. + fn() + + case <-cacheRequestsChan: + cacheRequests = pcap.ProcessPdmlRequests(cacheRequests, loader, + struct { + setNewPdmlRequests + setStructWidgets + }{ + setNewPdmlRequests{scheduler}, + setStructWidgets{loader, app}, + }) + + case <-ifaceFinChan: + // this state change only happens if the load from the interface is explicitly + // stopped by the user (e.g. the stop button). When the current data has come + // from loading from an interface, when stopped we still want to be able to filter + // on that data. So the load routines should treat it like a regular pcap + // (until the interface is started again). That means the psml reader should read + // from the file and not the fifo. + loaderIfaceFinChan = loader.IfaceFinishedChan + loader.SetState(loader.State() & ^pcap.LoadingIface) + + case <-psmlFinChan: + if loader.LoadWasCancelled { + // Don't reset cancel state here. If, after stopping an interface load, I + // apply a filter, I need to know if the load was cancelled previously because + // if it was cancelled, I need to load from the temp pcap; if not cancelled, + // (meaning still running), then I just apply a new filter and have the pcap + // reader read from the fifo + app.Run(gowid.RunFunction(func(app gowid.IApp) { + openError("Loading was cancelled.", app) + })) + } + // Reset + loaderPsmlFinChan = loader.PsmlFinishedChan + loader.SetState(loader.State() & ^pcap.LoadingPsml) + + case <-pdmlFinChan: + loaderPdmlFinChan = loader.Stage2FinishedChan + loader.SetState(loader.State() & ^pcap.LoadingPdml) + + case <-tickChan: + if termshark.HaveFdinfo && (loader.State() == pcap.LoadingPdml || !loader.ReadingFromFifo()) { + prev = updateProgressBarForFile(loader, prev, app) + } else { + updateProgressBarForInterface(loader, app) + } + + case ev := <-app.TCellEvents: + app.HandleTCellEvent(ev, gowid.IgnoreUnhandledInput) + + case ev, ok := <-app.AfterRenderEvents: + // This means app.Quit() has been called, which closes the AfterRenderEvents + // channel - and then will accept no more events. select will then return + // nil on this channel - which we then use to break the loop + if !ok { + break Loop + } + app.RunThenRenderEvent(ev) + + case <-watcher.ConfigChanged(): + configChangedFn(app) + } + + } + + return 0 +} + +//====================================================================== +// Local Variables: +// mode: Go +// fill-column: 78 +// End: diff --git a/confwatcher.go b/confwatcher.go new file mode 100644 index 0000000..6ad0ca8 --- /dev/null +++ b/confwatcher.go @@ -0,0 +1,78 @@ +// Copyright 2019 Graham Clark. All rights reserved. Use of this source +// code is governed by the MIT license that can be found in the LICENSE +// file. + +package termshark + +import ( + "os" + "sync" + + log "github.com/sirupsen/logrus" + fsnotify "gopkg.in/fsnotify.v1" +) + +//====================================================================== + +var Goroutinewg *sync.WaitGroup + +type ConfigWatcher struct { + watcher *fsnotify.Watcher + change chan struct{} + close chan struct{} +} + +func NewConfigWatcher() (*ConfigWatcher, error) { + watcher, err := fsnotify.NewWatcher() + if err != nil { + panic(err) + } + + change := make(chan struct{}) + close := make(chan struct{}) + + res := &ConfigWatcher{ + change: change, + close: close, + } + + TrackedGo(func() { + Loop: + for { + select { + // watch for events + case <-watcher.Events: + res.change <- struct{}{} + + case err := <-watcher.Errors: + log.Debugf("Error from config watcher: %v", err) + + case <-close: + break Loop + } + } + }, Goroutinewg) + + if err := watcher.Add(ConfFile("termshark.toml")); err != nil && !os.IsNotExist(err) { + return nil, err + } + + res.watcher = watcher + + return res, nil +} + +func (c *ConfigWatcher) Close() error { + c.close <- struct{}{} + return c.watcher.Close() +} + +func (c *ConfigWatcher) ConfigChanged() <-chan struct{} { + return c.change +} + +//====================================================================== +// Local Variables: +// mode: Go +// fill-column: 78 +// End: diff --git a/copycommand.go b/copycommand.go new file mode 100644 index 0000000..6ceef9d --- /dev/null +++ b/copycommand.go @@ -0,0 +1,9 @@ +// Copyright 2019 Graham Clark. All rights reserved. Use of this source +// code is governed by the MIT license that can be found in the LICENSE +// file. + +// +build !darwin,!android,!windows + +package termshark + +var CopyToClipboard = []string{"xsel", "-i", "-b"} diff --git a/copycommand_android.go b/copycommand_android.go new file mode 100644 index 0000000..dd891db --- /dev/null +++ b/copycommand_android.go @@ -0,0 +1,7 @@ +// Copyright 2019 Graham Clark. All rights reserved. Use of this source +// code is governed by the MIT license that can be found in the LICENSE +// file. + +package termshark + +var CopyToClipboard = []string{"termux-clipboard-set"} diff --git a/copycommand_darwin.go b/copycommand_darwin.go new file mode 100644 index 0000000..6fbbd43 --- /dev/null +++ b/copycommand_darwin.go @@ -0,0 +1,7 @@ +// Copyright 2019 Graham Clark. All rights reserved. Use of this source +// code is governed by the MIT license that can be found in the LICENSE +// file. + +package termshark + +var CopyToClipboard = []string{"pbcopy"} diff --git a/copycommand_windows.go b/copycommand_windows.go new file mode 100644 index 0000000..aebdab3 --- /dev/null +++ b/copycommand_windows.go @@ -0,0 +1,7 @@ +// Copyright 2019 Graham Clark. All rights reserved. Use of this source +// code is governed by the MIT license that can be found in the LICENSE +// file. + +package termshark + +var CopyToClipboard = []string{"clip"} diff --git a/docs/FAQ.md b/docs/FAQ.md new file mode 100644 index 0000000..01f1497 --- /dev/null +++ b/docs/FAQ.md @@ -0,0 +1,122 @@ + +# FAQ + +## How can I copy a section of a packet from a remote machine when I can't forward X11? + +You can set up a custom termshark copy command that sends the copied data to a pastebin service, for example. If your remote machine is Ubuntu, try making an executable script called e.g. `/usr/local/bin/ts-copy.sh` + +```bash +#!/bin/bash +echo -n "See " && pastebinit +``` +Then edit ```~/.config/termshark/termshark.toml``` and set + +```toml +[main] + copy-command = "/usr/local/bin/ts-copy.sh" + ``` + When you copy a section of a packet, you should see something like this: + +![othercopy](https://drive.google.com/uc?export=view&id=11kLyrEhBQL3e50Nrzk_BhhZgCzt1cqDn) + +## Can I run termshark on Android? + +Yes, through the amazing termux package. Here are the steps: + +- Install [Termux](https://play.google.com/store/apps/details?id=com.termux&hl=en_US) and [Termux:API](https://play.google.com/store/apps/details?id=com.termux.api&hl=en_US) through the Google Play Store +- Run termux and type +```bash +pkg update +pkg install termux-api +``` +- Now you need to install tshark. Get the termux X11 packages first: +```bash +pkg install x11-repo +pkg install tshark +``` +- Finally, copy the termshark Android binary to somewhere in your termux ```PATH```. + +## If I load a big pcap, termshark doesn't load all the packets at once - why? + +Termshark cheats. When you give it a pcap, it generates PSML XML for every packet, but not the complete PDML (packet structure) XML. If you run ```time tshark -T pdml -r huge.pcap > /dev/null``` you'll see it can take many minutes to complete. So rather than generating PDML for the entire pcap file, termshark generates PDML in 1000 packet chunks (by default). It will always prioritize packets that are in view or could soon be in view, so that the user isn't kept waiting. Now, if you open a large pcap, and - once the packet list is complete - hit `end`, you would want to be able to see the structure of packets at the end of the pcap. If termshark generated the PDML in one shot, the user could be kept waiting many minutes to see the end, while tshark chugs through the file emitting data. So to display the data more quickly, termshark runs something like +```bash +tshark -T pdml -r huge.pcap -Y 'frame.number >= 12340000 and frame.number < 12341000' +``` +tshark is able to seek through the pcap much more quickly when it doesn't have to generate PDML - so this results in termshark getting data back to the user much more rapidly. + +If you start to page up quickly, you will likely approach a range of packets that termshark hasn't loaded, and it will have to issue another tshark command to fetch the data. Termshark launches the tshark command before those unloaded packets come into view but there's room here for more sophistication. One problem with this approach is that if you sort the packet list by a field like source IP, then moving up or down one packet may result in needing to display the structure and bytes for a packet many thousands of packets away from the current one ordered by time - so termshark might kick off a new ```-T pdml``` command for each up or down movement, meaning termshark will continually display "Loading..." + +## Termshark's colors are limited... + +Termshark respects the ```TERM``` environment variable and chooses a color scheme based on what it thinks the terminal is capable of, via the excellent [tcell](https://github.com/gdamore/tcell) package. You might be running on a terminal that can display more colors than ```TERM``` reports - so you can try adjusting your ```TERM``` variable e.g. if ```TERM``` is ```xterm```, try + +```bash +export TERM=xterm-256color +``` + +or even + +```bash +export TERM=xterm-truecolor +``` +then re-run termshark. + +tcell makes use of the environment variable ```COLORTERM``` when determining how to emit color codes. If ```COLORTERM``` is set to ```truecolor```, then tcell will emit truecolor color codes when the application changes the foreground or background color. If you connect to a remote machine with ssh to run termshark, the ```COLORTERM``` variable will not be forwarded. If that leaves you with ```TERM=xterm``` for example, then termshark, via tcell, will fall back to 8-color support. Here again you can change ```TERM``` or add a setting for ```COLORTERM``` to your remote ```.bashrc``` file. + +## How does termshark use tshark? + +Termshark uses tshark to provide all the data it displays, and to validate display filter expressions. When you give termshark a pcap file, it will run + +```bash +tshark -T psml -r my.pcap -Y '' -o gui.column.format:\"...\"``` +``` + +to generate the packet list data. Note that the columns are currently unconfigurable (future work...) Let's say the user is focused on packet number 1234. Then termshark will load packet structure and hex/byte data using commands like: + +```bash +tshark -T pdml -r my.pcap -Y ' and frame.number >= 1000 and frame.number < 2000' +tshark -F pcap -r my.pcap -Y ' and frame.number >= 1000 and frame.number < 2000' -w - +``` +If the user is reading from an interface, some extra processes are needed. To capture the data, termshark runs + +```bash +dumpcap -P -i eth0 -f -w +``` +This process runs until the user hits `ctrl-c` or clicks the "Stop" button in the UI. The path to ```tmpfile``` is printed out to the user when termshark exits. Then to feed data continually to termshark, another process is started: + +```bash +tail -f -c +0 tmpfile +``` +The stdout of the ```tail``` command is connected to the stdin of the PSML reading command, which is adjusted to: + +```bash +tshark -T psml -i - -l -Y '' -o gui.column.format:\"...\"``` +``` +The ```-l``` switch might push the data to the UI more quickly... The PDML and byte/hex generating commands read directly from `tmpfile`, since they don't need to provide continual updates (they load data in batches as the user moves around). + +When the user types in termshark's display filter widget, termshark issues the following command for each change: + +```bash +tshark -Y '' -r empty.pcap +``` +and checks the return code of the process. If it's zero, termshark assumes the filter expression is valid, and turns the widget green. If the return code is non-zero, termshark assumes the expression is invalid and turns the widget red. The file `empty.pcap` is generated once on startup and cached in ```$XDG_CONFIG_CACHE/empty.pcap``` (on Linux, ```~/.cache/termshark/empty.pcap```) On slower systems like the Raspberry Pi, you might see this widget go orange for a couple of seconds while termshark waits for tshark to finish. + +Finally, termshark uses tshark in one more way - to generate the possible completions for prefixes of display filter terms. If you type ```tcp.``` in the filter widget, termshark will show a drop-down menu of possible completions. This is generated once at startup by running + +```bash +termshark -G fields +``` +then parsing the output into a nested collection of Go maps, and serializing it to ```$XDG_CONFIG_CACHE/tsharkfields.gob.gz```. + +## What's next? + +There are many obvious ways to extend termshark, just based on the long list of tshark capabilities. I'd like to be able to: + +- Select a packet and display the reassembled stream +- Show pcap statistics, conversation statics, etc - expose all tshark's ```-z``` options +- Colorize the packets in the packet list view using Wireshark's coloring rules +- Allow the user to start reading from available interfaces once the UI has started +- And since tshark can be customized via the TOML config file, don't be so trusting of its output - there are surely bugs lurking here + +But I drew the line here for v1.0 in order to ship something! + diff --git a/docs/UserGuide.md b/docs/UserGuide.md new file mode 100644 index 0000000..966b636 --- /dev/null +++ b/docs/UserGuide.md @@ -0,0 +1,185 @@ +# User Guide + +Termshark provides a terminal-based user interface for analyzing packet captures. It's inspired by Wireshark, and depends on tshark for all its intelligence. Termshark is run from the command-line. You can see its options with + +```bash +$ termshark -h +``` +```console +termshark v1.0.0 + +A wireshark-inspired terminal user interface for tshark. Analyze network traffic interactively from your terminal. +See https://github.com/gcla/termshark for more information. + +Usage: + termshark [FilterOrFile] + +Application Options: + -i= Interface to read. + -r= Pcap file to read. + -d===, Specify dissection of layer type. + -Y= Apply display filter. + -f= Apply capture filter. + --pass-thru=[yes|no|auto|true|false] Run tshark instead (auto => if stdout is not a tty). (default: auto) + --log-tty=[yes|no|true|false] Log to the terminal.. (default: false) + -h, --help Show this help message. + -v, --version Show version information. + +Arguments: + FilterOrFile: Filter (capture for iface, display for pcap), or pcap file to read. + +If --pass-thru is true (or auto, and stdout is not a tty), tshark will be +executed with the supplied command- line flags. You can provide +tshark-specific flags and they will be passed through to tshark (-n, -d, -T, +etc). For example: + +$ termshark -r file.pcap -T psml -n | less +``` + +By default, termshark will launch an ncurses-like application in your terminal window, but if your standard output is not a tty, termshark will simply defer to tshark and pass its options through: + +```console +$ termshark -r test.pcap | cat + 1 0.000000 192.168.44.123 → 192.168.44.213 TFTP 77 Read Request, File: C:\IBMTCPIP\lccm.1, Transfer type: octet + 2 0.000000 192.168.44.123 → 192.168.44.213 TFTP 77 Read Request, File: C:\IBMTCPIP\lccm.1, Transfer type: octet +``` + +## Read a pcap file + +Launch termshark like this to inspect a file: + +```bash +termshark -r test.pcap +``` + +You can also apply a display filter directly from the command-line: + +```bash +termshark -r test.pcap icmp +``` + +Note that when reading a file, the filter will be interpreted as a display filter. When reading from an interface, the filter is interpreted as a capture filter. This follows tshark's behavior. + +Termshark will launch in your terminal. From here, you can press `?` for help: + +![tshelp](https://drive.google.com/uc?export=view&id=1DOZEAlP5xiNAoCKrZoIhWJ9Zz3gX0gJf) + +## Filtering + +Press `/` to focus on the display filter. Now you can type in a Wireshark display filter expression. The UI will update in real-time to display the validity of the current expression. If the expression is invalid, the filter widget will change color to red. As you type, termshark presents a drop-down menu with possible completions for the current term: + +![filterbad](https://drive.google.com/uc?export=view&id=1KobuhX7KfA_i2VU-lCllPc3FkLUBEmQi) + +When the filter widget is green, you can hit the "Apply" button to make its value take effect. Termshark will then reload the packets with the new display filter applied. + +![filterbad](https://drive.google.com/uc?export=view&id=10AVIaRtLWgqJ_fi0kWS_PI-vOogZTVv-) + +## Changing Files + +Termshark provides a "Recent" button which will open a menu with your most recently-loaded pcap files. Each invocation of termshark with the ```-r``` flag will add a pcap to the start of this list: + +![recent](https://drive.google.com/uc?export=view&id=1jnENk7ANqo2TZeqA-4hujHDWfDko_isT) + +## Changing Views + +Press `tab` to move between the three packet views. You can also use the mouse to move views by clicking with the left mouse button. When focus is in any of these three views, hit the `\` key to maximize that view: + +![max](https://drive.google.com/uc?export=view&id=143PHT2YDEuDig2QqFIGcZTjNg9TA7awB) + +Press `\` to restore the original layout. Press `|` to move the hex view to the right-hand side: + +![altview](https://drive.google.com/uc?export=view&id=1RinO3imTgboVYKLWblaLOqwjhu7OcUt4) + +You can also press `<`,`>`,`+` and `-` to change the relative size of each view. + +## Packet List View + +Termshark's top-most view is a list of packets read from the capture (or interface). Termshark generates the data by running `tshark` on the input with the `-T psml` options, and parsing the resulting XML. Currently the columns displayed cannot be configured, and are the same as Wireshark's defaults. When the source is a pcap file, the list can be sorted by column by clicking the button next to each column header: + +![sortcol](https://drive.google.com/uc?export=view&id=1UaXNRUp8UtR728j_CPTRTb0hpVy6EUte) + +You can hit `home` to jump to the top of the list or `end` to jump to the bottom. Sometimes, especially if running on a small terminal, the values in a column will be truncated (e.g. long IPv6 addresses). To see the full value, move the purple cursor over the value: + +![ipv6](https://drive.google.com/uc?export=view&id=1LXLz0gFieOf3mZEiP9QzwKSzSJL1FLT6) + +## Packet Structure View + +Termshark's middle view shows the structure of the packet selected in the list view. You can expand and contract the structure using the `[+]` and `[-]` buttons: + +![structure](https://drive.google.com/uc?export=view&id=1Tv7kvLxXe5a2tbsvkWR6U8K6nhEBqk8D) + +As you navigate the packet structure, different sections of the bottom view - a hex representation of the packet - will be highlighted. + +## Packet Hex View + +Termshark's bottom view shows the bytes that the packet comprises. Like Wireshark, they are displayed in a hexdump-like format. Hit the `t` key to switch from the hex bytes to the printable bytes and vice versa. As you move around the bytes, the middle (structure) view will update to show you where you are in the packet's structure. + +## Reading from an Interface + +Launch termshark like this to read from an interface: + +```bash +termshark -i eth0 +``` + +You can also apply a capture filter directly from the command-line: + +```bash +termshark -i eth0 tcp +``` + +Termshark will apply the capture filter as it reads, but the UI currently does not provide any indication of the capture filter that is in effect. + +Termshark's UI will launch and the packet views will update as packets are read: + +![readiface](https://drive.google.com/uc?export=view&id=1UPD6KaNGsFrQ9lW-_dx_0SXhTbWBX4vn) + +You can apply a display filter while the packet capture process is ongoing - termshark will dynamically apply the filter without restarting the capture. Press `ctrl-c` to stop the capture process. + +When you exit termshark, it will print a message with the location of the pcap file that was captured: + +```console +$ termshark -i eth0 +Packets read from interface eth0 have been saved in /home/gcla/.cache/termshark/eth0-657695279.pcap +``` + +## Copy Mode + +Both the structure and hex view support "copy mode" a feature which lets you copy ranges of data from the currently selected packet. First, move focus to the part of the packet you wish to copy. Now hit the `c` key - a section of the packet will be highlighted in yellow: + +![copymode1](https://drive.google.com/uc?export=view&id=1EE9zNYyzi3vLz6FBEgFfU0gRkkWsX1Dz) + +You can hit the `left` and `right` arrow keys to expand or contract the selected region. Now hit `ctrl-c` to copy. Termshark will display a dialog showing you the format in which you can copy the data: + +![copymode2](https://drive.google.com/uc?export=view&id=1EJW7DE1ycm9MbQkBFGOdDryoo5wlBgnZ) + +Select the format you want and hit `enter` (or click). Copy mode is available in the packet structure and packet hex views. + +This feature comes with a caveat! If you are connected to a remote machine e.g. via ssh, then you should use the `-X` flag to forward X11. On Linux, the default copy command is `xsel`. If you forward X11 with ssh, then the packet data will be copied to your desktop machine's clipboard. You can customize the copy command using termshark's config file e.g. +```toml +[main] + copy-command = ["xsel", "-i", "-p"] +``` +to instead set the primary selection. If forwarding X11 is not an option, you could instead upload the data (received via stdin) to a service like pastebin, and print the URL on stdout - termshark will display the copy command's output in a dialog when the command completes. See the [FAQ](FAQ.md). + +If you are running on OSX, termux (Android) or Windows, termshark assumes you are running locally and uses a platform-specific copy command. + + +## Config File + +Termshark reads options from a TOML configuration file saved in ```$XDG_CONFIG_HOME/termshark.toml``` (e.g. ```~/.config/termshark/termshark.toml``` on Linux). All options are saved under the ```[main]``` section. The available options are: + +- ```copy-command``` (string) - the command termshark executes when the user hits ctrl-c in copy-mode. The default commands on each platform will copy the selected area to the clipboard. +- ```copy-command-timeout``` (int) - how long termshark will wait (in seconds) for the copy command to complete before reporting an error. +- ```recent-files``` (string list) - the pcap files shown when the user clicks the "recent" button in termshark. Newly viewed files are added to the beginning. +- ```recent-filters``` (string list) - recently used Wireshark display filters. +- ```tshark``` (string) - make termshark use this specific ```tshark```. +- ```dumpcap``` (string) - make termshark use this specific ```dumpcap``` (used when reading from an interface). +- ```tail-command``` (string) - make termshark use this specific ```tail``` command. This is used when reading from an interface in order to feed ```dumpcap```-saved data to ```tshark```. The default is ```tail -f -c +0 ```. If you are running on Windows, the default is set to the cygwin tail command. But probably better to use Wireshark on Windows :-) +- ```tshark-args``` (string list) - these are added to each invocation of ```tshark``` made by termshark. For example, you could add decoder parameters like ```["-d","udp.port==2075,cflow]"``` +- ```pdml-args``` (string list) - any extra parameters to pass to ```tshark``` when it is invoked to generate PDML. +- ```psml-args``` (string list) - any extra parameters to pass to ```tshark``` when it is invoked to generate PSML. +- ```validated-tsharks``` - (string list) - termshark saves the path of each ``tshark`` binary it invokes (in case the user upgrades the system ```tshark```). If the selected (e.g. ```PATH```) tshark binary has not been validated, termshark will check to ensure its version is compatible. tshark must be newer than v1.10.2 (from approximately 2013). +- ```ui-cache-size``` - (int) - termshark will remember the state of widgets representing packets e.g. which parts are expanded in the structure view, and which byte is in focus in the hex view. This setting allows the user to override the number of widgets that are cached. The default is 1000. +- ```pcap-cache-size``` - (int) - termshark loads packet PDML (structure) and pcap (bytes) data in bundles of 1000. This setting determines how many such bundles termshark will keep cached. The default is 32. + diff --git a/fdinfo.go b/fdinfo.go new file mode 100644 index 0000000..7037310 --- /dev/null +++ b/fdinfo.go @@ -0,0 +1,67 @@ +// Copyright 2019 Graham Clark. All rights reserved. Use of this source +// code is governed by the MIT license that can be found in the LICENSE +// file. + +package termshark + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "regexp" + "strconv" + + "github.com/gcla/gowid" +) + +//====================================================================== + +var re *regexp.Regexp = regexp.MustCompile(`^pos:\s*([0-9]+)`) + +var FileNotOpenError = fmt.Errorf("Could not find file among descriptors") +var ParseError = fmt.Errorf("Could not match file position") + +// current, max +func ProcessProgress(pid int, filename string) (int64, int64, error) { + filename, err := filepath.EvalSymlinks(filename) + if err != nil { + return -1, -1, err + } + fi, err := os.Stat(filename) + if err != nil { + return -1, -1, err + } + finfo, err := ioutil.ReadDir(fmt.Sprintf("/proc/%d/fd", pid)) + if err != nil { + return -1, -1, err + } + fd := -1 + for _, f := range finfo { + lname, err := os.Readlink(fmt.Sprintf("/proc/%d/fd/%s", pid, f.Name())) + if err == nil && lname == filename { + fd, _ = strconv.Atoi(f.Name()) + break + } + } + if fd == -1 { + return -1, -1, gowid.WithKVs(FileNotOpenError, map[string]interface{}{"filename": filename}) + } + info, err := ioutil.ReadFile(fmt.Sprintf("/proc/%d/fdinfo/%d", pid, fd)) + + matches := re.FindStringSubmatch(string(info)) + if len(matches) <= 1 { + return -1, -1, gowid.WithKVs(ParseError, map[string]interface{}{"fdinfo": finfo}) + } + pos, err := strconv.ParseUint(matches[1], 10, 64) + if err != nil { + return -1, -1, err + } + return int64(pos), fi.Size(), nil +} + +//====================================================================== +// Local Variables: +// mode: Go +// fill-column: 78 +// End: diff --git a/fields.go b/fields.go new file mode 100644 index 0000000..878960e --- /dev/null +++ b/fields.go @@ -0,0 +1,177 @@ +// Copyright 2019 Graham Clark. All rights reserved. Use of this source +// code is governed by the MIT license that can be found in the LICENSE +// file. + +package termshark + +import ( + "bufio" + "os/exec" + "sort" + "strings" + "sync" + + log "github.com/sirupsen/logrus" +) + +//====================================================================== + +type mapOrString struct { + // Need to be exported for mapOrString to be serializable + M map[string]*mapOrString +} + +type TSharkFields struct { + once sync.Once + fields *mapOrString +} + +type IPrefixCompleterCallback interface { + Call([]string) +} + +type IPrefixCompleter interface { + Completions(prefix string, cb IPrefixCompleterCallback) +} + +func NewFields() *TSharkFields { + return &TSharkFields{} +} + +// Can be run asynchronously. +// This ought to use interfaces to make it testable. +func (w *TSharkFields) Init() error { + newer, err := FileNewerThan(CacheFile("tsharkfields.gob.gz"), DirOfPathCommandUnsafe(TSharkBin())) + if err == nil { + if newer { + f := &mapOrString{} + err = ReadGob(CacheFile("tsharkfields.gob.gz"), f) + if err == nil { + w.fields = f + log.Infof("Read cached tshark fields.") + return nil + } else { + log.Infof("Could not read cached tshark fields (%v) - regenerating...", err) + } + } + } + + cmd := exec.Command(TSharkBin(), []string{"-G", "fields"}...) + + out, err := cmd.StdoutPipe() + if err != nil { + return err + } + + cmd.Start() + + top := &mapOrString{ + M: make(map[string]*mapOrString), + } + + scanner := bufio.NewScanner(out) + for scanner.Scan() { + line := scanner.Text() + if strings.HasPrefix(line, "F") { + fields := strings.Split(line, "\t") + field := fields[2] + protos := strings.Split(field, ".") + cur := top + for i := 0; i < len(protos); i++ { + if val, ok := cur.M[protos[i]]; ok { + cur = val + } else { + next := &mapOrString{ + M: make(map[string]*mapOrString), + } + cur.M[protos[i]] = next + cur = next + } + } + } else if strings.HasPrefix(line, "P") { + fields := strings.Split(line, "\t") + field := fields[2] + if _, ok := top.M[field]; !ok { + next := &mapOrString{ + M: make(map[string]*mapOrString), + } + top.M[field] = next + } + } + } + + cmd.Wait() + + err = WriteGob(CacheFile("tsharkfields.gob.gz"), top) + if err != nil { + return err + } + + w.fields = top + + return nil +} + +func (t *TSharkFields) Completions(prefix string, cb IPrefixCompleterCallback) { + var err error + res := make([]string, 0, 100) + + t.once.Do(func() { + err = t.Init() + }) + + if err != nil { + log.Infof("Field completion error: %v", err) + } + + if t.fields == nil { + cb.Call(res) + return + } + + field := "" + txt := prefix + if !strings.HasSuffix(txt, " ") && txt != "" { + fields := strings.Fields(txt) + if len(fields) > 0 { + field = fields[len(fields)-1] + } + } + + fields := strings.Split(field, ".") + + prefs := make([]string, 0, 10) + cur := t.fields.M + failed := false + for i := 0; i < len(fields)-1; i++ { + if cur == nil { + failed = true + break + } + if val, ok := cur[fields[i]]; ok && val != nil { + prefs = append(prefs, fields[i]) + cur = val.M + } else { + failed = true + break + } + } + + if !failed { + for k, _ := range cur { + if strings.HasPrefix(k, fields[len(fields)-1]) { + res = append(res, strings.Join(append(prefs, k), ".")) + } + } + } + + sort.Strings(res) + + cb.Call(res) +} + +//====================================================================== +// Local Variables: +// mode: Go +// fill-column: 78 +// End: diff --git a/fields_test.go b/fields_test.go new file mode 100644 index 0000000..7f263ca --- /dev/null +++ b/fields_test.go @@ -0,0 +1,35 @@ +// Copyright 2019 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license +// that can be found in the LICENSE file. + +package termshark + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +//====================================================================== + +func TestFields1(t *testing.T) { + + fields := NewFields() + err := fields.Init() + assert.NoError(t, err) + + m1, ok := fields.fields.M["tcp"] + assert.Equal(t, true, ok) + + m2, ok := m1.M["port"] + assert.Equal(t, true, ok) + + _, ok = m2.M["foo"] + assert.Equal(t, false, ok) + +} + +//====================================================================== +// Local Variables: +// mode: Go +// fill-column: 78 +// End: diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..049ed8a --- /dev/null +++ b/go.mod @@ -0,0 +1,19 @@ +module github.com/gcla/termshark + +require ( + github.com/BurntSushi/toml v0.3.1 // indirect + github.com/blang/semver v3.5.1+incompatible + github.com/gcla/deep v1.0.2 + github.com/gcla/gowid v1.0.0 + github.com/gdamore/tcell v1.1.2-0.20190412054914-dcf1bb30770e + github.com/hashicorp/golang-lru v0.5.1 + github.com/jessevdk/go-flags v1.4.0 + github.com/mattn/go-isatty v0.0.7 + github.com/pkg/errors v0.8.1 + github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0 + github.com/sirupsen/logrus v1.4.1 + github.com/spf13/viper v1.3.2 + github.com/stretchr/testify v1.3.0 + golang.org/x/sys v0.0.0-20190416152802-12500544f89f // indirect + gopkg.in/fsnotify.v1 v1.4.7 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..10a92da --- /dev/null +++ b/go.sum @@ -0,0 +1,108 @@ +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/DATA-DOG/go-sqlmock v1.3.3 h1:CWUqKXe0s8A2z6qCgkP4Kru7wC11YoAnoupUKFDnH08= +github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/gcla/deep v1.0.2 h1:qBOx6eepcOSRYnHJ+f2ih4hP4Vca1YnLtXxp73n5KWI= +github.com/gcla/deep v1.0.2/go.mod h1:evE9pbpSGhItmFoBIk8hPOIC/keKTGYhFl6Le1Av+GE= +github.com/gcla/gowid v1.0.0 h1:78Xf5G9+lb4/g3KCB3hX8UJ8VorymMH5PXu9Npvwf8s= +github.com/gcla/gowid v1.0.0/go.mod h1:Th3cr14AYIbbSAVZO/uBtswds/4iJGIqYsRmOo59X54= +github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= +github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= +github.com/gdamore/tcell v1.1.2-0.20190412054914-dcf1bb30770e h1:o+4qsOk0svYrJq8y7UyMmz9e4JBaeDxwHuucvA2xWvw= +github.com/gdamore/tcell v1.1.2-0.20190412054914-dcf1bb30770e/go.mod h1:h3kq4HO9l2On+V9ed8w8ewqQEmGCSSHOgQ+2h8uzurE= +github.com/go-test/deep v1.0.1 h1:UQhStjbkDClarlmv0am7OXXO4/GaPdCGiUiMTvi28sg= +github.com/go-test/deep v1.0.1/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/guptarohit/asciigraph v0.4.1/go.mod h1:9fYEfE5IGJGxlP1B+w8wHFy7sNZMhPtn59f0RLtpRFM= +github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/lucasb-eyer/go-colorful v1.0.2 h1:mCMFu6PgSozg9tDNMMK3g18oJBX7oYGrC09mS6CXfO4= +github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s= +github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc= +github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y= +github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rakyll/statik v0.1.6/go.mod h1:OEi9wJV/fMUAGx1eNjq75DKDsJVuEv1U0oYdX6GX8Zs= +github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0 h1:Xuk8ma/ibJ1fOy4Ee11vHhUFHQNpHhrBneOCNHVXS5w= +github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0/go.mod h1:7AwjWCpdPhkSmNAgUv5C7EJ4AbmjEB3r047r3DXWu3Y= +github.com/sirupsen/logrus v1.4.0 h1:yKenngtzGh+cUSSh6GWbxW2abRqhYUSR/t/6+2QqNvE= +github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.3.2 h1:VUFqw5KcqRf7i70GOzW7N+Q7+gxVBkSSqiXB12+JQ4M= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 h1:u+LnwYTOOW7Ukr/fppxEb1Nwz0AtPflrblfvUudpo+I= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c h1:Vj5n4GlwjmQteupaxJ9+0FNOmBrHfq7vN4btdGoDZgI= +golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTuEGr4PN7F4XJ1p4E3Y8= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190322080309-f49334f85ddc h1:4gbWbmmPFp4ySWICouJl6emP0MyS31yy9SrTlAGFT+g= +golang.org/x/sys v0.0.0-20190322080309-f49334f85ddc/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190416152802-12500544f89f h1:1ZH9RnjNgLzh6YrsRp/c6ddZ8Lq0fq9xztNOoWJ2sz4= +golang.org/x/sys v0.0.0-20190416152802-12500544f89f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 h1:z99zHgr7hKfrUcX/KsoJk5FJfjTceCKIp96+biqP4To= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/have_fdinfo.go b/have_fdinfo.go new file mode 100644 index 0000000..dc64197 --- /dev/null +++ b/have_fdinfo.go @@ -0,0 +1,15 @@ +// Copyright 2019 Graham Clark. All rights reserved. Use of this source +// code is governed by the MIT license that can be found in the LICENSE +// file. + +// +build !linux,!android + +package termshark + +const HaveFdinfo = false + +//====================================================================== +// Local Variables: +// mode: Go +// fill-column: 110 +// End: diff --git a/have_fdinfo_linux.go b/have_fdinfo_linux.go new file mode 100644 index 0000000..cc5ea6a --- /dev/null +++ b/have_fdinfo_linux.go @@ -0,0 +1,13 @@ +// Copyright 2019 Graham Clark. All rights reserved. Use of this source +// code is governed by the MIT license that can be found in the LICENSE +// file. + +package termshark + +const HaveFdinfo = true + +//====================================================================== +// Local Variables: +// mode: Go +// fill-column: 110 +// End: diff --git a/modeswap/modeswap.go b/modeswap/modeswap.go new file mode 100644 index 0000000..05d1b45 --- /dev/null +++ b/modeswap/modeswap.go @@ -0,0 +1,42 @@ +// Copyright 2019 Graham Clark. All rights reserved. Use of this source +// code is governed by the MIT license that can be found in the LICENSE +// file. + +// package modeswap provides an IColor-conforming type Color that renders differently +// if in low-color mode +package modeswap + +import ( + "github.com/gcla/gowid" +) + +//====================================================================== + +type Color struct { + modeReg gowid.IColor + modeLow gowid.IColor +} + +var _ gowid.IColor = (*Color)(nil) + +func New(reg, lofi gowid.IColor) *Color { + return &Color{ + modeReg: reg, + modeLow: lofi, + } +} + +func (c *Color) ToTCellColor(mode gowid.ColorMode) (gowid.TCellColor, bool) { + var col gowid.IColor = c.modeLow + switch mode { + case gowid.Mode256Colors, gowid.Mode24BitColors: + col = c.modeReg + } + return col.ToTCellColor(mode) +} + +//====================================================================== +// Local Variables: +// mode: Go +// fill-column: 110 +// End: diff --git a/noroot.go b/noroot.go new file mode 100644 index 0000000..708fcea --- /dev/null +++ b/noroot.go @@ -0,0 +1,42 @@ +// Copyright 2019 Graham Clark. All rights reserved. Use of this source +// code is governed by the MIT license that can be found in the LICENSE +// file. + +package termshark + +import ( + "github.com/gcla/gowid/widgets/list" + "github.com/gcla/gowid/widgets/tree" +) + +//====================================================================== + +type NoRootWalker struct { + *tree.TreeWalker +} + +func NewNoRootWalker(w *tree.TreeWalker) *NoRootWalker { + return &NoRootWalker{ + TreeWalker: w, + } +} + +// for omitting top level node +func (f *NoRootWalker) Next(pos list.IWalkerPosition) list.IWalkerPosition { + return tree.WalkerNext(f, pos) +} + +func (f *NoRootWalker) Previous(pos list.IWalkerPosition) list.IWalkerPosition { + fc := pos.(tree.IPos) + pp := tree.PreviousPosition(fc, f.Tree()) + if pp.Equal(tree.NewPos()) { + return nil + } + return tree.WalkerPrevious(f, pos) +} + +//====================================================================== +// Local Variables: +// mode: Go +// fill-column: 110 +// End: diff --git a/pcap/cmds.go b/pcap/cmds.go new file mode 100644 index 0000000..0276888 --- /dev/null +++ b/pcap/cmds.go @@ -0,0 +1,194 @@ +// Copyright 2019 Graham Clark. All rights reserved. Use of this source +// code is governed by the MIT license that can be found in the LICENSE +// file. + +package pcap + +import ( + "fmt" + "io" + "os" + "os/exec" + "runtime" + "sync" + + "github.com/gcla/termshark" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" +) + +//====================================================================== + +type ProcessNotStarted struct { + Command *exec.Cmd +} + +var _ error = ProcessNotStarted{} + +func (e ProcessNotStarted) Error() string { + return fmt.Sprintf("Process %v not started yet", e.Command) +} + +//====================================================================== + +type command struct { + sync.Mutex + *exec.Cmd +} + +func (c *command) String() string { + c.Lock() + defer c.Unlock() + return fmt.Sprintf("%v %v", c.Cmd.Path, c.Cmd.Args) +} + +func (c *command) Start() error { + c.Lock() + defer c.Unlock() + c.Cmd.Stderr = log.StandardLogger().Writer() + res := c.Cmd.Start() + return res +} + +func (c *command) Wait() error { + return c.Cmd.Wait() +} + +func (c *command) StdoutPipe() (io.ReadCloser, error) { + c.Lock() + defer c.Unlock() + return c.Cmd.StdoutPipe() +} + +func (c *command) SetStdout(w io.Writer) { + c.Lock() + defer c.Unlock() + c.Cmd.Stdout = w +} + +func (c *command) Kill() error { + if c.Cmd.Process == nil { + return errors.WithStack(ProcessNotStarted{Command: c.Cmd}) + } + if runtime.GOOS == "windows" { + return c.Cmd.Process.Kill() + } else { + return c.Cmd.Process.Signal(os.Interrupt) + } +} + +func (c *command) Pid() int { + c.Lock() + defer c.Unlock() + if c.Cmd.Process == nil { + return -1 + } + return c.Cmd.Process.Pid +} + +//====================================================================== + +type Commands struct { + DecodeAs []string + Args []string + PdmlArgs []string + PsmlArgs []string +} + +func MakeCommands(decodeAs []string, args []string, pdml []string, psml []string) Commands { + return Commands{ + DecodeAs: decodeAs, + Args: args, + PdmlArgs: pdml, + PsmlArgs: psml, + } +} + +var _ ILoaderCmds = Commands{} + +func (c Commands) Iface(iface string, captureFilter string, tmpfile string) IBasicCommand { + args := []string{"-P", "-i", iface, "-w", tmpfile} + if captureFilter != "" { + args = append(args, "-f", captureFilter) + } + return &command{Cmd: exec.Command(termshark.DumpcapBin(), args...)} +} + +func (c Commands) Tail(tmpfile string) ITailCommand { + args := termshark.TailCommand() + args = append(args, tmpfile) + return &command{Cmd: exec.Command(args[0], args[1:]...)} +} + +func (c Commands) Psml(pcap interface{}, displayFilter string) IPcapCommand { + fifo := true + switch pcap.(type) { + case string: + fifo = false + } + + args := []string{ + // "-i", + // "0", + // "-o", + // "0", + //"-f", "-o", fmt.Sprintf("/tmp/foo-%d", delme), "-s", "256", "-tt", + //termshark.TSharkBin(), + "-T", "psml", + "-o", "gui.column.format:\"No.\",\"%m\",\"Time\",\"%t\",\"Source\",\"%s\",\"Destination\",\"%d\",\"Protocol\",\"%p\",\"Length\",\"%L\",\"Info\",\"%i\"", + } + if !fifo { + // read from cmdline file + args = append(args, "-r", pcap.(string)) + } else { + args = append(args, "-i", "-") + args = append(args, "-l") // provide data sooner to decoder routine in termshark + } + + if displayFilter != "" { + args = append(args, "-Y", displayFilter) + } + + for _, arg := range c.DecodeAs { + args = append(args, "-d", arg) + } + args = append(args, c.PsmlArgs...) + args = append(args, c.Args...) + + //cmd := exec.Command("strace", args...) + cmd := exec.Command(termshark.TSharkBin(), args...) + //cmd := exec.Command("stdbuf", args...) + if fifo { + cmd.Stdin = pcap.(io.Reader) + } + return &command{Cmd: cmd} +} + +func (c Commands) Pcap(pcap string, displayFilter string) IPcapCommand { + // need to use stdout and -w - otherwise, tshark writes one-line text output + args := []string{"-F", "pcap", "-r", pcap, "-w", "-"} + if displayFilter != "" { + args = append(args, "-Y", displayFilter) + } + args = append(args, c.Args...) + return &command{Cmd: exec.Command(termshark.TSharkBin(), args...)} +} + +func (c Commands) Pdml(pcap string, displayFilter string) IPcapCommand { + args := []string{"-T", "pdml", "-r", pcap} + if displayFilter != "" { + args = append(args, "-Y", displayFilter) + } + for _, arg := range c.DecodeAs { + args = append(args, "-d", arg) + } + args = append(args, c.PdmlArgs...) + args = append(args, c.Args...) + return &command{Cmd: exec.Command(termshark.TSharkBin(), args...)} +} + +//====================================================================== +// Local Variables: +// mode: Go +// fill-column: 78 +// End: diff --git a/pcap/loader.go b/pcap/loader.go new file mode 100644 index 0000000..696c743 --- /dev/null +++ b/pcap/loader.go @@ -0,0 +1,1632 @@ +// Copyright 2019 Graham Clark. All rights reserved. Use of this source +// code is governed by the MIT license that can be found in the LICENSE +// file. + +package pcap + +import ( + "context" + "encoding/xml" + "fmt" + "io" + "io/ioutil" + "os" + "regexp" + "strconv" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/gcla/termshark" + lru "github.com/hashicorp/golang-lru" + log "github.com/sirupsen/logrus" + fsnotify "gopkg.in/fsnotify.v1" +) + +//====================================================================== + +var Goroutinewg *sync.WaitGroup + +type RunFn func() +type whenFn func() bool + +type runFnInState struct { + when whenFn + doit RunFn +} + +//====================================================================== + +type ICommand interface { + Start() error + Wait() error + Kill() error + StdoutPipe() (io.ReadCloser, error) + SetStdout(io.Writer) +} + +//====================================================================== + +type LoaderState int + +const ( + LoadingPsml LoaderState = 1 << iota // pcap+pdml might be finished, but this is what was initiated + LoadingPdml // from a cache request + LoadingIface // copying from iface to temp pcap +) + +func (c *Loader) State() LoaderState { + return c.state +} + +// Repeatedly go back to the start if anything is triggered. +func (c *Loader) SetState(st LoaderState) { + c.state = st +Outer: + for { + Inner: + for i, sc := range c.onStateChange { + if sc.when() { + c.onStateChange = append(c.onStateChange[:i], c.onStateChange[i+1:]...) + sc.doit() + break Inner + } + } + break Outer + } +} + +func (t LoaderState) String() string { + s := make([]string, 0, 3) + if t&LoadingPsml != 0 { + s = append(s, "psml") + } + if t&LoadingPdml != 0 { + s = append(s, "pdml") + } + if t&LoadingIface != 0 { + s = append(s, "iface") + } + if len(s) == 0 { + return fmt.Sprintf("idle") + } else { + return strings.Join(s, "+") + } +} + +//====================================================================== + +type IBasicCommand interface { + Start() error + Wait() error + Pid() int + Kill() error +} + +type ITailCommand interface { + IBasicCommand + SetStdout(io.Writer) +} + +type IPcapCommand interface { + IBasicCommand + StdoutPipe() (io.ReadCloser, error) +} + +type ILoaderCmds interface { + Iface(iface string, captureFilter string, tmpfile string) IBasicCommand + Tail(tmpfile string) ITailCommand + Psml(pcap interface{}, displayFilter string) IPcapCommand + Pcap(pcap string, displayFilter string) IPcapCommand + Pdml(pcap string, displayFilter string) IPcapCommand +} + +type Loader struct { + cmds ILoaderCmds + + state LoaderState // which pieces are currently loading + + pcap string // The pcap source for this loader, "" if the loader is based on an interface + iface string // The interface being read from, "" if the loader is based on a pcap file + ifaceFile string // The temp pcap file that is created by reading from the interface + displayFilter string + captureFilter string + + PcapPsml interface{} // Pcap file source for the psml reader - fifo if iface+!stopped; tmpfile if iface+stopped; pcap otherwise + PcapPdml string // Pcap file source for the pdml reader - tmpfile if iface; pcap otherwise + PcapPcap string // Pcap file source for the pcap reader - tmpfile if iface; pcap otherwise + + mainCtx context.Context // cancelling this cancels the dependent contexts + mainCancelFn context.CancelFunc + psmlCtx context.Context // cancels the psml loading process + psmlCancelFn context.CancelFunc + stage2Ctx context.Context // cancels the pcap/pdml loading process + stage2CancelFn context.CancelFunc + ifaceCtx context.Context // cancels the iface reader process + ifaceCancelFn context.CancelFunc + + //psmlDecodingProcessChan chan struct{} // signalled by psml load stage when the XML decoding is complete - signals rest of stage 1 to shut down + stage2GoroutineDoneChan chan struct{} // signalled by a goroutine in stage 2 for pcap/pdml - always signalled at end. When x2, signals rest of stage 2 to shut down + + //stage1Wg sync.WaitGroup + stage2Wg sync.WaitGroup + + // Signalled when the psml is fully loaded (or already loaded) - to tell + // the pdml and pcap reader goroutines to start - they can then map table + // row -> frame number + StartStage2Chan chan struct{} + // Signalled to start the pdml reader. Will start concurrent with psml if + // psml loaded already or if filter == "" (then table row == frame number) + startPdmlChan chan struct{} + startPcapChan chan struct{} + + PsmlFinishedChan chan struct{} // closed when entire psml load process is done + Stage2FinishedChan chan struct{} // closed when entire pdml+pcap load process is done + IfaceFinishedChan chan struct{} // closed when interface reader process has shut down (e.g. stopped) + + ifaceCmd IBasicCommand + tailCmd ITailCommand + PsmlCmd IPcapCommand + PcapCmd IPcapCommand + PdmlCmd IPcapCommand + + sync.Mutex + PacketPsmlData [][]string + PacketPsmlHeaders []string + PacketCache *lru.Cache // i -> [pdml(i * 1000)..pdml(i+1*1000)] + + onStateChange []runFnInState + + LoadWasCancelled bool // True if the last load (iface or file) was halted by the stop button + RowCurrentlyLoading int // set by the pdml loading stage + highestCachedRow int + KillAfterReadingThisMany int // A shortcut - tell pcap/pdml to read one + + opt Options +} + +type Options struct { + CacheSize int +} + +func NewPcapLoader(cmds ILoaderCmds, opts ...Options) *Loader { + var opt Options + if len(opts) > 0 { + opt = opts[0] + } + + if opt.CacheSize == 0 { + opt.CacheSize = 32 + } + + res := &Loader{ + cmds: cmds, + IfaceFinishedChan: make(chan struct{}), + stage2GoroutineDoneChan: make(chan struct{}), + PsmlFinishedChan: make(chan struct{}), + Stage2FinishedChan: make(chan struct{}), + onStateChange: make([]runFnInState, 0), + RowCurrentlyLoading: -1, + highestCachedRow: -1, + opt: opt, + } + + res.resetData() + res.mainCtx, res.mainCancelFn = context.WithCancel(context.Background()) + + return res +} + +func (c *Loader) resetData() { + c.Lock() + defer c.Unlock() + c.PacketPsmlData = make([][]string, 0) + c.PacketPsmlHeaders = make([]string, 0, 10) + packetCache, err := lru.New(c.opt.CacheSize) + if err != nil { + log.Fatal(err) + } + c.PacketCache = packetCache +} + +// Close shuts down the whole loader, including progress monitoring goroutines. Use this only +// when about to load a new pcap (use a new loader) +func (c *Loader) Close() error { + if c.mainCancelFn != nil { + c.mainCancelFn() + } + return nil +} + +func (c *Loader) stopLoadIface() { + if c.ifaceCancelFn != nil { + c.ifaceCancelFn() + } +} + +func (c *Loader) stopLoadPsml() { + if c.psmlCancelFn != nil { + c.psmlCancelFn() + } +} + +func (c *Loader) stopLoadPdml() { + if c.stage2CancelFn != nil { + c.stage2CancelFn() + } +} + +//====================================================================== + +type Scheduler struct { + *Loader + OperationsChan chan RunFn + disabled bool +} + +func NewScheduler(cmds ILoaderCmds, opts ...Options) *Scheduler { + return &Scheduler{ + OperationsChan: make(chan RunFn, 1000), + Loader: NewPcapLoader(cmds, opts...), + } +} + +func (c *Scheduler) IsEnabled() bool { + return !c.disabled +} + +func (c *Scheduler) Enable() { + c.disabled = false +} + +func (c *Scheduler) Disable() { + c.disabled = true +} + +func (c *Scheduler) RequestClearPcap(cb interface{}) { + c.OperationsChan <- func() { + c.Disable() + c.doClearPcapOperation(cb, func() { + c.Enable() + }) + + } +} + +func (c *Scheduler) RequestStopLoad(cb interface{}) { + c.OperationsChan <- func() { + c.Disable() + c.doStopLoadOperation(cb, func() { + c.Enable() + }) + } +} + +func (c *Scheduler) RequestNewFilter(newfilt string, cb interface{}) { + c.OperationsChan <- func() { + c.Disable() + c.doNewFilterOperation(newfilt, cb, c.Enable) + } +} + +func (c *Scheduler) RequestLoadInterface(iface string, captureFilter string, displayFilter string, cb interface{}) { + c.OperationsChan <- func() { + c.Disable() + c.doLoadInterfaceOperation(iface, captureFilter, displayFilter, cb, func() { + c.Enable() + }) + } +} + +func (c *Scheduler) RequestLoadPcap(pcap string, displayFilter string, cb interface{}) { + c.OperationsChan <- func() { + c.Disable() + c.doLoadPcapOperation(pcap, displayFilter, cb, func() { + c.Enable() + }) + } +} + +//====================================================================== + +// Clears the currently loaded data. If the loader is currently reading from an +// interface, the loading continues after the current data has been discarded. If +// the loader is currently reading from a file, the loading *stops*. +func (c *Loader) doClearPcapOperation(cb interface{}, fn RunFn) { + //var res EnableOperationsWhen + + // if bb, ok := cb.(IBeforeBegin); ok { + // ch := make(chan struct{}) + // bb.BeforeBegin(ch) + // } + + if c.State() == 0 { + c.resetData() + + if oc, ok := cb.(IClear); ok { + ch := make(chan struct{}) + oc.OnClear(ch) + } + //cb.OnClear() + + fn() + } else { + // If we are reading from an interface when the clear operation is issued, we should + // continue again afterwards. If we're reading from a file, the clear stops the read. + // Track this state. + startIfaceAgain := false + + if c.State()&LoadingIface != 0 { + startIfaceAgain = true + c.stopLoadIface() + } + + if c.State()&LoadingPsml != 0 { + c.stopLoadPsml() + } + + c.When(c.IdleState, func() { + // Document why this needs to be delayed again, since runWhenReadyFn + // will run in app goroutine + c.doClearPcapOperation(cb, func() { + if startIfaceAgain { + c.doLoadInterfaceOperation(c.Interface(), c.CaptureFilter(), c.DisplayFilter(), cb, fn) + } else { + fn() + } + }) + }) + + } +} + +func (c *Loader) IdleState() bool { + return c.State() == 0 +} + +func (c *Loader) When(pred whenFn, doit RunFn) { + c.onStateChange = append(c.onStateChange, runFnInState{pred, doit}) +} + +func (c *Loader) doStopLoadOperation(cb interface{}, fn RunFn) { + c.LoadWasCancelled = true + + if bb, ok := cb.(IBeforeBegin); ok { + ch := make(chan struct{}) + bb.BeforeBegin(ch) + } + + if c.State() != 0 { + c.stopLoadPsml() + c.stopLoadPdml() + c.stopLoadIface() + + c.When(c.IdleState, func() { + c.doStopLoadOperation(cb, fn) + }) + } else { + c.turnOffPipe() + fn() + if aa, ok := cb.(IAfterEnd); ok { + ch := make(chan struct{}) + aa.AfterEnd(ch) + } + } +} + +// Issued e.g. if a new filter is applied while loading from an interface. We need +// to stop the psml (and fifo) and pdml reading processes, but keep alive the spooling +// process from iface -> temp file. When the current state is simply Loadingiface then +// the next operation can commence (e.g. applying the new filter value) +func (c *Loader) doStopLoadToIfaceOperation(fn RunFn) { + c.stopLoadPsml() + c.stopLoadPdml() + + c.When(func() bool { + return c.State() == LoadingIface + }, fn) +} + +// Called when state is appropriate +func (c *Loader) doNewFilterOperation(newfilt string, cb interface{}, fn RunFn) { + //var res EnableOperationsWhen + + if c.DisplayFilter() == newfilt { + log.Infof("No operation - same filter applied.") + } else if c.State() == 0 || c.State() == LoadingIface { + handleClear(cb) + + c.startLoadNewFilter(newfilt, cb) + + c.When(func() bool { + return c.State()&LoadingPsml == LoadingPsml + }, fn) + + c.SetState(c.State() | LoadingPsml) + + } else { + if c.State()&LoadingPsml != 0 { + c.stopLoadPsml() + } + + c.When(func() bool { + return c.State()&LoadingPsml == 0 + }, func() { + c.doNewFilterOperation(newfilt, cb, fn) + }) + } +} + +type IClear interface { + OnClear(closeMe chan<- struct{}) +} + +type IOnError interface { + OnError(err error, closeMe chan<- struct{}) +} + +type IBeforeBegin interface { + BeforeBegin(closeMe chan<- struct{}) +} + +type IAfterEnd interface { + AfterEnd(closeMe chan<- struct{}) +} + +func (c *Loader) doLoadInterfaceOperation(iface string, captureFilter string, displayFilter string, cb interface{}, fn RunFn) { + // The channel is unbuffered, and monitored from the same goroutine, so this would block + // unless we start a new goroutine + + //var res EnableOperationsWhen + + // If we're already loading, but the request is for the same, then ignore. If we were stopped, then + // process the request, because it implicitly means start reading from the interface again (and we + // are stopped) + if c.State()&LoadingPsml != 0 && c.Interface() == iface && c.DisplayFilter() == displayFilter && c.CaptureFilter() == captureFilter { + log.Infof("No operation - same interface and filters.") + } else if c.State() == 0 { + if oc, ok := cb.(IClear); ok { + ch := make(chan struct{}) + oc.OnClear(ch) + } + + if err := c.startLoadInterfaceNew(iface, captureFilter, displayFilter, cb); err == nil { + c.When(func() bool { + return c.State()&(LoadingIface|LoadingPsml) == LoadingIface|LoadingPsml + }, fn) + + c.SetState(c.State() | LoadingIface | LoadingPsml) + } else { + handleError(err, cb) + } + } else if c.State() == LoadingIface && iface == c.Interface() { + //if iface == c.Interface() { // same interface, so just start it back up - iface spooler still running + handleClear(cb) + c.startLoadNewFilter(displayFilter, cb) + + c.When(func() bool { + return c.State()&(LoadingIface|LoadingPsml) == LoadingIface|LoadingPsml + }, fn) + + c.SetState(c.State() | LoadingPsml) + } else { + // State contains Loadingpdml and/or Loadingpdml. Need to stop those first. OR state contains + // Loadingiface but the interface requested is different. + if c.State()&LoadingIface != 0 && iface != c.Interface() { + c.doStopLoadOperation(cb, func() { + c.doLoadInterfaceOperation(iface, captureFilter, displayFilter, cb, fn) + }) // returns an enable function when idle + } else { + c.doStopLoadToIfaceOperation(func() { + c.doLoadInterfaceOperation(iface, captureFilter, displayFilter, cb, fn) + }) + } + } +} + +// Call from app goroutine context +func (c *Loader) doLoadPcapOperation(pcap string, displayFilter string, cb interface{}, fn RunFn) { + curDisplayFilter := displayFilter + // The channel is unbuffered, and monitored from the same goroutine, so this would block + // unless we start a new goroutine + + if c.Pcap() == pcap && c.DisplayFilter() == curDisplayFilter { + log.Infof("No operation - same pcap and filter.") + } else if c.State() == 0 { + handleClear(cb) + + c.startLoadNewFile(pcap, curDisplayFilter, cb) + + c.When(func() bool { + return c.State()&LoadingPsml == LoadingPsml + }, fn) + + c.SetState(c.State() | LoadingPsml) + } else { + + // First, wait until everything is stopped + c.doStopLoadOperation(cb, func() { + c.doLoadPcapOperation(pcap, displayFilter, cb, fn) + }) + } +} + +func (c *Loader) ReadingFromFifo() bool { + return c.PcapPdml != c.PcapPsml +} + +func handleBegin(cb interface{}) { + if c, ok := cb.(IBeforeBegin); ok { + ch := make(chan struct{}) + c.BeforeBegin(ch) + <-ch + } +} + +func handleEnd(cb interface{}) { + if c, ok := cb.(IAfterEnd); ok { + ch := make(chan struct{}) + c.AfterEnd(ch) + <-ch + } +} + +func handleError(err error, cb interface{}) { + if ec, ok := cb.(IOnError); ok { + ch := make(chan struct{}) + ec.OnError(err, ch) + <-ch + } +} + +func handleClear(cb interface{}) { + if c, ok := cb.(IClear); ok { + ch := make(chan struct{}) + c.OnClear(ch) + <-ch + } +} + +// Save the file first +// Always called from app goroutine context - so don't need to protect for race on cancelfn +// Assumes gstate is ready +func (c *Loader) startLoadInterfaceNew(iface string, captureFilter string, displayFilter string, cb interface{}) error { + re := regexp.MustCompile(`[^a-zA-Z0-9.-]`) + ifaceClean := re.ReplaceAllString(iface, "_") + + tmpfile, err := ioutil.TempFile(termshark.CacheDir(), fmt.Sprintf("%s-*.pcap", ifaceClean)) + if err != nil { + handleError(err, cb) + return err + } + err = tmpfile.Close() + if err != nil { + handleError(err, cb) + return err + } + + c.PcapPsml = nil + c.PcapPdml = tmpfile.Name() + c.PcapPcap = tmpfile.Name() + + c.pcap = "" + c.iface = iface + c.ifaceFile = tmpfile.Name() + c.displayFilter = displayFilter + c.captureFilter = captureFilter + + c.startLoadPsml(cb) + termshark.TrackedGo(func() { + c.loadIfaceAsync(cb) + }, Goroutinewg) + + return nil +} + +func (c *Loader) startLoadNewFilter(displayFilter string, cb interface{}) { + c.displayFilter = displayFilter + + c.startLoadPsml(cb) +} + +func (c *Loader) startLoadNewFile(pcap string, displayFilter string, cb interface{}) { + c.pcap = pcap + c.iface = "" + c.ifaceFile = "" + + c.PcapPsml = pcap + c.PcapPdml = pcap + c.PcapPcap = pcap + c.displayFilter = displayFilter + + c.startLoadPsml(cb) +} + +func (c *Loader) startLoadPsml(cb interface{}) { + c.Lock() + c.PacketCache.Purge() + c.Unlock() + + termshark.TrackedGo(func() { + c.loadPsmlAsync(cb) + }, Goroutinewg) +} + +// assumes no pcap is being loaded +func (c *Loader) startLoadPdml(row int, cb interface{}) { + c.RowCurrentlyLoading = row + + termshark.TrackedGo(func() { + c.loadPcapAsync(row, cb) + }, Goroutinewg) +} + +func (c *Loader) updateCacheEntryWithPdml(row int, pdml []*PdmlPacket, done bool) { + var ce CacheEntry + c.Lock() + defer c.Unlock() + if ce2, ok := c.PacketCache.Get(row); ok { + ce = ce2.(CacheEntry) + } + ce.Pdml = pdml + ce.PdmlComplete = done + c.PacketCache.Add(row, ce) +} + +func (c *Loader) updateCacheEntryWithPcap(row int, pcap [][]byte, done bool) { + var ce CacheEntry + c.Lock() + defer c.Unlock() + if ce2, ok := c.PacketCache.Get(row); ok { + ce = ce2.(CacheEntry) + } + ce.Pcap = pcap + ce.PcapComplete = done + c.PacketCache.Add(row, ce) +} + +func (c *Loader) LengthOfPdmlCacheEntry(row int) (int, error) { + c.Lock() + defer c.Unlock() + if ce, ok := c.PacketCache.Get(row); ok { + ce2 := ce.(CacheEntry) + return len(ce2.Pdml), nil + } + return -1, fmt.Errorf("No cache entry found for row %d", row) +} + +func (c *Loader) LengthOfPcapCacheEntry(row int) (int, error) { + c.Lock() + defer c.Unlock() + if ce, ok := c.PacketCache.Get(row); ok { + ce2 := ce.(CacheEntry) + return len(ce2.Pcap), nil + } + return -1, fmt.Errorf("No cache entry found for row %d", row) +} + +type ISimpleCache interface { + Complete() bool +} + +var _ ISimpleCache = CacheEntry{} + +type iPcapLoader interface { + Interface() string + DisplayFilter() string + CaptureFilter() string + NumLoaded() int + CacheAt(int) (ISimpleCache, bool) + LoadingRow() int +} + +var _ iPcapLoader = (*Loader)(nil) + +func (c *Loader) Pcap() string { + return c.pcap +} + +func (c *Loader) Interface() string { + return c.iface +} + +func (c *Loader) InterfaceFile() string { + return c.ifaceFile +} + +func (c *Loader) DisplayFilter() string { + return c.displayFilter +} + +func (c *Loader) CaptureFilter() string { + return c.captureFilter +} + +func (c *Loader) NumLoaded() int { + c.Lock() + defer c.Unlock() + return len(c.PacketPsmlData) +} + +func (c *Loader) CacheAt(row int) (ISimpleCache, bool) { + if ce, ok := c.PacketCache.Get(row); ok { + return ce.(CacheEntry), ok + } + return CacheEntry{}, false +} + +func (c *Loader) LoadingRow() int { + return c.RowCurrentlyLoading +} + +func (c *Loader) loadIsNecessary(ev LoadPcapSlice) bool { + res := true + if ev.Row > c.NumLoaded() { + res = false + } else if ce, ok := c.CacheAt((ev.Row / 1000) * 1000); ok && ce.Complete() { + // Might be less because a cache load might've been interrupted - if it's not truncated then + // we're set + res = false + } else if c.LoadingRow() == ev.Row { + res = false + } + return res +} + +func (c *Loader) signalStage2Done(cb interface{}) { + ch := c.Stage2FinishedChan + c.Stage2FinishedChan = make(chan struct{}) + if a, ok := cb.(IAfterEnd); ok { + a.AfterEnd(ch) + <-ch + } +} + +func (c *Loader) signalStage2Starting(cb interface{}) { + handleBegin(cb) +} + +// Call from any goroutine - avoid calling in render, don't block it +// Procedure: +// - caller passes context, keeps cancel function +// - create a derived context for pcap reading processes +// - run them in goroutines +// - for each pcap process, +// - defer signal pcapchan when done +// - check for ctx2.Err to break +// - if err, then break +// - run goroutine to update UI with latest data on ticker +// - if ctxt2 done, then break +// - run controller watching for +// - if original ctxt done, then break (ctxt2 automatically cancelled) +// - if both processes done, then +// - cancel ticker with ctxt2 +// - wait for all to shut down +// - final UI update +//func loadPcapAsync(ctx context.Context, pcapFile string, filter string, app gowid.IApp) error { +func (c *Loader) loadPcapAsync(row int, cb interface{}) { + + // Used to cancel the tickers below which update list widgets with the latest data and + // update the progress meter. Note that if ctx is cancelled, then this context is cancelled + // too. When the 2/3 data loading processes are done, a goroutine will then run uiCtxCancel() + // to stop the UI updates. + + c.stage2Ctx, c.stage2CancelFn = context.WithCancel(c.mainCtx) + + intStage2Ctx, intStage2CancelFn := context.WithCancel(context.Background()) + + // Set to true by a goroutine started within here if ctxCancel() is called i.e. the outer context + var stageIsCancelled int32 + c.startPdmlChan = make(chan struct{}) + c.startPcapChan = make(chan struct{}) + + // Returns true if it's an error we should bring to user's attention + unexpectedError := func(err error) bool { + cancelled := atomic.LoadInt32(&stageIsCancelled) + if err != io.EOF && cancelled == 0 { + return true + } + return false + } + + setCancelled := func() { + atomic.CompareAndSwapInt32(&stageIsCancelled, 0, 1) + } + + //====================================================================== + + var displayFilterStr string + + sidx := -1 + eidx := -1 + + // When we start a command (in service of loading pcaps), add it to this list. Then we wait + // for finished signals on a channel - + //procs := []ICommand{} + + // signal to updater that we're about to start. This will block until cb completes + c.signalStage2Starting(cb) + + // This should correctly wait for all resources, no matter where in the process of creating them + // an interruption or error occurs + defer func() { + procsDoneCount := 0 + L: + for { + // pdml and psml make 2 + select { + // Don't need to wait for ctx.Done. if that gets cancelled, then it will propagate + // to context2. The two tshark processes will wait on context2.Done, and complete - + // then their defer blocks will send procDoneChan messages. When the count hits 2, this + // select block will exit. Note that we also issue a cancel if count==2 because it might + // just be that the tshark processes finish normally - then we need to stop the other + // goroutines using ctxt2. + case <-c.stage2GoroutineDoneChan: + procsDoneCount++ + if procsDoneCount == 2 { + intStage2CancelFn() // stop the ticker + break L + } + } + } + + // Wait for all other goroutines to complete + c.stage2Wg.Wait() + + // Safe, in goroutine thread + c.RowCurrentlyLoading = -1 + + c.signalStage2Done(cb) + }() + + // + // Goroutine to set mapping between table rows and frame numbers + // + termshark.TrackedGo(func() { + select { + case <-c.StartStage2Chan: + break + case <-c.stage2Ctx.Done(): + setCancelled() + return + case <-intStage2Ctx.Done(): + return // shutdown signalled - don't start the pdml/pcap processes + } + + // Do this - but if we're cancelled first (stage2Ctx.Done), then they + // don't need to be signalled because the other selects waiting on these + // channels will be cancelled too. + defer func() { + // Signal the pdml and pcap reader to start. + for _, ch := range []chan struct{}{c.startPdmlChan, c.startPcapChan} { + select { + case <-ch: // it will be closed if the psml has loaded already, and this e.g. a cached load + default: + close(ch) + } + } + }() + + // If there's no filter, psml, pdml and pcap run concurrently for speed. Therefore the pdml and pcap + // don't know how large the psml will be. So we set numToRead to 1000. This might be too high, but + // we only use this to determine when we can kill the reading processes early. The result will be + // correct if we don't kill the processes, it just might load for longer. + c.KillAfterReadingThisMany = 1000 + var err error + if c.displayFilter == "" { + sidx = row + 1 + // +1 for frame.number being 1-based; +1 to read past the end so that + // the XML decoder doesn't stall and I can kill after 1000 + eidx = row + 1000 + 1 + 1 + } else { + c.Lock() + if len(c.PacketPsmlData) > row { + sidx, err = strconv.Atoi(c.PacketPsmlData[row][0]) + if err != nil { + log.Fatal(err) + } + if len(c.PacketPsmlData) > row+1000+1 { + // If we have enough packets to request one more than the amount to + // cache, then requesting one more will mean the XML decoder won't + // block at packet 999 waiting for - so this is a hack to + // let me promptly kill tshark when I've read enough. + eidx, err = strconv.Atoi(c.PacketPsmlData[row+1000+1][0]) + if err != nil { + log.Fatal(err) + } + } else { + eidx, err = strconv.Atoi(c.PacketPsmlData[len(c.PacketPsmlData)-1][0]) + if err != nil { + log.Fatal(err) + } + eidx += 1 // beyond end of last frame + c.KillAfterReadingThisMany = len(c.PacketPsmlData) - row + } + } + c.Unlock() + } + + if c.displayFilter != "" { + displayFilterStr = fmt.Sprintf("(%s) and (frame.number >= %d) and (frame.number < %d)", c.displayFilter, sidx, eidx) + } else { + displayFilterStr = fmt.Sprintf("(frame.number >= %d) and (frame.number < %d)", sidx, eidx) + } + + }, &c.stage2Wg, Goroutinewg) + + //====================================================================== + + // + // Goroutine to run pdml process + // + termshark.TrackedGo(func() { + defer func() { + c.stage2GoroutineDoneChan <- struct{}{} + }() + + // Wait for stage 2 to be kicked off (potentially by psml load, then mapping table row to frame num); or + // quit if that happens first + select { + case <-c.startPdmlChan: + case <-c.stage2Ctx.Done(): + setCancelled() + return + case <-intStage2Ctx.Done(): + return + } + + c.PdmlCmd = c.cmds.Pdml(c.PcapPdml, displayFilterStr) + + pdmlOut, err := c.PdmlCmd.StdoutPipe() + if err != nil { + handleError(err, cb) + return + } + + log.Infof("Starting PDML command: %v", c.PdmlCmd) + + err = c.PdmlCmd.Start() + if err != nil { + err = fmt.Errorf("Error starting PDML process %v: %v", c.PdmlCmd, err) + handleError(err, cb) + return + } + + defer func() { + c.PdmlCmd.Wait() + }() + + d := xml.NewDecoder(pdmlOut) + packets := make([]*PdmlPacket, 0, 1000) + issuedKill := false + Loop: + for { + tok, err := d.Token() + if err != nil { + if unexpectedError(err) { + err = fmt.Errorf("Could not read PDML data: %v", err) + handleError(err, cb) + } + break + } + switch tok := tok.(type) { + case xml.StartElement: + switch tok.Name.Local { + case "packet": + var packet PdmlPacket + err := d.DecodeElement(&packet, &tok) + if err != nil { + if !issuedKill && unexpectedError(err) { + err = fmt.Errorf("Could not decode PDML data: %v", err) + handleError(err, cb) + } + break Loop + } + packets = append(packets, &packet) + c.updateCacheEntryWithPdml(row, packets, false) + //if len(pdml2) == 1000 { + if len(packets) == c.KillAfterReadingThisMany { + // Shortcut - we never take more than 1000 - so just kill here + issuedKill = true + err = termshark.KillIfPossible(c.PdmlCmd) + if err != nil { + log.Infof("Did not kill pdml process: %v", err) + } + } + } + + } + + } + + // Want to preserve invariant - for simplicity - that we only add full loads + // to the cache + cancelled := atomic.LoadInt32(&stageIsCancelled) + if cancelled == 0 { + // never evict row 0 + c.PacketCache.Get(0) + if c.highestCachedRow != -1 { + // try not to evict "end" + c.PacketCache.Get(c.highestCachedRow) + } + + c.updateCacheEntryWithPdml(row, packets, !c.ReadingFromFifo()) + if row > c.highestCachedRow { + c.highestCachedRow = row + } + } + }, &c.stage2Wg, Goroutinewg) + + //====================================================================== + + // + // Goroutine to run pcap process + // + termshark.TrackedGo(func() { + defer func() { + c.stage2GoroutineDoneChan <- struct{}{} + }() + + // Wait for stage 2 to be kicked off (potentially by psml load, then mapping table row to frame num); or + // quit if that happens first + select { + case <-c.startPcapChan: + case <-c.stage2Ctx.Done(): + setCancelled() + return + case <-intStage2Ctx.Done(): + return + } + + c.PcapCmd = c.cmds.Pcap(c.PcapPcap, displayFilterStr) + + pcapOut, err := c.PcapCmd.StdoutPipe() + if err != nil { + handleError(err, cb) + return + } + + log.Infof("Starting pcap command: %v", c.PcapCmd) + + err = c.PcapCmd.Start() + if err != nil { + // e.g. on the pi + err = fmt.Errorf("Error starting PCAP process %v: %v", c.PcapCmd, err) + handleError(err, cb) + return + } + + defer func() { + c.PcapCmd.Wait() + }() + + packets := make([][]byte, 0, 1000) + + globalHdr := [24]byte{} + pktHdr := [16]byte{} + + _, err = io.ReadFull(pcapOut, globalHdr[:]) + if err != nil { + if unexpectedError(err) { + err = fmt.Errorf("Could not read PCAP header: %v", err) + handleError(err, cb) + } + return + } + + issuedKill := false + + for { + _, err = io.ReadFull(pcapOut, pktHdr[:]) + if err != nil { + if unexpectedError(err) { + err = fmt.Errorf("Could not read PCAP packet header: %v", err) + handleError(err, cb) + } + break + } + + var value uint32 + value |= uint32(pktHdr[8]) + value |= uint32(pktHdr[9]) << 8 + value |= uint32(pktHdr[10]) << 16 + value |= uint32(pktHdr[11]) << 24 + + packet := make([]byte, int(value)) + _, err = io.ReadFull(pcapOut, packet) + if err != nil { + if !issuedKill && unexpectedError(err) { + err = fmt.Errorf("Could not read PCAP packet: %v", err) + handleError(err, cb) + } + break + } + packets = append(packets, packet) + readEnough := (len(packets) >= c.KillAfterReadingThisMany) + c.updateCacheEntryWithPcap(row, packets, false) + + if readEnough { + // Shortcut - we never take more than 1000 - so just kill here + issuedKill = true + err = termshark.KillIfPossible(c.PcapCmd) + if err != nil { + log.Infof("Did not kill pdml process: %v", err) + } + } + } + + // I just want to ensure I read it from ram, obviously this is racey + cancelled := atomic.LoadInt32(&stageIsCancelled) + if cancelled == 0 { + // never evict row 0 + c.PacketCache.Get(0) + if c.highestCachedRow != -1 { + // try not to evict "end" + c.PacketCache.Get(c.highestCachedRow) + } + c.updateCacheEntryWithPcap(row, packets, !c.ReadingFromFifo()) + } + + }, &c.stage2Wg, Goroutinewg) + + // + // Goroutine to track an external shutdown - kills processes i case the external + // shutdown comes first. If it's an internal shutdown, no need to kill because + // that would only be triggered once processes are dead + // + termshark.TrackedGo(func() { + select { + case <-c.stage2Ctx.Done(): + setCancelled() + err := termshark.KillIfPossible(c.PcapCmd) + if err != nil { + log.Infof("Did not kill pcap process: %v", err) + } + err = termshark.KillIfPossible(c.PdmlCmd) + if err != nil { + log.Infof("Did not kill pdml process: %v", err) + } + case <-intStage2Ctx.Done(): + } + }, Goroutinewg) +} + +func (c *Loader) turnOffPipe() { + // Switch over to the temp pcap file. If a new filter is applied + // after stopping, we should read from the temp file and not the fifo + // because nothing will be feeding the fifo. + c.PcapPsml = c.PcapPdml +} + +func (c *Loader) signalPsmlStarting(cb interface{}) { + handleBegin(cb) +} + +func (c *Loader) signalPsmlDone(cb interface{}) { + ch := c.PsmlFinishedChan + c.PsmlFinishedChan = make(chan struct{}) + if ae, ok := cb.(IAfterEnd); ok { + ae.AfterEnd(ch) + <-ch // wait for the channel to close, which AfterEnd should do + } +} + +func (c *Loader) loadPsmlAsync(cb interface{}) { + // Used to cancel the tickers below which update list widgets with the latest data and + // update the progress meter. Note that if ctx is cancelled, then this context is cancelled + // too. When the 2/3 data loading processes are done, a goroutine will then run uiCtxCancel() + // to stop the UI updates. + + c.psmlCtx, c.psmlCancelFn = context.WithCancel(c.mainCtx) + + intPsmlCtx, intPsmlCancelFn := context.WithCancel(context.Background()) + + // signalling psml done to the goroutine that started + + //====================================================================== + + // Make sure data is cleared before we signal we're starting. This gives callbacks a clean + // view, not the old view of a loader with old data. + c.Lock() + c.PacketPsmlData = make([][]string, 0) + c.PacketPsmlHeaders = make([]string, 0, 10) + c.Unlock() + + c.PacketCache.Purge() + c.LoadWasCancelled = false + c.StartStage2Chan = make(chan struct{}) // do this before signalling start + + // signal to updater that we're about to start. This will block until cb completes + c.signalPsmlStarting(cb) + + defer func() { + c.signalPsmlDone(cb) + }() + + //====================================================================== + + var psmlOut io.ReadCloser + + // Only start this process if we are in interface mode + var err error + var pr *os.File + var pw *os.File + + //====================================================================== + + // Make sure we start the goroutine that monitors for shutdown early - so if/when + // a shutdown happens, and we get blocked in the XML parser, this will be able to + // respond + + termshark.TrackedGo(func() { + select { + case <-c.psmlCtx.Done(): + intPsmlCancelFn() // start internal shutdown + case <-intPsmlCtx.Done(): + } + + if c.tailCmd != nil { + err := termshark.KillIfPossible(c.tailCmd) + if err != nil { + log.Infof("Did not kill tail process: %v", err) + } + } + + if c.PsmlCmd != nil { + err := termshark.KillIfPossible(c.PsmlCmd) + if err != nil { + log.Infof("Did not kill psml process: %v", err) + } + } + + if psmlOut != nil { + psmlOut.Close() // explicitly close else this goroutine can block + } + + }, Goroutinewg) + + //====================================================================== + + // Set to true by a goroutine started within here if ctxCancel() is called i.e. the outer context + if c.displayFilter == "" || c.ReadingFromFifo() { + // don't hold up pdml and pcap generators. If the filter is "", then the frame numbers + // equal the row numbers, so we don't need the psml to map from row -> frame. + // + // And, if we are in interface mode, we won't reach the end of the psml anyway. + // + close(c.StartStage2Chan) + } + + //====================================================================== + + if c.ReadingFromFifo() { + // PcapPsml will be nil if here + pr, pw, err = os.Pipe() + if err != nil { + err = fmt.Errorf("Could not create pipe: %v", err) + handleError(err, cb) + intPsmlCancelFn() + return + } + // pw is used as Stdout for the tail command, which unwinds in this + // goroutine - so we can close at this point in the unwinding. pr + // is used as stdin for the psml command, which also runs in this + // goroutine. + defer func() { + pw.Close() + pr.Close() + }() + c.PcapPsml = pr + } + + c.Lock() + c.PsmlCmd = c.cmds.Psml(c.PcapPsml, c.displayFilter) + c.Unlock() + + psmlOut, err = c.PsmlCmd.StdoutPipe() + if err != nil { + err = fmt.Errorf("Could not access pipe output: %v", err) + handleError(err, cb) + intPsmlCancelFn() + return + } + + log.Infof("Starting PSML command: %v", c.PsmlCmd) + + err = c.PsmlCmd.Start() + if err != nil { + err = fmt.Errorf("Error starting PSML command %v: %v", c.PsmlCmd, err) + handleError(err, cb) + intPsmlCancelFn() + return + } + + defer func() { + c.PsmlCmd.Wait() + }() + + //====================================================================== + + // If it was cancelled, then we don't need to start the tail process because + // psml will read from the tmp pcap file generated by the interface reading + // process. + + c.tailCmd = nil + + if c.ReadingFromFifo() { + c.tailCmd = c.cmds.Tail(c.ifaceFile) + c.tailCmd.SetStdout(pw) + + watcher, err := fsnotify.NewWatcher() + if err != nil { + err = fmt.Errorf("Could not create FS watch: %v", err) + handleError(err, cb) + intPsmlCancelFn() + return + } + defer watcher.Close() + + if err := watcher.Add(c.ifaceFile); err != nil { //&& !os.IsNotExist(err) { + err = fmt.Errorf("Could not set up watcher for %s: %v", c.ifaceFile, err) + handleError(err, cb) + intPsmlCancelFn() + return + } else { + // If it's there, touch it so watcher below is notified that everything is in order + if _, err := os.Stat(c.ifaceFile); err == nil { + if err = os.Chtimes(c.ifaceFile, time.Now(), time.Now()); err != nil { + handleError(err, cb) + intPsmlCancelFn() + return + } + } + + } + + defer func() { + watcher.Remove(c.ifaceFile) + }() + + Loop: + for { + timer := time.NewTimer(10 * time.Second) + defer timer.Stop() + + select { + case <-watcher.Events: + break Loop + case err := <-watcher.Errors: + err = fmt.Errorf("Unexpected watcher error for %s: %v", c.ifaceFile, err) + handleError(err, cb) + intPsmlCancelFn() + return + case <-timer.C: + err = fmt.Errorf("Giving up waiting for %s: %v", c.ifaceFile, err) + handleError(err, cb) + intPsmlCancelFn() + return + } + } + + err = c.tailCmd.Start() + if err != nil { + err = fmt.Errorf("Could not start tail command %v: %v", c.tailCmd, err) + handleError(err, cb) + intPsmlCancelFn() + return + } + + // Do this in a goroutine - in a defer, it would block here before the code executes + defer func() { + c.tailCmd.Wait() // this will block the exit of this function until the command is killed + }() + } + + //====================================================================== + + // + // Goroutine to read psml xml and update data structures + // + defer func() { + select { + case <-c.StartStage2Chan: + // already done/closed, do nothing + default: + close(c.StartStage2Chan) + } + + // This will kill the tail process if there is one + intPsmlCancelFn() // stop the ticker + }() + + d := xml.NewDecoder(psmlOut) + + // + //
1
+ //
0.000000
+ //
192.168.44.123
+ //
192.168.44.213
+ //
TFTP
+ //
77
+ //
Read Request, File: C:\IBMTCPIP\lccm.1, Transfer type: octet
+ //
+ + var curPsml []string + ready := false + empty := true + structure := false + for { + if intPsmlCtx.Err() != nil { + break + } + tok, err := d.Token() + if err != nil { + if err != io.EOF && !c.LoadWasCancelled { + err = fmt.Errorf("Could not read PSML data: %v", err) + handleError(err, cb) + } + break + } + switch tok := tok.(type) { + case xml.EndElement: + switch tok.Name.Local { + case "structure": + structure = false + case "packet": + c.Lock() + c.PacketPsmlData = append(c.PacketPsmlData, curPsml) + c.Unlock() + + case "section": + ready = false + // Means we got without any char data i.e. empty
+ if empty { + curPsml = append(curPsml, "") + } + } + case xml.StartElement: + switch tok.Name.Local { + case "structure": + structure = true + case "packet": + curPsml = make([]string, 0, 10) + case "section": + ready = true + empty = true + } + case xml.CharData: + if ready { + if structure { + c.Lock() + c.PacketPsmlHeaders = append(c.PacketPsmlHeaders, string(tok)) + c.Unlock() + } else { + if line, err := strconv.Unquote("\"" + string(tok) + "\""); err == nil { + curPsml = append(curPsml, line) + } else { + curPsml = append(curPsml, string(tok)) + } + empty = false + } + } + } + } +} + +func (c *Loader) loadIfaceAsync(cb interface{}) { + c.ifaceCtx, c.ifaceCancelFn = context.WithCancel(c.mainCtx) + + defer func() { + ch := c.IfaceFinishedChan + c.IfaceFinishedChan = make(chan struct{}) + close(ch) + }() + + c.ifaceCmd = c.cmds.Iface(c.iface, c.captureFilter, c.ifaceFile) + + err := c.ifaceCmd.Start() + if err != nil { + err = fmt.Errorf("Error starting interface reader %v: %v", c.ifaceCmd, err) + handleError(err, cb) + return + } + + termshark.TrackedGo(func() { + // Wait for external cancellation. This is the shutdown procedure. + <-c.ifaceCtx.Done() + err := termshark.KillIfPossible(c.ifaceCmd) + if err != nil { + log.Infof("Did not kill iface reader process: %v", err) + } + + }, Goroutinewg) + + c.ifaceCmd.Wait() // it definitely started, so we must wait + // If something killed it, then start the internal shutdown procedure anyway to clean up + // goroutines waiting on the context. + c.ifaceCancelFn() +} + +//====================================================================== + +type PdmlPacket struct { + XMLName xml.Name `xml:"packet"` + Content []byte `xml:",innerxml"` +} + +type CacheEntry struct { + Pdml []*PdmlPacket + Pcap [][]byte + PdmlComplete bool + PcapComplete bool +} + +func (c CacheEntry) Complete() bool { + return c.PdmlComplete && c.PcapComplete +} + +//====================================================================== + +type LoadPcapSlice struct { + Row int + Cancel bool +} + +func (m *LoadPcapSlice) String() string { + if m.Cancel { + return fmt.Sprintf("[loadslice: %d, cancel: %v]", m.Row, m.Cancel) + } else { + return fmt.Sprintf("[loadslice: %d]", m.Row) + } +} + +//====================================================================== + +type ICacheUpdater interface { + WhenLoadingPdml() + WhenNotLoadingPdml() +} + +type ICacheLoader interface { + State() LoaderState + SetState(LoaderState) + loadIsNecessary(ev LoadPcapSlice) bool + stopLoadPdml() + startLoadPdml(int, interface{}) +} + +func ProcessPdmlRequests(requests []LoadPcapSlice, loader ICacheLoader, updater ICacheUpdater) []LoadPcapSlice { +Loop: + for { + if len(requests) == 0 { + break + } else { + ev := requests[0] + + if loader.loadIsNecessary(ev) { + if loader.State()&LoadingPdml != 0 { + // we are loading a piece. Do we need to cancel? If not, reschedule for when idle + if ev.Cancel { + loader.stopLoadPdml() + } + updater.WhenNotLoadingPdml() + } else { + loader.startLoadPdml(ev.Row, updater) + loader.SetState(loader.State() | LoadingPdml) + updater.WhenLoadingPdml() + } + break Loop + } else { + requests = requests[1:] + } + } + } + return requests +} + +//====================================================================== +// Local Variables: +// mode: Go +// fill-column: 78 +// End: diff --git a/pcap/loader_test.go b/pcap/loader_test.go new file mode 100644 index 0000000..6a83dcc --- /dev/null +++ b/pcap/loader_test.go @@ -0,0 +1,644 @@ +// Copyright 2019 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license +// that can be found in the LICENSE file. + +package pcap + +import ( + "bytes" + "context" + "fmt" + "io" + "io/ioutil" + "os" + "strings" + "sync" + "testing" + "time" + + "net/http" + _ "net/http" + _ "net/http/pprof" + + "github.com/gcla/termshark" + "github.com/stretchr/testify/assert" + + log "github.com/sirupsen/logrus" +) + +//====================================================================== + +var ensureGoroutinesStopWG sync.WaitGroup + +func init() { + go func() { + log.Println(http.ListenAndServe("0.0.0.0:6060", nil)) + }() + + Goroutinewg = &ensureGoroutinesStopWG +} + +//====================================================================== + +type pdmlAction struct{} + +func newPdmlAction() *pdmlAction { + return &pdmlAction{} +} + +func (p *pdmlAction) WhenLoadingPdml() { + fmt.Printf("FURTHER ACTION: when loading pdml\n") +} +func (p *pdmlAction) WhenNotLoadingPdml() { + fmt.Printf("FURTHER ACTION: when not loading pdml\n") +} + +//====================================================================== + +type iGoProc interface { + StopGR() +} + +type procReader struct { + io.ReadCloser + iGoProc +} + +var _ io.Reader = (*procReader)(nil) + +func (r *procReader) Read(p []byte) (int, error) { + n, err := r.ReadCloser.Read(p) + //fmt.Printf("Read result %d, %v\n", n, err) + if err != nil { + r.StopGR() + } + return n, err +} + +//====================================================================== + +type iStopLoop interface { + shouldStop() error +} + +type bodyMakerFn func() io.ReadCloser + +type loopReader struct { + maker bodyMakerFn + loops int + stopper iStopLoop + body io.ReadCloser + numdone int +} + +var _ io.Reader = (*loopReader)(nil) + +func newLoopReader(maker bodyMakerFn, loops int, stopper iStopLoop) *loopReader { + res := &loopReader{ + maker: maker, + loops: loops, + stopper: stopper, + } + res.body = maker() + return res +} + +func (r *loopReader) Read(p []byte) (int, error) { + read := 0 + for read < len(p) { + if r.numdone == r.loops { + return read, io.EOF + } + req := len(p[read:]) + n, err := r.body.Read(p[read:]) + read += n + if err != nil { + if err != io.EOF { + return read, err + } + r.numdone += 1 // EOF + r.body.Close() + r.body = r.maker() + if r.stopper != nil { + err = r.stopper.shouldStop() + if err != nil { + return read, err + } + } + } else if n < req { + break + } + } + + return read, nil +} + +//====================================================================== + +// Implements io.Reader - combines header, looping body and footer from disk +type pcapLoopReader struct { + io.Reader + loops int +} + +var _ io.Reader = (*pcapLoopReader)(nil) + +func newPcapLoopReader(prefix string, suffix string, loops int, stopper iStopLoop) *pcapLoopReader { + looper := newLoopReader(func() io.ReadCloser { + file, err := os.Open(fmt.Sprintf("testdata/%s.%s-body", prefix, suffix)) + if err != nil { + panic(err) + } + return file + }, loops, stopper) + + fileh, err := os.Open(fmt.Sprintf("testdata/%s.%s-header", prefix, suffix)) + if err != nil { + panic(err) + } + filef, err := os.Open(fmt.Sprintf("testdata/%s.%s-footer", prefix, suffix)) + if err != nil { + panic(err) + } + + res := &pcapLoopReader{ + Reader: io.MultiReader(fileh, looper, filef), + loops: loops, + } + + return res +} + +//====================================================================== + +// Provide Tail, Pdml, etc based on files on disk +type procsFromPrefix struct { + prefix string +} + +var _ ILoaderCmds = procsFromPrefix{} + +func makeProcsFromPrefix(pref string) procsFromPrefix { + return procsFromPrefix{prefix: pref} +} + +func (g procsFromPrefix) Iface(iface string, captureFilter string, tmpfile string) IBasicCommand { + panic(fmt.Errorf("Should not need")) +} + +func (g procsFromPrefix) Tail(tmpfile string) ITailCommand { + panic(fmt.Errorf("Should not need")) +} + +func (g procsFromPrefix) Psml(pcap interface{}, filter string) IPcapCommand { + file, err := os.Open(fmt.Sprintf("testdata/%s.psml", g.prefix)) + if err != nil { + panic(err) + } + return newSimpleCmd(file) +} + +func (g procsFromPrefix) Pcap(pcap string, filter string) IPcapCommand { + file, err := os.Open(fmt.Sprintf("testdata/%s.pcap", g.prefix)) + if err != nil { + panic(err) + } + return newSimpleCmd(file) +} + +func (g procsFromPrefix) Pdml(pcap string, filter string) IPcapCommand { + file, err := os.Open(fmt.Sprintf("testdata/%s.pdml", g.prefix)) + if err != nil { + panic(err) + } + return newSimpleCmd(file) +} + +//====================================================================== + +type loopingProcs struct { + prefix string + loops int +} + +var _ ILoaderCmds = loopingProcs{} + +func makeLoopingProcs(pref string, loops int) loopingProcs { + return loopingProcs{prefix: pref, loops: loops} +} + +func (g loopingProcs) Iface(iface string, captureFilter string, tmpfile string) IBasicCommand { + panic(fmt.Errorf("Should not need")) +} + +func (g loopingProcs) Tail(tmpfile string) ITailCommand { + panic(fmt.Errorf("Should not need")) +} + +func (g loopingProcs) Psml(pcap interface{}, filter string) IPcapCommand { + rd := newPcapLoopReader(g.prefix, "psml", g.loops, nil) + return newSimpleCmd(rd) +} + +func (g loopingProcs) Pcap(pcap string, filter string) IPcapCommand { + rd := newPcapLoopReader(g.prefix, "pcap", g.loops, nil) + return newSimpleCmd(rd) +} + +func (g loopingProcs) Pdml(pcap string, filter string) IPcapCommand { + rd := newPcapLoopReader(g.prefix, "pdml", g.loops, nil) + return newSimpleCmd(rd) +} + +//====================================================================== + +// A pretend external command - when started, runs a goroutine that waits until stopped +type simpleCmd struct { + pcap string + filter string + out io.Writer + pipe io.ReadCloser + started bool + dead bool + ctx context.Context // cancels the iface reader process + cancel context.CancelFunc +} + +var _ ICommand = (*simpleCmd)(nil) + +func newSimpleCmd(rd io.Reader) *simpleCmd { + res := &simpleCmd{} + + var rc io.ReadCloser + var ok bool + rc, ok = rd.(io.ReadCloser) + if !ok { + rc = ioutil.NopCloser(rd) + } + + res.pipe = &procReader{ + ReadCloser: rc, + iGoProc: res, + } + + return res +} + +func (f *simpleCmd) StopGR() { + f.cancel() +} + +func (f *simpleCmd) Start() error { + if f.started { + return fmt.Errorf("Started already") + } + if f.dead { + return fmt.Errorf("Started already and dead") + } + f.ctx, f.cancel = context.WithCancel(context.Background()) + termshark.TrackedGo(func() { + select { + case <-f.ctx.Done(): // terminate + } + }, Goroutinewg) + f.started = true + return nil +} + +func (f *simpleCmd) Wait() error { + if !f.started { + return fmt.Errorf("Not started yet") + } + if f.dead { + return fmt.Errorf("Dead already") + } + select { + case <-f.ctx.Done(): + f.dead = true + } + return nil +} + +func (f *simpleCmd) StdoutPipe() (io.ReadCloser, error) { + return f.pipe, nil +} + +func (f *simpleCmd) SetStdout(w io.Writer) { + f.out = w +} + +func (f *simpleCmd) Kill() error { + f.cancel() + return nil +} + +func (f *simpleCmd) Signal(s os.Signal) error { + f.cancel() + return nil +} + +func (f *simpleCmd) Pid() int { + return 1001 +} + +//====================================================================== + +// While tshark processes are running, signal (via close) when AfterEnd is triggered +type waitForEnd struct { + end chan struct{} +} + +var _ IOnError = (*waitForEnd)(nil) +var _ IClear = (*waitForEnd)(nil) +var _ IBeforeBegin = (*waitForEnd)(nil) +var _ IAfterEnd = (*waitForEnd)(nil) + +func newWaitForEnd() *waitForEnd { + return &waitForEnd{ + end: make(chan struct{}), + } +} + +func (p *waitForEnd) BeforeBegin(closeMe chan<- struct{}) { + close(closeMe) +} +func (p *waitForEnd) AfterEnd(closeMe chan<- struct{}) { + close(closeMe) + close(p.end) +} +func (p *waitForEnd) OnClear(closeMe chan<- struct{}) { + close(closeMe) +} +func (p *waitForEnd) OnError(err error, closeMe chan<- struct{}) { + close(closeMe) + panic(err) +} + +//====================================================================== + +type waitForClear struct { + end chan struct{} +} + +var _ IOnError = (*waitForClear)(nil) +var _ IClear = (*waitForClear)(nil) +var _ IBeforeBegin = (*waitForClear)(nil) +var _ IAfterEnd = (*waitForClear)(nil) + +func newWaitForClear() *waitForClear { + return &waitForClear{ + end: make(chan struct{}), + } +} + +func (p *waitForClear) BeforeBegin(closeMe chan<- struct{}) { + close(closeMe) +} +func (p *waitForClear) AfterEnd(closeMe chan<- struct{}) { + close(closeMe) +} +func (p *waitForClear) OnClear(closeMe chan<- struct{}) { + close(p.end) + close(closeMe) +} +func (p *waitForClear) OnError(err error, closeMe chan<- struct{}) { + close(closeMe) + panic(err) +} + +//====================================================================== + +type enabler struct { + val *bool +} + +func (e enabler) EnableOperations() { + *e.val = true +} + +//====================================================================== + +func TestSimpleCmd(t *testing.T) { + p := newSimpleCmd(bytes.NewReader([]byte("hello world"))) + + err := p.Start() + assert.NoError(t, err) + + so, err := p.StdoutPipe() + assert.NoError(t, err) + + read, err := ioutil.ReadAll(so) + assert.NoError(t, err) + assert.Equal(t, "hello world", string(read)) + + err = p.Wait() + assert.NoError(t, err) +} + +func TestLoopReader1(t *testing.T) { + maker := func() io.ReadCloser { + return ioutil.NopCloser(strings.NewReader("hello")) + } + + looper := newLoopReader(maker, 3, nil) + + read, err := ioutil.ReadAll(looper) + assert.NoError(t, err) + assert.Equal(t, "hellohellohello", string(read)) + + looper = newLoopReader(maker, 3, nil) + ball := make([]byte, 0) + b1 := make([]byte, 1) + var n int + + err = nil + for err != io.EOF { + n, err = looper.Read(b1) + if err != io.EOF { + assert.Equal(t, 1, n) + ball = append(ball, b1...) + } + } + + assert.Equal(t, "hellohellohello", string(ball)) +} + +//====================================================================== + +// Load psml+pdml+pcap from testdata/1.pcap, validate the data +func TestSinglePcap(t *testing.T) { + loader := NewPcapLoader(makeProcsFromPrefix("1")) + assert.NotEqual(t, nil, loader) + + // Save now because when psml load finishes, a new one is created + psmlFinChan := loader.PsmlFinishedChan + pdmlFinChan := loader.Stage2FinishedChan + + enabled := false + + updater := struct { + *pdmlAction + *waitForEnd + enabler + }{ + newPdmlAction(), newWaitForEnd(), enabler{&enabled}, + } + done := make(chan struct{}, 1) + loader.doLoadPcapOperation("abc", "def", updater, func() { + close(done) + }) + <-updater.end + <-done + + assert.Equal(t, 18, len(loader.PacketPsmlData)) + assert.Equal(t, "192.168.86.246", loader.PacketPsmlData[0][2]) + + <-psmlFinChan + assert.Equal(t, LoaderState(LoadingPsml), loader.State()) + loader.SetState(loader.State() & ^LoadingPsml) + + // No pdml yet + _, ok := loader.PacketCache.Get(0) + assert.Equal(t, false, ok) + + //further := pdmlAction{} + enabled = false + updater = struct { + *pdmlAction + *waitForEnd + enabler + }{ + newPdmlAction(), newWaitForEnd(), enabler{&enabled}, + } + instructions := []LoadPcapSlice{{0, false}} + + instructionsAfter := ProcessPdmlRequests(instructions, loader, updater) + assert.Equal(t, LoaderState(LoadingPdml), loader.State()) + assert.Equal(t, 1, len(instructionsAfter)) // not done yet - need to get to right state + + instructionsAfter = ProcessPdmlRequests(instructions, loader, updater) + <-pdmlFinChan + assert.Equal(t, LoaderState(LoadingPdml), loader.State()) + loader.SetState(loader.State() & ^LoadingPdml) + assert.Equal(t, 0, len(instructionsAfter)) + + cei, ok := loader.PacketCache.Get(0) + assert.Equal(t, true, ok) + ce := cei.(CacheEntry) + assert.Equal(t, true, ce.PdmlComplete) + assert.Equal(t, true, ce.PcapComplete) + assert.Equal(t, 18, len(ce.Pdml)) + assert.Equal(t, 18, len(ce.Pcap)) +} + +func TestLoopingPcap(t *testing.T) { + for i, loops := range []int{1, 5, 100} { + // The "2" loads up testdata/2.{psml,pdml,pcap}-{header,body,footer} + loader := NewPcapLoader(makeLoopingProcs("2", loops)) + // Make sure we can re-use the same loader, because that's what termshark does + for j, _ := range []int{1, 2} { + assert.NotEqual(t, nil, loader) + + // Save now because when psml load finishes, a new one is created + psmlFinChan := loader.PsmlFinishedChan + pdmlFinChan := loader.Stage2FinishedChan + + // make sure each time round it tries to load a "new" pcap - otherwise the loader + // returns early, and this test is set up to wait until we get the AfterEnd signal + fakePcap := fmt.Sprintf("%d-%d", i, j) + + enabled := false + updater := struct { + *pdmlAction + *waitForEnd + enabler + }{ + newPdmlAction(), newWaitForEnd(), enabler{&enabled}, + } + done := make(chan struct{}, 1) + loader.doLoadPcapOperation(fakePcap, "def", updater, func() { + close(done) + }) + <-updater.end + <-done + + assert.Equal(t, loops, len(loader.PacketPsmlData)) + assert.Equal(t, "192.168.44.123", loader.PacketPsmlData[0][2]) + + <-psmlFinChan + assert.Equal(t, LoaderState(LoadingPsml), loader.State()) + loader.SetState(loader.State() & ^LoadingPsml) + + // No pdml yet + _, ok := loader.PacketCache.Get(0) + assert.Equal(t, false, ok) + + updater = struct { + *pdmlAction + *waitForEnd + enabler + }{ + newPdmlAction(), newWaitForEnd(), enabler{&enabled}, + } + instructions := []LoadPcapSlice{{0, false}} + + instructionsAfter := ProcessPdmlRequests(instructions, loader, updater) + assert.Equal(t, LoaderState(LoadingPdml), loader.State()) + assert.Equal(t, 1, len(instructionsAfter)) // not done yet - need to get to right state + + instructionsAfter = ProcessPdmlRequests(instructions, loader, updater) + <-pdmlFinChan + assert.Equal(t, LoaderState(LoadingPdml), loader.State()) + loader.SetState(loader.State() & ^LoadingPdml) + assert.Equal(t, 0, len(instructionsAfter)) + + cei, ok := loader.PacketCache.Get(0) + assert.Equal(t, true, ok) + ce := cei.(CacheEntry) + assert.Equal(t, true, ce.PdmlComplete) + assert.Equal(t, true, ce.PcapComplete) + assert.Equal(t, loops, len(ce.Pdml)) + assert.Equal(t, loops, len(ce.Pcap)) + assert.Equal(t, loader.State(), LoaderState(0)) + + fmt.Printf("about to clear\n") + done = make(chan struct{}, 1) + waitForClear := newWaitForClear() + loader.doClearPcapOperation(waitForClear, func() { + close(done) + }) + <-done + + assert.Equal(t, loader.State(), LoaderState(0)) + <-waitForClear.end + + assert.Equal(t, 0, len(loader.PacketPsmlData)) + + _, ok = loader.PacketCache.Get(0) + assert.Equal(t, false, ok) + } + } +} + +//====================================================================== + +func TestKeepThisLast(t *testing.T) { + fmt.Printf("Waiting for test goroutines to stop\n") + done := make(chan struct{}) + go func() { + select { + case <-done: + return + case <-time.After(10 * time.Second): + assert.FailNow(t, "Not all test goroutines terminated in 10s") + } + }() + Goroutinewg.Wait() + close(done) + fmt.Printf("Done waiting for test goroutines to stop\n") +} + +//====================================================================== +// Local Variables: +// mode: Go +// fill-column: 78 +// End: diff --git a/pcap/loader_tshark_test.go b/pcap/loader_tshark_test.go new file mode 100644 index 0000000..6a4d523 --- /dev/null +++ b/pcap/loader_tshark_test.go @@ -0,0 +1,566 @@ +// Copyright 2019 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license +// that can be found in the LICENSE file. + +// +build tshark + +package pcap + +import ( + "bytes" + "fmt" + "io" + "os" + "regexp" + "strings" + "sync" + "testing" + "time" + + "github.com/gcla/termshark" + + "github.com/stretchr/testify/assert" +) + +//====================================================================== + +var ensureGoroutinesStopWG2 sync.WaitGroup + +func init() { + Goroutinewg = &ensureGoroutinesStopWG2 +} + +//====================================================================== + +// Test using same commands that termshark uses - load 1.pcap. Also tests re-use of a loader. +func TestRealProcs(t *testing.T) { + loader := NewPcapLoader(Commands{}) + // Make sure we can re-use the same loader, because that's what termshark does + for _, _ = range []int{1, 2, 3} { + assert.NotEqual(t, nil, loader) + + // Save now because when psml load finishes, a new one is created + psmlFinChan := loader.PsmlFinishedChan + pdmlFinChan := loader.Stage2FinishedChan + + fmt.Printf("about to load real pcap\n") + updater := struct { + *pdmlAction + *waitForEnd + //*whenIdler + }{ + newPdmlAction(), newWaitForEnd(), + } + loader.doLoadPcapOperation("testdata/1.pcap", "", updater, func() {}) + + <-updater.end + fmt.Printf("done loading real pcap\n") + + assert.Equal(t, 18, len(loader.PacketPsmlData)) + assert.Equal(t, "192.168.86.246", loader.PacketPsmlData[0][2]) + + <-psmlFinChan + assert.Equal(t, LoaderState(LoadingPsml), loader.State()) + loader.SetState(loader.State() & ^LoadingPsml) + + // No pdml yet + _, ok := loader.PacketCache.Get(0) + assert.Equal(t, false, ok) + + updater = struct { + *pdmlAction + *waitForEnd + }{ + newPdmlAction(), newWaitForEnd(), + } + instructions := []LoadPcapSlice{{0, false}} + + // Won't work yet because state needs to be LoadingPdml - so call again below + instructionsAfter := ProcessPdmlRequests(instructions, loader, updater) + assert.Equal(t, LoaderState(LoadingPdml), loader.State()) + assert.Equal(t, 1, len(instructionsAfter)) // not done yet - need to get to right state + + // Load first 1000 rows of pcap as pdml+pcap + instructionsAfter = ProcessPdmlRequests(instructions, loader, updater) + <-pdmlFinChan + assert.Equal(t, LoaderState(LoadingPdml), loader.State()) + loader.SetState(loader.State() & ^LoadingPdml) // manually reset state, termshark handles this + assert.Equal(t, 0, len(instructionsAfter)) + + cei, ok := loader.PacketCache.Get(0) + assert.Equal(t, true, ok) + ce := cei.(CacheEntry) + assert.Equal(t, true, ce.PdmlComplete) + assert.Equal(t, true, ce.PcapComplete) + assert.Equal(t, 18, len(ce.Pdml)) + assert.Equal(t, 18, len(ce.Pcap)) + + assert.Equal(t, loader.State(), LoaderState(0)) + + // Now clear for next run + fmt.Printf("ABOUT TO CLEAR\n") + waitForClear := newWaitForClear() + loader.doClearPcapOperation(waitForClear, func() {}) + + assert.Equal(t, loader.State(), LoaderState(0)) + // for _, fn := range waitForClear.idle { + // fn() + // } + <-waitForClear.end + + _, ok = loader.PacketCache.Get(0) + assert.Equal(t, false, ok) + + // So that the next run isn't rejected for being the same + fmt.Printf("clearing filename state\n") + loader.pcap = "" + loader.displayFilter = "" + } +} + +//====================================================================== + +// an io.Reader that will never hit EOF and will provide data like reading from an interface +type pcapLooper struct { + io.Reader +} + +var _ io.Reader = (*pcapLooper)(nil) + +func newPcapLooper(prefix string, suffix string, stopper iStopLoop) *pcapLooper { + looper := newLoopReader(func() io.ReadCloser { + file, err := os.Open(fmt.Sprintf("testdata/%s.%s-body", prefix, suffix)) + if err != nil { + panic(err) + } + return file + }, 100000, stopper) + + fileh, err := os.Open(fmt.Sprintf("testdata/%s.%s-header", prefix, suffix)) + if err != nil { + panic(err) + } + + res := &pcapLooper{ + Reader: io.MultiReader(fileh, looper), + } + + return res +} + +//====================================================================== + +var hdr []byte = []byte{ + 0xd4, 0xc3, 0xb2, 0xa1, 0x02, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, +} + +var pkt []byte = []byte{ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x39, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, + // + 0x30, 0xfd, 0x38, 0xd2, 0x76, 0x12, 0xe8, 0xde, + 0x27, 0x19, 0xde, 0x6c, 0x08, 0x00, 0x45, 0x00, + 0x00, 0x2b, 0x93, 0x80, 0x40, 0x00, 0x40, 0x11, + 0xdc, 0x5d, 0xc0, 0xa8, 0x56, 0xf6, 0x45, 0xae, + 0x00, 0x00, + 0xb6, 0x87, + 0x22, 0x61, 0x00, 0x17, + 0xb0, 0xd4, 0x05, 0x0a, 0x06, 0xae, 0x1a, 0xae, + 0x1a, 0xae, 0x1a, 0xae, 0x1a, 0xae, 0x1a, 0xae, + 0x1a, +} + +type portfn func() int + +type hackedPacket struct { + idx int + port portfn + actual io.Reader + stopper iStopLoop +} + +var _ io.Reader = (*hackedPacket)(nil) + +func (r *hackedPacket) Read(p []byte) (int, error) { + if r.actual == nil { + if r.stopper != nil { + err := r.stopper.shouldStop() + if err != nil { + return 0, err + } + } + + data := []byte(pkt) + p := r.port() + data[r.idx+1] = byte(p & 0xff) + data[r.idx+0] = byte((p & 0xff00) >> 8) + //r.actual = strings.NewReader(string(data)) + r.actual = bytes.NewReader(data) + } + return r.actual.Read(p) +} + +func newPortLooper(pfn portfn, stopper iStopLoop) io.Reader { + readers := make([]io.Reader, 65536) + for i := 0; i < len(readers); i++ { + readers[i] = &hackedPacket{idx: 34 + 16, port: pfn, stopper: stopper} + } + readers = append([]io.Reader{strings.NewReader(string(hdr))}, readers...) + return io.MultiReader(readers...) +} + +//====================================================================== + +type fakeIfaceCmd struct { + *simpleCmd + tmpfile string + input io.Reader +} + +var _ IBasicCommand = (*fakeIfaceCmd)(nil) + +func newLoopingIfaceCmd(prefix string, tmpfile string, stopper iStopLoop) *fakeIfaceCmd { + return &fakeIfaceCmd{ + simpleCmd: newSimpleCmd(strings.NewReader("")), + tmpfile: tmpfile, + input: newPcapLooper(prefix, "pcap", stopper), // loop forever until stopper signals to end + } +} + +func newHackedIfaceCmd(pfn portfn, tmpfile string, stopper iStopLoop) *fakeIfaceCmd { + return &fakeIfaceCmd{ + simpleCmd: newSimpleCmd(strings.NewReader("")), + tmpfile: tmpfile, + input: newPortLooper(pfn, stopper), // loop forever until stopper signals to end + } +} + +func (f *fakeIfaceCmd) Start() error { + err := f.simpleCmd.Start() + if err != nil { + return err + } + termshark.TrackedGo(func() { + file, err := os.Create(f.tmpfile) + if err != nil { + panic(err) + } + _, err = io.Copy(file, f.input) + if err != nil { + panic(err) + } + }, Goroutinewg) + return nil +} + +//====================================================================== + +type fakeIface struct { + prefix string + stopper iStopLoop +} + +func (f *fakeIface) Iface(iface string, tmpfile string) IBasicCommand { + return newLoopingIfaceCmd(f.prefix, tmpfile, f.stopper) +} + +//====================================================================== + +type hackedIface struct { + stopper iStopLoop + pfn portfn +} + +func (f *hackedIface) Iface(iface string, tmpfile string) IBasicCommand { + return newHackedIfaceCmd(f.pfn, tmpfile, f.stopper) +} + +//====================================================================== + +type IIface interface { + Iface(iface string, tmpfile string) IBasicCommand +} + +type fakeIfaceCommands struct { + fake IIface + Commands +} + +var _ ILoaderCmds = fakeIfaceCommands{} + +func (c fakeIfaceCommands) Iface(iface string, captureFilter string, tmpfile string) IBasicCommand { + return c.fake.Iface(iface, tmpfile) +} + +//====================================================================== + +type chanfn func() <-chan error + +type waitForAnswer struct { + ch chanfn +} + +var _ iStopLoop = (*waitForAnswer)(nil) + +func (s *waitForAnswer) shouldStop() error { + err := <-s.ch() + return err +} + +//====================================================================== + +func TestIface1(t *testing.T) { + answerChan := make(chan error) + getChan := func() <-chan error { + return answerChan + } + + fakeIfaceCmd := &fakeIface{ + prefix: "2", + stopper: &waitForAnswer{ + ch: getChan, + }, + } + loader := NewPcapLoader(fakeIfaceCommands{ + fake: fakeIfaceCmd, + }) + + // Save now because when psml load finishes, a new one is created + psmlFinChan := loader.PsmlFinishedChan + //ifaceFinChan := loader.IfaceFinishedChan + + updater := newWaitForEnd() + fmt.Printf("doing load interface op\n") + ch := make(chan struct{}) + loader.doLoadInterfaceOperation("dummy", "", "", updater, func() { close(ch) }) + <-ch + + fmt.Printf("fake sleep\n") + time.Sleep(1 * time.Second) + + read := 10000 + fmt.Printf("reading %d packets from looper\n", read) + for i := 0; i < read-1; i++ { // otherwise it reads one too many + answerChan <- nil + } + + fmt.Printf("giving processes time to catch up\n") + time.Sleep(2 * time.Second) + + fmt.Printf("stopping iface read\n") + ch = make(chan struct{}) + updater = newWaitForEnd() + loader.doStopLoadOperation(updater, func() { + close(ch) + }) + close(answerChan) + fmt.Printf("waiting for loader to signal end\n") + <-psmlFinChan + + fmt.Printf("done loading interface pcap\n") + + assert.NotEqual(t, 0, len(loader.PacketPsmlData)) + assert.Equal(t, read, len(loader.PacketPsmlData)) + assert.Equal(t, "192.168.44.123", loader.PacketPsmlData[0][2]) + + assert.Equal(t, LoaderState(LoadingPsml|LoadingIface), loader.State()) + loader.SetState(loader.State() & ^(LoadingPsml | LoadingIface)) + + // After SetState call, state should be idle, meaning my channel will be closed at last + <-ch + fmt.Printf("waiting for updater end to signal end\n") + <-updater.end + + // Now clear for next run + fmt.Printf("about to clear\n") + waitForClear := newWaitForClear() + ch = make(chan struct{}) + loader.doClearPcapOperation(waitForClear, func() { close(ch) }) + <-ch + + assert.Equal(t, loader.State(), LoaderState(0)) + // for _, fn := range waitForClear.idle { + // fn() + // } + <-waitForClear.end + + assert.Equal(t, 0, len(loader.PacketPsmlData)) + + // So that the next run isn't rejected for being the same + fmt.Printf("clearing filename state\n") + loader.pcap = "" + loader.displayFilter = "" +} + +func TestIfaceNewFilter(t *testing.T) { + port := 0 + + pfn := func() int { + res := port + port++ + return res + } + + answerChan := make(chan error) + getChan := func() <-chan error { + return answerChan + } + + hackedIfaceCmd := &hackedIface{ + stopper: &waitForAnswer{ + ch: getChan, + }, + pfn: pfn, + } + cmds := fakeIfaceCommands{ + fake: hackedIfaceCmd, + } + loader := NewPcapLoader(cmds) + + // Save now because when psml load finishes, a new one is created + psmlFinChan := loader.PsmlFinishedChan + + filtcount := 1000 + updater := newWaitForEnd() + fmt.Printf("doing load interface op\n") + ch := make(chan struct{}) + loader.doLoadInterfaceOperation("dummy", "", fmt.Sprintf("frame.number <= %d", filtcount), updater, func() { close(ch) }) + <-ch + + fmt.Printf("fake sleep\n") + time.Sleep(1 * time.Second) + + read := 30000 + fmt.Printf("reading %d packets from looper\n", read) + for i := 0; i < read; i++ { + //fmt.Printf("loop 1: sending answerchan for %d\n", i) + answerChan <- nil + //fmt.Printf("loop 1: sending answerchan for %d\n", i) + } + + fmt.Printf("giving processes time to catch up\n") + time.Sleep(2 * time.Second) + + fmt.Printf("stopping iface read\n") + ch = make(chan struct{}) + loader.doStopLoadToIfaceOperation(func() { close(ch) }) + close(answerChan) + + fmt.Printf("waiting for loader to signal end\n") + <-psmlFinChan + + fmt.Printf("done loading interface pcap\n") + + fmt.Printf("num packets was %d\n", len(loader.PacketPsmlData)) + assert.NotEqual(t, 0, len(loader.PacketPsmlData)) + assert.Equal(t, filtcount, len(loader.PacketPsmlData)) + assert.Equal(t, "192.168.86.246", loader.PacketPsmlData[0][2]) + + re, _ := regexp.Compile("^[0-9]+ ") + + // Check the source port is correct for each packet read + for i := 0; i < filtcount; i++ { + s := loader.PacketPsmlData[i][6] + if re.MatchString(s) { // rule out those where tshark converts port to name + pref := fmt.Sprintf("%d", i) + res := strings.HasPrefix(s, pref) + assert.True(t, res) + } + } + + assert.Equal(t, LoaderState(LoadingPsml|LoadingIface), loader.State()) + loader.SetState(loader.State() & ^LoadingPsml) + + // Now SetState called, can get these channel results + fmt.Printf("waiting for updater end to signal end\n") + <-updater.end + <-ch + + // Now reload with new filter + + // Save now because when psml load finishes, a new one is created + psmlFinChan = loader.PsmlFinishedChan + + answerChan = make(chan error) + filtcount = 1000 + port = 0 + updater = newWaitForEnd() + + fmt.Printf("doing load interface op\n") + ch = make(chan struct{}) + loader.doLoadInterfaceOperation("dummy", "", fmt.Sprintf("frame.number > 500 && frame.number <= %d", filtcount+500), updater, func() { close(ch) }) + <-ch + //loader.doLoadInterfaceOperation("dummy", fmt.Sprintf("frame.number <= %d", filtcount+1), gwtest.D, updater) + + fmt.Printf("fake sleep 22\n") + time.Sleep(1 * time.Second) + + // The iface reader doesn't need to read more packets - we are only applying a new filter + + fmt.Printf("loop 2: giving processes time to catch up\n") + time.Sleep(2 * time.Second) + + fmt.Printf("loop 2: stopping iface read\n") + ich := loader.IfaceFinishedChan // save the channel here, because it is reassigned before closing + updater = newWaitForEnd() + ch = make(chan struct{}) + loader.doStopLoadOperation(updater, func() { close(ch) }) + // in case the read is blocked here + close(answerChan) + + fmt.Printf("loop 2: waiting for loader to signal end\n") + <-psmlFinChan + + fmt.Printf("num packets was %d\n", len(loader.PacketPsmlData)) + assert.NotEqual(t, 0, len(loader.PacketPsmlData)) + assert.Equal(t, filtcount, len(loader.PacketPsmlData)) + // assert.Equal(t, "192.168.86.246", loader.PacketPsmlData[0][2]) + + // Check the source port is correct for each packet read + for i := 0; i < filtcount; i++ { + s := loader.PacketPsmlData[i][6] + if re.MatchString(s) { // rule out those where tshark converts port to name + pref := fmt.Sprintf("%d", i+500) + res := strings.HasPrefix(s, pref) + assert.True(t, res) + } + } + + // stop iface + fmt.Printf("waiting for iface to stop\n") + //loader.stopLoadIface() + <-ich + fmt.Printf("iface stopped\n") + + assert.Equal(t, LoaderState(LoadingPsml|LoadingIface), loader.State()) + loader.SetState(0) + + <-ch + fmt.Printf("loop 2: waiting for updater end to signal end\n") + <-updater.end + fmt.Printf("loop 2: done loading interface pcap\n") + + // Now clear and test + fmt.Printf("loop 2: about to clear\n") + waitForClear := newWaitForClear() + ch = make(chan struct{}) + loader.doClearPcapOperation(waitForClear, func() { close(ch) }) + <-ch + + assert.Equal(t, loader.State(), LoaderState(0)) + <-waitForClear.end + + assert.Equal(t, 0, len(loader.PacketPsmlData)) +} + +//====================================================================== + +func TestKeepThisLast2(t *testing.T) { + TestKeepThisLast(t) +} + +//====================================================================== +// Local Variables: +// mode: Go +// fill-column: 78 +// End: diff --git a/pcap/testdata/1.pcap b/pcap/testdata/1.pcap new file mode 100644 index 0000000000000000000000000000000000000000..1b1eab114c6e1a3d2edcd46aa0ca05c01dacff3b GIT binary patch literal 2360 zcmcgtX;4#F7`+c7LTD;b5paDhiba#WKmy4_WQ~A8f)F-w3nYZpKmrL2E+|+9t92Rb zI#iUUT2yK$GmcfTLTg8{*ivOWNKpqDK(sfhd(Z|1!>_nvdU^PTrz z+xNyw3&cSGE(Rj-&He`63a5H&RE)2os3GBve23mUo-TJZ)<}$yzOX+C1zGJ-G=7;l z$e-H1spcwCzqoG8cBk$=2aBAEz(Rk72*R5A-b!k7pvTg3Cre92n`wwoLnjY#Ul=!( zPOld2`;K$We_ej*SyfZx$+nOb=kBA=%dXWGKB68!7|t4=K6pMl<%~iBf)+osMk)*e zWtL)=WfGL){vrtVTL}yx21F6{HCI6l7Ime_8Rg|HH46&#kgWkxAc%Y;=>9gx6Gu*; zWb1lM(w}5rUhdc~(sy^&^a5=frVYUxp(gmiFB~xWP&LknE10^W$c_k1$H!j#up6iW z(fVg5l4CTe35fCT(*UvJ6d=xFg$S$qi28DU7!i2A^~WM-MhU;djJSELgxbq_=mCvP zUuMA|pU*lh-f=kKSc&28Gh=q=w3uCMJWL;N0@bZ9yp6sUifiKq`=NWO~^swU;aTzXm#er z>C6tLIR|y^4q_WgB$aQ(IryO2#XOIl%~6h$+HD5N!7TceYe7On`@zoAp_t<b+(F3h|M;IlZ;VktKmk0qbg!C zqotw2vPdbW&omfxG?&X0d2@KaLXOav!=nWhMPUf7Axx=H=M-dVSWIawj;h=1&HD6A zWtLpe0X-aTu8Ip6TppL=y7`mY`Tz>Q3>1$oiH@g9d>JB@$|A!!39*t`jx;XZ^bEVS zBr<&LQy(9{H?bcHwn67`rl5p7Q|8oGzx7tuddz?s_xuXez%^}O=bXrn;;V;tQ5{q8 zuR^u`ym`h6&ll}LYGo4SN{5l~vUE%wY}i#4<(jeotxk##5EK!!EMO-#i?W z`&fgLF8h2}v?V5E*%d=MUuWFY`^#Xy@d64aQJ@ZZ*z9fvL)HE_=TtE3Nagc0=vM;(pw1*v{`7!I9B%w1eKw{nJk zsY4wM!w-bvwU$w~uu$tMzfEokZ&@*n`ab8TF`ctB?8^2hJ^IC~;brYD&D55Q$#mn* zuM3w{JODL!ZcRc3_$<^I2SCk735h}IcdIX&KnW-!>TiUB60GPc{^V?Zm||9AUpjLh zC;~M+kb>9RfT+|OFR3PORsO|_3wsyLO_}n|>K^?<-I=EM=QYI4YCWB^!ggRyL3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pcap/testdata/1.psml b/pcap/testdata/1.psml new file mode 100644 index 0000000..0a12d10 --- /dev/null +++ b/pcap/testdata/1.psml @@ -0,0 +1,193 @@ + + + +
No.
+
Time
+
Source
+
Destination
+
Protocol
+
Length
+
Info
+
+ + +
1
+
0.000000
+
192.168.86.246
+
52.20.230.126
+
TLSv1.2
+
129
+
Application Data
+
+ + +
2
+
0.014887
+
52.20.230.126
+
192.168.86.246
+
TLSv1.2
+
103
+
Application Data
+
+ + +
3
+
0.014923
+
192.168.86.246
+
52.20.230.126
+
TCP
+
66
+
42184 \xe2\x86\x92 443 [ACK] Seq=64 Ack=38 Win=319 Len=0 TSval=207433870 TSecr=1059345504
+
+ + +
4
+
0.136177
+
192.168.86.246
+
31.13.66.56
+
TLSv1.2
+
97
+
Application Data
+
+ + +
5
+
0.171548
+
31.13.66.56
+
192.168.86.246
+
TLSv1.2
+
104
+
Application Data
+
+ + +
6
+
0.171592
+
192.168.86.246
+
31.13.66.56
+
TCP
+
66
+
41706 \xe2\x86\x92 443 [ACK] Seq=32 Ack=39 Win=1158 Len=0 TSval=2139377236 TSecr=2419013918
+
+ + +
7
+
0.616580
+
192.168.86.246
+
239.255.255.250
+
SSDP
+
143
+
M-SEARCH * HTTP/1.1
+
+ + +
8
+
0.659367
+
192.168.86.75
+
239.255.255.250
+
SSDP
+
143
+
M-SEARCH * HTTP/1.1
+
+ + +
9
+
0.666992
+
172.104.218.101
+
192.168.86.246
+
TLSv1.2
+
100
+
Application Data
+
+ + +
10
+
0.667023
+
192.168.86.246
+
172.104.218.101
+
TCP
+
66
+
44504 \xe2\x86\x92 443 [ACK] Seq=1 Ack=35 Win=319 Len=0 TSval=1319742541 TSecr=2345224981
+
+ + +
11
+
0.670219
+
192.168.86.1
+
192.168.86.246
+
SSDP
+
386
+
HTTP/1.1 200 OK
+
+ + +
12
+
1.034536
+
192.168.86.246
+
192.168.86.22
+
TCP
+
183
+
38108 \xe2\x86\x92 8009 [PSH, ACK] Seq=1 Ack=1 Win=359 Len=117 TSval=1442266250 TSecr=6907223 [TCP segment of a reassembled PDU]
+
+ + +
13
+
1.051383
+
192.168.86.246
+
192.168.86.22
+
TCP
+
66
+
38108 \xe2\x86\x92 8009 [ACK] Seq=118 Ack=118 Win=359 Len=0 TSval=1442266267 TSecr=6907724
+
+ + +
14
+
1.676315
+
Google_d2:76:12
+
Tp-LinkT_19:de:6c
+
ARP
+
42
+
Who has 192.168.86.246? Tell 192.168.86.1
+
+ + +
15
+
1.676325
+
Tp-LinkT_19:de:6c
+
Google_d2:76:12
+
ARP
+
42
+
192.168.86.246 is at e8:de:27:19:de:6c
+
+ + +
16
+
1.747769
+
192.168.86.246
+
35.174.87.41
+
TLSv1.2
+
126
+
Application Data
+
+ + +
17
+
1.811271
+
35.174.87.41
+
192.168.86.246
+
TLSv1.2
+
120
+
Application Data
+
+ + +
18
+
1.811307
+
192.168.86.246
+
35.174.87.41
+
TCP
+
66
+
53828 \xe2\x86\x92 443 [ACK] Seq=61 Ack=55 Win=274 Len=0 TSval=3132579163 TSecr=294067238
+
+ +
diff --git a/pcap/testdata/2.pcap-body b/pcap/testdata/2.pcap-body new file mode 100644 index 0000000000000000000000000000000000000000..2115ed40c0bfdef506ea02821ee1bfb82eb9166b GIT binary patch literal 93 zcmeyYWf{Z(1inx#;J~0@%BUU5z@{la`NyhN%s_Dt23H0K^PiRs3=V?JZXZ~oQw_vd mfqYj6<=$cjMrW%SPbc3H=K#-un4IL~Ts=dE{N$3<5(WVIUl`f| literal 0 HcmV?d00001 diff --git a/pcap/testdata/2.pcap-footer b/pcap/testdata/2.pcap-footer new file mode 100644 index 0000000..e69de29 diff --git a/pcap/testdata/2.pcap-header b/pcap/testdata/2.pcap-header new file mode 100644 index 0000000000000000000000000000000000000000..ebed66101df9898cb4640b039597b49817f0ea4b GIT binary patch literal 24 Vcmca|c+)~A1{MYcU}0bbasWb}0{Z{} literal 0 HcmV?d00001 diff --git a/pcap/testdata/2.pdml-body b/pcap/testdata/2.pdml-body new file mode 100644 index 0000000..201ae02 --- /dev/null +++ b/pcap/testdata/2.pdml-body @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pcap/testdata/2.pdml-footer b/pcap/testdata/2.pdml-footer new file mode 100644 index 0000000..b540842 --- /dev/null +++ b/pcap/testdata/2.pdml-footer @@ -0,0 +1,3 @@ + + + diff --git a/pcap/testdata/2.pdml-header b/pcap/testdata/2.pdml-header new file mode 100644 index 0000000..dc32697 --- /dev/null +++ b/pcap/testdata/2.pdml-header @@ -0,0 +1,4 @@ + + + + diff --git a/pcap/testdata/2.psml-body b/pcap/testdata/2.psml-body new file mode 100644 index 0000000..b2a1395 --- /dev/null +++ b/pcap/testdata/2.psml-body @@ -0,0 +1,9 @@ + +
1
+
0.000000
+
192.168.44.123
+
192.168.44.213
+
TFTP
+
77
+
Read Request, File: C:\IBMTCPIP\lccm.1, Transfer type: octet
+
diff --git a/pcap/testdata/2.psml-footer b/pcap/testdata/2.psml-footer new file mode 100644 index 0000000..b301fbb --- /dev/null +++ b/pcap/testdata/2.psml-footer @@ -0,0 +1,2 @@ + + diff --git a/pcap/testdata/2.psml-header b/pcap/testdata/2.psml-header new file mode 100644 index 0000000..97fdab7 --- /dev/null +++ b/pcap/testdata/2.psml-header @@ -0,0 +1,12 @@ + + + +
No.
+
Time
+
Source
+
Destination
+
Protocol
+
Length
+
Info
+
+ diff --git a/pdmltree/pdmltree.go b/pdmltree/pdmltree.go new file mode 100644 index 0000000..deb7e03 --- /dev/null +++ b/pdmltree/pdmltree.go @@ -0,0 +1,237 @@ +// Copyright 2019 Graham Clark. All rights reserved. Use of this source +// code is governed by the MIT license that can be found in the LICENSE +// file. + +// A demonstration of gowid's tree widget. +package pdmltree + +import ( + "bytes" + "encoding/xml" + "fmt" + "strconv" + "strings" + + "github.com/gcla/gowid" + "github.com/gcla/gowid/widgets/tree" + "github.com/gcla/termshark/widgets/hexdumper" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" +) + +//====================================================================== + +type EmptyIterator struct{} + +var _ tree.IIterator = EmptyIterator{} + +func (e EmptyIterator) Next() bool { + return false +} + +func (e EmptyIterator) Value() tree.IModel { + panic(errors.New("Should not call")) +} + +// pos points one head, so logically is -1 on init, but I use zero so the +// go default init makes sense. +type Iterator struct { + tree *Model + pos int +} + +var _ tree.IIterator = (*Iterator)(nil) + +func (p *Iterator) Next() bool { + p.pos += 1 + return (p.pos - 1) < len(p.tree.Children_) +} + +func (p *Iterator) Value() tree.IModel { + return p.tree.Children_[p.pos-1] +} + +type Model struct { + UiName string `xml:"-"` + Name string `xml:"-"` // needed for stripping geninfp from UI + Expanded bool `xml:"-"` + Pos int `xml:"-"` + Size int `xml:"-"` + Hide bool `xml:"-"` + Children_ []*Model `xml:",any"` + Content []byte `xml:",innerxml"` // needed for copying PDML to clipboard + NodeName string `xml:"-"` + Attrs map[string]string `xml:"-"` +} + +var _ tree.IModel = (*Model)(nil) + +// This ignores the first child, "Frame 15", because its range covers the whole packet +// which results in me always including that in the layers for any position. +func (n *Model) HexLayers(pos int, includeFirst bool) []hexdumper.LayerStyler { + res := make([]hexdumper.LayerStyler, 0) + sidx := 1 + if includeFirst { + sidx = 0 + } + for _, c := range n.Children_[sidx:] { + if c.Pos <= pos && pos < c.Pos+c.Size { + res = append(res, hexdumper.LayerStyler{ + Start: c.Pos, + End: c.Pos + c.Size, + ColUnselected: "hex-bottom-unselected", + ColSelected: "hex-bottom-selected", + }) + for _, c2 := range c.Children_ { + if c2.Pos <= pos && pos < c2.Pos+c2.Size { + res = append(res, hexdumper.LayerStyler{ + Start: c2.Pos, + End: c2.Pos + c2.Size, + ColUnselected: "hex-top-unselected", + ColSelected: "hex-top-selected", + }) + } + } + } + } + return res +} + +// Implement xml.Unmarshaler +func (n *Model) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + var err error + n.Attrs = map[string]string{} + for _, a := range start.Attr { + n.Attrs[a.Name.Local] = a.Value + switch a.Name.Local { + case "pos": + n.Pos, err = strconv.Atoi(a.Value) + if err != nil { + return errors.WithStack(err) + } + case "size": + n.Size, err = strconv.Atoi(a.Value) + if err != nil { + return errors.WithStack(err) + } + case "showname": + n.UiName = a.Value + case "show": + if n.UiName == "" { + n.UiName = a.Value + } + case "hide": + n.Hide = (a.Value == "yes") + case "name": + n.Name = a.Value + } + } + + n.NodeName = start.Name.Local + + type pt Model + res := d.DecodeElement((*pt)(n), &start) + return res +} + +func DecodePacket(data []byte) *Model { // nil if failure + d := xml.NewDecoder(bytes.NewReader(data)) + + var n Model + err := d.Decode(&n) + if err != nil { + log.Error(err) + return nil + } + + tr := n.removeUnneeded() + return tr +} + +func (p *Model) removeUnneeded() *Model { + if p.Hide { + return nil + } + if p.Name == "geninfo" { + return nil + } + if p.Name == "fake-field-wrapper" { // for now... + return nil + } + ch := make([]*Model, 0, len(p.Children_)) + for _, c := range p.Children_ { + nc := c.removeUnneeded() + if nc != nil { + ch = append(ch, nc) + } + } + p.Children_ = ch + return p +} + +func (p *Model) Children() tree.IIterator { + if p.Expanded { + return &Iterator{ + tree: p, + } + } else { + return EmptyIterator{} + } +} + +func (p *Model) HasChildren() bool { + return len(p.Children_) > 0 +} + +func (p *Model) Leaf() string { + return p.UiName +} + +func (p *Model) String() string { + return p.stringAt(1) +} + +func (p *Model) stringAt(level int) string { + x := make([]string, len(p.Children_)) + for i, t := range p.Children_ { + //x[i] = t.(*ModelExt).String2(level + 1) + x[i] = t.stringAt(level + 1) + } + for i, _ := range x { + x[i] = strings.Repeat(" ", 2*level) + x[i] + } + if len(x) == 0 { + return fmt.Sprintf("[%s]", p.UiName) + } else { + return fmt.Sprintf("[%s]\n%s", p.UiName, strings.Join(x, "\n")) + } +} + +//func (p *Model) Children() tree.IIterator { +//} + +func (p *Model) IsCollapsed() bool { + //return false + return !p.Expanded + // fp := d.FullPath() + // if v, res := (*d.cache)[fp]; res { + // return (v == collapsed) + // } else { + // return true + // } +} + +func (p *Model) SetCollapsed(app gowid.IApp, isCollapsed bool) { + // fp := d.FullPath() + if isCollapsed { + p.Expanded = false + } else { + p.Expanded = true + } +} + +//====================================================================== +// Local Variables: +// mode: Go +// fill-column: 110 +// End: diff --git a/pdmltree/pdmltree_test.go b/pdmltree/pdmltree_test.go new file mode 100644 index 0000000..e0123b1 --- /dev/null +++ b/pdmltree/pdmltree_test.go @@ -0,0 +1,210 @@ +// Copyright 2019 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license +// that can be found in the LICENSE file. + +package pdmltree + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +//====================================================================== + +var p1 string = ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +` + +func TestPdml1(t *testing.T) { + + tree := DecodePacket([]byte(p1)) + + assert.Equal(t, 8, len(tree.Children_)) + assert.Equal(t, 13, len(tree.Children_[0].Children_)) +} + +//====================================================================== +// Local Variables: +// mode: Go +// fill-column: 78 +// End: diff --git a/psmltable/model.go b/psmltable/model.go new file mode 100644 index 0000000..fbf33bc --- /dev/null +++ b/psmltable/model.go @@ -0,0 +1,152 @@ +// Copyright 2019 Graham Clark. All rights reserved. Use of this source +// code is governed by the MIT license that can be found in the LICENSE +// file. + +package psmltable + +import ( + "sort" + + "github.com/gcla/gowid" + "github.com/gcla/gowid/widgets/button" + "github.com/gcla/gowid/widgets/columns" + "github.com/gcla/gowid/widgets/holder" + "github.com/gcla/gowid/widgets/isselected" + "github.com/gcla/gowid/widgets/styled" + "github.com/gcla/gowid/widgets/table" + "github.com/gcla/gowid/widgets/text" + "github.com/gcla/termshark/widgets/expander" +) + +//====================================================================== + +// Model is a table model that provides a widget that will render +// in one row only when not selected. +type Model struct { + *table.SimpleModel + styler gowid.ICellStyler +} + +func New(m *table.SimpleModel, st gowid.ICellStyler) *Model { + return &Model{ + SimpleModel: m, + styler: st, + } +} + +// Provides the ith "cell" widget, upstream makes the "row" +func (c *Model) CellWidget(i int, s string) gowid.IWidget { + w := table.SimpleCellWidget(c, i, s) + if w != nil { + w = expander.New(w) + } + return w +} + +func (c *Model) CellWidgets(row table.RowId) []gowid.IWidget { + return table.SimpleCellWidgets(c, row) +} + +// table.ITable2 +func (c *Model) HeaderWidget(ws []gowid.IWidget, focus int) gowid.IWidget { + hws := c.HeaderWidgets() + hw := c.SimpleModel.HeaderWidget(hws, focus).(*columns.Widget) + hw2 := isselected.NewExt( + hw, + styled.New(hw, c.styler), + styled.New(hw, c.styler), + ) + return hw2 +} + +func (c *Model) HeaderWidgets() []gowid.IWidget { + var res []gowid.IWidget + if c.Headers != nil { + + res = make([]gowid.IWidget, 0, len(c.Headers)) + bhs := make([]*holder.Widget, len(c.Headers)) + bms := make([]*button.Widget, len(c.Headers)) + for i, s := range c.Headers { + i2 := i + var all, label gowid.IWidget + label = text.New(s + " ") + label = button.NewBare(label) + + sorters := c.Comparators + if sorters != nil { + sorteri := sorters[i2] + if sorteri != nil { + bmid := button.NewBare(text.New("-")) + bfor := button.NewBare(text.New("^")) + brev := button.NewBare(text.New("v")) + bh := holder.New(bmid) + bhs[i] = bh + bms[i] = bmid + + action := func(rev bool, next *button.Widget, app gowid.IApp) { + sorter := &table.SimpleTableByColumn{ + SimpleModel: c.SimpleModel, + Column: i2, + } + if rev { + sort.Sort(sort.Reverse(sorter)) + } else { + sort.Sort(sorter) + } + bh.SetSubWidget(next, app) + for j, bhj := range bhs { + if j != i2 { + bhj.SetSubWidget(bms[j], app) + } + } + } + + bmid.OnClick(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, widget gowid.IWidget) { + action(false, bfor, app) + })) + + bfor.OnClick(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, widget gowid.IWidget) { + action(true, brev, app) + })) + + brev.OnClick(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, widget gowid.IWidget) { + action(false, bfor, app) + })) + + all = columns.NewFixed(label, styled.NewFocus(bh, gowid.MakeStyledAs(gowid.StyleReverse))) + } + } + var w gowid.IWidget + if c.Style.HeaderStyleProvided { + w = isselected.New( + styled.New( + all, + c.GetStyle().HeaderStyleNoFocus, + ), + styled.New( + all, + c.GetStyle().HeaderStyleSelected, + ), + styled.New( + all, + c.GetStyle().HeaderStyleFocus, + ), + ) + } else { + w = styled.NewExt( + all, + nil, + gowid.MakeStyledAs(gowid.StyleReverse), + ) + } + res = append(res, w) + } + } + return res +} + +//====================================================================== +// Local Variables: +// mode: Go +// fill-column: 110 +// End: diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..2197eb6 --- /dev/null +++ b/utils.go @@ -0,0 +1,448 @@ +// Copyright 2019 Graham Clark. All rights reserved. Use of this source +// code is governed by the MIT license that can be found in the LICENSE +// file. + +package termshark + +import ( + "bufio" + "bytes" + "compress/gzip" + "encoding/binary" + "encoding/gob" + "encoding/xml" + "fmt" + "io" + "io/ioutil" + "os" + "os/exec" + "path" + "path/filepath" + "regexp" + "runtime" + "strings" + "sync" + "syscall" + "text/template" + "time" + + "github.com/gcla/gowid" + "github.com/blang/semver" + "github.com/pkg/errors" + "github.com/shibukawa/configdir" + log "github.com/sirupsen/logrus" + "github.com/spf13/viper" +) + +//====================================================================== + +type BadStateError struct{} + +var _ error = BadStateError{} + +func (e BadStateError) Error() string { + return "Bad state" +} + +var BadState = BadStateError{} + +//====================================================================== + +func IsCommandInPath(bin string) bool { + _, err := exec.LookPath(bin) + return err == nil +} + +func DirOfPathCommandUnsafe(bin string) string { + d, err := DirOfPathCommand(bin) + if err != nil { + panic(err) + } + return d +} + +func DirOfPathCommand(bin string) (string, error) { + return exec.LookPath(bin) +} + +func TSharkBin() string { + return ConfString("main.tshark", "tshark") +} + +func DumpcapBin() string { + return ConfString("main.dumpcap", "dumpcap") +} + +func TailCommand() []string { + def := []string{"tail", "-f", "-c", "+0"} + if runtime.GOOS == "windows" { + def[0] = "c:\\cygwin64\\bin\\tail.exe" + } + return ConfStringSlice("main.tail-command", def) +} + +func ConfString(name string, def string) string { + if viper.Get(name) != nil { + return viper.GetString(name) + } else { + return def + } +} + +func ConfInt(name string, def int) int { + if viper.Get(name) != nil { + return viper.GetInt(name) + } else { + return def + } +} + +var TSharkVersionUnknown = fmt.Errorf("Could not determine version of tshark") + +func TSharkVersionFromOutput(output string) (semver.Version, error) { + var ver = regexp.MustCompile(`^TShark .*?(\d+\.\d+\.\d+)`) + res := ver.FindStringSubmatch(output) + + if len(res) > 0 { + if v, err := semver.Make(res[1]); err == nil { + return v, nil + } else { + return semver.Version{}, err + } + } + + return semver.Version{}, errors.WithStack(TSharkVersionUnknown) +} + +func TSharkVersion(tshark string) (semver.Version, error) { + cmd := exec.Command(tshark, "--version") + cmdOutput := &bytes.Buffer{} + cmd.Stdout = cmdOutput + cmd.Run() // don't check error - older versions return error code 1. Just search output. + output := cmdOutput.Bytes() + + return TSharkVersionFromOutput(string(output)) +} + +func RunForExitCode(prog string, args ...string) (int, error) { + var err error + exitCode := -1 // default bad + cmd := exec.Command(prog, args...) + err = cmd.Run() + if err != nil { + if exerr, ok := err.(*exec.ExitError); ok { + ws := exerr.Sys().(syscall.WaitStatus) + exitCode = ws.ExitStatus() + } + } else { + ws := cmd.ProcessState.Sys().(syscall.WaitStatus) + exitCode = ws.ExitStatus() + } + + return exitCode, err +} + +func ConfStringSlice(name string, def []string) []string { + res := viper.GetStringSlice(name) + if res == nil { + res = def + } + return res +} + +func ConfFile(file string) string { + stdConf := configdir.New("", "termshark") + dirs := stdConf.QueryFolders(configdir.Global) + return path.Join(dirs[0].Path, file) +} + +func CacheFile(bin string) string { + return filepath.Join(CacheDir(), bin) +} + +func CacheDir() string { + stdConf := configdir.New("", "termshark") + dirs := stdConf.QueryFolders(configdir.Cache) + return dirs[0].Path +} + +func RemoveFromStringSlice(pcap string, comps []string) []string { + var newcomps []string + for _, v := range comps { + if v == pcap { + continue + } else { + newcomps = append(newcomps, v) + } + } + newcomps = append([]string{pcap}, newcomps...) + return newcomps +} + +const magicMicroseconds = 0xA1B2C3D4 +const versionMajor = 2 +const versionMinor = 4 +const dlt_en10mb = 1 + +func WriteEmptyPcap(filename string) error { + var buf [24]byte + binary.LittleEndian.PutUint32(buf[0:4], magicMicroseconds) + binary.LittleEndian.PutUint16(buf[4:6], versionMajor) + binary.LittleEndian.PutUint16(buf[6:8], versionMinor) + // bytes 8:12 stay 0 (timezone = UTC) + // bytes 12:16 stay 0 (sigfigs is always set to zero, according to + // http://wiki.wireshark.org/Development/LibpcapFileFormat + binary.LittleEndian.PutUint32(buf[16:20], 10000) + binary.LittleEndian.PutUint32(buf[20:24], uint32(dlt_en10mb)) + + err := ioutil.WriteFile(filename, buf[:], 0644) + + return err +} + +func FileNewerThan(f1, f2 string) (bool, error) { + file1, err := os.Open(f1) + if err != nil { + return false, err + } + defer file1.Close() + file2, err := os.Open(f2) + if err != nil { + return false, err + } + defer file2.Close() + f1s, err := file1.Stat() + if err != nil { + return false, err + } + f2s, err := file2.Stat() + if err != nil { + return false, err + } + return f1s.ModTime().After(f2s.ModTime()), nil +} + +func ReadGob(filePath string, object interface{}) error { + file, err := os.Open(filePath) + if err == nil { + defer file.Close() + var gr io.Reader + gr, err = gzip.NewReader(file) + if err != nil { + return err + } + decoder := gob.NewDecoder(gr) + err = decoder.Decode(object) + } + return err +} + +func WriteGob(filePath string, object interface{}) error { + file, err := os.Create(filePath) + if err == nil { + defer file.Close() + gzipper := gzip.NewWriter(file) + defer gzipper.Close() + encoder := gob.NewEncoder(gzipper) + err = encoder.Encode(object) + } + return err +} + +func StringInSlice(a string, list []string) bool { + for _, b := range list { + if b == a { + return true + } + } + return false +} + +// Must succeed - use on internal templates +func TemplateToString(tmpl *template.Template, name string, data interface{}) string { + var res bytes.Buffer + if err := tmpl.ExecuteTemplate(&res, name, data); err != nil { + log.Fatal(err) + } + + return res.String() +} + +func StringIsArgPrefixOf(a string, list []string) bool { + for _, b := range list { + if strings.HasPrefix(a, fmt.Sprintf("%s=", b)) { + return true + } + } + return false +} + +func RunOnDoubleTicker(ch <-chan struct{}, fn func(), dur1 time.Duration, dur2 time.Duration, loops int) { + + ticker := time.NewTicker(dur1) + counter := 0 +Loop: + for { + select { + case <-ticker.C: + fn() + counter++ + if counter == loops { + ticker.Stop() + ticker = time.NewTicker(dur2) + } + case <-ch: + ticker.Stop() + break Loop + } + } +} + +func TrackedGo(fn func(), wgs ...*sync.WaitGroup) { + for _, wg := range wgs { + wg.Add(1) + } + go func() { + for _, wg := range wgs { + defer wg.Done() + } + fn() + }() +} + +type IProcess interface { + Kill() error + Pid() int +} + +func KillIfPossible(p IProcess) error { + if p == nil { + return nil + } + err := p.Kill() + return err +} + +func SafePid(p IProcess) int { + if p == nil { + return -1 + } + return p.Pid() +} + +//====================================================================== + +// From http://blog.kamilkisiel.net/blog/2012/07/05/using-the-go-regexp-package/ +// +type tsregexp struct { + *regexp.Regexp +} + +func (r *tsregexp) FindStringSubmatchMap(s string) map[string]string { + captures := make(map[string]string) + + match := r.FindStringSubmatch(s) + if match == nil { + return captures + } + + for i, name := range r.SubexpNames() { + if i == 0 { + continue + } + captures[name] = match[i] + } + + return captures +} + +var flagRE = tsregexp{regexp.MustCompile(`--tshark-(?P[a-zA-Z0-9])=(?P.+)`)} + +func ConvertArgToTShark(arg string) (string, string, bool) { + matches := flagRE.FindStringSubmatchMap(arg) + if flag, ok := matches["flag"]; ok { + if val, ok := matches["val"]; ok { + if val == "false" { + return "", "", false + } else if val == "true" { + return flag, "", true + } else { + return flag, val, true + } + } + } + return "", "", false +} + +//====================================================================== + +var UnexpectedOutput = fmt.Errorf("Unexpected output") + +// Use tshark's output, becauses the indices can then be used to select +// an interface to sniff on, and net.Interfaces returns the interfaces in +// a different order. +func Interfaces() ([]string, error) { + cmd := exec.Command(TSharkBin(), "-D") + out, err := cmd.Output() + if err != nil { + return nil, err + } + return interfacesFrom(bytes.NewReader(out)) +} + +func interfacesFrom(reader io.Reader) ([]string, error) { + res := make([]string, 0, 20) + scanner := bufio.NewScanner(reader) + for scanner.Scan() { + line := scanner.Text() + pieces := strings.Fields(line) + if len(pieces) < 2 { + return nil, gowid.WithKVs(UnexpectedOutput, map[string]interface{}{"Output": line}) + } + res = append(res, pieces[1]) + } + + return res, nil +} + +//====================================================================== + +type pdmlany struct { + XMLName xml.Name + Attrs []xml.Attr `xml:",any,attr"` + Comment string `xml:",comment"` + Nested []*pdmlany `xml:",any"` + //Content string `xml:",chardata"` +} + +// IndentPdml reindents XML, disregarding content between tags (because we knoe +// PDML doesn't use that capability of XML) +func IndentPdml(in io.Reader, out io.Writer) error { + decoder := xml.NewDecoder(in) + + n := pdmlany{} + if err := decoder.Decode(&n); err != nil { + return err + } + + b, err := xml.MarshalIndent(n, "", " ") + if err != nil { + return err + } + out.Write(fixNewlines(b)) + return nil +} + +func fixNewlines(unix []byte) []byte { + if runtime.GOOS != "windows" { + return unix + } + + return bytes.Replace(unix, []byte{'\n'}, []byte{'\r', '\n'}, -1) +} + +//====================================================================== +// Local Variables: +// mode: Go +// fill-column: 78 +// End: diff --git a/utils_test.go b/utils_test.go new file mode 100644 index 0000000..304bda3 --- /dev/null +++ b/utils_test.go @@ -0,0 +1,95 @@ +// Copyright 2019 Graham Clark. All rights reserved. Use of this source +// code is governed by the MIT license that can be found in the LICENSE +// file. + +package termshark + +import ( + "bytes" + "testing" + + "github.com/blang/semver" + "github.com/stretchr/testify/assert" +) + +//====================================================================== + +func TestArgConv(t *testing.T) { + var tests = []struct { + arg string + flag string + val string + res bool + }{ + {"--tshark-d=foo", "d", "foo", true}, + {"--tshark-abc=foo", "", "", false}, + {"--tshark-V=true", "V", "", true}, + {"--tshark-V=false", "", "", false}, + {"--ts-V=wow", "", "", false}, + } + + for _, test := range tests { + f, v, ok := ConvertArgToTShark(test.arg) + assert.Equal(t, test.res, ok) + if test.res { + assert.Equal(t, test.flag, f) + assert.Equal(t, test.val, v) + } + } +} + +func TestVer1(t *testing.T) { + out1 := `TShark (Wireshark) 2.6.6 (Git v2.6.6 packaged as 2.6.6-1~ubuntu18.04.0) + +Copyright 1998-2019 Gerald Combs and contributors.` + + v1, err := TSharkVersionFromOutput(out1) + assert.NoError(t, err) + res, _ := semver.Make("2.6.6") + assert.Equal(t, res, v1) +} + +func TestVer2(t *testing.T) { + out1 := `TShark 1.6.7 + +Copyright 1998-2012 Gerald Combs and contributors. +This is free software; see the source for copying conditions. There is NO +warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + +Compiled (64-bit) with GLib 2.32.0, with libpcap (version unknown), with libz +1.2.3.4, with POSIX capabilities (Linux), without libpcre, with SMI 0.4.8, with +c-ares 1.7.5, with Lua 5.1, without Python, with GnuTLS 2.12.14, with Gcrypt +1.5.0, with MIT Kerberos, with GeoIP. + +Running on Linux 3.2.0-126-generic, with libpcap version 1.1.1, with libz +1.2.3.4. +` + + v1, err := TSharkVersionFromOutput(out1) + assert.NoError(t, err) + res, _ := semver.Make("1.6.7") + assert.Equal(t, res, v1) +} + +func TestInterfaces1(t *testing.T) { + out1 := ` +1. \Device\NPF_{BAC1CFBD-DE27-4023-B478-0C490B99DC5E} (Local Area Connection 2) +2. \Device\NPF_{78032B7E-4968-42D3-9F37-287EA86C0AAA} (Local Area Connection* 10) +3. \Device\NPF_NdisWanIp (NdisWan Adapter) +4. \Device\NPF_NdisWanBh (NdisWan Adapter) +5. \Device\NPF_{84E7CAE6-E96F-4F31-96FD-170B0F514AB2} (Npcap Loopback Adapter) +6. \Device\NPF_NdisWanIpv6 (NdisWan Adapter) +7. \Device\NPF_{503E1F71-C57C-438D-B004-EA5563723C16} (Local Area Connection 5) +8. \Device\NPF_{15DDE443-C208-4328-8919-9666682EE804} (Local Area Connection* 11) +`[1:] + interfaces, err := interfacesFrom(bytes.NewReader([]byte(out1))) + assert.NoError(t, err) + assert.Equal(t, 8, len(interfaces)) + assert.Equal(t, `\Device\NPF_{78032B7E-4968-42D3-9F37-287EA86C0AAA}`, interfaces[1]) +} + +//====================================================================== +// Local Variables: +// mode: Go +// fill-column: 110 +// End: diff --git a/version.go b/version.go new file mode 100644 index 0000000..641ba73 --- /dev/null +++ b/version.go @@ -0,0 +1,14 @@ +// Copyright 2019 Graham Clark. All rights reserved. Use of this source +// code is governed by the MIT license that can be found in the LICENSE +// file. + +package termshark + +var Version string = "" + +//====================================================================== +// Local Variables: +// indent-tabs-mode: nil +// tab-width: 4 +// fill-column: 78 +// End: diff --git a/widgets/appkeys/appkeys.go b/widgets/appkeys/appkeys.go new file mode 100644 index 0000000..3022371 --- /dev/null +++ b/widgets/appkeys/appkeys.go @@ -0,0 +1,187 @@ +// Copyright 2019 Graham Clark. All rights reserved. Use of this source +// code is governed by the MIT license that can be found in the LICENSE +// file. + +// Package appkeys provides a widget which responds to keyboard input. +package appkeys + +import ( + "fmt" + + "github.com/gcla/gowid" + "github.com/gdamore/tcell" +) + +//====================================================================== + +type IWidget interface { + gowid.ICompositeWidget +} + +type IAppInput interface { + gowid.IComposite + ApplyBefore() bool +} + +type IAppKeys interface { + KeyInput(ev *tcell.EventKey, app gowid.IApp) bool +} + +type IAppMouse interface { + MouseInput(ev *tcell.EventMouse, app gowid.IApp) bool +} + +type KeyInputFn func(ev *tcell.EventKey, app gowid.IApp) bool +type MouseInputFn func(ev *tcell.EventMouse, app gowid.IApp) bool + +type Options struct { + ApplyBefore bool +} + +type Widget struct { + gowid.IWidget + opt Options +} + +type KeyWidget struct { + *Widget + fn KeyInputFn +} + +type MouseWidget struct { + *Widget + fn MouseInputFn +} + +func New(inner gowid.IWidget, fn KeyInputFn, opts ...Options) *KeyWidget { + var opt Options + if len(opts) > 0 { + opt = opts[0] + } + + res := &KeyWidget{ + Widget: &Widget{ + IWidget: inner, + opt: opt, + }, + fn: fn, + } + + return res +} + +var _ gowid.ICompositeWidget = (*KeyWidget)(nil) +var _ IWidget = (*KeyWidget)(nil) +var _ IAppKeys = (*KeyWidget)(nil) + +func NewMouse(inner gowid.IWidget, fn MouseInputFn, opts ...Options) *MouseWidget { + var opt Options + if len(opts) > 0 { + opt = opts[0] + } + + res := &MouseWidget{ + Widget: &Widget{ + IWidget: inner, + opt: opt, + }, + fn: fn, + } + + return res +} + +var _ gowid.ICompositeWidget = (*MouseWidget)(nil) +var _ IWidget = (*MouseWidget)(nil) +var _ IAppMouse = (*MouseWidget)(nil) + +func (w *Widget) String() string { + return fmt.Sprintf("appkeys[%v]", w.SubWidget()) +} + +func (w *Widget) ApplyBefore() bool { + return w.opt.ApplyBefore +} + +func (w *KeyWidget) KeyInput(k *tcell.EventKey, app gowid.IApp) bool { + return w.fn(k, app) +} + +func (w *MouseWidget) MouseInput(k *tcell.EventMouse, app gowid.IApp) bool { + return w.fn(k, app) +} + +func (w *Widget) SubWidget() gowid.IWidget { + return w.IWidget +} + +func (w *Widget) SetSubWidget(wi gowid.IWidget, app gowid.IApp) { + w.IWidget = wi +} + +func (w *Widget) SubWidgetSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderSize { + return SubWidgetSize(w, size, focus, app) +} + +func (w *KeyWidget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { + return UserInput(w, ev, size, focus, app) +} + +func (w *MouseWidget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { + return UserInput(w, ev, size, focus, app) +} + +//====================================================================== + +func SubWidgetSize(w gowid.ICompositeWidget, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderSize { + return size +} + +func RenderSize(w IWidget, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { + return gowid.RenderSize(w.SubWidget(), size, focus, app) +} + +func UserInput(w IAppInput, ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { + var res bool + + if w.ApplyBefore() { + switch ev := ev.(type) { + case *tcell.EventKey: + if wk, ok := w.(IAppKeys); ok { + res = wk.KeyInput(ev, app) + } + case *tcell.EventMouse: + if wm, ok := w.(IAppMouse); ok { + res = wm.MouseInput(ev, app) + } + } + if !res { + res = gowid.UserInput(w.SubWidget(), ev, size, focus, app) + } + } else { + res = gowid.UserInput(w.SubWidget(), ev, size, focus, app) + if !res { + switch ev := ev.(type) { + case *tcell.EventKey: + if wk, ok := w.(IAppKeys); ok { + res = wk.KeyInput(ev, app) + } + case *tcell.EventMouse: + if wm, ok := w.(IAppMouse); ok { + res = wm.MouseInput(ev, app) + } + } + } + } + return res +} + +func Render(w IWidget, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { + return gowid.Render(w.SubWidget(), size, focus, app) +} + +//====================================================================== +// Local Variables: +// mode: Go +// fill-column: 110 +// End: diff --git a/widgets/copymodetree/copymodetree.go b/widgets/copymodetree/copymodetree.go new file mode 100644 index 0000000..2fce61a --- /dev/null +++ b/widgets/copymodetree/copymodetree.go @@ -0,0 +1,155 @@ +// Copyright 2019 Graham Clark. All rights reserved. Use of this source +// code is governed by the MIT license that can be found in the LICENSE +// file. + +// Package copymodetree provides a wrapper around a tree that supports copy mode. +// It assumes the underlying tree is a termshark PDML tree and allows copying +// the PDML substructure or a serialized representation of the substructure. +package copymodetree + +import ( + "bytes" + "fmt" + "strings" + + "github.com/gcla/gowid" + "github.com/gcla/gowid/widgets/list" + "github.com/gcla/gowid/widgets/tree" + "github.com/gcla/termshark" + "github.com/gcla/termshark/pdmltree" +) + +//====================================================================== + +type Widget struct { + *list.Widget + clip gowid.IClipboardSelected +} + +type ITreeAndListWalker interface { + list.IWalker + Decorator() tree.IDecorator + Maker() tree.IWidgetMaker + Tree() tree.IModel +} + +func New(l *list.Widget, clip gowid.IClipboardSelected) *Widget { + return &Widget{ + Widget: l, + clip: clip, + } +} + +func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { + if app.InCopyMode() && app.CopyModeClaimedBy().ID() == w.ID() && focus.Focus { + diff := w.CopyModeLevels() - (app.CopyModeClaimedAt() - app.CopyLevel()) + + walk := w.Walker().(ITreeAndListWalker) + w.SetWalker(NewWalker(walk, walk.Focus().(tree.IPos), diff, w.clip), app) + + res := gowid.Render(w.Widget, size, focus, app) + w.SetWalker(walk, app) + return res + } else { + return gowid.Render(w.Widget, size, focus, app) + } +} + +func (w *Widget) SubWidget() gowid.IWidget { + return w.Widget +} + +func (w *Widget) CopyModeLevels() int { + pos := w.Walker().Focus().(tree.IPos) + return len(pos.Indices()) +} + +func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { + return gowid.CopyModeUserInput(w, ev, size, focus, app) +} + +func (w *Widget) Clips(app gowid.IApp) []gowid.ICopyResult { + walker := w.Walker().(tree.ITreeWalker) + pos := walker.Focus().(tree.IPos) + lvls := w.CopyModeLevels() + + diff := lvls - (app.CopyModeClaimedAt() - app.CopyLevel()) + + npos := pos + for i := 0; i < diff; i++ { + npos = tree.ParentPosition(npos) + } + + tr := npos.GetSubStructure(walker.Tree()) + ptr := tr.(*pdmltree.Model) + + atts := make([]string, 0) + atts = append(atts, string(ptr.NodeName)) + for k, v := range ptr.Attrs { + atts = append(atts, fmt.Sprintf("%s=\"%s\"", k, v)) + } + + var tidyxmlstr string + messyxmlstr := fmt.Sprintf("<%s>%s", strings.Join(atts, " "), ptr.Content, string(ptr.NodeName)) + buf := bytes.Buffer{} + if termshark.IndentPdml(bytes.NewReader([]byte(messyxmlstr)), &buf) != nil { + tidyxmlstr = messyxmlstr + } else { + tidyxmlstr = buf.String() + } + + return []gowid.ICopyResult{ + gowid.CopyResult{ + Name: "Selected subtree", + Val: ptr.String(), + }, + gowid.CopyResult{ + Name: "Selected subtree PDML", + Val: tidyxmlstr, + }, + } +} + +//====================================================================== + +type Walker struct { + ITreeAndListWalker + pos tree.IPos + diff int + gowid.IClipboardSelected +} + +func NewWalker(walker ITreeAndListWalker, pos tree.IPos, diff int, clip gowid.IClipboardSelected) *Walker { + return &Walker{ + ITreeAndListWalker: walker, + pos: pos, + diff: diff, + IClipboardSelected: clip, + } +} + +func (f *Walker) At(lpos list.IWalkerPosition) gowid.IWidget { + if lpos == nil { + return nil + } + + pos := lpos.(tree.IPos) + w := tree.WidgetAt(f, pos) + + npos := f.pos + for i := 0; i < f.diff; i++ { + npos = tree.ParentPosition(npos) + } + + if tree.IsSubPosition(npos, pos) { + return f.AlterWidget(w, nil) + } else { + return w + } +} + +//====================================================================== +// Local Variables: +// mode: Go +// fill-column: 110 +// End: diff --git a/widgets/enableselected/enableselected.go b/widgets/enableselected/enableselected.go new file mode 100644 index 0000000..2a4024c --- /dev/null +++ b/widgets/enableselected/enableselected.go @@ -0,0 +1,54 @@ +// Copyright 2019 Graham Clark. All rights reserved. Use of this source +// code is governed by the MIT license that can be found in the LICENSE +// file. + +// Package enableselected provides a widget that turns on focus.Selected. +// It can be used to wrap container widgets (pile, columns) which may +// change their look according to the selected state. One use for this is +// highlighting selected rows or columns when the widget itself is not in +// focus. +package enableselected + +import ( + "github.com/gcla/gowid" +) + +//====================================================================== + +// Widget turns on the selected field in the Widget when operations are done on this widget. Then +// children widgets that respond to the selected state will be activated. +type Widget struct { + gowid.IWidget +} + +var _ gowid.IWidget = (*Widget)(nil) +var _ gowid.IComposite = (*Widget)(nil) + +func New(w gowid.IWidget) *Widget { + return &Widget{w} +} + +func (w *Widget) SubWidget() gowid.IWidget { + return w.IWidget +} + +func (w *Widget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { + focus.Selected = true + return gowid.RenderSize(w.IWidget, size, focus, app) +} + +func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { + focus.Selected = true + return gowid.Render(w.IWidget, size, focus, app) +} + +func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { + focus.Selected = true + return gowid.UserInput(w.IWidget, ev, size, focus, app) +} + +//====================================================================== +// Local Variables: +// mode: Go +// fill-column: 110 +// End: diff --git a/widgets/expander/expander.go b/widgets/expander/expander.go new file mode 100644 index 0000000..8271c46 --- /dev/null +++ b/widgets/expander/expander.go @@ -0,0 +1,68 @@ +// Copyright 2019 Graham Clark. All rights reserved. Use of this source +// code is governed by the MIT license that can be found in the LICENSE +// file. + +// Package expander provides a widget that renders in one line when not in focus +// but that may render using more than one line when in focus. This is useful for +// showing an item in full when needed, but otherwise saving screen real-estate. +package expander + +import ( + "github.com/gcla/gowid" + "github.com/gcla/gowid/widgets/boxadapter" +) + +//====================================================================== + +// Widget will render in one row when not selected, and then using +// however many rows required when selected. +type Widget struct { + orig gowid.IWidget + w *boxadapter.Widget +} + +var _ gowid.IWidget = (*Widget)(nil) +var _ gowid.IComposite = (*Widget)(nil) + +func New(w gowid.IWidget) *Widget { + b := boxadapter.New(w, 1) + return &Widget{w, b} +} + +func (w *Widget) SubWidget() gowid.IWidget { + return w.orig +} + +func (w *Widget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { + if focus.Selected { + return gowid.RenderSize(w.orig, size, focus, app) + } else { + return gowid.RenderSize(w.w, size, focus, app) + } +} + +func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { + if focus.Selected { + return gowid.Render(w.orig, size, focus, app) + } else { + return gowid.Render(w.w, size, focus, app) + } +} + +func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { + if focus.Selected { + return gowid.UserInput(w.orig, ev, size, focus, app) + } else { + return gowid.UserInput(w.w, ev, size, focus, app) + } +} + +func (w *Widget) Selectable() bool { + return w.w.Selectable() +} + +//====================================================================== +// Local Variables: +// mode: Go +// fill-column: 110 +// End: diff --git a/widgets/filter/filter.go b/widgets/filter/filter.go new file mode 100644 index 0000000..c50a96b --- /dev/null +++ b/widgets/filter/filter.go @@ -0,0 +1,507 @@ +// Copyright 2019 Graham Clark. All rights reserved. Use of this source +// code is governed by the MIT license that can be found in the LICENSE +// file. + +// Package filter prpvides a termshark-specific edit widget which changes +// color according to the validity of its input, and which activates a +// drop-down menu of possible completions for the term at point. +package filter + +import ( + "context" + "fmt" + "io" + "os/exec" + "sync" + "syscall" + "time" + + "github.com/gcla/gowid" + "github.com/gcla/gowid/gwutil" + "github.com/gcla/gowid/widgets/button" + "github.com/gcla/gowid/widgets/cellmod" + "github.com/gcla/gowid/widgets/columns" + "github.com/gcla/gowid/widgets/edit" + "github.com/gcla/gowid/widgets/framed" + "github.com/gcla/gowid/widgets/holder" + "github.com/gcla/gowid/widgets/hpadding" + "github.com/gcla/gowid/widgets/list" + "github.com/gcla/gowid/widgets/menu" + "github.com/gcla/gowid/widgets/styled" + "github.com/gcla/gowid/widgets/text" + "github.com/gcla/termshark" + "github.com/gcla/termshark/widgets/appkeys" + "github.com/gdamore/tcell" +) + +//====================================================================== + +// This is a debugging aid - I use it to ensure goroutines stop as expected. If they don't +// the main program will hang at termination. +var Goroutinewg *sync.WaitGroup + +type filtStruct struct { + txt string + app gowid.IApp +} + +type Widget struct { + wrapped gowid.IWidget + opts Options + ed *edit.Widget // what the user types into - wrapped by validity styling + dropDown *menu.Widget // the menu of possible completions + dropDownSite *menu.SiteWidget // where in this widget structure the drop down is rendered + validitySite *holder.Widget // the widget swaps out the contents of this placeholder on validity changes + valid gowid.IWidget // what to display when the filter value is valid + invalid gowid.IWidget // what to display when the filter value is invalid + intermediate gowid.IWidget // what to display when the filter value's validity is being determined + edCtx context.Context + edCancelFn context.CancelFunc + fields termshark.IPrefixCompleter // provides completions, given a prefix + completionsList *list.Widget // the filter widget replaces the list walker when new completions are generated + completions []string // the current set of completions, used when rendering + runthisfilterchan chan *filtStruct + filterchangedchan chan *filtStruct + quitchan chan struct{} + readytorunchan chan struct{} + *gowid.Callbacks + gowid.IsSelectable +} + +var _ gowid.IWidget = (*Widget)(nil) +var _ io.Closer = (*Widget)(nil) + +type IntermediateCB struct{} +type ValidCB struct{} +type InvalidCB struct{} + +type Options struct { + Completer termshark.IPrefixCompleter + MaxCompletions int +} + +func New(opt Options) *Widget { + ed := edit.New() + + fixed := gowid.RenderFixed{} + l2 := list.New(list.NewSimpleListWalker([]gowid.IWidget{})) + + if opt.MaxCompletions == 0 { + opt.MaxCompletions = 20 + } + + menuListBox2 := styled.New( + framed.NewUnicode(cellmod.Opaque(l2)), + gowid.MakePaletteRef("filter-menu-focus"), + ) + + drop := menu.New("filter", menuListBox2, gowid.RenderWithUnits{U: opt.MaxCompletions + 2}, + menu.Options{ + IgnoreKeysProvided: true, + IgnoreKeys: []gowid.IKey{ + gowid.MakeKeyExt(tcell.KeyUp), + gowid.MakeKeyExt(tcell.KeyDown), + }, + CloseKeysProvided: true, + CloseKeys: []gowid.IKey{}, + }, + ) + + site := menu.NewSite(menu.SiteOptions{ + YOffset: 1, + }) + + onelineEd := appkeys.New(ed, filterOutEnter, appkeys.Options{ + ApplyBefore: true, + }) + + valid := styled.New(onelineEd, + gowid.MakePaletteRef("filter-valid"), + ) + invalid := styled.New(onelineEd, + gowid.MakePaletteRef("filter-invalid"), + ) + intermediate := styled.New(onelineEd, + gowid.MakePaletteRef("filter-intermediate"), + ) + + placeholder := holder.New(valid) + + var wrapped gowid.IWidget = columns.New([]gowid.IContainerWidget{ + &gowid.ContainerWidget{IWidget: site, D: fixed}, + &gowid.ContainerWidget{IWidget: placeholder, D: gowid.RenderWithWeight{W: 1}}, + }) + + runthisfilterchan := make(chan *filtStruct) + quitchan := make(chan struct{}) + readytorunchan := make(chan struct{}) + filterchangedchan := make(chan *filtStruct) + + res := &Widget{ + wrapped: wrapped, + opts: opt, + ed: ed, + dropDown: drop, + dropDownSite: site, + validitySite: placeholder, + valid: valid, + invalid: invalid, + intermediate: intermediate, + fields: opt.Completer, + completionsList: l2, + completions: []string{}, + filterchangedchan: filterchangedchan, + runthisfilterchan: runthisfilterchan, + quitchan: quitchan, + readytorunchan: readytorunchan, + Callbacks: gowid.NewCallbacks(), + } + + validcb := &ValidateCB{ + Fn: func(app gowid.IApp) { + app.Run(gowid.RunFunction(func(app gowid.IApp) { + res.validitySite.SetSubWidget(res.valid, app) + gowid.RunWidgetCallbacks(res.Callbacks, ValidCB{}, app, res) + })) + }, + } + + invalidcb := &ValidateCB{ + Fn: func(app gowid.IApp) { + app.Run(gowid.RunFunction(func(app gowid.IApp) { + res.validitySite.SetSubWidget(res.invalid, app) + gowid.RunWidgetCallbacks(res.Callbacks, InvalidCB{}, app, res) + })) + }, + } + + killedcb := &ValidateCB{ + Fn: func(app gowid.IApp) { + app.Run(gowid.RunFunction(func(app gowid.IApp) { + res.validitySite.SetSubWidget(res.intermediate, app) + gowid.RunWidgetCallbacks(res.Callbacks, IntermediateCB{}, app, res) + })) + }, + } + + validator := Validator{ + Valid: validcb, + Invalid: invalidcb, + KilledCB: killedcb, + } + + // Save up filter changes, send latest over when process is ready, discard ones in between + termshark.TrackedGo(func() { + send := false + var latest *filtStruct + CL2: + for { + if send && latest != nil { + res.runthisfilterchan <- latest + latest = nil + send = false + } + select { + // tshark process ready + case <-res.quitchan: + break CL2 + case <-res.readytorunchan: + send = true + // Sent by tshark process goroutine + case fs := <-res.filterchangedchan: + latest = fs + // We're ready to run a new one, so kill any process that is in progress. Take care + // because it might not have actually started yet! + validator.Kill() + } + } + }, Goroutinewg) + + // Every time it gets an event, it means run the process. Another goroutine takes care of consolidating + // events. Stops when channel is closed + termshark.TrackedGo(func() { + CL: + for { + // Tell other goroutine we are ready for more - each time round the loop. This makes sure + // we don't run more than one tshark process - it will get killed if a new filter should take + // priority. + res.readytorunchan <- struct{}{} + select { + case <-res.quitchan: + break CL + case fs := <-res.runthisfilterchan: + validcb.App = fs.app + invalidcb.App = fs.app + killedcb.App = fs.app + validator.Validate(fs.txt) + } + } + }, Goroutinewg) + + ed.OnTextSet(gowid.MakeWidgetCallback("cb", gowid.WidgetChangedFunction(func(app gowid.IApp, ew gowid.IWidget) { + // Shortcut - we know that "" is always valid + if ed.Text() != "" { + res.validitySite.SetSubWidget(res.intermediate, app) + gowid.RunWidgetCallbacks(res.Callbacks, IntermediateCB{}, app, res) + } + + if res.edCancelFn != nil { + res.edCancelFn() + } + res.edCtx, res.edCancelFn = context.WithCancel(context.Background()) + + // don't kick things off right away in case user is typing fast + go func(ctx context.Context) { + select { + case <-ctx.Done(): + return + case <-time.After(time.Millisecond * 200): + break + } + + res.filterchangedchan <- &filtStruct{ed.Text(), app} + + app.Run(gowid.RunFunction(func(app gowid.IApp) { + _, y := app.GetScreen().Size() + makeCompletions(res.fields, ed.Text(), y, app, func(completions []string, app gowid.IApp) { + app.Run(gowid.RunFunction(func(app gowid.IApp) { + res.processCompletions(completions, app) + })) + }) + })) + }(res.edCtx) + }))) + + return res +} + +type IValidateCB interface { + Call(filter string) +} + +type AppFilterCB func(gowid.IApp) + +type ValidateCB struct { + App gowid.IApp + Fn AppFilterCB +} + +var _ IValidateCB = (*ValidateCB)(nil) + +func (v *ValidateCB) Call(filter string) { + v.Fn(v.App) +} + +type Validator struct { + Valid IValidateCB + Invalid IValidateCB + KilledCB IValidateCB + Cmd *exec.Cmd +} + +func (f *Validator) Kill() (bool, error) { + var err error + var res bool + if f.Cmd != nil { + proc := f.Cmd.Process + if proc != nil { + res = true + err = proc.Kill() + } + } + return res, err +} + +func (f *Validator) Validate(filter string) { + var err error + + if filter != "" { + f.Cmd = exec.Command(termshark.TSharkBin(), []string{"-Y", filter, "-r", termshark.CacheFile("empty.pcap")}...) + err = f.Cmd.Run() + } + + if err == nil { + if f.Valid != nil { + f.Valid.Call(filter) + } + } else { + killed := true + if exiterr, ok := err.(*exec.ExitError); ok { + if status, ok := exiterr.Sys().(syscall.WaitStatus); ok { + if status.ExitStatus() == 2 { + killed = false + } + } + } + if killed { + if f.KilledCB != nil { + f.KilledCB.Call(filter) + } + } else { + if f.Invalid != nil { + f.Invalid.Call(filter) + } + } + } +} + +func filterOutEnter(evk *tcell.EventKey, app gowid.IApp) bool { + handled := false + switch evk.Key() { + case tcell.KeyEnter: + handled = true + } + return handled +} + +func newMenuWidgets(ed *edit.Widget, completions []string) []gowid.IWidget { + menu2Widgets := make([]gowid.IWidget, 0) + + fixed := gowid.RenderFixed{} + for _, s := range completions { + scopy := s + + clickme := button.New( + hpadding.New( + text.New(s), + gowid.HAlignLeft{}, + gowid.RenderWithUnits{U: gwutil.Max(12, len(s))}, + ), + button.Options{ + Decoration: button.BareDecoration, + SelectKeysProvided: true, + SelectKeys: []gowid.IKey{gowid.MakeKeyExt(tcell.KeyEnter)}, + }, + ) + clickmeStyled := styled.NewInvertedFocus(clickme, gowid.MakePaletteRef("filter-menu-focus")) + clickme.OnClick(gowid.MakeWidgetCallback(gowid.ClickCB{}, func(app gowid.IApp, target gowid.IWidget) { + txt := ed.Text() + end := ed.CursorPos() + start := end + for { + if start == 0 { + break + } + if start < len(txt) && txt[start] == ' ' { + start++ + break + } + start-- + } + ed.SetText(fmt.Sprintf("%s%s%s", txt[0:start], scopy, txt[end:len(txt)]), app) + ed.SetCursorPos(len(txt[0:start])+len(scopy), app) + + })) + cols := columns.New([]gowid.IContainerWidget{ + &gowid.ContainerWidget{IWidget: clickmeStyled, D: fixed}, + }) + + menu2Widgets = append(menu2Widgets, cols) + } + + return menu2Widgets +} + +type fnCallback struct { + app gowid.IApp + fn func([]string, gowid.IApp) +} + +var _ termshark.IPrefixCompleterCallback = fnCallback{} + +func (f fnCallback) Call(res []string) { + f.fn(res, f.app) +} + +func makeCompletions(comp termshark.IPrefixCompleter, txt string, max int, app gowid.IApp, fn func([]string, gowid.IApp)) { + cb := fnCallback{ + app: app, + fn: func(completions []string, app gowid.IApp) { + completions = completions[0:gwutil.Min(max, len(completions))] + fn(completions, app) + }, + } + comp.Completions(txt, cb) +} + +func (w *Widget) UpdateCompletions(app gowid.IApp) { + makeCompletions(w.fields, "", w.opts.MaxCompletions, app, func(completions []string, app gowid.IApp) { + w.processCompletions(completions, app) + }) +} + +func (w *Widget) processCompletions(completions []string, app gowid.IApp) { + max := w.opts.MaxCompletions + for _, c := range completions { + max = gwutil.Max(max, len(c)) + } + + menu2Widgets := newMenuWidgets(w.ed, completions) + w.completions = completions + app.Run(gowid.RunFunction(func(app gowid.IApp) { + w.completionsList.SetWalker(list.NewSimpleListWalker(menu2Widgets), app) + w.dropDown.SetWidth(gowid.RenderWithUnits{U: max + 2}, app) + })) +} + +func (w *Widget) Close() error { + // Two for the aggregator goroutine and the filter runner goroutine + w.quitchan <- struct{}{} + w.quitchan <- struct{}{} + return nil +} + +func (w *Widget) OnIntermediate(f gowid.IWidgetChangedCallback) { + gowid.AddWidgetCallback(w, IntermediateCB{}, f) +} + +func (w *Widget) OnValid(f gowid.IWidgetChangedCallback) { + gowid.AddWidgetCallback(w, ValidCB{}, f) +} + +func (w *Widget) OnInvalid(f gowid.IWidgetChangedCallback) { + gowid.AddWidgetCallback(w, InvalidCB{}, f) +} + +func (w *Widget) IsValid() bool { + return w.validitySite.SubWidget() == w.valid +} + +func (w *Widget) Value() string { + return w.ed.Text() +} + +func (w *Widget) SetValue(v string, app gowid.IApp) { + w.ed.SetText(v, app) +} + +func (w *Widget) Menus() []gowid.IMenuCompatible { + return []gowid.IMenuCompatible{w.dropDown} +} + +func (w *Widget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { + return gowid.RenderSize(w.wrapped, size, focus, app) +} + +func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { + if focus.Focus && len(w.completions) > 0 { + w.dropDown.Open(w.dropDownSite, app) + } else { + w.dropDown.Close(app) + } + return w.wrapped.Render(size, focus, app) +} + +// Reject tab because I want it to switch views. Not intended to be transferable. +func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { + if evk, ok := ev.(*tcell.EventKey); ok && evk.Key() == tcell.KeyTAB { + return false + } + return gowid.UserInput(w.wrapped, ev, size, focus, app) +} + +//====================================================================== +// Local Variables: +// mode: Go +// fill-column: 110 +// End: diff --git a/widgets/hexdumper/hexdumper.go b/widgets/hexdumper/hexdumper.go new file mode 100644 index 0000000..2faa455 --- /dev/null +++ b/widgets/hexdumper/hexdumper.go @@ -0,0 +1,619 @@ +// Copyright 2019 Graham Clark. All rights reserved. Use of this source +// code is governed by the MIT license that can be found in the LICENSE +// file. + +// Package hexdumper provides a widget which displays selectable hexdump-like +// output. Because it's built for termshark, it also allows styling to be +// applied to ranges of data intended to correspond to packet structure selected +// in another termshark view. +package hexdumper + +import ( + "bytes" + "encoding/hex" + "fmt" + "strings" + "unicode" + + "github.com/gcla/gowid" + "github.com/gcla/gowid/widgets/button" + "github.com/gcla/gowid/widgets/columns" + "github.com/gcla/gowid/widgets/palettemap" + "github.com/gcla/gowid/widgets/pile" + "github.com/gcla/gowid/widgets/styled" + "github.com/gcla/gowid/widgets/text" + "github.com/gcla/termshark" + "github.com/gcla/termshark/widgets/renderfocused" + "github.com/gdamore/tcell" + "github.com/pkg/errors" +) + +//====================================================================== + +type LayerStyler struct { + Start int + End int + ColUnselected string + ColSelected string +} + +type PositionChangedCB struct{} + +//====================================================================== + +type boxedText struct { + width int + gowid.IWidget +} + +func (h boxedText) String() string { + return fmt.Sprintf("[hacktext %v]", h.IWidget) +} + +func (h boxedText) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { + return gowid.RenderBox{C: h.width, R: 1} +} + +//====================================================================== + +type Widget struct { + w gowid.IWidget + data []byte + layers []LayerStyler + chrs []boxedText + cursorUnselected string + cursorSelected string + lineNumUnselected string + lineNumSelected string + paletteIfCopying string + gowid.AddressProvidesID + styled.UsePaletteIfSelectedForCopy + Callbacks *gowid.Callbacks + gowid.IsSelectable +} + +var _ gowid.IWidget = (*Widget)(nil) +var _ gowid.IIdentityWidget = (*Widget)(nil) +var _ gowid.IClipboard = (*Widget)(nil) +var _ gowid.IClipboardSelected = (*Widget)(nil) + +func New(data []byte, layers []LayerStyler, + cursorUnselected string, cursorSelected string, + lineNumUnselected string, lineNumSelected string, + paletteIfCopying string) *Widget { + + res := &Widget{ + data: data, + layers: layers, + cursorUnselected: cursorUnselected, + cursorSelected: cursorSelected, + lineNumUnselected: lineNumUnselected, + lineNumSelected: lineNumSelected, + paletteIfCopying: paletteIfCopying, + UsePaletteIfSelectedForCopy: styled.UsePaletteIfSelectedForCopy{Entry: paletteIfCopying}, + Callbacks: gowid.NewCallbacks(), + } + + res.chrs = make([]boxedText, 256) + for i := 0; i < 256; i++ { + if unicode.IsPrint(rune(i)) { + // copyable text widgets need a unique ID, so gowid can tell if the current focus + // widget (moving up the hierarchy) is the one claiming the copy + res.chrs[i] = boxedText{ + width: 1, + IWidget: text.NewCopyable(string(rune(i)), hexChrsId{i}, styled.UsePaletteIfSelectedForCopy{Entry: paletteIfCopying}), + } + } + } + + res.w = res.Build(0) + return res +} + +func (w *Widget) OnPositionChanged(f gowid.IWidgetChangedCallback) { + gowid.AddWidgetCallback(w.Callbacks, PositionChangedCB{}, f) +} + +func (w *Widget) RemoveOnPositionChanged(f gowid.IIdentity) { + gowid.RemoveWidgetCallback(w.Callbacks, PositionChangedCB{}, f) +} + +func (w *Widget) String() string { + return "hexdump" +} + +func (w *Widget) CursorUnselected() string { + return w.cursorUnselected +} +func (w *Widget) CursorSelected() string { + return w.cursorSelected +} +func (w *Widget) LineNumUnselected() string { + return w.lineNumUnselected +} +func (w *Widget) LineNumSelected() string { + return w.lineNumSelected +} + +func (w *Widget) Layers() []LayerStyler { + return w.layers +} + +func (w *Widget) SetLayers(layers []LayerStyler, app gowid.IApp) { + w.layers = layers + pos := w.Position() + inhex := w.InHex() + w.w = w.Build(pos) + w.SetInHex(inhex, app) + w.SetPosition(pos, app) +} + +func (w *Widget) Data() []byte { + return w.data +} + +func (w *Widget) SetData(data []byte, app gowid.IApp) { + w.data = data + pos := w.Position() + inhex := w.InHex() + w.w = w.Build(pos) + w.SetInHex(inhex, app) + w.SetPosition(pos, app) +} + +func (w *Widget) InHex() bool { + fp := gowid.FocusPath(w.w) + if len(fp) < 3 { + panic(errors.WithStack(gowid.WithKVs(termshark.BadState, map[string]interface{}{"focus path": fp}))) + } + return fp[0] == 3 +} + +func (w *Widget) SetInHex(val bool, app gowid.IApp) { + fp := gowid.FocusPath(w.w) + if len(fp) < 3 { + panic(errors.WithStack(gowid.WithKVs(termshark.BadState, map[string]interface{}{"focus path": fp}))) + } + if val { + if fp[0].(int) == 3 { + return + } + // from 7 to 3 + fp[0] = 3 + x := fp[2].(int) + if x > 7 { + fp[2] = (x * 2) - 1 + } else { + fp[2] = x * 2 + } + } else { + if fp[0].(int) == 7 { + return + } + // from 3 to 7 + fp[0] = 7 + x := fp[2].(int) + if x > 14 { + fp[2] = (x + 1) / 2 + } else { + fp[2] = x / 2 + } + } + gowid.SetFocusPath(w.w, fp, app) +} + +func (w *Widget) Position() int { + fp := gowid.FocusPath(w.w) + if len(fp) < 3 { + panic(gowid.WithKVs(termshark.BadState, map[string]interface{}{"focus path": fp})) + } + if fp[0] == 3 { + // in hex + x := fp[2].(int) + if x > 14 { + return (fp[1].(int) * 16) + (x / 2) // same as below + } else { + return (fp[1].(int) * 16) + (x / 2) + } + } else { + // in ascii + x := fp[2].(int) + if x > 7 { + return (fp[1].(int) * 16) + (x - 1) + } else { + return (fp[1].(int) * 16) + x + } + } +} + +func (w *Widget) SetPosition(pos int, app gowid.IApp) { + fp := gowid.FocusPath(w.w) + if len(fp) < 3 { + panic(gowid.WithKVs(termshark.BadState, map[string]interface{}{"focus path": fp})) + } + curpos := w.Position() + fp[1] = pos / 16 + if fp[0] == 3 { + // from 3 to 7 + if pos%16 > 7 { + fp[2] = ((pos % 16) * 2) + 1 + } else { + fp[2] = (pos % 16) * 2 + } + } else { + if pos%16 > 7 { + fp[2] = pos%16 + 1 + } else { + fp[2] = pos % 16 + } + } + gowid.SetFocusPath(w.w, fp, app) + if curpos != pos { + gowid.RunWidgetCallbacks(w.Callbacks, PositionChangedCB{}, app, w) + } +} + +type viewSwitchFn func(ev *tcell.EventKey) bool + +type viewSwitch struct { + w *Widget + fn viewSwitchFn +} + +// Compatible with appkeys.Widget +func (v viewSwitch) SwitchView(ev *tcell.EventKey, app gowid.IApp) bool { + if v.fn(ev) { + v.w.SetInHex(!v.w.InHex(), app) + return true + } + return false +} + +func (w *Widget) OnKey(fn viewSwitchFn) viewSwitch { + return viewSwitch{ + w: w, + fn: fn, + } +} + +func (w *Widget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { + return gowid.RenderSize(w.w, size, focus, app) +} + +type privateId struct { + *Widget +} + +func (d privateId) ID() interface{} { + return d +} + +func (d privateId) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { + // Skip the embedded Widget to avoid a loop + return gowid.Render(d.w, size, focus, app) +} + +func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { + if app.InCopyMode() && app.CopyModeClaimedBy().ID() == w.ID() && focus.Focus { + + var wa gowid.IWidget + diff := app.CopyModeClaimedAt() - app.CopyLevel() + if diff == 0 { + wa = w.AlterWidget(privateId{w}, app) // whole hexdump + } else { + layerConv := make(map[string]string) + for i := diff - 1; i < len(w.Layers()); i++ { + layerConv[w.layers[i].ColSelected] = "copy-mode" // only right layers + } + wa = palettemap.New(privateId{w}, layerConv, layerConv) + } + return gowid.Render(wa, size, focus, app) + } else { + return gowid.Render(w.w, size, focus, app) + } +} + +func MakeEscapedString(data []byte) string { + res := make([]string, 0) + var buffer bytes.Buffer + for i := 0; i < len(data); i++ { + buffer.WriteString(fmt.Sprintf("\\x%02x", data[i])) + if i%16 == 16-1 || i+1 == len(data) { + res = append(res, fmt.Sprintf("\"%s\"", buffer.String())) + buffer.Reset() + } + } + return strings.Join(res, " \\\n") +} + +func MakeHexStream(data []byte) string { + var buffer bytes.Buffer + for i := 0; i < len(data); i++ { + buffer.WriteString(fmt.Sprintf("%02x", data[i])) + } + return buffer.String() +} + +func MakePrintableString(data []byte) string { + var buffer bytes.Buffer + for i := 0; i < len(data); i++ { + if unicode.IsPrint(rune(data[i])) { + buffer.WriteString(string(rune(data[i]))) + } + } + return buffer.String() +} + +func clipsForBytes(data []byte, start int, end int) []gowid.ICopyResult { + dump := hex.Dump(data[start:end]) + dump2 := MakeEscapedString(data[start:end]) + dump3 := MakePrintableString(data[start:end]) + dump4 := MakeHexStream(data[start:end]) + + return []gowid.ICopyResult{ + gowid.CopyResult{ + Name: fmt.Sprintf("Copy bytes %d-%d as hex + ascii", start, end), + Val: dump, + }, + gowid.CopyResult{ + Name: fmt.Sprintf("Copy bytes %d-%d as escaped string", start, end), + Val: dump2, + }, + gowid.CopyResult{ + Name: fmt.Sprintf("Copy bytes %d-%d as printable string", start, end), + Val: dump3, + }, + gowid.CopyResult{ + Name: fmt.Sprintf("Copy bytes %d-%d as hex stream", start, end), + Val: dump4, + }, + } +} + +func (w *Widget) Clips(app gowid.IApp) []gowid.ICopyResult { + + diff := app.CopyModeClaimedAt() - app.CopyLevel() + if diff == 0 { + return clipsForBytes(w.Data(), 0, len(w.Data())) + } else { + return clipsForBytes(w.Data(), w.layers[diff-1].Start, w.layers[diff-1].End) + } +} + +// Reject tab because I want it to switch views. Not intended to be transferable. +func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { + res := false + if _, ok := ev.(gowid.CopyModeEvent); ok { + if app.CopyModeClaimedAt() >= app.CopyLevel() && app.CopyModeClaimedAt() < app.CopyLevel()+len(w.Layers())+1 { + app.CopyModeClaimedBy(w) + res = true + } else { + cl := app.CopyLevel() + app.CopyLevel(cl + len(w.Layers()) + 1) // this is how many levels hexdumper will support + res = gowid.UserInput(w.w, ev, size, focus, app) + app.CopyLevel(cl) + + if !res { + app.CopyModeClaimedAt(app.CopyLevel() + len(w.Layers())) + app.CopyModeClaimedBy(w) + } + } + } else if evc, ok := ev.(gowid.CopyModeClipsEvent); ok && (app.CopyModeClaimedAt() >= app.CopyLevel() && app.CopyModeClaimedAt() < app.CopyLevel()+len(w.Layers())+1) { + evc.Action.Collect(w.Clips(app)) + res = true + } else { + cur := w.Position() + res = gowid.UserInput(w.w, ev, size, focus, app) + + if res { + newpos := w.Position() + if newpos != cur { + gowid.RunWidgetCallbacks(w.Callbacks, PositionChangedCB{}, app, w) + } + } + } + return res +} + +//====================================================================== + +func init() { + twosp = boxedText{width: 2, IWidget: text.New(" ")} + onesp = boxedText{width: 1, IWidget: text.New(" ")} + dot = boxedText{width: 1, IWidget: text.New(".")} + pad = &boxedText{width: 1, IWidget: text.New(" ")} +} + +type hexChrsId struct { + idx int +} + +func (h hexChrsId) ID() interface{} { + return h +} + +var twosp boxedText +var onesp boxedText +var dot boxedText +var pad *boxedText + +type IHexBuilder interface { + Data() []byte + Layers() []LayerStyler + CursorUnselected() string + CursorSelected() string + LineNumUnselected() string + LineNumSelected() string +} + +func (w *Widget) Build(curpos int) gowid.IWidget { + data := w.Data() + layers := w.Layers() + + hexBytes := make([]interface{}, 0, 16*2+1) + asciiBytes := make([]interface{}, 0, 16+1) + + fixed := gowid.RenderFixed{} + + hexRows := make([]interface{}, 0) + asciiRows := make([]interface{}, 0) + + dlen := ((len(data) + 15) / 16) * 16 // round up to nearest chunk of 16 + + layerConv := make(map[string]string) + for _, layer := range layers { + layerConv[layer.ColUnselected] = layer.ColSelected + } + layerConv[w.CursorUnselected()] = w.CursorSelected() + layerConv[w.LineNumUnselected()] = w.LineNumSelected() + + var active gowid.ICellStyler // for styling the hex data "41" and the ascii "A" + var spactive gowid.ICellStyler // for styling the spaces between data e.g. "41 42" + + for i := 0; i < dlen; i++ { + active = nil + spactive = nil + + for _, layer := range layers { + if i >= layer.Start && i < layer.End { + active = gowid.MakePaletteRef(layer.ColUnselected) + } + if i >= layer.Start && i < layer.End-1 { + spactive = gowid.MakePaletteRef(layer.ColUnselected) + } + } + + var curHex gowid.IWidget + var curAscii gowid.IWidget + if i >= len(data) { + curHex = twosp + curAscii = onesp + } else { + hexBtn := w.newButtonFromByte(i, data[i]) + + curHex = hexBtn + curHex = styled.NewFocus(curHex, gowid.MakePaletteRef(w.CursorUnselected())) + if active != nil { + curHex = styled.New(curHex, active) + } + + asciiBtn := w.newAsciiFromByte(data[i]) + + curAscii = asciiBtn + curAscii = styled.NewFocus(curAscii, gowid.MakePaletteRef(w.CursorUnselected())) + if active != nil { + curAscii = styled.New(curAscii, active) + } + } + + hexBytes = append(hexBytes, curHex) + asciiBytes = append(asciiBytes, curAscii) + + if (i+1)%16 == 0 { + hexRow := columns.NewFixed(hexBytes...) + hexRows = append(hexRows, hexRow) + hexBytes = make([]interface{}, 0, 16*2+1) + + asciiRow := columns.NewFixed(asciiBytes...) + asciiRows = append(asciiRows, asciiRow) + asciiBytes = make([]interface{}, 0, 16+1) + } else { + // Put a blank between the buttons + var blank gowid.IWidget = onesp + if spactive != nil { + blank = styled.New(blank, spactive) + } + + hexBytes = append(hexBytes, blank) + // separator in middle of row + if (i+1)%16 == 8 { + hexBytes = append(hexBytes, blank) + asciiBytes = append(asciiBytes, blank) + } + } + } + + hexPile := pile.NewWithDim(fixed, hexRows...) + asciiPile := pile.NewWithDim(fixed, asciiRows...) + + lines := make([]interface{}, 0) + + for i := 0; i < dlen; i += 16 { + active := false + var txt gowid.IWidget = text.New(fmt.Sprintf(" %04x ", i)) + for _, layer := range layers { + if i+16 >= layer.Start && i < layer.End { + active = true + break + } + } + if active { + txt = styled.New(txt, gowid.MakePaletteRef(w.LineNumUnselected())) + } + lines = append(lines, txt) + } + + linesPile := pile.NewWithDim(fixed, lines...) + + layout := columns.NewFixed(linesPile, pad, pad, hexPile, pad, pad, pad, asciiPile) + + // When the whole widget (that fills the panel) is in focus (not down to the subwidgets yet) + // then change the palette to use bright colors + layoutFocused := renderfocused.New(layout) + + res := palettemap.New( + layoutFocused, + layerConv, + palettemap.Map{}, + ) + + return res +} + +func toChar(b byte) byte { + if b < 32 || b > 126 { + return '.' + } + return b +} + +type hexBytesId struct { + idx int +} + +func (h hexBytesId) ID() interface{} { + return h +} + +const hextable = "0123456789abcdef" + +func (w *Widget) newButtonFromByte(idx int, v byte) *button.Widget { + var dst [2]byte + + dst[0] = hextable[v>>4] + dst[1] = hextable[v&0x0f] + + return button.NewBare(boxedText{ + width: 2, + IWidget: text.NewCopyable( + string(dst[:]), + hexBytesId{idx}, + styled.UsePaletteIfSelectedForCopy{Entry: w.paletteIfCopying}, + ), + }) +} + +func (w *Widget) newAsciiFromByte(v byte) *button.Widget { + r := rune(v) + if r < 32 || r > 126 { + return button.NewBare(dot) + } else { + return button.NewBare(w.chrs[int(r)]) + } +} + +//====================================================================== +// Local Variables: +// mode: Go +// fill-column: 110 +// End: diff --git a/widgets/hexdumper/hexdumper_test.go b/widgets/hexdumper/hexdumper_test.go new file mode 100644 index 0000000..69f2cfd --- /dev/null +++ b/widgets/hexdumper/hexdumper_test.go @@ -0,0 +1,40 @@ +// Copyright 2019 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license +// that can be found in the LICENSE file. + +package hexdumper + +import ( + "testing" + + "github.com/gcla/gowid" + "github.com/gcla/gowid/gwtest" + log "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" +) + +//====================================================================== + +func TestDump1(t *testing.T) { + widget1 := New([]byte("abcdefghijklmnopqrstuvwxyz0123456789 abcdefghijklmnopqrstuvwxyz0123456789"), + []LayerStyler{}, + "default", "default", "default", "default", "default") + //stylers: []LayerStyler{styler}, + canvas1 := widget1.Render(gowid.RenderFlowWith{C: 80}, gowid.NotSelected, gwtest.D) + log.Infof("Canvas1 is %s", canvas1.String()) + assert.Equal(t, 5, canvas1.BoxRows()) +} + +func TestDump2(t *testing.T) { + widget1 := New([]byte(""), + []LayerStyler{}, + "default", "default", "default", "default", "default") + canvas2 := widget1.Render(gowid.RenderFlowWith{C: 60}, gowid.NotSelected, gwtest.D) + log.Infof("Canvas2 is %s", canvas2.String()) + assert.Equal(t, 1, canvas2.BoxRows()) +} + +//====================================================================== +// Local Variables: +// mode: Go +// fill-column: 110 +// End: diff --git a/widgets/ifwidget/ifwidget.go b/widgets/ifwidget/ifwidget.go new file mode 100644 index 0000000..03d4f8e --- /dev/null +++ b/widgets/ifwidget/ifwidget.go @@ -0,0 +1,97 @@ +// Copyright 2019 Graham Clark. All rights reserved. Use of this source +// code is governed by the MIT license that can be found in the LICENSE +// file. + +// Package ifwidget provides a simple widget that behaves differently depending on the condition +// supplied. +package ifwidget + +import ( + "fmt" + + "github.com/gcla/gowid" +) + +//====================================================================== + +type Widget struct { + wtrue gowid.IWidget + wfalse gowid.IWidget + pred Predicate +} + +var _ gowid.IWidget = (*Widget)(nil) +var _ gowid.ICompositeWidget = (*Widget)(nil) + +type Predicate func() bool + +func New(wtrue gowid.IWidget, wfalse gowid.IWidget, pred Predicate) *Widget { + res := &Widget{ + wtrue: wtrue, + wfalse: wfalse, + pred: pred, + } + return res +} + +func (w *Widget) String() string { + return fmt.Sprintf("ifwidget[%v]", w.SubWidget()) +} + +func (w *Widget) SubWidget() gowid.IWidget { + if w.pred() { + return w.wtrue + } else { + return w.wfalse + } +} + +func (w *Widget) SetSubWidget(wi gowid.IWidget, app gowid.IApp) { + if w.pred() { + w.wtrue = wi + } else { + w.wfalse = wi + } +} + +func (w *Widget) SubWidgetSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderSize { + return size +} + +func (w *Widget) Selectable() bool { + if w.pred() { + return w.wtrue.Selectable() + } else { + return w.wfalse.Selectable() + } +} + +func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { + if w.pred() { + return gowid.UserInput(w.wtrue, ev, size, focus, app) + } else { + return gowid.UserInput(w.wfalse, ev, size, focus, app) + } +} + +func (w *Widget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { + if w.pred() { + return gowid.RenderSize(w.wtrue, size, focus, app) + } else { + return gowid.RenderSize(w.wfalse, size, focus, app) + } +} + +func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { + if w.pred() { + return gowid.Render(w.wtrue, size, focus, app) + } else { + return gowid.Render(w.wfalse, size, focus, app) + } +} + +//====================================================================== +// Local Variables: +// mode: Go +// fill-column: 110 +// End: diff --git a/widgets/renderfocused/renderfocused.go b/widgets/renderfocused/renderfocused.go new file mode 100644 index 0000000..1b86f8d --- /dev/null +++ b/widgets/renderfocused/renderfocused.go @@ -0,0 +1,56 @@ +// Copyright 2019 Graham Clark. All rights reserved. Use of this source +// code is governed by the MIT license that can be found in the LICENSE +// file. + +// Package renderfocused will render a widget with focus true +package renderfocused + +import ( + "github.com/gcla/gowid" +) + +//====================================================================== + +type Widget struct { + gowid.IWidget +} + +var _ gowid.IWidget = (*Widget)(nil) +var _ gowid.ICompositeWidget = (*Widget)(nil) + +func New(w gowid.IWidget) *Widget { + return &Widget{ + IWidget: w, + } +} + +func (w *Widget) SubWidget() gowid.IWidget { + return w.IWidget +} + +func (w *Widget) SubWidgetSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderSize { + return w.SubWidget().RenderSize(size, focus, app) +} + +func (w *Widget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { + return gowid.RenderSize(w.IWidget, size, gowid.Focused, app) +} + +func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { + return gowid.Render(w.IWidget, size, gowid.Focused, app) +} + +func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { + return gowid.UserInput(w.IWidget, ev, size, focus, app) +} + +// TODO - this isn't right. Should Selectable be conditioned on focus? +func (w *Widget) Selectable() bool { + return w.IWidget.Selectable() +} + +//====================================================================== +// Local Variables: +// mode: Go +// fill-column: 110 +// End: diff --git a/widgets/resizable/resizable.go b/widgets/resizable/resizable.go new file mode 100644 index 0000000..ad23b37 --- /dev/null +++ b/widgets/resizable/resizable.go @@ -0,0 +1,246 @@ +// Copyright 2019 Graham Clark. All rights reserved. Use of this source +// code is governed by the MIT license that can be found in the LICENSE +// file. + +// Package resizable provides columns and piles that can be adjusted. +package resizable + +import ( + "github.com/gcla/gowid" + "github.com/gcla/gowid/widgets/columns" + "github.com/gcla/gowid/widgets/pile" +) + +//====================================================================== + +type Offset struct { + Col1 int `json:"col1"` + Col2 int `json:"col2"` + Adjust int `json:"adjust"` +} + +type IOffsets interface { + GetOffsets() []Offset + SetOffsets([]Offset, gowid.IApp) +} + +type OffsetsCB struct{} + +type ColumnsWidget struct { + *columns.Widget + Offsets []Offset + Callbacks *gowid.Callbacks +} + +var _ IOffsets = (*ColumnsWidget)(nil) + +func NewColumns(widgets []gowid.IContainerWidget) *ColumnsWidget { + res := &ColumnsWidget{ + Widget: columns.New(widgets), + Offsets: make([]Offset, 0, 2), + Callbacks: gowid.NewCallbacks(), + } + return res +} + +func (w *ColumnsWidget) GetOffsets() []Offset { + return w.Offsets +} + +func (w *ColumnsWidget) SetOffsets(offs []Offset, app gowid.IApp) { + w.Offsets = offs +} + +func (w *ColumnsWidget) OnOffsetsSet(cb gowid.IWidgetChangedCallback) { + gowid.AddWidgetCallback(w.Callbacks, OffsetsCB{}, cb) +} + +func (w *ColumnsWidget) RemoveOnOffsetsSet(cb gowid.IIdentity) { + gowid.RemoveWidgetCallback(w.Callbacks, OffsetsCB{}, cb) +} + +type AdjustFn func(x int) int + +var Add1 AdjustFn = func(x int) int { + return x + 1 +} + +var Subtract1 AdjustFn = func(x int) int { + return x - 1 +} + +func (w *ColumnsWidget) AdjustOffset(col1 int, col2 int, fn AdjustFn, app gowid.IApp) { + AdjustOffset(w, col1, col2, fn, app) + gowid.RunWidgetCallbacks(w.Callbacks, OffsetsCB{}, app, w) +} + +func AdjustOffset(w IOffsets, col1 int, col2 int, fn AdjustFn, app gowid.IApp) { + idx := -1 + var off Offset + for i, o := range w.GetOffsets() { + if o.Col1 == col1 && o.Col2 == col2 { + idx = i + break + } + } + if idx == -1 { + off.Col1 = col1 + off.Col2 = col2 + w.SetOffsets(append(w.GetOffsets(), off), app) + idx = len(w.GetOffsets()) - 1 + } + w.GetOffsets()[idx].Adjust = fn(w.GetOffsets()[idx].Adjust) +} + +func (w *ColumnsWidget) WidgetWidths(size gowid.IRenderSize, focus gowid.Selector, focusIdx int, app gowid.IApp) []int { + widths := w.Widget.WidgetWidths(size, focus, focusIdx, app) + for _, off := range w.Offsets { + addme := off.Adjust + if widths[off.Col1]+addme < 0 { + addme = -widths[off.Col1] + } else if widths[off.Col2]-addme < 0 { + addme = widths[off.Col2] + } + widths[off.Col1] += addme + widths[off.Col2] -= addme + } + return widths +} + +func (w *ColumnsWidget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { + return columns.Render(w, size, focus, app) +} + +func (w *ColumnsWidget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { + return columns.UserInput(w, ev, size, focus, app) +} + +func (w *ColumnsWidget) RenderSubWidgets(size gowid.IRenderSize, focus gowid.Selector, focusIdx int, app gowid.IApp) []gowid.ICanvas { + return columns.RenderSubWidgets(w, size, focus, focusIdx, app) +} + +func (w *ColumnsWidget) RenderedSubWidgetsSizes(size gowid.IRenderSize, focus gowid.Selector, focusIdx int, app gowid.IApp) []gowid.IRenderBox { + return columns.RenderedSubWidgetsSizes(w, size, focus, focusIdx, app) +} + +func (w *ColumnsWidget) SubWidgetSize(size gowid.IRenderSize, newX int, sub gowid.IWidget, dim gowid.IWidgetDimension) gowid.IRenderSize { + return w.Widget.SubWidgetSize(size, newX, sub, dim) +} + +//====================================================================== + +type PileWidget struct { + *pile.Widget + Offsets []Offset + Callbacks *gowid.Callbacks +} + +func NewPile(widgets []gowid.IContainerWidget) *PileWidget { + res := &PileWidget{ + Widget: pile.New(widgets), + Offsets: make([]Offset, 0, 2), + Callbacks: gowid.NewCallbacks(), + } + return res +} + +var _ IOffsets = (*ColumnsWidget)(nil) + +func (w *PileWidget) GetOffsets() []Offset { + return w.Offsets +} + +func (w *PileWidget) SetOffsets(offs []Offset, app gowid.IApp) { + w.Offsets = offs +} + +func (w *PileWidget) OnOffsetsSet(cb gowid.IWidgetChangedCallback) { + gowid.AddWidgetCallback(w.Callbacks, OffsetsCB{}, cb) +} + +func (w *PileWidget) RemoveOnOffsetsSet(cb gowid.IIdentity) { + gowid.RemoveWidgetCallback(w.Callbacks, OffsetsCB{}, cb) +} + +func (w *PileWidget) AdjustOffset(col1 int, col2 int, fn AdjustFn, app gowid.IApp) { + AdjustOffset(w, col1, col2, fn, app) + gowid.RunWidgetCallbacks(w.Callbacks, OffsetsCB{}, app, w) +} + +type PileAdjuster struct { + widget *PileWidget + origSizer pile.IPileBoxMaker +} + +func (f PileAdjuster) MakeBox(w gowid.IWidget, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { + adjustedSize := size + var box gowid.RenderBox + isbox := false + switch size := size.(type) { + case gowid.IRenderBox: + box.C = size.BoxColumns() + box.R = size.BoxRows() + isbox = true + } + i := 0 + for ; i < len(f.widget.SubWidgets()); i++ { + if w == f.widget.SubWidgets()[i] { + break + } + } + if i == len(f.widget.SubWidgets()) { + panic("Unexpected pile state!") + } + if isbox { + for _, off := range f.widget.Offsets { + if i == off.Col1 { + if box.R+off.Adjust < 0 { + off.Adjust = -box.R + } + box.R += off.Adjust + } else if i == off.Col2 { + if box.R-off.Adjust < 0 { + off.Adjust = box.R + } + box.R -= off.Adjust + } + } + adjustedSize = box + } + return f.origSizer.MakeBox(w, adjustedSize, focus, app) +} + +func (w *PileWidget) FindNextSelectable(dir gowid.Direction, wrap bool) (int, bool) { + return gowid.FindNextSelectableFrom(w, w.Focus(), dir, wrap) +} + +func (w *PileWidget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { + return pile.UserInput(w, ev, size, focus, app) +} + +func (w *PileWidget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { + return pile.Render(w, size, focus, app) +} + +func (w *PileWidget) RenderedSubWidgetsSizes(size gowid.IRenderSize, focus gowid.Selector, focusIdx int, app gowid.IApp) []gowid.IRenderBox { + res, _ := pile.RenderedChildrenSizes(w, size, focus, focusIdx, app) + return res +} + +func (w *PileWidget) RenderSubWidgets(size gowid.IRenderSize, focus gowid.Selector, focusIdx int, app gowid.IApp) []gowid.ICanvas { + return pile.RenderSubwidgets(w, size, focus, focusIdx, app) +} + +func (w *PileWidget) RenderBoxMaker(size gowid.IRenderSize, focus gowid.Selector, focusIdx int, app gowid.IApp, sizer pile.IPileBoxMaker) ([]gowid.IRenderBox, []gowid.IRenderSize) { + x := &PileAdjuster{ + widget: w, + origSizer: sizer, + } + return pile.RenderBoxMaker(w, size, focus, focusIdx, app, x) +} + +//====================================================================== +// Local Variables: +// mode: Go +// fill-column: 110 +// End: diff --git a/widgets/resizable/resizable_test.go b/widgets/resizable/resizable_test.go new file mode 100644 index 0000000..c557de8 --- /dev/null +++ b/widgets/resizable/resizable_test.go @@ -0,0 +1,37 @@ +// Copyright 2019 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license +// that can be found in the LICENSE file. + +package resizable + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" +) + +//====================================================================== + +func TestOffset1(t *testing.T) { + off1 := Offset{2, 4, 7} + off1m, err := json.Marshal(off1) + assert.NoError(t, err) + assert.Equal(t, "{\"col1\":2,\"col2\":4,\"adjust\":7}", string(off1m)) + + off2 := Offset{3, 1, 15} + offs := []Offset{off1, off2} + offsm, err := json.Marshal(offs) + assert.NoError(t, err) + assert.Equal(t, "[{\"col1\":2,\"col2\":4,\"adjust\":7},{\"col1\":3,\"col2\":1,\"adjust\":15}]", string(offsm)) + + offs2 := make([]Offset, 0) + err = json.Unmarshal(offsm, &offs2) + assert.NoError(t, err) + assert.Equal(t, offs, offs2) +} + +//====================================================================== +// Local Variables: +// mode: Go +// fill-column: 110 +// End: diff --git a/widgets/withscrollbar/withscrollbar.go b/widgets/withscrollbar/withscrollbar.go new file mode 100644 index 0000000..997f182 --- /dev/null +++ b/widgets/withscrollbar/withscrollbar.go @@ -0,0 +1,149 @@ +// Copyright 2019 Graham Clark. All rights reserved. Use of this source +// code is governed by the MIT license that can be found in the LICENSE +// file. + +// Package withscrollbar provides a widget that renders with a scrollbar on the right +package withscrollbar + +import ( + "github.com/gcla/gowid" + "github.com/gcla/gowid/widgets/columns" + "github.com/gcla/gowid/widgets/selectable" + "github.com/gcla/gowid/widgets/vscroll" + log "github.com/sirupsen/logrus" +) + +//====================================================================== + +type Widget struct { + *columns.Widget + w IScrollSubWidget + sb *vscroll.Widget + goUpDown int // positive means down + pgUpDown int // positive means down +} + +type IScrollSubWidget interface { + gowid.IWidget + CalculateOnScreen(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) (int, int, int, error) + Up(lines int, size gowid.IRenderSize, app gowid.IApp) + Down(lines int, size gowid.IRenderSize, app gowid.IApp) + UpPage(num int, size gowid.IRenderSize, app gowid.IApp) + DownPage(num int, size gowid.IRenderSize, app gowid.IApp) +} + +func New(w IScrollSubWidget) *Widget { + sb := vscroll.NewExt(vscroll.VerticalScrollbarUnicodeRunes) + res := &Widget{ + Widget: columns.New([]gowid.IContainerWidget{ + &gowid.ContainerWidget{ + IWidget: w, + D: gowid.RenderWithWeight{W: 1}, + }, + // So that the vscroll doesn't take the focus when moving from above + // and below in the main termshark window + &gowid.ContainerWidget{ + IWidget: selectable.NewUnselectable(sb), + D: gowid.RenderWithUnits{U: 1}, + }, + }), + w: w, + sb: sb, + goUpDown: 0, + pgUpDown: 0, + } + sb.OnClickAbove(gowid.MakeWidgetCallback("cb", res.clickUp)) + sb.OnClickBelow(gowid.MakeWidgetCallback("cb", res.clickDown)) + sb.OnClickUpArrow(gowid.MakeWidgetCallback("cb", res.clickUpArrow)) + sb.OnClickDownArrow(gowid.MakeWidgetCallback("cb", res.clickDownArrow)) + return res +} + +func (e *Widget) clickUp(app gowid.IApp, w gowid.IWidget) { + e.pgUpDown -= 1 +} + +func (e *Widget) clickDown(app gowid.IApp, w gowid.IWidget) { + e.pgUpDown += 1 +} + +func (e *Widget) clickUpArrow(app gowid.IApp, w gowid.IWidget) { + e.goUpDown -= 1 +} + +func (e *Widget) clickDownArrow(app gowid.IApp, w gowid.IWidget) { + e.goUpDown += 1 +} + +func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { + box, ok := size.(gowid.IRenderBox) + if !ok { + panic(gowid.WidgetSizeError{Widget: w, Size: size, Required: "gowid.IRenderBox"}) + } + + ecols := box.BoxColumns() - 1 + ebox := gowid.MakeRenderBox(ecols, box.BoxRows()) + + x, y, z, err := w.w.CalculateOnScreen(ebox, focus, app) + if err != nil { + log.Error(err) + } + + w.sb.Top = x + w.sb.Middle = y + w.sb.Bottom = z + + res := w.Widget.UserInput(ev, size, focus, app) + if res { + w.Widget.SetFocus(app, 0) + } + return res +} + +func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { + box, ok := size.(gowid.IRenderBox) + if !ok { + panic(gowid.WidgetSizeError{Widget: w, Size: size, Required: "gowid.IRenderBox"}) + } + ecols := box.BoxColumns() - 1 + var x int + var y int + var z int + var err error + if ecols >= 1 { + ebox := gowid.MakeRenderBox(ecols, box.BoxRows()) + if w.goUpDown != 0 || w.pgUpDown != 0 { + if w.goUpDown > 0 { + w.w.Down(w.goUpDown, ebox, app) + } else if w.goUpDown < 0 { + w.w.Up(-w.goUpDown, ebox, app) + } + + if w.pgUpDown > 0 { + w.w.DownPage(w.pgUpDown, ebox, app) + } else if w.pgUpDown < 0 { + w.w.UpPage(-w.pgUpDown, ebox, app) + } + } + w.goUpDown = 0 + w.pgUpDown = 0 + + x, y, z, err = w.w.CalculateOnScreen(ebox, focus, app) + if err != nil { + log.Error(err) + } + } + w.sb.Top = x + w.sb.Middle = y + w.sb.Bottom = z + + canvas := gowid.Render(w.Widget, size, focus, app) + + return canvas +} + +//====================================================================== +// Local Variables: +// mode: Go +// fill-column: 110 +// End: