Skip to content

Commit

Permalink
Add a -w flag so the user can choose the location of capture file
Browse files Browse the repository at this point in the history
This work is in support of issue #111. Termshark now takes a `-w` flag:

```
Application Options:
  ...
  -w=<outfile>                                               Write raw packet data to outfile.
```

If you invoke termshark like this:

```
$ termshark -i eth0 -w foo.pcap
```

then the UI will launch as usual but termshark will save the capture to
`foo.pcap` instead of e.g. `~/.cache/termshark/pcaps/eth0-xyz.pcap`. The
argument to `-w` has to be a file and not something like stdout because
termshark repeatedly re-reads the file during its operation.

If you invoke termshark on an interface but without `-w`, then
termshark's behavior depends on these new config variables:

- `main.always-keep-pcap` (default: false) - unless this is true, when
you quit termshark after reading from an interface, termshark will
prompt you to see whether you want to keep or delete the capture file.

- `main.use-tshark-temp-for-pcap-cache` (default: false) - if true,
termshark will write the capture file to tshark's configured `Temp`
directory.

- `main.pcap-cache-dir` (string) - if set, and if
`main.use-tshark-temp-for-pcap-cache` is false, termshark will write the
capture file to this directory.

If you invoke termshark on an interface and use the `-w` flag, termshark
will not prompt you when it terminates, and will keep the capture file.
  • Loading branch information
