diff --git a/cli/all.go b/cli/all.go index affe9313..6ac34469 100644 --- a/cli/all.go +++ b/cli/all.go @@ -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:"" short:"i" description:"Interface(s) to read."` - Pcap flags.Filename `value-name:"" short:"r" description:"Pcap file/fifo to read. Use - for stdin."` + Pcap flags.Filename `value-name:"" short:"r" description:"Pcap file/fifo to read. Use - for stdin."` + WriteTo flags.Filename `value-name:"" short:"w" description:"Write raw packet data to outfile."` DecodeAs []string `short:"d" description:"Specify dissection of layer type." value-name:"==,"` 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:""` diff --git a/cmd/termshark/termshark.go b/cmd/termshark/termshark.go index 6d88160a..c4b0e609 100644 --- a/cmd/termshark/termshark.go +++ b/cmd/termshark/termshark.go @@ -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() } @@ -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 { @@ -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 @@ -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 { @@ -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) } }() @@ -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 @@ -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 @@ -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) diff --git a/system/fd.go b/system/fd.go index d79f3503..324ed45d 100644 --- a/system/fd.go +++ b/system/fd.go @@ -7,6 +7,8 @@ package system import ( + "io/fs" + "os" "syscall" ) @@ -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 diff --git a/system/fd_windows.go b/system/fd_windows.go index d57f2446..a345ac4f 100644 --- a/system/fd_windows.go +++ b/system/fd_windows.go @@ -9,6 +9,10 @@ package system func CloseDescriptor(fd int) { } +func FileRegularOrLink(filename string) bool { + return true +} + //====================================================================== // Local Variables: // mode: Go diff --git a/ui/lastline.go b/ui/lastline.go index a38c6fc1..70f1cf99 100644 --- a/ui/lastline.go +++ b/ui/lastline.go @@ -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 { @@ -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 { diff --git a/ui/ui.go b/ui/ui.go index f96baed8..65f264fc 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -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 @@ -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) @@ -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() + } }, ), }, @@ -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 { @@ -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), @@ -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) }, }, ) diff --git a/utils.go b/utils.go index 7b844b37..3b774d38 100644 --- a/utils.go +++ b/utils.go @@ -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") } @@ -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) } diff --git a/utils_test.go b/utils_test.go index a90fd872..3288ab03 100644 --- a/utils_test.go +++ b/utils_test.go @@ -6,6 +6,7 @@ package termshark import ( "bytes" + "os" "testing" "github.com/blang/semver" @@ -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