gcla committed Apr 25, 2021
1 parent 5fd1548 commit 484db5c
Show file tree
Hide file tree
Showing 8 changed files with 189 additions and 20 deletions.
3 changes: 2 additions & 1 deletion cli/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ type Tshark struct {
// Termshark's own command line arguments. Used if we don't pass through to tshark.
type Termshark struct {
Ifaces []string `value-name:"<interfaces>" short:"i" description:"Interface(s) to read."`
Pcap flags.Filename `value-name:"<file/fifo>" short:"r" description:"Pcap file/fifo to read. Use - for stdin."`
Pcap flags.Filename `value-name:"<infile/fifo>" short:"r" description:"Pcap file/fifo to read. Use - for stdin."`
WriteTo flags.Filename `value-name:"<outfile>" short:"w" description:"Write raw packet data to outfile."`
DecodeAs []string `short:"d" description:"Specify dissection of layer type." value-name:"<layer type>==<selector>,<decode-as protocol>"`
PrintIfaces bool `short:"D" optional:"true" optional-value:"true" description:"Print a list of the interfaces on which termshark can capture."`
DisplayFilter string `short:"Y" description:"Apply display filter." value-name:"<displaY filter>"`
Expand Down
49 changes: 36 additions & 13 deletions cmd/termshark/termshark.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,16 +99,16 @@ func cmain() int {
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)
fmt.Fprintf(os.Stderr, "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)
fmt.Fprintf(os.Stderr, "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)
fmt.Fprintf(os.Stderr, "Warning: could not create initial config file: %v\n", err)
} else {
f.Close()
}
Expand Down Expand Up @@ -150,6 +150,8 @@ func cmain() int {
}
}

// On Windows, termshark itself is used to tail the pcap generated by dumpcap, and the output
// is fed into tshark -T psml ...
if tsopts.TailFileValue() != "" {
err = termshark.TailFile(tsopts.TailFileValue())
if err != nil {
Expand Down Expand Up @@ -437,6 +439,22 @@ func cmain() int {
captureFilter = argsFilter
}

// -w something
if opts.WriteTo != "" {
if len(fileSrcs) > 0 {
fmt.Fprintf(os.Stderr, "The -w flag is incompatible with regular capture sources %v\n", fileSrcs)
return 1
}
if opts.WriteTo == "-" {
fmt.Fprintf(os.Stderr, "Cannot set -w to stdout. Target file must be regular or a symlink.\n")
return 1
}
if !system.FileRegularOrLink(string(opts.WriteTo)) {
fmt.Fprintf(os.Stderr, "Cannot set -w to %s. Target file must be regular or a symlink.\n", opts.WriteTo)
return 1
}
}

displayFilter := opts.DisplayFilter

// Validate supplied filters e.g. no capture filter when reading from file
Expand Down Expand Up @@ -485,7 +503,7 @@ func cmain() int {
}()
}

for _, dir := range []string{termshark.CacheDir(), termshark.PcapDir()} {
for _, dir := range []string{termshark.CacheDir(), termshark.DefaultPcapDir(), termshark.PcapDir()} {
if _, err = os.Stat(dir); os.IsNotExist(err) {
err = os.Mkdir(dir, 0777)
if err != nil {
Expand Down Expand Up @@ -643,8 +661,8 @@ func cmain() int {
// fifo. In all cases, we save the packets to a file so that if a
// filter is applied, we can restart - and so that we preserve the
// capture at the end of running termshark.
if len(pcap.FileSystemSources(psrcs)) == 0 && startedSuccessfully {
fmt.Printf("Packets read from %s have been saved in %s\n", pcap.SourcesString(psrcs), ifacePcapFilename)
if len(pcap.FileSystemSources(psrcs)) == 0 && startedSuccessfully && !ui.WriteToSelected && !ui.WriteToDeleted {
fmt.Fprintf(os.Stderr, "Packets read from %s have been saved in %s\n", pcap.SourcesString(psrcs), ifacePcapFilename)
}
}()

Expand Down Expand Up @@ -698,13 +716,18 @@ func cmain() int {
var ifaceTmpFile string
var waitingForPackets bool

// no file sources - so interface or fifo
if len(pcap.FileSystemSources(psrcs)) == 0 {
srcNames := make([]string, 0, len(psrcs))
for _, psrc := range psrcs {
srcNames = append(srcNames, psrc.Name())
if opts.WriteTo != "" {
ifaceTmpFile = string(opts.WriteTo)
ui.WriteToSelected = true
} else {
srcNames := make([]string, 0, len(psrcs))
for _, psrc := range psrcs {
srcNames = append(srcNames, psrc.Name())
}
ifaceTmpFile = pcap.TempPcapFile(srcNames...)
}
ifaceTmpFile = pcap.TempPcapFile(srcNames...)

waitingForPackets = true
} else {
// Start UI right away, reading from a file
Expand Down Expand Up @@ -817,7 +840,7 @@ func cmain() int {
// If this message is needed, we want it to appear after the init message for the packet
// columns - after InitValidColumns
if waitingForPackets {
fmt.Printf("(The termshark UI will start when packets are detected...)\n")
fmt.Fprintf(os.Stderr, "(The termshark UI will start when packets are detected...)\n")
}

// Refresh
Expand All @@ -834,7 +857,7 @@ func cmain() int {
app.Run(gowid.RunFunction(func(app gowid.IApp) {
ui.FilterWidget.SetValue(displayFilter, app)
}))
ui.RequestLoadPcapWithCheck(absfile, displayFilter, ui.NoGlobalJump, app)
ui.RequestLoadPcap(absfile, displayFilter, ui.NoGlobalJump, app)
}
validator.Valid = &filter.ValidateCB{Fn: doit, App: app}
validator.Validate(displayFilter)
Expand Down
11 changes: 11 additions & 0 deletions system/fd.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
package system

import (
"io/fs"
"os"
"syscall"
)

Expand All @@ -16,6 +18,15 @@ func CloseDescriptor(fd int) {
syscall.Close(fd)
}

func FileRegularOrLink(filename string) bool {
fi, err := os.Stat(filename)
if err != nil {
return false
}

return fi.Mode().IsRegular() || (fi.Mode()&fs.ModeSymlink != 0)
}

//======================================================================
// Local Variables:
// mode: Go
Expand Down
4 changes: 4 additions & 0 deletions system/fd_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ package system
func CloseDescriptor(fd int) {
}

func FileRegularOrLink(filename string) bool {
return true
}

//======================================================================
// Local Variables:
// mode: Go
Expand Down
4 changes: 2 additions & 2 deletions ui/lastline.go
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,7 @@ func (d readCommand) Run(app gowid.IApp, args ...string) error {
if len(args) != 2 {
err = invalidReadCommandErr
} else {
RequestLoadPcapWithCheck(args[1], FilterWidget.Value(), NoGlobalJump, app)
MaybeKeepThenRequestLoadPcap(args[1], FilterWidget.Value(), NoGlobalJump, app)
}

if err != nil {
Expand Down Expand Up @@ -469,7 +469,7 @@ func (d recentsCommand) Run(app gowid.IApp, args ...string) error {
if len(args) != 2 {
err = invalidRecentsCommandErr
} else {
RequestLoadPcapWithCheck(args[1], FilterWidget.Value(), NoGlobalJump, app)
MaybeKeepThenRequestLoadPcap(args[1], FilterWidget.Value(), NoGlobalJump, app)
}

if err != nil {
Expand Down
76 changes: 72 additions & 4 deletions ui/ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,8 @@ var NoGlobalJump termshark.GlobalJumpPos // leave as default, like a placeholder
var Loader *pcap.PacketLoader
var FieldCompleter *termshark.TSharkFields // share this - safe once constructed

var WriteToSelected bool // true if the user provided the -w flag
var WriteToDeleted bool // true if the user deleted the temporary pcap before quitting
var DarkMode bool // global state in app
var PacketColors bool // global state in app
var PacketColorsSupported bool // global state in app - true if it's even possible
Expand Down Expand Up @@ -1257,6 +1259,50 @@ func processCopyChoices(copyLen int, app gowid.IApp) {
dialog.OpenExt(cc, appView, ratio(0.5), ratio(0.8), app)
}

type callWithAppFn func(gowid.IApp)

func askToSave(app gowid.IApp, next callWithAppFn) {
msgt := fmt.Sprintf("Current capture saved to %s", Loader.InterfaceFile())
msg := text.New(msgt)
var keepPackets *dialog.Widget
keepPackets = dialog.New(
framed.NewSpace(hpadding.New(msg, hmiddle, fixed)),
dialog.Options{
Buttons: []dialog.Button{
dialog.Button{
Msg: "Keep",
Action: gowid.MakeWidgetCallback("cb",
func(app gowid.IApp, widget gowid.IWidget) {
keepPackets.Close(app)
next(app)
},
),
},
dialog.Button{
Msg: "Delete",
Action: gowid.MakeWidgetCallback("cb",
func(app gowid.IApp, widget gowid.IWidget) {
WriteToDeleted = true
err := os.Remove(Loader.InterfaceFile())
if err != nil {
log.Errorf("Could not delete file %s: %v", Loader.InterfaceFile(), err)
}
keepPackets.Close(app)
next(app)
},
),
},
dialog.Cancel,
},
NoShadow: true,
BackgroundStyle: gowid.MakePaletteRef("dialog"),
BorderStyle: gowid.MakePaletteRef("dialog"),
ButtonStyle: gowid.MakePaletteRef("dialog-button"),
},
)
keepPackets.Open(appView, units(len(msgt)+20), app)
}

func reallyQuit(app gowid.IApp) {
msgt := "Do you want to quit?"
msg := text.New(msgt)
Expand All @@ -1268,7 +1314,17 @@ func reallyQuit(app gowid.IApp) {
Msg: "Ok",
Action: gowid.MakeWidgetCallback("cb",
func(app gowid.IApp, widget gowid.IWidget) {
RequestQuit()
YesNo.Close(app)
// (a) Loader is in interface mode (b) User did not set -w flag
// (c) always-keep-pcap setting is unset (def false) or false
if Loader.InterfaceFile() != "" && !WriteToSelected &&
!termshark.ConfBool("main.always-keep-pcap", false) {
askToSave(app, func(app gowid.IApp) {
RequestQuit()
})
} else {
RequestQuit()
}
},
),
},
Expand Down Expand Up @@ -1745,7 +1801,7 @@ func vimKeysMainView(evk *tcell.EventKey, app gowid.IApp) bool {
OpenError("Mark not found.", app)
} else {
if Loader.Pcap() != markedPacket.Filename {
RequestLoadPcapWithCheck(markedPacket.Filename, FilterWidget.Value(), markedPacket, app)
MaybeKeepThenRequestLoadPcap(markedPacket.Filename, FilterWidget.Value(), markedPacket, app)
} else {

if packetListView != nil {
Expand Down Expand Up @@ -2702,8 +2758,20 @@ func RequestLoadInterfaces(psrcs []pcap.IPacketSource, captureFilter string, dis

//======================================================================

// MaybeKeepThenRequestLoadPcap loads a pcap after first checking to see whether
// the current load is a live load and the packets need to be kept.
func MaybeKeepThenRequestLoadPcap(pcapf string, displayFilter string, jump termshark.GlobalJumpPos, app gowid.IApp) {
if Loader.InterfaceFile() != "" && !WriteToSelected && !termshark.ConfBool("main.always-keep-pcap", false) {
askToSave(app, func(app gowid.IApp) {
RequestLoadPcap(pcapf, displayFilter, jump, app)
})
} else {
RequestLoadPcap(pcapf, displayFilter, jump, app)
}
}

// Call from app goroutine context
func RequestLoadPcapWithCheck(pcapf string, displayFilter string, jump termshark.GlobalJumpPos, app gowid.IApp) {
func RequestLoadPcap(pcapf string, displayFilter string, jump termshark.GlobalJumpPos, app gowid.IApp) {
handlers := pcap.HandlerList{
SimpleErrors{},
MakeSaveRecents(pcapf, displayFilter),
Expand Down Expand Up @@ -2825,7 +2893,7 @@ func makeRecentMenuWidget() (gowid.IWidget, int) {
CB: func(app gowid.IApp, w gowid.IWidget) {
multiMenu1Opener.CloseMenu(savedMenu, app)
// capFilter global, set up in cmain()
RequestLoadPcapWithCheck(scopy, FilterWidget.Value(), NoGlobalJump, app)
MaybeKeepThenRequestLoadPcap(scopy, FilterWidget.Value(), NoGlobalJump, app)
},
},
)
Expand Down
48 changes: 48 additions & 0 deletions utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,27 @@ func CacheDir() string {
// circumstances for a non-existent file, meaning I need to track a directory,
// and I don't want to be constantly triggered by log file updates.
func PcapDir() string {
var res string
// If use-tshark-temp-for-cache is set, use that
if ConfBool("main.use-tshark-temp-for-pcap-cache", false) {
tmp, err := TsharkSetting("Temp")
if err == nil {
res = tmp
}
}
// Otherwise try the user's preference
if res == "" {
res = ConfString("main.pcap-cache-dir", "")
}
if res == "" {
res = DefaultPcapDir()
}
return res
}

// DefaultPcapDir returns ~/.cache/pcaps by default. Termshark will check a
// couple of user settings first before using this.
func DefaultPcapDir() string {
return path.Join(CacheDir(), "pcaps")
}

Expand Down Expand Up @@ -953,6 +974,33 @@ func interfacesFrom(reader io.Reader) (map[int][]string, error) {

//======================================================================

var foldersRE = regexp.MustCompile(`:\s*`)

// $ env TMPDIR=/foo tshark -G folders Temp
// Temp: /foo
// Personal configuration: /home/gcla/.config/wireshark
// Global configuration: /usr/share/wireshark
//
func TsharkSetting(field string) (string, error) {
out, err := exec.Command(TSharkBin(), []string{"-G", "folders"}...).Output()
if err != nil {
return "", err
}

scanner := bufio.NewScanner(strings.NewReader(string(out)))
for scanner.Scan() {
line := scanner.Text()
pieces := foldersRE.Split(line, 2)
if len(pieces) == 2 && pieces[0] == field {
return pieces[1], nil
}
}

return "", fmt.Errorf("Field %s not found in output of tshark -G folders", field)
}

//======================================================================

func IsTerminal(fd uintptr) bool {
return isatty.IsTerminal(fd) || isatty.IsCygwinTerminal(fd)
}
Expand Down
14 changes: 14 additions & 0 deletions utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package termshark

import (
"bytes"
"os"
"testing"

"github.com/blang/semver"
Expand Down Expand Up @@ -163,6 +164,19 @@ func TestIPComp1(t *testing.T) {
assert.False(t, ip.Less("2001:db8::68", "192.168.0.253"))
}

func TestFolders(t *testing.T) {
tmp := os.Getenv("TMPDIR")
os.Setenv("TMPDIR", "/foo")
defer os.Setenv("TMPDIR", tmp)

val, err := TsharkSetting("Temp")
assert.NoError(t, err)
assert.Equal(t, "/foo", val)

val, err = TsharkSetting("Deliberately missing")
assert.Error(t, err)
}

//======================================================================
// Local Variables:
// mode: Go
Expand Down

0 comments on commit 484db5c

Please sign in to comment.