pax_global_header00006660000000000000000000000064137744204730014525gustar00rootroot0000000000000052 comment=88f03745cb23eb6aae02a1e2d0c6dc361d23db86 termshark-2.2.0/000077500000000000000000000000001377442047300135265ustar00rootroot00000000000000termshark-2.2.0/.all-contributorsrc000066400000000000000000000257351377442047300173730ustar00rootroot00000000000000{ "projectName": "termshark", "projectOwner": "gcla", "repoType": "github", "repoHost": "https://github.com", "files": [ "README.md" ], "imageSize": 80, "commit": false, "commitConvention": "none", "contributors": [ { "login": "pocc", "name": "Ross Jacobs", "avatar_url": "https://avatars0.githubusercontent.com/u/10995145?v=4", "profile": "https://swit.sh", "contributions": [ "code", "bug", "userTesting" ] }, { "login": "Hongarc", "name": "Hongarc", "avatar_url": "https://avatars1.githubusercontent.com/u/19208123?v=4", "profile": "https://github.com/Hongarc", "contributions": [ "doc" ] }, { "login": "zi0r", "name": "Ryan Steinmetz", "avatar_url": "https://avatars0.githubusercontent.com/u/1676702?v=4", "profile": "https://github.com/zi0r", "contributions": [ "platform" ] }, { "login": "NicolaiSoeborg", "name": "Nicolai Søborg", "avatar_url": "https://avatars2.githubusercontent.com/u/8722223?v=4", "profile": "https://søb.org/", "contributions": [ "platform" ] }, { "login": "QuLogic", "name": "Elliott Sales de Andrade", "avatar_url": "https://avatars2.githubusercontent.com/u/302469?v=4", "profile": "https://qulogic.gitlab.io/", "contributions": [ "code" ] }, { "login": "rski", "name": "Romanos", "avatar_url": "https://avatars2.githubusercontent.com/u/2960312?v=4", "profile": "http://rski.github.io", "contributions": [ "code" ] }, { "login": "denyspozniak", "name": "Denys", "avatar_url": "https://avatars0.githubusercontent.com/u/22612345?v=4", "profile": "https://github.com/denyspozniak", "contributions": [ "bug" ] }, { "login": "jerry73204", "name": "jerry73204", "avatar_url": "https://avatars1.githubusercontent.com/u/7629150?v=4", "profile": "https://github.com/jerry73204", "contributions": [ "platform" ] }, { "login": "Thann", "name": "Jon Knapp", "avatar_url": "https://avatars1.githubusercontent.com/u/578515?v=4", "profile": "http://thann.github.com", "contributions": [ "platform" ] }, { "login": "mharjac", "name": "Mario Harjac", "avatar_url": "https://avatars2.githubusercontent.com/u/2997453?v=4", "profile": "https://github.com/mharjac", "contributions": [ "platform" ] }, { "login": "abenson", "name": "Andrew Benson", "avatar_url": "https://avatars1.githubusercontent.com/u/227317?v=4", "profile": "https://github.com/abenson", "contributions": [ "bug" ] }, { "login": "sagis-tikal", "name": "sagis-tikal", "avatar_url": "https://avatars2.githubusercontent.com/u/46102019?v=4", "profile": "https://github.com/sagis-tikal", "contributions": [ "bug" ] }, { "login": "punkymaniac", "name": "punkymaniac", "avatar_url": "https://avatars2.githubusercontent.com/u/9916797?v=4", "profile": "https://github.com/punkymaniac", "contributions": [ "bug" ] }, { "login": "msenturk", "name": "msenturk", "avatar_url": "https://avatars3.githubusercontent.com/u/9482568?v=4", "profile": "https://github.com/msenturk", "contributions": [ "bug" ] }, { "login": "szuecs", "name": "Sandor Szücs", "avatar_url": "https://avatars3.githubusercontent.com/u/50872?v=4", "profile": "https://github.com/szuecs", "contributions": [ "bug" ] }, { "login": "dawidd6", "name": "Dawid Dziurla", "avatar_url": "https://avatars1.githubusercontent.com/u/9713907?v=4", "profile": "https://github.com/dawidd6", "contributions": [ "bug", "platform" ] }, { "login": "jJit0", "name": "jJit0", "avatar_url": "https://avatars1.githubusercontent.com/u/23521148?v=4", "profile": "https://github.com/jJit0", "contributions": [ "bug" ] }, { "login": "inzel", "name": "inzel", "avatar_url": "https://avatars3.githubusercontent.com/u/20195547?v=4", "profile": "http://colinrogers001.com", "contributions": [ "bug" ] }, { "login": "thejerrod", "name": "thejerrod", "avatar_url": "https://avatars1.githubusercontent.com/u/25254103?v=4", "profile": "https://github.com/thejerrod", "contributions": [ "ideas" ] }, { "login": "gdluca", "name": "gdluca", "avatar_url": "https://avatars3.githubusercontent.com/u/12004506?v=4", "profile": "https://github.com/gdluca", "contributions": [ "bug" ] }, { "login": "winpat", "name": "Patrick Winter", "avatar_url": "https://avatars2.githubusercontent.com/u/6016963?v=4", "profile": "https://github.com/winpat", "contributions": [ "platform" ] }, { "login": "RobertLarsen", "name": "Robert Larsen", "avatar_url": "https://avatars0.githubusercontent.com/u/795303?v=4", "profile": "https://github.com/RobertLarsen", "contributions": [ "ideas", "userTesting" ] }, { "login": "mingrammer", "name": "MinJae Kwon", "avatar_url": "https://avatars0.githubusercontent.com/u/6178510?v=4", "profile": "https://mingrammer.com", "contributions": [ "bug" ] }, { "login": "the-c0d3r", "name": "the-c0d3r", "avatar_url": "https://avatars2.githubusercontent.com/u/4526565?v=4", "profile": "https://github.com/the-c0d3r", "contributions": [ "ideas" ] }, { "login": "gvanem", "name": "Gisle Vanem", "avatar_url": "https://avatars0.githubusercontent.com/u/945271?v=4", "profile": "https://github.com/gvanem", "contributions": [ "bug" ] }, { "login": "hook-s3c", "name": "hook", "avatar_url": "https://avatars1.githubusercontent.com/u/31825993?v=4", "profile": "https://github.com/hook-s3c", "contributions": [ "bug" ] }, { "login": "lennartkoopmann", "name": "Lennart Koopmann", "avatar_url": "https://avatars0.githubusercontent.com/u/35022?v=4", "profile": "https://twitter.com/_lennart", "contributions": [ "ideas" ] }, { "login": "ReK2Fernandez", "name": "Fernandez, ReK2", "avatar_url": "https://avatars1.githubusercontent.com/u/5316229?v=4", "profile": "https://keybase.io/cfernandez", "contributions": [ "bug" ] }, { "login": "mazball", "name": "mazball", "avatar_url": "https://avatars2.githubusercontent.com/u/22456251?v=4", "profile": "https://github.com/mazball", "contributions": [ "ideas" ] }, { "login": "wfailla", "name": "wfailla", "avatar_url": "https://avatars1.githubusercontent.com/u/5494665?v=4", "profile": "https://github.com/wfailla", "contributions": [ "ideas" ] }, { "login": "rongyi", "name": "荣怡", "avatar_url": "https://avatars3.githubusercontent.com/u/1034762?v=4", "profile": "https://github.com/rongyi", "contributions": [ "ideas" ] }, { "login": "thebyrdman-git", "name": "thebyrdman-git", "avatar_url": "https://avatars1.githubusercontent.com/u/55452713?v=4", "profile": "https://github.com/thebyrdman-git", "contributions": [ "bug" ] }, { "login": "cmosig", "name": "Clemens Mosig", "avatar_url": "https://avatars2.githubusercontent.com/u/32590522?v=4", "profile": "http://www.mi.fu-berlin.de/en/inf/groups/ilab/members/mosig.html", "contributions": [ "bug" ] }, { "login": "mrash", "name": "Michael Rash", "avatar_url": "https://avatars3.githubusercontent.com/u/380228?v=4", "profile": "http://www.cipherdyne.org/", "contributions": [ "userTesting" ] }, { "login": "joelparker", "name": "joelparker", "avatar_url": "https://avatars3.githubusercontent.com/u/136451?v=4", "profile": "https://github.com/joelparker", "contributions": [ "userTesting" ] }, { "login": "dragosmaftei", "name": "Dragos Maftei", "avatar_url": "https://avatars1.githubusercontent.com/u/15351028?v=4", "profile": "https://github.com/dragosmaftei", "contributions": [ "ideas" ] }, { "login": "IAXES", "name": "Matthew Giassa", "avatar_url": "https://avatars1.githubusercontent.com/u/8325672?v=4", "profile": "http://www.giassa.net", "contributions": [ "ideas" ] }, { "login": "sean-abbott", "name": "Sean Abbott", "avatar_url": "https://avatars0.githubusercontent.com/u/1402071?v=4", "profile": "https://github.com/sean-abbott", "contributions": [ "platform" ] }, { "login": "linsong", "name": "Vincent Wang", "avatar_url": "https://avatars1.githubusercontent.com/u/36017?v=4", "profile": "http://www.linsong.org", "contributions": [ "ideas" ] }, { "login": "Piping", "name": "piping", "avatar_url": "https://avatars3.githubusercontent.com/u/12042284?v=4", "profile": "https://github.com/Piping", "contributions": [ "ideas" ] }, { "login": "kevinhwang91", "name": "kevinhwang91", "avatar_url": "https://avatars0.githubusercontent.com/u/17562139?v=4", "profile": "https://github.com/kevinhwang91", "contributions": [ "ideas", "bug" ] }, { "login": "jboverfelt", "name": "Justin Overfelt", "avatar_url": "https://avatars0.githubusercontent.com/u/936126?v=4", "profile": "https://jbo.io", "contributions": [ "ideas" ] }, { "login": "loudsong", "name": "Anthony", "avatar_url": "https://avatars3.githubusercontent.com/u/1447613?v=4", "profile": "https://github.com/loudsong", "contributions": [ "ideas" ] }, { "login": "basondole", "name": "basondole", "avatar_url": "https://avatars2.githubusercontent.com/u/50369643?v=4", "profile": "https://github.com/basondole", "contributions": [ "bug" ] }, { "login": "zoulja", "name": "zoulja", "avatar_url": "https://avatars1.githubusercontent.com/u/10187203?v=4", "profile": "https://github.com/zoulja", "contributions": [ "bug" ] } ], "contributorsPerLine": 7 } termshark-2.2.0/.github/000077500000000000000000000000001377442047300150665ustar00rootroot00000000000000termshark-2.2.0/.github/ISSUE_TEMPLATE/000077500000000000000000000000001377442047300172515ustar00rootroot00000000000000termshark-2.2.0/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000022171377442047300217450ustar00rootroot00000000000000--- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- ## Prerequisites Please verify these before submitting an issue. - [ ] I am running the latest versions of Termshark and Wireshark. - [ ] I checked the [README](https://github.com/gcla/termshark) and [User Guide](https://github.com/gcla/termshark/blob/master/docs/UserGuide.md) and found no answer - [ ] I searched [issues](https://github.com/gcla/termshark/issues?q=is%3Aissue) and this has not yet been filed ## Problem ### Current Behavior Please describe the behavior you are seeing. ### Expected Behavior Please describe the behavior you are expecting. ### Screenshots as applicable * Provide screenshots of wireshark/tshark if that's what the expectation is based on. * Provide screenshots of the problem state. ### Steps to Reproduce 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error ## Context Please provide the complete output of these commands: * termshark -v (or termshark -vv if running from git/HEAD) * termshark -v | cat Please also provide any relevant information about your environment (OS, VM, pi,...) termshark-2.2.0/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000011251377442047300227750ustar00rootroot00000000000000--- name: Feature request about: Suggest an idea for this project title: '' labels: '' assignees: '' --- ### Is your feature request related to a problem? Please describe. A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] ### Describe the solution you'd like A clear and concise description of what you want to happen. ### Describe alternatives you've considered A clear and concise description of any alternative solutions or features you've considered. ### Additional context Add any other context or screenshots about the feature request here. termshark-2.2.0/.github/workflows/000077500000000000000000000000001377442047300171235ustar00rootroot00000000000000termshark-2.2.0/.github/workflows/go.yml000066400000000000000000000014051377442047300202530ustar00rootroot00000000000000name: Go on: [push] jobs: build: name: Build runs-on: ubuntu-latest steps: - name: Set up Go 1.12 uses: actions/setup-go@v1 with: go-version: 1.12 id: go - name: Check out code into the Go module directory uses: actions/checkout@v1 - name: Get dependencies run: | go get -v -t -d ./... if [ -f Gopkg.toml ]; then curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh dep ensure fi - name: Build run: go build -v ./... - name: Install tshark as prequisite for testing run: sudo sh -c 'export DEBIAN_FRONTEND=noninteractive ; apt -y update && apt -y install tshark' - name: Test run: go test -v ./... termshark-2.2.0/.gitignore000066400000000000000000000000701377442047300155130ustar00rootroot00000000000000dist/ .vscode/ *~ /cmd/termshark/termshark /typescript termshark-2.2.0/.goreleaser.yml000066400000000000000000000022331377442047300164570ustar00rootroot00000000000000# 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 - netbsd - openbsd - windows - linux - darwin goarch: - arm - arm64 - amd64 ignore: - goos: darwin goarch: arm - goos: freebsd goarch: arm - goos: netbsd goarch: arm - goos: openbsd goarch: arm - goos: windows goarch: arm - goos: darwin goarch: arm64 - goos: freebsd goarch: arm64 - goos: netbsd goarch: arm64 - goos: openbsd goarch: arm64 - goos: windows goarch: arm64 archives: - replacements: darwin: macOS linux: linux windows: windows amd64: x64 wrap_in_directory: true format_overrides: - goos: windows format: zip files: - none* signs: - artifacts: checksum checksum: name_template: 'checksums.txt' snapshot: name_template: "{{ .Env.TERMSHARK_GIT_DESCRIBE }}" changelog: sort: asc filters: exclude: - '^docs:' - '^test:' termshark-2.2.0/.travis.yml000066400000000000000000000060731377442047300156450ustar00rootroot00000000000000language: go env: global: - GO111MODULE=on - GOOGLE_APPLICATION_CREDENTIALS=/tmp/google.json - secure: I+3P2j2pxXjaKBAemdC/NN8SozyG4BEcMXJqUPY4jmiJO6aVXHkEQUMHzk1hCzNNo2QA2ACz0ptSUKx9AIXlQuGmLPsbNGkCaLklT8+lLgwISbM+BGs3w8Bz777czbD1c2zHjb3k/90fgo0j+96Y+qpJOo3GAEBsz8Q47GQEZUFM05DPzd2Opj6fXkZBY0qaG5cXHF8UisgpYL4E6iYnXnPS7O3jPnyL28QnzYV9zsZGfPGWTSo0vOaO2l5gmPNZ33w+fnuN9pLayh6L8lLDYlwzAyzMUx9HTgp1qLknSzPmhSIqn1OebKVX5UiTWlfI/5mJj2TI0E/S+sFNTlkNR3r1qoNs09m7zRijxkQVKNj/Pzx5fbT5yYB6g7kPuD6Ag1NQb3TPocxOk/m4OHoU7m1kSl7IKr9LmNIGwEAzwIOPwxoHBXsSiZo4Zu4uD0pl6Jse7NTXCc5sPZagEwac70TZ0aPwuOwQDnwPU8PUUfDQwoYtuXkY8uv/lBki0y1JV8vVTNZoel6ZwJuJPB6IXn4ctI7c+MtX1eIg0zppkjaqnpi3eKbtpFGlbOAtquChd4LEr1O+Q5IJyPg7DFVfscH41owWR9T3sYXSpjVU8XKsAfjazS4Xx/brFItlvLjRQHdpZzng23EWBeYP9nCM5X5UZ2xEnrDJXefhrsHqWLw= - secure: FLDaxfvg2nwSPo045nwSINI/84MgGwlku/ZgkvVbDQUNiIUc305UhaNCPuxq503Y2OJdw39wN3kkLQd08g4vYHXygBYTEr7k63D1Dq+0GltItTGPpMmhW2LCZFxMZ6QLVpLf/LGYY9DviT+zIXfDJekgvCcxT9cXTWxaKNAxENnvbUu/arnNg0liL9zyhoCIMiEJmJ/Pybq9MkL8mv+4i6DpzWTh6vGsO2OXfN52QQ/y4TqnJYsOfsRzKX30AzX+OvONUOhwt4j5AbQWn1VTFHWUz++GhY/LerJxOXYea2GqaY0NZ0+cvpUaMRpeAENJA778IQvRojMZnWgyRm7RAScHkJ0dT8CTuCkIXn/XN/r+bmTBdoB9C9DuDovo2Y86HgOeM1ZIRGjgNAja6oBJ7xi94m8TlsjoQuPl4eilB8Y5dX+tkUGTOZ90zWaP5xViAMIZpTiMS8ZWnHOGWB3M4EfmDDiaX6SOEcT7QFQMS/kj0RtBSZlXYcXEfvZnduW2eykfYeOVoDZoBZOiB93yVfhg5NrOeaKPe3XiiqlTygNUNRllO1SaYB317EQwSPAuFG1X2ocS0uHvVNT/DvoHDTmztEvJBB3aI+9jZbj1ZzVFtD525MaVGHCvXpCYjfKkhq3eMTsdTFOH2QlW2cf1UWhMPQyvMABr4C0Ro0ep/Kk= branches: only: - gh-pages - "/.*/" git: depth: false go: - 1.15.x - 1.14.x - 1.13.x - 1.12.x notifications: email: true before_install: - openssl aes-256-cbc -K $encrypted_1286cb654632_key -iv $encrypted_1286cb654632_iv -in configs/termshark-dd01307f2423.json.enc -out /tmp/google.json -d - sudo apt-get install -y tshark socat coreutils addons: apt: update: true script: - go test -v ./... - bash scripts/simple-tests.sh deploy: - provider: script skip_cleanup: true script: bash scripts/do-release.sh on: all_branches: true condition: "$TRAVIS_OS_NAME = linux && $TRAVIS_GO_VERSION = 1.15.x" - provider: gcs skip_cleanup: true access_key_id: GOOGKKO2OYB5BE3XDNBJMDRK secret_access_key: secure: fKoBQYUVQLh5gZB/LE3jmv9atfuOh1oM3YZ+9nIvv39771WbI9nfcxpO9/LZldoWludUeamR8fGz8rqzP19g6aZK+tTAvBG8XZnbEIi7lCtrLmBY6FB7t9VkcA5oPyAd+ygnUF4BRh92gqGbYx4vdJdjPUSruiIq8HTw1eSdLCIjl2h+Rk4KSpmnPmZk1YtWI9TDkBG8dRLnIq16Rwb0ep5oeK1Omlvqr5PiuwUL3gJFfE7KWznjs2hv52yAXQY6J8vWyiifBnQI7wq3JvbcV0PkJjQ7oyCsIx56R4dMl+UjFynmA7PpOiZqCj+Rp4EcBmGnh1ofS7U9hDfvLiy9jbHNwPUBiYQj2aD/fykMmXvAUwqzvjxCqx1Ky/QweBcVUbs2jOwiCofl4gSrPYdB2dkcZ8I0x9BhhbByOIEN42MNdYGucaqAia8yP8fDgfofi7H3XW3dZCySFbpb2n2poskpDFqEiJdubtm4b15YxV3gKn0NppJVojlMinEFk0jQh0BIVSY6/30rygamhbTps7JR2rNRo/82QIStZ00ME8CgFBQuEU0tOvyX3ifJAsTJPy3g4/0p3SGLrc0iV9CW3ieboA0vXJQ7DkR+yAyKhvz0QoAZs3QiAxfJLFvKxe2L/UGlKqisRghW7WRiZn8AzQNuD0jhi63SqimA2q/HFe8= bucket: termshark acl: public-read local-dir: dist upload-dir: "$TRAVIS_COMMIT" on: repo: gcla/termshark all_branches: true condition: "$TRAVIS_OS_NAME = linux && $TRAVIS_GO_VERSION = 1.15.x" termshark-2.2.0/CHANGELOG.md000066400000000000000000000130521377442047300153400ustar00rootroot00000000000000# Changelog ## [2.2.0] - 2021-01-03 ### Added - Termshark is now available for linux/arm64, NetBSD and OpenBSD. - Vim keys h, j, k and l can now be used in widgets that accept left, down, up and right user input. - Termshark's tables support vim-style navigation - use G to go to the bottom, gg to go to the top, or add a numeric prefix. - Other vim-style navigation keypresses are now implemented :r/:e to load a pcap, :q! to quit, ZZ to quit, C-w C-w to cycle through views and C-w = to reset spacing. - You can now set packet marks with the m key (e.g. ma, mb). Jump to packet marks with the ' key (e.g. 'a, 'b). Set cross-file packet marks with capital letters (e.g. mA, mB). Jump to last location with ''. - Display termshark's log file via the new menu option "Show Log" - Termshark now provides last-line mode/a minibuffer for issuing commands. Access it with the ":" key. - Termshark provides the following minibuffer commands: - `recents` - pick a pcap from recently loaded files. - `filter` - pick a display filter from the recently used list. - `set` - set various config properties. - `marks` - display currently set local and cross-file packet marks. - Map keys to other key sequences using a vim-style map command e.g. `map ZZ`. Use vim-syntax to express keystrokes - alphanumeric characters, and angle brackets for compound keys (``, ``, ``, ``, ``) - Added support for themes. See this [example](https://raw.githubusercontent.com/gcla/termshark/master/assets/themes/dracula-256.toml). Themes are loaded from `~/.config/termshark/themes/` or from a small cache built-in to termshark. A new minibuffer command `theme` can be used to change theme; `no-theme` turns off theming. ### Changed - Fixed a race condition that allowed an invalid Wireshark display filter to be applied. - Fixed race conditions that resulted in spurious warnings about a failure to kill tshark processes - If auto-scroll is enabled, and you navigate to a different packet in the packet list view during a live capture, auto-scroll is resumed if you hit 'G' or the `end` key. - Fixed a problem preventing the correct operation of piped input to termshark on freebsd. - The Escape key no longer opens the main menu. Instead it puts focus on the menu button. Hit Enter to open. This is more intuitive with the presence of ":" to open the minibuffer. ## [2.1.1] - 2020-02-02 ### Added - Termshark now provides a conversations view for the most common conversation types. - Termshark now supports multiple live captures/interfaces on the command-line e.g. `termshark -i eth0 -i eth1` - Termshark's packet hex view displays a scrollbar if the data doesn't fit in the space available. - Termshark can show a capture file's properties using the capinfos binary (bundled with tshark). - Termshark now supports [extcap interfaces](https://tshark.dev/capture/sources/extcap_interfaces/) by default. ## [2.0.3] - 2019-12-23 ### Added - Termshark now colorizes its packet list view by default, using the current Wireshark `colorfilter` rules. - Termshark now supports tshark's `-t` option to specify the timestamp format in the packet list view. ### Changed - Fixed a potential deadlock when reassembling very long streams. ## [2.0.2] - 2019-11-11 ### Changed - Internal Go API name changes that I didn't understand when I released termshark V2. ## [2.0.1] - 2019-11-10 ### Changed - Fix a mistake that caused a build break on homebrew. ## [2.0.0] - 2019-11-10 ### Added - Termshark supports TCP and UDP stream reassembly. See termshark's "Analysis" menu. - By popular demand, termshark now has a dark mode! To turn on, run termshark and open the menu. - Termshark can be configured to "auto-scroll" when reading live data (interface, fifo or stdin). - Termshark uses less CPU, is less laggy under mouse input, and will use less than half as much RAM on larger pcaps. - Termshark now supports piped input e.g. ``` $ tshark -i eth0 -w - | termshark ``` - Termshark now supports input from a fifo e.g. ``` 1$ mkfifo myfifo 1$ tshark -i eth0 -w myfifo 2$ termshark -r myfifo ``` - Termshark supports running its UI on a different tty (make sure the tty doesn't have another process competing for reads and writes). This is useful if you are feeding termshark with data from a process that writes to stderr, or if you want to see information displayed in the terminal that would be covered up by termshark's UI e.g. ``` termshark -i eth0 --tty=/dev/pts/5 ``` - Like Wireshark, termshark will now preserve the opened and closed structure of a packet as you move from one packet to the next. This lets the user see differences between packets more easily. - Termshark can now be installed for MacOS from [Homebrew](docs/FAQ.md#homebrew). - Termshark now respects job control signals sent via the shell i.e. SIGTSTP and SIGCONT. - Termshark on Windows no longer depends on the Cywgin tail command (and thus a Cygwin installation). - The current packet capture source (file, interface, pipe, etc) is displayed in the termshark title bar. - Termshark can be configured to eagerly load all pcap PDML data, rather than 1000 packets at a time. ### Changed - You can now simply hit enter in the display filter widget to make its value take effect. ## [1.0.0] - 2019-04-17 - Initial release. [Unreleased]: https://github.com/gcla/termshark/commpare/v2.1.1...HEAD [1.0.0]: https://github.com/gcla/termshark/releases/tag/v1.0.0 [2.0.0]: https://github.com/gcla/termshark/releases/tag/v2.0.0 [2.0.3]: https://github.com/gcla/termshark/releases/tag/v2.0.3 [2.1.1]: https://github.com/gcla/termshark/releases/tag/v2.1.1 termshark-2.2.0/LICENSE000066400000000000000000000020701377442047300145320ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2019 Graham Clark Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. termshark-2.2.0/README.md000066400000000000000000000333011377442047300150050ustar00rootroot00000000000000[twitter-follow-url]: https://twitter.com/intent/follow?screen_name=termshark [twitter-follow-img]: https://img.shields.io/twitter/follow/termshark.svg?style=social&label=Follow # Termshark A terminal user-interface for tshark, inspired by Wireshark. **V2.2 is out now with vim keys, packet marks, a command-line and themes! See the [ChangeLog](CHANGELOG.md#changelog).** ![demo21](/../gh-pages/images/demo21.png?raw=true) 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) - Filter pcaps or live captures using Wireshark's display filters - Reassemble and inspect TCP and UDP flows - View network conversations by protocol - 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, macOS, FreeBSD, Android (termux) and Windows tshark has many more features that termshark doesn't expose yet! See [What's Next](docs/FAQ.md#whats-next). ## Install Packages Termshark is pre-packaged for the following platforms: [Arch Linux](docs/Packages.md#arch-linux), [Debian (unstable)](docs/Packages.md#debian), [FreeBSD](docs/Packages.md#freebsd), [Homebrew](docs/Packages.md#homebrew), [Kali Linux](docs/Packages.md#kali-linux), [NixOS](docs/Packages.md#nixos), [SnapCraft](docs/Packages.md#snapcraft), [Termux (Android)](docs/Packages.md#termux-android) and [Ubuntu](docs/Packages.md#ubuntu). ## Building Termshark uses Go modules, so it's best to compile with Go 1.12 or higher. Set `GO111MODULE=on` then run: ```bash go get github.com/gcla/termshark/v2/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). Or download the latest build from the master branch - [![Build Status](https://travis-ci.org/gcla/termshark.svg?branch=master)](https://travis-ci.org/gcla/termshark). ## Documentation See the [termshark user guide](docs/UserGuide.md), and my best guess at some [FAQs](docs/FAQ.md). For a summary of updates, see the [ChangeLog](CHANGELOG.md#changelog). ## 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). ## Contributors Thanks to everyone that's contributed ports, patches and effort! | [
Ross Jacobs](https://swit.sh)
[💻](https://github.com/gcla/termshark/commits?author=pocc "Code") [🐛](https://github.com/gcla/termshark/issues?q=author%3Apocc "Bug reports") [📓](#userTesting-pocc "User Testing") | [
Hongarc](https://github.com/Hongarc)
[📖](https://github.com/gcla/termshark/commits?author=Hongarc "Documentation") | [
Ryan Steinmetz](https://github.com/zi0r)
[📦](#platform-zi0r "Packaging/porting to new platform") | [
Nicolai Søborg](https://søb.org/)
[📦](#platform-NicolaiSoeborg "Packaging/porting to new platform") | [
Elliott Sales de Andrade](https://qulogic.gitlab.io/)
[💻](https://github.com/gcla/termshark/commits?author=QuLogic "Code") | [
Romanos](http://rski.github.io)
[💻](https://github.com/gcla/termshark/commits?author=rski "Code") | [
Denys](https://github.com/denyspozniak)
[🐛](https://github.com/gcla/termshark/issues?q=author%3Adenyspozniak "Bug reports") | | :---: | :---: | :---: | :---: | :---: | :---: | :---: | | [
jerry73204](https://github.com/jerry73204)
[📦](#platform-jerry73204 "Packaging/porting to new platform") | [
Jon Knapp](http://thann.github.com)
[📦](#platform-Thann "Packaging/porting to new platform") | [
Mario Harjac](https://github.com/mharjac)
[📦](#platform-mharjac "Packaging/porting to new platform") | [
Andrew Benson](https://github.com/abenson)
[🐛](https://github.com/gcla/termshark/issues?q=author%3Aabenson "Bug reports") | [
sagis-tikal](https://github.com/sagis-tikal)
[🐛](https://github.com/gcla/termshark/issues?q=author%3Asagis-tikal "Bug reports") | [
punkymaniac](https://github.com/punkymaniac)
[🐛](https://github.com/gcla/termshark/issues?q=author%3Apunkymaniac "Bug reports") | [
msenturk](https://github.com/msenturk)
[🐛](https://github.com/gcla/termshark/issues?q=author%3Amsenturk "Bug reports") | | [
Sandor Szücs](https://github.com/szuecs)
[🐛](https://github.com/gcla/termshark/issues?q=author%3Aszuecs "Bug reports") | [
Dawid Dziurla](https://github.com/dawidd6)
[🐛](https://github.com/gcla/termshark/issues?q=author%3Adawidd6 "Bug reports") [📦](#platform-dawidd6 "Packaging/porting to new platform") | [
jJit0](https://github.com/jJit0)
[🐛](https://github.com/gcla/termshark/issues?q=author%3AjJit0 "Bug reports") | [
inzel](http://colinrogers001.com)
[🐛](https://github.com/gcla/termshark/issues?q=author%3Ainzel "Bug reports") | [
thejerrod](https://github.com/thejerrod)
[🤔](#ideas-thejerrod "Ideas, Planning, & Feedback") | [
gdluca](https://github.com/gdluca)
[🐛](https://github.com/gcla/termshark/issues?q=author%3Agdluca "Bug reports") | [
Patrick Winter](https://github.com/winpat)
[📦](#platform-winpat "Packaging/porting to new platform") | | [
Robert Larsen](https://github.com/RobertLarsen)
[🤔](#ideas-RobertLarsen "Ideas, Planning, & Feedback") [📓](#userTesting-RobertLarsen "User Testing") | [
MinJae Kwon](https://mingrammer.com)
[🐛](https://github.com/gcla/termshark/issues?q=author%3Amingrammer "Bug reports") | [
the-c0d3r](https://github.com/the-c0d3r)
[🤔](#ideas-the-c0d3r "Ideas, Planning, & Feedback") | [
Gisle Vanem](https://github.com/gvanem)
[🐛](https://github.com/gcla/termshark/issues?q=author%3Agvanem "Bug reports") | [
hook](https://github.com/hook-s3c)
[🐛](https://github.com/gcla/termshark/issues?q=author%3Ahook-s3c "Bug reports") | [
Lennart Koopmann](https://twitter.com/_lennart)
[🤔](#ideas-lennartkoopmann "Ideas, Planning, & Feedback") | [
Fernandez, ReK2](https://keybase.io/cfernandez)
[🐛](https://github.com/gcla/termshark/issues?q=author%3AReK2Fernandez "Bug reports") | | [
mazball](https://github.com/mazball)
[🤔](#ideas-mazball "Ideas, Planning, & Feedback") | [
wfailla](https://github.com/wfailla)
[🤔](#ideas-wfailla "Ideas, Planning, & Feedback") | [
荣怡](https://github.com/rongyi)
[🤔](#ideas-rongyi "Ideas, Planning, & Feedback") | [
thebyrdman-git](https://github.com/thebyrdman-git)
[🐛](https://github.com/gcla/termshark/issues?q=author%3Athebyrdman-git "Bug reports") | [
Clemens Mosig](http://www.mi.fu-berlin.de/en/inf/groups/ilab/members/mosig.html)
[🐛](https://github.com/gcla/termshark/issues?q=author%3Acmosig "Bug reports") | [
Michael Rash](http://www.cipherdyne.org/)
[📓](#userTesting-mrash "User Testing") | [
joelparker](https://github.com/joelparker)
[📓](#userTesting-joelparker "User Testing") | | [
Dragos Maftei](https://github.com/dragosmaftei)
[🤔](#ideas-dragosmaftei "Ideas, Planning, & Feedback") | [
Matthew Giassa](http://www.giassa.net)
[🤔](#ideas-IAXES "Ideas, Planning, & Feedback") | [
Sean Abbott](https://github.com/sean-abbott)
[📦](#platform-sean-abbott "Packaging/porting to new platform") | [
Vincent Wang](http://www.linsong.org)
[🤔](#ideas-linsong "Ideas, Planning, & Feedback") | [
piping](https://github.com/Piping)
[🤔](#ideas-Piping "Ideas, Planning, & Feedback") | [
kevinhwang91](https://github.com/kevinhwang91)
[🤔](#ideas-kevinhwang91 "Ideas, Planning, & Feedback") [🐛](https://github.com/gcla/termshark/issues?q=author%3Akevinhwang91 "Bug reports") | [
Justin Overfelt](https://jbo.io)
[🤔](#ideas-jboverfelt "Ideas, Planning, & Feedback") | | [
Anthony](https://github.com/loudsong)
[🤔](#ideas-loudsong "Ideas, Planning, & Feedback") | [
basondole](https://github.com/basondole)
[🐛](https://github.com/gcla/termshark/issues?q=author%3Abasondole "Bug reports") | [
zoulja](https://github.com/zoulja)
[🐛](https://github.com/gcla/termshark/issues?q=author%3Azoulja "Bug reports") | ## Contact - The author - Graham Clark (grclark@gmail.com) [![Follow on Twitter][twitter-follow-img]][twitter-follow-url] ## License [![License: MIT](https://img.shields.io/github/license/gcla/termshark.svg?color=yellow)](LICENSE) termshark-2.2.0/assets/000077500000000000000000000000001377442047300150305ustar00rootroot00000000000000termshark-2.2.0/assets/gen.go000066400000000000000000000000571377442047300161320ustar00rootroot00000000000000//go:generate statik -src=. -f package assets termshark-2.2.0/assets/statik/000077500000000000000000000000001377442047300163275ustar00rootroot00000000000000termshark-2.2.0/assets/statik/statik.go000066400000000000000000000313461377442047300201640ustar00rootroot00000000000000// Code generated by statik. DO NOT EDIT. package statik import ( "github.com/rakyll/statik/fs" ) func init() { data := "PK\x03\x04\x14\x00\x08\x00\x08\x00\x08\x83#R\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00 \x00gen.goUT\x05\x00\x01\xb1\xef\xf1_\xd2\xd7O\xcf\xb7JO\xcdK-J,IU(.I,\xc9\xccV\xd0-.J\xb6\xd5S\xd0M\xe3\xe2*HL\xceNLOUH,.N-)\xe6\x02\x04\x00\x00\xff\xffPK\x07\x080\xf3\x8fG5\x00\x00\x00/\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x08\x00p\x86#R\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x16\x00 \x00themes/base16-256.tomlUT\x05\x00\x01\x15\xf6\xf1_\xd4\x96\xcbr\xa3:\x10\x86\xf7~\n\x17g\x1bR!\x89\x93\x93\xc5\xac\xe61\\Y\x08\xd1\xb6Un\x10\xa5\x8b=~\xfb) ,q\xb1@\xa4\xca\x93\x99\xa5\xdb\xfd\xf1w7\xa8\xf5\xaft\xa5%\x14\xeb\x1f\xeb\xe4\xbf\xf7\x0f\xc82\x92\xacV\xdb\x9cH\xc8\xde>W\xebu\x8e\x84\x1e\xcd\xbf?9r\xf1\xf4\x94\x98\x18\x91\xf0\x94\xb9`\xf6\xec\x82\xcf>\xf8\xe2\x82/\x1e\xff\xdf\x05_}\xe6\xab\x0bn|\xe6\xbb\x0b\xbe\xf9\xcc\x8d \x9e\x0fL\x81O\xdc\x99\x98h:h\"\x1f&\xc2\x05\xa9\xf6\xb0\xf6\xac\xad\xfc\x02\x88\xfc\xecSs\x13\xdc\x0b\x80\xca\xc7\x88\x89\xd1\x0b\xe9\x84\xc0\x16\x83\xba#k\x8b\xae\xb5\xa8\xb1\x13\xb4\xf5\xe5\x82\x9f=\x9bef\x9e\x05\x11G;M\xad\x147\x7fn\x93f\xc4\x8fv\xbe\xc9\x83\xfbig\x93\xf8\xdct\xc7\xa9\x96]\xc2\xf6\xef\x89\xa6\x86.!\x01\x81*;\x91\x10\xd4\x91\xa1e\x81\xac\x82\x89\x9a\x9a\xa1u\x93\xd3q\x1fm\xd2C\xff\x19=\x86\x8b\x02D\xa4\x0e\xaf/i\xc9\x8b\xd8\xb2\xae\xe9)A\x15Y\x95C\x90\xe4\x80\x13\xb3\x12P4\x84\x16\x02*\x95RR+-\xe0\x16\xa1\x0eP\x82|l\x8e\x94\x85\nF\x90\xef\xe3\xbahr\x17\xcd\xb6\x80\x1d\xd1\xfd\x96\x87o\xda\xe5\xee\x18*\x10)\xab\x14\x88\x12\nF\xd4\xd4t\x9b\xf3\xd3\x07O\x04Y1\xc1\\'\xd5\x02%TzIes\x8f\xb7\xe7\xd4\x02\x07\xf8\x95\xe6\x17\x051\x9fz\xe7|8LW\x11\xa0\xaf\xcfp;\x06X\xdc\xd4\x1bTi6\xc7\x80\xba-7\xe0\x1a\xf5+h\xdf\xd2\x89`L\x87\xb9]\xb2c4\xaeK\xbb\xb6\x1d\x8c\xe4\x02\xe2\x0b\xa2\x0d\xb7T\xb1&\xf4\x08*E&UJ\x01q\xc9\xb2\x1b\xb1\xb1\xd2/#\\\xf0\xf3\xac\xb2{\xadC0V\xb6\xd7\xb1TBS\xb5X\xb4\xc5\"\xbe\xa6n\xa7\x82\xef\x05H\x99R^\xd6\x08\xea\xe6\xe6\xba1\xdf+\xb6d\xc78H\xd6\xac\xaa\xfa\xdb~\xb8\xc5:Z\x11\xd9^C*\x01\xa4L)2\xa8T\xc46j\xf3K\xa2\xe8!n\x11\xb7\x84\x04\"&\x11\x7fd\x1d!N\x93W\x9c\xf1\x10\x16PLa\xefE\x98j\x1f\x12ws\xac\xb6\xc8\xf6\x07\xf5\x19\xe3\x19|\x19!\xcb0\x00\xe2,C\xd8\x99\xfc;\x96a\xf0\xa1~\xc52\xf8\xe9NX\x86\xc0\xa77a\x19\xae\xc4\xb7z\x85\xe0\x87\xf4\xed^a\xae\xb2\xb9\xc7\xdf\xdb+L^\xdew\xf7\ny\xe0\xc6\x8fh1\x84\xc6\xebn\x1c|\x0f\xb3\x10V\xbc\xbbY\x08/\xbc\xbb\x9a\x85\xd9\x8e\xff\x8cYh%\xffR\xb3\xd0\xb9\xfe\xe7\x93G\xf7\xf2\xd8+\x0c\x8a\x9a\xf3\n\xe1{$\xe8\x15\x82m\x07\xbd\xc2\x88\x88\xf4\n\xbf\x03\x00\x00\xff\xffPK\x07\x08\xe7\xe3(\xa1\xaa\x02\x00\x00\xc3\x11\x00\x00PK\x03\x04\x14\x00\x08\x00\x08\x00p\x86#R\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x16\x00 \x00themes/default-16.tomlUT\x05\x00\x01\x15\xf6\xf1_\xd4\x97\xcfn\xdb0\x0c\xc6\xefy\n\xc3\xe7\xaa\xe8\xda\x0c\xd8\x0e;\xed1\x82\x1e\x14\x99I\x84\xd0\x96!S\xc9\xfc\xf6\x83%\xff\x8b-\xcbJ\x81f\xdd\xb1\x8c~\xfeDJ\xa4\xbenLa*\xc8\x92_I\xfa[\xa1\xd2//\xe9f\xb3\xcb\xe0\xc0\x0d\xd2\xfb&I\xf6\xc8\xc5\xf9\xe6\xe7$9j^\x7f\x1bb?\xba\xd8\xab'\xf6\xe6\x89m'\xb1\xebI\x12\x0c\xb1C\x13\xd3\xe3M\xfdl\"5 \xaa\xeb\x10\xdc\xbb\xcf\x01\x14C\x8c71Q\xf3\"\x19b\x90\xda4\xccHa\xdb\x84J\xa3K\x1c\x05\xbf\xdb\xcc\xb9>\xdb\xb4\x0d\x91j>\xbcK\xdbb<\xdbJ\xa4O\xfd\xdf\xb6\n\xe9\xb0\x98\x1d\x940\xd5\x0db\x13\x1b!Nr\xccT\x80 \xc8\xe6\xba\x8c9\xe5\x86\x12y\x86\xb2\x80\xe0\xbe\\\x9d\xc6\xcb\x99'\x9bvU@\x84\xed\x95\xce@Gk\xa9\xb2f\xb9\xca\xe27\xd7\x01\x8c#E\xef\xad\x87\x90\xef\x01\x83e\xd3\x909\xc6h\x0d\x051\xc1K2\x1a\xbc\x0c\x9d \x87\xea\xd9u\x83\xa52\xc9Q\x1dc\x93q\xab\xef,t\x1b\x89<\xfb\x83D\x02\xcddA\xa0s\xc8$\xa7\xe8R\xf7\xe8\x85\xa3\xcc\x82TW\xb4\x16\xc9\xa10\xf7\xedo]\xc2\xf6\xacEN\xf0\x87\xedk\x02\x7f\x1bL\xb9Q\xf7\xf4\xa0)\xee\xec\xa0\x86\xde\xc7\x86\\\xaf\xff\xde6\xc4\x8a\xffh\x1b2\xb0\x15!\xcb/\xee\xfa \x172N\xed\xaa\x0b\xf1\\G\xf3.d\xbe\x01\xdf\xe1B\xfe\x06\x00\x00\xff\xffPK\x07\x08\x18D\x83\x17\xbf\x02\x00\x00i\x12\x00\x00PK\x03\x04\x14\x00\x08\x00\x08\x00p\x86#R\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x17\x00 \x00themes/dracula-256.tomlUT\x05\x00\x01\x15\xf6\xf1_\xd4\x96\xcbr\xe3*\x10\x86\xf7~\n\x97\xce6J\x1d+\x96d/\xe6IRY\xb4\xa0\xb1\xa9\xa0K!H\xc6o?%\x90u\xb3\x90p\xaa\xe2\xc9d\x97\xdf|\xea\x0b\xd0\xfc\x1b]\xe8\x1a\xe9\xf6\xd76\xf8/=\xe2n\x07\xc1f\xf3J%\x10-\xe0m\xb3\xdd\x9e$\\v\xe6\xe7}\xb2O\xe3(h\xb5\xc8hq\x12\xa7\xc9\xee\xaa\xbd\x18-K\xb2$\xeb\xd6\xed\x8dF\x9a\xbf\xb4\xd12\x01\xe4\xddh\xd1!\x82\x97\xc4j\x1a\x8d\x94Di\x04\xfbF\"\x17(\x8ct\xc8\xf0\xc8\xa8\xfd\x1a\xa2\xd5\xe2\xff\x19\xa4Y\xa3\xe5p\xc2B\x81Q\x19K\x8f\xc4|\xaf\x94P\x9c\xb0\x15\xb3CB\x1a\xb1\xd2\xb2\x12V\xcc\xe8\xf1\x85\x1d\x1bQ\xb6\xa53\x16\xc7q\xdc(\x9fg\xaeZ\xf4\xc0\x0e\xcc\xd4qA!\xcaO+\xee\x18\x1c\x88\xe9\x11\xc8\xf7\xa6A\x99V\xaal\xf2z\x0d\xda\xb6=\x9b\x1a\x83\xa7\xee\x7f\xd3\x9b\xa0_\x1c\xb2\x92\xe8z\x84\x98\xb0\x03\xa4-l\x08\xd5(\x90(\x93\xb0\x9b\xebC\x91\x9c\n^\xe0bb\xb6\xb0\xe1\xf2p\xa6\x9cv\xd5\xd3\xe43#\xaa\x94\x14\xa5w\xac\xb2\xba\x84yI\xfd\x93\xbb\x02!\x08\xe5\x9d[\x07 \xc8P,\xb6M\"\xb5\x8c\x96\x12\x0b\x15\x12\xa8\x94\x968\xcb\xa83\xe6X?\xdb\x8bc(\xcaA\x94'\xdfb\xec\xea;\x1bM\x91\x81\x9e\xd4>-\xa2_\xcd\xb8P(C^(\x949R\x0ej\xb9\xd5\xf6\xc2\x8c\xd1\x0f\x10\x9c.R\xd7\xa6\xb5H\x8e\x85\xbe/\xbf\xf5\x10\xe6\xce\x1b\xe4\x8c\xbf\xc3\xec\xa2\xd0\xef\x1a\xd8\xcb>\x06u\xe1\x85\xf6Y6$\xe3(\xe8|\xcci\xae\xcd\xc8\x9ap\x8e\x90S\xd2\xa6pE\xcd\xae}\x80\xf0\xab\xd4\xcc\xe2[\xd6\xb3X3\xdd;Z\xc0\x05\xe5W\xc2Z\xf0\xee\x98\x15\x90wT\xa1\xe0\xb5\n \n\xe11\x17\x07\x1b{C{G\x8fnxY~z\x04\xefvx\x8azG\x1e\xd5]+\xa9\x89\x9a\x89\xeb\x85\xe5l\x9b)\x01EX\xccf^\xc3\xea\xc5!9h\xaa\xc3\xa5B\xe8\x1e\xe7\xe9\xa7H\xb8i4\x06k\xda\xea\xde\xfe\xc5\x97s\x0b'\xff\xa2)\x9a\x1a\xe9\xf2\xd7r\xf5c\x97\xa0\xef\xc3j\xb1\xd8\xd7%\x07\xc1\xfe\"}],\x97\x07\x01'_}\xb0\x8d#\xdcmW\x9d\x16(-\xda\xee\xd28\xec\xb5Piq\x98l\x92\xa8\xd76JKB\xf0\xc1o\xb5\x94\x03yS\xdaz\x1d\xa4a\xa4\xb5\x06\x95\x14DqJ\x83V\"'(\xb4\x04\xe0'\xb1>\x0dQk\xf16I\xd6\xebV\xcb\xe1\x80\x85\x04\xa5\xd20\x8cb\x05\x97\x02\x8a\x83>\x91\xa4\x9b\xd4WA\xaaFT\\\x8b\x11\xd9\xf9d\xd3\x8a\xa2+\x9e\x920\x08\xb2V\xf982\xa9\xbf\xcah\x16\xa1\xaa\xed\x84\x9c\x97\x1fJ\xfc\xbd\x8dU\xe8\xc5\x9e\x82xk\x1b\x946R\x96m^\xfb\xd5\xd0\xb8\x9f\xaa\xca\xd5\xcbDQ\xfdY\x8d\x80\x97\x95\xa4\xa9/0\x15\xfc\x0c\xeb\n\x9c\x825r$R%ng\xc7\x90$\xa7\x9c\x15x7I]\xe8\x14\xf1\xae\x96\xd7}\xf7b\x1cvF\x96\x82\xa2\x98\x15\xb3\xacN^^\xd2y\x89\xf6\x90\x07\\\xce\xcas\x009\xa4\xc8\xef\xb6S \xd5\\#\x04\x16\xd2#P\xc9F\\\xe6\xdas\xfa\xcfR\x00e\xc0\xcb\xc3\x9c\x9a4\xf1@\xef)f\xd0\x18m0k\x19\x89\x8cq\x89\xc2c\x85D\x91#e \xefw_\xff_\xe7\xf8;pvy%M\xb2\xefa\x87\xe5X4\xf3su\x0b\xa5\xd6\x85\xc2\x8e\xf8\xc7KO\x12\xdd\xff\x1c\xbd+\xce\xe1\xa6p\xc6\xc7\x8c[:c\xc8\xe9\xad\xd8f\xde\xed\xe6\xbb`o\x866i\x9dL\x8f\xab\xa9\xbe\x03w\xaf\\\xadv\x93\x9fQ\xbcz0\x86\x138\x9cP<\x1a^\xc3\x0f\xc5\xae\x80\xbc\xa1\xf48\xab\xa5G\x90s\xc7u;\x19\xbcq\xc2\xac,\x02\xe3\x0cQ~8&1\xdc\x80K|V\x06g}\xa8\xa5h\x88\xbc\x1a\xdfr\x03\xcfa\xe7;8\xa9_\x94\x07\x81u\xed\x912\xaf8\x1a\xab\xc5>\x80\x1e\x9e\xbf\xd4\x06\xb4\xaeXQ\x18\x8f\xd0\x95\x15:\x89\xeb\xcc\x8c\xf1j)\x10r\x8fp\x86\xc5\xfd<\xfb%\xd8Q9Hr\xbc\xdb\xd4\xc9\xeb\xd0q5\x82p\x00\xc7\x8d0p\xe2\xdd(\xefZ7\x1b\x8dI&\xf9\xe5\xe0\xda\x12&o\xdcb\xcf\xd9\xe1(_\xdd\xed\xd0\x98\x95\xc5\x0e\x99\xd8dLv7d3`\xdf\xdc\x0d\x99\xa3z\xdc\x0d\x8dc\xb0\xbb\xa1\xdb/\xb9\xdd\x0d\xf5\xdcWrC\xb6\xcb\xf8\xd5\xdc\x90C\xaen\xa1\x9e\xe6\x86\xeeY\x92\xa7\xba\xa1\x1bn\xc6\xb9\xf2\xb9n\xe8z\x06\x9b\xe1\x84\xcftC\xb6\xd8\xcfsC\xb6\xbd\xfb\x1c7\xe4\xd0\x87\xa7\xbb\xa1.\xfa\xb7qC\x13g\xe3\x8a\x186\xe3\xff\x99!\xeb\x83g3C\xb6\xa6|\xa2\x19\xfa\x17\x00\x00\xff\xffPK\x07\x08\x03C\x9d\xa5\xcb\x02\x00\x00V\x13\x00\x00PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00\x08\x83#R0\xf3\x8fG5\x00\x00\x00/\x00\x00\x00\x06\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xb4\x81\x00\x00\x00\x00gen.goUT\x05\x00\x01\xb1\xef\xf1_PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00p\x86#R\xe7\xe3(\xa1\xaa\x02\x00\x00\xc3\x11\x00\x00\x16\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xb4\x81r\x00\x00\x00themes/base16-256.tomlUT\x05\x00\x01\x15\xf6\xf1_PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00p\x86#R2\xaf\x1a\x17\x88\x02\x00\x00\xf5\x11\x00\x00\x16\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xb4\x81i\x03\x00\x00themes/default-16.tomlUT\x05\x00\x01\x15\xf6\xf1_PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00p\x86#R\x18D\x83\x17\xbf\x02\x00\x00i\x12\x00\x00\x17\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xb4\x81>\x06\x00\x00themes/default-256.tomlUT\x05\x00\x01\x15\xf6\xf1_PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00p\x86#R\xb18m\x91\xc9\x02\x00\x00\"\x12\x00\x00\x17\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xb4\x81K \x00\x00themes/dracula-256.tomlUT\x05\x00\x01\x15\xf6\xf1_PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00p\x86#R\x03C\x9d\xa5\xcb\x02\x00\x00V\x13\x00\x00\x19\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xb4\x81b\x0c\x00\x00themes/solarized-256.tomlUT\x05\x00\x01\x15\xf6\xf1_PK\x05\x06\x00\x00\x00\x00\x06\x00\x06\x00\xc3\x01\x00\x00}\x0f\x00\x00\x00\x00" fs.Register(data) } termshark-2.2.0/assets/themes/000077500000000000000000000000001377442047300163155ustar00rootroot00000000000000termshark-2.2.0/assets/themes/base16-256.toml000066400000000000000000000107031377442047300206060ustar00rootroot00000000000000 unused = "#79e11a" [base16] black = "Color00" base01 = "Color12" base02 = "Color13" base03 = "Color08" base04 = "Color14" base05 = "Color07" base06 = "Color15" white = "Color0f" red = "Color09" orange = "Color10" yellow = "Color0b" green = "Color0a" cyan = "Color0e" blue = "Color04" purple = "Color05" brown = "Color11" [dark] button = ["base16.black","base16.base04"] button-focus = ["base16.white","base16.purple"] button-selected = ["base16.white","base16.base04"] cmdline = ["base16.black","base16.yellow"] cmdline-button = ["base16.yellow","base16.black"] cmdline-border = ["base16.black","base16.yellow"] copy-mode = ["base16.black","base16.yellow"] copy-mode-alt = ["base16.yellow","base16.black"] copy-mode-label = ["base16.white","base16.red"] current-capture = ["base16.white","themes.unused"] dialog = ["base16.black","base16.yellow"] dialog-button = ["base16.yellow","base16.black"] default = ["base16.white","base16.black"] filter-intermediate = ["base16.black","base16.orange"] filter-invalid = ["base16.black","base16.red"] filter-menu = ["base16.white","base16.black"] filter-valid = ["base16.black","base16.green"] hex-byte-selected = ["base16.white","base16.purple"] hex-byte-unselected = ["base16.white","base16.black"] hex-field-selected = ["base16.black","base16.cyan"] hex-field-unselected = ["base16.black","base16.white"] hex-interval-selected = ["base16.white","base16.base03"] hex-interval-unselected = ["base16.white","base16.base02"] hex-layer-selected = ["base16.white","base16.base03"] hex-layer-unselected = ["base16.white","base16.base02"] packet-list-cell-focus = ["base16.white","base16.purple"] packet-list-cell-selected = ["base16.white","base16.base03"] packet-list-row-focus = ["base16.white","base16.cyan"] packet-list-row-selected = ["base16.white","base16.base02"] packet-struct-focus = ["base16.white","base16.cyan"] packet-struct-selected = ["base16.black","base16.base03"] progress-complete = ["base16.white","base16.purple"] progress-default = ["base16.white","base16.black"] progress-spinner = ["base16.yellow","base16.purple"] spinner = ["base16.yellow","base16.black"] stream-client = ["base16.black","base16.red"] stream-match = ["base16.black","base16.yellow"] stream-search = ["base16.black","base16.white"] stream-server = ["base16.black","base16.blue"] title = ["base16.red","unused"] [light] button = ["base16.black","base16.white"] button-focus = ["base16.black","base16.purple"] button-selected = ["base16.black","base16.base04"] cmdline = ["base16.black","base16.yellow"] cmdline-button = ["base16.yellow","base16.black"] cmdline-border = ["base16.black","base16.yellow"] copy-mode = ["base16.white","base16.yellow"] copy-mode-alt = ["base16.yellow","base16.white"] copy-mode-label = ["base16.black","base16.red"] current-capture = ["base16.black","unused"] dialog = ["base16.black","base16.yellow"] dialog-button = ["base16.yellow","base16.black"] default = ["base16.black","base16.white"] filter-intermediate = ["base16.black","base16.orange"] filter-invalid = ["base16.black","base16.red"] filter-menu = ["base16.black","base16.white"] filter-valid = ["base16.black","base16.green"] hex-byte-selected = ["base16.white","base16.purple"] hex-byte-unselected = ["base16.black","base16.white"] hex-field-selected = ["base16.black","base16.cyan"] hex-field-unselected = ["base16.black","base16.base03"] hex-interval-selected = ["base16.white","base16.base03"] hex-interval-unselected = ["base16.black","base16.base05"] hex-layer-selected = ["base16.white","base16.base03"] hex-layer-unselected = ["base16.black","base16.base05"] packet-list-cell-focus = ["base16.white","base16.purple"] packet-list-cell-selected = ["base16.black","base16.base04"] packet-list-row-focus = ["base16.white","base16.cyan"] packet-list-row-selected = ["base16.black","base16.base05"] packet-struct-focus = ["base16.white","base16.cyan"] packet-struct-selected = ["base16.black","base16.base05"] progress-complete = ["base16.white","base16.purple"] progress-default = ["base16.white","base16.black"] progress-spinner = ["base16.yellow","base16.black"] spinner = ["base16.yellow","base16.white"] stream-client = ["base16.white","base16.red"] stream-match = ["base16.white","base16.yellow"] stream-search = ["base16.white","base16.black"] stream-server = ["base16.white","base16.blue"] title = ["base16.red","unused"] termshark-2.2.0/assets/themes/default-16.toml000066400000000000000000000107651377442047300210730ustar00rootroot00000000000000 unused = "Color00" [default] black = "Color00" gray1 = "Color08" gray2 = "Color08" gray3 = "Color08" gray4 = "Color08" white = "Color0f" red = "Color09" yellow = "Color0b" green = "Color0a" cyan = "Color0e" blue = "Color04" purple = "Color05" [dark] button = ["default.black","default.gray1"] button-focus = ["default.white","default.purple"] button-selected = ["default.white","default.black"] cmdline = ["default.black","default.yellow"] cmdline-button = ["default.yellow","default.black"] cmdline-border = ["default.black","default.yellow"] copy-mode = ["default.black","default.yellow"] copy-mode-alt = ["default.yellow","default.black"] copy-mode-label = ["default.white","default.red"] current-capture = ["default.white","themes.unused"] dialog = ["default.black","default.yellow"] dialog-button = ["default.yellow","default.black"] default = ["default.white","default.black"] filter-intermediate = ["default.black","default.yellow"] filter-invalid = ["default.black","default.red"] filter-menu = ["default.white","default.black"] filter-valid = ["default.black","default.green"] hex-byte-selected = ["default.black","default.purple"] hex-byte-unselected = ["default.white","default.black"] hex-field-selected = ["default.black","default.cyan"] hex-field-unselected = ["default.black","default.white"] hex-interval-selected = ["default.white","default.gray2"] hex-interval-unselected = ["default.white","default.gray1"] hex-layer-selected = ["default.white","default.gray2"] hex-layer-unselected = ["default.white","default.gray1"] packet-list-cell-focus = ["default.black","default.purple"] packet-list-cell-selected = ["default.white","default.gray2"] packet-list-row-focus = ["default.gray2","default.cyan"] packet-list-row-selected = ["default.white","default.gray1"] packet-struct-focus = ["default.black","default.cyan"] packet-struct-selected = ["default.black","default.gray2"] progress-complete = ["default.white","default.purple"] progress-default = ["default.white","default.black"] progress-spinner = ["default.yellow","default.purple"] spinner = ["default.yellow","default.black"] stream-client = ["default.black","default.red"] stream-match = ["default.black","default.yellow"] stream-search = ["default.black","default.white"] stream-server = ["default.cyan","default.blue"] title = ["default.red","unused"] [light] button = ["default.white","default.gray3"] button-focus = ["default.black","default.purple"] button-selected = ["default.black","default.gray3"] cmdline = ["default.black","default.yellow"] cmdline-button = ["default.yellow","default.black"] cmdline-border = ["default.black","default.yellow"] copy-mode = ["default.white","default.yellow"] copy-mode-alt = ["default.yellow","default.white"] copy-mode-label = ["default.black","default.red"] current-capture = ["default.black","unused"] dialog = ["default.black","default.yellow"] dialog-button = ["default.yellow","default.black"] default = ["default.black","default.white"] filter-intermediate = ["default.black","default.yellow"] filter-invalid = ["default.black","default.red"] filter-menu = ["default.black","default.white"] filter-valid = ["default.black","default.green"] hex-byte-selected = ["default.black","default.purple"] hex-byte-unselected = ["default.black","default.white"] hex-field-selected = ["default.black","default.cyan"] hex-field-unselected = ["default.black","default.gray2"] hex-interval-selected = ["default.white","default.gray2"] hex-interval-unselected = ["default.black","default.gray4"] hex-layer-selected = ["default.white","default.gray2"] hex-layer-unselected = ["default.black","default.gray4"] packet-list-cell-focus = ["default.black","default.purple"] packet-list-cell-selected = ["default.black","default.gray3"] packet-list-row-focus = ["default.black","default.cyan"] packet-list-row-selected = ["default.black","default.gray4"] packet-struct-focus = ["default.black","default.cyan"] packet-struct-selected = ["default.black","default.gray4"] progress-complete = ["default.white","default.purple"] progress-default = ["default.white","default.black"] progress-spinner = ["default.yellow","default.black"] spinner = ["default.yellow","default.white"] stream-client = ["default.white","default.red"] stream-match = ["default.white","default.yellow"] stream-search = ["default.white","default.black"] stream-server = ["default.cyan","default.blue"] title = ["default.red","unused"] termshark-2.2.0/assets/themes/default-256.toml000066400000000000000000000111511377442047300211470ustar00rootroot00000000000000 unused = "#79e11a" [default] black = "#000000" gray02 = "#464752" gray03 = "#565761" gray04 = "#969692" gray05 = "#ccccc7" white = "#ffffff" red = "#ff0000" orange = "#ffa42c" yellow = "#ffff00" green = "#5fff00" cyan = "#0087ff" blue = "#0000ff" brightblue = "#0084fb" purple = "#800080" magenta = "#8b008b" [dark] button = ["default.black","default.gray04"] button-focus = ["default.white","default.magenta"] button-selected = ["default.white","default.gray04"] cmdline = ["default.black","default.yellow"] cmdline-button = ["default.yellow","default.black"] cmdline-border = ["default.black","default.yellow"] copy-mode = ["default.black","default.yellow"] copy-mode-alt = ["default.yellow","default.black"] copy-mode-label = ["default.white","default.red"] current-capture = ["default.white","themes.unused"] dialog = ["default.black","default.yellow"] dialog-button = ["default.yellow","default.black"] default = ["default.white","default.black"] filter-intermediate = ["default.black","default.orange"] filter-invalid = ["default.black","default.red"] filter-menu = ["default.white","default.black"] filter-valid = ["default.black","default.green"] hex-byte-selected = ["default.white","default.purple"] hex-byte-unselected = ["default.white","default.black"] hex-field-selected = ["default.black","default.cyan"] hex-field-unselected = ["default.black","default.white"] hex-interval-selected = ["default.white","default.gray03"] hex-interval-unselected = ["default.white","default.gray02"] hex-layer-selected = ["default.white","default.gray03"] hex-layer-unselected = ["default.white","default.gray02"] packet-list-cell-focus = ["default.white","default.purple"] packet-list-cell-selected = ["default.white","default.gray03"] packet-list-row-focus = ["default.white","default.brightblue"] packet-list-row-selected = ["default.white","default.gray02"] packet-struct-focus = ["default.white","default.brightblue"] packet-struct-selected = ["default.black","default.gray03"] progress-complete = ["default.white","default.purple"] progress-default = ["default.white","default.black"] progress-spinner = ["default.yellow","default.purple"] spinner = ["default.yellow","default.black"] stream-client = ["default.black","default.red"] stream-match = ["default.black","default.yellow"] stream-search = ["default.black","default.white"] stream-server = ["default.white","default.blue"] title = ["default.red","unused"] [light] button = ["default.black","default.white"] button-focus = ["default.black","default.purple"] button-selected = ["default.black","default.gray04"] cmdline = ["default.black","default.yellow"] cmdline-button = ["default.yellow","default.black"] cmdline-border = ["default.black","default.yellow"] copy-mode = ["default.white","default.yellow"] copy-mode-alt = ["default.yellow","default.white"] copy-mode-label = ["default.black","default.red"] current-capture = ["default.black","unused"] dialog = ["default.black","default.yellow"] dialog-button = ["default.yellow","default.black"] default = ["default.black","default.white"] filter-intermediate = ["default.black","default.orange"] filter-invalid = ["default.black","default.red"] filter-menu = ["default.black","default.white"] filter-valid = ["default.black","default.green"] hex-byte-selected = ["default.white","default.purple"] hex-byte-unselected = ["default.black","default.white"] hex-field-selected = ["default.black","default.cyan"] hex-field-unselected = ["default.black","default.gray03"] hex-interval-selected = ["default.white","default.gray03"] hex-interval-unselected = ["default.black","default.gray05"] hex-layer-selected = ["default.white","default.gray03"] hex-layer-unselected = ["default.black","default.gray05"] packet-list-cell-focus = ["default.white","default.purple"] packet-list-cell-selected = ["default.black","default.gray04"] packet-list-row-focus = ["default.white","default.brightblue"] packet-list-row-selected = ["default.black","default.gray05"] packet-struct-focus = ["default.white","default.brightblue"] packet-struct-selected = ["default.black","default.gray05"] progress-complete = ["default.white","default.purple"] progress-default = ["default.white","default.black"] progress-spinner = ["default.yellow","default.black"] spinner = ["default.yellow","default.white"] stream-client = ["default.white","default.red"] stream-match = ["default.white","default.yellow"] stream-search = ["default.white","default.black"] stream-server = ["default.white","default.blue"] title = ["default.red","unused"] termshark-2.2.0/assets/themes/dracula-256.toml000066400000000000000000000110421377442047300211350ustar00rootroot00000000000000 unused = "#79e11a" [dracula] gray1 = "#464752" gray2 = "#565761" gray3 = "#b6b6b2" gray4 = "#ccccc7" black = "#282a36" blue = "#6272a4" cyan = "#8be9fd" green = "#50fa7b" magenta = "#ff79c6" orange = "#ffb86c" purple = "#bd93f9" red = "#ff5555" white = "#f8f8f2" yellow = "#f1fa8c" [dark] button = ["dracula.black","dracula.gray3"] button-focus = ["dracula.white","dracula.magenta"] button-selected = ["dracula.white","dracula.gray3"] cmdline = ["dracula.black","dracula.yellow"] cmdline-button = ["dracula.yellow","dracula.black"] cmdline-border = ["dracula.black","dracula.yellow"] copy-mode = ["dracula.black","dracula.yellow"] copy-mode-alt = ["dracula.yellow","dracula.black"] copy-mode-label = ["dracula.white","dracula.red"] current-capture = ["dracula.white","themes.unused"] dialog = ["dracula.black","dracula.yellow"] dialog-button = ["dracula.yellow","dracula.black"] default = ["dracula.white","dracula.black"] filter-intermediate = ["dracula.black","dracula.orange"] filter-invalid = ["dracula.black","dracula.red"] filter-menu = ["dracula.white","dracula.black"] filter-valid = ["dracula.black","dracula.green"] hex-byte-selected = ["dracula.white","dracula.purple"] hex-byte-unselected = ["dracula.white","dracula.black"] hex-field-selected = ["dracula.black","dracula.cyan"] hex-field-unselected = ["dracula.black","dracula.white"] hex-interval-selected = ["dracula.white","dracula.gray2"] hex-interval-unselected = ["dracula.white","dracula.gray1"] hex-layer-selected = ["dracula.white","dracula.gray2"] hex-layer-unselected = ["dracula.white","dracula.gray1"] packet-list-cell-focus = ["dracula.white","dracula.purple"] packet-list-cell-selected = ["dracula.white","dracula.gray2"] packet-list-row-focus = ["dracula.white","dracula.cyan"] packet-list-row-selected = ["dracula.white","dracula.gray1"] packet-struct-focus = ["dracula.black","dracula.cyan"] packet-struct-selected = ["dracula.black","dracula.gray2"] progress-complete = ["dracula.white","dracula.purple"] progress-default = ["dracula.white","dracula.black"] progress-spinner = ["dracula.yellow","dracula.purple"] spinner = ["dracula.yellow","dracula.black"] stream-client = ["dracula.black","dracula.red"] stream-match = ["dracula.black","dracula.yellow"] stream-search = ["dracula.black","dracula.white"] stream-server = ["dracula.white","dracula.blue"] title = ["dracula.red","unused"] [light] button = ["dracula.black","dracula.white"] button-focus = ["dracula.black","dracula.purple"] button-selected = ["dracula.black","dracula.gray3"] cmdline = ["dracula.black","dracula.yellow"] cmdline-button = ["dracula.yellow","dracula.black"] cmdline-border = ["dracula.black","dracula.yellow"] copy-mode = ["dracula.white","dracula.yellow"] copy-mode-alt = ["dracula.yellow","dracula.white"] copy-mode-label = ["dracula.black","dracula.red"] current-capture = ["dracula.black","unused"] dialog = ["dracula.black","dracula.yellow"] dialog-button = ["dracula.yellow","dracula.black"] default = ["dracula.black","dracula.white"] filter-intermediate = ["dracula.black","dracula.orange"] filter-invalid = ["dracula.black","dracula.red"] filter-menu = ["dracula.black","dracula.white"] filter-valid = ["dracula.black","dracula.green"] hex-byte-selected = ["dracula.white","dracula.purple"] hex-byte-unselected = ["dracula.black","dracula.white"] hex-field-selected = ["dracula.black","dracula.cyan"] hex-field-unselected = ["dracula.black","dracula.gray2"] hex-interval-selected = ["dracula.white","dracula.gray2"] hex-interval-unselected = ["dracula.black","dracula.gray4"] hex-layer-selected = ["dracula.white","dracula.gray2"] hex-layer-unselected = ["dracula.black","dracula.gray4"] packet-list-cell-focus = ["dracula.white","dracula.purple"] packet-list-cell-selected = ["dracula.black","dracula.gray3"] packet-list-row-focus = ["dracula.white","dracula.cyan"] packet-list-row-selected = ["dracula.black","dracula.gray4"] packet-struct-focus = ["dracula.black","dracula.cyan"] packet-struct-selected = ["dracula.black","dracula.gray4"] progress-complete = ["dracula.white","dracula.purple"] progress-default = ["dracula.white","dracula.black"] progress-spinner = ["dracula.yellow","dracula.black"] spinner = ["dracula.yellow","dracula.white"] stream-client = ["dracula.white","dracula.red"] stream-match = ["dracula.white","dracula.yellow"] stream-search = ["dracula.white","dracula.black"] stream-server = ["dracula.white","dracula.blue"] title = ["dracula.red","unused"] termshark-2.2.0/assets/themes/solarized-256.toml000066400000000000000000000115261377442047300215250ustar00rootroot00000000000000 unused = "#79e11a" [solarized] gray1 = "#586e75" gray2 = "#657b83" gray3 = "#839496" gray4 = "#93a1a1" black = "#002b36" blue = "#268bd2" cyan = "#2aa198" green = "#859900" magenta = "#d33682" orange = "#cb4b16" purple = "#6c71c4" red = "#dc322f" white = "#fdf6e3" yellow = "#B58900" [dark] button = ["solarized.black","solarized.gray3"] button-focus = ["solarized.white","solarized.magenta"] button-selected = ["solarized.white","solarized.gray3"] cmdline = ["solarized.black","solarized.yellow"] cmdline-button = ["solarized.yellow","solarized.black"] cmdline-border = ["solarized.black","solarized.yellow"] copy-mode = ["solarized.black","solarized.yellow"] copy-mode-alt = ["solarized.yellow","solarized.black"] copy-mode-label = ["solarized.white","solarized.red"] current-capture = ["solarized.white","unused"] dialog = ["solarized.black","solarized.yellow"] dialog-button = ["solarized.yellow","solarized.black"] default = ["solarized.white","solarized.black"] filter-intermediate = ["solarized.black","solarized.orange"] filter-invalid = ["solarized.black","solarized.red"] filter-menu = ["solarized.white","solarized.black"] filter-valid = ["solarized.black","solarized.green"] hex-byte-selected = ["solarized.white","solarized.purple"] hex-byte-unselected = ["solarized.white","solarized.black"] hex-field-selected = ["solarized.black","solarized.cyan"] hex-field-unselected = ["solarized.black","solarized.white"] hex-interval-selected = ["solarized.white","solarized.gray2"] hex-interval-unselected = ["solarized.white","solarized.gray1"] hex-layer-selected = ["solarized.white","solarized.gray2"] hex-layer-unselected = ["solarized.white","solarized.gray1"] packet-list-cell-focus = ["solarized.white","solarized.purple"] packet-list-cell-selected = ["solarized.white","solarized.gray2"] packet-list-row-focus = ["solarized.white","solarized.cyan"] packet-list-row-selected = ["solarized.white","solarized.gray1"] packet-struct-focus = ["solarized.black","solarized.cyan"] packet-struct-selected = ["solarized.black","solarized.gray2"] progress-complete = ["solarized.white","solarized.purple"] progress-default = ["solarized.white","solarized.black"] progress-spinner = ["solarized.yellow","solarized.purple"] spinner = ["solarized.yellow","solarized.black"] stream-client = ["solarized.white","solarized.red"] stream-match = ["solarized.black","solarized.yellow"] stream-search = ["solarized.black","solarized.white"] stream-server = ["solarized.white","solarized.blue"] title = ["solarized.red","unused"] [light] button = ["solarized.black","solarized.white"] button-focus = ["solarized.black","solarized.purple"] button-selected = ["solarized.black","solarized.gray3"] cmdline = ["solarized.black","solarized.yellow"] cmdline-button = ["solarized.yellow","solarized.black"] cmdline-border = ["solarized.black","solarized.yellow"] copy-mode = ["solarized.white","solarized.yellow"] copy-mode-alt = ["solarized.yellow","solarized.white"] copy-mode-label = ["solarized.black","solarized.red"] current-capture = ["solarized.black","unused"] dialog = ["solarized.black","solarized.yellow"] dialog-button = ["solarized.yellow","solarized.black"] default = ["solarized.black","solarized.white"] filter-intermediate = ["solarized.black","solarized.orange"] filter-invalid = ["solarized.black","solarized.red"] filter-menu = ["solarized.black","solarized.white"] filter-valid = ["solarized.black","solarized.green"] hex-byte-selected = ["solarized.white","solarized.purple"] hex-byte-unselected = ["solarized.black","solarized.white"] hex-field-selected = ["solarized.black","solarized.cyan"] hex-field-unselected = ["solarized.black","solarized.gray2"] hex-interval-selected = ["solarized.white","solarized.gray2"] hex-interval-unselected = ["solarized.black","solarized.gray4"] hex-layer-selected = ["solarized.white","solarized.gray2"] hex-layer-unselected = ["solarized.black","solarized.gray4"] packet-list-cell-focus = ["solarized.white","solarized.purple"] packet-list-cell-selected = ["solarized.black","solarized.gray3"] packet-list-row-focus = ["solarized.white","solarized.cyan"] packet-list-row-selected = ["solarized.black","solarized.gray4"] packet-struct-focus = ["solarized.black","solarized.cyan"] packet-struct-selected = ["solarized.black","solarized.gray4"] progress-complete = ["solarized.white","solarized.purple"] progress-default = ["solarized.white","solarized.black"] progress-spinner = ["solarized.yellow","solarized.black"] spinner = ["solarized.yellow","solarized.white"] stream-client = ["solarized.white","solarized.red"] stream-match = ["solarized.white","solarized.yellow"] stream-search = ["solarized.white","solarized.black"] stream-server = ["solarized.white","solarized.blue"] title = ["solarized.red","unused"] termshark-2.2.0/capinfo/000077500000000000000000000000001377442047300151455ustar00rootroot00000000000000termshark-2.2.0/capinfo/loader.go000066400000000000000000000103701377442047300167430ustar00rootroot00000000000000// Copyright 2019-2021 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 capinfo import ( "bytes" "context" "fmt" "os/exec" "sync" "github.com/gcla/gowid" "github.com/gcla/termshark/v2" "github.com/gcla/termshark/v2/pcap" log "github.com/sirupsen/logrus" ) //====================================================================== var Goroutinewg *sync.WaitGroup //====================================================================== type ILoaderCmds interface { Capinfo(pcap string) pcap.IPcapCommand } type commands struct{} func MakeCommands() commands { return commands{} } var _ ILoaderCmds = commands{} func (c commands) Capinfo(pcapfile string) pcap.IPcapCommand { args := []string{pcapfile} return &pcap.Command{ Cmd: exec.Command(termshark.CapinfosBin(), args...), } } //====================================================================== type Loader struct { cmds ILoaderCmds SuppressErrors bool // if true, don't report process errors e.g. at shutdown mainCtx context.Context // cancelling this cancels the dependent contexts mainCancelFn context.CancelFunc capinfoCtx context.Context capinfoCancelFn context.CancelFunc capinfoCmd pcap.IPcapCommand } func NewLoader(cmds ILoaderCmds, ctx context.Context) *Loader { res := &Loader{ cmds: cmds, } res.mainCtx, res.mainCancelFn = context.WithCancel(ctx) return res } func (c *Loader) StopLoad() { if c.capinfoCancelFn != nil { c.capinfoCancelFn() } } //====================================================================== type ICapinfoCallbacks interface { OnCapinfoData(data string) AfterCapinfoEnd(success bool) } func (c *Loader) StartLoad(pcap string, app gowid.IApp, cb ICapinfoCallbacks) { termshark.TrackedGo(func() { c.loadCapinfoAsync(pcap, app, cb) }, Goroutinewg) } func (c *Loader) loadCapinfoAsync(pcapf string, app gowid.IApp, cb ICapinfoCallbacks) { c.capinfoCtx, c.capinfoCancelFn = context.WithCancel(c.mainCtx) procChan := make(chan int) pid := 0 defer func() { if pid == 0 { close(procChan) } }() c.capinfoCmd = c.cmds.Capinfo(pcapf) termChan := make(chan error) termshark.TrackedGo(func() { var err error cmd := c.capinfoCmd cancelledChan := c.capinfoCtx.Done() procChan := procChan state := pcap.NotStarted kill := func() { err := termshark.KillIfPossible(cmd) if err != nil { log.Infof("Did not kill tshark capinfos process: %v", err) } } loop: for { select { case err = <-termChan: state = pcap.Terminated if !c.SuppressErrors && err != nil { if _, ok := err.(*exec.ExitError); ok { cerr := gowid.WithKVs(termshark.BadCommand, map[string]interface{}{ "command": c.capinfoCmd.String(), "error": err, }) pcap.HandleError(pcap.CapinfoCode, app, cerr, cb) } } case pid := <-procChan: procChan = nil if pid != 0 { state = pcap.Started if cancelledChan == nil { kill() } } case <-cancelledChan: cancelledChan = nil if state == pcap.Started { kill() } } if state == pcap.Terminated || (cancelledChan == nil && state == pcap.NotStarted) { break loop } } }, Goroutinewg) capinfoOut, err := c.capinfoCmd.StdoutReader() if err != nil { pcap.HandleError(pcap.CapinfoCode, app, err, cb) return } defer func() { cb.AfterCapinfoEnd(true) }() app.Run(gowid.RunFunction(func(app gowid.IApp) { pcap.HandleBegin(pcap.CapinfoCode, app, cb) })) defer func() { app.Run(gowid.RunFunction(func(app gowid.IApp) { pcap.HandleEnd(pcap.CapinfoCode, app, cb) })) }() err = c.capinfoCmd.Start() if err != nil { err = fmt.Errorf("Error starting capinfo %v: %v", c.capinfoCmd, err) pcap.HandleError(pcap.CapinfoCode, app, err, cb) return } log.Infof("Started capinfo command %v with pid %d", c.capinfoCmd, c.capinfoCmd.Pid()) termshark.TrackedGo(func() { termChan <- c.capinfoCmd.Wait() }, Goroutinewg) pid = c.capinfoCmd.Pid() procChan <- pid buf := new(bytes.Buffer) buf.ReadFrom(capinfoOut) cb.OnCapinfoData(buf.String()) c.capinfoCancelFn() } //====================================================================== // Local Variables: // mode: Go // fill-column: 78 // End: termshark-2.2.0/cli/000077500000000000000000000000001377442047300142755ustar00rootroot00000000000000termshark-2.2.0/cli/all.go000066400000000000000000000063141377442047300154000ustar00rootroot00000000000000// Copyright 2019-2021 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 cli import "github.com/jessevdk/go-flags" //====================================================================== // Used to determine if we should run tshark instead e.g. stdout is not a tty type Tshark 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)."` PrintIfaces bool `short:"D" optional:"true" optional-value:"true" description:"Print a list of the interfaces on which termshark can capture."` TailSwitch } // 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."` 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:""` CaptureFilter string `short:"f" description:"Apply capture filter." value-name:""` TimestampFormat string `short:"t" description:"Set the format of the packet timestamp printed in summary lines." choice:"a" choice:"ad" choice:"adoy" choice:"d" choice:"dd" choice:"e" choice:"r" choice:"u" choice:"ud" choice:"udoy" value-name:""` PlatformSwitches PassThru string `long:"pass-thru" default:"auto" optional:"true" optional-value:"true" choice:"auto" choice:"true" choice:"false" description:"Run tshark instead (auto => if stdout is not a tty)."` LogTty bool `long:"log-tty" optional:"true" optional-value:"true" choice:"true" choice:"false" description:"Log to the terminal."` Debug TriState `long:"debug" default:"unset" hidden:"true" optional:"true" optional-value:"true" description:"Enable termshark debugging. See https://termshark.io/userguide."` 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 { FilterOrPcap string `value-name:"" description:"Filter (capture for iface, display for pcap), or pcap 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. var TermsharkOnly = []string{"--pass-thru", "--log-tty", "--debug", "--tail"} func FlagIsTrue(val string) bool { return val == "true" || val == "yes" } //====================================================================== // Local Variables: // mode: Go // fill-column: 78 // End: termshark-2.2.0/cli/flags.go000066400000000000000000000014231377442047300157200ustar00rootroot00000000000000// Copyright 2019-2021 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 !windows package cli //====================================================================== // Embedded in the CLI options struct. type PlatformSwitches struct { Tty string `long:"tty" description:"Display the UI on this terminal." value-name:""` } func (p PlatformSwitches) TtyValue() string { return p.Tty } //====================================================================== type TailSwitch struct{} func (t TailSwitch) TailFileValue() string { return "" } //====================================================================== // Local Variables: // mode: Go // fill-column: 78 // End: termshark-2.2.0/cli/flags_windows.go000066400000000000000000000014321377442047300174720ustar00rootroot00000000000000// Copyright 2019-2021 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 cli import "github.com/jessevdk/go-flags" //====================================================================== type PlatformSwitches struct{} func (p PlatformSwitches) TtyValue() string { return "" } //====================================================================== type TailSwitch struct { Tail flags.Filename `value-name:"" long:"tail" hidden:"true" description:"Tail a file (private)."` } func (t TailSwitch) TailFileValue() string { return string(t.Tail) } //====================================================================== // Local Variables: // mode: Go // fill-column: 78 // End: termshark-2.2.0/cli/tristate.go000066400000000000000000000015701377442047300164660ustar00rootroot00000000000000// Copyright 2019-2021 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 cli //====================================================================== type TriState struct { Set bool Val bool } func (b *TriState) UnmarshalFlag(value string) error { switch value { case "true", "TRUE", "t", "T", "1", "y", "Y", "yes", "Yes", "YES": b.Set = true b.Val = true case "false", "FALSE", "f", "F", "0", "n", "N", "no", "No", "NO": b.Set = true b.Val = false default: b.Set = false } return nil } func (b TriState) MarshalFlag() string { if b.Set { if b.Val { return "true" } else { return "false" } } else { return "unset" } } //====================================================================== // Local Variables: // mode: Go // fill-column: 78 // End: termshark-2.2.0/cmd/000077500000000000000000000000001377442047300142715ustar00rootroot00000000000000termshark-2.2.0/cmd/termshark/000077500000000000000000000000001377442047300162715ustar00rootroot00000000000000termshark-2.2.0/cmd/termshark/termshark.go000066400000000000000000001203431377442047300206230ustar00rootroot00000000000000// Copyright 2019-2021 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 ( "fmt" "os" "os/exec" "path/filepath" "runtime" "strconv" "strings" "sync" "syscall" "time" "github.com/blang/semver" "github.com/gcla/gowid" "github.com/gcla/termshark/v2" "github.com/gcla/termshark/v2/capinfo" "github.com/gcla/termshark/v2/cli" "github.com/gcla/termshark/v2/convs" "github.com/gcla/termshark/v2/pcap" "github.com/gcla/termshark/v2/streams" "github.com/gcla/termshark/v2/system" "github.com/gcla/termshark/v2/theme" "github.com/gcla/termshark/v2/tty" "github.com/gcla/termshark/v2/ui" "github.com/gcla/termshark/v2/widgets/filter" "github.com/gdamore/tcell" flags "github.com/jessevdk/go-flags" "github.com/mattn/go-isatty" "github.com/shibukawa/configdir" log "github.com/sirupsen/logrus" "github.com/spf13/viper" "net/http" _ "net/http" _ "net/http/pprof" ) //====================================================================== // 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. var ensureGoroutinesStopWG sync.WaitGroup filter.Goroutinewg = &ensureGoroutinesStopWG termshark.Goroutinewg = &ensureGoroutinesStopWG pcap.Goroutinewg = &ensureGoroutinesStopWG streams.Goroutinewg = &ensureGoroutinesStopWG capinfo.Goroutinewg = &ensureGoroutinesStopWG convs.Goroutinewg = &ensureGoroutinesStopWG ui.Goroutinewg = &ensureGoroutinesStopWG res := cmain() ensureGoroutinesStopWG.Wait() os.Exit(res) } func cmain() int { startedSuccessfully := false // true if we reached the point where packets were received and the UI started. uiSuspended := false // true if the UI was suspended due to SIGTSTP sigChan := make(chan os.Signal, 100) // SIGINT and SIGQUIT will arrive only via an external kill command, // not the keyboard, because our line discipline is set up to pass // ctrl-c and ctrl-\ to termshark as keypress events. But we slightly // modify tcell's default and set up ctrl-z to invoke signal SIGTSTP // on the foreground process group. An alternative would just be to // recognize ctrl-z in termshark and issue a SIGSTOP to getpid() from // termshark but this wouldn't stop other processes in a termshark // pipeline e.g. // // tcpdump -i eth0 -w - | termshark -i - // // sending SIGSTOP to getpid() would not stop tcpdump. The expectation // with bash job control is that all processes in the foreground // process group will be suspended. I could send SIGSTOP to 0, to try // to get all processes in the group, but if e.g. tcpdump is running // as root and termshark is not, tcpdump will not be suspended. If // instead I set the line discipline such that ctrl-z is not passed // through but maps to SIGTSTP, then tcpdump will be stopped by ctrl-z // via the shell by virtue of the fact that when all pipeline // processes start running, they use the same tty line discipline. system.RegisterForSignals(sigChan) 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...") } if os.Getenv("TERMSHARK_CAPTURE_MODE") == "1" { err = system.DumpcapExt(termshark.DumpcapBin(), termshark.TSharkBin(), os.Args[1:]...) if err != nil { return 1 } else { return 0 } } // Used to determine if we should run tshark instead e.g. stdout is not a tty var tsopts cli.Tshark // 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 } } if tsopts.TailFileValue() != "" { err = termshark.TailFile(tsopts.TailFileValue()) if err != nil { fmt.Fprintf(os.Stderr, "Error: %v", err) return 1 } else { return 0 } } // 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 && (cli.FlagIsTrue(tsopts.PassThru) || (tsopts.PassThru == "auto" && !isatty.IsTerminal(os.Stdout.Fd())) || tsopts.PrintIfaces) { tsharkBin, kverr := termshark.TSharkPath() if kverr != nil { fmt.Fprintf(os.Stderr, kverr.KeyVals["msg"].(string)) return 1 } args := []string{} for _, arg := range os.Args[1:] { if !termshark.StringInSlice(arg, cli.TermsharkOnly) && !termshark.StringIsArgPrefixOf(arg, cli.TermsharkOnly) { args = append(args, arg) } } args = append([]string{tsharkBin}, args...) if runtime.GOOS != "windows" { err = syscall.Exec(tsharkBin, 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 } } // Termshark's own command line arguments. Used if we don't pass through to tshark. var opts cli.Termshark // 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.Fprintf(os.Stderr, "Command-line error: %v\n\n", err) ui.WriteHelp(tmFlags, os.Stderr) return 1 } if opts.Help { ui.WriteHelp(tmFlags, os.Stdout) return 0 } if len(opts.Version) > 0 { res := 0 ui.WriteVersion(tmFlags, os.Stdout) if len(opts.Version) > 1 { if tsharkBin, kverr := termshark.TSharkPath(); kverr != nil { fmt.Fprintf(os.Stderr, kverr.KeyVals["msg"].(string)) res = 1 } else { if ver, err := termshark.TSharkVersion(tsharkBin); err != nil { fmt.Fprintf(os.Stderr, "Could not determine version of tshark from binary %s\n", tsharkBin) res = 1 } else { ui.WriteTsharkVersion(tmFlags, tsharkBin, ver, os.Stdout) } } } return res } usetty := opts.TtyValue() if usetty != "" { if ttyf, err := os.Open(usetty); err != nil { fmt.Fprintf(os.Stderr, "Could not open terminal %s: %v.\n", usetty, err) return 1 } else { if !isatty.IsTerminal(ttyf.Fd()) { fmt.Fprintf(os.Stderr, "%s is not a terminal.\n", usetty) ttyf.Close() return 1 } ttyf.Close() } } else { // Always override - in case the user has GOWID_TTY in a shell script (if they're // using the gcla fork of tcell for another application). usetty = "/dev/tty" } os.Setenv("GOWID_TTY", usetty) // Allow the user to override the shell's TERM variable this way. Perhaps the user runs // under screen/tmux, and the TERM variable doesn't reflect the fact their preferred // terminal emumlator supports 256 colors. termVar := termshark.ConfString("main.term", "") if termVar != "" { log.Infof("Configuration file overrides TERM setting, using TERM=%s", termVar) os.Setenv("TERM", termVar) } var psrcs []pcap.IPacketSource defer func() { for _, psrc := range psrcs { if psrc != nil { if remover, ok := psrc.(pcap.ISourceRemover); ok { remover.Remove() } } } }() 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 == "" && len(opts.Ifaces) == 0 { pcapf = string(opts.Args.FilterOrPcap) // `termshark` => `termshark -i 1` (livecapture on default interface if no args) if pcapf == "" { if termshark.IsTerminal(os.Stdin.Fd()) { pfile, err := system.PickFile() switch err { case nil: // We're on termux/android, and we were given a file. Note that termux // makes a copy, so we ought to clean that up when termshark terminates. psrcs = append(psrcs, pcap.TemporaryFileSource{pcap.FileSource{Filename: pfile}}) case system.NoPicker: // We're not on termux/android. Treat like this: // $ termshark // # use network interface 1 - maps to // # termshark -i 1 psrcs = append(psrcs, pcap.InterfaceSource{Iface: "1"}) default: // We're on termux/android, but got an unexpected error. //if err != termshark.NoPicker { // !NoPicker means we could be on android/termux, but something else went wrong if err = system.PickFileError(err.Error()); err != nil { // Termux's toast ran into an error...! Maybe not installed? fmt.Fprintf(os.Stderr, err.Error()) } return 1 } } else { // $ cat foo.pcap | termshark // # use stdin - maps to // $ cat foo.pcap | termshark -r - psrcs = append(psrcs, pcap.FileSource{Filename: "-"}) } } } else { // Add it to filter args. Figure out later if they're capture or display. filterArgs = append(filterArgs, opts.Args.FilterOrPcap) } if pcapf != "" && len(opts.Ifaces) > 0 { fmt.Fprintf(os.Stderr, "Please supply either a pcap or one or more live captures.\n") return 1 } // Invariant: pcap != "" XOR len(opts.Ifaces) > 0 if len(psrcs) == 0 { switch { case pcapf != "": psrcs = append(psrcs, pcap.FileSource{Filename: pcapf}) case len(opts.Ifaces) > 0: for _, iface := range opts.Ifaces { psrcs = append(psrcs, pcap.InterfaceSource{Iface: iface}) } } } // Here we check for // (a) sources named '-' - these need rewritten to /dev/fd/N and stdin needs to be moved // (b) fifo sources - these are switched from -r to -i because that's what tshark needs haveStdin := false for pi, psrc := range psrcs { switch { case psrc.Name() == "-": if haveStdin { fmt.Fprintf(os.Stderr, "Requested live capture %v (\"stdin\") cannot be supplied more than once.\n", psrc.Name()) return 1 } if termshark.IsTerminal(os.Stdin.Fd()) { fmt.Fprintf(os.Stderr, "Requested live capture is %v (\"stdin\") but stdin is a tty.\n", psrc.Name()) fmt.Fprintf(os.Stderr, "Perhaps you intended to pipe packet input to termshark?\n") return 1 } if runtime.GOOS != "windows" { psrcs[pi] = pcap.PipeSource{Descriptor: "/dev/fd/0", Fd: int(os.Stdin.Fd())} haveStdin = true } else { fmt.Fprintf(os.Stderr, "Sorry, termshark does not yet support piped input on Windows.\n") return 1 } default: stat, err := os.Stat(psrc.Name()) if err != nil { if psrc.IsFile() || psrc.IsFifo() { // Means this was supplied with -r - since any file sources means there's (a) 1 and (b) // no other sources. So it must stat. Note if we started with -i fifo, this check // isn't done... but it still ought to exist. fmt.Fprintf(os.Stderr, "Error reading file %s: %v.\n", psrc.Name(), err) return 1 } continue } if stat.Mode()&os.ModeNamedPipe != 0 { // If termshark was invoked with -r myfifo, switch to -i myfifo, which tshark uses. This // also puts termshark in "interface" mode where it assumes the source is unbounded // (e.g. a different spinner) psrcs[pi] = pcap.FifoSource{Filename: psrc.Name()} } else { if pcapffile, err := os.Open(psrc.Name()); err != nil { // Do this up front before the UI starts to catch simple errors quickly - like // the file not being readable. It's possible that tshark would be able to read // it and the termshark user not, but unlikely. fmt.Fprintf(os.Stderr, "Error reading file %s: %v.\n", psrc.Name(), err) return 1 } else { pcapffile.Close() } } } } // Means files fileSrcs := pcap.FileSystemSources(psrcs) if len(fileSrcs) == 1 { if len(psrcs) > 1 { fmt.Fprintf(os.Stderr, "You can't specify both a pcap and a live capture.\n") return 1 } } else if len(fileSrcs) > 1 { fmt.Fprintf(os.Stderr, "You can't specify more than one pcap.\n") return 1 } // Invariant: len(psrcs) > 0 // Invariant: len(fileSrcs) == 1 => len(psrcs) == 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 // Meaning there are only live captures if len(fileSrcs) == 0 && 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 // Validate supplied filters e.g. no capture filter when reading from file if len(fileSrcs) > 0 { 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 } } // Here we now have an accurate view of all psrcs - either file, fifo, pipe or interface // Helpful to use logging when enumerating interfaces below, so do it first if !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) } debug := false if (opts.Debug.Set && opts.Debug.Val == true) || (!opts.Debug.Set && termshark.ConfBool("main.debug", false)) { debug = true } if debug { for _, addr := range termshark.LocalIPs() { log.Infof("Starting debug web server at http://%s:6060/debug/pprof/", addr) } go func() { log.Println(http.ListenAndServe("0.0.0.0:6060", nil)) }() } for _, dir := range []string{termshark.CacheDir(), termshark.PcapDir()} { if _, err = os.Stat(dir); os.IsNotExist(err) { err = os.Mkdir(dir, 0777) if err != nil { fmt.Fprintf(os.Stderr, "Unexpected error making dir %s: %v", dir, err) return 1 } } } // Write this pcap out here because the color validation code later depends on empty.pcap 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 } } tsharkBin, kverr := termshark.TSharkPath() if kverr != nil { fmt.Fprintf(os.Stderr, kverr.KeyVals["msg"].(string)) return 1 } // Here, tsharkBin is a fully-qualified tshark binary that exists on the fs (absent race // conditions...) valids := termshark.ConfStrings("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.LT(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) termshark.SetConf("main.validated-tsharks", valids) } // If the last tshark we used isn't the same as the current one, then remove the cached fields // data structure so it can be regenerated. if tsharkBin != termshark.ConfString("main.last-used-tshark", "") { termshark.DeleteCachedFields() } // Write out the last-used tshark path. We do this to make the above fields cache be consistent // with the tshark binary we're using. termshark.SetConf("main.last-used-tshark", tsharkBin) // Determine if the current binary supports color. Tshark will fail with an error if it's too old // and you supply the --color flag. Assume true, and check if our current binary is not in the // validate list. ui.PacketColorsSupported = true colorTsharks := termshark.ConfStrings("main.color-tsharks") if !termshark.StringInSlice(tsharkBin, colorTsharks) { ui.PacketColorsSupported, err = termshark.TSharkSupportsColor(tsharkBin) if err != nil { ui.PacketColorsSupported = false } else { colorTsharks = append(colorTsharks, tsharkBin) termshark.SetConf("main.color-tsharks", colorTsharks) } } // If any of opts.Ifaces 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. var systemInterfaces map[int][]string // See if the interface argument is an integer for pi, psrc := range psrcs { checkInterfaceName := false ifaceIdx := -1 if psrc.IsInterface() { if i, err := strconv.Atoi(psrc.Name()); err == nil { ifaceIdx = i } // If it's a fifo, then always treat is as a fifo and not a reference to something in tshark -D if ifaceIdx != -1 { // if the argument is an integer, then confirm it in the output of tshark -D checkInterfaceName = true } else if runtime.GOOS == "windows" { // If we're on windows, then all interfaces - indices and names - // will be in tshark -D, so confirm it there checkInterfaceName = true } } if checkInterfaceName { if systemInterfaces == nil { systemInterfaces, err = termshark.Interfaces() if err != nil { fmt.Fprintf(os.Stderr, "Could not enumerate network interfaces: %v\n", err) return 1 } } gotit := false var canonicalName string iLoop: for n, i := range systemInterfaces { // (7, ["NDIS_...", "Local Area..."]) if n == ifaceIdx { gotit = true canonicalName = i[0] break } else { for _, iname := range i { if iname == psrc.Name() { gotit = true canonicalName = i[0] break iLoop } } } } if gotit { // Guaranteed that psrc.IsInterface() is true // Use the canonical name e.g. "NDIS_...". Then the temporary filename will // have a more meaningful name. psrcs[pi] = pcap.InterfaceSource{Iface: canonicalName} } else { fmt.Fprintf(os.Stderr, "Could not find network interface %s\n", psrc.Name()) 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() //====================================================================== // If != "", then the name of the file to which packets are saved when read from an // interface source. We can't just use the loader because the user might clear then load // a recent pcap on top of the originally loaded packets. ifacePcapFilename := "" defer func() { // if useIface != "" then we run dumpcap with the -i option - which // means the packet source is either an interface, a pipe, or a // 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) } }() //====================================================================== 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.Fprintf(os.Stderr, "Cannot capture on device %s", pcap.SourcesString(psrcs)) if ifaceErr != nil { fmt.Fprintf(os.Stderr, ": %v", ifaceErr) } fmt.Fprintf(os.Stderr, " (exit code %d)\n", ifaceExitCode) if runtime.GOOS == "linux" && os.Geteuid() != 0 { fmt.Fprintf(os.Stderr, "You might need: sudo setcap cap_net_raw,cap_net_admin+eip %s\n", termshark.PrivilegedBin()) fmt.Fprintf(os.Stderr, "Or try running with sudo or as root.\n") } fmt.Fprintf(os.Stderr, "See https://termshark.io/no-root for more info.\n") } }() // Initialize application state for dark mode and auto-scroll ui.DarkMode = termshark.ConfBool("main.dark-mode", false) ui.AutoScroll = termshark.ConfBool("main.auto-scroll", true) ui.PacketColors = termshark.ConfBool("main.packet-colors", true) // 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{}) if opts.TimestampFormat != "" { psmlArgs = append(psmlArgs, "-t", opts.TimestampFormat) } tsharkArgs := termshark.ConfStringSlice("main.tshark-args", []string{}) if ui.PacketColors && !ui.PacketColorsSupported { log.Warnf("Packet coloring is enabled, but %s does not support --color", tsharkBin) ui.PacketColors = false } cacheSize := termshark.ConfInt("main.pcap-cache-size", 64) bundleSize := termshark.ConfInt("main.pcap-bundle-size", 1000) if bundleSize <= 0 { maxBundleSize := 100000 log.Infof("Config specifies pcap-bundle-size as %d - setting to max (%d)", bundleSize, maxBundleSize) bundleSize = maxBundleSize } var ifaceTmpFile string if len(pcap.FileSystemSources(psrcs)) == 0 { srcNames := make([]string, 0, len(psrcs)) for _, psrc := range psrcs { srcNames = append(srcNames, psrc.Name()) } ifaceTmpFile = pcap.TempPcapFile(srcNames...) fmt.Printf("(The termshark UI will start when packets are detected...)\n") } else { // Start UI right away, reading from a file close(ui.StartUIChan) } // Need to figure out possible changes to COLORTERM before creating the // tcell screen. Note that even though apprunner.Start() below will create // a new screen, it will use a terminfo that it constructed the first time // we call NewApp(), because tcell stores these in a global map. So if the // first terminfo is created in an environment with COLORTERM=truecolor, // the terminfo Go struct is extended with codes that emit truecolor-compatible // ansi codes for colors. Then if I later create a new screen without COLORTERM, // tcell will still use the extended terminfo struct and emit truecolor-codes // anyway. // // If you are using base16-shell, the lowest colors 0-21 in the 256 color space // will be remapped to whatever colors the terminal base16 theme sets up. If you // are using a termshark theme that expresses colors in RGB style (#7799AA), and // termshark is running in a 256-color terminal, then termshark will find the closest // match for the RGB color in the 256 color-space. But termshark assumes that colors // 0-21 are set up normally, and not remapped. If the closest match is one of those // colors, then the theme won't look as expected. A workaround is to tell // gowid not to use colors 0-21 when finding the closest match. if termshark.ConfKeyExists("main.ignore-base16-colors") { gowid.IgnoreBase16 = termshark.ConfBool("main.ignore-base16-colors", false) } else { // Try to auto-detect whether or not base16-shell is installed and in-use gowid.IgnoreBase16 = (os.Getenv("BASE16_SHELL") != "") } if gowid.IgnoreBase16 { log.Infof("Will not consider colors 0-21 from the terminal 256-color-space when interpolating theme colors") // If main.respect-colorterm=true then termshark will leave COLORTERM set and use // 24-bit color if possible. The problem with this, in the presence of base16, is that // some terminal-emulators - e.g. gnome-terminal - still seems to map RGB ANSI codes // colors that are set at values 0-21 in the 256-color space. I'm not sure if this is // just an implementation snafu, or if something else is going on... In any case, // termshark will fall back to 256-colors if base16 is detected because I can // programmatically avoid choosing colors 0-21 for anything termshark needs. if os.Getenv("COLORTERM") != "" && !termshark.ConfBool("main.respect-colorterm", false) { log.Infof("Pessimistically disabling 24-bit color to avoid conflicts with base16") os.Unsetenv("COLORTERM") } } // Do this before ui.Build. If ui.Build fails (e.g. bad TERM), then the filter will be left // running, so we need the defer to be in effect here and not after the processing of ui.Build's // error defer func() { if ui.FilterWidget != nil { ui.FilterWidget.Close() } }() var app *gowid.App if app, err = ui.Build(); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) // Tcell returns ExitError now because if its internal terminfo DB does not have // a matching entry, it tries to build one with infocmp. if _, ok := termshark.RootCause(err).(*exec.ExitError); ok { fmt.Fprintf(os.Stderr, "Termshark could not recognize your terminal. Try changing $TERM.\n") } return 1 } appRunner := app.Runner() pcap.PcapCmds = pcap.MakeCommands(opts.DecodeAs, tsharkArgs, pdmlArgs, psmlArgs, ui.PacketColors) pcap.PcapOpts = pcap.Options{ CacheSize: cacheSize, PacketsPerLoad: bundleSize, } // This is a global. The type supports swapping out the real loader by embedding it via // pointer, but I assume this only happens in the main goroutine. ui.Loader = &pcap.PacketLoader{ParentLoader: pcap.NewPcapLoader(pcap.PcapCmds, &pcap.Runner{app}, pcap.PcapOpts)} // Populate the filter widget initially - runs asynchronously go ui.FilterWidget.UpdateCompletions(app) ui.Running = false validator := filter.Validator{ Invalid: &filter.ValidateCB{ App: app, Fn: func(app gowid.IApp) { if !ui.Running { fmt.Fprintf(os.Stderr, "Invalid filter: %s\n", displayFilter) ui.RequestQuit() } else { app.Run(gowid.RunFunction(func(app gowid.IApp) { ui.OpenError(fmt.Sprintf("Invalid filter: %s", displayFilter), app) })) } }, }, } // Refresh fileSrcs = pcap.FileSystemSources(psrcs) if len(fileSrcs) > 0 { psrc := fileSrcs[0] absfile, err := filepath.Abs(psrc.Name()) if err != nil { fmt.Fprintf(os.Stderr, "Could not determine working directory: %v\n", err) return 1 } doit := func(app gowid.IApp) { app.Run(gowid.RunFunction(func(app gowid.IApp) { ui.FilterWidget.SetValue(displayFilter, app) })) ui.RequestLoadPcapWithCheck(absfile, displayFilter, ui.NoGlobalJump, app) } validator.Valid = &filter.ValidateCB{Fn: doit, App: app} validator.Validate(displayFilter) } else { // Verifies whether or not we will be able to read from the interface (hopefully) ifaceExitCode = 0 for _, psrc := range psrcs { if psrc.IsInterface() { if ifaceExitCode, ifaceErr = termshark.RunForExitCode( termshark.CaptureBin(), []string{"-i", psrc.Name(), "-a", "duration:1"}, append(os.Environ(), "TERMSHARK_CAPTURE_MODE=1"), ); ifaceExitCode != 0 { return 1 } } else { // We only test one - the assumption is that if dumpcap can read from eth0, it can also read from eth1, ... And // this lets termshark start up more quickly. break } } ifValid := func(app gowid.IApp) { app.Run(gowid.RunFunction(func(app gowid.IApp) { ui.FilterWidget.SetValue(displayFilter, app) })) ifacePcapFilename = ifaceTmpFile ui.RequestLoadInterfaces(psrcs, captureFilter, displayFilter, ifaceTmpFile, app) } validator.Valid = &filter.ValidateCB{Fn: ifValid, App: app} validator.Validate(displayFilter) } quitIssuedToApp := false wasLoadingPdmlLastTime := ui.Loader.PdmlLoader.IsLoading() wasLoadingAnythingLastTime := ui.Loader.LoadingAnything() // Keep track of this across runs of the main loop so we don't go backwards (because // that looks wrong to the user) var prevProgPercentage float64 progTicker := time.NewTicker(time.Duration(200) * time.Millisecond) ctrlzLineDisc := tty.TerminalSignals{} // This is used to stop iface load and any stream reassembly. Make sure to // avoid any stream reassembly errors, since this is a controlled shutdown // but the tshark processes reading data for stream reassembly may still // complain about interruptions stopLoaders := func() { if ui.StreamLoader != nil { ui.StreamLoader.SuppressErrors = true } ui.Loader.CloseMain() } inactiveDuration := 30 * time.Second inactivityTimer := time.NewTimer(inactiveDuration) var progCancelTimer *time.Timer Loop: for { var finChan <-chan time.Time var tickChan <-chan time.Time var inactivityChan <-chan time.Time var tcellEvents <-chan tcell.Event var opsChan <-chan gowid.RunFunction var afterRenderEvents <-chan gowid.IAfterRenderEvent // For setting struct views empty. This isn't done as soon as a load is initiated because // in the case we are loading from an interface and following new packets, we get an ugly // blinking effect where the loading message is displayed, shortly followed by the struct or // hex view which comes back from the pdml process (because the pdml process can only read // up to the end of the currently seen packets, each time it has to start afresh from the // beginning to get new packets). Waiting 500ms to display loading gives enough time, in // practice, // On change of state - check for new pdml requests if ui.Loader.PdmlLoader.IsLoading() != wasLoadingPdmlLastTime { ui.CacheRequestsChan <- struct{}{} } // This should really be moved to a handler... if !ui.Loader.LoadingAnything() { if wasLoadingAnythingLastTime { // If the state has just switched to 0, it means no interface-reading process is // running. That means we will no longer be reading from an interface or a fifo, so // we point the loader at the file we wrote to the cache, and redirect all // loads/filters to that now. ui.Loader.TurnOffPipe() app.Run(gowid.RunFunction(func(app gowid.IApp) { ui.ClearProgressWidget(app) ui.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) prevProgPercentage = 0.0 } // EnableOpsVar will be enabled when all the handlers have run, which happen in the main goroutine. // I need them to run because the loader channel is closed in one, and the ticker goroutines // don't terminate until these goroutines stop if ui.QuitRequested { if ui.Running { if !quitIssuedToApp { app.Quit() quitIssuedToApp = true // Avoid closing app twice - doubly-closed channel } } else { // No UI so exit loop immediately break Loop } } } // Only display the progress bar if PSML is loading or if PDML is loading that is needed // by the UI. If the PDML is an optimistic load out of the display, then no need for // progress. if ui.Loader.PsmlLoader.IsLoading() || (ui.Loader.PdmlLoader.IsLoading() && ui.Loader.PdmlLoader.LoadIsVisible()) { tickChan = progTicker.C // progress is only enabled when a pcap may be loading } else { // Reset for the next load prevProgPercentage = 0.0 } if ui.Loader.InterfaceLoader.IsLoading() { inactivityChan = inactivityTimer.C } // Only process tcell and gowid events if the UI is running. if ui.Running { tcellEvents = app.TCellEvents } if ui.Fin != nil && ui.Fin.Active() { finChan = ui.Fin.C() } // For operations like ClearPcap - need previous loads to be fully finished first. The operations // channel is enabled until an operation starts, then disabled until the operation re-enables it // via a handler. // // Make sure state doesn't change until all handlers have been run if !ui.Loader.PdmlLoader.IsLoading() && !ui.Loader.PsmlLoader.IsLoading() { opsChan = pcap.OpsChan } afterRenderEvents = app.AfterRenderEvents wasLoadingPdmlLastTime = ui.Loader.PdmlLoader.IsLoading() wasLoadingAnythingLastTime = ui.Loader.LoadingAnything() select { case <-inactivityChan: ui.Fin.Activate() app.Redraw() case <-finChan: ui.Fin.Advance() app.Redraw() case <-ui.StartUIChan: log.Infof("Launching termshark UI") // Go to termshark UI view if err = app.ActivateScreen(); err != nil { fmt.Fprintf(os.Stderr, "Error starting UI: %v\n", err) return 1 } // Need to do that here because the app won't know how many colors the screen // has (and therefore which variant of the theme to load) until the screen is // activated. mode := theme.Mode(app.GetColorMode()).String() // more concise themeName := termshark.ConfString(fmt.Sprintf("main.theme-%s", mode), "default") loaded := false if themeName != "" { err = theme.Load(themeName, app) if err != nil { log.Warnf("Theme %s could not be loaded: %v", themeName, err) } else { loaded = true } } if !loaded && themeName != "default" { err = theme.Load("default", app) if err != nil { log.Warnf("Theme %s could not be loaded: %v", themeName, err) } } // This needs to run after the toml config file is loaded. ui.SetupColors() // Start tcell/gowid events for keys, etc appRunner.Start() // Reinstate our terminal overrides that allow ctrl-z if err := ctrlzLineDisc.Set(); err != nil { ui.OpenError(fmt.Sprintf("Unexpected error setting Ctrl-z handler: %v\n", err), app) } ui.Running = true startedSuccessfully = true ui.StartUIChan = nil // make sure it's not triggered again defer func() { // 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 app.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 stopLoaders() appRunner.Stop() app.Close() ui.Running = false }() case fn := <-opsChan: app.Run(fn) case <-ui.QuitRequestedChan: ui.QuitRequested = true // Without this, a quit during a pcap load won't happen until the load is finished if ui.Loader.LoadingAnything() { // We know we're not idle, so stop any load so the quit op happens quickly for the user. Quit // will happen next time round because the quitRequested flag is checked. stopLoaders() } case sig := <-sigChan: if system.IsSigTSTP(sig) { if ui.Running { // Remove our terminal overrides that allow ctrl-z ctrlzLineDisc.Restore() // Stop tcell/gowid events for keys, etc appRunner.Stop() // Go back to terminal view app.DeactivateScreen() ui.Running = false uiSuspended = true } else { log.Infof("UI not active - no terminal changes required.") } // This is not synchronous, but some time after calling this, we'll be suspended. if err := system.StopMyself(); err != nil { fmt.Fprintf(os.Stderr, "Unexpected error issuing SIGSTOP: %v\n", err) return 1 } } else if system.IsSigCont(sig) { if uiSuspended { // Go to termshark UI view if err = app.ActivateScreen(); err != nil { fmt.Fprintf(os.Stderr, "Error starting UI: %v\n", err) return 1 } // Start tcell/gowid events for keys, etc appRunner.Start() // Reinstate our terminal overrides that allow ctrl-z if err := ctrlzLineDisc.Set(); err != nil { ui.OpenError(fmt.Sprintf("Unexpected error setting Ctrl-z handler: %v\n", err), app) } ui.Running = true uiSuspended = false } } else if system.IsSigUSR1(sig) { if debug { termshark.ProfileCPUFor(20) } else { log.Infof("SIGUSR1 ignored by termshark - see the --debug flag") } } else if system.IsSigUSR2(sig) { if debug { termshark.ProfileHeap() } else { log.Infof("SIGUSR2 ignored by termshark - see the --debug flag") } } else { log.Infof("Starting termination via signal %v", sig) ui.RequestQuit() } case <-ui.CacheRequestsChan: ui.CacheRequests = pcap.ProcessPdmlRequests(ui.CacheRequests, ui.Loader.ParentLoader, ui.Loader.PdmlLoader, ui.SetStructWidgets{ui.Loader}, app) case <-tickChan: // We already know that we are LoadingPdml|LoadingPsml ui.SetProgressWidget(app) if progCancelTimer != nil { progCancelTimer.Reset(time.Duration(500) * time.Millisecond) } else { progCancelTimer = time.AfterFunc(time.Duration(500)*time.Millisecond, func() { app.Run(gowid.RunFunction(func(app gowid.IApp) { ui.ClearProgressWidget(app) })) }) } // Rule: // - prefer progress if we can apply it to psml only (not pdml) // - otherwise use a spinner if interface load or fifo load in operation // - otherwise use progress for pdml doprog := false if system.HaveFdinfo { // Prefer progress, if the OS supports it. doprog = true if ui.Loader.ReadingFromFifo() { // But if we are have an interface load (or a pipe load), then we can't // predict when the data will run out, so use a spinner. That's because we // feed the data to tshark -T psml with a tail command which reads from // the tmp file being created by the pipe/interface source. doprog = false if !ui.Loader.InterfaceLoader.IsLoading() && !ui.Loader.PsmlLoader.IsLoading() { // Unless those loads are finished, and the only loading activity is now // PDML/pcap, which is loaded on demand in blocks of 1000. Then we can // use the progress bar. doprog = true } } } if doprog { app.Run(gowid.RunFunction(func(app gowid.IApp) { prevProgPercentage = ui.UpdateProgressBarForFile(ui.Loader, prevProgPercentage, app) })) } else { app.Run(gowid.RunFunction(func(app gowid.IApp) { ui.UpdateProgressBarForInterface(ui.Loader.InterfaceLoader, app) })) } case ev := <-tcellEvents: app.HandleTCellEvent(ev, gowid.IgnoreUnhandledInput) inactivityTimer.Reset(inactiveDuration) case ev, ok := <-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(): // Re-read so changes that can take effect immediately do so if err := viper.ReadInConfig(); err != nil { log.Warnf("Unexpected error re-reading toml config: %v", err) } ui.UpdateRecentMenu(app) } } return 0 } //====================================================================== // Local Variables: // mode: Go // fill-column: 78 // End: termshark-2.2.0/configs/000077500000000000000000000000001377442047300151565ustar00rootroot00000000000000termshark-2.2.0/configs/termshark-dd01307f2423.json.enc000066400000000000000000000044001377442047300222540ustar00rootroot00000000000000c*ƄM3 A4* NeX*NӺO}t(y-hG=-M7vő 0d_)Lڑ jőzBd^rS+Y8T"ħ/A{wQUC_D5͜6S١A pj 6H T~@E+ZOcNN~{G1VW:}@Ǘ)jyW[a JNeX5|xej:g`#MZpѧY c#Y 3{RbiddE`Z$i9F]'A&U]*h#_IU"q;A"<@dNكeQV.`MZ:oђKPEv۞VAvEk:xV04)uc<%\kb]$@ #MuDW)O`s@Á6Em}*MP=1@%6įTZbKѣ^p?~}+c>QmXWA\rWPd ֙c>+k hBu2)-I7ֻ'%+EQ:ڮŦ eDz{ E)gH0oSZYJ_ﹷWoPW>\m+:ZǪi xaZ\Ɣ'PD=79PnNBH|~iBd|@"$e-d'Bf5;(lrE!II% s }㏆JRs# TsW\(誡7UWY%~Yj xrK `a],AWʯNsm'XUOIB͊2JeOZCiX"$k}/n5zI<mRp[ k2M⒣$Bc#)'+ :~+Wbm?CG$I9E+vc+p&#K-:F;s7 )tg 6%|^wӪK7G/Eb$NfdjD_'-l9./w"jߜ+Xc=cp=HQQ ݁;|PFd7!:bw]i۟H,,|uA~Tt)vhnYM7Ӂmk="~t3L?8E JXnk ra)7sAl]K9WsVfJoeX\")hU{4M:;}~=B RSftnm6U%{B`nۆyFTOdA5uKOr,C)W$|Xbdy39qD<X.IN5KyCBM[) &De~ O 134_܍P}=ىKTP\U֒*,9qkfYHZ a)+R.eIézY0  NQ :P*Ȟma  Ńtfz#,C  sdp7vskOv^![sªǝ  V7t_78P aw#KbTxW:oY# ^3O`^)}UHN9VËH_GY~r6rH:5f'y""6(I"g(_غU| zgq?Tkc7 [B}WVץ.ssRy^(a/듺hGV,U \\}n4ħZF_"g{|D#ΣLTxTj* /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 is too bright! Termshark v2 supports dark-mode! Hit Esc to bring up the main menu then "Toggle Dark Mode". See the [User Guide](UserGuide.md#dark-mode). ## Termshark's colors are wrong! See [this answer](#what-settings-affect-termsharks-colors). If termshark's background is a strange color like dark blue or orange, maybe a tool like base16-shell has remapped some of the colors in the 256-color-space, but termshark is unaware of this. Try setting this in `termshark.toml`: ```toml [main] ignore-base16-colors = true ``` ## What settings affect termshark's colors? Unfortunately there are several :-/ First of all, your terminal emulator's `TERM` variable determines the range of colors available to termshark e.g. `xterm-16color`, `xterm-256color`. If you also have `COLORTERM=truecolor` set, and the terminal emulator has support, 24-bit color will be available. Termshark will emit these 24-bit ANSI color codes and color should be faithfully reproduced. You can override the value of `TERM` with termshark's `main.term` setting in `termshark.toml` e.g. ```toml [main] term = "screen-256color" ``` When termshark runs, it will load your selected theme if it's available in the terminal's color mode. If not, it will choose the built-in `default` theme which is available in every mode. If you run in truecolor mode, and your chosen theme is only defined for 256-colors, termshark will load the 256-color theme. Termshark will load its theme from `~/.config/termshark/themes/` if it can find it, otherwise it will look in its built-in database. Termshark has themes called `default`, `dracula`, `solarized` and `base16` built-in. If you are using [base16-shell](https://github.com/chriskempson/base16-shell), then you might have colors 0-21 of your terminal's 256-color-space remapped. If you are running in 256-color mode, and your theme specifies RGB colors, termshark will choose the closest match among those in the 256-color-space. Termshark will ignore colors 0-21 as match candidates if `BASE16_SHELL` is set in the environment. It will also ignore these colors if you set `main.ignore-base16-colors` in `termshark.toml`. Otherwise, termshark will assume colors 0-21 are displayed "normally", and may pick these remapped colors as the closest match to a theme's color - resulting in incorrect colors. ## How do I rebuild termshark? If you don't have the source, clone it like this: ```bash $ git clone https://github.com/gcla/termshark ``` You'll get best results with the latest version of Golang - 1.15 as I write this - but anything >= 1.12 will work. Set your environment: ```bash $ export GO111MODULE=on ``` Change to the termshark dir and type ```bash $ go generate ./... $ go install ./... ``` The generate step is only necessary if you have changed any files under `termshark/assets/themes/`. If not, just run ```bash $ go install ./... ``` Termshark will be installed as `~/go/bin/termshark`. ## Where are the config and log files? You can find the config file, `termshark.toml`, in: - `${XDG_CONFIG_HOME}/termshark/` `(${HOME}/.config/termshark/)` on Linux - `${HOME}/Library/Application Support/termshark/` on macOS - `%APPDATA%\termshark\` `(C:\Users\\AppData\Roaming\termshark\)` on Windows You can find the log file, `termshark.log`, in: - `${XDG_CACHE_HOME}/termshark/` `(${HOME}/.cache//termshark/)` on Linux - `${HOME}/Library/Caches/termshark/` on macOS - `%LOCALAPPDATA%\termshark\` `(C:\Users\\AppData\Local\termshark\)` on Windows ## The console is too narrow on Windows Unfortunately, the standard console window won't let you increase its size beyond its initial bounds using the mouse. To work around this, after termshark starts, right-click on the window title and select "Properties". Click "Layout" and then adjust the "Window Size" settings. When you quit termshark, your console window will be restored to its original size. ![winconsole](/../gh-pages/images/winconsole.png?raw=true) ## Can I pass extra arguments to tshark? Yes, via `~/.config/termshark/termshark.toml`. Here is an example I use: ```toml [main] tshark-args = ["-d","udp.port==2075,cflow","-d","udp.port==9191,cflow","-d","udp.port==2055,cflow","-d","udp.port==2095,cflow"] ``` ## 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_CACHE_HOME/termshark/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. If the user selects the "Analysis -> Reassemble stream" menu option, termshark starts two more tshark processes to gather the data to display. First, tshark is invoked with the '-z' option to generate the reassembled stream information. Termshark knows the protocol and stream index to supply to tshark because it saves this information when processing the PDML to populate the packet structure view: ```bash tshark -r my.pcap -q -z follow,tcp,raw,15 ``` This means "follow TCP stream number 15". The output will look something like: ```console =================================================================== Follow: tcp,raw Filter: tcp.stream eq 15 Node 0: 192.168.0.114:1137 Node 1: 192.168.0.193:21 3232302043687269732053616e6465727320465450205365727665720d0a 55534552206373616e646572730d0a 3333312050617373776f726420726571756972656420666f72206373616e646572732e0d0a ... ``` A second tshark process is started concurrently: ```bash tshark -T pdml -r my.pcap -Y "tcp.stream eq 15" ``` The output of that is parsed to build an array mapping the index of each "chunk" of the stream (e.g. above, 0 is "3232", 1 is "5553", 2 is "3333") to the index of the corresponding packet. This is not always x->x because the stream payloads for some packets are of zero length and are not represented in the output from the first tshark '-z' process. The mapping is used when the user clicks on a chunk of the reassembled stream in the UI - termshark will then change focus behind the scenes to the corresponding packet in the packet list view. If you exit the stream reassembly UI, you can see the newly selected packet. When termshark starts these stream reassembly processes, it also sets a display filter in the main UI e.g. "tcp.stream eq 15". This causes termshark to invoke the PSML and PDML processes again - in addition to the two stream-reassembly-specific processes that I've just described. If the user selects "Analysis -> Conversations" from the menu, termshark starts a tshark process to gather this information. If the configured conversation types are `eth`, `ip`, `tcp`, then the invocation will look like: ```bash tshark -r my.pcap -q -z conv,eth -z conv,ip -z conv,tcp ``` The information is displayed in a table by conversation type. If the user has a display filter active - e.g. `http` - and hits the "Limit to filter" checkbox, then tshark will be invoked like this: ```bash tshark -r my.pcap -q -z conv,eth,http -z conv,ip,http -z conv,tcp,http ``` 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 tshark -G fields ``` then parsing the output into a nested collection of Go maps, and serializing it to `$XDG_CACHE_HOME/termshark/tsharkfieldsv2.gob.gz`. Termshark also uses the `capinfos` binary to compute the information displayed via the menu "Analysis -> Capture file properties". `capinfos` is typically distributed with tshark. ## How can I make termshark run without root? Termshark depends on tshark, and termshark will run without root if tshark/dumpcap will. On Linux, these are the most common ways to allow tshark to run as a non-root user - For Ubuntu/Debian systems, you can add your user to the `wireshark` group. These instructions are taken [from this answer](https://osqa-ask.wireshark.org/questions/7976/wireshark-setup-linux-for-nonroot-user/51058) on [wireshark.org](https://ask.wireshark.org/questions/): ```bash sudo apt-get install wireshark sudo dpkg-reconfigure wireshark-common sudo usermod -a -G wireshark $USER newgrp wireshark ``` If you logout and login again after `usermod`, you can omit the `newgrp` command. - You might need to set the capabilities of `dumpcap` using a command like this: ```bash sudo setcap cap_net_raw,cap_net_admin+eip /usr/sbin/dumpcap ``` You can find more detail at https://wiki.wireshark.org/CaptureSetup/CapturePrivileges. ## Why do is termshark generating traffic on port 5037? See [this issue](https://github.com/gcla/termshark/issues/98). TL;DR - try deleting `/usr/lib/wireshark/extcap/androiddump`. ## How can termshark capture from extcap interfaces with dumpcap? Termshark doesn't always capture using dumpcap. It will try to use dumpcap if possible, because testing (from @pocc) indicated that it is less likely to drop packets - presumably because dumpcap's job is limited to generating a pcap with little interpretation of data. However, dumpcap doesn't support extcap interfaces like `randpkt`. If termshark detects that the live capture device is an extcap interface, it will use tshark as the capture binary instead. It does this automatically by using `termshark` itself as the default `capture-command`, and to make this work, termshark now runs the capture command with the environment variable `TERMSHARK_CAPTURE_MODE` set. dumpcap and tshark will ignore that, but termshark will detect it at startup and switch immediately to capture mode. It then runs this, in pseudo-code form`: ```go cmd := exec.Command(dumpcap, args...) if cmd.Run() != nil { syscall.Exec(tshark, append([]string{tshark}, args...), os.Environ()) } ``` This trick is only implemented for Unix OSes. On Windows, termshark will use dumpcap. If you need to read extcap interfaces on Windows, you can set `capture-command` to `tshark` in the toml config file. ## Termshark is laggy or using a lot of RAM I hope this is much-improved with v2. If you still experience problems, try running termshark with the `--debug` flag e.g. ```bash termshark --debug -r foo.pcap ``` You can then generate a CPU profile with ```bash pkill -SIGUSR1 termshark ``` or a heap/memory profile with ```bash pkill -SIGUSR2 termshark ``` The profiles are stored under `$XDG_CACHE_HOME/termshark` (e.g. ~/.cache/termshark/). You can investigate with `go tool pprof` like this: ```bash go tool pprof -http=:6061 $(which termshark) ~/.cache/termshark/mem-20190929122218.prof ``` and then navigate to http://127.0.0.1:6061/ui/ (or remote IP) - or open a termshark issue and upload the profile for us to check :-) There will also be a debug web server running at http://127.0.0.1:6060/debug/pprof (or rmote IP) from where you can see running goroutines and other information. ## How much memory does termshark use? It's hard to be precise, but I can provide some rough numbers. Termshark uses memory for two things: - for each packet in the whole pcap, a subsection of the PSML (XML) for that packet - in groups of 1000 (by default), loaded on demand, a subsection of the PDML (XML) for each packet in the group. See [this question](FAQ.md#if-i-load-a-big-pcap-termshark-doesnt-load-all-the-packets-at-once---why) for more information on the on-demand loading. Using a sequence of pcaps with respectively 100000, 200000, 300000 and 400000 packets, I can see termshark v2 (on linux) adds about 100 MB of VM space and about 50MB of RSS (resident set size) per 100000 packets - with only PSML loaded. As you scroll through the pcap, each 1000 packet boundary causes a load of 1000 PDML elements from tshark. Each extra 1000 packets increases RSS by about 30MB. This is about an 80% improvement over termshark v1 - accomplished by simply compressing the serialized representation in RAM. ## What is the oldest supported version of tshark? As much as possible, I want termshark to work "right out of the box", and to me that meant not requiring the user to have to update tshark. On Linux I have successfully tested termshark with tshark versions back to git tag v1.11.0; but v1.10.0 failed to display the hex view. I didn't debug further. So v1.11.0 is the oldest supported version of tshark. Wireshark v1.11.0 was released in October 2013. ## What's next? Termshark v2 implemented stream reassembly, a "What's next" feature from v1. For Termshark v3, some possibilities are: - Expose more of tshark's `-z` options (there are many more) - Better navigation of the UI with the keyboard e.g. VIM-style commands! - 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 termshark-2.2.0/docs/Maintainer.md000066400000000000000000000056261377442047300171000ustar00rootroot00000000000000# How to Package Termshark for Release ## Termux (Android) I've been building use the termux docker builder. ```bash docker pull termux/package-builder ``` Clone the `termux-packages` and `termux-root-packages` repos: ```bash cd source/ git clone https://github.com/termux/termux-packages cd termux-packages git clone https://github.com/termux/termux-root-packages ``` Open `termux-packages/termux-root-packages/packages/termshark/build.sh` in an editor. Change ```bash cd $TERMUX_PKG_BUILDDIR go get -d -v github.com/gcla/termshark/v2/cmd/termshark@e185fa59d87c06fe1bafb83ce6dc15591434ccc8 go install github.com/gcla/termshark/v2/cmd/termshark ``` to use the correct uuid - I am using the uuid for v2.0.3 ```bash cd $TERMUX_PKG_BUILDDIR go get -d -v github.com/gcla/termshark/v2/cmd/termshark@73dfd1f6cb8c553eb524ebc27d991f637c1ac5ea go install github.com/gcla/termshark/v2/cmd/termshark ``` Change `TERMUX_PKG_VERSION` too. Save. Start docker and build (from `termux-packages` dir): ```bash gcla@elgin:~/source/termux-packages$ ./scripts/run-docker.sh Running container 'termux-package-builder' from image 'termux/package-builder'... builder@201c39983bf8:~/termux-packages$ rm /data/data/.built-packages/termshark builder@201c39983bf8:~/termux-packages$ ./clean.sh # to rebuild everything! builder@201c39983bf8:~/termux-packages$ ./build-package.sh termux-root-packages/packages/termshark/ ... ``` This will take several minutes. You'll probably see an error like this: ``` Wrong checksum for https://termshark.io: Expected: 36e45dfeb97f89379bda5be6bfe69c46e5c4211674120977e7b0033f5d90321a Actual: c05a64f1e502d406cc149c6e8b92720ad6310aecd1dd206e05713fd8a2247a84 ``` Open `termux-packages/termux-root-packages/packages/termshark/build.sh` again and change `TERMUX_PKG_SHA256`. Rebuild. Submit a PR to `termux-root-packages`. To edit files in use by a docker container, you can use tramp + emacs with a path like this: `/docker:builder@201c39983bf8:/home/builder/termux-packages/termux-root-packages/packages/termshark/build.sh` ## Snapcraft Fork Mario's termshark-snap repository: https://github.com/mharjac/termshark-snap (@mharjac) and clone it to a recentish Linux. Edit `snapcraft.yaml`. Change `version:` and edit this section to use the correct hash - this one corresponds to v2.0.3: ``` go get github.com/gcla/termshark/v2/cmd/termshark@73dfd1f6cb8c553eb524ebc27d991f637c1ac5ea ``` From a shell, type ``` snapcraft ``` If you have prior snapcraft builds of termshark, you might need ``` snapcraft clean ``` first. On my 19.10 machine, I ran into snapcraft failures that resolved when I simply ran `snapcraft` again... When this succeeds, the working directory should have a file `termshark_2.0.3_amd64.snap`. To install this - to test it out - try this: ``` snap install --dangerous ./termshark_2.0.3_amd64.snap ``` then to run it: ``` /snap/bin/termshark -h ``` Check your changes in and submit a PR to Mario (@mharjac). termshark-2.2.0/docs/Packages.md000066400000000000000000000050641377442047300165230ustar00rootroot00000000000000# Install Packages Here's how to install termshark on various OSes and with various package managers. ## Arch Linux - [termshark-bin](https://aur.archlinux.org/packages/termshark-bin): binary package which simply copies the released binary to install directory. Made by [jerry73204](https://github.com/jerry73204) - [termshark-git](https://aur.archlinux.org/packages/termshark-git): Compiles from source, made by [Thann](https://github.com/Thann) ## Debian Termshark is only available in unstable/sid at the moment. ```bash apt update apt install termshark ``` ## FreeBSD Thanks to [Ryan Steinmetz](https://github.com/zi0r) Termshark is in the FreeBSD ports tree! To install the package, run: `pkg install termshark` To build/install the port, run: `cd /usr/ports/net/termshark/ && make install clean` ## Homebrew ```bash brew update brew install termshark ``` ## Kali Linux ```bash apt update apt install termshark ``` ## NixOS Thanks to [Patrick Winter](https://github.com/winpat) ```bash nix-channel --add https://nixos.org/channels/nixpkgs-unstable nix-channel --update nix-env -iA nixpkgs.termshark ``` ## SnapCraft Thanks to [mharjac](https://github.com/mharjac) Termshark can be easily installed on almost all major distros just by issuing: ```bash snap install termshark ``` Note there is a big caveat with Snap and the architecture of Wireshark that prevents termshark being able to read network interfaces. If installed via Snap, termshark will only be able to work with pcap files. See [this explanation](https://forum.snapcraft.io/t/wireshark-and-setcap/9629/6). ## Termux (Android) ```bash pkg install root-repo pkg install termshark ``` Note that termshark does not require a rooted phone to inspect a pcap, but it does depend on tshark which is itself in Termux's root-repo for programs that do work best on a rooted phone. If you would like to use termshark's copy-mode to copy sections of packets to your Android clipboard, you will also need [Termux:API](https://play.google.com/store/apps/details?id=com.termux.api&hl=en_US). Install from the Play Store, then from termux, type: ```bash pkg install termux-api ``` ![device art](/../gh-pages/images/device art.png?raw=true) ## Ubuntu If you are running Ubuntu 19.10 (eoan) or higher, termshark can be installed like this: ```bash sudo apt install termshark ``` For Ubuntu < 19.10, you can use the PPA _nicolais/termshark_ to install termshark: ```bash sudo add-apt-repository --update ppa:nicolais/termshark sudo apt install termshark ``` Thanks to [Nicolai Søberg](https://github.com/NicolaiSoeborg) termshark-2.2.0/docs/UserGuide.md000066400000000000000000001015371377442047300167030ustar00rootroot00000000000000# Termshark User Guide Termshark provides a terminal-based user interface for analyzing packet captures. ## Table of Contents - [Table of Contents](#table-of-contents) - [Basic Usage](#basic-usage) - [Choose a Source](#choose-a-source) - [Reading from an Interface](#reading-from-an-interface) - [Read a pcap file](#read-a-pcap-file) - [Changing Files](#changing-files) - [Reading from a fifo or stdin](#reading-from-a-fifo-or-stdin) - [Using the TUI](#using-the-tui) - [Filtering](#filtering) - [Changing Views](#changing-views) - [Packet List View](#packet-list-view) - [Packet Structure View](#packet-structure-view) - [Packet Hex View](#packet-hex-view) - [Marking Packets](#marking-packets) - [Copy Mode](#copy-mode) - [Packet Capture Information](#packet-capture-information) - [Stream Reassembly](#stream-reassembly) - [Conversations](#conversations) - [Command-Line](#command-line) - [Macros](#macros) - [Configuration](#configuration) - [Dark Mode](#dark-mode) - [Packet Colors](#packet-colors) - [Themes](#themes) - [Config File](#config-file) - [Troubleshooting](#troubleshooting) ## Basic Usage Termshark is inspired by Wireshark, and depends on tshark for all its intelligence. Termshark is run from the command-line. You can see its options with ```console $ termshark -h termshark v2.1.1 A wireshark-inspired terminal user interface for tshark. Analyze network traffic interactively from your terminal. See https://termshark.io for more information. Usage: termshark [FilterOrPcap] Application Options: -i= Interface(s) to read. -r= Pcap file/fifo to read. Use - for stdin. -d===, Specify dissection of layer type. -D Print a list of the interfaces on which termshark can capture. -Y= Apply display filter. -f= Apply capture filter. -t=[a|ad|adoy|d|dd|e|r|u|ud|udoy] Set the format of the packet timestamp printed in summary lines. --tty= Display the UI on this terminal. --pass-thru=[auto|true|false] Run tshark instead (auto => if stdout is not a tty). (default: auto) --log-tty Log to the terminal. -h, --help Show this help message. -v, --version Show version information. Arguments: FilterOrPcap: Filter (capture for iface, display for pcap), or pcap 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 ``` ## Choose a Source ### 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 supports reading from more than one interface at a time: ```bash termshark -i eth0 -i eth1 ``` Once packets are detected, termshark's UI will launch and the packet views will update as packets are read: ![readiface](/../gh-pages/images/readiface.png?raw=true) 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 ``` ### 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](https://wiki.wireshark.org/DisplayFilters). When reading from an interface, the filter is interpreted as a [capture filter](https://wiki.wireshark.org/CaptureFilters). This follows tshark's behavior. Termshark will launch in your terminal. From here, you can press `?` for help: ![tshelp](/../gh-pages/images/tshelp.png?raw=true) #### 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](/../gh-pages/images/recent.png?raw=true) ### Reading from a fifo or stdin Termshark supports reading packets from a Unix fifo or from standard input - for example ```bash tcpdump -i eth0 -w - icmp | termshark ``` On some machines, packet capture commands might require sudo or root access. To facilitate this, termshark's UI will not launch until it detects that it has received some packet data on its input. This makes it easier for the user to type in his or her root password on the tty before termshark takes over: ```console $ sudo tcpdump -i eth0 -w - icmp | termshark (The termshark UI will start when packets are detected...) [sudo] password for gcla: ``` If the termshark UI is active in the terminal but you want to see something displayed there before termshark started, you can now issue a SIGTSTP signal (on Unix) and termshark will suspend itself and give up control of the terminal. In bash, this operation is usually bound to `ctrl-z`. ```console $ termshark -r foo.pcap [1]+ Stopped termshark -r foo.pcap $ ``` Type `fg` to resume termshark. Another option is to launch termshark in its own tty. You could do this using a split screen in tmux. In one pane, type ```bash tty && sleep infinity ``` If the output is e.g. `/dev/pts/10`, then you can launch termshark in the other tmux pane like this: ```bash termshark -r foo.pcap --tty=/dev/pts/10 ``` Issue a sleep in the pane for `/dev/pts/10` so that no other process reads from the terminal while it is dedicated to termshark. ## Using the TUI ### 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](/../gh-pages/images/filterbad.png?raw=true) 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](/../gh-pages/images/filterbad.png?raw=true) ### Changing Views Press `tab` or `ctrl-w ctrl-w` 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](/../gh-pages/images/max.png?raw=true) Press `\` to restore the original layout. Press `|` to move the hex view to the right-hand side: ![altview](/../gh-pages/images/altview.png?raw=true) You can also press `<`,`>`,`+` and `-` to change the relative size of each view. To reset termshark to use its original relative sizes, hit `ctrl-w =`. All termshark views support vim-style navigation with `h`, `j`, `k` and `l` along with regular cursor keys. ### 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](/../gh-pages/images/sortcol.png?raw=true) You can hit `home` or `gg` to jump to the top of the list and `end` or `G` to jump to the bottom. You can jump to a specific packet by entering its number - as a prefix - before hitting `gg` or `G` 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](/../gh-pages/images/ipv6.png?raw=true) ### 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, the 'enter' key, or the right and left cursor keys: ![structure](/../gh-pages/images/structure.png?raw=true) 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. As you move around the bytes, the middle (structure) view will update to show you where you are in the packet's structure. ### Marking Packets To make it easier to compare packets, you can mark a packet in the packet list view and then jump back to it later. Termshark's marks are modeled on vim's. Set a mark by navigating to the packet and then hit `m` followed by a letter - `a` through `z`. ![marks1](/../gh-pages/images/marks1.png?raw=true) To jump back to that mark, hit `'` followed by the letter you selected. To jump back to the packet that was selected prior to your jump, hit `''`. When you exit termshark or load a new pcap, these marks are deleted; but termshark also supports cross-pcap marks which are saved in termshark's config file. To make a cross-pcap mark, hit `m` followed by a capital letter - `A` through `Z`. If you jump to a cross-pcap mark made in another pcap, termshark will load that pcap back up. To display your current marks, use the [command-line](#command-line) `marks` command: ![marks2](/../gh-pages/images/marks2.png?raw=true) ### 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](/../gh-pages/images/copymode1.png?raw=true) 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](/../gh-pages/images/copymode2.png?raw=true) 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](UserGuide.md#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. ### Packet Capture Information To show a summary of the information represented in the current pcap file, go to the "Analysis" menu and choose "Capture file properties". Termshark generates this information using the `capinfos` binary which is distributed with `tshark`. ![capinfos1](/../gh-pages/images/capinfos1.png?raw=true) ### Stream Reassembly Termshark is able to present reassembled TCP and UDP streams in a similar manner to Wireshark. In the packet list view, select a TCP or UDP packet then go to the "Analysis" menu and choose "Reassemble stream": ![streams1](/../gh-pages/images/streams1.png?raw=true) Termshark shows you: - A list of each client and server payload, in order, colored accordingly. - The number of client and server packets, and times the conversation switched sides. - A search box. - A button to display the entire conversation, only the client side, or only the server side. You can type a string in the search box and hit enter - or the Next button - to move through the matches. ![streams2](/../gh-pages/images/streams2.png?raw=true) Select Regex to instead have termshark interpret your search string as a regular expression. Because termshark is written in Golang, the regular expression uses Golang's regex dialect. [regex101](https://regex101.com/) provides a nice online way to experiment with matches. A quick tip - if you want your match to [cross line endings](https://stackoverflow.com/a/58318036/784226), prefix your search with `(?s)`. You can choose how to view the reassembled data by using the buttons at the bottom of the screen - ASCII, hex or Wireshark's raw format. Termshark will remember your preferred format. ![streams3](/../gh-pages/images/streams3.png?raw=true) Like Wireshark, you can filter the displayed data to show only the client-side or only the server-side of the conversation: ![streams4](/../gh-pages/images/streams4.png?raw=true) You can use Copy Mode in stream reassembly too. Hit the `c` key to enter Copy Mode. The currently selected "chunk" will be highlighted. Hit `ctrl-c` to copy that data. By default, termshark will copy the data to your clipboard. Hit the left arrow key to widen the data copied to the entire conversation (or filtered by client or server if that is selected). ![streams5](/../gh-pages/images/streams5.png?raw=true) Finally, clicking on a reassembled piece of the stream (enter or left mouse click) will cause termshark to select the underlying packet that contributed that payload. If you hit `q` to exit stream reassembly, termshark will set focus on the selected packet. ### Conversations To display a table of conversations represented in the current pcap, go to the "Analysis" menu and choose "Conversations". Termshark uses `tshark` to generate a list of conversations by protocol. Currently, termshark supports displaying Ethernet, IPv4, IPv6, UDP and TCP. ![convs1](/../gh-pages/images/convs1.png?raw=true) You can make termshark filter the packets displayed according to the current conversation selected. The "Prepare..." button will set termshark's display filter field, but *not* apply it, letting you futher edit it first. The "Apply..." button will set the display filter and apply it immediately. Navigate to the interesting conversation, then click either "Prepare..." or "Apply..." ![convs2](/../gh-pages/images/convs2.png?raw=true) In the first pop-up menu, you can choose how to extend the current display filter, if there is one. In the second pop-up menu, you can choose whether to filter by the conversation bidirectionally, unidirectionally, or just using the source or destination. These menus mirror those used in Wireshark. When you hit enter, the filter will be adjusted. Hit 'q' to quit the conversations screen. ![convs3](/../gh-pages/images/convs3.png?raw=true) ### Command-Line For fast navigation around the UI, termshark offers a vim-style command-line. To activate the command-line, hit the ':' key: ![cmdline1](/../gh-pages/images/cmdline1.png?raw=true) Many of termshark's operations can be initiated from the command-line. After opening the command-line, hit tab to show all the commands available: - **capinfo** - Show the current capture file properties (using the `capinfos` command) - **clear** - Clear the current pcap - **convs** - Open the conversations view - **filter** - Choose a display filter from those recently-used - **help** - Show one of several help dialogs - **load** - Load a pcap from the filesystem - **logs** - Show termshark's log file (Unix-only) - **map** - Map a keypress to a key sequence (see `help map`) - **marks** - Show file-local and global packet marks - **quit** - Quit termshark - **recents** - Load a pcap from those recently-used - **set** - Set various config properties (see `help set`) - **streams** - Open the stream reassemably view - **theme** - Set a new termshark theme - **unmap** - Remove a keypress mapping made with the `map` command Some commands require a parameter or more. Candidate completions will be shown when possible; you can then scroll up or down through them and hit tab or enter to complete the candidate. Candidates are filtered as you type. Hit enter to run a valid command or hit `ctrl-c` to close the command-line. ### Macros To support navigational shortcuts that are not directly built-in to the termshark UI, you can now create simple keyboard macros. These are modeled on vim's key mappings. To create a macro, open the [command-line](#command-line) and use the `map` command. The first argument is the key to map and the second argument is a sequence of keypresses that your first key should now map to. Termshark uses vim-syntax for keys. To express a keypress for a printable character, simply use the printable character. Here is the syntax for the other keys that termshark understands: - `` - `` - `` - `-` - modifiers - ``, `` - ``, ``, ``, `` - ``, `` - ``, `` Here are some example macros: - `map /` - hit ctrl-s to activate the display filter widget - `map :quit` - hit f1 to quit termshark without asking for confirmation - `map ZZ` - another way to quit quickly! - `map d` - toggle dark-mode A termshark user requested the ability to move up and down the packet list but to keep focus on the packet structure view. This can be accomplished by setting these macros: - `map ` - `map ` Then with focus on the packet structure view, hit `f5` to go down a packet and `f6` to go up a packet. Macros are saved in the termshark config file. To display the current list of macros, simply type `map` from the command-line with no arguments. ![macros](/../gh-pages/images/macros.png?raw=true) ## Configuration ### Dark Mode If termshark is too bright for your taste, try dark-mode. To enable, hit Esc to open the main menu and select "Toggle Dark Mode". ![darkmode](/../gh-pages/images/darkmode.png?raw=true) Your choice is stored in the termshark [config file](UserGuide.md#config-file). Dark-mode is supported throughout the termshark user-interface. ### Packet Colors By default, termshark will now display packets in the packet list view colored according to Wireshark's color rules. With recent installations of Wireshark, you can find this file at `$XDG_CONFIG_HOME/wireshark/colorfilters`. Termshark doesn't provide a way to edit the colors - the colors are provided by `tshark`. You can read about Wireshark's support [here](https://www.wireshark.org/docs/wsug_html_chunked/ChCustColorizationSection.html). If you don't like the way this looks in termshark, you can turn it off using termshark's main menu. ### Themes Termshark can be themed to better line up with other terminal applications that you use. Most of termshark's UI elements have names and you can tie colors to these names. Here is an example theme: ```toml [dracula] gray1 = "#464752" ... orange = "#ffb86c" purple = "#bd93f9" red = "#ff5555" white = "#f8f8f2" yellow = "#f1fa8c" [dark] button = ["dracula.black","dracula.gray3"] button-focus = ["dracula.white","dracula.magenta"] button-selected = ["dracula.white","dracula.gray3"] ... [light] button = ["dracula.black","dracula.white"] button-focus = ["dracula.black","dracula.purple"] button-selected = ["dracula.black","dracula.gray3"] ... ``` Termshark finds themes in two ways - from: - `$XDG_CONFIG_HOME/termshark/themes/*.toml` (e.g. `~/.config/termshark/themes/dracula.toml`) - from a small database compiled-in to the termshark binary. The termshark command-line provides two commands to interact with themes: - `theme` - choose a new theme from those provided and apply it. - `no-theme` - use no theme. Termshark saves your selected theme against the terminal color mode, which can be one of - 16-color - 256-color - truecolor i.e. 24-bit color The theme is saved in `termshark.toml` under, respectively, the keys: - `main.theme-16` - `main.theme-256` - `main.theme-truecolor` This means that if you run termshark on the same machine but with a different terminal emulator, you might need to re-apply the theme if the color mode has changed (e.g. `xterm` v `gnome terminal`) If you are running in truecolor/24-bit color, termshark will make the 256-color themes available too. Terminal emulators that support 24-bit color will support 256-color mode as well. If you have enabled termshark's packet colors - shown in the packet list view - then these colors will be reproduced faithfully according to Wireshark's rules. These colors don't adhere to termshark's themes. #### Built-in Themes and Base16 Termshark has four themes built-in: - `default` - termshark's original color scheme (16-color, 256-color, truecolor) - `dracula` - colors based on [Dracula theme](https://draculatheme.com/) project (256-color, truecolor) - `solarized` - based on [Ethan Schoonover's](https://ethanschoonover.com/solarized/) work (256-color, truecolor) - `base16` - (256-color, truecolor) If you make another, please submit it! :-) [Base16](https://github.com/chriskempson/base16) is a set of guidelines for building themes using a limited range of 8 colors and 8 grays. The [base16-shell](https://github.com/chriskempson/base16-shell) project is a set of scripts that remap colors 0-21 in the 256-color space of a terminal emulator. If you're in 256-color mode, this lets you have consistent coloring of your work in a terminal emulator, whether it's typing at the shell, or running a TUI. If you use base16-shell, choose termshark's `base16` theme to make use of your shell theme's colors. #### Make a Theme The layout of a theme is: - the color definitions - `[mytheme]` - set the foreground and background color of UI elements for - dark mode - `[dark]` - regular/light mode - `[light]` Here's an [example theme](https://raw.githubusercontent.com/gcla/termshark/master/assets/themes/dracula-256.toml) to follow. This [tcell source file](https://github.com/gdamore/tcell/blob/fcaa20f283682d6bbe19ceae067b37df3dc699d7/color.go#L821) shows some sample color names you can use. The UI elements are listed in the `[dark]` and `[light]` sections. Each element is assigned a pair of colors - foreground and background. The colors can be: - a reference to another field in the theme toml e.g. `dracula.black` - a color that termshark understands natively e.g. `#ffcc43`, `dark green`, `g50` (medium gray). Save your theme toml file under `~/.config/termshark/themes/` with a suffix indicating the color-mode e.g. `mytheme-256.toml`. If your theme is a truecolor theme (suffix `-truecolor.toml`), then RGB colors will be reproduced precisely by termshark and so by the terminal emulator. If your theme is a 256-color theme (suffix `-256.toml`), you can still use RGB colors in your toml, and termshark will then try to pick the closest matching color in the 256-color space. If termshark detects you are using base16-shell, then it will ignore colors 0-21 when choosing the closest match, since these will likely be remapped by the base16-shell theme. Hopefully the meaning of the UI-element names is guessable, but one detail to know is the difference between focus, selected and unselected. In a gowid application, one widget at a time will have "focus". For example, if you are navigating the packet's tree structure (the middle pane), one level of that protocol structure will be shown in blue, and will be the focus widget. If you hit tab to move to the hex view of the packet's bytes (the lower pane), then focus will move to the hex byte under the cursor; but the previously blue protocol structure in the middle pane will still be obvious, shown in grey. That protocol level is now "selected", but not in "focus". So selected is a way to highlight a widget in a container of widgets that will have focus when control returns to the container. Unselected means neither focus nor selected. ### Config File Termshark reads options from a TOML configuration file saved in `$XDG_CONFIG_HOME/termshark/termshark.toml` (e.g. `~/.config/termshark/termshark.toml` on Linux). All options are saved under the `[main]` section. The available options are: - `auto-scroll` (bool) - if true, termshark will automatically scroll down when packets are read in a live-capture mode (e.g. `-i eth0`) - `browse-command` (string list) - termshark will run this command with a URL e.g. when the user selects "FAQ" from the main menu. Any argument in the list that equals `$1` will be replaced by the URL prior to the command being run e.g. ```toml [main] browse-command = ["firefox", "$1"] ``` - `capinfos` (string) - make termshark use this specific `capinfos` binary (for pcap properties). - `capture-command` (string) - use this binary to capture packets, passing `-i`, `-w` and `-f` flags. - `color-tsharks` (string list) - a list of the paths of tshark binaries that termshark has confirmed support the `--color` flag. If you run termshark and the selected tshark binary is not in this list, termshark will check to see if it supports the `--color` flag. - `colors` (bool) - if true, and tshark supports the feature, termshark will colorize packets in its list view. - `conv-absolute-time` (bool) - if true, have tshark provide conversation data with a relative start time field. - `conv-resolve-names` (bool) - if true, have tshark provide conversation data with ethernet names resolved. - `conv-use-filter` (bool) - if true, have tshark provide conversation data limited to match the active display filter. - `conv-types` (string list) - a list of the conversation types termshark will query for and display in the conversations view. Currently limited to `eth`, `ip`, `ipv6`, `udp`, `tcp`. - `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. ```toml [main] copy-command = ["xsel", "-i", "-b"] ``` - `copy-command-timeout` (int) - how long termshark will wait (in seconds) for the copy command to complete before reporting an error. - `dark-mode` (bool) - if true, termshark will run in dark-mode. - `debug` (bool) - if true, run a debug web-server on http://localhost:6060. Shows termshark/golang internals - in case of a problem. - `disable-shark-fin` (bool) - if true then turn off the shark-fin screen-saver permanently. - `disk-cache-size-mb` (int) - how large termshark will allow `$XDG_CACHE_HOME/termshark/pcaps/` to grow; if the limit is exceeded, termshark will delete pcaps, oldest first. Set to -1 to disable (grow indefinitely). - `dumpcap` (string) - make termshark use this specific `dumpcap` (used when reading from an interface). - `ignore-base16-colors` (bool) - if true, when running in a terminal with 256-colors, ignore colors 0-21 in the 256-color-space when choosing the best match for a theme's RGB (24-bit) color. This avoids choosing colors that are remapped using e.g. [base16-shell](https://github.com/chriskempson/base16-shell). - `key-mappings` (string list) - a list of macros, where each string contains a vim-style keypress, a space, and then a sequence of keypresses. - `marks` (string json) - a serialized json structure representing the cross-pcap marks - for each, the keypress (`A` through `Z`); the pcap filename; the packet number; and a short summary of the packet. - `packet-colors` (bool) - if true (or missing), termshark will colorize packets according to Wireshark's rules. - `pager` (string) - the pager program to use when displaying termshark's log file - run like this: `sh -c " termshark.log"` - `pcap-bundle-size` - (int) - load tshark PDML this many packets at a time. Termshark will lazily load PDML because it's a slow process and uses a lot of RAM. For example, if `pcap-bundle-size`=1000, then on first loading a pcap, termshark will load PDML for packets 1-1000. If you scroll past packet 500, termshark will optimistically load PDML for packets 1001-2000. A higher value will make termshark load more packets at a time; a value of 0 means load the entire pcap's worth of PDML. Termshark stores the data compressed in RAM, but expect approximately 10MB per 1000 packets loaded. If you have the memory, can wait a minute or two for the entire pcap to load, and e.g. plan to use the packet list header to sort the packets in various ways, setting `pcap-bundle-size` to 0 will provide the best experience. - `pcap-cache-size` - (int) - termshark loads packet PDML (structure) and pcap (bytes) data in bundles of `pcap-bundle-size`. This setting determines how many such bundles termshark will keep cached. The default is 32. - `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. - `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. - `stream-cache-size` (int) - termshark caches the structures and UI used to display reassembled TCP and UDP streams. This allows for quickly redisplaying a stream that's been loaded before. This setting determines how many streams are cached. The default is 100. - `stream-view` (string - the default view when displaying a reassembled stream. Choose from "hex"/"ascii"/"raw". - `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 to use `termshark` itself with a special hidden `--tail` flag. But probably better to use Wireshark on Windows :-) - `term` (string) - termshark will use this as a replacement for the TERM environment variable. ```toml [main] term = "screen-256color" ``` - `theme` (string) - the currently selected theme, if absent, no theme is used. - `tshark` (string) - make termshark use this specific `tshark`. - `tshark-args` (string list) - these are added to each invocation of `tshark` made by termshark e.g. ```toml [main] tshark-args = ["-d","udp.port==2075,cflow]" ``` - `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. - `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). ## Troubleshooting If termshark is running slowly or otherwise misbehaving, you might be able to narrow the issue down by using the `--debug` flag. When you start termshark with `--debug`, three things happen: 1. A web server runs with content available at [http://127.0.0.1:6060/debug/pprof](http://127.0.0.1:6060/debug/pprof) (or the remote IP). This is a Golang feature and provides a view of some low-level internals of the process such as running goroutines. 2. On receipt of SIGUSR1, termshark will start a Golang CPU profile that runs for 20 seconds. 3. On receipt of SIGUSR2, termshark will create a Golang memory/heap profile. Profiles are stored under `$XDG_CACHE_HOME/termshark` (e.g. `~/.cache/termshark/`). If you open a termshark issue on github, these profiles will be useful for debugging. For commonly asked questions, check out the [FAQ](/docs/FAQ.md). termshark-2.2.0/fields.go000066400000000000000000000072771377442047300153400ustar00rootroot00000000000000// Copyright 2019-2021 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" "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{} } func DeleteCachedFields() error { return os.Remove(CacheFile("tsharkfieldsv2.gob.gz")) } // Can be run asynchronously. // This ought to use interfaces to make it testable. func (w *TSharkFields) Init() error { newer, err := FileNewerThan(CacheFile("tsharkfieldsv2.gob.gz"), DirOfPathCommandUnsafe(TSharkBin())) if err == nil { if newer { f := &mapOrString{} err = ReadGob(CacheFile("tsharkfieldsv2.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) } } } err = w.InitNoCache() if err != nil { return err } err = WriteGob(CacheFile("tsharkfieldsv2.gob.gz"), w.fields) if err != nil { return err } return nil } func (w *TSharkFields) InitNoCache() error { 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") { // Wireshark field fields := strings.Split(line, "\t") field := fields[2] protos := strings.SplitN(field, ".", 2) if len(protos) > 1 { 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") { // Wireshark protocol 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() 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.SplitN(field, ".", 2) 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: termshark-2.2.0/fields_test.go000066400000000000000000000013221377442047300163600ustar00rootroot00000000000000// Copyright 2019-2021 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.InitNoCache() 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: termshark-2.2.0/format/000077500000000000000000000000001377442047300150165ustar00rootroot00000000000000termshark-2.2.0/format/hexdump.go000066400000000000000000000025361377442047300170250ustar00rootroot00000000000000// Copyright 2019-2021 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 format implements useful string/byte formatting functions. package format import ( "encoding/hex" "fmt" "regexp" "strings" ) type Options struct { LeftAsciiDelimiter string RightAsciiDelimiter string } var re *regexp.Regexp func init() { re = regexp.MustCompile(`(?m)^(.{60})\|(.+?)\|$`) // do each line } // HexDump produces a wireshark-like hexdump, with an option to set the left and // right delimiter used for the ascii section. This is a cheesy implementation using // a regex to change the golang hexdump output. func HexDump(data []byte, opts ...Options) string { var opt Options if len(opts) > 0 { opt = opts[0] } // Output: // 00000000 47 6f 20 69 73 20 61 6e 20 6f 70 65 6e 20 73 6f |Go is an open so| // 00000010 75 72 63 65 20 70 72 6f 67 72 61 6d 6d 69 6e 67 |urce programming| // 00000020 20 6c 61 6e 67 75 61 67 65 2e | language.| res := hex.Dump(data) res = re.ReplaceAllString(res, fmt.Sprintf(`${1}%s${2}%s`, opt.LeftAsciiDelimiter, opt.RightAsciiDelimiter)) return strings.TrimRight(res, "\n") } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.2.0/format/hexdump_test.go000066400000000000000000000017371377442047300200660ustar00rootroot00000000000000// Copyright 2019-2021 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 format import ( "testing" "github.com/stretchr/testify/assert" ) //====================================================================== func TestHexDump1(t *testing.T) { var tests = []struct { in string out string }{ { "Go is an open source programming language.", "00000000 47 6f 20 69 73 20 61 6e 20 6f 70 65 6e 20 73 6f QGo is an open so\n" + "00000010 75 72 63 65 20 70 72 6f 67 72 61 6d 6d 69 6e 67 Qurce programming\n" + "00000020 20 6c 61 6e 67 75 61 67 65 2e Q language.", }, } for _, test := range tests { assert.Equal(t, true, (HexDump([]byte(test.in), Options{ LeftAsciiDelimiter: "Q", }) == test.out)) } } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.2.0/format/printable.go000066400000000000000000000036651377442047300173370ustar00rootroot00000000000000// Copyright 2019-2021 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 format implements useful string/byte formatting functions. package format import ( "bytes" "encoding/hex" "fmt" "regexp" "strings" "unicode" ) 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 MakePrintableStringWithNewlines(data []byte) string { var buffer bytes.Buffer for i := 0; i < len(data); i++ { if (data[i] >= 32 && data[i] < 127) || data[i] == '\n' { buffer.WriteString(string(rune(data[i]))) } else { buffer.WriteRune('.') } } return buffer.String() } 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() } var hexRe = regexp.MustCompile(`\\x[0-9a-fA-F][0-9a-fA-F]`) // TranslateHexCodes will change instances of "\x41" in the input to the // byte 'A' in the output, passing through other characters. This is a small // subset of strconv.Unquote() for wireshark PSML data. func TranslateHexCodes(s []byte) []byte { return hexRe.ReplaceAllFunc(s, func(m []byte) []byte { r, err := hex.DecodeString(string(m[2:])) if err != nil { panic(err) } return []byte{r[0]} }) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.2.0/go.mod000066400000000000000000000031041377442047300146320ustar00rootroot00000000000000module github.com/gcla/termshark/v2 go 1.12 require ( github.com/BurntSushi/toml v0.3.1 // indirect github.com/adam-hanna/arrayOperations v0.2.5 github.com/antchfx/xmlquery v1.0.0 github.com/antchfx/xpath v1.0.0 // indirect github.com/blang/semver v3.5.1+incompatible github.com/fsnotify/fsnotify v1.4.7 github.com/gcla/deep v1.0.2 github.com/gcla/gowid v1.1.1-0.20201029034135-cc3f828591d3 github.com/gcla/tail v1.0.1-0.20190505190527-650e90873359 github.com/gdamore/tcell v1.3.1-0.20200115030318-bff4943f9a29 github.com/go-test/deep v1.0.2 // indirect github.com/hashicorp/golang-lru v0.5.3 github.com/jessevdk/go-flags v1.4.0 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/kr/pretty v0.1.0 // indirect github.com/mattn/go-isatty v0.0.9 github.com/mreiferson/go-snappystream v0.2.3 github.com/pkg/errors v0.8.1 github.com/pkg/term v0.0.0-20190109203006-aa71e9d9e942 github.com/rakyll/statik v0.1.6 github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0 github.com/sirupsen/logrus v1.4.2 github.com/spf13/viper v1.3.2 github.com/stretchr/testify v1.4.0 github.com/tevino/abool v0.0.0-20170917061928-9b9efcf221b5 golang.org/x/net v0.0.0-20190620200207-3b0461eec859 // indirect gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect gopkg.in/fsnotify/fsnotify.v1 v1.4.7 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect ) replace github.com/gdamore/tcell => github.com/gcla/tcell v1.1.2-0.20200927150251-decc2045f510 replace github.com/pkg/term => github.com/gcla/term v0.0.0-20191015020247-31cba2f9f402 termshark-2.2.0/go.sum000066400000000000000000000305071377442047300146660ustar00rootroot00000000000000github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/adam-hanna/arrayOperations v0.2.5 h1:zphKpB5HGhHDkztF2oLcvnqIAu/L/YU3FB/9UghdsO0= github.com/adam-hanna/arrayOperations v0.2.5/go.mod h1:PhqKQzzPMRjFcC4Heh+kxha3nMvJ6lQNKuVEgoyimgU= 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/antchfx/xmlquery v1.0.0 h1:YuEPqexGG2opZKNc9JU3Zw6zFXwC47wNcy6/F8oKsrM= github.com/antchfx/xmlquery v1.0.0/go.mod h1:/+CnyD/DzHRnv2eRxrVbieRU/FIF6N0C+7oTtyUtCKk= github.com/antchfx/xpath v1.0.0 h1:Q5gFgh2O40VTSwMOVbFE7nFNRBu3tS21Tn0KAWeEjtk= github.com/antchfx/xpath v1.0.0/go.mod h1:Yee4kTMuNiPYJ7nSNorELQMr1J33uOpXDMByNYhvtNk= 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.1.1-0.20201029034135-cc3f828591d3 h1:6Cq8U0UgCovdZYXjDkW5XyALMgqjLI9Z6neSYGHk6wA= github.com/gcla/gowid v1.1.1-0.20201029034135-cc3f828591d3/go.mod h1:kwHYNePmuaNa60IAkHfd/OfPeZuoSuz7ww+CeA5q/aQ= github.com/gcla/tail v1.0.1-0.20190505190527-650e90873359 h1:3xEhacR7pIJV8daurdBygptxhzTJeYFqJp1V6SDl+pE= github.com/gcla/tail v1.0.1-0.20190505190527-650e90873359/go.mod h1:Wn+pZpM98JHSOYkPDtmdvlqmc0OzQGHWOsHB2d28WtQ= github.com/gcla/tcell v1.1.2-0.20200927150251-decc2045f510 h1:TlEZ0DHOvn0P79nHtkfemw7XFn2h8Lacd6AZpXrPU/o= github.com/gcla/tcell v1.1.2-0.20200927150251-decc2045f510/go.mod h1:vxEiSDZdW3L+Uhjii9c3375IlDmR05bzxY404ZVSMo0= github.com/gcla/term v0.0.0-20191015020247-31cba2f9f402 h1:d8cpYNgYjXDjvrdnSaFy4+o1D3BpPUKTBIcijrBKv4c= github.com/gcla/term v0.0.0-20191015020247-31cba2f9f402/go.mod h1:YCPU+G35BFc/575HWMl8oeqq+dTbunufWbWaV0Y2sqY= github.com/gcla/termshark v1.0.0 h1:3jDyqYHeGIfN2khlAfWmJzoJJTh6Iau2mz81nakLBPk= github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= 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/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw= github.com/go-test/deep v1.0.2/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/golang-lru v0.5.3 h1:YPkqC67at8FYaadspW/6uE0COsBxS2656RLEr8Bppgk= github.com/hashicorp/golang-lru v0.5.3/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= 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/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= 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/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.4 h1:5Myjjh3JY/NaAi4IsUbHADytDyl1VE1Y9PXDlL+P/VQ= github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac= github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 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.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54= github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mreiferson/go-snappystream v0.2.3 h1:ETSjz9NhUz13J3Aq0NisB/8h0nb2QG8DAcQNEw1T8cw= github.com/mreiferson/go-snappystream v0.2.3/go.mod h1:hPB+SkMcb49n7i7BErAtgT4jFQcaCVp6Vyu7aZ46qQo= 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 h1:uICcfUXpgqtw2VopbIncslhAmE5hwc4g20TEyEENBNs= 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.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 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/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/tevino/abool v0.0.0-20170917061928-9b9efcf221b5 h1:hNna6Fi0eP1f2sMBe/rJicDmaHmoXGe1Ta84FPYHLuE= github.com/tevino/abool v0.0.0-20170917061928-9b9efcf221b5/go.mod h1:f1SCnEOt6sc3fOJfPQDRDzHOtSXuTtnz0ImG9kPRDV0= 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-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-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 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-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191010194322-b09406accb47 h1:/XfQ9z7ib8eEJX2hdgFTZJ/ntt0swNk5oYBziWeTCvY= golang.org/x/sys v0.0.0-20191010194322-b09406accb47/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.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 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/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify/fsnotify.v1 v1.4.7 h1:XNNYLJHt73EyYiCZi6+xjupS9CpvmiDgjPTAjrBlQbo= gopkg.in/fsnotify/fsnotify.v1 v1.4.7/go.mod h1:Fyux9zXlo4rWoMSIzpn9fDAYjalPqJ/K1qJ27s+7ltE= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= termshark-2.2.0/noroot.go000066400000000000000000000017501377442047300154000ustar00rootroot00000000000000// Copyright 2019-2021 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: termshark-2.2.0/pcap/000077500000000000000000000000001377442047300144515ustar00rootroot00000000000000termshark-2.2.0/pcap/cmds.go000066400000000000000000000124711377442047300157330ustar00rootroot00000000000000// Copyright 2019-2021 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" "sync" "github.com/gcla/termshark/v2" "github.com/kballard/go-shellquote" log "github.com/sirupsen/logrus" ) //====================================================================== type ProcessNotStarted struct { Command *exec.Cmd } var _ error = ProcessNotStarted{} func (e ProcessNotStarted) Error() string { return fmt.Sprintf("Process %v [%v] not started yet", e.Command.Path, shellquote.Join(e.Command.Args...)) } //====================================================================== 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, shellquote.Join(c.Cmd.Args...)) } func (c *Command) Start() error { c.Lock() defer c.Unlock() c.Cmd.Stderr = log.StandardLogger().Writer() c.PutInNewGroupOnUnix() res := c.Cmd.Start() return res } func (c *Command) Wait() error { return c.Cmd.Wait() } func (c *Command) StdoutReader() (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 } // If stdout supports Close(), call it. If stdout is a pipe, for example, // this can be used to have EOF appear on the reading side (e.g. tshark -T psml) func (c *Command) Close() error { c.Lock() defer c.Unlock() if cl, ok := c.Cmd.Stdout.(io.Closer); ok { return cl.Close() } return nil } 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 Color bool } func MakeCommands(decodeAs []string, args []string, pdml []string, psml []string, color bool) Commands { return Commands{ DecodeAs: decodeAs, Args: args, PdmlArgs: pdml, PsmlArgs: psml, Color: color, } } var _ ILoaderCmds = Commands{} func (c Commands) Iface(ifaces []string, captureFilter string, tmpfile string) IBasicCommand { args := make([]string, 0) for _, iface := range ifaces { args = append(args, "-i", iface) } args = append(args, "-w", tmpfile) if captureFilter != "" { args = append(args, "-f", captureFilter) } res := &Command{ Cmd: exec.Command(termshark.CaptureBin(), args...), } // This tells termshark to start in a special capture mode. It allows termshark // to run itself like this: // // termshark -i eth0 -w foo.pcap // // which will then run dumpcap and if that fails, tshark. The idea // is to use the most specialized/efficient capture method if that // works, but fall back to tshark if needed e.g. for randpkt, sshcapture, etc // (extcap interfaces). res.Cmd.Env = append(os.Environ(), "TERMSHARK_CAPTURE_MODE=1") res.Cmd.Stdin = os.Stdin res.Cmd.Stderr = os.Stderr res.Cmd.Stdout = os.Stdout return res } 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, "-r", "-") 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) } if c.Color { args = append(args, "--color") } 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{"-r", pcap, "-x"} 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 c.Color { args = append(args, "--color") } 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: termshark-2.2.0/pcap/cmds_unix.go000066400000000000000000000014621377442047300167740ustar00rootroot00000000000000// Copyright 2019-2021 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 !windows package pcap import ( "syscall" "github.com/kballard/go-shellquote" "github.com/pkg/errors" log "github.com/sirupsen/logrus" ) func (c *Command) PutInNewGroupOnUnix() { c.Cmd.SysProcAttr = &syscall.SysProcAttr{ Setpgid: true, Pgid: 0, } } func (c *Command) Kill() error { c.Lock() defer c.Unlock() if c.Cmd.Process == nil { return errors.WithStack(ProcessNotStarted{Command: c.Cmd}) } log.Infof("Sending SIGKILL to %v: %v", c.Cmd.Process.Pid, shellquote.Join(c.Cmd.Args...)) // The PSML tshark process doesn't reliably die with a SIGTERM - not sure why return syscall.Kill(-c.Cmd.Process.Pid, syscall.SIGKILL) } termshark-2.2.0/pcap/cmds_windows.go000066400000000000000000000006611377442047300175030ustar00rootroot00000000000000// Copyright 2019-2021 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 "github.com/pkg/errors" func (c *Command) PutInNewGroupOnUnix() {} func (c *Command) Kill() error { c.Lock() defer c.Unlock() if c.Cmd.Process == nil { return errors.WithStack(ProcessNotStarted{Command: c.Cmd}) } return c.Cmd.Process.Kill() } termshark-2.2.0/pcap/handlers.go000066400000000000000000000060141377442047300166010ustar00rootroot00000000000000// Copyright 2019-2021 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 "github.com/gcla/gowid" //====================================================================== type HandlerCode int const ( NoneCode HandlerCode = 1 << iota PdmlCode PsmlCode TailCode IfaceCode ConvCode StreamCode CapinfoCode ) type IClear interface { OnClear(code HandlerCode, app gowid.IApp) } type INewSource interface { OnNewSource(code HandlerCode, app gowid.IApp) } type IOnError interface { OnError(code HandlerCode, app gowid.IApp, err error) } type IBeforeBegin interface { BeforeBegin(code HandlerCode, app gowid.IApp) } type IAfterEnd interface { AfterEnd(code HandlerCode, app gowid.IApp) } type IPsmlHeader interface { OnPsmlHeader(code HandlerCode, app gowid.IApp) } type IUnpack interface { Unpack() []interface{} } type HandlerList []interface{} func (h HandlerList) Unpack() []interface{} { return h } type unpackedHandlerFunc func(HandlerCode, gowid.IApp, interface{}) bool func HandleUnpack(code HandlerCode, cb interface{}, handler unpackedHandlerFunc, app gowid.IApp) bool { if c, ok := cb.(IUnpack); ok { handlers := c.Unpack() for _, cb := range handlers { handler(code, app, cb) // will wait on channel if it has to, doesn't matter if not } return true } return false } func HandleBegin(code HandlerCode, app gowid.IApp, cb interface{}) bool { res := false if !HandleUnpack(code, cb, HandleBegin, app) { if c, ok := cb.(IBeforeBegin); ok { c.BeforeBegin(code, app) res = true } } return res } func HandleEnd(code HandlerCode, app gowid.IApp, cb interface{}) bool { res := false if !HandleUnpack(code, cb, HandleEnd, app) { if c, ok := cb.(IAfterEnd); ok { c.AfterEnd(code, app) res = true } } return res } func HandleError(code HandlerCode, app gowid.IApp, err error, cb interface{}) bool { res := false if !HandleUnpack(code, cb, func(code HandlerCode, app gowid.IApp, cb2 interface{}) bool { return HandleError(code, app, err, cb2) }, app) { if ec, ok := cb.(IOnError); ok { ec.OnError(code, app, err) res = true } } return res } func handlePsmlHeader(code HandlerCode, app gowid.IApp, cb interface{}) bool { res := false if !HandleUnpack(code, cb, handlePsmlHeader, app) { if c, ok := cb.(IPsmlHeader); ok { c.OnPsmlHeader(code, app) res = true } } return res } func handleClear(code HandlerCode, app gowid.IApp, cb interface{}) bool { res := false if !HandleUnpack(code, cb, handleClear, app) { if c, ok := cb.(IClear); ok { c.OnClear(code, app) res = true } } return res } func handleNewSource(code HandlerCode, app gowid.IApp, cb interface{}) bool { res := false if !HandleUnpack(code, cb, handleNewSource, app) { if c, ok := cb.(INewSource); ok { c.OnNewSource(code, app) res = true } } return res } //====================================================================== // Local Variables: // mode: Go // fill-column: 78 // End: termshark-2.2.0/pcap/loader.go000066400000000000000000001772351377442047300162650ustar00rootroot00000000000000// Copyright 2019-2021 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 ( "bufio" "context" "encoding/xml" "fmt" "io" "os" "os/exec" "path/filepath" "regexp" "strconv" "strings" "sync" "sync/atomic" "github.com/gcla/gowid" "github.com/gcla/gowid/gwutil" "github.com/gcla/termshark/v2" "github.com/gcla/termshark/v2/format" lru "github.com/hashicorp/golang-lru" log "github.com/sirupsen/logrus" fsnotify "gopkg.in/fsnotify/fsnotify.v1" ) //====================================================================== var PcapCmds ILoaderCmds var PcapOpts Options var OpsChan chan gowid.RunFunction func init() { OpsChan = make(chan gowid.RunFunction, 100) } //====================================================================== var Goroutinewg *sync.WaitGroup type RunFn func() //====================================================================== type LoaderState bool const ( NotLoading LoaderState = false Loading LoaderState = true ) func (t LoaderState) String() string { if t { return "loading" } else { return "not-loading" } } //====================================================================== type ProcessState int const ( NotStarted ProcessState = 0 Started ProcessState = 1 Terminated ProcessState = 2 ) func (p ProcessState) String() string { switch p { case NotStarted: return "NotStarted" case Started: return "Started" case Terminated: return "Terminated" default: return "Unknown" } } //====================================================================== type IBasicCommand interface { fmt.Stringer Start() error Wait() error Pid() int Kill() error } type ITailCommand interface { IBasicCommand SetStdout(io.Writer) // set to the write side of a fifo, for example - the command will .Write() here Close() error // closes stdout, which signals tshark -T psml } type IPcapCommand interface { IBasicCommand StdoutReader() (io.ReadCloser, error) // termshark will .Read() from result } type ILoaderCmds interface { Iface(ifaces []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 } //====================================================================== // PacketLoader supports swapping out loaders type PacketLoader struct { *ParentLoader } func (c *PacketLoader) Renew() { if c.ParentLoader != nil { c.ParentLoader.CloseMain() } c.ParentLoader = NewPcapLoader(c.ParentLoader.cmds, c.runner, c.ParentLoader.opt) } type ParentLoader struct { // Note that a nil InterfaceLoader implies this loader is not handling a "live" packet source *InterfaceLoader // these are only replaced from the main goroutine, so no lock needed *PsmlLoader *PdmlLoader cmds ILoaderCmds tailStoppedDeliberately bool // true if tail is stopped because its packet feed has run out psrcs []IPacketSource // The canonical struct for the loader's current packet source. displayFilter string captureFilter string ifaceFile string // shared between InterfaceLoader and PsmlLoader - to preserve and feed packets mainCtx context.Context // cancelling this cancels the dependent contexts - used to close whole loader. mainCancelFn context.CancelFunc loadWasCancelled bool // True if the last load (iface or file) was halted by the stop button or ctrl-c runner IMainRunner opt Options // held only to pass to the PDML and PSML loaders when renewed } type InterfaceLoader struct { state LoaderState ifaceCtx context.Context // cancels the iface reader process ifaceCancelFn context.CancelFunc ifaceCmd IBasicCommand sync.Mutex // set by the iface procedure when it has finished e.g. the pipe to the fifo has finished, the // iface process has been killed, etc. This tells the psml-reading procedure when it should stop i.e. // when this many bytes have passed through. totalFifoBytesWritten gwutil.Int64Option totalFifoBytesRead gwutil.Int64Option fifoError error } type PsmlLoader struct { state LoaderState // which pieces are currently loading PcapPsml interface{} // Pcap file source for the psml reader - fifo if iface+!stopped; tmpfile if iface+stopped; pcap otherwise psmlStoppedDeliberately_ bool // true if loader is in a transient state due to a user operation e.g. stop, reload, etc psmlCtx context.Context // cancels the psml loading process psmlCancelFn context.CancelFunc tailCtx context.Context // cancels the tail reader process (if iface in operation) tailCancelFn context.CancelFunc // 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{} PsmlFinishedChan chan struct{} // closed when entire psml load process is done tailCmd ITailCommand PsmlCmd IPcapCommand // gcla later todo - change to pid like PdmlPid sync.Mutex packetPsmlData [][]string packetPsmlColors []PacketColors packetPsmlHeaders []string PacketNumberMap map[int]int // map from actual packet row
12
to pos in unsorted table // This would be affected by a display filter e.g. packet 12 might be the 1st packet in the table. // I need this so that if the user jumps to a mark stored as "packet 12", I can find the right table row. PacketCache *lru.Cache // i -> [pdml(i * 1000)..pdml(i+1*1000)] - accessed from any goroutine opt Options } type PdmlLoader struct { state LoaderState // which pieces are currently loading 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 pdmlStoppedDeliberately_ bool // true if loader is in a transient state due to a user operation e.g. stop, reload, etc stage2Ctx context.Context // cancels the pcap/pdml loading process stage2CancelFn context.CancelFunc stage2Wg sync.WaitGroup startChan chan struct{} Stage2FinishedChan chan struct{} // closed when entire pdml+pcap load process is done PdmlPid int // 0 if process not started PcapPid int // 0 if process not started sync.Mutex visible bool // true if this pdml load is needed right now by the UI rowCurrentlyLoading int // set by the pdml loading stage - main goroutine only highestCachedRow int // main goroutine only KillAfterReadingThisMany int // A shortcut - tell pcap/pdml to read one - no lock worked out yet opt Options } type PacketColors struct { FG gowid.IColor BG gowid.IColor } type Options struct { CacheSize int PacketsPerLoad int } type iLoaderEnv interface { Commands() ILoaderCmds MainRun(fn gowid.RunFunction) Context() context.Context } type iPsmlLoaderEnv interface { iLoaderEnv iTailCommand PsmlStoppedDeliberately() bool TailStoppedDeliberately() bool LoadWasCancelled() bool DisplayFilter() string InterfaceFile() string PacketSources() []IPacketSource } // IMainRunner is implemented by a type that runs a closure on termshark's main loop // (via gowid's App.Run) type IMainRunner interface { Run(fn gowid.RunFunction) } type Runner struct { gowid.IApp } var _ IMainRunner = (*Runner)(nil) func (a *Runner) Run(fn gowid.RunFunction) { a.IApp.Run(fn) } //====================================================================== func NewPcapLoader(cmds ILoaderCmds, runner IMainRunner, opts ...Options) *ParentLoader { var opt Options if len(opts) > 0 { opt = opts[0] } if opt.CacheSize == 0 { opt.CacheSize = 32 } if opt.PacketsPerLoad == 0 { opt.PacketsPerLoad = 1000 // default } else if opt.PacketsPerLoad < 100 { opt.PacketsPerLoad = 100 // minimum } res := &ParentLoader{ PsmlLoader: &PsmlLoader{}, // so default fields are set and XmlLoader is not nil PdmlLoader: &PdmlLoader{ opt: opt, }, cmds: cmds, runner: runner, opt: opt, } res.mainCtx, res.mainCancelFn = context.WithCancel(context.Background()) res.RenewPsmlLoader() res.RenewPdmlLoader() return res } func (c *ParentLoader) RenewPsmlLoader() { c.PsmlLoader = &PsmlLoader{ PcapPsml: c.PsmlLoader.PcapPsml, tailCmd: c.PsmlLoader.tailCmd, PsmlCmd: c.PsmlLoader.PsmlCmd, packetPsmlData: make([][]string, 0), packetPsmlColors: make([]PacketColors, 0), packetPsmlHeaders: make([]string, 0, 10), PacketNumberMap: make(map[int]int), startStage2Chan: make(chan struct{}), // do this before signalling start PsmlFinishedChan: make(chan struct{}), opt: c.opt, } packetCache, err := lru.New(c.opt.CacheSize) if err != nil { log.Fatal(err) } c.PacketCache = packetCache } func (c *ParentLoader) RenewPdmlLoader() { c.PdmlLoader = &PdmlLoader{ PcapPdml: c.PcapPdml, PcapPcap: c.PcapPcap, rowCurrentlyLoading: -1, highestCachedRow: -1, opt: c.opt, } } func (c *ParentLoader) RenewIfaceLoader() { c.InterfaceLoader = &InterfaceLoader{} } func (p *ParentLoader) LoadingAnything() bool { return p.PsmlLoader.IsLoading() || p.PdmlLoader.IsLoading() || p.InterfaceLoader.IsLoading() } func (p *ParentLoader) InterfaceFile() string { return p.ifaceFile } func (p *ParentLoader) DisplayFilter() string { return p.displayFilter } func (p *ParentLoader) CaptureFilter() string { return p.captureFilter } func (p *ParentLoader) 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. if p.PsmlLoader.PcapPsml != p.PdmlLoader.PcapPdml { log.Infof("Switching from interface/fifo mode to file mode") p.PsmlLoader.PcapPsml = p.PdmlLoader.PcapPdml } } var _ iPsmlLoaderEnv = (*ParentLoader)(nil) func (p *ParentLoader) PacketSources() []IPacketSource { return p.psrcs } func (p *ParentLoader) PsmlStoppedDeliberately() bool { return p.psmlStoppedDeliberately_ } func (p *ParentLoader) TailStoppedDeliberately() bool { return p.tailStoppedDeliberately } func (p *ParentLoader) LoadWasCancelled() bool { return p.loadWasCancelled } func (p *ParentLoader) Commands() ILoaderCmds { return p.cmds } func (p *ParentLoader) Context() context.Context { return p.mainCtx } func (p *ParentLoader) MainRun(fn gowid.RunFunction) { p.runner.Run(fn) } // CloseMain 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 *ParentLoader) CloseMain() { c.psmlStoppedDeliberately_ = true c.pdmlStoppedDeliberately_ = true if c.mainCancelFn != nil { c.mainCancelFn() c.mainCancelFn = nil } } func (c *ParentLoader) StopLoadPsmlAndIface(cb interface{}) { log.Infof("Requested stop psml + iface") c.psmlStoppedDeliberately_ = true c.loadWasCancelled = true c.stopTail() c.stopLoadPsml() c.stopLoadIface() } //====================================================================== // NewFilter is essentially a completely new load - psml + pdml. But not iface, if that's running func (c *PacketLoader) NewFilter(newfilt string, cb interface{}, app gowid.IApp) { log.Infof("Requested application of display filter '%v'", newfilt) if c.DisplayFilter() == newfilt { log.Infof("No operation - same filter applied ('%s').", newfilt) } else { c.stopTail() c.stopLoadPsml() c.stopLoadPdml() OpsChan <- gowid.RunFunction(func(app gowid.IApp) { c.RenewPsmlLoader() c.RenewPdmlLoader() // This is not ideal. I'm clearing the views, but I'm about to // restart. It's not really a new source, so called the new source // handler is an untify way of updating the current capture in the // title bar again handleClear(NoneCode, app, cb) c.displayFilter = newfilt log.Infof("Applying new display filter '%s'", newfilt) c.loadPsmlSync(c.InterfaceLoader, c, cb, app) }) } } func (c *PacketLoader) LoadPcap(pcap string, displayFilter string, cb interface{}, app gowid.IApp) { log.Infof("Requested pcap file load for '%v'", pcap) 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.") HandleError(NoneCode, app, fmt.Errorf("Same pcap and filter - nothing to do."), cb) } else { c.stopTail() c.stopLoadPsml() c.stopLoadPdml() c.stopLoadIface() OpsChan <- gowid.RunFunction(func(app gowid.IApp) { // This will enable the operation when clear completes handleClear(NoneCode, app, cb) c.Renew() c.psrcs = []IPacketSource{FileSource{Filename: pcap}} c.ifaceFile = "" c.PcapPsml = pcap c.PcapPdml = pcap c.PcapPcap = pcap c.displayFilter = displayFilter // call from main goroutine - when new filename is established handleNewSource(NoneCode, app, cb) log.Infof("Starting new pcap file load '%s'", pcap) c.loadPsmlSync(nil, c.ParentLoader, cb, app) }) } } // 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*. // Intended to restart iface loader - since a clear will discard all data up to here. func (c *PacketLoader) ClearPcap(cb interface{}) { startIfaceAgain := false if c.InterfaceLoader != nil { // Don't restart if the previous interface load was deliberately cancelled if !c.loadWasCancelled { startIfaceAgain = true for _, psrc := range c.psrcs { startIfaceAgain = startIfaceAgain && CanRestart(psrc) // Only try to restart if the packet source allows } } c.stopLoadIface() } // Don't close main context, it's used by interface process. // We may not have anything running, but it's ok - then the op channel // will be enabled if !startIfaceAgain { c.loadWasCancelled = true } c.stopTail() c.stopLoadPsml() c.stopLoadPdml() // When stop is done, launch the clear and restart OpsChan <- gowid.RunFunction(func(app gowid.IApp) { handleClear(NoneCode, app, cb) // Don't CloseMain - that will stop the interface process too c.loadWasCancelled = false c.RenewPsmlLoader() c.RenewPdmlLoader() if !startIfaceAgain { c.psrcs = c.psrcs[:0] c.ifaceFile = "" c.PcapPsml = "" c.PcapPdml = "" c.PcapPcap = "" c.displayFilter = "" } else { c.RenewIfaceLoader() if err := c.loadInterfaces(c.psrcs, c.CaptureFilter(), c.DisplayFilter(), c.InterfaceFile(), cb, app); err != nil { HandleError(NoneCode, app, err, cb) } } }) } // Always called from app goroutine context - so don't need to protect for race on cancelfn // Assumes gstate is ready // iface can be a number, or a fifo, or a pipe... func (c *PacketLoader) LoadInterfaces(psrcs []IPacketSource, captureFilter string, displayFilter string, tmpfile string, cb interface{}, app gowid.IApp) error { c.RenewIfaceLoader() return c.loadInterfaces(psrcs, captureFilter, displayFilter, tmpfile, cb, app) } func (c *ParentLoader) loadPsmlForInterfaces(psrcs []IPacketSource, captureFilter string, displayFilter string, tmpfile string, cb interface{}, app gowid.IApp) error { // It's a temporary unique file, and no processes are started yet, so either // (a) it doesn't exist, OR // (b) it does exist in which case this load is a result of a restart. // In ths second case, we need to discard existing packets before starting // tail in case it catches this file with existing data. err := os.Remove(tmpfile) if err != nil && !os.IsNotExist(err) { return err } c.PcapPsml = nil c.PcapPdml = tmpfile c.PcapPcap = tmpfile c.psrcs = psrcs // dpm't know if it's fifo (tfifo), pipe (/dev/fd/3) or iface (eth0). Treated same way c.ifaceFile = tmpfile c.displayFilter = displayFilter c.captureFilter = captureFilter handleNewSource(NoneCode, app, cb) log.Infof("Starting new interface/fifo load '%v'", SourcesString(psrcs)) c.PsmlLoader.loadPsmlSync(c.InterfaceLoader, c, cb, app) return nil } // intended for internal use func (c *ParentLoader) loadInterfaces(psrcs []IPacketSource, captureFilter string, displayFilter string, tmpfile string, cb interface{}, app gowid.IApp) error { if err := c.loadPsmlForInterfaces(psrcs, captureFilter, displayFilter, tmpfile, cb, app); err != nil { return err } // Deliberately use only HandleEnd handler once, in the PSML load - when it finishes, // we'll reenable ops c.InterfaceLoader.loadIfacesSync(c, cb, app) return nil } func (c *ParentLoader) String() string { names := make([]string, 0, len(c.psrcs)) for _, psrc := range c.psrcs { switch { case psrc.IsFile() || psrc.IsFifo(): names = append(names, filepath.Base(psrc.Name())) case psrc.IsPipe(): names = append(names, "") case psrc.IsInterface(): names = append(names, psrc.Name()) default: names = append(names, "(no packet source)") } } return strings.Join(names, " + ") } func (c *ParentLoader) Empty() bool { return len(c.psrcs) == 0 } func (c *ParentLoader) Pcap() string { for _, psrc := range c.psrcs { if psrc != nil && psrc.IsFile() { return psrc.Name() } } return "" } func (c *ParentLoader) Interfaces() []string { names := make([]string, 0, len(c.psrcs)) for _, psrc := range c.psrcs { if psrc != nil && !psrc.IsFile() { names = append(names, psrc.Name()) } } return names } func (c *ParentLoader) loadIsNecessary(ev LoadPcapSlice) bool { res := true if ev.Row > c.NumLoaded() { res = false } else if ce, ok := c.CacheAt((ev.Row / c.opt.PacketsPerLoad) * c.opt.PacketsPerLoad); 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 // I can't conclude that a load based at row 0 is sufficient to ignore this one. // The previous load might've started when only 10 packets were available (via the // the PSML data), so the PDML end idx would be frame.number < 10. This load might // be for a rocus position of 20, which would map via rounding to row 0. But we // don't have the data. // Hang on - this is for a load that has finished. If it was a live load, the cache // will not be marked complete for this batch of data - so a live load that is loading // this batch, but started earlier in the load (so frame.number < X where X < row) // will not be marked complete in the cache, so the load will be redone if needed. If // we get here, the load is still underway, so let it complete. } else if c.LoadingRow() == ev.Row { res = false } return res } //====================================================================== // Holds a reference to the loader, and wraps Read() around the tail process's // Read(). Count the bytes, and when they are equal to the final total of bytes // written by the tshark -i process (spooling to a tmp file), a function is called // which stops the PSML process. type tailReadTracker struct { tailReader io.Reader loader *InterfaceLoader tail iTailCommand callback interface{} app gowid.IApp } func (r *tailReadTracker) Read(p []byte) (int, error) { n, err := r.tailReader.Read(p) r.loader.Lock() if r.loader.totalFifoBytesRead.IsNone() { r.loader.totalFifoBytesRead = gwutil.SomeInt64(int64(n)) } else { r.loader.totalFifoBytesRead = gwutil.SomeInt64(int64(n) + r.loader.totalFifoBytesRead.Val()) } // err == ErrClosed if the pipe (tailReader) that is wrapped in this tracker is closed. // This can happen because this call to Read() and the deferred closepipe() function run // at the same time. if err != nil && r.loader.fifoError == nil && err != io.EOF && !errIsAlreadyClosed(err) { r.loader.fifoError = err } r.loader.Unlock() r.loader.checkAllBytesRead(r.tail, r.callback, r.app) return n, err } func errIsAlreadyClosed(err error) bool { if err == os.ErrClosed { return true } else if err, ok := err.(*os.PathError); ok { return errIsAlreadyClosed(err.Err) } else { return false } } //====================================================================== type iPdmlLoaderEnv interface { iLoaderEnv DisplayFilter() string ReadingFromFifo() bool StartStage2ChanFn() chan struct{} PacketCacheFn() *lru.Cache // i -> [pdml(i * 1000)..pdml(i+1*1000)] updateCacheEntryWithPdml(row int, pdml []IPdmlPacket, done bool) updateCacheEntryWithPcap(row int, pcap [][]byte, done bool) LengthOfPdmlCacheEntry(row int) (int, error) LengthOfPcapCacheEntry(row int) (int, error) CacheAt(row int) (CacheEntry, bool) DoWithPsmlData(func([][]string)) } func (c *PdmlLoader) loadPcapSync(row int, visible bool, ps iPdmlLoaderEnv, cb interface{}, app gowid.IApp) { // 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(ps.Context()) c.state = Loading c.rowCurrentlyLoading = row c.visible = visible // Set to true by a goroutine started within here if ctxCancel() is called i.e. the outer context var pdmlCancelled int32 var pcapCancelled int32 c.startChan = make(chan struct{}) c.Stage2FinishedChan = make(chan struct{}) // gcla later todo - suspect // Returns true if it's an error we should bring to user's attention unexpectedPdmlError := func(err error) bool { cancelled := atomic.LoadInt32(&pdmlCancelled) if cancelled == 0 { if err != io.EOF { if err, ok := err.(*xml.SyntaxError); !ok || err.Msg != "unexpected EOF" { return true } } } return false } unexpectedPcapError := func(err error) bool { cancelled := atomic.LoadInt32(&pcapCancelled) if cancelled == 0 { if err != io.EOF { if err, ok := err.(*xml.SyntaxError); !ok || err.Msg != "unexpected EOF" { return true } } } return false } setPcapCancelled := func() { atomic.CompareAndSwapInt32(&pcapCancelled, 0, 1) } setPdmlCancelled := func() { atomic.CompareAndSwapInt32(&pdmlCancelled, 0, 1) } //====================================================================== var displayFilterStr string sidx := -1 eidx := -1 // Determine this in main goroutine termshark.TrackedGo(func() { ps.MainRun(gowid.RunFunction(func(app gowid.IApp) { HandleBegin(PdmlCode, app, cb) })) // This should correctly wait for all resources, no matter where in the process of creating them // an interruption or error occurs defer func(p *PdmlLoader) { // Wait for all other goroutines to complete p.stage2Wg.Wait() // The process Wait() goroutine will always expect a stage2 cancel at some point. It can // come early, if the user interrupts the load. If not, then we send it now, to let // that goroutine terminate. p.stage2CancelFn() ps.MainRun(gowid.RunFunction(func(app gowid.IApp) { close(p.Stage2FinishedChan) HandleEnd(PdmlCode, app, cb) p.state = NotLoading p.rowCurrentlyLoading = -1 p.stage2CancelFn = nil })) }(c) // Set these before starting the pcap and pdml process goroutines so that // at the beginning, PdmlCmd and PcapCmd are definitely not nil. These // values are saved by the goroutine, and used to access the pid of these // processes, if they are started. var pdmlCmd IPcapCommand var pcapCmd IPcapCommand // // Goroutine to set mapping between table rows and frame numbers // termshark.TrackedGo(func() { select { case <-ps.StartStage2ChanFn(): break case <-c.stage2Ctx.Done(): return } // 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. // This has to wait until the PsmlCmd and PcapCmd are set - because next stages depend // on those defer func() { // Signal the pdml and pcap reader to start. select { case <-c.startChan: // it will be closed if the psml has loaded already, and this e.g. a cached load default: close(c.startChan) } }() // 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 = c.opt.PacketsPerLoad var err error if ps.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 abcdex eidx = row + c.opt.PacketsPerLoad + 1 + 1 } else { ps.DoWithPsmlData(func(psmlData [][]string) { if len(psmlData) > row { sidx, err = strconv.Atoi(psmlData[row][0]) if err != nil { log.Fatal(err) } if len(psmlData) > row+c.opt.PacketsPerLoad+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(psmlData[row+c.opt.PacketsPerLoad+1][0]) if err != nil { log.Fatal(err) } } else { eidx, err = strconv.Atoi(psmlData[len(psmlData)-1][0]) if err != nil { log.Fatal(err) } eidx += 1 // beyond end of last frame c.KillAfterReadingThisMany = len(psmlData) - row } } }) } if ps.DisplayFilter() != "" { displayFilterStr = fmt.Sprintf("(%s) and (frame.number >= %d) and (frame.number < %d)", ps.DisplayFilter(), sidx, eidx) } else { displayFilterStr = fmt.Sprintf("(frame.number >= %d) and (frame.number < %d)", sidx, eidx) } // These need to be set after displayFilterStr is set but before stage 2 is started pdmlCmd = ps.Commands().Pdml(c.PcapPdml, displayFilterStr) pcapCmd = ps.Commands().Pcap(c.PcapPcap, displayFilterStr) }, &c.stage2Wg, Goroutinewg) //====================================================================== pdmlPidChan := make(chan int) pcapPidChan := make(chan int) pdmlTermChan := make(chan error) pcapTermChan := make(chan error) pdmlCtx, pdmlCancelFn := context.WithCancel(c.stage2Ctx) pcapCtx, pcapCancelFn := context.WithCancel(c.stage2Ctx) // // Goroutine to track pdml and pcap process lifetimes // termshark.TrackedGo(func() { select { case <-c.startChan: case <-c.stage2Ctx.Done(): return } var err error stage2CtxChan := c.stage2Ctx.Done() pdmlPidChan := pdmlPidChan pcapPidChan := pcapPidChan pdmlCancelledChan := pdmlCtx.Done() pcapCancelledChan := pcapCtx.Done() pdmlState := NotStarted pcapState := NotStarted killPcap := func() { err := termshark.KillIfPossible(pcapCmd) if err != nil { log.Infof("Did not kill pcap process: %v", err) } } killPdml := func() { err = termshark.KillIfPossible(pdmlCmd) if err != nil { log.Infof("Did not kill pdml process: %v", err) } } loop: for { select { case err = <-pdmlTermChan: pdmlState = Terminated case err = <-pcapTermChan: pcapState = Terminated case pid := <-pdmlPidChan: // this channel can be closed on a stage2 cancel, before the // pdml process has been started, meaning we get nil for the // pid. If that's the case, don't save the cmd, so we know not // to try to kill anything later. pdmlPidChan = nil // don't select on this channel again if pid != 0 { pdmlState = Started // gcla later todo - use lock? c.PdmlPid = pid if stage2CtxChan == nil || pdmlCancelledChan == nil { // means that stage2 has been cancelled (so stop the load), and // pdmlCmd != nil => for sure a process was started. So kill it. // It won't have been cleaned up anywhere else because Wait() is // only called below, in this goroutine. killPdml() } } case pid := <-pcapPidChan: pcapPidChan = nil // don't select on this channel again if pid != 0 { pcapState = Started c.PcapPid = pid if stage2CtxChan == nil || pcapCancelledChan == nil { killPcap() } } case <-pdmlCancelledChan: pdmlCancelledChan = nil // don't select on this channel again setPdmlCancelled() if pdmlState == Started { killPdml() } case <-pcapCancelledChan: pcapCancelledChan = nil // don't select on this channel again setPcapCancelled() if pcapState == Started { // means that for sure, a process was started killPcap() } case <-stage2CtxChan: // This will automatically signal pdmlCtx.Done and pcapCtx.Done() // Once the pcap/pdml load is initiated, we guarantee we get a stage2 cancel // once all the stage2 goroutines are finished. So we don't quit the select loop // until this channel (as well as the others) has received a signal stage2CtxChan = nil } // if pdmlpidchan is nil, it means the the channel has been closed or we've received a message // a message means the proc has started // closed means it won't be started // if closed, then pdmlCmd == nil if (pdmlState == Terminated || (pdmlCancelledChan == nil && pdmlState == NotStarted)) && (pcapState == Terminated || (pcapCancelledChan == nil && pcapState == NotStarted)) { // nothing to select on so break break loop } } }, Goroutinewg) //====================================================================== // // Goroutine to run pdml process // termshark.TrackedGo(func() { // 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.startChan: case <-c.stage2Ctx.Done(): close(pdmlPidChan) return } // We didn't get a stage2 cancel yet. We could now, but for now we've been told to continue // now we'll guarantee either: // - we'll send the pdml pid on pdmlPidChan if it starts // - we'll close the channel if it doesn't start pid := 0 defer func() { // Guarantee that at the end of this goroutine, if we didn't start a process (pid == 0) // we will close the channel to signal the Wait() goroutine above. if pid == 0 { close(pdmlPidChan) } }() pdmlOut, err := pdmlCmd.StdoutReader() if err != nil { HandleError(PdmlCode, app, err, cb) return } err = pdmlCmd.Start() if err != nil { err = fmt.Errorf("Error starting PDML process %v: %v", pdmlCmd, err) HandleError(PdmlCode, app, err, cb) return } log.Infof("Started PDML command %v with pid %d", pdmlCmd, pdmlCmd.Pid()) pid = pdmlCmd.Pid() pdmlPidChan <- pid d := xml.NewDecoder(pdmlOut) packets := make([]IPdmlPacket, 0, c.opt.PacketsPerLoad) issuedKill := false readAllRequiredPdml := false var packet PdmlPacket var cpacket IPdmlPacket Loop: for { tok, err := d.Token() if err != nil { if !issuedKill && unexpectedPdmlError(err) { err = fmt.Errorf("Could not read PDML data: %v", err) HandleError(PdmlCode, app, err, cb) } if err == io.EOF { readAllRequiredPdml = true } break } switch tok := tok.(type) { case xml.StartElement: switch tok.Name.Local { case "packet": err := d.DecodeElement(&packet, &tok) if err != nil { if !issuedKill && unexpectedPdmlError(err) { err = fmt.Errorf("Could not decode PDML data: %v", err) HandleError(PdmlCode, app, err, cb) } break Loop } // Enabled for now - do something more subtle perhaps in the future if true { cpacket = SnappyPdmlPacket(packet) } else { cpacket = packet } packets = append(packets, cpacket) ps.updateCacheEntryWithPdml(row, packets, false) if len(packets) == c.KillAfterReadingThisMany { // Shortcut - we never take more than abcdex - so just kill here issuedKill = true readAllRequiredPdml = true c.pdmlStoppedDeliberately_ = true pdmlCancelFn() } } } } // The Wait has to come after the last read, which is above pdmlTermChan <- pdmlCmd.Wait() // Want to preserve invariant - for simplicity - that we only add full loads // to the cache ps.MainRun(gowid.RunFunction(func(gowid.IApp) { // never evict row 0 ps.PacketCacheFn().Get(0) if c.highestCachedRow != -1 { // try not to evict "end" ps.PacketCacheFn().Get(c.highestCachedRow) } // the cache entry is marked complete if we are not reading from a fifo, which implies // the source of packets will not grow larger. If it could grow larger, we want to ensure // that termshark doesn't think that there are only 900 packets, because that's what's // in the cache from a previous request - now there might be 950 packets. // // If the PDML routine was stopped programmatically, that implies the load was not complete // so we don't mark the cache as complete then either. markComplete := false if !ps.ReadingFromFifo() && readAllRequiredPdml { markComplete = true } ps.updateCacheEntryWithPdml(row, packets, markComplete) if row > c.highestCachedRow { c.highestCachedRow = row } })) }, &c.stage2Wg, Goroutinewg) //====================================================================== // // Goroutine to run pcap process // termshark.TrackedGo(func() { // 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.startChan: case <-c.stage2Ctx.Done(): close(pcapPidChan) return } pid := 0 defer func() { if pid == 0 { close(pcapPidChan) } }() pcapOut, err := pcapCmd.StdoutReader() if err != nil { HandleError(PdmlCode, app, err, cb) return } err = pcapCmd.Start() if err != nil { // e.g. on the pi err = fmt.Errorf("Error starting PCAP process %v: %v", pcapCmd, err) HandleError(PdmlCode, app, err, cb) return } log.Infof("Started pcap command %v with pid %d", pcapCmd, pcapCmd.Pid()) pid = pcapCmd.Pid() pcapPidChan <- pid packets := make([][]byte, 0, c.opt.PacketsPerLoad) issuedKill := false readAllRequiredPcap := false re := regexp.MustCompile(`([0-9a-f][0-9a-f] )`) rd := bufio.NewReader(pcapOut) packet := make([]byte, 0) for { line, err := rd.ReadString('\n') if err != nil { if !issuedKill && unexpectedPcapError(err) { err = fmt.Errorf("Could not read PCAP packet: %v", err) HandleError(PdmlCode, app, err, cb) } if err == io.EOF { readAllRequiredPcap = true } break } parseResults := re.FindAllStringSubmatch(string(line), -1) if len(parseResults) < 1 { packets = append(packets, packet) packet = make([]byte, 0) readEnough := (len(packets) >= c.KillAfterReadingThisMany) ps.updateCacheEntryWithPcap(row, packets, false) if readEnough && !issuedKill { // Shortcut - we never take more than abcdex - so just kill here issuedKill = true readAllRequiredPcap = true pcapCancelFn() } } else { // Ignore line number for _, parsedByte := range parseResults[1:] { b, err := strconv.ParseUint(string(parsedByte[0][0:2]), 16, 8) if err != nil { err = fmt.Errorf("Could not read PCAP packet: %v", err) if !issuedKill { HandleError(PdmlCode, app, err, cb) } break } packet = append(packet, byte(b)) } } } // The Wait has to come after the last read, which is above pcapTermChan <- pcapCmd.Wait() // I just want to ensure I read it from ram, obviously this is racey // never evict row 0 ps.PacketCacheFn().Get(0) if c.highestCachedRow != -1 { // try not to evict "end" ps.PacketCacheFn().Get(c.highestCachedRow) } markComplete := false if !ps.ReadingFromFifo() && readAllRequiredPcap { markComplete = true } ps.updateCacheEntryWithPcap(row, packets, markComplete) }, &c.stage2Wg, Goroutinewg) }, Goroutinewg) } // loadPsmlSync starts tshark processes, and other processes, to generate PSML // data. There is coordination with the PDML loader via a channel, // startStage2Chan. If a filter is set, then we might need to read far more // than a block of 1000 PDML packets (via frame.number <= 4000, for example), // and we don't know how many to read until the PSML is loaded. We don't want // to only load one PDML packet at a time, and reload as the user hits arrow // down through the PSML (in the case the packets selected by the filter are // very spaced out). // // The flow is as follows: // - if the source of packets is a fifo/interface then // - create a pipe // - set PcapPsml to a Reader object that tracks bytes read from the pipe // - start the PSML tshark command and get its stdout // - if the source of packets is a fifo/interface then // - use inotify to wait for the tmp pcap file to appear // - start the tail command to read the tmp file created by the interface loader // - read the PSML and add to data structures // // Goroutines are started to track the process lifetimes of both processes. // func (p *PsmlLoader) loadPsmlSync(iloader *InterfaceLoader, e iPsmlLoaderEnv, cb interface{}, app gowid.IApp) { // 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. p.psmlCtx, p.psmlCancelFn = context.WithCancel(e.Context()) p.tailCtx, p.tailCancelFn = context.WithCancel(e.Context()) intPsmlCtx, intPsmlCancelFn := context.WithCancel(context.Background()) p.state = Loading //====================================================================== var psmlOut io.ReadCloser // Only start this process if we are in interface mode var err error var fifoPipeReader *os.File var fifoPipeWriter *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 psmlPidChan := make(chan int) tailPidChan := make(chan int) psmlTermChan := make(chan error) tailTermChan := make(chan error) psmlPid := 0 // 0 means not running tailPid := 0 //====================================================================== termshark.TrackedGo(func() { e.MainRun(gowid.RunFunction(func(app gowid.IApp) { HandleBegin(PsmlCode, app, cb) })) defer func(ch chan struct{}) { // This will signal goroutines using select on this channel to terminate - like // ticker routines that update the packet list UI with new data every second. close(p.PsmlFinishedChan) e.MainRun(gowid.RunFunction(func(gowid.IApp) { HandleEnd(PsmlCode, app, cb) p.state = NotLoading p.psmlCancelFn = nil })) }(p.PsmlFinishedChan) //====================================================================== // Set to true by a goroutine started within here if ctxCancel() is called i.e. the outer context if e.DisplayFilter() == "" || p.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(p.startStage2Chan) } //====================================================================== if p.ReadingFromFifo() { // PcapPsml will be nil if here // Build a pipe - the side to be read from will be given to the PSML process // and the side to be written to is given to the tail process, which feeds in // data from the pcap source. // fifoPipeReader, fifoPipeWriter, err = os.Pipe() if err != nil { err = fmt.Errorf("Could not create pipe: %v", err) HandleError(PsmlCode, app, 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() { fifoPipeWriter.Close() fifoPipeReader.Close() }() // wrap the read end of the pipe with a Read() function that counts // bytes. If they are equal to the total bytes written to the tmpfile by // the tshark -i process, then that means the source is exhausted, and // the tail + psml processes are stopped. p.PcapPsml = &tailReadTracker{ tailReader: fifoPipeReader, loader: iloader, tail: e, callback: cb, app: app, } } // Set c.PsmlCmd before it's referenced in the goroutine below. We want to be // sure that if if psmlCmd is nil then that means the process has finished (not // has not yet started) p.PsmlCmd = e.Commands().Psml(p.PcapPsml, e.DisplayFilter()) // this channel always needs to be signalled or else the goroutine below won't terminate. // Closing it will pass a zero-value int (pid) to the goroutine which will understand that // means the psml process is NOT running, so it won't call cmd.Wait() on it. defer func() { if psmlPid == 0 { close(psmlPidChan) } }() //====================================================================== // Goroutine to track process state changes termshark.TrackedGo(func() { cancelledChan := p.psmlCtx.Done() intCancelledChan := intPsmlCtx.Done() var err error psmlCmd := p.PsmlCmd pidChan := psmlPidChan state := NotStarted kill := func() { err := termshark.KillIfPossible(psmlCmd) if err != nil { log.Infof("Did not kill tshark psml process: %v", err) } } loop: for { select { case err = <-psmlTermChan: state = Terminated if !p.psmlStoppedDeliberately_ { if err != nil { if _, ok := err.(*exec.ExitError); ok { cerr := gowid.WithKVs(termshark.BadCommand, map[string]interface{}{ "command": psmlCmd.String(), "error": err, }) HandleError(PsmlCode, app, cerr, cb) } } } case <-cancelledChan: intPsmlCancelFn() // start internal shutdown cancelledChan = nil case <-intCancelledChan: intCancelledChan = nil if state == Started { kill() } case pid := <-pidChan: pidChan = nil if pid != 0 { state = Started if intCancelledChan == nil { kill() } } } if state == Terminated || (intCancelledChan == nil && state == NotStarted) { break loop } } }, Goroutinewg) //====================================================================== psmlOut, err = p.PsmlCmd.StdoutReader() if err != nil { err = fmt.Errorf("Could not access pipe output: %v", err) HandleError(PsmlCode, app, err, cb) intPsmlCancelFn() return } err = p.PsmlCmd.Start() if err != nil { err = fmt.Errorf("Error starting PSML command %v: %v", p.PsmlCmd, err) HandleError(PsmlCode, app, err, cb) intPsmlCancelFn() return } log.Infof("Started PSML command %v with pid %d", p.PsmlCmd, p.PsmlCmd.Pid()) // Do this here because code later can return early - e.g. the watcher fails to be // set up - and then we'll never issue a Wait waitedForPsml := false // Prefer a defer rather than a goroutine here. That's because otherwise, this goroutine // and the XML processing routine reading the process's StdoutPipe are running in parallel, // and the XML routine should not issue a Read() (which it does behind the scenes) after // Wait() has been called. waitForPsml := func() { if !waitedForPsml { psmlTermChan <- p.PsmlCmd.Wait() waitedForPsml = true } } defer waitForPsml() psmlPid = p.PsmlCmd.Pid() psmlPidChan <- psmlPid //====================================================================== // 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. p.tailCmd = nil // Need to run dumpcap -i eth0 -w if p.ReadingFromFifo() { p.tailCmd = e.Commands().Tail(e.InterfaceFile()) defer func() { if tailPid == 0 { close(tailPidChan) } }() //====================================================================== // process lifetime goroutine for the tail process: // tshark -i > tmp // tail -f tmp | tshark -i - -t psml // ^^^^^^^^^^^ termshark.TrackedGo(func() { cancelledChan := p.tailCtx.Done() var err error tailCmd := p.tailCmd pidChan := tailPidChan state := NotStarted kill := func() { err := termshark.KillIfPossible(tailCmd) if err != nil { log.Infof("Did not kill tshark tail process: %v", err) } } loop: for { select { case err = <-tailTermChan: state = Terminated // Don't close the pipe - the psml might not have finished reading yet // gcla later todo - is this right or wrong // Close the pipe so that the psml reader gets EOF and will also terminate; // otherwise the PSML reader will block waiting for more data from the pipe fifoPipeWriter.Close() if !p.psmlStoppedDeliberately_ && !e.TailStoppedDeliberately() { if err != nil { if _, ok := err.(*exec.ExitError); ok { cerr := gowid.WithKVs(termshark.BadCommand, map[string]interface{}{ "command": tailCmd.String(), "error": err, }) HandleError(PsmlCode, app, cerr, cb) } } } case <-cancelledChan: cancelledChan = nil if state == Started { kill() } case pid := <-pidChan: pidChan = nil if pid != 0 { state = Started if cancelledChan == nil { kill() } } } // successfully started then died/kill, OR // was never started, won't be started, and cancelled if state == Terminated || (cancelledChan == nil && state == NotStarted) { break loop } } }, Goroutinewg) //====================================================================== p.tailCmd.SetStdout(fifoPipeWriter) // this set up is so that I can detect when there are actually packets to read (e.g // maybe there's no traffic on the interface). When there's something to read, the // rest of the procedure can spring into action. Why not spring into action right away? // Because the tail command needs a file to exist to watch it with -f. Can I rely on // tail -F across all supported platforms? (e.g. Windows) watcher, err := fsnotify.NewWatcher() if err != nil { err = fmt.Errorf("Could not create FS watch: %v", err) HandleError(PsmlCode, app, err, cb) intPsmlCancelFn() p.tailCancelFn() // needed to end the goroutine, end if tailcmd has not started return } defer watcher.Close() file, err := os.OpenFile(e.InterfaceFile(), os.O_RDWR|os.O_CREATE, 0644) if err != nil { err = fmt.Errorf("Could not touch temporary pcap file %s: %v", e.InterfaceFile(), err) HandleError(PsmlCode, app, err, cb) intPsmlCancelFn() p.tailCancelFn() // needed to end the goroutine, end if tailcmd has not started } file.Close() if err := watcher.Add(e.InterfaceFile()); err != nil { err = fmt.Errorf("Could not set up watcher for %s: %v", e.InterfaceFile(), err) HandleError(PsmlCode, app, err, cb) intPsmlCancelFn() p.tailCancelFn() // needed to end the goroutine, end if tailcmd has not started return } removeWatcher := func(file string) { if watcher != nil { watcher.Remove(file) watcher = nil } } // Make sure that no matter what happens from here on, the watcher is not leaked. But we'll remove // it earlier under normal operation so that setting and removing watches with new loaders do not // race. defer removeWatcher(e.InterfaceFile()) Loop: for { select { case fe := <-watcher.Events: if fe.Name == e.InterfaceFile() { break Loop } case err := <-watcher.Errors: err = fmt.Errorf("Unexpected watcher error for %s: %v", e.InterfaceFile(), err) HandleError(PsmlCode, app, err, cb) intPsmlCancelFn() p.tailCancelFn() // needed to end the goroutine, end if tailcmd has not started return case <-intPsmlCtx.Done(): return } } // Remove early if possible - because then if we clear the pcap and restart, we won't // race the termination of this function with the starting of a new instance of it, meaning // the new call adds the same watcher (idempotent) but then the terminating instance removes // it removeWatcher(e.InterfaceFile()) log.Infof("Starting Tail command: %v", p.tailCmd) err = p.tailCmd.Start() if err != nil { err = fmt.Errorf("Could not start tail command %v: %v", p.tailCmd, err) HandleError(PsmlCode, app, err, cb) intPsmlCancelFn() p.tailCancelFn() // needed to end the goroutine, end if tailcmd has not started return } termshark.TrackedGo(func() { tailTermChan <- p.tailCmd.Wait() }, Goroutinewg) tailPid = p.tailCmd.Pid() tailPidChan <- tailPid } // end of reading from fifo //====================================================================== // // Goroutine to read psml xml and update data structures // defer func(ch chan struct{}) { select { case <-ch: // already done/closed, do nothing default: close(ch) } // This will kill the tail process if there is one intPsmlCancelFn() // stop the ticker }(p.startStage2Chan) 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 var fg string var bg string var pidx int ready := false empty := true structure := false for { if intPsmlCtx.Err() != nil { break } tok, err := d.Token() if err != nil { // gcla later todo - LoadWasCancelled is checked outside of the main goroutine here if err != io.EOF && !e.LoadWasCancelled() { err = fmt.Errorf("Could not read PSML data: %v", err) HandleError(PsmlCode, app, err, cb) } break } switch tok := tok.(type) { case xml.EndElement: switch tok.Name.Local { case "structure": structure = false case "packet": p.Lock() p.packetPsmlData = append(p.packetPsmlData, curPsml) // Track the mapping of packet number
12
to position // in the table e.g. 5th element. This is so that I can jump to the correct // row with marks even if a filter is currently applied. pidx, err = strconv.Atoi(curPsml[0]) if err != nil { log.Fatal(err) } p.PacketNumberMap[pidx] = len(p.packetPsmlData) - 1 p.packetPsmlColors = append(p.packetPsmlColors, PacketColors{ FG: psmlColorToIColor(fg), BG: psmlColorToIColor(bg), }) p.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) fg = "" bg = "" for _, attr := range tok.Attr { switch attr.Name.Local { case "foreground": fg = attr.Value case "background": bg = attr.Value } } case "section": ready = true empty = true } case xml.CharData: if ready { if structure { p.Lock() p.packetPsmlHeaders = append(p.packetPsmlHeaders, string(tok)) p.Unlock() e.MainRun(gowid.RunFunction(func(app gowid.IApp) { handlePsmlHeader(PsmlCode, app, cb) })) } else { curPsml = append(curPsml, string(format.TranslateHexCodes(tok))) empty = false } } } } }, Goroutinewg) } func (c *PsmlLoader) DoWithPsmlData(fn func([][]string)) { c.Lock() defer c.Unlock() fn(c.packetPsmlData) } func (c *PsmlLoader) ReadingFromFifo() bool { // If it's a string it means that it's a filename, so it's not a fifo. Other values // in practise are the empty interface, or the read end of a fifo _, ok := c.PcapPsml.(string) return !ok } func (c *PsmlLoader) IsLoading() bool { return c.state == Loading } func (c *PsmlLoader) StartStage2ChanFn() chan struct{} { return c.startStage2Chan } func (c *PsmlLoader) PacketCacheFn() *lru.Cache { // i -> [pdml(i * 1000)..pdml(i+1*1000)] return c.PacketCache } func (c *PsmlLoader) packetPsmlDataFn() [][]string { return c.packetPsmlData } // Assumes this is a clean stop, not an error func (p *ParentLoader) stopTail() { p.tailStoppedDeliberately = true if p.tailCancelFn != nil { p.tailCancelFn() } } func (p *PsmlLoader) PacketsPerLoad() int { p.Lock() defer p.Unlock() return p.opt.PacketsPerLoad } func (p *PsmlLoader) stopLoadPsml() { p.psmlStoppedDeliberately_ = true if p.psmlCancelFn != nil { p.psmlCancelFn() } } func (p *PsmlLoader) PsmlData() [][]string { return p.packetPsmlData } func (p *PsmlLoader) PsmlHeaders() []string { return p.packetPsmlHeaders } func (p *PsmlLoader) PsmlColors() []PacketColors { return p.packetPsmlColors } // if done==true, then this cache entry is complete func (p *PsmlLoader) updateCacheEntryWithPdml(row int, pdml []IPdmlPacket, done bool) { var ce CacheEntry p.Lock() defer p.Unlock() if ce2, ok := p.PacketCache.Get(row); ok { ce = ce2.(CacheEntry) } ce.Pdml = pdml ce.PdmlComplete = done p.PacketCache.Add(row, ce) } func (p *PsmlLoader) updateCacheEntryWithPcap(row int, pcap [][]byte, done bool) { var ce CacheEntry p.Lock() defer p.Unlock() if ce2, ok := p.PacketCache.Get(row); ok { ce = ce2.(CacheEntry) } ce.Pcap = pcap ce.PcapComplete = done p.PacketCache.Add(row, ce) } func (p *PsmlLoader) LengthOfPdmlCacheEntry(row int) (int, error) { p.Lock() defer p.Unlock() if ce, ok := p.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 (p *PsmlLoader) LengthOfPcapCacheEntry(row int) (int, error) { p.Lock() defer p.Unlock() if ce, ok := p.PacketCache.Get(row); ok { ce2 := ce.(CacheEntry) return len(ce2.Pcap), nil } return -1, fmt.Errorf("No cache entry found for row %d", row) } func (c *PsmlLoader) CacheAt(row int) (CacheEntry, bool) { if ce, ok := c.PacketCache.Get(row); ok { return ce.(CacheEntry), ok } return CacheEntry{}, false } func (c *PsmlLoader) NumLoaded() int { c.Lock() defer c.Unlock() return len(c.packetPsmlData) } //====================================================================== func (c *PdmlLoader) IsLoading() bool { return c.state == Loading } func (c *PdmlLoader) LoadIsVisible() bool { return c.visible } // Only call from main goroutine func (c *PdmlLoader) LoadingRow() int { return c.rowCurrentlyLoading } func (p *PdmlLoader) stopLoadPdml() { p.pdmlStoppedDeliberately_ = true if p.stage2CancelFn != nil { p.stage2CancelFn() } } //====================================================================== type iTailCommand interface { stopTail() } type iIfaceLoaderEnv interface { iLoaderEnv iTailCommand PsmlStoppedDeliberately() bool InterfaceFile() string PacketSources() []IPacketSource CaptureFilter() string } // dumpcap -i eth0 -w /tmp/foo.pcap // dumpcap -i /dev/fd/3 -w /tmp/foo.pcap func (i *InterfaceLoader) loadIfacesSync(e iIfaceLoaderEnv, cb interface{}, app gowid.IApp) { i.totalFifoBytesWritten = gwutil.NoneInt64() i.ifaceCtx, i.ifaceCancelFn = context.WithCancel(e.Context()) log.Infof("Starting Iface command: %v", i.ifaceCmd) pid := 0 ifacePidChan := make(chan int) defer func() { if pid == 0 { close(ifacePidChan) } }() // tshark -i eth0 -w foo.pcap i.ifaceCmd = e.Commands().Iface(SourcesNames(e.PacketSources()), e.CaptureFilter(), e.InterfaceFile()) err := i.ifaceCmd.Start() if err != nil { err = fmt.Errorf("Error starting interface reader %v: %v", i.ifaceCmd, err) HandleError(IfaceCode, app, err, cb) return } ifaceTermChan := make(chan error) i.state = Loading log.Infof("Started Iface command %v with pid %d", i.ifaceCmd, i.ifaceCmd.Pid()) // Do this in a goroutine because the function is expected to return quickly termshark.TrackedGo(func() { ifaceTermChan <- i.ifaceCmd.Wait() }, Goroutinewg) //====================================================================== // Process goroutine termshark.TrackedGo(func() { defer func() { // if psrc is a PipeSource, then we open /dev/fd/3 in termshark, and reroute descriptor // stdin to number 3 when termshark starts. So to kill the process writing in, we need // to close our side of the pipe. for _, psrc := range e.PacketSources() { if cl, ok := psrc.(io.Closer); ok { cl.Close() } } e.MainRun(gowid.RunFunction(func(gowid.IApp) { i.state = NotLoading i.ifaceCancelFn = nil })) }() cancelledChan := i.ifaceCtx.Done() state := NotStarted var err error pidChan := ifacePidChan ifaceCmd := i.ifaceCmd killIface := func() { err = termshark.KillIfPossible(i.ifaceCmd) if err != nil { log.Infof("Did not kill iface process: %v", err) } } loop: for { select { case err = <-ifaceTermChan: state = Terminated if !e.PsmlStoppedDeliberately() && err != nil { if _, ok := err.(*exec.ExitError); ok { // This could be if termshark is started like this: cat nosuchfile.pcap | termshark -i - // Then dumpcap will be started with /dev/fd/3 as its stdin, but will fail with EOF and // exit status 1. cerr := gowid.WithKVs(termshark.BadCommand, map[string]interface{}{ "command": ifaceCmd.String(), "error": err, }) HandleError(IfaceCode, app, cerr, cb) } } case pid := <-pidChan: // this channel can be closed on a stage2 cancel, before the // pdml process has been started, meaning we get nil for the // pid. If that's the case, don't save the cmd, so we know not // to try to kill anything later. pidChan = nil if pid != 0 { state = Started if cancelledChan == nil { killIface() } } case <-cancelledChan: cancelledChan = nil if state == Started { killIface() } } // if pdmlpidchan is nil, it means the the channel has been closed or we've received a message // a message means the proc has started // closed means it won't be started // if closed, then pdmlCmd == nil if state == Terminated || (cancelledChan == nil && state == NotStarted) { // nothing to select on so break break loop } } // Calculate the final size of the tmp file we wrote with packets read from the // interface/pipe. This runs after the dumpcap command finishes. fi, err := os.Stat(e.InterfaceFile()) i.Lock() if err != nil { log.Warn(err) // Deliberately not a fatal error - it can happen if the source of packets to tshark -i // is corrupt, resulting in a tshark error. Setting zero here will line up with the // reading end which will read zero, and so terminate the tshark -T psml procedure. if i.fifoError == nil && !os.IsNotExist(err) { // Ignore ENOENT because it means there was an error before dumpcap even wrote // anything to disk i.fifoError = err } } else { i.totalFifoBytesWritten = gwutil.SomeInt64(fi.Size()) } i.Unlock() i.checkAllBytesRead(e, cb, app) }, Goroutinewg) //====================================================================== pid = i.ifaceCmd.Pid() ifacePidChan <- pid } // checkAllBytesRead is called (a) when the tshark -i process is finished // writing to the tmp file and (b) every time the tmpfile tail process reads // bytes. totalFifoBytesWrite is set to non-nil only when the tail process // completes. totalFifoBytesRead is updated every read. If they are every // found to be equal, it means that (1) the tail process has finished, meaning // killed or has reached EOF with its packet source (e.g. stdin, fifo) and (2) // the tail process has read all those bytes - so no packets will be // missed. In that case, the tail process is killed and its stdout closed, // which will trigger the psml reading process to shut down, and termshark // will turn off its loading UI. func (i *InterfaceLoader) checkAllBytesRead(e iTailCommand, cb interface{}, app gowid.IApp) { cancel := false if !i.totalFifoBytesWritten.IsNone() && !i.totalFifoBytesRead.IsNone() { if i.totalFifoBytesRead.Val() == i.totalFifoBytesWritten.Val() { cancel = true } } if i.fifoError != nil { cancel = true } // if there was a fifo error, OR we have read all the bytes that were written, then // we need to stop the tail command if cancel { if i.fifoError != nil { err := fmt.Errorf("Fifo error: %v", i.fifoError) HandleError(IfaceCode, app, err, cb) } e.stopTail() } } func (i *InterfaceLoader) stopLoadIface() { if i != nil && i.ifaceCancelFn != nil { i.ifaceCancelFn() } } func (c *InterfaceLoader) IsLoading() bool { return c != nil && c.state == Loading } //====================================================================== type CacheEntry struct { Pdml []IPdmlPacket Pcap [][]byte PdmlComplete bool PcapComplete bool } func (c CacheEntry) Complete() bool { return c.PdmlComplete && c.PcapComplete } //====================================================================== type LoadPcapSlice struct { Row int CancelCurrent bool Jump int // 0 means no jump } func (m LoadPcapSlice) String() string { pieces := make([]string, 0, 3) pieces = append(pieces, fmt.Sprintf("loadslice: %d", m.Row)) if m.CancelCurrent { pieces = append(pieces, fmt.Sprintf("cancelcurrent: %v", m.CancelCurrent)) } if m.Jump != 0 { pieces = append(pieces, fmt.Sprintf("jumpto: %d", m.Jump)) } return fmt.Sprintf("[%s]", strings.Join(pieces, ", ")) } //====================================================================== func ProcessPdmlRequests(requests []LoadPcapSlice, mloader *ParentLoader, loader *PdmlLoader, cb interface{}, app gowid.IApp) []LoadPcapSlice { Loop: for { if len(requests) == 0 { break } else { ev := requests[0] if !mloader.loadIsNecessary(ev) { requests = requests[1:] } else { if loader.state == Loading { if ev.CancelCurrent { loader.stopLoadPdml() } } else { mloader.RenewPdmlLoader() // ops? mloader.loadPcapSync(ev.Row, ev.CancelCurrent, mloader, cb, app) requests = requests[1:] } break Loop } } } return requests } //====================================================================== func psmlColorToIColor(col string) gowid.IColor { if res, err := gowid.MakeRGBColorSafe(col); err != nil { return nil } else { return res } } // https://stackoverflow.com/a/28005931/784226 func TempPcapFile(tokens ...string) string { tokensClean := make([]string, 0, len(tokens)) for _, token := range tokens { re := regexp.MustCompile(`[^a-zA-Z0-9.-]`) tokensClean = append(tokensClean, re.ReplaceAllString(token, "_")) } tokenClean := strings.Join(tokensClean, "-") return filepath.Join(termshark.PcapDir(), fmt.Sprintf("%s--%s.pcap", tokenClean, termshark.DateStringForFilename(), )) } //====================================================================== // Local Variables: // mode: Go // fill-column: 78 // End: termshark-2.2.0/pcap/loader_tshark_test.go000066400000000000000000000366131377442047300206720ustar00rootroot00000000000000// Copyright 2019-2021 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/v2" "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 foocount int } 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) } resi, rese := r.actual.Read(p) return resi, rese } 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, foocount: i} } readers = append([]io.Reader{strings.NewReader(string(hdr))}, readers...) return io.MultiReader(readers...) } //====================================================================== type fakeIfaceCmd struct { *simpleCmd output io.Writer input io.Reader } func newLoopingIfaceCmd(prefix string, stopper iStopLoop) *fakeIfaceCmd { return &fakeIfaceCmd{ simpleCmd: newSimpleCmd(strings.NewReader("")), input: newPcapLooper(prefix, "pcap", stopper), // loop forever until stopper signals to end } } func newHackedIfaceCmd(pfn portfn, stopper iStopLoop) *fakeIfaceCmd { return &fakeIfaceCmd{ simpleCmd: newSimpleCmd(strings.NewReader("")), 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() { n, err := io.Copy(f.output, f.input) if err != nil { //panic(err) //log.Infof("GCLA: err is %T", err) } }, Goroutinewg) return nil } func (f *fakeIfaceCmd) Kill() error { return f.simpleCmd.Kill() } func (f *fakeIfaceCmd) Signal(s os.Signal) error { return f.Kill() } func (f *fakeIfaceCmd) StdoutPipe() (io.ReadCloser, error) { panic(nil) } func (f *fakeIfaceCmd) Stdout() io.Writer { return f.output } func (f *fakeIfaceCmd) SetStdout(w io.WriteCloser) { f.output = w } //====================================================================== type fakeIface struct { prefix string stopper iStopLoop } func (f *fakeIface) Iface(iface string, filter string, tmpfile string) IBasicCommand { return newLoopingIfaceCmd(f.prefix, f.stopper) } //====================================================================== type hackedIface struct { stopper iStopLoop pfn portfn } func (f *hackedIface) Iface(iface string, filter string, tmpfile string) IBasicCommand { return newHackedIfaceCmd(f.pfn, f.stopper) } //====================================================================== type IIface interface { Iface(iface string, filter 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, captureFilter, tmpfile) } //====================================================================== type inputStoppedError struct{} func (e inputStoppedError) Error() string { return "Test stopped input" } type chanerr struct { err error valid bool } type chanfn func() <-chan chanerr type waitForAnswer struct { ch chanfn } var _ iStopLoop = (*waitForAnswer)(nil) func (s *waitForAnswer) shouldStop() error { errv := <-s.ch() if errv.valid { return errv.err } else { return inputStoppedError{} } } //====================================================================== func TestIface1(t *testing.T) { answerChan := make(chan chanerr) getChan := func() <-chan chanerr { 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() ch := make(chan struct{}) // Start the packet generation and reading process 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 <- chanerr{err: nil, valid: true} } 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() // Stop the packet generation and reading process 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 chanerr) getChan := func() <-chan chanerr { 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("buggy foo 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("fake reading %d packets from looper\n", read) for i := 0; i < read; i++ { //fmt.Printf("loop 1: sending answerchan for %d\n", i) answerChan <- chanerr{err: nil, valid: true} //fmt.Printf("loop 1: sending answerchan for %d\n", i) } fmt.Printf("fake giving processes time to catch up\n") time.Sleep(2 * time.Second) fmt.Printf("fake stopping iface read\n") ch = make(chan struct{}) loader.doStopLoadToIfaceOperation(func() { close(ch) }) close(answerChan) fmt.Printf("fake waiting for loader to signal end\n") <-psmlFinChan fmt.Printf("fake done loading interface pcap\n") fmt.Printf("fake 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("fake 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 chanerr) filtcount = 1000 port = 0 updater = newWaitForEnd() fmt.Printf("buggy foo fake 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: fake giving processes time to catch up\n") time.Sleep(2 * time.Second) fmt.Printf("loop 2: fake 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: fake waiting for loader to signal end\n") <-psmlFinChan fmt.Printf("fake 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("fake waiting for iface to stop\n") //loader.stopLoadIface() <-ich fmt.Printf("fake 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: termshark-2.2.0/pcap/pdml.go000066400000000000000000000051611377442047300157370ustar00rootroot00000000000000// Copyright 2019-2021 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" "compress/gzip" "encoding/gob" "encoding/xml" "io" "github.com/mreiferson/go-snappystream" ) //====================================================================== type IPdmlPacket interface { Packet() PdmlPacket } type PdmlPacket struct { XMLName xml.Name `xml:"packet"` Content []byte `xml:",innerxml"` } var _ IPdmlPacket = PdmlPacket{} func (p PdmlPacket) Packet() PdmlPacket { return p } //====================================================================== type GzippedPdmlPacket struct { Data bytes.Buffer } var _ IPdmlPacket = GzippedPdmlPacket{} func (p GzippedPdmlPacket) Packet() PdmlPacket { return p.Uncompress() } func (p GzippedPdmlPacket) Uncompress() PdmlPacket { greader, err := gzip.NewReader(&p.Data) if err != nil { panic(err) } decoder := gob.NewDecoder(greader) var res PdmlPacket err = decoder.Decode(&res) if err != nil { panic(err) } return res } func GzipPdmlPacket(p PdmlPacket) IPdmlPacket { res := GzippedPdmlPacket{} gwriter := gzip.NewWriter(&res.Data) encoder := gob.NewEncoder(gwriter) err := encoder.Encode(p) if err != nil { panic(err) } gwriter.Close() return res } //====================================================================== type SnappiedPdmlPacket struct { Data bytes.Buffer } var _ IPdmlPacket = SnappiedPdmlPacket{} func (p SnappiedPdmlPacket) Packet() PdmlPacket { return p.Uncompress() } func (p SnappiedPdmlPacket) Uncompress() PdmlPacket { var res PdmlPacket UnsnappyMe(&res, &p.Data) return res } func SnappyPdmlPacket(p PdmlPacket) IPdmlPacket { res := SnappiedPdmlPacket{} SnappyMe(p, &res.Data) return res } //====================================================================== // SnappyMe compresses the object within interface p to the // writer w. func SnappyMe(p interface{}, w io.Writer) { gwriter := snappystream.NewBufferedWriter(w) encoder := gob.NewEncoder(gwriter) if err := encoder.Encode(p); err != nil { panic(err) } gwriter.Close() } // UnsnappyMe decompresses from reader r into res. Afterwards, // res will be an interface whose type is a pointer to whatever // was serialized in the first place. func UnsnappyMe(res interface{}, r io.Reader) { greader := snappystream.NewReader(r, false) decoder := gob.NewDecoder(greader) if err := decoder.Decode(res); err != nil { panic(err) } } //====================================================================== // Local Variables: // mode: Go // fill-column: 78 // End: termshark-2.2.0/pcap/source.go000066400000000000000000000074311377442047300163050ustar00rootroot00000000000000// Copyright 2019-2021 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" "os" "strings" "github.com/gcla/termshark/v2/system" ) //====================================================================== type IPacketSource interface { Name() string IsFile() bool IsInterface() bool IsFifo() bool IsPipe() bool } //====================================================================== func FileSystemSources(srcs []IPacketSource) []IPacketSource { res := make([]IPacketSource, 0) for _, src := range srcs { if src.IsFile() { res = append(res, src) } } return res } func SourcesString(srcs []IPacketSource) string { return strings.Join(SourcesNames(srcs), " + ") } func SourcesNames(srcs []IPacketSource) []string { names := make([]string, 0, len(srcs)) for _, psrc := range srcs { names = append(names, psrc.Name()) } return names } func UIName(src IPacketSource) string { if src.IsPipe() { return "" } else { return src.Name() } } func CanRestart(src IPacketSource) bool { return src.IsFile() || src.IsInterface() } //====================================================================== type FileSource struct { Filename string } var _ IPacketSource = FileSource{} func (p FileSource) Name() string { return p.Filename } func (p FileSource) IsFile() bool { return true } func (p FileSource) IsInterface() bool { return false } func (p FileSource) IsFifo() bool { return false } func (p FileSource) IsPipe() bool { return false } func (p FileSource) String() string { return fmt.Sprintf("File:%s", p.Filename) } //====================================================================== type TemporaryFileSource struct { FileSource } type ISourceRemover interface { Remove() error } func (p TemporaryFileSource) Remove() error { return os.Remove(p.Filename) } func (p TemporaryFileSource) String() string { return fmt.Sprintf("TempFile:%s", p.Filename) } //====================================================================== type InterfaceSource struct { Iface string } var _ IPacketSource = InterfaceSource{} func (p InterfaceSource) Name() string { return p.Iface } func (p InterfaceSource) IsFile() bool { return false } func (p InterfaceSource) IsInterface() bool { return true } func (p InterfaceSource) IsFifo() bool { return false } func (p InterfaceSource) IsPipe() bool { return false } func (p InterfaceSource) String() string { return fmt.Sprintf("Interface:%s", p.Iface) } //====================================================================== type FifoSource struct { Filename string } var _ IPacketSource = FifoSource{} func (p FifoSource) Name() string { return p.Filename } func (p FifoSource) IsFile() bool { return false } func (p FifoSource) IsInterface() bool { return false } func (p FifoSource) IsFifo() bool { return true } func (p FifoSource) IsPipe() bool { return false } func (p FifoSource) String() string { return fmt.Sprintf("Fifo:%s", p.Filename) } //====================================================================== type PipeSource struct { Descriptor string Fd int } var _ IPacketSource = PipeSource{} func (p PipeSource) Name() string { return p.Descriptor } func (p PipeSource) IsFile() bool { return false } func (p PipeSource) IsInterface() bool { return false } func (p PipeSource) IsFifo() bool { return false } func (p PipeSource) IsPipe() bool { return true } func (p PipeSource) Close() error { system.CloseDescriptor(p.Fd) return nil } func (p PipeSource) String() string { return fmt.Sprintf("Pipe:%s(%d)", p.Descriptor, p.Fd) } //====================================================================== // Local Variables: // mode: Go // fill-column: 78 // End: termshark-2.2.0/pcap/testdata/000077500000000000000000000000001377442047300162625ustar00rootroot00000000000000termshark-2.2.0/pcap/testdata/1.hexdump000066400000000000000000000234051377442047300200220ustar00rootroot000000000000000000 30 fd 38 d2 76 12 e8 de 27 19 de 6c 08 00 45 00 0.8.v...'..l..E. 0010 00 73 7d ef 40 00 40 06 8a 64 c0 a8 56 f6 34 14 .s}.@.@..d..V.4. 0020 e6 7e a4 c8 01 bb 3e ad 82 88 17 e6 8f 97 80 18 .~....>......... 0030 01 3f 7d 3b 00 00 01 01 08 0a 0c 5d 30 7f 3f 24 .?};.......]0.?$ 0040 4a 9b 17 03 03 00 3a 00 00 00 00 00 00 01 8c 43 J.....:........C 0050 07 f0 2f f9 b2 7b 87 1a a3 39 94 c2 2e a0 3b 79 ../..{...9....;y 0060 76 87 b5 67 c1 c0 aa d4 42 59 18 e6 9f f7 92 ca v..g....BY...... 0070 ad 7d e9 30 eb e4 fe 6d fe 1a f6 b9 4c 59 af 64 .}.0...m....LY.d 0080 64 d 0000 e8 de 27 19 de 6c 30 fd 38 d2 76 12 08 00 45 00 ..'..l0.8.v...E. 0010 00 59 23 f9 40 00 ef 06 35 74 34 14 e6 7e c0 a8 .Y#.@...5t4..~.. 0020 56 f6 01 bb a4 c8 17 e6 8f 97 3e ad 82 c7 80 18 V.........>..... 0030 00 75 71 4a 00 00 01 01 08 0a 3f 24 54 60 0c 5d .uqJ......?$T`.] 0040 30 7f 17 03 03 00 20 00 00 00 00 00 00 01 8c e3 0..... ......... 0050 85 15 a7 9d ac aa 2d 70 e8 86 63 ec 68 c4 5a 15 ......-p..c.h.Z. 0060 d8 39 73 e6 e1 a4 e8 .9s.... 0000 30 fd 38 d2 76 12 e8 de 27 19 de 6c 08 00 45 00 0.8.v...'..l..E. 0010 00 34 7d f0 40 00 40 06 8a a2 c0 a8 56 f6 34 14 .4}.@.@.....V.4. 0020 e6 7e a4 c8 01 bb 3e ad 82 c7 17 e6 8f bc 80 10 .~....>......... 0030 01 3f 63 42 00 00 01 01 08 0a 0c 5d 30 8e 3f 24 .?cB.......]0.?$ 0040 54 60 T` 0000 30 fd 38 d2 76 12 e8 de 27 19 de 6c 08 00 45 00 0.8.v...'..l..E. 0010 00 53 23 16 40 00 40 06 9e ab c0 a8 56 f6 1f 0d .S#.@.@.....V... 0020 42 38 a2 ea 01 bb 9b 73 44 01 3f cd d0 cb 80 18 B8.....sD.?..... 0030 04 86 34 9e 00 00 01 01 08 0a 7f 84 4e 30 90 2e ..4.........N0.. 0040 e5 24 17 03 03 00 1a 51 02 04 20 f7 1d 99 45 8a .$.....Q.. ...E. 0050 99 3d a0 86 74 df f5 b2 6e dc 16 a4 56 54 11 34 .=..t...n...VT.4 0060 a1 . 0000 e8 de 27 19 de 6c 30 fd 38 d2 76 12 08 00 45 00 ..'..l0.8.v...E. 0010 00 5a 07 d4 40 00 58 06 a1 e6 1f 0d 42 38 c0 a8 .Z..@.X.....B8.. 0020 56 f6 01 bb a2 ea 3f cd d0 cb 9b 73 44 20 80 18 V.....?....sD .. 0030 00 c1 5d ea 00 00 01 01 08 0a 90 2f 39 1e 7f 84 ..]......../9... 0040 4e 30 17 03 03 00 21 3a 3f 24 21 3e 3a 54 25 3c N0....!:?$!>:T%< 0050 fe af 72 f2 61 fb c4 a3 c4 19 1e e5 cc 25 96 e2 ..r.a........%.. 0060 7d 36 8b ef 94 f6 fb 95 }6...... 0000 30 fd 38 d2 76 12 e8 de 27 19 de 6c 08 00 45 00 0.8.v...'..l..E. 0010 00 34 23 17 40 00 40 06 9e c9 c0 a8 56 f6 1f 0d .4#.@.@.....V... 0020 42 38 a2 ea 01 bb 9b 73 44 20 3f cd d0 f1 80 10 B8.....sD ?..... 0030 04 86 cd 34 00 00 01 01 08 0a 7f 84 4e 54 90 2f ...4........NT./ 0040 39 1e 9. 0000 01 00 5e 7f ff fa e8 de 27 19 de 6c 08 00 45 00 ..^.....'..l..E. 0010 00 81 79 35 40 00 01 11 f8 9d c0 a8 56 f6 ef ff ..y5@.......V... 0020 ff fa c1 75 07 6c 00 6d 3b 89 4d 2d 53 45 41 52 ...u.l.m;.M-SEAR 0030 43 48 20 2a 20 48 54 54 50 2f 31 2e 31 0d 0a 48 CH * HTTP/1.1..H 0040 4f 53 54 3a 20 32 33 39 2e 32 35 35 2e 32 35 35 OST: 239.255.255 0050 2e 32 35 30 3a 31 39 30 30 0d 0a 4d 41 4e 3a 20 .250:1900..MAN: 0060 22 73 73 64 70 3a 64 69 73 63 6f 76 65 72 22 0d "ssdp:discover". 0070 0a 53 54 3a 20 75 70 6e 70 3a 72 6f 6f 74 64 65 .ST: upnp:rootde 0080 76 69 63 65 0d 0a 4d 58 3a 20 35 0d 0a 0d 0a vice..MX: 5.... 0000 01 00 5e 7f ff fa 28 b2 bd 04 5c 7c 08 00 45 00 ..^...(...\|..E. 0010 00 81 da 35 40 00 01 11 98 48 c0 a8 56 4b ef ff ...5@....H..VK.. 0020 ff fa df 09 07 6c 00 6d 1e a0 4d 2d 53 45 41 52 .....l.m..M-SEAR 0030 43 48 20 2a 20 48 54 54 50 2f 31 2e 31 0d 0a 48 CH * HTTP/1.1..H 0040 4f 53 54 3a 20 32 33 39 2e 32 35 35 2e 32 35 35 OST: 239.255.255 0050 2e 32 35 30 3a 31 39 30 30 0d 0a 4d 41 4e 3a 20 .250:1900..MAN: 0060 22 73 73 64 70 3a 64 69 73 63 6f 76 65 72 22 0d "ssdp:discover". 0070 0a 53 54 3a 20 75 70 6e 70 3a 72 6f 6f 74 64 65 .ST: upnp:rootde 0080 76 69 63 65 0d 0a 4d 58 3a 20 35 0d 0a 0d 0a vice..MX: 5.... 0000 e8 de 27 19 de 6c 30 fd 38 d2 76 12 08 00 45 00 ..'..l0.8.v...E. 0010 00 56 f4 3b 40 00 38 06 af f9 ac 68 da 65 c0 a8 .V.;@.8....h.e.. 0020 56 f6 01 bb ad d8 12 45 09 4e 49 30 76 b2 80 18 V......E.NI0v... 0030 00 13 c9 19 00 00 01 01 08 0a 8b c9 4b 15 4e a9 ............K.N. 0040 85 74 17 03 03 00 1d 3a ab 21 78 55 55 d8 97 dc .t.....:.!xUU... 0050 87 fd 4d a6 5d bf 4e cd 3b 67 dc 44 eb b5 93 a2 ..M.].N.;g.D.... 0060 cc d4 aa 80 .... 0000 30 fd 38 d2 76 12 e8 de 27 19 de 6c 08 00 45 00 0.8.v...'..l..E. 0010 00 34 09 f6 40 00 40 06 92 61 c0 a8 56 f6 ac 68 .4..@.@..a..V..h 0020 da 65 ad d8 01 bb 49 30 76 b2 12 45 09 70 80 10 .e....I0v..E.p.. 0030 01 3f 7a 10 00 00 01 01 08 0a 4e a9 ac 4d 8b c9 .?z.......N..M.. 0040 4b 15 K. 0000 e8 de 27 19 de 6c 30 fd 38 d2 76 12 08 00 45 00 ..'..l0.8.v...E. 0010 01 74 71 6b 40 00 40 11 99 c5 c0 a8 56 01 c0 a8 .tqk@.@.....V... 0020 56 f6 07 6c c1 75 01 60 c9 bf 48 54 54 50 2f 31 V..l.u.`..HTTP/1 0030 2e 31 20 32 30 30 20 4f 4b 0d 0a 43 41 43 48 45 .1 200 OK..CACHE 0040 2d 43 4f 4e 54 52 4f 4c 3a 20 6d 61 78 2d 61 67 -CONTROL: max-ag 0050 65 3d 31 32 30 0d 0a 53 54 3a 20 75 70 6e 70 3a e=120..ST: upnp: 0060 72 6f 6f 74 64 65 76 69 63 65 0d 0a 55 53 4e 3a rootdevice..USN: 0070 20 75 75 69 64 3a 36 35 39 62 35 35 64 66 2d 64 uuid:659b55df-d 0080 65 35 38 2d 34 30 64 65 2d 39 37 36 61 2d 64 36 e58-40de-976a-d6 0090 61 36 38 32 62 38 65 33 37 63 3a 3a 75 70 6e 70 a682b8e37c::upnp 00a0 3a 72 6f 6f 74 64 65 76 69 63 65 0d 0a 45 58 54 :rootdevice..EXT 00b0 3a 0d 0a 53 45 52 56 45 52 3a 20 63 68 72 6f 6d :..SERVER: chrom 00c0 69 75 6d 6f 73 2f 72 6f 6c 6c 69 6e 67 20 55 50 iumos/rolling UP 00d0 6e 50 2f 31 2e 31 20 4d 69 6e 69 55 50 6e 50 64 nP/1.1 MiniUPnPd 00e0 2f 31 2e 39 0d 0a 4c 4f 43 41 54 49 4f 4e 3a 20 /1.9..LOCATION: 00f0 68 74 74 70 3a 2f 2f 31 39 32 2e 31 36 38 2e 38 http://192.168.8 0100 36 2e 31 3a 35 30 30 30 2f 72 6f 6f 74 44 65 73 6.1:5000/rootDes 0110 63 2e 78 6d 6c 0d 0a 4f 50 54 3a 20 22 68 74 74 c.xml..OPT: "htt 0120 70 3a 2f 2f 73 63 68 65 6d 61 73 2e 75 70 6e 70 p://schemas.upnp 0130 2e 6f 72 67 2f 75 70 6e 70 2f 31 2f 30 2f 22 3b .org/upnp/1/0/"; 0140 20 6e 73 3d 30 31 0d 0a 30 31 2d 4e 4c 53 3a 20 ns=01..01-NLS: 0150 31 0d 0a 42 4f 4f 54 49 44 2e 55 50 4e 50 2e 4f 1..BOOTID.UPNP.O 0160 52 47 3a 20 31 0d 0a 43 4f 4e 46 49 47 49 44 2e RG: 1..CONFIGID. 0170 55 50 4e 50 2e 4f 52 47 3a 20 31 33 33 37 0d 0a UPNP.ORG: 1337.. 0180 0d 0a .. 0000 38 8b 59 1f a9 a3 e8 de 27 19 de 6c 08 00 45 00 8.Y.....'..l..E. 0010 00 a9 24 1b 40 00 40 06 e7 d6 c0 a8 56 f6 c0 a8 ..$.@.@.....V... 0020 56 16 94 dc 1f 49 da 81 c8 98 8d 30 da 0f 80 18 V....I.....0.... 0030 01 67 88 b9 00 00 01 01 08 0a 55 f7 3c 8a 00 69 .g........U.<..i 0040 65 57 17 03 03 00 70 00 00 00 00 00 00 03 15 56 eW....p........V 0050 f6 bc 8d 80 4b 21 58 8b 2a 50 a3 f5 ec 78 14 38 ....K!X.*P...x.8 0060 7f da fd 34 2e ff 8e 10 d3 be 0b a4 3f 9e 2d 45 ...4........?.-E 0070 fd 71 e7 dd ea 28 c0 c0 27 87 2a 36 45 e6 00 d0 .q...(..'.*6E... 0080 e8 27 6c 04 d3 3d 7a 5f ff bb 49 3a 3b 92 33 0b .'l..=z_..I:;.3. 0090 e3 e7 87 6e 7c ab a1 72 fe 19 54 49 db 24 12 58 ...n|..r..TI.$.X 00a0 ea 0d f6 4e e1 11 76 21 00 82 4d 66 51 c7 74 9b ...N..v!..MfQ.t. 00b0 34 70 b2 8f e7 21 85 4p...!. 0000 38 8b 59 1f a9 a3 e8 de 27 19 de 6c 08 00 45 00 8.Y.....'..l..E. 0010 00 34 24 1c 40 00 40 06 e8 4a c0 a8 56 f6 c0 a8 .4$.@.@..J..V... 0020 56 16 94 dc 1f 49 da 81 c9 0d 8d 30 da 84 80 10 V....I.....0.... 0030 01 67 8d 46 00 00 01 01 08 0a 55 f7 3c 9b 00 69 .g.F......U.<..i 0040 67 4c gL 0000 e8 de 27 19 de 6c 30 fd 38 d2 76 12 08 06 00 01 ..'..l0.8.v..... 0010 08 00 06 04 00 01 30 fd 38 d2 76 12 c0 a8 56 01 ......0.8.v...V. 0020 00 00 00 00 00 00 c0 a8 56 f6 ........V. 0000 30 fd 38 d2 76 12 e8 de 27 19 de 6c 08 06 00 01 0.8.v...'..l.... 0010 08 00 06 04 00 02 e8 de 27 19 de 6c c0 a8 56 f6 ........'..l..V. 0020 30 fd 38 d2 76 12 c0 a8 56 01 0.8.v...V. 0000 30 fd 38 d2 76 12 e8 de 27 19 de 6c 08 00 45 00 0.8.v...'..l..E. 0010 00 70 92 a6 40 00 40 06 15 6c c0 a8 56 f6 23 ae .p..@.@..l..V.#. 0020 57 29 d2 44 01 bb f0 9f 15 b2 61 2b 5b 37 80 18 W).D......a+[7.. 0030 01 12 dc 9d 00 00 01 01 08 0a ba b7 5d 1b 11 87 ............]... 0040 12 ad 17 03 03 00 37 00 00 00 00 00 00 00 6b 04 ......7.......k. 0050 92 2c 7d 6f 27 61 dd 20 7b 47 cc 5c fe 00 ea 9c .,}o'a. {G.\.... 0060 2f 62 0a 18 6e 66 10 92 95 57 e9 d1 2a bc fa a9 /b..nf...W..*... 0070 d9 6c 14 cc bd 58 3a c0 d3 b0 7d 46 9e e4 .l...X:...}F.. 0000 e8 de 27 19 de 6c 30 fd 38 d2 76 12 08 00 45 00 ..'..l0.8.v...E. 0010 00 6a 55 56 40 00 ed 06 a5 c1 23 ae 57 29 c0 a8 .jUV@.....#.W).. 0020 56 f6 01 bb d2 44 61 2b 5b 37 f0 9f 15 ee 80 18 V....Da+[7...... 0030 00 84 e5 59 00 00 01 01 08 0a 11 87 1c 26 ba b7 ...Y.........&.. 0040 5d 1b 17 03 03 00 31 00 00 00 00 00 00 00 6b 3d ].....1.......k= 0050 4b 5b a4 2a 4f 6c 52 5e 76 bd 9e ba 90 2b 25 59 K[.*OlR^v....+%Y 0060 0f b8 5f e7 73 2c 70 af c1 0e 26 bc 4d 1d d0 ac .._.s,p...&.M... 0070 71 5c 0b f0 60 78 d8 57 q\..`x.W 0000 30 fd 38 d2 76 12 e8 de 27 19 de 6c 08 00 45 00 0.8.v...'..l..E. 0010 00 34 92 a7 40 00 40 06 15 a7 c0 a8 56 f6 23 ae .4..@.@.....V.#. 0020 57 29 d2 44 01 bb f0 9f 15 ee 61 2b 5b 6d 80 10 W).D......a+[m.. 0030 01 12 06 4f 00 00 01 01 08 0a ba b7 5d 5b 11 87 ...O........][.. 0040 1c 26 .& termshark-2.2.0/pcap/testdata/1.pcap000066400000000000000000000044701377442047300172740ustar00rootroot00000000000000òp\08v'lEs}@@dV4~>揗?}; ]0?$J:C/{9.;yvgBYʭ}0mLYddp\>gg'l08vEY#@5t4~V揗>ǀuqJ ?$T` ]0 -pchZ9s蕼p\bBB08v'lE4}@@V4~>揼?cB ]0?$T`p\ aa08v'lES#@@V B8sD?ˀ4 N0.$Q E=tnVT4p\3Y hh'l08vEZ@X B8V?˛sD ] /9N0!:?$!>:T% termshark-2.2.0/pcap/testdata/1.psml000066400000000000000000000112761377442047300173260ustar00rootroot00000000000000
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
termshark-2.2.0/pcap/testdata/2.hexdump-body000066400000000000000000000005531377442047300207550ustar00rootroot000000000000000000 10 40 00 20 35 01 2b 59 00 06 29 17 93 f8 aa aa .@. 5.+Y..)..... 0010 03 00 00 00 08 00 45 00 00 37 f9 39 00 00 40 11 ......E..7.9..@. 0020 a6 db c0 a8 2c 7b c0 a8 2c d5 f9 39 00 45 00 23 ....,{..,..9.E.# 0030 8d 73 00 01 43 3a 5c 49 42 4d 54 43 50 49 50 5c .s..C:\IBMTCPIP\ 0040 6c 63 63 6d 2e 31 00 6f 63 74 65 74 00 lccm.1.octet. termshark-2.2.0/pcap/testdata/2.hexdump-footer000066400000000000000000000000001377442047300213010ustar00rootroot00000000000000termshark-2.2.0/pcap/testdata/2.hexdump-header000066400000000000000000000000001377442047300212330ustar00rootroot00000000000000termshark-2.2.0/pcap/testdata/2.pcap-body000066400000000000000000000001351377442047300202220ustar00rootroot00000000000000*9RMM@ 5+Y)E79@,{,9E#sC:\IBMTCPIP\lccm.1octettermshark-2.2.0/pcap/testdata/2.pcap-footer000066400000000000000000000000001377442047300205520ustar00rootroot00000000000000termshark-2.2.0/pcap/testdata/2.pcap-header000066400000000000000000000000301377442047300205070ustar00rootroot00000000000000òtermshark-2.2.0/pcap/testdata/2.pdml-body000066400000000000000000000252631377442047300202440ustar00rootroot00000000000000 termshark-2.2.0/pcap/testdata/2.pdml-footer000066400000000000000000000000121377442047300205660ustar00rootroot00000000000000 termshark-2.2.0/pcap/testdata/2.pdml-header000066400000000000000000000005511377442047300205300ustar00rootroot00000000000000 termshark-2.2.0/pcap/testdata/2.psml-body000066400000000000000000000004061377442047300202530ustar00rootroot00000000000000
1
0.000000
192.168.44.123
192.168.44.213
TFTP
77
Read Request, File: C:\IBMTCPIP\lccm.1, Transfer type: octet
termshark-2.2.0/pcap/testdata/2.psml-footer000066400000000000000000000000111377442047300206040ustar00rootroot00000000000000 termshark-2.2.0/pcap/testdata/2.psml-header000066400000000000000000000004441377442047300205500ustar00rootroot00000000000000
No.
Time
Source
Destination
Protocol
Length
Info
termshark-2.2.0/pdmltree/000077500000000000000000000000001377442047300153425ustar00rootroot00000000000000termshark-2.2.0/pdmltree/pdmltree.go000066400000000000000000000202451377442047300175100ustar00rootroot00000000000000// Copyright 2019-2021 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 contains a type used as the model for a PDML document for a // packet, and associated functions. package pdmltree import ( "bytes" "encoding/xml" "fmt" "reflect" "strconv" "strings" "github.com/antchfx/xmlquery" "github.com/gcla/gowid" "github.com/gcla/gowid/gwutil" "github.com/gcla/gowid/widgets/tree" "github.com/gcla/termshark/v2/widgets/hexdumper2" "github.com/pkg/errors" log "github.com/sirupsen/logrus" ) //====================================================================== type ExpandedPaths [][]string 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] } // Model is a struct model of the PDML proto or field element. type Model struct { UiName string `xml:"-"` Name string `xml:"-"` // needed for stripping geninfo 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:"-"` QueryModel *xmlquery.Node `xml:"-"` Parent *Model `xml:"-"` ExpandedFields *ExpandedPaths `xml:"-"` } var _ tree.IModel = (*Model)(nil) var _ tree.ICollapsible = (*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) []hexdumper2.LayerStyler { res := make([]hexdumper2.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, hexdumper2.LayerStyler{ Start: c.Pos, End: c.Pos + c.Size, ColUnselected: "hex-layer-unselected", ColSelected: "hex-layer-selected", }) for _, c2 := range c.Children_ { if c2.Pos <= pos && pos < c2.Pos+c2.Size { res = append(res, hexdumper2.LayerStyler{ Start: c2.Pos, End: c2.Pos + c2.Size, ColUnselected: "hex-field-unselected", ColSelected: "hex-field-selected", }) } } } } return res } // Implement xml.Unmarshaler. Create a Model struct by unmarshaling the // provided XML. Takes special action before deferring to DecodeElement. 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 } // Make a *Model from the slice of bytes, and expand nodes according // to the map parameter. 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() // Have to make this here because this is when I have access to the data... n.QueryModel, err = xmlquery.Parse(strings.NewReader(string(data))) if err != nil { log.Error(err) } return tr } func (p *Model) TCPStreamIndex() gwutil.IntOption { return p.streamIndex("tcp") } func (p *Model) UDPStreamIndex() gwutil.IntOption { return p.streamIndex("udp") } // Return None if not TCP func (p *Model) streamIndex(proto string) gwutil.IntOption { var res gwutil.IntOption if showNode := xmlquery.FindOne(p.QueryModel, fmt.Sprintf("//field[@name='%s.stream']/@show", proto)); showNode != nil { idx, err := strconv.Atoi(showNode.InnerText()) if err != nil { log.Warnf("Unexpected %s node innertext value %s", proto, showNode.InnerText()) } else { res = gwutil.SomeInt(idx) } } return res } func (p *Model) ApplyExpandedPaths(exp *ExpandedPaths) { if exp != nil { p.makeParentLinks(exp) // TODO - fixup p.expandAllPaths(*exp) } } func (p *Model) expandAllPaths(exp ExpandedPaths) { for _, path := range exp { // path is [udp, udp.srcport,...] p.expandByPath(path) } } func (p *Model) expandByPath(path []string) { if len(path) == 0 { return } p2 := path[0] if p.Name == p2 { subpath := path[1:] if len(subpath) == 0 { // Only explicitly expand the leaf - the paths must include // a path ending at each node along the way for a complete path // expansion. This lets us collapse root nodes and preserve the // state of inner nodes p.Expanded = true } else { for _, ch := range p.Children_ { ch.expandByPath(subpath) } } } } func (p *Model) makeParentLinks(exp *ExpandedPaths) { if p != nil { p.ExpandedFields = exp for _, ch := range p.Children_ { ch.Parent = p ch.makeParentLinks(exp) } } } 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) PathToRoot() []string { if p == nil { return []string{} } return append(p.Parent.PathToRoot(), p.Name) } func (p *Model) IsCollapsed() bool { return !p.Expanded } func (p *Model) SetCollapsed(app gowid.IApp, isCollapsed bool) { if isCollapsed { p.Expanded = false } else { p.Expanded = true } path := p.PathToRoot() if p.Expanded { // We need to add an expanded entry for [/], [/, tcp], [/, tcp, tcp.srcport] - because // expanding a node implicitly expands all parent nodes. But contracting an outer node // should leave the expanded state of inner nodes alone. for i := 0; i < len(path); i++ { p.ExpandedFields.addExpanded(path[0 : i+1]) } } else { p.ExpandedFields.removeExpanded(path) } } func (m *ExpandedPaths) addExpanded(path []string) bool { for _, p := range *m { if reflect.DeepEqual(p, path) { return false } } *m = append(*m, path) return true } func (m *ExpandedPaths) removeExpanded(path []string) bool { for i, p := range *m { if reflect.DeepEqual(p, path) { *m = append((*m)[:i], (*m)[i+1:]...) return true } } return false } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.2.0/pdmltree/pdmltree_test.go000066400000000000000000000705071377442047300205550ustar00rootroot00000000000000// Copyright 2019-2021 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) { dummy := make(ExpandedPaths, 0) tree := DecodePacket([]byte(p1)) tree.ApplyExpandedPaths(&dummy) assert.Equal(t, 8, len(tree.Children_)) assert.Equal(t, 13, len(tree.Children_[0].Children_)) } //====================================================================== // Local Variables: // mode: Go // fill-column: 78 // End: termshark-2.2.0/psmlmodel/000077500000000000000000000000001377442047300155225ustar00rootroot00000000000000termshark-2.2.0/psmlmodel/model.go000066400000000000000000000072101377442047300171510ustar00rootroot00000000000000// Copyright 2019-2021 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 psmlmodel 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/v2/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 + " ") 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: termshark-2.2.0/scripts/000077500000000000000000000000001377442047300152155ustar00rootroot00000000000000termshark-2.2.0/scripts/do-release.sh000077500000000000000000000005261377442047300175770ustar00rootroot00000000000000#!/usr/bin/env bash # For Travis, so that git describe gives something useful git fetch --tags . export TERMSHARK_GIT_DESCRIBE="$(git describe --tags HEAD)" curl -sL https://git.io/goreleaser > /tmp/goreleaser.sh # testing bash /tmp/goreleaser.sh --snapshot --skip-sign --rm-dist # release # bash /tmp/goreleaser.sh --skip-sign --rm-dist termshark-2.2.0/scripts/pcaps/000077500000000000000000000000001377442047300163235ustar00rootroot00000000000000termshark-2.2.0/scripts/pcaps/telnet-cooked.pcap000066400000000000000000000220341377442047300217260ustar00rootroot00000000000000ò@8JJ;E}x '$%,@8]];EOF>@@sŠc>}xng '$%, !"'#@83EE;E7=@c>šCg %,'$%@84BB;E4F?@@s!šcA}x '3%,@84EE;E7F@@@sšcA}xȢ '3%,%@8 :BB;E4_@cAš C'@ %,'3@8@[[;EM:0@cAš C %,'3 !"" @8A;EtFA@@rš cZ}x '3%,P "bb B  "@8EBB;E4_@jcZšKC'' %,'3@8 "${PCAP}" d4c3b2a102000400 0000000000000000 0000040006000000 f32a395200000000 4d0000004d000000 1040002035012b59 0006291793f8aaaa 0300000008004500 0037f93900004011 a6dbc0a82c7bc0a8 2cd5f93900450023 8d730001433a5c49 424d54435049505c 6c63636d2e31006f 6374657400f32a39 52000000004d0000 004d000000104000 2035012b59000629 1793f8aaaa030000 00080045000037f9 3900004011a6dbc0 a82c7bc0a82cd5f9 39004500238d7300 01433a5c49424d54 435049505c6c6363 6d2e31006f637465 7400 EOF echo Running termshark cli tests. # if timeout is invoked because termshark is stuck, the exit code will be non-zero export TS="$GOPATH/bin/termshark" # stdout is not a tty, so falls back to tshark $TS -r "${PCAP}" | grep '192.168.44.213 TFTP 77' # prove that options provided are passed through to tshark [[ $($TS -r "${PCAP}" -T psml -n | grep '' | wc -l) == 2 ]] # Must choose either a file or an interface ! $TS -r "${PCAP}" -i eth0 # only display the second line via tshark [[ $($TS -r "${PCAP}" 'frame.number == 2' | wc -l) == 1 ]] # test fifos mkfifo "${FIFO}" cat "${PCAP}" > "${FIFO}" & $TS -r "${FIFO}" | grep '192.168.44.213 TFTP 77' wait rm "${FIFO}" # Check pass-thru option works. Make termshark run in a tty to ensure it's taking effect [[ $(script -q -e -c "$TS -r "${PCAP}" --pass-thru" | wc -l) == 2 ]] [[ $(script -q -e -c "$TS -r "${PCAP}" --pass-thru=true" | wc -l) == 2 ]] # run in script so termshark thinks it's in a tty cat version.go | grep -o -E "v[0-9]+\.[0-9]+(\.[0-9]+)?" | \ xargs -i bash -c "script -q -e -c \"$TS -v\" | grep {}" echo Running termshark UI tests. in_tty() { ARGS=$@ # make into one token socat - EXEC:"bash -c \\\"stty rows 50 cols 80 && TERM=xterm && $ARGS\\\"",pty,setsid,ctty } wait_for_load() { rm ~/.cache/termshark/termshark.log > /dev/null 2>&1 tail -F ~/.cache/termshark/termshark.log 2> /dev/null | while [ 1 ] ; do read ; echo Log: $REPLY 1>&2 ; grep "Load operation complete" <<<$REPLY && break ; done } echo UI test 1 # Load a pcap, quit { wait_for_load ; sleep 0.5s ; echo q ; sleep 0.5s ; echo ; } | in_tty $TS -r "${PCAP}" > /dev/null echo Tests disabled for now until I understand whats going on with Travis... exit 0 echo UI test 2 # Run with stdout not a tty, but disable the pass-thru to tshark { wait_for_load ; sleep 0.5s ; echo q ; sleep 0.5s ; echo ; } | in_tty "$TS -r "${PCAP}" --pass-thru=false | cat" > /dev/null echo UI test 3 # Load a pcap, very rudimentary scrape for an IP, quit { wait_for_load ; sleep 0.5s ; echo q ; sleep 0.5s ; echo ; } | in_tty "$TS -r "${PCAP}"" | grep -a 192.168.44.123 > /dev/null # Ensure -r flag isn't needed { wait_for_load ; sleep 0.5s ; echo q ; sleep 0.5s ; echo ; } | in_tty "$TS "${PCAP}"" | grep -a 192.168.44.123 > /dev/null echo UI test 4 # Load a pcap from stdin { wait_for_load ; sleep 0.5s ; echo q ; sleep 0.5s ; echo ; } | in_tty "cat "${PCAP}" | TERM=xterm $TS -i -" > /dev/null { wait_for_load ; sleep 0.5s ; echo q ; sleep 0.5s ; echo ; } | in_tty "cat "${PCAP}" | TERM=xterm $TS -r -" > /dev/null { wait_for_load ; sleep 0.5s ; echo q ; sleep 0.5s ; echo ; } | in_tty "cat "${PCAP}" | TERM=xterm $TS" > /dev/null echo UI test 5 # Display filter at end of command line { wait_for_load ; sleep 0.5s ; echo q ; sleep 0.5s ; echo ; } | in_tty "$TS -r scripts/pcaps/telnet-cooked.pcap \'frame.number == 2\'" | grep -a "Frame 2: 74 bytes" > /dev/null echo UI test 6 mkfifo "${FIFO}" cat "${PCAP}" > "${FIFO}" & { wait_for_load ; sleep 0.5s ; echo q ; sleep 0.5s ; echo ; } | in_tty "$TS -r "${FIFO}"" > /dev/null wait cat "${PCAP}" > "${FIFO}" & { wait_for_load ; sleep 0.5s ; echo q ; sleep 0.5s ; echo ; } | in_tty "$TS -i "${FIFO}"" > /dev/null wait cat "${PCAP}" > "${FIFO}" & { wait_for_load ; sleep 0.5s ; echo q ; sleep 0.5s ; echo ; } | in_tty "$TS "${FIFO}"" > /dev/null #{ sleep 5s ; echo q ; echo ; } | in_tty "$TS "${FIFO}" \'frame.number == 2\'" | grep -a "Frame 2: 74 bytes" > /dev/null wait echo Tests were successful. termshark-2.2.0/streams/000077500000000000000000000000001377442047300152045ustar00rootroot00000000000000termshark-2.2.0/streams/follow.go000066400000000000000000001562171377442047300170510ustar00rootroot00000000000000// Code generated by pigeon; DO NOT EDIT. package streams import ( "bytes" "errors" "fmt" "io" "io/ioutil" "math" "os" "sort" "strconv" "strings" "sync" "unicode" "unicode/utf8" ) var g = &grammar{ rules: []*rule{ { name: "Input", pos: position{line: 45, col: 1, offset: 1065}, expr: &actionExpr{ pos: position{line: 45, col: 10, offset: 1074}, run: (*parser).callonInput1, expr: &seqExpr{ pos: position{line: 45, col: 10, offset: 1074}, exprs: []interface{}{ &ruleRefExpr{ pos: position{line: 45, col: 10, offset: 1074}, name: "_", }, &ruleRefExpr{ pos: position{line: 45, col: 12, offset: 1076}, name: "Separator", }, &labeledExpr{ pos: position{line: 45, col: 22, offset: 1086}, label: "follow", expr: &ruleRefExpr{ pos: position{line: 45, col: 29, offset: 1093}, name: "Follow", }, }, &ruleRefExpr{ pos: position{line: 45, col: 36, offset: 1100}, name: "Separator", }, &ruleRefExpr{ pos: position{line: 45, col: 46, offset: 1110}, name: "EOF", }, }, }, }, }, { name: "Separator", pos: position{line: 49, col: 1, offset: 1141}, expr: &seqExpr{ pos: position{line: 49, col: 14, offset: 1154}, exprs: []interface{}{ &oneOrMoreExpr{ pos: position{line: 49, col: 14, offset: 1154}, expr: &litMatcher{ pos: position{line: 49, col: 14, offset: 1154}, val: "=", ignoreCase: false, }, }, &ruleRefExpr{ pos: position{line: 49, col: 19, offset: 1159}, name: "__", }, }, }, }, { name: "DataLines", pos: position{line: 55, col: 1, offset: 1342}, expr: &actionExpr{ pos: position{line: 55, col: 14, offset: 1355}, run: (*parser).callonDataLines1, expr: &labeledExpr{ pos: position{line: 55, col: 14, offset: 1355}, label: "dl", expr: &choiceExpr{ pos: position{line: 55, col: 19, offset: 1360}, alternatives: []interface{}{ &ruleRefExpr{ pos: position{line: 55, col: 19, offset: 1360}, name: "DataLineBackComplete", }, &ruleRefExpr{ pos: position{line: 55, col: 42, offset: 1383}, name: "DataLineForwardComplete", }, }, }, }, }, }, { name: "ThrowUndefLabel", pos: position{line: 68, col: 1, offset: 1618}, expr: &throwExpr{ pos: position{line: 68, col: 19, offset: 1636}, label: "undeflabel", }, }, { name: "DataLineBackComplete", pos: position{line: 70, col: 1, offset: 1651}, expr: &actionExpr{ pos: position{line: 70, col: 25, offset: 1675}, run: (*parser).callonDataLineBackComplete1, expr: &labeledExpr{ pos: position{line: 70, col: 25, offset: 1675}, label: "dl", expr: &ruleRefExpr{ pos: position{line: 70, col: 28, offset: 1678}, name: "DataLineBack", }, }, }, }, { name: "DataLineForwardComplete", pos: position{line: 81, col: 1, offset: 1872}, expr: &actionExpr{ pos: position{line: 81, col: 28, offset: 1899}, run: (*parser).callonDataLineForwardComplete1, expr: &labeledExpr{ pos: position{line: 81, col: 28, offset: 1899}, label: "dl", expr: &ruleRefExpr{ pos: position{line: 81, col: 31, offset: 1902}, name: "DataLineForward", }, }, }, }, { name: "DataLineBack", pos: position{line: 95, col: 1, offset: 2177}, expr: &actionExpr{ pos: position{line: 95, col: 17, offset: 2193}, run: (*parser).callonDataLineBack1, expr: &seqExpr{ pos: position{line: 95, col: 17, offset: 2193}, exprs: []interface{}{ &charClassMatcher{ pos: position{line: 95, col: 17, offset: 2193}, val: "[\\t]", chars: []rune{'\t'}, ignoreCase: false, inverted: false, }, &labeledExpr{ pos: position{line: 95, col: 22, offset: 2198}, label: "data", expr: &ruleRefExpr{ pos: position{line: 95, col: 27, offset: 2203}, name: "Data", }, }, }, }, }, }, { name: "DataLineForward", pos: position{line: 102, col: 1, offset: 2309}, expr: &actionExpr{ pos: position{line: 102, col: 20, offset: 2328}, run: (*parser).callonDataLineForward1, expr: &labeledExpr{ pos: position{line: 102, col: 20, offset: 2328}, label: "data", expr: &ruleRefExpr{ pos: position{line: 102, col: 25, offset: 2333}, name: "Data", }, }, }, }, { name: "Data", pos: position{line: 106, col: 1, offset: 2400}, expr: &actionExpr{ pos: position{line: 106, col: 9, offset: 2408}, run: (*parser).callonData1, expr: &seqExpr{ pos: position{line: 106, col: 9, offset: 2408}, exprs: []interface{}{ &labeledExpr{ pos: position{line: 106, col: 9, offset: 2408}, label: "ds", expr: &oneOrMoreExpr{ pos: position{line: 106, col: 13, offset: 2412}, expr: &ruleRefExpr{ pos: position{line: 106, col: 13, offset: 2412}, name: "Datum", }, }, }, &zeroOrOneExpr{ pos: position{line: 106, col: 21, offset: 2420}, expr: &charClassMatcher{ pos: position{line: 106, col: 21, offset: 2420}, val: "[\\r]", chars: []rune{'\r'}, ignoreCase: false, inverted: false, }, }, &charClassMatcher{ pos: position{line: 106, col: 27, offset: 2426}, val: "[\\n]", chars: []rune{'\n'}, ignoreCase: false, inverted: false, }, }, }, }, }, { name: "Datum", pos: position{line: 114, col: 1, offset: 2591}, expr: &actionExpr{ pos: position{line: 114, col: 10, offset: 2600}, run: (*parser).callonDatum1, expr: &seqExpr{ pos: position{line: 114, col: 10, offset: 2600}, exprs: []interface{}{ &labeledExpr{ pos: position{line: 114, col: 10, offset: 2600}, label: "b1", expr: &charClassMatcher{ pos: position{line: 114, col: 13, offset: 2603}, val: "[0-9a-fA-F]", ranges: []rune{'0', '9', 'a', 'f', 'A', 'F'}, ignoreCase: false, inverted: false, }, }, &labeledExpr{ pos: position{line: 114, col: 25, offset: 2615}, label: "b2", expr: &charClassMatcher{ pos: position{line: 114, col: 28, offset: 2618}, val: "[0-9a-fA-F]", ranges: []rune{'0', '9', 'a', 'f', 'A', 'F'}, ignoreCase: false, inverted: false, }, }, }, }, }, }, { name: "DataIndex", pos: position{line: 121, col: 1, offset: 2821}, expr: &actionExpr{ pos: position{line: 121, col: 14, offset: 2834}, run: (*parser).callonDataIndex1, expr: &seqExpr{ pos: position{line: 121, col: 14, offset: 2834}, exprs: []interface{}{ &labeledExpr{ pos: position{line: 121, col: 14, offset: 2834}, label: "b0", expr: &charClassMatcher{ pos: position{line: 121, col: 17, offset: 2837}, val: "[0-9a-fA-F]", ranges: []rune{'0', '9', 'a', 'f', 'A', 'F'}, ignoreCase: false, inverted: false, }, }, &labeledExpr{ pos: position{line: 121, col: 29, offset: 2849}, label: "b1", expr: &charClassMatcher{ pos: position{line: 121, col: 32, offset: 2852}, val: "[0-9a-fA-F]", ranges: []rune{'0', '9', 'a', 'f', 'A', 'F'}, ignoreCase: false, inverted: false, }, }, &labeledExpr{ pos: position{line: 121, col: 44, offset: 2864}, label: "b2", expr: &charClassMatcher{ pos: position{line: 121, col: 47, offset: 2867}, val: "[0-9a-fA-F]", ranges: []rune{'0', '9', 'a', 'f', 'A', 'F'}, ignoreCase: false, inverted: false, }, }, &labeledExpr{ pos: position{line: 121, col: 59, offset: 2879}, label: "b3", expr: &charClassMatcher{ pos: position{line: 121, col: 62, offset: 2882}, val: "[0-9a-fA-F]", ranges: []rune{'0', '9', 'a', 'f', 'A', 'F'}, ignoreCase: false, inverted: false, }, }, &labeledExpr{ pos: position{line: 121, col: 74, offset: 2894}, label: "b4", expr: &charClassMatcher{ pos: position{line: 121, col: 77, offset: 2897}, val: "[0-9a-fA-F]", ranges: []rune{'0', '9', 'a', 'f', 'A', 'F'}, ignoreCase: false, inverted: false, }, }, &labeledExpr{ pos: position{line: 121, col: 89, offset: 2909}, label: "b5", expr: &charClassMatcher{ pos: position{line: 121, col: 92, offset: 2912}, val: "[0-9a-fA-F]", ranges: []rune{'0', '9', 'a', 'f', 'A', 'F'}, ignoreCase: false, inverted: false, }, }, &labeledExpr{ pos: position{line: 121, col: 104, offset: 2924}, label: "b6", expr: &charClassMatcher{ pos: position{line: 121, col: 107, offset: 2927}, val: "[0-9a-fA-F]", ranges: []rune{'0', '9', 'a', 'f', 'A', 'F'}, ignoreCase: false, inverted: false, }, }, &labeledExpr{ pos: position{line: 121, col: 119, offset: 2939}, label: "b7", expr: &charClassMatcher{ pos: position{line: 121, col: 122, offset: 2942}, val: "[0-9a-fA-F]", ranges: []rune{'0', '9', 'a', 'f', 'A', 'F'}, ignoreCase: false, inverted: false, }, }, }, }, }, }, { name: "DataSegment", pos: position{line: 134, col: 1, offset: 3618}, expr: &actionExpr{ pos: position{line: 134, col: 16, offset: 3633}, run: (*parser).callonDataSegment1, expr: &labeledExpr{ pos: position{line: 134, col: 16, offset: 3633}, label: "ds", expr: &oneOrMoreExpr{ pos: position{line: 134, col: 19, offset: 3636}, expr: &ruleRefExpr{ pos: position{line: 134, col: 21, offset: 3638}, name: "Datum", }, }, }, }, }, { name: "Node0Clause", pos: position{line: 143, col: 1, offset: 3821}, expr: &actionExpr{ pos: position{line: 143, col: 16, offset: 3836}, run: (*parser).callonNode0Clause1, expr: &seqExpr{ pos: position{line: 143, col: 16, offset: 3836}, exprs: []interface{}{ &litMatcher{ pos: position{line: 143, col: 16, offset: 3836}, val: "Node 0:", ignoreCase: false, }, &ruleRefExpr{ pos: position{line: 143, col: 26, offset: 3846}, name: "_", }, &labeledExpr{ pos: position{line: 143, col: 28, offset: 3848}, label: "node", expr: &ruleRefExpr{ pos: position{line: 143, col: 33, offset: 3853}, name: "NodeExpr", }, }, &ruleRefExpr{ pos: position{line: 143, col: 42, offset: 3862}, name: "__nl", }, }, }, }, }, { name: "Node1Clause", pos: position{line: 148, col: 1, offset: 3935}, expr: &actionExpr{ pos: position{line: 148, col: 16, offset: 3950}, run: (*parser).callonNode1Clause1, expr: &seqExpr{ pos: position{line: 148, col: 16, offset: 3950}, exprs: []interface{}{ &litMatcher{ pos: position{line: 148, col: 16, offset: 3950}, val: "Node 1:", ignoreCase: false, }, &ruleRefExpr{ pos: position{line: 148, col: 26, offset: 3960}, name: "_", }, &labeledExpr{ pos: position{line: 148, col: 28, offset: 3962}, label: "node", expr: &ruleRefExpr{ pos: position{line: 148, col: 33, offset: 3967}, name: "NodeExpr", }, }, &ruleRefExpr{ pos: position{line: 148, col: 42, offset: 3976}, name: "__nl", }, }, }, }, }, { name: "NodeExpr", pos: position{line: 153, col: 1, offset: 4024}, expr: &actionExpr{ pos: position{line: 153, col: 13, offset: 4036}, run: (*parser).callonNodeExpr1, expr: &oneOrMoreExpr{ pos: position{line: 153, col: 13, offset: 4036}, expr: &charClassMatcher{ pos: position{line: 153, col: 13, offset: 4036}, val: "[^\\n]", chars: []rune{'\n'}, ignoreCase: false, inverted: true, }, }, }, }, { name: "FollowClause", pos: position{line: 157, col: 1, offset: 4078}, expr: &actionExpr{ pos: position{line: 157, col: 17, offset: 4094}, run: (*parser).callonFollowClause1, expr: &seqExpr{ pos: position{line: 157, col: 17, offset: 4094}, exprs: []interface{}{ &litMatcher{ pos: position{line: 157, col: 17, offset: 4094}, val: "Follow:", ignoreCase: false, }, &ruleRefExpr{ pos: position{line: 157, col: 27, offset: 4104}, name: "_", }, &labeledExpr{ pos: position{line: 157, col: 29, offset: 4106}, label: "fexpr", expr: &ruleRefExpr{ pos: position{line: 157, col: 35, offset: 4112}, name: "FollowExpr", }, }, &ruleRefExpr{ pos: position{line: 157, col: 46, offset: 4123}, name: "__nl", }, }, }, }, }, { name: "FollowExpr", pos: position{line: 161, col: 1, offset: 4154}, expr: &actionExpr{ pos: position{line: 161, col: 15, offset: 4168}, run: (*parser).callonFollowExpr1, expr: &oneOrMoreExpr{ pos: position{line: 161, col: 15, offset: 4168}, expr: &charClassMatcher{ pos: position{line: 161, col: 15, offset: 4168}, val: "[a-zA-Z,]", chars: []rune{','}, ranges: []rune{'a', 'z', 'A', 'Z'}, ignoreCase: false, inverted: false, }, }, }, }, { name: "FilterClause", pos: position{line: 165, col: 1, offset: 4214}, expr: &actionExpr{ pos: position{line: 165, col: 17, offset: 4230}, run: (*parser).callonFilterClause1, expr: &seqExpr{ pos: position{line: 165, col: 17, offset: 4230}, exprs: []interface{}{ &litMatcher{ pos: position{line: 165, col: 17, offset: 4230}, val: "Filter:", ignoreCase: false, }, &ruleRefExpr{ pos: position{line: 165, col: 27, offset: 4240}, name: "_", }, &labeledExpr{ pos: position{line: 165, col: 29, offset: 4242}, label: "fexpr", expr: &ruleRefExpr{ pos: position{line: 165, col: 35, offset: 4248}, name: "FilterExpr", }, }, &ruleRefExpr{ pos: position{line: 165, col: 46, offset: 4259}, name: "__nl", }, }, }, }, }, { name: "FilterExpr", pos: position{line: 169, col: 1, offset: 4290}, expr: &actionExpr{ pos: position{line: 169, col: 15, offset: 4304}, run: (*parser).callonFilterExpr1, expr: &oneOrMoreExpr{ pos: position{line: 169, col: 15, offset: 4304}, expr: &charClassMatcher{ pos: position{line: 169, col: 15, offset: 4304}, val: "[^\\n]", chars: []rune{'\n'}, ignoreCase: false, inverted: true, }, }, }, }, { name: "Header", pos: position{line: 178, col: 1, offset: 4451}, expr: &actionExpr{ pos: position{line: 178, col: 11, offset: 4461}, run: (*parser).callonHeader1, expr: &seqExpr{ pos: position{line: 178, col: 11, offset: 4461}, exprs: []interface{}{ &labeledExpr{ pos: position{line: 178, col: 11, offset: 4461}, label: "fc", expr: &ruleRefExpr{ pos: position{line: 178, col: 14, offset: 4464}, name: "FollowClause", }, }, &labeledExpr{ pos: position{line: 178, col: 27, offset: 4477}, label: "fic", expr: &ruleRefExpr{ pos: position{line: 178, col: 31, offset: 4481}, name: "FilterClause", }, }, &labeledExpr{ pos: position{line: 178, col: 44, offset: 4494}, label: "node0", expr: &ruleRefExpr{ pos: position{line: 178, col: 50, offset: 4500}, name: "Node0Clause", }, }, &labeledExpr{ pos: position{line: 178, col: 62, offset: 4512}, label: "node1", expr: &ruleRefExpr{ pos: position{line: 178, col: 68, offset: 4518}, name: "Node1Clause", }, }, }, }, }, }, { name: "Follow", pos: position{line: 208, col: 1, offset: 5268}, expr: &actionExpr{ pos: position{line: 208, col: 11, offset: 5278}, run: (*parser).callonFollow1, expr: &seqExpr{ pos: position{line: 208, col: 11, offset: 5278}, exprs: []interface{}{ &labeledExpr{ pos: position{line: 208, col: 11, offset: 5278}, label: "hdr", expr: &ruleRefExpr{ pos: position{line: 208, col: 15, offset: 5282}, name: "Header", }, }, &labeledExpr{ pos: position{line: 208, col: 22, offset: 5289}, label: "data", expr: &zeroOrMoreExpr{ pos: position{line: 208, col: 27, offset: 5294}, expr: &ruleRefExpr{ pos: position{line: 208, col: 27, offset: 5294}, name: "DataLines", }, }, }, }, }, }, }, { name: "_", displayName: "\"whitespace\"", pos: position{line: 219, col: 1, offset: 5544}, expr: &zeroOrMoreExpr{ pos: position{line: 219, col: 19, offset: 5562}, expr: &charClassMatcher{ pos: position{line: 219, col: 19, offset: 5562}, val: "[ \\n\\t\\r]", chars: []rune{' ', '\n', '\t', '\r'}, ignoreCase: false, inverted: false, }, }, }, { name: "__", displayName: "\"mandatory whitespace\"", pos: position{line: 221, col: 1, offset: 5574}, expr: &oneOrMoreExpr{ pos: position{line: 221, col: 30, offset: 5603}, expr: &charClassMatcher{ pos: position{line: 221, col: 30, offset: 5603}, val: "[ \\n\\t\\r]", chars: []rune{' ', '\n', '\t', '\r'}, ignoreCase: false, inverted: false, }, }, }, { name: "__nl", displayName: "\"whitespace and newline\"", pos: position{line: 223, col: 1, offset: 5615}, expr: &seqExpr{ pos: position{line: 223, col: 34, offset: 5648}, exprs: []interface{}{ &zeroOrMoreExpr{ pos: position{line: 223, col: 34, offset: 5648}, expr: &charClassMatcher{ pos: position{line: 223, col: 34, offset: 5648}, val: "[ \\t\\r]", chars: []rune{' ', '\t', '\r'}, ignoreCase: false, inverted: false, }, }, &charClassMatcher{ pos: position{line: 223, col: 43, offset: 5657}, val: "[\\n]", chars: []rune{'\n'}, ignoreCase: false, inverted: false, }, }, }, }, { name: "EOF", pos: position{line: 225, col: 1, offset: 5663}, expr: ¬Expr{ pos: position{line: 225, col: 8, offset: 5670}, expr: &anyMatcher{ line: 225, col: 9, offset: 5671, }, }, }, }, } func (c *current) onInput1(follow interface{}) (interface{}, error) { return follow, nil } func (p *parser) callonInput1() (interface{}, error) { stack := p.vstack[len(p.vstack)-1] _ = stack return p.cur.onInput1(stack["follow"]) } func (c *current) onDataLines1(dl interface{}) (interface{}, error) { if cb, ok := c.globalStore["callbacks"]; ok { if cb, ok := cb.(IOnStreamChunk); ok { cb.OnStreamChunk(dl.(Bytes)) } } return dl, nil } func (p *parser) callonDataLines1() (interface{}, error) { stack := p.vstack[len(p.vstack)-1] _ = stack return p.cur.onDataLines1(stack["dl"]) } func (c *current) onDataLineBackComplete1(dl interface{}) (interface{}, error) { if ctx, ok := c.globalStore["context"]; ok { ctx := ctx.(parseContext) if ctx.Err() != nil { panic(StreamParseError{}) } } return dl, nil } func (p *parser) callonDataLineBackComplete1() (interface{}, error) { stack := p.vstack[len(p.vstack)-1] _ = stack return p.cur.onDataLineBackComplete1(stack["dl"]) } func (c *current) onDataLineForwardComplete1(dl interface{}) (interface{}, error) { if ctx, ok := c.globalStore["context"]; ok { ctx := ctx.(parseContext) if ctx.Err() != nil { panic(StreamParseError{}) } } return dl, nil } func (p *parser) callonDataLineForwardComplete1() (interface{}, error) { stack := p.vstack[len(p.vstack)-1] _ = stack return p.cur.onDataLineForwardComplete1(stack["dl"]) } func (c *current) onDataLineBack1(data interface{}) (interface{}, error) { return Bytes{Dirn: Server, Data: data.([]byte)}, nil } func (p *parser) callonDataLineBack1() (interface{}, error) { stack := p.vstack[len(p.vstack)-1] _ = stack return p.cur.onDataLineBack1(stack["data"]) } func (c *current) onDataLineForward1(data interface{}) (interface{}, error) { return Bytes{Dirn: Client, Data: data.([]byte)}, nil } func (p *parser) callonDataLineForward1() (interface{}, error) { stack := p.vstack[len(p.vstack)-1] _ = stack return p.cur.onDataLineForward1(stack["data"]) } func (c *current) onData1(ds interface{}) (interface{}, error) { data := make([]byte, 0, len(ds.([]interface{}))) for _, l := range ds.([]interface{}) { data = append(data, l.(byte)) } return data, nil } func (p *parser) callonData1() (interface{}, error) { stack := p.vstack[len(p.vstack)-1] _ = stack return p.cur.onData1(stack["ds"]) } func (c *current) onDatum1(b1, b2 interface{}) (interface{}, error) { byte1, _ := strconv.ParseUint(string(b1.([]byte)[0]), 16, 4) byte2, _ := strconv.ParseUint(string(b2.([]byte)[0]), 16, 4) return byte(byte1<<4 | byte2), nil } func (p *parser) callonDatum1() (interface{}, error) { stack := p.vstack[len(p.vstack)-1] _ = stack return p.cur.onDatum1(stack["b1"], stack["b2"]) } func (c *current) onDataIndex1(b0, b1, b2, b3, b4, b5, b6, b7 interface{}) (interface{}, error) { byte0, _ := strconv.ParseUint(string(b0.([]byte)[0]), 16, 4) byte1, _ := strconv.ParseUint(string(b1.([]byte)[0]), 16, 4) byte2, _ := strconv.ParseUint(string(b2.([]byte)[0]), 16, 4) byte3, _ := strconv.ParseUint(string(b3.([]byte)[0]), 16, 4) byte4, _ := strconv.ParseUint(string(b4.([]byte)[0]), 16, 4) byte5, _ := strconv.ParseUint(string(b5.([]byte)[0]), 16, 4) byte6, _ := strconv.ParseUint(string(b6.([]byte)[0]), 16, 4) byte7, _ := strconv.ParseUint(string(b7.([]byte)[0]), 16, 4) return uint32(byte0<<28 | byte1<<24 | byte2<<20 | byte3<<16 | byte4<<12 | byte5<<8 | byte6<<4 | byte7), nil } func (p *parser) callonDataIndex1() (interface{}, error) { stack := p.vstack[len(p.vstack)-1] _ = stack return p.cur.onDataIndex1(stack["b0"], stack["b1"], stack["b2"], stack["b3"], stack["b4"], stack["b5"], stack["b6"], stack["b7"]) } func (c *current) onDataSegment1(ds interface{}) (interface{}, error) { res := make([]byte, 0) for _, d := range ds.([]interface{}) { res = append(res, d.(byte)) } return res, nil } func (p *parser) callonDataSegment1() (interface{}, error) { stack := p.vstack[len(p.vstack)-1] _ = stack return p.cur.onDataSegment1(stack["ds"]) } func (c *current) onNode0Clause1(node interface{}) (interface{}, error) { return node, nil } func (p *parser) callonNode0Clause1() (interface{}, error) { stack := p.vstack[len(p.vstack)-1] _ = stack return p.cur.onNode0Clause1(stack["node"]) } func (c *current) onNode1Clause1(node interface{}) (interface{}, error) { return node, nil } func (p *parser) callonNode1Clause1() (interface{}, error) { stack := p.vstack[len(p.vstack)-1] _ = stack return p.cur.onNode1Clause1(stack["node"]) } func (c *current) onNodeExpr1() (interface{}, error) { return string(c.text), nil } func (p *parser) callonNodeExpr1() (interface{}, error) { stack := p.vstack[len(p.vstack)-1] _ = stack return p.cur.onNodeExpr1() } func (c *current) onFollowClause1(fexpr interface{}) (interface{}, error) { return fexpr, nil } func (p *parser) callonFollowClause1() (interface{}, error) { stack := p.vstack[len(p.vstack)-1] _ = stack return p.cur.onFollowClause1(stack["fexpr"]) } func (c *current) onFollowExpr1() (interface{}, error) { return string(c.text), nil } func (p *parser) callonFollowExpr1() (interface{}, error) { stack := p.vstack[len(p.vstack)-1] _ = stack return p.cur.onFollowExpr1() } func (c *current) onFilterClause1(fexpr interface{}) (interface{}, error) { return fexpr, nil } func (p *parser) callonFilterClause1() (interface{}, error) { stack := p.vstack[len(p.vstack)-1] _ = stack return p.cur.onFilterClause1(stack["fexpr"]) } func (c *current) onFilterExpr1() (interface{}, error) { return string(c.text), nil } func (p *parser) callonFilterExpr1() (interface{}, error) { stack := p.vstack[len(p.vstack)-1] _ = stack return p.cur.onFilterExpr1() } func (c *current) onHeader1(fc, fic, node0, node1 interface{}) (interface{}, error) { fh := FollowHeader{ Follow: fc.(string), Filter: fic.(string), Node0: node0.(string), Node1: node1.(string), } if cb, ok := c.globalStore["callbacks"]; ok { if cb, ok := cb.(IOnStreamHeader); ok { cb.OnStreamHeader(fh) } } return fh, nil } func (p *parser) callonHeader1() (interface{}, error) { stack := p.vstack[len(p.vstack)-1] _ = stack return p.cur.onHeader1(stack["fc"], stack["fic"], stack["node0"], stack["node1"]) } func (c *current) onFollow1(hdr, data interface{}) (interface{}, error) { bytes := make([]Bytes, 0, len(data.([]interface{}))) for _, dl := range data.([]interface{}) { bytes = append(bytes, dl.(Bytes)) } return &FollowStream{ FollowHeader: hdr.(FollowHeader), Bytes: bytes, }, nil } func (p *parser) callonFollow1() (interface{}, error) { stack := p.vstack[len(p.vstack)-1] _ = stack return p.cur.onFollow1(stack["hdr"], stack["data"]) } var ( // errNoRule is returned when the grammar to parse has no rule. errNoRule = errors.New("grammar has no rule") // errInvalidEntrypoint is returned when the specified entrypoint rule // does not exit. errInvalidEntrypoint = errors.New("invalid entrypoint") // errInvalidEncoding is returned when the source is not properly // utf8-encoded. errInvalidEncoding = errors.New("invalid encoding") // errMaxExprCnt is used to signal that the maximum number of // expressions have been parsed. errMaxExprCnt = errors.New("max number of expresssions parsed") ) // Option is a function that can set an option on the parser. It returns // the previous setting as an Option. type Option func(*parser) Option // MaxExpressions creates an Option to stop parsing after the provided // number of expressions have been parsed, if the value is 0 then the parser will // parse for as many steps as needed (possibly an infinite number). // // The default for maxExprCnt is 0. func MaxExpressions(maxExprCnt uint64) Option { return func(p *parser) Option { oldMaxExprCnt := p.maxExprCnt p.maxExprCnt = maxExprCnt return MaxExpressions(oldMaxExprCnt) } } // Entrypoint creates an Option to set the rule name to use as entrypoint. // The rule name must have been specified in the -alternate-entrypoints // if generating the parser with the -optimize-grammar flag, otherwise // it may have been optimized out. Passing an empty string sets the // entrypoint to the first rule in the grammar. // // The default is to start parsing at the first rule in the grammar. func Entrypoint(ruleName string) Option { return func(p *parser) Option { oldEntrypoint := p.entrypoint p.entrypoint = ruleName if ruleName == "" { p.entrypoint = g.rules[0].name } return Entrypoint(oldEntrypoint) } } // Statistics adds a user provided Stats struct to the parser to allow // the user to process the results after the parsing has finished. // Also the key for the "no match" counter is set. // // Example usage: // // input := "input" // stats := Stats{} // _, err := Parse("input-file", []byte(input), Statistics(&stats, "no match")) // if err != nil { // log.Panicln(err) // } // b, err := json.MarshalIndent(stats.ChoiceAltCnt, "", " ") // if err != nil { // log.Panicln(err) // } // fmt.Println(string(b)) // func Statistics(stats *Stats, choiceNoMatch string) Option { return func(p *parser) Option { oldStats := p.Stats p.Stats = stats oldChoiceNoMatch := p.choiceNoMatch p.choiceNoMatch = choiceNoMatch if p.Stats.ChoiceAltCnt == nil { p.Stats.ChoiceAltCnt = make(map[string]map[string]int) } return Statistics(oldStats, oldChoiceNoMatch) } } // Debug creates an Option to set the debug flag to b. When set to true, // debugging information is printed to stdout while parsing. // // The default is false. func Debug(b bool) Option { return func(p *parser) Option { old := p.debug p.debug = b return Debug(old) } } // Memoize creates an Option to set the memoize flag to b. When set to true, // the parser will cache all results so each expression is evaluated only // once. This guarantees linear parsing time even for pathological cases, // at the expense of more memory and slower times for typical cases. // // The default is false. func Memoize(b bool) Option { return func(p *parser) Option { old := p.memoize p.memoize = b return Memoize(old) } } // AllowInvalidUTF8 creates an Option to allow invalid UTF-8 bytes. // Every invalid UTF-8 byte is treated as a utf8.RuneError (U+FFFD) // by character class matchers and is matched by the any matcher. // The returned matched value, c.text and c.offset are NOT affected. // // The default is false. func AllowInvalidUTF8(b bool) Option { return func(p *parser) Option { old := p.allowInvalidUTF8 p.allowInvalidUTF8 = b return AllowInvalidUTF8(old) } } // Recover creates an Option to set the recover flag to b. When set to // true, this causes the parser to recover from panics and convert it // to an error. Setting it to false can be useful while debugging to // access the full stack trace. // // The default is true. func Recover(b bool) Option { return func(p *parser) Option { old := p.recover p.recover = b return Recover(old) } } // GlobalStore creates an Option to set a key to a certain value in // the globalStore. func GlobalStore(key string, value interface{}) Option { return func(p *parser) Option { old := p.cur.globalStore[key] p.cur.globalStore[key] = value return GlobalStore(key, old) } } // InitState creates an Option to set a key to a certain value in // the global "state" store. func InitState(key string, value interface{}) Option { return func(p *parser) Option { old := p.cur.state[key] p.cur.state[key] = value return InitState(key, old) } } // ParseFile parses the file identified by filename. func ParseFile(filename string, opts ...Option) (i interface{}, err error) { f, err := os.Open(filename) if err != nil { return nil, err } defer func() { if closeErr := f.Close(); closeErr != nil { err = closeErr } }() return ParseReader(filename, f, opts...) } // ParseReader parses the data from r using filename as information in the // error messages. func ParseReader(filename string, r io.Reader, opts ...Option) (interface{}, error) { b, err := ioutil.ReadAll(r) if err != nil { return nil, err } return Parse(filename, b, opts...) } // Parse parses the data from b using filename as information in the // error messages. func Parse(filename string, b []byte, opts ...Option) (interface{}, error) { return newParser(filename, b, opts...).parse(g) } // position records a position in the text. type position struct { line, col, offset int } func (p position) String() string { return strconv.Itoa(p.line) + ":" + strconv.Itoa(p.col) + " [" + strconv.Itoa(p.offset) + "]" } // savepoint stores all state required to go back to this point in the // parser. type savepoint struct { position rn rune w int } type current struct { pos position // start position of the match text []byte // raw text of the match // state is a store for arbitrary key,value pairs that the user wants to be // tied to the backtracking of the parser. // This is always rolled back if a parsing rule fails. state storeDict // globalStore is a general store for the user to store arbitrary key-value // pairs that they need to manage and that they do not want tied to the // backtracking of the parser. This is only modified by the user and never // rolled back by the parser. It is always up to the user to keep this in a // consistent state. globalStore storeDict } type storeDict map[string]interface{} // the AST types... type grammar struct { pos position rules []*rule } type rule struct { pos position name string displayName string expr interface{} } type choiceExpr struct { pos position alternatives []interface{} } type actionExpr struct { pos position expr interface{} run func(*parser) (interface{}, error) } type recoveryExpr struct { pos position expr interface{} recoverExpr interface{} failureLabel []string } type seqExpr struct { pos position exprs []interface{} } type throwExpr struct { pos position label string } type labeledExpr struct { pos position label string expr interface{} } type expr struct { pos position expr interface{} } type andExpr expr type notExpr expr type zeroOrOneExpr expr type zeroOrMoreExpr expr type oneOrMoreExpr expr type ruleRefExpr struct { pos position name string } type stateCodeExpr struct { pos position run func(*parser) error } type andCodeExpr struct { pos position run func(*parser) (bool, error) } type notCodeExpr struct { pos position run func(*parser) (bool, error) } type litMatcher struct { pos position val string ignoreCase bool } type charClassMatcher struct { pos position val string basicLatinChars [128]bool chars []rune ranges []rune classes []*unicode.RangeTable ignoreCase bool inverted bool } type anyMatcher position // errList cumulates the errors found by the parser. type errList []error func (e *errList) add(err error) { *e = append(*e, err) } func (e errList) err() error { if len(e) == 0 { return nil } e.dedupe() return e } func (e *errList) dedupe() { var cleaned []error set := make(map[string]bool) for _, err := range *e { if msg := err.Error(); !set[msg] { set[msg] = true cleaned = append(cleaned, err) } } *e = cleaned } func (e errList) Error() string { switch len(e) { case 0: return "" case 1: return e[0].Error() default: var buf bytes.Buffer for i, err := range e { if i > 0 { buf.WriteRune('\n') } buf.WriteString(err.Error()) } return buf.String() } } // parserError wraps an error with a prefix indicating the rule in which // the error occurred. The original error is stored in the Inner field. type parserError struct { Inner error pos position prefix string expected []string } // Error returns the error message. func (p *parserError) Error() string { return p.prefix + ": " + p.Inner.Error() } // newParser creates a parser with the specified input source and options. func newParser(filename string, b []byte, opts ...Option) *parser { stats := Stats{ ChoiceAltCnt: make(map[string]map[string]int), } p := &parser{ filename: filename, errs: new(errList), data: b, pt: savepoint{position: position{line: 1}}, recover: true, cur: current{ state: make(storeDict), globalStore: make(storeDict), }, maxFailPos: position{col: 1, line: 1}, maxFailExpected: make([]string, 0, 20), Stats: &stats, // start rule is rule [0] unless an alternate entrypoint is specified entrypoint: g.rules[0].name, } p.setOptions(opts) if p.maxExprCnt == 0 { p.maxExprCnt = math.MaxUint64 } return p } // setOptions applies the options to the parser. func (p *parser) setOptions(opts []Option) { for _, opt := range opts { opt(p) } } type resultTuple struct { v interface{} b bool end savepoint } const choiceNoMatch = -1 // Stats stores some statistics, gathered during parsing type Stats struct { // ExprCnt counts the number of expressions processed during parsing // This value is compared to the maximum number of expressions allowed // (set by the MaxExpressions option). ExprCnt uint64 // ChoiceAltCnt is used to count for each ordered choice expression, // which alternative is used how may times. // These numbers allow to optimize the order of the ordered choice expression // to increase the performance of the parser // // The outer key of ChoiceAltCnt is composed of the name of the rule as well // as the line and the column of the ordered choice. // The inner key of ChoiceAltCnt is the number (one-based) of the matching alternative. // For each alternative the number of matches are counted. If an ordered choice does not // match, a special counter is incremented. The name of this counter is set with // the parser option Statistics. // For an alternative to be included in ChoiceAltCnt, it has to match at least once. ChoiceAltCnt map[string]map[string]int } type parser struct { filename string pt savepoint cur current data []byte errs *errList depth int recover bool debug bool memoize bool // memoization table for the packrat algorithm: // map[offset in source] map[expression or rule] {value, match} memo map[int]map[interface{}]resultTuple // rules table, maps the rule identifier to the rule node rules map[string]*rule // variables stack, map of label to value vstack []map[string]interface{} // rule stack, allows identification of the current rule in errors rstack []*rule // parse fail maxFailPos position maxFailExpected []string maxFailInvertExpected bool // max number of expressions to be parsed maxExprCnt uint64 // entrypoint for the parser entrypoint string allowInvalidUTF8 bool *Stats choiceNoMatch string // recovery expression stack, keeps track of the currently available recovery expression, these are traversed in reverse recoveryStack []map[string]interface{} } // push a variable set on the vstack. func (p *parser) pushV() { if cap(p.vstack) == len(p.vstack) { // create new empty slot in the stack p.vstack = append(p.vstack, nil) } else { // slice to 1 more p.vstack = p.vstack[:len(p.vstack)+1] } // get the last args set m := p.vstack[len(p.vstack)-1] if m != nil && len(m) == 0 { // empty map, all good return } m = make(map[string]interface{}) p.vstack[len(p.vstack)-1] = m } // pop a variable set from the vstack. func (p *parser) popV() { // if the map is not empty, clear it m := p.vstack[len(p.vstack)-1] if len(m) > 0 { // GC that map p.vstack[len(p.vstack)-1] = nil } p.vstack = p.vstack[:len(p.vstack)-1] } // push a recovery expression with its labels to the recoveryStack func (p *parser) pushRecovery(labels []string, expr interface{}) { if cap(p.recoveryStack) == len(p.recoveryStack) { // create new empty slot in the stack p.recoveryStack = append(p.recoveryStack, nil) } else { // slice to 1 more p.recoveryStack = p.recoveryStack[:len(p.recoveryStack)+1] } m := make(map[string]interface{}, len(labels)) for _, fl := range labels { m[fl] = expr } p.recoveryStack[len(p.recoveryStack)-1] = m } // pop a recovery expression from the recoveryStack func (p *parser) popRecovery() { // GC that map p.recoveryStack[len(p.recoveryStack)-1] = nil p.recoveryStack = p.recoveryStack[:len(p.recoveryStack)-1] } func (p *parser) print(prefix, s string) string { if !p.debug { return s } fmt.Printf("%s %d:%d:%d: %s [%#U]\n", prefix, p.pt.line, p.pt.col, p.pt.offset, s, p.pt.rn) return s } func (p *parser) in(s string) string { p.depth++ return p.print(strings.Repeat(" ", p.depth)+">", s) } func (p *parser) out(s string) string { p.depth-- return p.print(strings.Repeat(" ", p.depth)+"<", s) } func (p *parser) addErr(err error) { p.addErrAt(err, p.pt.position, []string{}) } func (p *parser) addErrAt(err error, pos position, expected []string) { var buf bytes.Buffer if p.filename != "" { buf.WriteString(p.filename) } if buf.Len() > 0 { buf.WriteString(":") } buf.WriteString(fmt.Sprintf("%d:%d (%d)", pos.line, pos.col, pos.offset)) if len(p.rstack) > 0 { if buf.Len() > 0 { buf.WriteString(": ") } rule := p.rstack[len(p.rstack)-1] if rule.displayName != "" { buf.WriteString("rule " + rule.displayName) } else { buf.WriteString("rule " + rule.name) } } pe := &parserError{Inner: err, pos: pos, prefix: buf.String(), expected: expected} p.errs.add(pe) } func (p *parser) failAt(fail bool, pos position, want string) { // process fail if parsing fails and not inverted or parsing succeeds and invert is set if fail == p.maxFailInvertExpected { if pos.offset < p.maxFailPos.offset { return } if pos.offset > p.maxFailPos.offset { p.maxFailPos = pos p.maxFailExpected = p.maxFailExpected[:0] } if p.maxFailInvertExpected { want = "!" + want } p.maxFailExpected = append(p.maxFailExpected, want) } } // read advances the parser to the next rune. func (p *parser) read() { p.pt.offset += p.pt.w rn, n := utf8.DecodeRune(p.data[p.pt.offset:]) p.pt.rn = rn p.pt.w = n p.pt.col++ if rn == '\n' { p.pt.line++ p.pt.col = 0 } if rn == utf8.RuneError && n == 1 { // see utf8.DecodeRune if !p.allowInvalidUTF8 { p.addErr(errInvalidEncoding) } } } // restore parser position to the savepoint pt. func (p *parser) restore(pt savepoint) { if p.debug { defer p.out(p.in("restore")) } if pt.offset == p.pt.offset { return } p.pt = pt } // Cloner is implemented by any value that has a Clone method, which returns a // copy of the value. This is mainly used for types which are not passed by // value (e.g map, slice, chan) or structs that contain such types. // // This is used in conjunction with the global state feature to create proper // copies of the state to allow the parser to properly restore the state in // the case of backtracking. type Cloner interface { Clone() interface{} } var statePool = &sync.Pool{ New: func() interface{} { return make(storeDict) }, } func (sd storeDict) Discard() { for k := range sd { delete(sd, k) } statePool.Put(sd) } // clone and return parser current state. func (p *parser) cloneState() storeDict { if p.debug { defer p.out(p.in("cloneState")) } state := statePool.Get().(storeDict) for k, v := range p.cur.state { if c, ok := v.(Cloner); ok { state[k] = c.Clone() } else { state[k] = v } } return state } // restore parser current state to the state storeDict. // every restoreState should applied only one time for every cloned state func (p *parser) restoreState(state storeDict) { if p.debug { defer p.out(p.in("restoreState")) } p.cur.state.Discard() p.cur.state = state } // get the slice of bytes from the savepoint start to the current position. func (p *parser) sliceFrom(start savepoint) []byte { return p.data[start.position.offset:p.pt.position.offset] } func (p *parser) getMemoized(node interface{}) (resultTuple, bool) { if len(p.memo) == 0 { return resultTuple{}, false } m := p.memo[p.pt.offset] if len(m) == 0 { return resultTuple{}, false } res, ok := m[node] return res, ok } func (p *parser) setMemoized(pt savepoint, node interface{}, tuple resultTuple) { if p.memo == nil { p.memo = make(map[int]map[interface{}]resultTuple) } m := p.memo[pt.offset] if m == nil { m = make(map[interface{}]resultTuple) p.memo[pt.offset] = m } m[node] = tuple } func (p *parser) buildRulesTable(g *grammar) { p.rules = make(map[string]*rule, len(g.rules)) for _, r := range g.rules { p.rules[r.name] = r } } func (p *parser) parse(g *grammar) (val interface{}, err error) { if len(g.rules) == 0 { p.addErr(errNoRule) return nil, p.errs.err() } // TODO : not super critical but this could be generated p.buildRulesTable(g) if p.recover { // panic can be used in action code to stop parsing immediately // and return the panic as an error. defer func() { if e := recover(); e != nil { if p.debug { defer p.out(p.in("panic handler")) } val = nil switch e := e.(type) { case error: p.addErr(e) default: p.addErr(fmt.Errorf("%v", e)) } err = p.errs.err() } }() } startRule, ok := p.rules[p.entrypoint] if !ok { p.addErr(errInvalidEntrypoint) return nil, p.errs.err() } p.read() // advance to first rune val, ok = p.parseRule(startRule) if !ok { if len(*p.errs) == 0 { // If parsing fails, but no errors have been recorded, the expected values // for the farthest parser position are returned as error. maxFailExpectedMap := make(map[string]struct{}, len(p.maxFailExpected)) for _, v := range p.maxFailExpected { maxFailExpectedMap[v] = struct{}{} } expected := make([]string, 0, len(maxFailExpectedMap)) eof := false if _, ok := maxFailExpectedMap["!."]; ok { delete(maxFailExpectedMap, "!.") eof = true } for k := range maxFailExpectedMap { expected = append(expected, k) } sort.Strings(expected) if eof { expected = append(expected, "EOF") } p.addErrAt(errors.New("no match found, expected: "+listJoin(expected, ", ", "or")), p.maxFailPos, expected) } return nil, p.errs.err() } return val, p.errs.err() } func listJoin(list []string, sep string, lastSep string) string { switch len(list) { case 0: return "" case 1: return list[0] default: return strings.Join(list[:len(list)-1], sep) + " " + lastSep + " " + list[len(list)-1] } } func (p *parser) parseRule(rule *rule) (interface{}, bool) { if p.debug { defer p.out(p.in("parseRule " + rule.name)) } if p.memoize { res, ok := p.getMemoized(rule) if ok { p.restore(res.end) return res.v, res.b } } start := p.pt p.rstack = append(p.rstack, rule) p.pushV() val, ok := p.parseExpr(rule.expr) p.popV() p.rstack = p.rstack[:len(p.rstack)-1] if ok && p.debug { p.print(strings.Repeat(" ", p.depth)+"MATCH", string(p.sliceFrom(start))) } if p.memoize { p.setMemoized(start, rule, resultTuple{val, ok, p.pt}) } return val, ok } func (p *parser) parseExpr(expr interface{}) (interface{}, bool) { var pt savepoint if p.memoize { res, ok := p.getMemoized(expr) if ok { p.restore(res.end) return res.v, res.b } pt = p.pt } p.ExprCnt++ if p.ExprCnt > p.maxExprCnt { panic(errMaxExprCnt) } var val interface{} var ok bool switch expr := expr.(type) { case *actionExpr: val, ok = p.parseActionExpr(expr) case *andCodeExpr: val, ok = p.parseAndCodeExpr(expr) case *andExpr: val, ok = p.parseAndExpr(expr) case *anyMatcher: val, ok = p.parseAnyMatcher(expr) case *charClassMatcher: val, ok = p.parseCharClassMatcher(expr) case *choiceExpr: val, ok = p.parseChoiceExpr(expr) case *labeledExpr: val, ok = p.parseLabeledExpr(expr) case *litMatcher: val, ok = p.parseLitMatcher(expr) case *notCodeExpr: val, ok = p.parseNotCodeExpr(expr) case *notExpr: val, ok = p.parseNotExpr(expr) case *oneOrMoreExpr: val, ok = p.parseOneOrMoreExpr(expr) case *recoveryExpr: val, ok = p.parseRecoveryExpr(expr) case *ruleRefExpr: val, ok = p.parseRuleRefExpr(expr) case *seqExpr: val, ok = p.parseSeqExpr(expr) case *stateCodeExpr: val, ok = p.parseStateCodeExpr(expr) case *throwExpr: val, ok = p.parseThrowExpr(expr) case *zeroOrMoreExpr: val, ok = p.parseZeroOrMoreExpr(expr) case *zeroOrOneExpr: val, ok = p.parseZeroOrOneExpr(expr) default: panic(fmt.Sprintf("unknown expression type %T", expr)) } if p.memoize { p.setMemoized(pt, expr, resultTuple{val, ok, p.pt}) } return val, ok } func (p *parser) parseActionExpr(act *actionExpr) (interface{}, bool) { if p.debug { defer p.out(p.in("parseActionExpr")) } start := p.pt val, ok := p.parseExpr(act.expr) if ok { p.cur.pos = start.position p.cur.text = p.sliceFrom(start) state := p.cloneState() actVal, err := act.run(p) if err != nil { p.addErrAt(err, start.position, []string{}) } p.restoreState(state) val = actVal } if ok && p.debug { p.print(strings.Repeat(" ", p.depth)+"MATCH", string(p.sliceFrom(start))) } return val, ok } func (p *parser) parseAndCodeExpr(and *andCodeExpr) (interface{}, bool) { if p.debug { defer p.out(p.in("parseAndCodeExpr")) } state := p.cloneState() ok, err := and.run(p) if err != nil { p.addErr(err) } p.restoreState(state) return nil, ok } func (p *parser) parseAndExpr(and *andExpr) (interface{}, bool) { if p.debug { defer p.out(p.in("parseAndExpr")) } pt := p.pt state := p.cloneState() p.pushV() _, ok := p.parseExpr(and.expr) p.popV() p.restoreState(state) p.restore(pt) return nil, ok } func (p *parser) parseAnyMatcher(any *anyMatcher) (interface{}, bool) { if p.debug { defer p.out(p.in("parseAnyMatcher")) } if p.pt.rn == utf8.RuneError && p.pt.w == 0 { // EOF - see utf8.DecodeRune p.failAt(false, p.pt.position, ".") return nil, false } start := p.pt p.read() p.failAt(true, start.position, ".") return p.sliceFrom(start), true } func (p *parser) parseCharClassMatcher(chr *charClassMatcher) (interface{}, bool) { if p.debug { defer p.out(p.in("parseCharClassMatcher")) } cur := p.pt.rn start := p.pt // can't match EOF if cur == utf8.RuneError && p.pt.w == 0 { // see utf8.DecodeRune p.failAt(false, start.position, chr.val) return nil, false } if chr.ignoreCase { cur = unicode.ToLower(cur) } // try to match in the list of available chars for _, rn := range chr.chars { if rn == cur { if chr.inverted { p.failAt(false, start.position, chr.val) return nil, false } p.read() p.failAt(true, start.position, chr.val) return p.sliceFrom(start), true } } // try to match in the list of ranges for i := 0; i < len(chr.ranges); i += 2 { if cur >= chr.ranges[i] && cur <= chr.ranges[i+1] { if chr.inverted { p.failAt(false, start.position, chr.val) return nil, false } p.read() p.failAt(true, start.position, chr.val) return p.sliceFrom(start), true } } // try to match in the list of Unicode classes for _, cl := range chr.classes { if unicode.Is(cl, cur) { if chr.inverted { p.failAt(false, start.position, chr.val) return nil, false } p.read() p.failAt(true, start.position, chr.val) return p.sliceFrom(start), true } } if chr.inverted { p.read() p.failAt(true, start.position, chr.val) return p.sliceFrom(start), true } p.failAt(false, start.position, chr.val) return nil, false } func (p *parser) incChoiceAltCnt(ch *choiceExpr, altI int) { choiceIdent := fmt.Sprintf("%s %d:%d", p.rstack[len(p.rstack)-1].name, ch.pos.line, ch.pos.col) m := p.ChoiceAltCnt[choiceIdent] if m == nil { m = make(map[string]int) p.ChoiceAltCnt[choiceIdent] = m } // We increment altI by 1, so the keys do not start at 0 alt := strconv.Itoa(altI + 1) if altI == choiceNoMatch { alt = p.choiceNoMatch } m[alt]++ } func (p *parser) parseChoiceExpr(ch *choiceExpr) (interface{}, bool) { if p.debug { defer p.out(p.in("parseChoiceExpr")) } for altI, alt := range ch.alternatives { // dummy assignment to prevent compile error if optimized _ = altI state := p.cloneState() p.pushV() val, ok := p.parseExpr(alt) p.popV() if ok { p.incChoiceAltCnt(ch, altI) return val, ok } p.restoreState(state) } p.incChoiceAltCnt(ch, choiceNoMatch) return nil, false } func (p *parser) parseLabeledExpr(lab *labeledExpr) (interface{}, bool) { if p.debug { defer p.out(p.in("parseLabeledExpr")) } p.pushV() val, ok := p.parseExpr(lab.expr) p.popV() if ok && lab.label != "" { m := p.vstack[len(p.vstack)-1] m[lab.label] = val } return val, ok } func (p *parser) parseLitMatcher(lit *litMatcher) (interface{}, bool) { if p.debug { defer p.out(p.in("parseLitMatcher")) } ignoreCase := "" if lit.ignoreCase { ignoreCase = "i" } val := string(strconv.AppendQuote([]byte{}, lit.val)) + ignoreCase // wrap 'lit.val' with double quotes start := p.pt for _, want := range lit.val { cur := p.pt.rn if lit.ignoreCase { cur = unicode.ToLower(cur) } if cur != want { p.failAt(false, start.position, val) p.restore(start) return nil, false } p.read() } p.failAt(true, start.position, val) return p.sliceFrom(start), true } func (p *parser) parseNotCodeExpr(not *notCodeExpr) (interface{}, bool) { if p.debug { defer p.out(p.in("parseNotCodeExpr")) } state := p.cloneState() ok, err := not.run(p) if err != nil { p.addErr(err) } p.restoreState(state) return nil, !ok } func (p *parser) parseNotExpr(not *notExpr) (interface{}, bool) { if p.debug { defer p.out(p.in("parseNotExpr")) } pt := p.pt state := p.cloneState() p.pushV() p.maxFailInvertExpected = !p.maxFailInvertExpected _, ok := p.parseExpr(not.expr) p.maxFailInvertExpected = !p.maxFailInvertExpected p.popV() p.restoreState(state) p.restore(pt) return nil, !ok } func (p *parser) parseOneOrMoreExpr(expr *oneOrMoreExpr) (interface{}, bool) { if p.debug { defer p.out(p.in("parseOneOrMoreExpr")) } var vals []interface{} for { p.pushV() val, ok := p.parseExpr(expr.expr) p.popV() if !ok { if len(vals) == 0 { // did not match once, no match return nil, false } return vals, true } vals = append(vals, val) } } func (p *parser) parseRecoveryExpr(recover *recoveryExpr) (interface{}, bool) { if p.debug { defer p.out(p.in("parseRecoveryExpr (" + strings.Join(recover.failureLabel, ",") + ")")) } p.pushRecovery(recover.failureLabel, recover.recoverExpr) val, ok := p.parseExpr(recover.expr) p.popRecovery() return val, ok } func (p *parser) parseRuleRefExpr(ref *ruleRefExpr) (interface{}, bool) { if p.debug { defer p.out(p.in("parseRuleRefExpr " + ref.name)) } if ref.name == "" { panic(fmt.Sprintf("%s: invalid rule: missing name", ref.pos)) } rule := p.rules[ref.name] if rule == nil { p.addErr(fmt.Errorf("undefined rule: %s", ref.name)) return nil, false } return p.parseRule(rule) } func (p *parser) parseSeqExpr(seq *seqExpr) (interface{}, bool) { if p.debug { defer p.out(p.in("parseSeqExpr")) } vals := make([]interface{}, 0, len(seq.exprs)) pt := p.pt state := p.cloneState() for _, expr := range seq.exprs { val, ok := p.parseExpr(expr) if !ok { p.restoreState(state) p.restore(pt) return nil, false } vals = append(vals, val) } return vals, true } func (p *parser) parseStateCodeExpr(state *stateCodeExpr) (interface{}, bool) { if p.debug { defer p.out(p.in("parseStateCodeExpr")) } err := state.run(p) if err != nil { p.addErr(err) } return nil, true } func (p *parser) parseThrowExpr(expr *throwExpr) (interface{}, bool) { if p.debug { defer p.out(p.in("parseThrowExpr")) } for i := len(p.recoveryStack) - 1; i >= 0; i-- { if recoverExpr, ok := p.recoveryStack[i][expr.label]; ok { if val, ok := p.parseExpr(recoverExpr); ok { return val, ok } } } return nil, false } func (p *parser) parseZeroOrMoreExpr(expr *zeroOrMoreExpr) (interface{}, bool) { if p.debug { defer p.out(p.in("parseZeroOrMoreExpr")) } var vals []interface{} for { p.pushV() val, ok := p.parseExpr(expr.expr) p.popV() if !ok { return vals, true } vals = append(vals, val) } } func (p *parser) parseZeroOrOneExpr(expr *zeroOrOneExpr) (interface{}, bool) { if p.debug { defer p.out(p.in("parseZeroOrOneExpr")) } p.pushV() val, _ := p.parseExpr(expr.expr) p.popV() // whether it matched or not, consider it a match return val, true } termshark-2.2.0/streams/follow.peg000066400000000000000000000130561377442047300172100ustar00rootroot00000000000000// Copyright 2019-2021 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // // This peg file should be compiled with something like this: // // go get github.com/mna/pigeon@f3db42a // cd termshark/streams/ // pigeon follow.peg > follow.go // { package streams import ( "io" "unicode" "strings" "os" "fmt" "strconv" "errors" "io/ioutil" "bytes" "unicode/utf8" log "github.com/sirupsen/logrus" ) } // // Parse input that looks roughly like: // =================================================================== // Follow: tcp,raw // Filter: tcp.stream eq 0 // Node 0: 192.168.0.114:1137 // Node 1: 192.168.0.193:21 // 3232302043687269732053616e6465727320465450205365727665720d0a // 55534552206373616e646572730d0a // 3333312050617373776f726420726571756972656420666f72206373616e646572732e0d0a // 50415353206563686f0d0a // 3233302055736572206373616e64657273206c6f6767656420696e2e0d0a // ... Input <- _ Separator follow:Follow Separator EOF { return follow, nil } Separator <- '='+ __ // 00000010 30 61 20 53 65 72 76 65 72 20 28 50 72 6f 46 54 0a Serve r (ProFT // 00000086 20 70 61 73 73 77 6f 72 64 2e 0d 0a passwor d... // Returns Bytes{} DataLines <- dl:( DataLineBackComplete / DataLineForwardComplete ) { if cb, ok := c.globalStore["callbacks"]; ok { if cb, ok := cb.(IOnStreamChunk); ok { ch := make(chan struct{}) cb.OnStreamChunk(dl.(Bytes), ch) <-ch } } return dl, nil } ThrowUndefLabel = %{undeflabel} DataLineBackComplete <- dl:DataLineBack { if ctx, ok := c.globalStore["context"]; ok { ctx := ctx.(parseContext) if ctx.Err() != nil { panic(StreamParseError{}) } } return dl, nil } DataLineForwardComplete <- dl:DataLineForward { if ctx, ok := c.globalStore["context"]; ok { ctx := ctx.(parseContext) if ctx.Err() != nil { panic(StreamParseError{}) } } return dl, nil } // // 3232302043687269732053616e6465727320465450205365727665720d0a // DataLineBack <- [\t] data:Data { return Bytes{Dirn: Server, Data: data.([]byte)}, nil } // // 55534552206373616e646572730d0a // DataLineForward <- data:Data { return Bytes{Dirn: Client, Data: data.([]byte)}, nil } Data <- ds:(Datum+) [\r]? [\n] { data := make([]byte, 0, len(ds.([]interface{}))) for _, l := range ds.([]interface{}) { data = append(data, l.(byte)) } return data, nil } Datum <- b1:[0-9a-fA-F] b2:[0-9a-fA-F] { byte1, _ := strconv.ParseUint(string(b1.([]byte)[0]), 16, 4) byte2, _ := strconv.ParseUint(string(b2.([]byte)[0]), 16, 4) return byte(byte1 << 4 | byte2), nil } // Returns uint32 DataIndex <- b0:[0-9a-fA-F] b1:[0-9a-fA-F] b2:[0-9a-fA-F] b3:[0-9a-fA-F] b4:[0-9a-fA-F] b5:[0-9a-fA-F] b6:[0-9a-fA-F] b7:[0-9a-fA-F] { byte0, _ := strconv.ParseUint(string(b0.([]byte)[0]), 16, 4) byte1, _ := strconv.ParseUint(string(b1.([]byte)[0]), 16, 4) byte2, _ := strconv.ParseUint(string(b2.([]byte)[0]), 16, 4) byte3, _ := strconv.ParseUint(string(b3.([]byte)[0]), 16, 4) byte4, _ := strconv.ParseUint(string(b4.([]byte)[0]), 16, 4) byte5, _ := strconv.ParseUint(string(b5.([]byte)[0]), 16, 4) byte6, _ := strconv.ParseUint(string(b6.([]byte)[0]), 16, 4) byte7, _ := strconv.ParseUint(string(b7.([]byte)[0]), 16, 4) return uint32(byte0 << 28 | byte1 << 24 | byte2 << 20 | byte3 << 16 | byte4 << 12 | byte5 << 8 | byte6 << 4 | byte7), nil } // Returns []byte DataSegment <- ds:( Datum )+ { res := make([]byte, 0) for _, d := range ds.([]interface{}) { res = append(res, d.(byte)) } return res, nil } // Returns string e.g. "192.168.1.1:12345" Node0Clause <- "Node 0:" _ node:NodeExpr __nl { return node, nil } // Returns string e.g. "192.168.1.1:12345" Node1Clause <- "Node 1:" _ node:NodeExpr __nl { return node, nil } // Returns string NodeExpr <- [^\n]+ { return string(c.text), nil } FollowClause <- "Follow:" _ fexpr:FollowExpr __nl { return fexpr, nil } FollowExpr <- [a-zA-Z,]+ { return string(c.text), nil } FilterClause <- "Filter:" _ fexpr:FilterExpr __nl { return fexpr, nil } FilterExpr <- [^\n]+ { return string(c.text), nil } // Follow: tcp,raw // Filter: tcp.stream eq 0 // Node 0: 192.168.0.114:1137 // Node 1: 192.168.0.193:21 Header <- fc:FollowClause fic:FilterClause node0:Node0Clause node1:Node1Clause { fh := FollowHeader{ Follow: fc.(string), Filter: fic.(string), Node0: node0.(string), Node1: node1.(string), } if cb, ok := c.globalStore["callbacks"]; ok { if cb, ok := cb.(IOnStreamHeader); ok { ch := make(chan struct{}) cb.OnStreamHeader(fh, ch) <-ch } } return fh, nil } // Follow: tcp,raw // Filter: tcp.stream eq 0 // Node 0: 192.168.0.114:1137 // Node 1: 192.168.0.193:21 // 3232302043687269732053616e6465727320465450205365727665720d0a // 55534552206373616e646572730d0a // 3333312050617373776f726420726571756972656420666f72206373616e646572732e0d0a // 50415353206563686f0d0a // 3233302055736572206373616e64657273206c6f6767656420696e2e0d0a // ... Follow <- hdr:Header data:DataLines* { bytes := make([]Bytes, 0, len(data.([]interface{}))) for _, dl := range data.([]interface{}) { bytes = append(bytes, dl.(Bytes)) } return &FollowStream{ FollowHeader: hdr.(FollowHeader), Bytes: bytes, }, nil } _ "whitespace" <- [ \n\t\r]* __ "mandatory whitespace" <- [ \n\t\r]+ __nl "whitespace and newline" <- [ \t\r]* [\n] EOF <- !. termshark-2.2.0/streams/follow_test.go000066400000000000000000000125131377442047300200760ustar00rootroot00000000000000// Copyright 2019-2021 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 streams import ( "fmt" "log" "strings" "testing" "github.com/stretchr/testify/assert" ) //====================================================================== // 00000010 30 61 20 53 65 72 76 65 72 20 28 50 72 6f 46 54 0a Serve r (ProFT // 00000020 50 44 20 41 6e 6f 6e 79 6d 6f 75 73 20 53 65 72 PD Anony mous Ser // 00000030 76 65 72 29 20 5b 31 39 32 2e 31 36 38 2e 31 2e ver) [19 2.168.1. // 00000040 32 33 31 5d 0d 0a 231].. // 00000000 55 53 45 52 20 66 74 70 0d 0a USER ftp .. // 00000046 33 33 31 20 41 6e 6f 6e 79 6d 6f 75 73 20 6c 6f 331 Anon ymous lo // 00000056 67 69 6e 20 6f 6b 2c 20 73 65 6e 64 20 79 6f 75 gin ok, send you // 00000066 72 20 63 6f 6d 70 6c 65 74 65 20 65 6d 61 69 6c r comple te email // 00000076 20 61 64 64 72 65 73 73 20 61 73 20 79 6f 75 72 address as your // 00000086 20 70 61 73 73 77 6f 72 64 2e 0d 0a passwor d... // 0000000A 50 41 53 53 20 66 74 70 0d 0a PASS ftp .. func TestArgConv(t *testing.T) { inp1 := ` =================================================================== Follow: tcp,raw Filter: tcp.stream eq 0 Node 0: 192.168.1.182:62014 Node 1: 192.168.1.231:21 3232302050726f4654504420312e332e306120536572766572202850726f4654504420416e6f6e796d6f75732053657276657229205b3139322e3136382e312e3233315d0d0a 55534552206674700d0a 33333120416e6f6e796d6f7573206c6f67696e206f6b2c2073656e6420796f757220636f6d706c65746520656d61696c206164647265737320617320796f75722070617373776f72642e0d0a =================================================================== ` fmt.Printf("GCLA: testing '%s'\n", inp1) got, err := ParseReader("", strings.NewReader(inp1)) if err != nil { log.Fatal(err) } fmt.Println("=", got) } func TestArgConv2(t *testing.T) { inp1 := ` =================================================================== Follow: tcp,raw Filter: tcp.stream eq 0 Node 0: 192.168.0.114:1137 Node 1: 192.168.0.193:21 3232302050726f4654504420312e332e306120536572766572202850726f4654504420416e6f6e796d6f75732053657276657229205b3139322e3136382e312e3233315d0d0a 55534552206674700d0a 33333120416e6f6e796d6f7573206c6f67696e206f6b2c2073656e6420796f757220636f6d706c65746520656d61696c206164647265737320617320796f75722070617373776f72642e0d0a 50415353206674700d0a 32333020416e6f6e796d6f757320616363657373206772616e7465642c207265737472696374696f6e73206170706c792e0d0a 535953540d0a 32313520554e495820547970653a204c380d0a 464541540d0a 3231312d46656174757265733a0a204d44544d0a20524553542053545245414d0a2053495a450d0a 32313120456e640d0a 5057440d0a 32353720222f222069732063757272656e74206469726563746f72792e0d0a 455053560d0a 32323920456e746572696e6720457874656e6465642050617373697665204d6f646520287c7c7c35383631327c290d0a 4c4953540d0a 313530204f70656e696e67204153434949206d6f6465206461746120636f6e6e656374696f6e20666f722066696c65206c6973740d0a 323236205472616e7366657220636f6d706c6574652e0d0a 5459504520490d0a 32303020547970652073657420746f20490d0a 53495a4520726573756d652e646f630d0a 3231332033393432340d0a 455053560d0a 32323920456e746572696e6720457874656e6465642050617373697665204d6f646520287c7c7c33373130307c290d0a 5245545220726573756d652e646f630d0a 313530204f70656e696e672042494e415259206d6f6465206461746120636f6e6e656374696f6e20666f7220726573756d652e646f6320283339343234206279746573290d0a 323236205472616e7366657220636f6d706c6574652e0d0a 4d44544d20726573756d652e646f630d0a 3231332032303037303831353032323235320d0a 4357442075706c6f6164730d0a 3235302043574420636f6d6d616e64207375636365737366756c0d0a 5057440d0a 32353720222f75706c6f616473222069732063757272656e74206469726563746f72792e0d0a 455053560d0a 32323920456e746572696e6720457874656e6465642050617373697665204d6f646520287c7c7c33363938367c290d0a 53544f5220524541444d450d0a 313530204f70656e696e672042494e415259206d6f6465206461746120636f6e6e656374696f6e20666f7220524541444d450d0a 323236205472616e7366657220636f6d706c6574652e0d0a 4d4b4420746573746469720d0a 35353020746573746469723a2046696c65206578697374730d0a =================================================================== ` fmt.Printf("Testing: '%s'\n", inp1) got, err := ParseReader("", strings.NewReader(inp1)) if err != nil { log.Fatal(err) } fmt.Println("=", got) } type errContext struct{} func (f errContext) Err() error { return StreamParseError{} } type noErrContext struct{} func (f noErrContext) Err() error { return nil } func TestArgConv3(t *testing.T) { inp1 := ` =================================================================== Follow: tcp,raw Filter: tcp.stream eq 0 Node 0: 192.168.0.114:1137 Node 1: 192.168.0.193:21 3232302050726f4654504420312e332e306120536572766572202850726f4654504420416e6f6e796d6f75732053657276657229205b3139322e3136382e312e3233315d0d0a 55534552206674700d0a =================================================================== ` fmt.Printf("Testing: '%s'\n", inp1) _, err := ParseReader("", strings.NewReader(inp1), GlobalStore("context", errContext{})) assert.Error(t, err) _, err = ParseReader("", strings.NewReader(inp1), GlobalStore("context", noErrContext{})) assert.NoError(t, err) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.2.0/streams/loader.go000066400000000000000000000226401377442047300170050ustar00rootroot00000000000000// Copyright 2019-2021 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 streams import ( "context" "encoding/xml" "fmt" "io" "os/exec" "strconv" "sync" "github.com/gcla/gowid" "github.com/gcla/termshark/v2" "github.com/gcla/termshark/v2/pcap" log "github.com/sirupsen/logrus" ) //====================================================================== var Goroutinewg *sync.WaitGroup //====================================================================== type ILoaderCmds interface { Stream(pcap string, proto string, idx int) pcap.IPcapCommand Indexer(pcap string, proto string, idx int) pcap.IPcapCommand } type commands struct{} func MakeCommands() commands { return commands{} } var _ ILoaderCmds = commands{} func (c commands) Stream(pcapfile string, proto string, idx int) pcap.IPcapCommand { args := []string{"-r", pcapfile, "-q", "-z", fmt.Sprintf("follow,%s,raw,%d", proto, idx)} return &pcap.Command{Cmd: exec.Command(termshark.TSharkBin(), args...)} } // startAt is zero-indexed func (c commands) Indexer(pcapfile string, proto string, idx int) pcap.IPcapCommand { args := []string{"-T", "pdml", "-r", pcapfile, "-Y", fmt.Sprintf("%s.stream eq %d", proto, idx)} return &pcap.Command{Cmd: exec.Command(termshark.TSharkBin(), args...)} } //====================================================================== type Loader struct { cmds ILoaderCmds SuppressErrors bool // if true, don't report process errors e.g. at shutdown mainCtx context.Context // cancelling this cancels the dependent contexts mainCancelFn context.CancelFunc streamCtx context.Context // cancels the stream chunk reader process streamCancelFn context.CancelFunc indexerCtx context.Context // cancels the stream indexer (pdml) process indexerCancelFn context.CancelFunc streamCmd pcap.IPcapCommand indexerCmd pcap.IPcapCommand } func NewLoader(cmds ILoaderCmds, ctx context.Context) *Loader { res := &Loader{ cmds: cmds, } res.mainCtx, res.mainCancelFn = context.WithCancel(ctx) return res } // Called by user to cancel a stream reassembly op. Stops both processes straight away. // Note that typically, the indexer will be further behind. func (c *Loader) StopLoad() { c.SuppressErrors = true if c.streamCancelFn != nil { c.streamCancelFn() } if c.indexerCancelFn != nil { c.indexerCancelFn() } } //====================================================================== type ITrackPayload interface { TrackPayloadPacket(packet int) } type IIndexerCallbacks interface { IOnStreamChunk ITrackPayload AfterIndexEnd(success bool) } func (c *Loader) StartLoad(pcap string, proto string, idx int, app gowid.IApp, cb IIndexerCallbacks) { c.SuppressErrors = false termshark.TrackedGo(func() { c.loadStreamReassemblyAsync(pcap, proto, idx, app, cb) }, Goroutinewg) termshark.TrackedGo(func() { c.startStreamIndexerAsync(pcap, proto, idx, app, cb) }, Goroutinewg) } type ISavedData interface { NumChunks() int Chunk(i int) IChunk } func (c *Loader) loadStreamReassemblyAsync(pcapf string, proto string, idx int, app gowid.IApp, cb interface{}) { c.streamCtx, c.streamCancelFn = context.WithCancel(c.mainCtx) procChan := make(chan int) pid := 0 defer func() { if pid == 0 { close(procChan) } }() c.streamCmd = c.cmds.Stream(pcapf, proto, idx) termChan := make(chan error) termshark.TrackedGo(func() { var err error origCmd := c.streamCmd cancelled := c.streamCtx.Done() procChan := procChan state := pcap.NotStarted kill := func() { err := termshark.KillIfPossible(origCmd) if err != nil { log.Infof("Did not kill tshark stream process: %v", err) } } loop: for { select { case err = <-termChan: state = pcap.Terminated if !c.SuppressErrors && err != nil { if _, ok := err.(*exec.ExitError); ok { cerr := gowid.WithKVs(termshark.BadCommand, map[string]interface{}{ "command": c.streamCmd.String(), "error": err, }) pcap.HandleError(pcap.StreamCode, app, cerr, cb) } } case pid := <-procChan: procChan = nil if pid != 0 { state = pcap.Started if cancelled == nil { kill() } } case <-cancelled: cancelled = nil if state == pcap.Started { kill() } } if state == pcap.Terminated || (cancelled == nil && state == pcap.NotStarted) { break loop } } }, Goroutinewg) streamOut, err := c.streamCmd.StdoutReader() if err != nil { pcap.HandleError(pcap.StreamCode, app, err, cb) return } app.Run(gowid.RunFunction(func(app gowid.IApp) { pcap.HandleBegin(pcap.StreamCode, app, cb) })) defer func() { app.Run(gowid.RunFunction(func(app gowid.IApp) { pcap.HandleEnd(pcap.StreamCode, app, cb) })) }() err = c.streamCmd.Start() if err != nil { err = fmt.Errorf("Error starting stream reassembly %v: %v", c.streamCmd, err) pcap.HandleError(pcap.StreamCode, app, err, cb) return } log.Infof("Started stream reassembly command %v with pid %d", c.streamCmd, c.streamCmd.Pid()) termshark.TrackedGo(func() { termChan <- c.streamCmd.Wait() }, Goroutinewg) pid = c.streamCmd.Pid() procChan <- pid var ops []Option ops = append(ops, GlobalStore("app", app)) ops = append(ops, GlobalStore("context", c.streamCtx)) ops = append(ops, GlobalStore("callbacks", cb)) func() { _, err := ParseReader("", streamOut, ops...) if err != nil { log.Infof("Stream parser reported error: %v", err) } }() c.streamCancelFn() } func (c *Loader) startStreamIndexerAsync(pcapf string, proto string, idx int, app gowid.IApp, cb IIndexerCallbacks) { res := false procChan := make(chan int) pid := 0 defer func() { if pid == 0 { close(procChan) } }() c.indexerCtx, c.indexerCancelFn = context.WithCancel(c.mainCtx) c.indexerCmd = c.cmds.Indexer(pcapf, proto, idx) streamOut, err := c.indexerCmd.StdoutReader() if err != nil { pcap.HandleError(pcap.StreamCode, app, err, cb) return } procWaitChan := make(chan error, 1) termshark.TrackedGo(func() { var err error cancelledChan := c.indexerCtx.Done() procChan := procChan state := pcap.NotStarted kill := func() { err = termshark.KillIfPossible(c.indexerCmd) if err != nil { log.Infof("Did not kill indexer process: %v", err) } } loop: for { select { case err = <-procWaitChan: state = pcap.Terminated if !c.SuppressErrors && err != nil { if _, ok := err.(*exec.ExitError); ok { cerr := gowid.WithKVs(termshark.BadCommand, map[string]interface{}{ "command": c.indexerCmd.String(), "error": err, }) pcap.HandleError(pcap.StreamCode, app, cerr, cb) } } streamOut.Close() case pid := <-procChan: procChan = nil if pid != 0 { state = pcap.Started if cancelledChan == nil { kill() } } case <-cancelledChan: cancelledChan = nil if state == pcap.Started { kill() } } if state == pcap.Terminated || (cancelledChan == nil && state == pcap.NotStarted) { break loop } } }, Goroutinewg) defer func() { cb.AfterIndexEnd(res) }() err = c.indexerCmd.Start() if err != nil { err = fmt.Errorf("Error starting stream indexer %v: %v", c.indexerCmd, err) pcap.HandleError(pcap.StreamCode, app, err, cb) return } log.Infof("Started stream indexer command %v with pid %d", c.indexerCmd, c.indexerCmd.Pid()) defer func() { procWaitChan <- c.indexerCmd.Wait() }() pid = c.indexerCmd.Pid() procChan <- pid res = decodeStreamXml(streamOut, proto, c.indexerCtx, cb) } func decodeStreamXml(streamOut io.Reader, proto string, ctx context.Context, cb ITrackPayload) bool { inTCP := false inUDP := false curPkt := 0 curDataLen := 0 res := false d := xml.NewDecoder(streamOut) for { if ctx.Err() != nil { break } t, tokenErr := d.Token() if tokenErr != nil { if tokenErr == io.EOF { res = true break } } switch t := t.(type) { case xml.EndElement: switch t.Name.Local { case "packet": if curDataLen > 0 { cb.TrackPayloadPacket(curPkt) } curPkt++ curDataLen = 0 inTCP = false inUDP = false } case xml.StartElement: switch t.Name.Local { case "proto": for _, attr := range t.Attr { if attr.Name.Local == "name" { switch attr.Value { case "tcp": inTCP = true case "udp": inUDP = true } break } } case "field": aloop: for _, attr := range t.Attr { if attr.Name.Local == "name" { switch attr.Value { case "tcp.len": if proto == "tcp" && inTCP { for _, attr2 := range t.Attr { if attr2.Name.Local == "show" { if val, err := strconv.Atoi(attr2.Value); err == nil { // add val to end of list for tcp:curTCP curDataLen = val } break aloop } } } case "udp.length": if proto == "udp" && inUDP { for _, attr2 := range t.Attr { if attr2.Name.Local == "show" { if val, err := strconv.Atoi(attr2.Value); err == nil { // add val to end of list for udp:curUDP curDataLen = val } break aloop } } } } } } } } } return res } //====================================================================== //====================================================================== // Local Variables: // mode: Go // fill-column: 78 // End: termshark-2.2.0/streams/loader_test.go000066400000000000000000004210251377442047300200440ustar00rootroot00000000000000// Copyright 2019-2021 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 streams import ( "context" "strings" "testing" "github.com/stretchr/testify/assert" ) //====================================================================== type payloadTracker struct { indices []int } func (p *payloadTracker) TrackPayloadPacket(packet int) { p.indices = append(p.indices, packet) } //====================================================================== func TestDecode1(t *testing.T) { pdml := ` ` pt := &payloadTracker{ indices: make([]int, 0), } decodeStreamXml(strings.NewReader(pdml), "tcp", context.TODO(), pt) assert.Equal(t, []int{0, 1, 3, 4, 6, 7}, pt.indices) } //====================================================================== // Local Variables: // mode: Go // fill-column: 78 // End: termshark-2.2.0/streams/parse.go000066400000000000000000000052471377442047300166550ustar00rootroot00000000000000// Copyright 2019-2021 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 streams import ( "encoding/hex" "fmt" "strings" ) //====================================================================== type IChunk interface { Direction() Direction StreamData() []byte } type IOnStreamChunk interface { OnStreamChunk(chunk IChunk) } type IOnStreamHeader interface { OnStreamHeader(header FollowHeader) } //====================================================================== type parseContext interface { Err() error } type StreamParseError struct{} func (e StreamParseError) Error() string { return "Stream reassembly parse error" } var _ error = StreamParseError{} //====================================================================== type Protocol int const ( Unspecified Protocol = 0 TCP Protocol = iota UDP Protocol = iota ) var _ fmt.Stringer = Protocol(0) func (p Protocol) String() string { switch p { case Unspecified: return "Unspecified" case TCP: return "TCP" case UDP: return "UDP" default: panic(nil) } } //====================================================================== type Direction int const ( Client Direction = 0 Server Direction = iota ) func (d Direction) String() string { switch d { case Client: return "Client" case Server: return "Server" default: return "Unknown!" } } //====================================================================== type Bytes struct { Dirn Direction Data []byte } var _ fmt.Stringer = Bytes{} var _ IChunk = Bytes{} func (b Bytes) Direction() Direction { return b.Dirn } func (b Bytes) StreamData() []byte { return b.Data } func (b Bytes) String() string { return fmt.Sprintf("Direction: %v\n%s", b.Dirn, hex.Dump(b.Data)) } //====================================================================== type FollowHeader struct { Follow string Filter string Node0 string Node1 string } func (h FollowHeader) String() string { return fmt.Sprintf("[client:%s server:%s follow:%s filter:%s]", h.Node0, h.Node1, h.Follow, h.Filter) } type FollowStream struct { FollowHeader Bytes []Bytes } var _ fmt.Stringer = FollowStream{} func (f FollowStream) String() string { datastrs := make([]string, 0, len(f.Bytes)) for _, b := range f.Bytes { datastrs = append(datastrs, b.String()) } data := strings.Join(datastrs, "\n") return fmt.Sprintf("Follow: %s\nFilter: %s\nNode0: %s\nNode1: %s\nData:\n%s", f.Follow, f.Filter, f.Node0, f.Node1, data) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.2.0/system/000077500000000000000000000000001377442047300150525ustar00rootroot00000000000000termshark-2.2.0/system/dumpcapext.go000066400000000000000000000046471377442047300175660ustar00rootroot00000000000000// Copyright 2019-2021 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 !windows // +build !arm64 package system import ( "os" "os/exec" "regexp" "strconv" "syscall" log "github.com/sirupsen/logrus" ) //====================================================================== var fdre *regexp.Regexp = regexp.MustCompile(`/dev/fd/([[:digit:]]+)`) // DumpcapExt will run dumpcap first, but if it fails, run tshark. Intended as // a special case to allow termshark -i to use dumpcap if possible, // but if it fails (e.g. iface==randpkt), fall back to tshark. dumpcap is more // efficient than tshark at just capturing, and will drop fewer packets, but // tshark supports extcap interfaces. func DumpcapExt(dumpcapBin string, tsharkBin string, args ...string) error { var err error // If the first argument is /dev/fd/X, it means the process should have // descriptor X open and will expect packet data to be readable on it. // This /dev/fd feature does not work on tshark when run on freebsd, meaning // tshark will fail if you do something like // // cat foo.pcap | tshark -r /dev/fd/0 // // The fix here is to replace /dev/fd/X with the arg "-", which tshark will // interpret as stdin, then dup descriptor X to 0 before starting dumpcap/tshark // if len(args) >= 2 { if os.Getenv("TERMSHARK_REPLACE_DEVFD") != "0" { fdnum := fdre.FindStringSubmatch(args[1]) if len(fdnum) == 2 { fd, err := strconv.Atoi(fdnum[1]) if err != nil { log.Warnf("Unexpected error parsing %s: %v", args[1], err) } else { err = syscall.Dup2(fd, 0) if err != nil { log.Warnf("Problem duplicating fd %d to 0: %v", fd, err) log.Warnf("Will not try to replace argument %s to tshark", args[1]) } else { log.Infof("Replacing argument %s with - for tshark compatibility", args[1]) args[1] = "-" } } } } } dumpcapCmd := exec.Command(dumpcapBin, args...) log.Infof("Starting dumpcap command %v", dumpcapCmd) dumpcapCmd.Stdin = os.Stdin dumpcapCmd.Stdout = os.Stdout dumpcapCmd.Stderr = os.Stderr if dumpcapCmd.Run() != nil { var tshark string tshark, err = exec.LookPath(tsharkBin) if err == nil { log.Infof("Retrying with dumpcap command %v", append([]string{tshark}, args...)) err = syscall.Exec(tshark, append([]string{tshark}, args...), os.Environ()) } } return err } termshark-2.2.0/system/dumpcapext_arm64.go000066400000000000000000000023471377442047300205720ustar00rootroot00000000000000// Copyright 2019-2021 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 system import ( "os" "os/exec" "syscall" log "github.com/sirupsen/logrus" ) //====================================================================== // DumpcapExt will run dumpcap first, but if it fails, run tshark. Intended as // a special case to allow termshark -i to use dumpcap if possible, // but if it fails (e.g. iface==randpkt), fall back to tshark. dumpcap is more // efficient than tshark at just capturing, and will drop fewer packets, but // tshark supports extcap interfaces. func DumpcapExt(dumpcapBin string, tsharkBin string, args ...string) error { var err error dumpcapCmd := exec.Command(dumpcapBin, args...) log.Infof("Starting dumpcap command %v", dumpcapCmd) dumpcapCmd.Stdin = os.Stdin dumpcapCmd.Stdout = os.Stdout dumpcapCmd.Stderr = os.Stderr if dumpcapCmd.Run() != nil { var tshark string tshark, err = exec.LookPath(tsharkBin) if err == nil { log.Infof("Retrying with dumpcap command %v", append([]string{tshark}, args...)) err = syscall.Exec(tshark, append([]string{tshark}, args...), os.Environ()) } } return err } termshark-2.2.0/system/dumpcapext_windows.go000066400000000000000000000022601377442047300213250ustar00rootroot00000000000000// Copyright 2019-2021 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 system import ( "os" "os/exec" log "github.com/sirupsen/logrus" ) //====================================================================== // DumpcapExt will run dumpcap first, but if it fails, run tshark. Intended as // a special case to allow termshark -i to use dumpcap if possible, // but if it fails (e.g. iface==randpkt), fall back to tshark. dumpcap is more // efficient than tshark at just capturing, and will drop fewer packets, but // tshark supports extcap interfaces. func DumpcapExt(dumpcapBin string, tsharkBin string, args ...string) error { dumpcapCmd := exec.Command(dumpcapBin, args...) log.Infof("Starting dumpcap command %v", dumpcapCmd) dumpcapCmd.Stdin = os.Stdin dumpcapCmd.Stdout = os.Stdout dumpcapCmd.Stderr = os.Stderr if dumpcapCmd.Run() == nil { return nil } tsharkCmd := exec.Command(tsharkBin, args...) log.Infof("Retrying with dumpcap command %v", tsharkCmd) tsharkCmd.Stdin = os.Stdin tsharkCmd.Stdout = os.Stdout tsharkCmd.Stderr = os.Stderr return tsharkCmd.Run() } termshark-2.2.0/system/errors.go000066400000000000000000000011051377442047300167120ustar00rootroot00000000000000// Copyright 2019-2021 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 system //====================================================================== type NotImplementedError struct{} var _ error = NotImplementedError{} func (e NotImplementedError) Error() string { return "Feature not implemented" } var NotImplemented = NotImplementedError{} //====================================================================== // Local Variables: // mode: Go // fill-column: 78 // End: termshark-2.2.0/system/extcmds.go000066400000000000000000000004541377442047300170530ustar00rootroot00000000000000// Copyright 2019-2021 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 system var CopyToClipboard = []string{"xsel", "-i", "-b"} var OpenURL = []string{"xdg-open"} termshark-2.2.0/system/extcmds_android.go000066400000000000000000000004701377442047300205510ustar00rootroot00000000000000// Copyright 2019-2021 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 system var CopyToClipboard = []string{"termux-clipboard-set"} var OpenURL = []string{"am", "start", "-a", "android.intent.action.VIEW", "-d"} termshark-2.2.0/system/extcmds_darwin.go000066400000000000000000000003711377442047300204150ustar00rootroot00000000000000// Copyright 2019-2021 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 system var CopyToClipboard = []string{"pbcopy"} var OpenURL = []string{"open"} termshark-2.2.0/system/extcmds_windows.go000066400000000000000000000003731377442047300206250ustar00rootroot00000000000000// Copyright 2019-2021 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 system var CopyToClipboard = []string{"clip"} var OpenURL = []string{"explorer"} termshark-2.2.0/system/fd.go000066400000000000000000000007361377442047300160000ustar00rootroot00000000000000// Copyright 2019-2021 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 !windows package system import ( "syscall" ) //====================================================================== func CloseDescriptor(fd int) { syscall.Close(fd) } //====================================================================== // Local Variables: // mode: Go // fill-column: 78 // End: termshark-2.2.0/system/fd_windows.go000066400000000000000000000005511377442047300175450ustar00rootroot00000000000000// Copyright 2019-2021 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 windows package system func CloseDescriptor(fd int) { } //====================================================================== // Local Variables: // mode: Go // fill-column: 78 // End: termshark-2.2.0/system/fdinfo.go000066400000000000000000000032631377442047300166520ustar00rootroot00000000000000// Copyright 2019-2021 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 system 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: termshark-2.2.0/system/have_fdinfo.go000066400000000000000000000005521377442047300176530ustar00rootroot00000000000000// Copyright 2019-2021 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 system const HaveFdinfo = false //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.2.0/system/have_fdinfo_linux.go000066400000000000000000000005161377442047300210720ustar00rootroot00000000000000// Copyright 2019-2021 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 system const HaveFdinfo = true //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.2.0/system/picker.go000066400000000000000000000006211377442047300166550ustar00rootroot00000000000000// Copyright 2019-2021 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 !android package system import ( "fmt" ) var NoPicker error = fmt.Errorf("No file picker available") func PickFile() (string, error) { return "", NoPicker } func PickFileError(e string) error { fmt.Println(e) return nil } termshark-2.2.0/system/picker_android.go000066400000000000000000000041141377442047300203560ustar00rootroot00000000000000// Copyright 2019-2021 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 system import ( "fmt" "os" "os/exec" "path" fsnotify "gopkg.in/fsnotify/fsnotify.v1" ) var NoPicker error = fmt.Errorf("No file picker available") // not running on termux var NoTermuxApi error = fmt.Errorf("Could not launch file picker. Please install termux-api:\npkg install termux-api") func PickFile() (string, error) { tsdir := "/data/data/com.termux/files/home" tsfile := "termux" tsabs := path.Join(tsdir, tsfile) if err := os.Remove(tsabs); err != nil && !os.IsNotExist(err) { return "", fmt.Errorf("Could not remove previous temporary termux file %s: %v", tsabs, err) } if _, err := exec.Command("termux-storage-get", tsabs).Output(); err != nil { exerr, ok := err.(*exec.Error) if ok && (exerr.Err == exec.ErrNotFound) { return "", NoTermuxApi } else { return "", fmt.Errorf("Could not select input for termshark: %v", err) } } if iwatcher, err := fsnotify.NewWatcher(); err != nil { return "", fmt.Errorf("Could not start filesystem watcher: %v\n", err) } else { defer iwatcher.Close() if err := iwatcher.Add(tsdir); err != nil { //&& !os.IsNotExist(err) { return "", fmt.Errorf("Could not set up file watcher for %s: %v\n", tsfile, err) } // Don't time it - the user might be tied up with the file picker for a while. No real way to tell... //tmr := time.NewTimer(time.Duration(10000) * time.Millisecond) //defer tmr.Close() Loop: for { select { case we := <-iwatcher.Events: if path.Base(we.Name) == tsfile { break Loop } case err := <-iwatcher.Errors: return "", fmt.Errorf("File watcher error for %s: %v", tsfile, err) } } return tsabs, nil } } func PickFileError(e string) error { if _, err := exec.Command("termux-toast", e).Output(); err != nil { exerr, ok := err.(*exec.Error) if ok && (exerr.Err == exec.ErrNotFound) { return NoTermuxApi } else { return fmt.Errorf("Error running termux-toast: %v", err) } } return nil } termshark-2.2.0/system/signals.go000066400000000000000000000022431377442047300170420ustar00rootroot00000000000000// Copyright 2019-2021 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 !windows package system import ( "os" "os/signal" "syscall" ) //====================================================================== func RegisterForSignals(ch chan<- os.Signal) { signal.Notify(ch, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGTSTP, syscall.SIGCONT, syscall.SIGUSR1, syscall.SIGUSR2) } func IsSigUSR1(sig os.Signal) bool { return isUnixSig(sig, syscall.SIGUSR1) } func IsSigUSR2(sig os.Signal) bool { return isUnixSig(sig, syscall.SIGUSR2) } func IsSigTSTP(sig os.Signal) bool { return isUnixSig(sig, syscall.SIGTSTP) } func IsSigCont(sig os.Signal) bool { return isUnixSig(sig, syscall.SIGCONT) } func StopMyself() error { return syscall.Kill(syscall.Getpid(), syscall.SIGSTOP) } func isUnixSig(sig os.Signal, usig syscall.Signal) bool { if ssig, ok := sig.(syscall.Signal); ok && ssig == usig { return true } return false } //====================================================================== // Local Variables: // mode: Go // fill-column: 78 // End: termshark-2.2.0/system/signals_windows.go000066400000000000000000000015231377442047300206140ustar00rootroot00000000000000// Copyright 2019-2021 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 system import ( "os" "os/signal" "github.com/gcla/gowid" ) //====================================================================== func RegisterForSignals(ch chan<- os.Signal) { signal.Notify(ch, os.Interrupt) } func IsSigUSR1(sig os.Signal) bool { return false } func IsSigUSR2(sig os.Signal) bool { return false } func IsSigTSTP(sig os.Signal) bool { return false } func IsSigCont(sig os.Signal) bool { return false } func StopMyself() error { return gowid.WithKVs(NotImplemented, map[string]interface{}{"feature": "SIGSTOP"}) } //====================================================================== // Local Variables: // mode: Go // fill-column: 78 // End: termshark-2.2.0/tailfile.go000066400000000000000000000010521377442047300156440ustar00rootroot00000000000000// Copyright 2019-2021 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 !windows package termshark import ( "os" "os/exec" ) //====================================================================== func TailFile(file string) error { cmd := exec.Command("tail", "-f", file) cmd.Stdout = os.Stdout return cmd.Run() } //====================================================================== // Local Variables: // mode: Go // fill-column: 78 // End: termshark-2.2.0/tailfile_windows.go000066400000000000000000000013101377442047300174130ustar00rootroot00000000000000// Copyright 2019-2021 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" "github.com/gcla/tail" ) //====================================================================== func TailFile(file string) error { t, err := tail.TailFile(file, tail.Config{ Follow: true, ReOpen: true, Poll: true, Logger: tail.DiscardingLogger, }) if err != nil { return err } for chunk := range t.Bytes { os.Stdout.Write([]byte(chunk.Text)) } return nil } //====================================================================== // Local Variables: // mode: Go // fill-column: 78 // End: termshark-2.2.0/theme/000077500000000000000000000000001377442047300146305ustar00rootroot00000000000000termshark-2.2.0/theme/modeswap/000077500000000000000000000000001377442047300164475ustar00rootroot00000000000000termshark-2.2.0/theme/modeswap/modeswap.go000066400000000000000000000020321377442047300206120ustar00rootroot00000000000000// Copyright 2019-2021 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 { modeHi gowid.IColor mode256 gowid.IColor mode16 gowid.IColor } var _ gowid.IColor = (*Color)(nil) func New(hi, mid, lo gowid.IColor) *Color { return &Color{ modeHi: hi, mode256: mid, mode16: lo, } } func (c *Color) ToTCellColor(mode gowid.ColorMode) (gowid.TCellColor, bool) { var col gowid.IColor = c.mode16 switch mode { case gowid.Mode24BitColors: col = c.modeHi case gowid.Mode256Colors: col = c.mode256 default: col = c.mode16 } return col.ToTCellColor(mode) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.2.0/theme/utils.go000066400000000000000000000073631377442047300163300ustar00rootroot00000000000000// Copyright 2019-2021 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 theme provides utilities for customizing the styling of termshark. package theme import ( "fmt" "io" "os" "path" "path/filepath" "github.com/gcla/gowid" "github.com/rakyll/statik/fs" "github.com/shibukawa/configdir" log "github.com/sirupsen/logrus" "github.com/spf13/viper" _ "github.com/gcla/termshark/v2/assets/statik" ) //====================================================================== type Layer int const ( Foreground Layer = 0 Background Layer = iota ) var theme *viper.Viper // MakeColorSafe extends gowid's MakeColorSafe function, prefering to interpret // its string argument as a toml file config key lookup first; if this fails, then // fall back to gowid.MakeColorSafe, which will then read colors as urwid color names, // #-prefixed hex digits, grayscales, etc. func MakeColorSafe(s string, l Layer) (gowid.Color, error) { loops := 10 cur := s if theme != nil { for { next := theme.GetString(cur) if next != "" { cur = next } else { next := theme.GetStringSlice(cur) if next == nil || len(next) != 2 { break } else { cur = next[l] } } loops -= 1 if loops == 0 { break } } } col, err := gowid.MakeColorSafe(cur) if err == nil { return gowid.Color{IColor: col, Id: s}, nil } return gowid.MakeColorSafe(s) } type Mode gowid.ColorMode func (m Mode) String() string { switch gowid.ColorMode(m) { case gowid.Mode256Colors: return "256" case gowid.Mode88Colors: return "88" case gowid.Mode16Colors: return "16" case gowid.Mode8Colors: return "8" case gowid.ModeMonochrome: return "mono" case gowid.Mode24BitColors: return "truecolor" default: return "unknown" } } // Load will set the package-level theme object to a viper object representing the // toml file either (a) read from disk, or failing that (b) built-in to termshark. // Disk themes are prefered and take precedence. func Load(name string, app gowid.IApp) error { var err error theme = viper.New() defer func() { if err != nil { theme = nil } }() theme.SetConfigType("toml") stdConf := configdir.New("", "termshark") dirs := stdConf.QueryFolders(configdir.Global) mode := Mode(app.GetColorMode()).String() log.Infof("Loading theme %s in terminal mode %v", name, app.GetColorMode()) // If there's not a truecolor theme, we assume the user wants the best alternative to be loaded, // and if a terminal has truecolor support, it'll surely have 256-color support. modes := []string{mode} if mode == "truecolor" { modes = append(modes, Mode(gowid.Mode256Colors).String()) } for _, m := range modes { // Prefer to load from disk themeFileName := filepath.Join(dirs[0].Path, "themes", fmt.Sprintf("%s-%s.toml", name, m)) log.Infof("Trying to load user theme %s", themeFileName) var file io.ReadCloser file, err = os.Open(themeFileName) if err == nil { defer file.Close() log.Infof("Loaded user theme %s", themeFileName) return theme.ReadConfig(file) } } // Fall back to built-in themes statikFS, err := fs.New() if err != nil { return fmt.Errorf("in mode %v: %v", app.GetColorMode(), err) } for _, m := range modes { themeFileName := path.Join("/themes", fmt.Sprintf("%s-%s.toml", name, m)) log.Infof("Trying to load built-in theme %s", themeFileName) var file io.ReadCloser file, err = statikFS.Open(themeFileName) if err == nil { defer file.Close() log.Infof("Loaded built-in theme %s", themeFileName) return theme.ReadConfig(file) } } return err } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.2.0/tty/000077500000000000000000000000001377442047300143465ustar00rootroot00000000000000termshark-2.2.0/tty/tty.go000066400000000000000000000032201377442047300155120ustar00rootroot00000000000000// Copyright 2019-2021 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 !windows package tty import ( "os" "syscall" "github.com/pkg/term/termios" ) //====================================================================== type TerminalSignals struct { tiosp syscall.Termios out *os.File set bool } func (t *TerminalSignals) IsSet() bool { return t.set } func (t *TerminalSignals) Restore() { if t.out != nil { fd := uintptr(t.out.Fd()) termios.Tcsetattr(fd, termios.TCSANOW, &t.tiosp) t.out.Close() t.out = nil } t.set = false } func (t *TerminalSignals) Set() error { var e error var newtios syscall.Termios var fd uintptr outtty := "/dev/tty" gwtty := os.Getenv("GOWID_TTY") if gwtty != "" { outtty = gwtty } if t.out, e = os.OpenFile(outtty, os.O_WRONLY, 0); e != nil { goto failed } fd = uintptr(t.out.Fd()) if e = termios.Tcgetattr(fd, &t.tiosp); e != nil { goto failed } newtios = t.tiosp newtios.Lflag |= syscall.ISIG // Enable ctrl-z for suspending the foreground process group via the // line discipline. Ctrl-c and Ctrl-\ are not handled, so the terminal // app will receive these keypresses. newtios.Cc[syscall.VSUSP] = 032 newtios.Cc[syscall.VINTR] = 0 newtios.Cc[syscall.VQUIT] = 0 if e = termios.Tcsetattr(fd, termios.TCSANOW, &newtios); e != nil { goto failed } t.set = true return nil failed: if t.out != nil { t.out.Close() t.out = nil } return e } //====================================================================== // Local Variables: // mode: Go // fill-column: 78 // End: termshark-2.2.0/tty/tty_windows.go000066400000000000000000000011351377442047300172670ustar00rootroot00000000000000// Copyright 2019-2021 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 tty //====================================================================== type TerminalSignals struct { set bool } func (t *TerminalSignals) IsSet() bool { return t.set } func (t *TerminalSignals) Restore() { t.set = false } func (t *TerminalSignals) Set() error { t.set = true return nil } //====================================================================== // Local Variables: // mode: Go // fill-column: 78 // End: termshark-2.2.0/ui/000077500000000000000000000000001377442047300141435ustar00rootroot00000000000000termshark-2.2.0/ui/capinfoui.go000066400000000000000000000065541377442047300164610ustar00rootroot00000000000000// Copyright 2019-2021 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 ui contains user-interface functions and helpers for termshark. package ui import ( "os" "strings" "time" "github.com/gcla/gowid" "github.com/gcla/termshark/v2" "github.com/gcla/termshark/v2/capinfo" "github.com/gcla/termshark/v2/pcap" log "github.com/sirupsen/logrus" ) var CapinfoLoader *capinfo.Loader var CapinfoData string var CapinfoTime time.Time //====================================================================== func startCapinfo(app gowid.IApp) { if Loader.PcapPdml == "" { OpenError("No pcap loaded.", app) return } fi, err := os.Stat(Loader.PcapPdml) if err != nil || CapinfoTime.Before(fi.ModTime()) { CapinfoLoader = capinfo.NewLoader(capinfo.MakeCommands(), Loader.Context()) handler := capinfoParseHandler{} CapinfoLoader.StartLoad( Loader.PcapPdml, app, &handler, ) } else { OpenMessageForCopy(CapinfoData, appView, app) } } //====================================================================== type capinfoParseHandler struct { tick *time.Ticker // for updating the spinner stop chan struct{} pleaseWaitClosed bool } var _ capinfo.ICapinfoCallbacks = (*capinfoParseHandler)(nil) var _ pcap.IBeforeBegin = (*capinfoParseHandler)(nil) var _ pcap.IAfterEnd = (*capinfoParseHandler)(nil) func (t *capinfoParseHandler) OnCapinfoData(data string) { CapinfoData = strings.Replace(data, "\r\n", "\n", -1) // For windows... fi, err := os.Stat(Loader.PcapPdml) if err != nil { log.Warnf("Could not read mtime from pcap %s: %v", Loader.PcapPdml, err) } else { CapinfoTime = fi.ModTime() } } func (t *capinfoParseHandler) AfterCapinfoEnd(success bool) { } func (t *capinfoParseHandler) BeforeBegin(code pcap.HandlerCode, app gowid.IApp) { if code&pcap.CapinfoCode == 0 { return } app.Run(gowid.RunFunction(func(app gowid.IApp) { OpenPleaseWait(appView, app) })) t.tick = time.NewTicker(time.Duration(200) * time.Millisecond) t.stop = make(chan struct{}) termshark.TrackedGo(func() { Loop: for { select { case <-t.tick.C: app.Run(gowid.RunFunction(func(app gowid.IApp) { pleaseWaitSpinner.Update() })) case <-t.stop: break Loop } } }, Goroutinewg) } func (t *capinfoParseHandler) AfterEnd(code pcap.HandlerCode, app gowid.IApp) { if code&pcap.CapinfoCode == 0 { return } app.Run(gowid.RunFunction(func(app gowid.IApp) { if !t.pleaseWaitClosed { t.pleaseWaitClosed = true ClosePleaseWait(app) } OpenMessageForCopy(CapinfoData, appView, app) })) close(t.stop) } //====================================================================== func clearCapinfoState() { CapinfoTime = time.Time{} } //====================================================================== type ManageCapinfoCache struct{} var _ pcap.INewSource = ManageCapinfoCache{} var _ pcap.IClear = ManageCapinfoCache{} // Make sure that existing stream widgets are discarded if the user loads a new pcap. func (t ManageCapinfoCache) OnNewSource(pcap.HandlerCode, gowid.IApp) { clearCapinfoState() } func (t ManageCapinfoCache) OnClear(pcap.HandlerCode, gowid.IApp) { clearCapinfoState() } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.2.0/ui/convscallbacks.go000066400000000000000000000042511377442047300174640ustar00rootroot00000000000000// Copyright 2019-2021 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 ui contains user-interface functions and helpers for termshark. package ui import ( "strings" "time" "github.com/gcla/gowid" "github.com/gcla/termshark/v2" "github.com/gcla/termshark/v2/pcap" ) //====================================================================== type IOnDataSync interface { OnData(data string, app gowid.IApp) OnCancel(gowid.IApp) } type convsParseHandler struct { app gowid.IApp tick *time.Ticker // for updating the spinner stop chan struct{} ondata IOnDataSync pleaseWaitClosed bool } var _ pcap.IBeforeBegin = (*convsParseHandler)(nil) var _ pcap.IAfterEnd = (*convsParseHandler)(nil) func (t *convsParseHandler) OnData(data string) { data = strings.Replace(data, "\r\n", "\n", -1) // For windows... if t.ondata != nil { t.app.Run(gowid.RunFunction(func(app gowid.IApp) { t.ondata.OnData(data, app) })) } } func (t *convsParseHandler) AfterDataEnd(success bool) { if t.ondata != nil && !success { t.app.Run(gowid.RunFunction(func(app gowid.IApp) { t.ondata.OnCancel(app) })) } } func (t *convsParseHandler) BeforeBegin(code pcap.HandlerCode, app gowid.IApp) { if code&pcap.ConvCode == 0 { return } app.Run(gowid.RunFunction(func(app gowid.IApp) { OpenPleaseWait(appView, t.app) })) t.tick = time.NewTicker(time.Duration(200) * time.Millisecond) t.stop = make(chan struct{}) termshark.TrackedGo(func() { Loop: for { select { case <-t.tick.C: app.Run(gowid.RunFunction(func(app gowid.IApp) { pleaseWaitSpinner.Update() })) case <-t.stop: break Loop } } }, Goroutinewg) } func (t *convsParseHandler) AfterEnd(code pcap.HandlerCode, app gowid.IApp) { if code&pcap.ConvCode == 0 { return } t.app.Run(gowid.RunFunction(func(app gowid.IApp) { if !t.pleaseWaitClosed { t.pleaseWaitClosed = true ClosePleaseWait(t.app) } })) close(t.stop) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.2.0/ui/convsui.go000066400000000000000000000610221377442047300161610ustar00rootroot00000000000000// Copyright 2019-2021 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 ui contains user-interface functions and helpers for termshark. package ui import ( "bufio" "context" "fmt" "runtime" "strings" "github.com/gcla/gowid" "github.com/gcla/gowid/widgets/button" "github.com/gcla/gowid/widgets/checkbox" "github.com/gcla/gowid/widgets/columns" "github.com/gcla/gowid/widgets/divider" "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/list" "github.com/gcla/gowid/widgets/menu" "github.com/gcla/gowid/widgets/null" "github.com/gcla/gowid/widgets/overlay" "github.com/gcla/gowid/widgets/pile" "github.com/gcla/gowid/widgets/styled" "github.com/gcla/gowid/widgets/table" "github.com/gcla/gowid/widgets/text" "github.com/gcla/gowid/widgets/vpadding" "github.com/gcla/termshark/v2" "github.com/gcla/termshark/v2/convs" "github.com/gcla/termshark/v2/pcap" "github.com/gcla/termshark/v2/psmlmodel" "github.com/gcla/termshark/v2/ui/tableutil" "github.com/gcla/termshark/v2/widgets/appkeys" "github.com/gcla/termshark/v2/widgets/copymodetable" "github.com/gcla/termshark/v2/widgets/enableselected" "github.com/gcla/termshark/v2/widgets/keepselected" "github.com/gcla/termshark/v2/widgets/scrollabletable" "github.com/gcla/termshark/v2/widgets/withscrollbar" "github.com/gdamore/tcell" ) var convsView *holder.Widget var convsUi *ConvsUiWidget var convCancel context.CancelFunc var convsPcapSize int64 // track size of source, if changes then recalculation conversations var vdiv string var frameRunes framed.FrameRunes type Direction int const ( Any Direction = 0 To Direction = iota From Direction = iota ) type FilterMask int const ( AtfB FilterMask = 0 AtB FilterMask = iota BtA FilterMask = iota AtfAny FilterMask = iota AtAny FilterMask = iota AnytA FilterMask = iota AnytfB FilterMask = iota AnytB FilterMask = iota BtAny FilterMask = iota ) type FilterCombinator int const ( Selected FilterCombinator = 0 NotSelected FilterCombinator = iota AndSelected FilterCombinator = iota OrSelected FilterCombinator = iota AndNotSelected FilterCombinator = iota OrNotSelected FilterCombinator = iota ) // Use to construct a string like "ip.addr == 1.2.3.4 && tcp.port == 12345" type IFilterBuilder interface { fmt.Stringer FilterFrom(vals ...string) string FilterTo(vals ...string) string FilterAny(vals ...string) string AIndex() []int BIndex() []int } var convTypes = map[string]IFilterBuilder{} func init() { convTypes[convs.Ethernet{}.Short()] = convs.Ethernet{} convTypes[convs.IPv4{}.Short()] = convs.IPv4{} convTypes[convs.IPv6{}.Short()] = convs.IPv6{} convTypes[convs.UDP{}.Short()] = convs.UDP{} convTypes[convs.TCP{}.Short()] = convs.TCP{} if runtime.GOOS == "windows" { vdiv = "│" frameRunes = framed.FrameRunes{'┌', '┐', '└', '┘', 0, '─', '│', '│'} } else { vdiv = "┃" frameRunes = framed.FrameRunes{'┏', '┓', '┗', '┛', 0, '━', '┃', '┃'} } } //====================================================================== type ManageConvsCache struct{} var _ pcap.INewSource = ManageConvsCache{} // Make sure that existing data is discarded if the user loads a new pcap. func (t ManageConvsCache) OnNewSource(pcap.HandlerCode, gowid.IApp) { convsView = nil // which then deletes all refs to loaded data convsPcapSize = 0 } //====================================================================== type ConvsModel struct { *psmlmodel.Model proto IFilterBuilder } func (m ConvsModel) GetAFilter(row int, dir Direction) string { line := m.Data[row] parms := []string{} for _, idx := range m.proto.AIndex() { parms = append(parms, line[idx]) } switch dir { case To: return m.proto.FilterTo(parms...) case From: return m.proto.FilterFrom(parms...) default: return m.proto.FilterAny(parms...) } } func (m ConvsModel) GetBFilter(row int, dir Direction) string { line := m.Data[row] parms := []string{} for _, idx := range m.proto.BIndex() { parms = append(parms, line[idx]) } switch dir { case To: return m.proto.FilterTo(parms...) case From: return m.proto.FilterFrom(parms...) default: return m.proto.FilterAny(parms...) } } //====================================================================== func convsKeyPress(sections *pile.Widget, evk *tcell.EventKey, app gowid.IApp) bool { handled := false switch { case evk.Rune() == 'q' || evk.Rune() == 'Q' || evk.Key() == tcell.KeyEscape: closeConvsUi(app) convCancel() handled = true case evk.Key() == tcell.KeyTAB: if next, ok := sections.FindNextSelectable(gowid.Forwards, true); ok { sections.SetFocus(app, next) handled = true } case evk.Key() == tcell.KeyBacktab: if next, ok := sections.FindNextSelectable(gowid.Backwards, true); ok { sections.SetFocus(app, next) handled = true } } return handled } //====================================================================== type pleaseWait struct{} func (p pleaseWait) OpenPleaseWait(app gowid.IApp) { OpenPleaseWait(appView, app) } func (p pleaseWait) ClosePleaseWait(app gowid.IApp) { ClosePleaseWait(app) } // Dynamically load conv. If the convs window was last opened with a different filter, and the "limit to // filter" checkbox is checked, then the data needs to be reloaded. func openConvsUi(app gowid.IApp) { var convCtx context.Context convCtx, convCancel = context.WithCancel(Loader.Context()) newSize, reset := termshark.FileSizeDifferentTo(Loader.PcapPdml, convsPcapSize) if reset { convsView = nil } // This is nil if a new pcap is loaded (or the old one cleared) if convsView == nil { convsPcapSize = newSize // gcla later todo - PcapPdml - hack? convsUi = NewConvsUi( Loader.String(), Loader.DisplayFilter(), Loader.PcapPdml, pleaseWait{}, ConvsUiOptions{ CopyModeWidget: CopyModeWidget, }, ) convsView = holder.New(convsUi) } else if convsUi.FilterValue() != Loader.DisplayFilter() && convsUi.UseFilter() { convsUi.ReloadNeeded() } convsUi.ctx = convCtx convsUi.focusOnFilter = false convsUi.displayFilter = Loader.DisplayFilter() copyModeConvsView := appkeys.New( appkeys.New( convsView, copyModeExitKeys20, appkeys.Options{ ApplyBefore: true, }, ), copyModeEnterKeys, appkeys.Options{ ApplyBefore: true, }, ) appViewNoKeys.SetSubWidget(copyModeConvsView, app) } func closeConvsUi(app gowid.IApp) { appViewNoKeys.SetSubWidget(mainView, app) if convsUi.focusOnFilter { setFocusOnDisplayFilter(app) } else { // Do this if the user starts conversations from the menu - better UX setFocusOnPacketList(app) } } //====================================================================== func NewConvsUi(captureDevice string, displayFilter string, pcapf string, pw IPleaseWait, opts ...ConvsUiOptions) *ConvsUiWidget { var opt ConvsUiOptions if len(opts) > 0 { opt = opts[0] } res := &ConvsUiWidget{ opt: opt, displayFilter: displayFilter, captureDevice: captureDevice, pcapf: pcapf, pleaseWait: pw, tabIndex: make(map[string]int), buttonLabels: make(map[string]*text.Widget), } res.construct() return res } type IPleaseWait interface { OpenPleaseWait(app gowid.IApp) ClosePleaseWait(app gowid.IApp) } type ConvsUiOptions struct { CopyModeWidget gowid.IWidget // What to display when copy-mode is started. } type ConvsUiWidget struct { gowid.IWidget opt ConvsUiOptions captureDevice string // "eth0" displayFilter string // "tcp.stream eq 1" pcapf string // "eth0-ddddd.pcap" ctx context.Context pleaseWait IPleaseWait convHolder *holder.Widget convs []*oneConvWidget // the widgets displayed in each tab prepFiltBtn *button.Widget // "Prepare filter" -> click to prep filter applyFiltBtn *button.Widget // "Apply filter" -> click to prep filter filterPrep bool // if true prepare filter, don't apply; otherwise apply immediately filterSelectedIndex FilterCombinator // which filter combination is active e.g. A -> B focusOnFilter bool // Whether to set focus on display filter on closing widget buttonLabels map[string]*text.Widget // map "eth" to button, so I can update with a count of conversations shortNames []string // ["eth", "ip", ...] - from config file tabIndex map[string]int // {"eth": 0, "ipv6": 2, ...} -> mapping to tabs in UI started bool // false if stream load needs to be done, true if under way or done } func (w *ConvsUiWidget) AbsoluteTime() bool { return termshark.ConfBool("main.conv-absolute-time", false) } func (w *ConvsUiWidget) SetAbsoluteTime(val bool) { termshark.SetConf("main.conv-absolute-time", val) } func (w *ConvsUiWidget) ResolveNames() bool { return termshark.ConfBool("main.conv-resolve-names", false) } func (w *ConvsUiWidget) SetResolveNames(val bool) { termshark.SetConf("main.conv-resolve-names", val) } func (w *ConvsUiWidget) Context() context.Context { return w.ctx } func (w *ConvsUiWidget) FilterValue() string { return w.displayFilter } func (w *ConvsUiWidget) UseFilter() bool { return termshark.ConfBool("main.conv-use-filter", false) } func (w *ConvsUiWidget) SetUseFilter(val bool) { termshark.SetConf("main.conv-use-filter", val) } func (w *ConvsUiWidget) construct() { convs := make([]*oneConvWidget, 0) header := w.makeHeaderConvsUiWidget() convsHeader := columns.NewWithDim( gowid.RenderWithWeight{1}, header, ) colws := make([]interface{}, 0) colws = append(colws, text.New(vdiv), ) w.shortNames = termshark.ConvTypes() // Just in case there are none w.convHolder = holder.New(null.New()) for i, p := range w.shortNames { p := p i := i w.tabIndex[p] = i newconv := newOneConv(p) convs = append(convs, newconv) if i == 0 { w.convHolder = holder.New(newconv) } w.buttonLabels[p] = text.New(fmt.Sprintf(" %s ", convTypes[p])) b := button.NewBare(w.buttonLabels[p]) b.OnClick(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, w2 gowid.IWidget) { w.convHolder.SetSubWidget(newconv, app) })) bs := isselected.NewExt( b, styled.New(b, gowid.MakePaletteRef("button-selected")), styled.New(b, gowid.MakePaletteRef("button-focus")), ) colws = append(colws, bs, text.New(vdiv)) } panel := framed.New(w.convHolder, framed.Options{ Frame: frameRunes, }) cols := keepselected.New(columns.NewFixed(colws...)) nameCheck := checkbox.New(w.ResolveNames()) nameCheck.OnClick(gowid.WidgetCallback{"cb", func(app gowid.IApp, w2 gowid.IWidget) { w.SetResolveNames(nameCheck.IsChecked()) w.ReloadNeeded() }}) nameLabel := text.New(" Name res.") nameW := hpadding.New( columns.NewFixed(nameCheck, nameLabel), gowid.HAlignMiddle{}, gowid.RenderFixed{}, ) filterCheck := checkbox.New(w.UseFilter()) filterCheck.OnClick(gowid.WidgetCallback{"cb", func(app gowid.IApp, w2 gowid.IWidget) { w.SetUseFilter(filterCheck.IsChecked()) w.ReloadNeeded() }}) filterLabel := text.New(" Limit to filter") filterW := hpadding.New( columns.NewFixed(filterCheck, filterLabel), gowid.HAlignMiddle{}, gowid.RenderFixed{}, ) absTimeCheck := checkbox.New(w.AbsoluteTime()) absTimeCheck.OnClick(gowid.WidgetCallback{"cb", func(app gowid.IApp, w2 gowid.IWidget) { w.SetAbsoluteTime(absTimeCheck.IsChecked()) w.ReloadNeeded() }}) absTimeLabel := text.New(" Abs. time") absTimeW := hpadding.New( columns.NewFixed(absTimeCheck, absTimeLabel), gowid.HAlignMiddle{}, gowid.RenderFixed{}, ) //==================== prepFiltBtnSite := menu.NewSite(menu.SiteOptions{YOffset: -8}) w.prepFiltBtn = button.New(text.New("Prep Filter")) w.prepFiltBtn.OnClick(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, w2 gowid.IWidget) { w.filterPrep = true filterConvsMenu1.Open(prepFiltBtnSite, app) })) styledPrepFiltBtn := styled.NewExt( w.prepFiltBtn, gowid.MakePaletteRef("button"), gowid.MakePaletteRef("button-focus"), ) prepFiltCols := columns.NewFixed(prepFiltBtnSite, styledPrepFiltBtn) prepFiltColsW := hpadding.New( prepFiltCols, gowid.HAlignMiddle{}, gowid.RenderFixed{}, ) //==================== applyFiltBtnSite := menu.NewSite(menu.SiteOptions{YOffset: -8}) w.applyFiltBtn = button.New(text.New("Apply Filter")) w.applyFiltBtn.OnClick(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, w2 gowid.IWidget) { w.filterPrep = false filterConvsMenu1.Open(applyFiltBtnSite, app) })) styledApplyFiltBtn := styled.NewExt( w.applyFiltBtn, gowid.MakePaletteRef("button"), gowid.MakePaletteRef("button-focus"), ) applyFiltCols := columns.NewFixed(applyFiltBtnSite, styledApplyFiltBtn) applyFiltColsW := hpadding.New( applyFiltCols, gowid.HAlignMiddle{}, gowid.RenderFixed{}, ) //==================== bcols := columns.NewWithDim(gowid.RenderWithWeight{W: 1}, prepFiltColsW, applyFiltColsW, nameW, filterW, absTimeW, ) main := pile.New([]gowid.IContainerWidget{ &gowid.ContainerWidget{ IWidget: convsHeader, D: gowid.RenderWithUnits{U: 2}, }, &gowid.ContainerWidget{ IWidget: cols, D: gowid.RenderWithUnits{U: 1}, }, &gowid.ContainerWidget{ IWidget: panel, D: gowid.RenderWithWeight{W: 1}, }, &gowid.ContainerWidget{ IWidget: bcols, D: gowid.RenderWithUnits{U: 1}, }, }) w.IWidget = appkeys.New( main, func(ev *tcell.EventKey, app gowid.IApp) bool { return convsKeyPress(main, ev, app) }, appkeys.Options{ ApplyBefore: true, }, ) w.convs = convs } func (w *ConvsUiWidget) ReloadNeeded() { w.started = false } func (w *ConvsUiWidget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { if !w.started { w.started = true ld := convs.NewLoader(convs.MakeCommands(), w.Context()) handler := convsParseHandler{ app: app, ondata: w, } filter := "" if w.UseFilter() { filter = w.FilterValue() } ld.StartLoad( w.pcapf, w.shortNames, //w.ctype, filter, w.AbsoluteTime(), w.ResolveNames(), app, &handler, ) } return w.IWidget.Render(size, focus, app) } // The widget displayed in the first line of the stream reassembly UI. func (w *ConvsUiWidget) makeHeaderConvsUiWidget() gowid.IWidget { var headerText string var headerText1 string var headerText2 string var headerText3 string headerText1 = "Conversations" if w.displayFilter != "" { headerText2 = fmt.Sprintf("(%s)", w.displayFilter) } if w.captureDevice != "" { headerText3 = fmt.Sprintf("- %s", w.captureDevice) } headerText = strings.Join([]string{headerText1, headerText2, headerText3}, " ") headerView := overlay.New( hpadding.New(w.opt.CopyModeWidget, gowid.HAlignMiddle{}, fixed), hpadding.New( text.New(headerText), gowid.HAlignMiddle{}, fixed, ), gowid.VAlignTop{}, gowid.RenderWithRatio{R: 1}, gowid.HAlignMiddle{}, gowid.RenderWithRatio{R: 1}, overlay.Options{ BottomGetsFocus: true, TopGetsNoFocus: true, BottomGetsCursor: true, }, ) return headerView } func (w *ConvsUiWidget) doFilterMenuOp(dirOp FilterMask, app gowid.IApp) { conv1 := w.convHolder.SubWidget() if conv1 != nil { if conv1, ok := conv1.(*oneConvWidget); ok { if conv1.tbl.Length() == 0 { OpenError("No conversation selected.", app) return } pos := conv1.tbl.Pos() var filter string switch dirOp { case AtfB: filter = fmt.Sprintf("%s && %s", conv1.model.GetAFilter(pos, Any), conv1.model.GetBFilter(pos, Any)) case AtB: filter = fmt.Sprintf("%s && %s", conv1.model.GetAFilter(pos, From), conv1.model.GetBFilter(pos, To)) case BtA: filter = fmt.Sprintf("%s && %s", conv1.model.GetBFilter(pos, From), conv1.model.GetAFilter(pos, To)) case AtfAny: filter = conv1.model.GetAFilter(pos, Any) case AtAny: filter = conv1.model.GetAFilter(pos, From) case AnytA: filter = conv1.model.GetAFilter(pos, To) case AnytfB: filter = conv1.model.GetBFilter(pos, Any) case AnytB: filter = conv1.model.GetBFilter(pos, To) case BtAny: filter = conv1.model.GetBFilter(pos, From) } existingFilter := FilterWidget.Value() switch w.filterSelectedIndex { case NotSelected: filter = fmt.Sprintf("!(%s)", filter) case AndSelected: if existingFilter != "" { filter = fmt.Sprintf("%s && (%s)", existingFilter, filter) } case OrSelected: if existingFilter != "" { filter = fmt.Sprintf("%s || (%s)", existingFilter, filter) } case AndNotSelected: if existingFilter != "" { filter = fmt.Sprintf("%s && !(%s)", existingFilter, filter) } case OrNotSelected: if existingFilter != "" { filter = fmt.Sprintf("%s || !(%s)", existingFilter, filter) } } FilterWidget.SetValue(filter, app) if w.filterPrep { // Don't run the filter, just add to the displayfilter widget. Leave focus there w.focusOnFilter = true OpenMessage("Display filter prepared.", appView, app) } else { RequestNewFilter(filter, app) w.displayFilter = filter OpenMessage("Display filter applied.", appView, app) w.ReloadNeeded() } } } } func (w *ConvsUiWidget) OnCancel(app gowid.IApp) { for _, cw := range w.convs { cw.IWidget = cw.cancelledWidget } } func (w *ConvsUiWidget) OnData(data string, app gowid.IApp) { var hdrs []string var wids []gowid.IWidgetDimension var comps []table.ICompare var cur string var next string var ports bool = false var ( addra string porta string addrb string portb string framesto string bytesto string framesfrom string bytesfrom string frames string bytes string start string durn string ) var datas [][]string saveConversation := func(cur string) { tblModel := table.NewSimpleModel(hdrs, datas, table.SimpleOptions{ Comparators: comps, Style: table.StyleOptions{ HorizontalSeparator: nil, TableSeparator: divider.NewUnicode(), VerticalSeparator: nil, CellStyleProvided: true, CellStyleSelected: gowid.MakePaletteRef("packet-list-cell-selected"), CellStyleFocus: gowid.MakePaletteRef("packet-list-cell-focus"), HeaderStyleProvided: true, HeaderStyleFocus: gowid.MakePaletteRef("packet-list-cell-focus"), }, Layout: table.LayoutOptions{ Widths: wids, }, }) ptblModel := psmlmodel.New( tblModel, gowid.MakePaletteRef("packet-list-row-focus"), ) if currentShortName, ok := convs.OfficialNameToType[cur]; ok { model := &ConvsModel{ Model: ptblModel, proto: convTypes[currentShortName], } tbl := &table.BoundedWidget{ Widget: table.New(model), } boundedTbl := NewRowFocusTableWidget( tbl, "packet-list-row-selected", "packet-list-row-focus", ) var _ list.IWalker = boundedTbl var _ gowid.IWidget = boundedTbl var _ table.IBoundedModel = tblModel w.convs[w.tabIndex[currentShortName]].IWidget = appkeys.New( enableselected.New( withscrollbar.New( scrollabletable.New( copymodetable.New( boundedTbl, CsvTableCopier{hdrs, datas}, CsvTableCopier{hdrs, datas}, "convstable", copyModePalette{}, ), ), withscrollbar.Options{ HideIfContentFits: true, }, ), ), tableutil.GotoHandler(&tableutil.GoToAdapter{ BoundedWidget: tbl, KeyState: &keyState, }), ) w.convs[w.tabIndex[currentShortName]].tbl = tbl w.convs[w.tabIndex[currentShortName]].model = model w.buttonLabels[currentShortName].SetText(fmt.Sprintf(" %s (%d) ", cur, len(datas)), app) } } scanner := bufio.NewScanner(strings.NewReader(data)) var n int var err error for scanner.Scan() { line := scanner.Text() r := strings.NewReader(line) n, err = fmt.Fscanf(r, "%s Conversations", &next) if err == nil && n == 1 { if cur != "" { saveConversation(cur) } datas = make([][]string, 0) cur = next ports = termshark.StringInSlice(cur, []string{"UDP", "TCP"}) ipv6 := (cur == "IPv6") if ports { hdrs = []string{ "Addr A", "Port A", "Addr B", "Port B", "Pkts", "Bytes", "Pkts A→B", "Bytes A→B", "Pkts B→A", "Bytes B→A", "Start", "Durn", } wids = []gowid.IWidgetDimension{ weightupto(400, 32), // addra weightupto(200, 7), // port weightupto(400, 32), // addrb weightupto(200, 7), // port weightupto(200, 8), // pkts weightupto(200, 10), weightupto(200, 12), // pkts a -> b weightupto(200, 12), // bytes a -> b weightupto(200, 12), // pkts a -> b weightupto(200, 12), // bytes a -> b weightupto(500, 14), // start weightupto(200, 8), // durn } comps = []table.ICompare{ table.StringCompare{}, table.IntCompare{}, table.StringCompare{}, table.IntCompare{}, table.IntCompare{}, table.IntCompare{}, table.IntCompare{}, table.IntCompare{}, table.IntCompare{}, table.IntCompare{}, table.FloatCompare{}, table.FloatCompare{}, } } else { hdrs = []string{ "Addr A", "Addr B", "Pkts", "Bytes", "Pkts A→B", "Bytes A→B", "Pkts B→A", "Bytes B→A", "Start", "Durn", } wids = []gowid.IWidgetDimension{ weightupto(400, 32), // addra weightupto(400, 32), // addrb weightupto(200, 8), // pkts weightupto(200, 10), weightupto(200, 12), // pkts a -> b weightupto(200, 12), // bytes a -> b weightupto(200, 12), // pkts a -> b weightupto(200, 12), // bytes a -> b weightupto(500, 14), // start weightupto(200, 10), // durn } if ipv6 { wids[0] = weightupto(500, 42) wids[1] = weightupto(500, 42) } comps = []table.ICompare{ table.StringCompare{}, table.StringCompare{}, table.IntCompare{}, table.IntCompare{}, table.IntCompare{}, table.IntCompare{}, table.IntCompare{}, table.IntCompare{}, table.FloatCompare{}, table.FloatCompare{}, } } continue } r = strings.NewReader(line) n, err = fmt.Fscanf(r, "%s <-> %s %s %s %s %s %s %s %s %s", &addra, &addrb, &framesto, &bytesto, &framesfrom, &bytesfrom, &frames, &bytes, &start, &durn, ) if err == nil && n == 10 { if ports { pa := strings.Split(addra, ":") pb := strings.Split(addrb, ":") if len(pa) == 2 && len(pb) == 2 { addra = pa[0] porta = pa[1] addrb = pb[0] portb = pb[1] datas = append(datas, []string{addra, porta, addrb, portb, framesto, bytesto, framesfrom, bytesfrom, frames, bytes, start, durn}) } } else { datas = append(datas, []string{addra, addrb, framesto, bytesto, framesfrom, bytesfrom, frames, bytes, start, durn}) } } } saveConversation(cur) } //====================================================================== type oneConvWidget struct { gowid.IWidget ctype string pleaseWaitWidget gowid.IWidget cancelledWidget gowid.IWidget model *ConvsModel tbl *table.BoundedWidget } func newOneConv(ctype string) *oneConvWidget { pleaseWaitWidget := vpadding.New( hpadding.New( text.New(fmt.Sprintf("Please wait for %s", ctype)), gowid.HAlignMiddle{}, gowid.RenderFixed{}, ), gowid.VAlignMiddle{}, gowid.RenderFlow{}, ) cancelledWidget := text.New("Conversation load was cancelled.") res := &oneConvWidget{ IWidget: pleaseWaitWidget, ctype: ctype, pleaseWaitWidget: pleaseWaitWidget, cancelledWidget: cancelledWidget, } return res } //====================================================================== type CsvTableCopier struct { hdrs []string data [][]string } func (c CsvTableCopier) CopyRow(id table.RowId) []gowid.ICopyResult { row := strings.Join(c.data[id], ",") return []gowid.ICopyResult{ gowid.CopyResult{ Name: "Copy conversation", Val: row, }, } } func (c CsvTableCopier) CopyTable() []gowid.ICopyResult { res := make([]string, 0, len(c.data)+1) res = append(res, strings.Join(c.hdrs, ",")) for _, d := range c.data { res = append(res, strings.Join(d, ",")) } prt := strings.Join(res, "\n") return []gowid.ICopyResult{ gowid.CopyResult{ Name: "Copy all", Val: prt, }, } } var _ copymodetable.IRowCopier = CsvTableCopier{} var _ copymodetable.ITableCopier = CsvTableCopier{} //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.2.0/ui/convsui_test.go000066400000000000000000000022351377442047300172210ustar00rootroot00000000000000// Copyright 2019-2021 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 ui contains user-interface functions and helpers for termshark. package ui import ( "fmt" "strings" "testing" "github.com/stretchr/testify/assert" ) func TestScan1(t *testing.T) { line := `127.0.0.1:47416 <-> 127.0.0.1:9191 0 0 43549 9951808 43549 9951808 4.160565000 9.4522` var ( addra string addrb string framesto int bytesto int framesfrom int bytesfrom int frames int bytes int start string durn string ) r := strings.NewReader(line) n, err := fmt.Fscanf(r, "%s <-> %s %d %d %d %d %d %d %s %s", &addra, &addrb, &framesto, &bytesto, &framesfrom, &bytesfrom, &frames, &bytes, &start, &durn, ) assert.NoError(t, err) assert.Equal(t, 10, n) assert.Equal(t, "4.160565000", start) assert.Equal(t, "127.0.0.1:9191", addrb) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.2.0/ui/darkmode.go000066400000000000000000000016251377442047300162640ustar00rootroot00000000000000// Copyright 2019-2021 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 ui import "github.com/gcla/gowid" //====================================================================== type PaletteSwitcher struct { P1 gowid.IPalette P2 gowid.IPalette ChooseOne *bool } var _ gowid.IPalette = (*PaletteSwitcher)(nil) func (p PaletteSwitcher) CellStyler(name string) (gowid.ICellStyler, bool) { if *p.ChooseOne { return p.P1.CellStyler(name) } else { return p.P2.CellStyler(name) } } func (p PaletteSwitcher) RangeOverPalette(f func(key string, value gowid.ICellStyler) bool) { if *p.ChooseOne { p.P1.RangeOverPalette(f) } else { p.P2.RangeOverPalette(f) } } //====================================================================== // Local Variables: // mode: Go // fill-column: 78 // End: termshark-2.2.0/ui/dialog.go000066400000000000000000000113761377442047300157410ustar00rootroot00000000000000// Copyright 2019-2021 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 ui contains user-interface functions and helpers for termshark. package ui import ( "strings" "github.com/gcla/gowid" "github.com/gcla/gowid/widgets/dialog" "github.com/gcla/gowid/widgets/framed" "github.com/gcla/gowid/widgets/hpadding" "github.com/gcla/gowid/widgets/selectable" "github.com/gcla/gowid/widgets/styled" "github.com/gcla/gowid/widgets/text" "github.com/gcla/termshark/v2" "github.com/gcla/termshark/v2/widgets/appkeys" "github.com/gcla/termshark/v2/widgets/minibuffer" "github.com/gcla/termshark/v2/widgets/scrollabletext" "github.com/gcla/termshark/v2/widgets/withscrollbar" ) //====================================================================== var ( fixed gowid.RenderFixed flow gowid.RenderFlow hmiddle gowid.HAlignMiddle hleft gowid.HAlignLeft vmiddle gowid.VAlignMiddle YesNo *dialog.Widget MiniBuffer *minibuffer.Widget PleaseWait *dialog.Widget ) type textID string func (t textID) ID() interface{} { return string(t) } // So that I can capture ctrl-c etc before the dialog type copyable struct { *dialog.Widget wrapper gowid.IWidget } func (w *copyable) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { return w.wrapper.UserInput(ev, size, focus, app) } func OpenMessage(msgt string, openOver gowid.ISettableComposite, app gowid.IApp) *dialog.Widget { return openMessage(msgt, openOver, false, false, app) } func OpenLongMessage(msgt string, openOver gowid.ISettableComposite, app gowid.IApp) *dialog.Widget { return openMessage(msgt, openOver, false, true, app) } func OpenMessageForCopy(msgt string, openOver gowid.ISettableComposite, app gowid.IApp) *dialog.Widget { return openMessage(msgt, openOver, true, false, app) } func openMessage(msgt string, openOver gowid.ISettableComposite, focusOnWidget bool, doFlow bool, app gowid.IApp) *dialog.Widget { var dh gowid.IWidgetDimension = fixed var dw gowid.IWidgetDimension = fixed if doFlow { dh = flow dw = ratio(0.7) } var al gowid.IHAlignment = hmiddle if strings.Count(msgt, "\n") > 0 || doFlow { al = hleft } var view gowid.IWidget = text.NewCopyable(msgt, textID(msgt), styled.UsePaletteIfSelectedForCopy{Entry: "copy-mode-alt"}, text.Options{ Align: al, }, ) view = selectable.New(view) view = hpadding.New( view, hmiddle, dh, ) view = framed.NewSpace(view) YesNo = dialog.New( view, dialog.Options{ Buttons: dialog.CloseOnly, NoShadow: true, BackgroundStyle: gowid.MakePaletteRef("dialog"), BorderStyle: gowid.MakePaletteRef("dialog"), ButtonStyle: gowid.MakePaletteRef("dialog-button"), FocusOnWidget: focusOnWidget, }, ) wrapper := appkeys.New( appkeys.New( YesNo, copyModeExitKeys20, appkeys.Options{ ApplyBefore: true, }, ), copyModeEnterKeys, appkeys.Options{ ApplyBefore: true, }, ) dialog.OpenExt( ©able{ Widget: YesNo, wrapper: wrapper, }, openOver, dw, dh, app, ) return YesNo } func OpenTemplatedDialog(container gowid.ISettableComposite, tmplName string, app gowid.IApp) *dialog.Widget { YesNo = dialog.New( framed.NewSpace( text.New(termshark.TemplateToString(Templates, tmplName, TemplateData)), ), dialog.Options{ Buttons: dialog.CloseOnly, NoShadow: true, BackgroundStyle: gowid.MakePaletteRef("dialog"), BorderStyle: gowid.MakePaletteRef("dialog"), ButtonStyle: gowid.MakePaletteRef("dialog-button"), }, ) YesNo.Open(container, ratio(0.5), app) return YesNo } func OpenTemplatedDialogExt(container gowid.ISettableComposite, tmplName string, width gowid.IWidgetDimension, height gowid.IWidgetDimension, app gowid.IApp) *dialog.Widget { YesNo = dialog.New(framed.NewSpace( withscrollbar.New( scrollabletext.New( termshark.TemplateToString(Templates, tmplName, TemplateData), ), withscrollbar.Options{ HideIfContentFits: true, }, ), ), dialog.Options{ Buttons: dialog.CloseOnly, NoShadow: true, BackgroundStyle: gowid.MakePaletteRef("dialog"), BorderStyle: gowid.MakePaletteRef("dialog"), ButtonStyle: gowid.MakePaletteRef("dialog-button"), }, ) dialog.OpenExt(YesNo, container, width, height, app) return YesNo } func OpenPleaseWait(container gowid.ISettableComposite, app gowid.IApp) { PleaseWait.Open(container, fixed, app) if Fin != nil { Fin.Activate() } } func ClosePleaseWait(app gowid.IApp) { PleaseWait.Close(app) if Fin != nil { Fin.Deactivate() } } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.2.0/ui/filterconvs.go000066400000000000000000000063261377442047300170370ustar00rootroot00000000000000// Copyright 2019-2021 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 ui contains user-interface functions and helpers for termshark. package ui import ( "github.com/gcla/gowid" "github.com/gcla/gowid/widgets/columns" "github.com/gcla/gowid/widgets/holder" "github.com/gcla/gowid/widgets/menu" "github.com/gcla/termshark/v2/ui/menuutil" "github.com/gdamore/tcell" ) //====================================================================== var filterConvsMenu1 *menu.Widget var filterConvsMenu1Site *menu.SiteWidget var filterConvsMenu2 *menu.Widget type indirect struct { *holder.Widget } func buildFilterConvsMenu() { filterConvsMenu1Holder := &indirect{} filterConvsMenu2Holder := &indirect{} filterConvsMenu1 = menu.New("filterconvs1", filterConvsMenu1Holder, fixed, menu.Options{ Modal: true, CloseKeysProvided: true, CloseKeys: []gowid.IKey{ gowid.MakeKey('q'), gowid.MakeKeyExt(tcell.KeyLeft), gowid.MakeKeyExt(tcell.KeyEscape), gowid.MakeKeyExt(tcell.KeyCtrlC), }, }) filterConvsMenu2 = menu.New("filterconvs2", filterConvsMenu2Holder, fixed, menu.Options{ Modal: true, CloseKeysProvided: true, CloseKeys: []gowid.IKey{ gowid.MakeKey('q'), gowid.MakeKeyExt(tcell.KeyLeft), gowid.MakeKeyExt(tcell.KeyEscape), gowid.MakeKeyExt(tcell.KeyCtrlC), }, }) w := makeFilterConvsMenuWidget() filterConvsMenu1Site = menu.NewSite(menu.SiteOptions{ XOffset: -3, YOffset: -3, }) cols := columns.New([]gowid.IContainerWidget{ &gowid.ContainerWidget{IWidget: w, D: fixed}, &gowid.ContainerWidget{IWidget: filterConvsMenu1Site, D: fixed}, }) filterConvsMenu1Holder.Widget = holder.New(cols) w2 := makeFilterConvs2MenuWidget() filterConvsMenu2Holder.Widget = holder.New(w2) } func makeFilterConvsMenuWidget() gowid.IWidget { menuItems := make([]menuutil.SimpleMenuItem, 0) for i, s := range []string{ "Selected", "Not Selected", "...and Selected", "...or Selected", "...and not Selected", "...or not Selected", } { i2 := i menuItems = append(menuItems, menuutil.SimpleMenuItem{ Txt: s, Key: gowid.MakeKey('1' + rune(i)), CB: func(app gowid.IApp, w2 gowid.IWidget) { convsUi.filterSelectedIndex = FilterCombinator(i2) filterConvsMenu2.Open(filterConvsMenu1Site, app) }, }, ) } convListBox := menuutil.MakeMenuWithHotKeys(menuItems) return convListBox } func makeFilterConvs2MenuWidget() gowid.IWidget { menuItems := make([]menuutil.SimpleMenuItem, 0) for i, s := range []string{ "A ↔ B", "A → B", "B → A", "A ↔ Any", "A → Any", "Any → A", "Any ↔ B", "Any → B", "B → Any", } { i2 := i menuItems = append(menuItems, menuutil.SimpleMenuItem{ Txt: s, Key: gowid.MakeKey('1' + rune(i)), CB: func(app gowid.IApp, w2 gowid.IWidget) { filterConvsMenu1.Close(app) filterConvsMenu2.Close(app) convsUi.doFilterMenuOp(FilterMask(i2), app) }, }, ) } convListBox := menuutil.MakeMenuWithHotKeys(menuItems) return convListBox } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.2.0/ui/lastline.go000066400000000000000000000376521377442047300163220ustar00rootroot00000000000000// Copyright 2019-2021 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 ui contains user-interface functions and helpers for termshark. package ui import ( "fmt" "io/ioutil" "path/filepath" "strconv" "strings" "github.com/gcla/gowid" "github.com/gcla/gowid/gwutil" "github.com/gcla/gowid/vim" "github.com/gcla/termshark/v2" "github.com/gcla/termshark/v2/theme" "github.com/gcla/termshark/v2/widgets/mapkeys" "github.com/gcla/termshark/v2/widgets/minibuffer" "github.com/gdamore/tcell/terminfo" "github.com/gdamore/tcell/terminfo/dynamic" "github.com/rakyll/statik/fs" "github.com/shibukawa/configdir" _ "github.com/gcla/termshark/v2/assets/statik" ) //====================================================================== var notEnoughArgumentsErr = fmt.Errorf("Not enough arguments provided") var invalidSetCommandErr = fmt.Errorf("Invalid set command") var invalidReadCommandErr = fmt.Errorf("Invalid read command") var invalidRecentsCommandErr = fmt.Errorf("Invalid recents command") var invalidMapCommandErr = fmt.Errorf("Invalid map command") var invalidFilterCommandErr = fmt.Errorf("Invalid filter command") var invalidThemeCommandErr = fmt.Errorf("Invalid theme command") type minibufferFn func(gowid.IApp, ...string) error func (m minibufferFn) Run(app gowid.IApp, args ...string) error { return m(app, args...) } func (m minibufferFn) OfferCompletion() bool { return true } func (m minibufferFn) Arguments([]string, gowid.IApp) []minibuffer.IArg { return nil } type quietMinibufferFn func(gowid.IApp, ...string) error func (m quietMinibufferFn) Run(app gowid.IApp, args ...string) error { return m(app, args...) } func (m quietMinibufferFn) OfferCompletion() bool { return false } func (m quietMinibufferFn) Arguments([]string, gowid.IApp) []minibuffer.IArg { return nil } //====================================================================== type substrArg struct { candidates []string sub string } var _ minibuffer.IArg = substrArg{} func (s substrArg) OfferCompletion() bool { return true } // return these in sorted order func (s substrArg) Completions() []string { res := make([]string, 0) for _, str := range s.candidates { if strings.Contains(str, s.sub) { res = append(res, str) } } return res } //====================================================================== func newBoolArg(sub string) substrArg { return substrArg{ sub: sub, candidates: []string{"false", "true"}, } } func newOnOffArg(sub string) substrArg { return substrArg{ sub: sub, candidates: []string{"off", "on"}, } } func newSetArg(sub string) substrArg { return substrArg{ sub: sub, candidates: []string{ "auto-scroll", "copy-command-timeout", "dark-mode", "disable-shark-fin", "packet-colors", "pager", "nopager", "term", "noterm", }, } } func newHelpArg(sub string) substrArg { return substrArg{ sub: sub, candidates: []string{ "cmdline", "map", "set", "vim", }, } } //====================================================================== type unhelpfulArg struct { arg string } var _ minibuffer.IArg = unhelpfulArg{} func (s unhelpfulArg) OfferCompletion() bool { return false } // return these in sorted order func (s unhelpfulArg) Completions() []string { return nil } //====================================================================== type fileArg struct { substr string } var _ minibuffer.IArg = fileArg{} func (s fileArg) OfferCompletion() bool { return true } func (s fileArg) Completions() []string { matches, _ := filepath.Glob(s.substr + "*") if matches == nil { return []string{} } return matches } //====================================================================== type recentsArg struct { substr string } var _ minibuffer.IArg = recentsArg{} func (s recentsArg) OfferCompletion() bool { return true } func (s recentsArg) Completions() []string { matches := make([]string, 0) cfiles := termshark.ConfStringSlice("main.recent-files", []string{}) if cfiles != nil { for _, sc := range cfiles { scopy := sc if strings.Contains(scopy, s.substr) { matches = append(matches, scopy) } } } return matches } //====================================================================== type filterArg struct { substr string } var _ minibuffer.IArg = filterArg{} func (s filterArg) OfferCompletion() bool { return true } func (s filterArg) Completions() []string { matches := make([]string, 0) cfiles := termshark.ConfStringSlice("main.recent-filters", []string{}) if cfiles != nil { for _, sc := range cfiles { scopy := sc if strings.Contains(scopy, s.substr) { matches = append(matches, scopy) } } } return matches } //====================================================================== type themeArg struct { substr string modes []string } var _ minibuffer.IArg = themeArg{} func (s themeArg) OfferCompletion() bool { return true } func (s themeArg) Completions() []string { matches := make([]string, 0) // First gather built-in themes statikFS, err := fs.New() if err == nil { dir, err := statikFS.Open("/themes") if err == nil { info, err := dir.Readdir(-1) if err == nil { for _, finfo := range info { for _, mode := range s.modes { suff := fmt.Sprintf("-%s.toml", mode) if strings.HasSuffix(finfo.Name(), suff) { m := strings.TrimSuffix(finfo.Name(), suff) if strings.Contains(m, s.substr) { matches = append(matches, m) } } } } } } } // Then from filesystem stdConf := configdir.New("", "termshark") conf := stdConf.QueryFolderContainsFile("themes") if conf != nil { files, err := ioutil.ReadDir(filepath.Join(conf.Path, "themes")) if err == nil { for _, file := range files { for _, mode := range s.modes { suff := fmt.Sprintf("-%s.toml", mode) if strings.HasSuffix(file.Name(), suff) { m := strings.TrimSuffix(file.Name(), suff) if !termshark.StringInSlice(m, matches) { if strings.Contains(m, s.substr) { matches = append(matches, m) } } } } } } } return matches } //====================================================================== func stringIn(s string, a []string) bool { for _, s2 := range a { if s == s2 { return true } } return false } func parseOnOff(str string) (bool, error) { switch str { case "on", "ON", "On": return true, nil case "off", "OFF", "Off": return false, nil } return false, strconv.ErrSyntax } func validateTerm(term string) error { var err error _, err = terminfo.LookupTerminfo(term) if err != nil { _, _, err = dynamic.LoadTerminfo(term) } return err } type setCommand struct{} var _ minibuffer.IAction = setCommand{} func (d setCommand) Run(app gowid.IApp, args ...string) error { var err error var b bool var i uint64 switch len(args) { case 3: switch args[1] { case "auto-scroll": if b, err = parseOnOff(args[2]); err == nil { AutoScroll = b termshark.SetConf("main.auto-scroll", AutoScroll) OpenMessage(fmt.Sprintf("Packet auto-scroll is now %s", gwutil.If(b, "on", "off").(string)), appView, app) } case "copy-timeout": if i, err = strconv.ParseUint(args[2], 10, 32); err == nil { termshark.SetConf("main.copy-command-timeout", i) OpenMessage(fmt.Sprintf("Copy timeout is now %ds", i), appView, app) } case "dark-mode": if b, err = parseOnOff(args[2]); err == nil { DarkMode = b termshark.SetConf("main.dark-mode", DarkMode) } case "disable-shark-fin": if b, err = strconv.ParseBool(args[2]); err == nil { termshark.SetConf("main.disable-shark-fin", DarkMode) OpenMessage(fmt.Sprintf("Shark-saver is now %s", gwutil.If(b, "off", "on").(string)), appView, app) } case "packet-colors": if b, err = parseOnOff(args[2]); err == nil { PacketColors = b termshark.SetConf("main.packet-colors", PacketColors) OpenMessage(fmt.Sprintf("Packet colors are now %s", gwutil.If(b, "on", "off").(string)), appView, app) } case "term": if err = validateTerm(args[2]); err == nil { termshark.SetConf("main.term", args[2]) OpenMessage(fmt.Sprintf("Terminal type is now %s\n(Requires restart)", args[2]), appView, app) } case "pager": termshark.SetConf("main.pager", args[2]) OpenMessage(fmt.Sprintf("Pager is now %s", args[2]), appView, app) default: err = invalidSetCommandErr } case 2: switch args[1] { case "noterm": termshark.DeleteConf("main.term") OpenMessage("Terminal type is now unset\n(Requires restart)", appView, app) case "nopager": termshark.DeleteConf("main.pager") OpenMessage("Pager is now unset", appView, app) default: err = invalidSetCommandErr } } if err != nil { OpenMessage(fmt.Sprintf("Error: %s", err), appView, app) } return err } func (d setCommand) OfferCompletion() bool { return true } func (d setCommand) Arguments(toks []string, app gowid.IApp) []minibuffer.IArg { res := make([]minibuffer.IArg, 0) res = append(res, newSetArg(toks[0])) if len(toks) > 0 { onOffCmds := []string{"auto-scroll", "dark-mode", "packet-colors"} boolCmds := []string{"disable-shark-fin"} intCmds := []string{"disk-cache-size-mb", "copy-command-timeout"} pref := "" if len(toks) > 1 { pref = toks[1] } if stringIn(toks[0], boolCmds) { res = append(res, newBoolArg(pref)) } else if stringIn(toks[0], intCmds) { res = append(res, unhelpfulArg{}) } else if stringIn(toks[0], onOffCmds) { res = append(res, newOnOffArg(pref)) } } return res } //====================================================================== type readCommand struct { complete bool } var _ minibuffer.IAction = readCommand{} func (d readCommand) Run(app gowid.IApp, args ...string) error { var err error if len(args) != 2 { err = invalidReadCommandErr } else { RequestLoadPcapWithCheck(args[1], FilterWidget.Value(), NoGlobalJump, app) } if err != nil { OpenMessage(fmt.Sprintf("Error: %s", err), appView, app) } return err } func (d readCommand) OfferCompletion() bool { return d.complete } func (d readCommand) Arguments(toks []string, app gowid.IApp) []minibuffer.IArg { res := make([]minibuffer.IArg, 0) pref := "" if len(toks) > 0 { pref = toks[0] } res = append(res, fileArg{substr: pref}) return res } //====================================================================== type recentsCommand struct{} var _ minibuffer.IAction = recentsCommand{} func (d recentsCommand) Run(app gowid.IApp, args ...string) error { var err error if len(args) != 2 { err = invalidRecentsCommandErr } else { RequestLoadPcapWithCheck(args[1], FilterWidget.Value(), NoGlobalJump, app) } if err != nil { OpenMessage(fmt.Sprintf("Error: %s", err), appView, app) } return err } func (d recentsCommand) OfferCompletion() bool { return true } func (d recentsCommand) Arguments(toks []string, app gowid.IApp) []minibuffer.IArg { res := make([]minibuffer.IArg, 0) pref := "" if len(toks) > 0 { pref = toks[0] } res = append(res, recentsArg{substr: pref}) return res } //====================================================================== type filterCommand struct{} var _ minibuffer.IAction = filterCommand{} func (d filterCommand) Run(app gowid.IApp, args ...string) error { var err error if len(args) != 2 { err = invalidFilterCommandErr } else { setFocusOnDisplayFilter(app) FilterWidget.SetValue(args[1], app) } if err != nil { OpenMessage(fmt.Sprintf("Error: %s", err), appView, app) } return err } func (d filterCommand) OfferCompletion() bool { return true } func (d filterCommand) Arguments(toks []string, app gowid.IApp) []minibuffer.IArg { res := make([]minibuffer.IArg, 0) pref := "" if len(toks) > 0 { pref = toks[0] } res = append(res, filterArg{substr: pref}) return res } //====================================================================== type themeCommand struct{} var _ minibuffer.IAction = themeCommand{} func (d themeCommand) Run(app gowid.IApp, args ...string) error { var err error if len(args) != 2 { err = invalidThemeCommandErr } else { mode := theme.Mode(app.GetColorMode()).String() // more concise termshark.SetConf(fmt.Sprintf("main.theme-%s", mode), args[1]) theme.Load(args[1], app) SetupColors() OpenMessage(fmt.Sprintf("Set %s theme for terminal mode %v.", args[1], app.GetColorMode()), appView, app) } if err != nil { OpenMessage(fmt.Sprintf("Error: %s", err), appView, app) } return err } func (d themeCommand) OfferCompletion() bool { return true } func (d themeCommand) Arguments(toks []string, app gowid.IApp) []minibuffer.IArg { res := make([]minibuffer.IArg, 0) pref := "" if len(toks) > 0 { pref = toks[0] } modes := make([]string, 0, 3) switch app.GetColorMode() { case gowid.Mode24BitColors: modes = append(modes, "truecolor", "256") case gowid.Mode256Colors: modes = append(modes, "256") default: modes = append(modes, "16") } res = append(res, themeArg{substr: pref, modes: modes}) return res } //====================================================================== type mapCommand struct { w *mapkeys.Widget } var _ minibuffer.IAction = mapCommand{} func (d mapCommand) Run(app gowid.IApp, args ...string) error { var err error if len(args) == 3 { key1 := vim.VimStringToKeys(args[1]) if len(key1) != 1 { err = fmt.Errorf("Invalid: first map argument must be a single key (got '%s')", args[1]) } else { keys2 := vim.VimStringToKeys(args[2]) termshark.AddKeyMapping(termshark.KeyMapping{From: key1[0], To: keys2}) mappings := termshark.LoadKeyMappings() for _, mapping := range mappings { d.w.AddMapping(mapping.From, mapping.To, app) } } } else if len(args) == 1 { OpenTemplatedDialogExt(appView, "Key Mappings", fixed, ratio(0.6), app) } else { err = invalidMapCommandErr } if err != nil { OpenMessage(fmt.Sprintf("Error: %s", err), appView, app) } return err } func (d mapCommand) OfferCompletion() bool { return true } func (d mapCommand) Arguments(toks []string, app gowid.IApp) []minibuffer.IArg { res := make([]minibuffer.IArg, 0) if len(toks) == 2 { res = append(res, unhelpfulArg{}, unhelpfulArg{}) } return res } //====================================================================== type unmapCommand struct { w *mapkeys.Widget } var _ minibuffer.IAction = unmapCommand{} func (d unmapCommand) Run(app gowid.IApp, args ...string) error { var err error if len(args) != 2 { err = invalidMapCommandErr } else { key1 := vim.VimStringToKeys(args[1]) d.w.ClearMappings(app) termshark.RemoveKeyMapping(key1[0]) mappings := termshark.LoadKeyMappings() for _, mapping := range mappings { d.w.AddMapping(mapping.From, mapping.To, app) } } if err != nil { OpenMessage(fmt.Sprintf("Error: %s", err), appView, app) } return err } func (d unmapCommand) OfferCompletion() bool { return true } func (d unmapCommand) Arguments(toks []string, app gowid.IApp) []minibuffer.IArg { res := make([]minibuffer.IArg, 0) res = append(res, unhelpfulArg{}) return res } //====================================================================== type helpCommand struct{} var _ minibuffer.IAction = helpCommand{} func (d helpCommand) Run(app gowid.IApp, args ...string) error { var err error switch len(args) { case 2: switch args[1] { case "cmdline": OpenTemplatedDialog(appView, "CmdLineHelp", app) case "map": OpenTemplatedDialog(appView, "MapHelp", app) case "set": OpenTemplatedDialog(appView, "SetHelp", app) default: OpenTemplatedDialog(appView, "VimHelp", app) } default: OpenTemplatedDialog(appView, "UIHelp", app) } return err } func (d helpCommand) OfferCompletion() bool { return true } func (d helpCommand) Arguments(toks []string, app gowid.IApp) []minibuffer.IArg { res := make([]minibuffer.IArg, 0) if len(toks) == 1 { res = append(res, newHelpArg(toks[0])) } return res } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.2.0/ui/logsui.go000066400000000000000000000031461377442047300160000ustar00rootroot00000000000000// Copyright 2019-2021 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 !windows // Package ui contains user-interface functions and helpers for termshark. package ui import ( "fmt" "github.com/gcla/gowid" "github.com/gcla/gowid/widgets/holder" "github.com/gcla/gowid/widgets/terminal" "github.com/gcla/termshark/v2/widgets/logviewer" ) //====================================================================== // Dynamically load conv. If the convs window was last opened with a different filter, and the "limit to // filter" checkbox is checked, then the data needs to be reloaded. func openLogsUi(app gowid.IApp) { logsUi, err := logviewer.New(gowid.WidgetCallback{"cb", func(app gowid.IApp, w gowid.IWidget) { t := w.(*terminal.Widget) ecode := t.Cmd.ProcessState.ExitCode() // -1 for signals - don't show an error for that if ecode != 0 && ecode != -1 { d := OpenError(fmt.Sprintf( "Could not run logs viewer\n\n%s", t.Cmd.ProcessState), app) d.OnOpenClose(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, w gowid.IWidget) { closeLogsUi(app) })) } else { closeLogsUi(app) } }, }, ) if err != nil { OpenError(fmt.Sprintf("Error launching terminal: %v", err), app) return } logsView := holder.New(logsUi) appViewNoKeys.SetSubWidget(logsView, app) } func closeLogsUi(app gowid.IApp) { appViewNoKeys.SetSubWidget(mainView, app) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.2.0/ui/logsui_windows.go000066400000000000000000000013431377442047300175470ustar00rootroot00000000000000// Copyright 2019-2021 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 ui contains user-interface functions and helpers for termshark. package ui import ( "github.com/gcla/gowid" ) // Dynamically load conv. If the convs window was last opened with a different filter, and the "limit to // filter" checkbox is checked, then the data needs to be reloaded. func openLogsUi(app gowid.IApp) { } func closeLogsUi(app gowid.IApp) { } //====================================================================== //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.2.0/ui/menu.go000066400000000000000000000124561377442047300154460ustar00rootroot00000000000000// Copyright 2019-2021 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 ui contains user-interface functions and helpers for termshark. package ui import ( "fmt" "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/divider" "github.com/gcla/gowid/widgets/framed" "github.com/gcla/gowid/widgets/hpadding" "github.com/gcla/gowid/widgets/keypress" "github.com/gcla/gowid/widgets/menu" "github.com/gcla/gowid/widgets/pile" "github.com/gcla/gowid/widgets/selectable" "github.com/gcla/gowid/widgets/styled" "github.com/gcla/gowid/widgets/text" "github.com/gcla/termshark/v2/widgets/appkeys" "github.com/gdamore/tcell" ) //====================================================================== type SimpleMenuItem struct { Txt string Key gowid.IKey CB gowid.WidgetChangedFunction } func MakeMenuDivider() SimpleMenuItem { return SimpleMenuItem{} } func MakeMenuWithHotKeys(items []SimpleMenuItem) gowid.IWidget { menu1Widgets := make([]gowid.IWidget, len(items)) menu1HotKeys := make([]gowid.IWidget, len(items)) // Figure out the length of the longest hotkey string representation max := 0 for _, w := range items { if w.Txt != "" { k := fmt.Sprintf("%v", w.Key) if len(k) > max { max = len(k) } } } // Construct the hotkey widget and menu item widget for each menu entry for i, w := range items { if w.Txt != "" { load1B := button.NewBare(text.New(w.Txt)) var ks string if w.Key != nil { ks = fmt.Sprintf("%v", w.Key) } load1K := button.NewBare(text.New(ks)) load1CB := gowid.MakeWidgetCallback("cb", w.CB) load1B.OnClick(load1CB) if w.Key != nil { load1K.OnClick(load1CB) } menu1Widgets[i] = load1B menu1HotKeys[i] = load1K } } for i, w := range menu1Widgets { if w != nil { menu1Widgets[i] = styled.NewInvertedFocus(selectable.New(w), gowid.MakePaletteRef("default")) } } for i, w := range menu1HotKeys { if w != nil { menu1HotKeys[i] = styled.NewInvertedFocus(w, gowid.MakePaletteRef("default")) } } // Build the menu "row" for each menu entry menu1Widgets2 := make([]gowid.IWidget, len(menu1Widgets)) for i, w := range menu1Widgets { if w == nil { menu1Widgets2[i] = divider.NewUnicode() } else { 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( selectable.NewUnselectable( // don't want to be able to navigate to the hotkey itself 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 { var dim gowid.IWidgetDimension if menu1Widgets[i] != nil { dim = fixed } else { dim = gowid.RenderFlow{} } menu1cwidgets[i] = &gowid.ContainerWidget{ IWidget: w, D: dim, } } keys := make([]gowid.IKey, 0) for _, i := range items { if i.Key != nil { keys = append(keys, i.Key) } } // Surround the menu with a widget that captures the hotkey keypresses 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 r.Key != nil && gowid.KeysEqual(k, r.Key) { r.CB(app, w) break } } })) return menuListBox1 } //====================================================================== type NextMenu struct { Cur *menu.Widget Next *menu.Widget // nil if menu is nil Site *menu.SiteWidget Container gowid.IFocus // container holding menu buttons, etc Focus int // index of next menu in container } func MakeMenuNavigatingKeyPress(left *NextMenu, right *NextMenu) appkeys.KeyInputFn { return func(evk *tcell.EventKey, app gowid.IApp) bool { return MenuNavigatingKeyPress(evk, left, right, app) } } func MenuNavigatingKeyPress(evk *tcell.EventKey, left *NextMenu, right *NextMenu, app gowid.IApp) bool { res := false switch evk.Key() { case tcell.KeyLeft: if left != nil { left.Cur.Close(app) left.Next.Open(left.Site, app) left.Container.SetFocus(app, left.Focus) // highlight next menu selector res = true } case tcell.KeyRight: if right != nil { right.Cur.Close(app) right.Next.Open(right.Site, app) right.Container.SetFocus(app, right.Focus) // highlight next menu selector res = true } } return res } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.2.0/ui/menuutil/000077500000000000000000000000001377442047300160055ustar00rootroot00000000000000termshark-2.2.0/ui/menuutil/menu.go000066400000000000000000000125311377442047300173020ustar00rootroot00000000000000// Copyright 2019-2021 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 menuutil contains user-interface functions and helpers for termshark. package menuutil import ( "fmt" "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/divider" "github.com/gcla/gowid/widgets/framed" "github.com/gcla/gowid/widgets/hpadding" "github.com/gcla/gowid/widgets/keypress" "github.com/gcla/gowid/widgets/menu" "github.com/gcla/gowid/widgets/pile" "github.com/gcla/gowid/widgets/selectable" "github.com/gcla/gowid/widgets/styled" "github.com/gcla/gowid/widgets/text" "github.com/gcla/termshark/v2/widgets/appkeys" "github.com/gdamore/tcell" ) //====================================================================== type SimpleMenuItem struct { Txt string Key gowid.IKey CB gowid.WidgetChangedFunction } func MakeMenuDivider() SimpleMenuItem { return SimpleMenuItem{} } func MakeMenuWithHotKeys(items []SimpleMenuItem) gowid.IWidget { menu1Widgets := make([]gowid.IWidget, len(items)) menu1HotKeys := make([]gowid.IWidget, len(items)) // Figure out the length of the longest hotkey string representation max := 0 for _, w := range items { if w.Txt != "" { k := fmt.Sprintf("%v", w.Key) if len(k) > max { max = len(k) } } } // Construct the hotkey widget and menu item widget for each menu entry for i, w := range items { if w.Txt != "" { load1B := button.NewBare(text.New(w.Txt)) var ks string if w.Key != nil { ks = fmt.Sprintf("%v", w.Key) } load1K := button.NewBare(text.New(ks)) load1CB := gowid.MakeWidgetCallback("cb", w.CB) load1B.OnClick(load1CB) if w.Key != nil { load1K.OnClick(load1CB) } menu1Widgets[i] = load1B menu1HotKeys[i] = load1K } } for i, w := range menu1Widgets { if w != nil { menu1Widgets[i] = styled.NewInvertedFocus(selectable.New(w), gowid.MakePaletteRef("default")) } } for i, w := range menu1HotKeys { if w != nil { menu1HotKeys[i] = styled.NewInvertedFocus(w, gowid.MakePaletteRef("default")) } } fixed := gowid.RenderFixed{} // Build the menu "row" for each menu entry menu1Widgets2 := make([]gowid.IWidget, len(menu1Widgets)) for i, w := range menu1Widgets { if w == nil { menu1Widgets2[i] = divider.NewUnicode() } else { 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( selectable.NewUnselectable( // don't want to be able to navigate to the hotkey itself 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 { var dim gowid.IWidgetDimension if menu1Widgets[i] != nil { dim = fixed } else { dim = gowid.RenderFlow{} } menu1cwidgets[i] = &gowid.ContainerWidget{ IWidget: w, D: dim, } } keys := make([]gowid.IKey, 0) for _, i := range items { if i.Key != nil { keys = append(keys, i.Key) } } // Surround the menu with a widget that captures the hotkey keypresses 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 r.Key != nil && gowid.KeysEqual(k, r.Key) { r.CB(app, w) break } } })) return menuListBox1 } //====================================================================== type NextMenu struct { Cur *menu.Widget Next *menu.Widget // nil if menu is nil Site *menu.SiteWidget Container gowid.IFocus // container holding menu buttons, etc Focus int // index of next menu in container } func MakeMenuNavigatingKeyPress(left *NextMenu, right *NextMenu) appkeys.KeyInputFn { return func(evk *tcell.EventKey, app gowid.IApp) bool { return MenuNavigatingKeyPress(evk, left, right, app) } } func MenuNavigatingKeyPress(evk *tcell.EventKey, left *NextMenu, right *NextMenu, app gowid.IApp) bool { res := false switch evk.Key() { case tcell.KeyLeft: if left != nil { left.Cur.Close(app) left.Next.Open(left.Site, app) left.Container.SetFocus(app, left.Focus) // highlight next menu selector res = true } case tcell.KeyRight: if right != nil { right.Cur.Close(app) right.Next.Open(right.Site, app) right.Container.SetFocus(app, right.Focus) // highlight next menu selector res = true } } return res } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.2.0/ui/messages.go000066400000000000000000000165661377442047300163170ustar00rootroot00000000000000// Copyright 2019-2021 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 ui contains user-interface functions and helpers for termshark. package ui import ( "fmt" "io" "log" "sync" "text/template" "github.com/blang/semver" "github.com/gcla/termshark/v2" "github.com/jessevdk/go-flags" ) //====================================================================== // For fixing off-by-one errors in packet marks - NOT NEEDED now var funcMap = template.FuncMap{ "inc": func(i int) int { return i + 1 }, } var TemplateData map[string]interface{} var Templates = template.Must(template.New("Help").Funcs(funcMap).Parse(` {{define "NameVer"}}termshark {{.Version}}{{end}} {{define "TsharkVer"}}using tshark {{.TsharkVersion}} (from {{.TsharkAbsolutePath}}){{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://termshark.io 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 "UIUserGuide"}}{{.UserGuideURL}} {{.CopyCommandMessage}}{{end}} {{define "UIFAQ"}}{{.FAQURL}} {{.CopyCommandMessage}}{{end}} {{define "UIBug"}}{{.BugURL}} {{.CopyCommandMessage}}{{end}} {{define "UIFeature"}}{{.FeatureURL}} {{.CopyCommandMessage}}{{end}} {{define "UIHelp"}}{{template "NameVer" .}} A wireshark-inspired tui for tshark. Analyze network traffic interactively from your terminal. / - Go to display filter/stream search q - Quit tab - Switch panes c - Switch to copy-mode | - Cycle through pane layouts \ - Toggle pane zoom esc - Activate menu +/- - Adjust horizontal split - Adjust vertical split : - Activate cmdline mode (see help cmdline) z - Maximize/restore any modal dialog ? - 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 "VimHelp"}}{{template "NameVer" .}} Navigate the UI using vim-style keys. hjkl - Move left/down/up/right in various views gg - Go to the top of the current table G - Go to the bottom of the current table 5gg - Go to the 5th row of the table C-w C-w - Switch panes (same as tab) C-w = - Equalize pane spacing ma - Mark current packet (use a through z) 'a - Jump to packet marked 'a' mA - Mark current packet + pcap (use A through Z) 'A - Jump to packet + pcap marked 'A' '' - After a jump; jump back to prior packet ZZ - Quit without confirmation See also help cmdline.{{end}} {{define "CmdLineHelp"}}{{template "NameVer" .}} Activate cmdline mode with the : key. Hit tab to see and choose possible completions. capinfo - Capture file properties clear - Clear current pcap convs - Open conversations view filter - Choose a display filter from recently-used help - Various help dialogs load - Load a pcap from the filesystem logs - Show termshark's log file (Unix-only) map - Map a keypress to a key sequence (see help map) marks - Show file-local and global packet marks no-theme - Clear theme for the current terminal color mode quit - Quit termshark recents - Load a pcap from those recently-used set - Set various config properties (see help set) streams - Open stream reassembly view theme - Choose a theme for the current terminal color mode unmap - Remove a keypress mapping{{end}} {{define "SetHelp"}}{{template "NameVer" .}} Use the cmdline set command to change configuration. Type :set and hit tab for options. auto-scroll - scroll during live captures copy-timeout - wait this long before failing a copy dark-mode - enable or disable dark-mode disable-shark-fin - switch off the secret shark fin packet-colors - use colors in the packet list view pager - pager (used for termshark's log file) nopager - disable the pager (use PAGER instead) term - make termshark assume this terminal type noterm - disable the terminal type (use TERM){{end}} {{define "MapHelp"}}{{template "NameVer" .}} Use the cmdline map command to set key macros e.g. map ZZ - hit f1 key to quit Use vim-style syntax for key-presses. Printable characters represent themselves. Compound keys can be: - , , , , , , Use the unmap command to remove a mapping.{{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 - Widen selection right - Narrow selection '?' - Display copy-mode help {{end}} {{define "Marks"}}{{if not .Marks}}No local marks are set{{else}}Mark Packet Summary{{range $key, $value := .Marks }} {{printf " %c" $key}}{{printf "%6d" $value.Pos}} {{printf "%s" $value.Summary}}{{end}}{{end}} {{if not .GlobalMarks}}No cross-file marks are set{{else}}Mark Packet File Summary{{range $key, $value := .GlobalMarks }} {{printf " %-4c" $key}} {{printf "%-7d" $value.Pos}}{{printf "%-18s" $value.Base}}{{printf "%s" $value.Summary}}{{end}}{{end}}{{end}} {{define "Key Mappings"}}{{if .Maps.None}}No key mappings are set{{else}} From To {{range $mapping := .Maps.Get }} {{printf " %-14v" $mapping.From}}{{printf "%v" $mapping.To}} {{end}}{{end}} {{end}} `)) //====================================================================== var DoOnce sync.Once func EnsureTemplateData() { DoOnce.Do(func() { TemplateData = make(map[string]interface{}) }) } func init() { EnsureTemplateData() TemplateData["Version"] = termshark.Version TemplateData["FAQURL"] = termshark.FAQURL TemplateData["UserGuideURL"] = termshark.UserGuideURL TemplateData["BugURL"] = termshark.BugURL TemplateData["FeatureURL"] = termshark.FeatureURL } func WriteHelp(p *flags.Parser, w io.Writer) { if err := Templates.ExecuteTemplate(w, "Header", TemplateData); err != nil { log.Fatal(err) } fmt.Fprintln(w) fmt.Fprintln(w) p.WriteHelp(w) if err := Templates.ExecuteTemplate(w, "Footer", TemplateData); err != nil { log.Fatal(err) } fmt.Fprintln(w) fmt.Fprintln(w) } func WriteVersion(p *flags.Parser, w io.Writer) { if err := Templates.ExecuteTemplate(w, "NameVer", TemplateData); err != nil { log.Fatal(err) } fmt.Fprintln(w) } func WriteTsharkVersion(p *flags.Parser, bin string, ver semver.Version, w io.Writer) { TemplateData["TsharkVersion"] = ver.String() TemplateData["TsharkAbsolutePath"] = bin if err := Templates.ExecuteTemplate(w, "TsharkVer", TemplateData); err != nil { log.Fatal(err) } fmt.Fprintln(w) } func WriteMarks(p *flags.Parser, marks map[rune]int, w io.Writer) { if err := Templates.ExecuteTemplate(w, "Marks", TemplateData); err != nil { log.Fatal(err) } fmt.Fprintln(w) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.2.0/ui/palette.go000066400000000000000000000232221377442047300161310ustar00rootroot00000000000000// Copyright 2019-2021 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 ui contains user-interface functions and helpers for termshark. package ui import ( "fmt" "github.com/gcla/gowid" "github.com/gcla/termshark/v2/theme" log "github.com/sirupsen/logrus" ) //====================================================================== var ( RegularPalette gowid.Palette DarkModePalette gowid.Palette ) func SetupColors() { //====================================================================== // Regular mode // RegularPalette = gowid.Palette{ "default": gowid.MakePaletteEntry(lfg("default"), lbg("default")), "title": gowid.MakeForeground(lfg("title")), "packet-list-row-focus": gowid.MakePaletteEntry(lfg("packet-list-row-focus"), lbg("packet-list-row-focus")), "packet-list-row-selected": gowid.MakePaletteEntry(lfg("packet-list-row-selected"), lbg("packet-list-row-selected")), "packet-list-cell-focus": gowid.MakePaletteEntry(lfg("packet-list-cell-focus"), lbg("packet-list-cell-focus")), "packet-list-cell-selected": gowid.MakePaletteEntry(lfg("packet-list-cell-selected"), lbg("packet-list-cell-selected")), "packet-struct-focus": gowid.MakePaletteEntry(lfg("packet-struct-focus"), lbg("packet-struct-focus")), "packet-struct-selected": gowid.MakePaletteEntry(lfg("packet-struct-selected"), lbg("packet-struct-selected")), "filter-menu": gowid.MakeStyledPaletteEntry(lfg("filter-menu"), lbg("filter-menu"), gowid.StyleBold), "filter-valid": gowid.MakePaletteEntry(lfg("filter-valid"), lbg("filter-valid")), "filter-invalid": gowid.MakePaletteEntry(lfg("filter-invalid"), lbg("filter-invalid")), "filter-intermediate": gowid.MakePaletteEntry(lfg("filter-intermediate"), lbg("filter-intermediate")), "dialog": gowid.MakePaletteEntry(lfg("dialog"), lbg("dialog")), "dialog-button": gowid.MakePaletteEntry(lfg("dialog-button"), lbg("dialog-button")), "cmdline": gowid.MakePaletteEntry(lfg("cmdline"), lbg("cmdline")), "cmdline-button": gowid.MakePaletteEntry(lfg("cmdline-button"), lbg("cmdline-button")), "cmdline-border": gowid.MakePaletteEntry(lfg("cmdline-border"), lbg("cmdline-border")), "button": gowid.MakePaletteEntry(lfg("button"), lbg("button")), "button-focus": gowid.MakePaletteEntry(lfg("button-focus"), lbg("button-focus")), "button-selected": gowid.MakePaletteEntry(lfg("button-selected"), lbg("button-selected")), "progress-default": gowid.MakeStyledPaletteEntry(lfg("progress-default"), lbg("progress-default"), gowid.StyleBold), "progress-complete": gowid.MakeStyleMod(gowid.MakePaletteRef("progress-default"), gowid.MakeBackground(lbg("progress-complete"))), "progress-spinner": gowid.MakePaletteEntry(lfg("progress-spinner"), lbg("progress-spinner")), "hex-byte-selected": gowid.MakePaletteEntry(lfg("hex-byte-selected"), lbg("hex-byte-selected")), "hex-byte-unselected": gowid.MakePaletteEntry(lfg("hex-byte-unselected"), lbg("hex-byte-unselected")), "hex-field-selected": gowid.MakePaletteEntry(lfg("hex-field-selected"), lbg("hex-field-selected")), "hex-field-unselected": gowid.MakePaletteEntry(lfg("hex-field-unselected"), lbg("hex-field-unselected")), "hex-layer-selected": gowid.MakePaletteEntry(lfg("hex-layer-selected"), lbg("hex-layer-selected")), "hex-layer-unselected": gowid.MakePaletteEntry(lfg("hex-layer-unselected"), lbg("hex-layer-unselected")), "hex-interval-selected": gowid.MakePaletteEntry(lfg("hex-interval-selected"), lbg("hex-interval-selected")), "hex-interval-unselected": gowid.MakePaletteEntry(lfg("hex-interval-unselected"), lbg("hex-interval-unselected")), "copy-mode-label": gowid.MakePaletteEntry(lfg("copy-mode-label"), lbg("copy-mode-label")), "copy-mode": gowid.MakePaletteEntry(lfg("copy-mode"), lbg("copy-mode")), "copy-mode-alt": gowid.MakePaletteEntry(lfg("copy-mode-alt"), lbg("copy-mode-alt")), "stream-client": gowid.MakePaletteEntry(lfg("stream-client"), lbg("stream-client")), "stream-server": gowid.MakePaletteEntry(lfg("stream-server"), lbg("stream-server")), "stream-match": gowid.MakePaletteEntry(lfg("stream-match"), lbg("stream-match")), "stream-search": gowid.MakePaletteEntry(lfg("stream-search"), lbg("stream-search")), } //====================================================================== // Dark mode // DarkModePalette = gowid.Palette{ "default": gowid.MakePaletteEntry(dfg("default"), dbg("default")), "title": gowid.MakeForeground(dfg("title")), "current-capture": gowid.MakeForeground(dfg("current-capture")), "packet-list-row-focus": gowid.MakePaletteEntry(dfg("packet-list-row-focus"), dbg("packet-list-row-focus")), "packet-list-row-selected": gowid.MakePaletteEntry(dfg("packet-list-row-selected"), dbg("packet-list-row-selected")), "packet-list-cell-focus": gowid.MakePaletteEntry(dfg("packet-list-cell-focus"), dbg("packet-list-cell-focus")), "packet-list-cell-selected": gowid.MakePaletteEntry(dfg("packet-list-cell-selected"), dbg("packet-list-cell-selected")), "packet-struct-focus": gowid.MakePaletteEntry(dfg("packet-struct-focus"), dbg("packet-struct-focus")), "packet-struct-selected": gowid.MakePaletteEntry(dfg("packet-struct-selected"), dbg("packet-struct-selected")), "filter-menu": gowid.MakeStyledPaletteEntry(dfg("filter-menu"), dbg("filter-menu"), gowid.StyleBold), "filter-valid": gowid.MakePaletteEntry(dfg("filter-valid"), dbg("filter-valid")), "filter-invalid": gowid.MakePaletteEntry(dfg("filter-invalid"), dbg("filter-invalid")), "filter-intermediate": gowid.MakePaletteEntry(dfg("filter-intermediate"), dbg("filter-intermediate")), "dialog": gowid.MakePaletteEntry(dfg("dialog"), dbg("dialog")), "dialog-button": gowid.MakePaletteEntry(dfg("dialog-button"), dbg("dialog-button")), "cmdline": gowid.MakePaletteEntry(dfg("cmdline"), dbg("cmdline")), "cmdline-button": gowid.MakePaletteEntry(dfg("cmdline-button"), dbg("cmdline-button")), "cmdline-border": gowid.MakePaletteEntry(dfg("cmdline-border"), dbg("cmdline-border")), "button": gowid.MakePaletteEntry(dfg("button"), dbg("button")), "button-focus": gowid.MakePaletteEntry(dfg("button-focus"), dbg("button-focus")), "button-selected": gowid.MakePaletteEntry(dfg("button-selected"), dbg("button-selected")), "progress-default": gowid.MakeStyledPaletteEntry(dfg("progress-default"), dbg("progress-default"), gowid.StyleBold), "progress-complete": gowid.MakeStyleMod(gowid.MakePaletteRef("progress-default"), gowid.MakeBackground(dbg("progress-complete"))), "progress-spinner": gowid.MakePaletteEntry(dfg("spinner"), dbg("spinner")), "hex-byte-selected": gowid.MakePaletteEntry(dfg("hex-byte-selected"), dbg("hex-byte-selected")), "hex-byte-unselected": gowid.MakePaletteEntry(dfg("hex-byte-unselected"), dbg("hex-byte-unselected")), "hex-field-selected": gowid.MakePaletteEntry(dfg("hex-field-selected"), dbg("hex-field-selected")), "hex-field-unselected": gowid.MakePaletteEntry(dfg("hex-field-unselected"), dbg("hex-field-unselected")), "hex-layer-selected": gowid.MakePaletteEntry(dfg("hex-layer-selected"), dbg("hex-layer-selected")), "hex-layer-unselected": gowid.MakePaletteEntry(dfg("hex-layer-unselected"), dbg("hex-layer-unselected")), "hex-interval-selected": gowid.MakePaletteEntry(dfg("hex-interval-selected"), dbg("hex-interval-selected")), "hex-interval-unselected": gowid.MakePaletteEntry(dfg("hex-interval-unselected"), dbg("hex-interval-unselected")), "stream-client": gowid.MakePaletteEntry(dfg("stream-client"), dbg("stream-client")), "stream-server": gowid.MakePaletteEntry(dfg("stream-server"), dbg("stream-server")), "copy-mode-label": gowid.MakePaletteEntry(dfg("copy-mode-label"), dbg("copy-mode-label")), "copy-mode": gowid.MakePaletteEntry(dfg("copy-mode"), dbg("copy-mode")), "copy-mode-alt": gowid.MakePaletteEntry(dfg("copy-mode-alt"), dbg("copy-mode-alt")), "stream-match": gowid.MakePaletteEntry(dfg("stream-match"), dbg("stream-match")), "stream-search": gowid.MakePaletteEntry(dfg("stream-search"), dbg("stream-search")), } } func dfg(key string) gowid.IColor { return tomlCol(key, theme.Foreground, "dark") } func dbg(key string) gowid.IColor { return tomlCol(key, theme.Background, "dark") } func lfg(key string) gowid.IColor { return tomlCol(key, theme.Foreground, "light") } func lbg(key string) gowid.IColor { return tomlCol(key, theme.Background, "light") } func tomlCol(key string, layer theme.Layer, hue string) gowid.IColor { rule := fmt.Sprintf("%s.%s", hue, key) col, err := theme.MakeColorSafe(rule, layer) if err == nil { return col } else { // Warn if the user has defined themes.rules.etcetc, but the resulting // color can't be resolved. When this is called, we should always have a // theme loaded because we fall back to the "default" theme if no other is // specified. log.Warnf("Could not understand configured theme color '%s'", key) } return gowid.ColorBlack } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.2.0/ui/prochandlers.go000066400000000000000000000205471377442047300171660ustar00rootroot00000000000000// Copyright 2019-2021 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 ui contains user-interface functions and helpers for termshark. package ui import ( "fmt" "os" "strings" "time" "github.com/gcla/gowid" "github.com/gcla/gowid/widgets/table" "github.com/gcla/termshark/v2" "github.com/gcla/termshark/v2/pcap" log "github.com/sirupsen/logrus" ) //====================================================================== type NoHandlers struct{} //====================================================================== type updateCurrentCaptureInTitle struct { Ld *pcap.PacketLoader } var _ pcap.IBeforeBegin = updateCurrentCaptureInTitle{} var _ pcap.IClear = updateCurrentCaptureInTitle{} func MakeUpdateCurrentCaptureInTitle() updateCurrentCaptureInTitle { return updateCurrentCaptureInTitle{ Ld: Loader, } } func (t updateCurrentCaptureInTitle) BeforeBegin(code pcap.HandlerCode, app gowid.IApp) { if code&pcap.PsmlCode != 0 { currentCapture.SetText(t.Ld.String(), app) currentCaptureWidgetHolder.SetSubWidget(currentCaptureWidget, app) } } func (t updateCurrentCaptureInTitle) OnClear(code pcap.HandlerCode, app gowid.IApp) { currentCaptureWidgetHolder.SetSubWidget(nullw, app) } //====================================================================== type updatePacketViews struct { Ld *pcap.PacketLoader } var _ pcap.IOnError = updatePacketViews{} var _ pcap.IClear = updatePacketViews{} var _ pcap.IBeforeBegin = updatePacketViews{} var _ pcap.IAfterEnd = updatePacketViews{} func MakePacketViewUpdater() updatePacketViews { res := updatePacketViews{} res.Ld = Loader return res } func (t updatePacketViews) OnClear(code pcap.HandlerCode, app gowid.IApp) { clearPacketViews(app) } func (t updatePacketViews) BeforeBegin(code pcap.HandlerCode, app gowid.IApp) { if code&pcap.PsmlCode == 0 { return } ch2 := Loader.PsmlFinishedChan clearPacketViews(app) t.Ld.PsmlLoader.Lock() defer t.Ld.PsmlLoader.Unlock() setPacketListWidgets(t.Ld, 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) { updatePacketListWithData(Loader, app) })) } termshark.RunOnDoubleTicker(ch2, fn2, time.Duration(100)*time.Millisecond, time.Duration(2000)*time.Millisecond, 10) }, Goroutinewg) } func (t updatePacketViews) AfterEnd(code pcap.HandlerCode, app gowid.IApp) { if code&pcap.PsmlCode == 0 { return } updatePacketListWithData(t.Ld, app) StopEmptyStructViewTimer() StopEmptyHexViewTimer() log.Infof("Load operation complete") } func (t updatePacketViews) OnError(code pcap.HandlerCode, app gowid.IApp, err error) { if code&pcap.PsmlCode == 0 { return } log.Error(err) if !Running { fmt.Fprintf(os.Stderr, "%v\n", err) RequestQuit() } else { var errstr string if kverr, ok := err.(gowid.KeyValueError); ok { errstr = fmt.Sprintf("%v\n\n", kverr.Cause()) kvs := make([]string, 0, len(kverr.KeyVals)) for k, v := range kverr.KeyVals { kvs = append(kvs, fmt.Sprintf("%v: %v", k, v)) } errstr = errstr + strings.Join(kvs, "\n") } else { errstr = fmt.Sprintf("%v", err) } OpenLongError(errstr, app) StopEmptyStructViewTimer() StopEmptyHexViewTimer() } } //====================================================================== type SimpleErrors struct{} var _ pcap.IOnError = SimpleErrors{} func (t SimpleErrors) OnError(code pcap.HandlerCode, app gowid.IApp, err error) { if code&pcap.NoneCode == 0 { return } log.Error(err) // Hack to avoid picking up errors at other parts of the load // cycle. There should be specific handlers for specific errors. app.Run(gowid.RunFunction(func(app gowid.IApp) { OpenError(fmt.Sprintf("%v", err), app) })) } //====================================================================== type SaveRecents struct { Pcap string Filter string } var _ pcap.IBeforeBegin = SaveRecents{} func MakeSaveRecents(pcap string, filter string) SaveRecents { return SaveRecents{ Pcap: pcap, Filter: filter, } } func (t SaveRecents) BeforeBegin(code pcap.HandlerCode, app gowid.IApp) { if code&pcap.PsmlCode == 0 { return } // Run on main goroutine to avoid problems flagged by -race if t.Pcap != "" { termshark.AddToRecentFiles(t.Pcap) } if t.Filter != "" { // Run on main goroutine to avoid problems flagged by -race termshark.AddToRecentFilters(t.Filter) } } //====================================================================== type CancelledMessage struct{} var _ pcap.IAfterEnd = CancelledMessage{} func (t CancelledMessage) AfterEnd(code pcap.HandlerCode, app gowid.IApp) { if code&pcap.PsmlCode == 0 { return } // Run on main goroutine to avoid problems flagged by -race if Loader.LoadWasCancelled() { // Only do this if the user isn't quitting the app, // otherwise it looks clumsy. if !QuitRequested { OpenError("Loading was cancelled.", app) } } } //====================================================================== type StartUIWhenThereArePackets struct{} var _ pcap.IPsmlHeader = StartUIWhenThereArePackets{} func (t StartUIWhenThereArePackets) OnPsmlHeader(code pcap.HandlerCode, app gowid.IApp) { StartUIOnce.Do(func() { close(StartUIChan) }) } //====================================================================== type ClearMarksHandler struct{} var _ pcap.IClear = checkGlobalJumpAfterPsml{} var _ pcap.INewSource = checkGlobalJumpAfterPsml{} func clearMarks() { for k := range marksMap { delete(marksMap, k) } lastJumpPos = -1 } func (t checkGlobalJumpAfterPsml) OnNewSource(code pcap.HandlerCode, app gowid.IApp) { clearMarks() } func (t checkGlobalJumpAfterPsml) OnClear(code pcap.HandlerCode, app gowid.IApp) { clearMarks() } //====================================================================== type checkGlobalJumpAfterPsml struct { Jump termshark.GlobalJumpPos } var _ pcap.IAfterEnd = checkGlobalJumpAfterPsml{} func MakeCheckGlobalJumpAfterPsml(jmp termshark.GlobalJumpPos) checkGlobalJumpAfterPsml { return checkGlobalJumpAfterPsml{ Jump: jmp, } } func (t checkGlobalJumpAfterPsml) AfterEnd(code pcap.HandlerCode, app gowid.IApp) { // Run on main goroutine to avoid problems flagged by -race if code&pcap.PsmlCode == 0 { return } if QuitRequested { return } if t.Jump.Filename == Loader.Pcap() { if packetListView != nil { tableRow, err := tableRowFromPacketNumber(t.Jump.Pos) if err != nil { OpenError(err.Error(), app) } else { tableCol := 0 curTablePos, err := packetListView.FocusXY() if err == nil { tableCol = curTablePos.Column } packetListView.SetFocusXY(app, table.Coords{Column: tableCol, Row: tableRow}) } } } } //====================================================================== // used for the pdml loader type SetStructWidgets struct { Ld *pcap.PacketLoader } var _ pcap.IOnError = SetStructWidgets{} var _ pcap.IBeforeBegin = SetStructWidgets{} var _ pcap.IAfterEnd = SetStructWidgets{} func (s SetStructWidgets) BeforeBegin(code pcap.HandlerCode, app gowid.IApp) { if code&pcap.PdmlCode == 0 { return } s2ch := s.Ld.Stage2FinishedChan termshark.TrackedGo(func() { fn2 := func() { 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) }, Goroutinewg) } // 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(code pcap.HandlerCode, app gowid.IApp) { if code&pcap.PdmlCode == 0 { return } setLowerWidgets(app) StopEmptyHexViewTimer() StopEmptyStructViewTimer() } func (s SetStructWidgets) OnError(code pcap.HandlerCode, app gowid.IApp, err error) { if code&pcap.PdmlCode == 0 { return } log.Error(err) // Hack to avoid picking up errors at other parts of the load // cycle. There should be specific handlers for specific errors. if s.Ld.PdmlLoader.IsLoading() { app.Run(gowid.RunFunction(func(app gowid.IApp) { OpenLongError(fmt.Sprintf("%v", err), app) })) } } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.2.0/ui/streamui.go000066400000000000000000000344241377442047300163320ustar00rootroot00000000000000// Copyright 2019-2021 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 ui contains user-interface functions and helpers for termshark. package ui import ( "fmt" "os" "strings" "sync" "time" "github.com/gcla/gowid" "github.com/gcla/gowid/gwutil" "github.com/gcla/gowid/widgets/holder" "github.com/gcla/gowid/widgets/menu" "github.com/gcla/gowid/widgets/null" "github.com/gcla/gowid/widgets/table" "github.com/gcla/termshark/v2" "github.com/gcla/termshark/v2/pcap" "github.com/gcla/termshark/v2/pdmltree" "github.com/gcla/termshark/v2/streams" "github.com/gcla/termshark/v2/widgets/appkeys" "github.com/gcla/termshark/v2/widgets/streamwidget" "github.com/gdamore/tcell" lru "github.com/hashicorp/golang-lru" log "github.com/sirupsen/logrus" ) var streamViewNoKeysHolder *holder.Widget var streamView *appkeys.KeyWidget var conversationMenu *menu.Widget var conversationMenuHolder *holder.Widget var streamsPcapSize int64 var currentStreamKey *streamKey var streamWidgets *lru.Cache // map[streamKey]*streamwidget.Widget var StreamLoader *streams.Loader // DOC - one because it holds stream index state for pcap //====================================================================== // The index for the stream widget cache e.g. UDP stream 6 type streamKey struct { proto streams.Protocol idx int } //====================================================================== type ManageStreamCache struct{} var _ pcap.INewSource = ManageStreamCache{} var _ pcap.IClear = ManageStreamCache{} // Make sure that existing stream widgets are discarded if the user loads a new pcap. func (t ManageStreamCache) OnNewSource(pcap.HandlerCode, gowid.IApp) { clearStreamState() } func (t ManageStreamCache) OnClear(pcap.HandlerCode, gowid.IApp) { clearStreamState() } //====================================================================== func streamKeyPress(evk *tcell.EventKey, app gowid.IApp) bool { handled := false if evk.Rune() == 'q' || evk.Rune() == 'Q' || evk.Key() == tcell.KeyEscape { closeStreamUi(app, true) StreamLoader.StopLoad() handled = true } return handled } func startStreamReassembly(app gowid.IApp) { var model *pdmltree.Model if packetListView != nil { if fxy, err := packetListView.FocusXY(); err == nil { rid, _ := packetListView.Model().RowIdentifier(fxy.Row) row := int(rid) model = getCurrentStructModel(row) } } if model == nil { OpenError("No packets available.", app) return } proto := streams.TCP streamIndex := model.TCPStreamIndex() if streamIndex.IsNone() { proto = streams.UDP streamIndex = model.UDPStreamIndex() if streamIndex.IsNone() { OpenError("Please select a TCP or UDP packet.", app) return } } filterProto := gwutil.If(proto == streams.TCP, "tcp", "udp").(string) filter := fmt.Sprintf("%s.stream eq %d", filterProto, streamIndex.Val()) previousFilterValue := FilterWidget.Value() FilterWidget.SetValue(filter, app) RequestNewFilter(filter, app) currentStreamKey = &streamKey{proto: proto, idx: streamIndex.Val()} newSize, reset := termshark.FileSizeDifferentTo(Loader.PcapPdml, streamsPcapSize) if reset { streamWidgets = nil } // we maintain an lru.Cache of stream widgets so that we can quickly re-open // the UI for streams that have been calculated before. if streamWidgets == nil { initStreamWidgetCache() streamsPcapSize = newSize } var swid *streamwidget.Widget swid2, ok := streamWidgets.Get(*currentStreamKey) if ok { swid = swid2.(*streamwidget.Widget) ok = swid.Finished() } if ok { openStreamUi(swid, app) } else { swid = makeStreamWidget(previousFilterValue, filter, Loader.String(), proto) streamWidgets.Add(*currentStreamKey, swid) // Use the source context. At app shutdown, canceling main will cancel src which will cancel the stream // loader. And changing source should also cancel the stream loader on all occasions. StreamLoader = streams.NewLoader(streams.MakeCommands(), Loader.Context()) sh := &streamParseHandler{ app: app, name: Loader.String(), proto: proto, idx: streamIndex.Val(), wid: swid, } StreamLoader.StartLoad( Loader.PcapPdml, filterProto, streamIndex.Val(), app, sh, ) } } //====================================================================== type streamParseHandler struct { app gowid.IApp tick *time.Ticker // for updating the spinner stopChunks chan struct{} stopIndices chan struct{} chunks chan streams.IChunk pktIndices chan int name string proto streams.Protocol idx int wid *streamwidget.Widget pleaseWaitClosed bool openedStreams bool sync.Mutex } var _ streams.IOnStreamChunk = (*streamParseHandler)(nil) var _ streams.IOnStreamHeader = (*streamParseHandler)(nil) var _ pcap.IBeforeBegin = (*streamParseHandler)(nil) var _ pcap.IAfterEnd = (*streamParseHandler)(nil) var _ pcap.IOnError = (*streamParseHandler)(nil) // Run from the app goroutine func (t *streamParseHandler) drainChunks() int { curLen := len(t.chunks) for i := 0; i < curLen; i++ { chunk := <-t.chunks if !t.pleaseWaitClosed { t.pleaseWaitClosed = true ClosePleaseWait(t.app) } t.wid.AddChunkEntire(chunk, t.app) } return curLen } // Run from the app goroutine func (t *streamParseHandler) drainPacketIndices() int { curLen := len(t.pktIndices) for i := 0; i < curLen; i++ { packet := <-t.pktIndices t.wid.TrackPayloadPacket(packet) } return curLen } func (t *streamParseHandler) BeforeBegin(code pcap.HandlerCode, app gowid.IApp) { if code&pcap.StreamCode == 0 { return } app.Run(gowid.RunFunction(func(app gowid.IApp) { OpenPleaseWait(appView, app) })) t.tick = time.NewTicker(time.Duration(200) * time.Millisecond) t.stopChunks = make(chan struct{}) t.stopIndices = make(chan struct{}) t.chunks = make(chan streams.IChunk, 1000) t.pktIndices = make(chan int, 1000) // Start this after widgets have been cleared, to get focus change termshark.TrackedGo(func() { fn := func() { app.Run(gowid.RunFunction(func(app gowid.IApp) { t.drainChunks() if !t.openedStreams { appViewNoKeys.SetSubWidget(streamView, app) openStreamUi(t.wid, app) t.openedStreams = true } })) } termshark.RunOnDoubleTicker(t.stopChunks, fn, time.Duration(200)*time.Millisecond, time.Duration(200)*time.Millisecond, 10) }, Goroutinewg) termshark.TrackedGo(func() { fn := func() { app.Run(gowid.RunFunction(func(app gowid.IApp) { t.drainPacketIndices() })) } termshark.RunOnDoubleTicker(t.stopIndices, fn, time.Duration(200)*time.Millisecond, time.Duration(200)*time.Millisecond, 10) }, Goroutinewg) termshark.TrackedGo(func() { Loop: for { select { case <-t.tick.C: app.Run(gowid.RunFunction(func(app gowid.IApp) { pleaseWaitSpinner.Update() })) case <-t.stopChunks: break Loop } } }, Goroutinewg) } func (t *streamParseHandler) AfterIndexEnd(success bool) { t.wid.SetFinished(success) close(t.stopIndices) for { if t.drainPacketIndices() == 0 { break } } } func (t *streamParseHandler) AfterEnd(code pcap.HandlerCode, app gowid.IApp) { if code&pcap.StreamCode == 0 { return } app.Run(gowid.RunFunction(func(app gowid.IApp) { if !t.pleaseWaitClosed { t.pleaseWaitClosed = true ClosePleaseWait(app) } if !t.openedStreams { openStreamUi(t.wid, app) t.openedStreams = true } // Clear out anything lingering from last ticker run to now for { if t.drainChunks() == 0 { break } } if t.wid.NumChunks() == 0 { OpenMessage("No stream payloads found.", appView, app) } })) close(t.stopChunks) } func (t *streamParseHandler) TrackPayloadPacket(packet int) { t.Lock() defer t.Unlock() t.pktIndices <- packet } func (t *streamParseHandler) OnStreamHeader(hdr streams.FollowHeader) { t.app.Run(gowid.RunFunction(func(app gowid.IApp) { t.wid.AddHeader(hdr) })) } // Handle a line/chunk of input - one piece of reassembled data, which comes with // a client/server direction. func (t *streamParseHandler) OnStreamChunk(chunk streams.IChunk) { t.Lock() defer t.Unlock() t.chunks <- chunk } func (t *streamParseHandler) OnError(code pcap.HandlerCode, app gowid.IApp, err error) { if code&pcap.StreamCode == 0 { return } log.Error(err) if !Running { fmt.Fprintf(os.Stderr, "%v\n", err) RequestQuit() } else { var errstr string if kverr, ok := err.(gowid.KeyValueError); ok { errstr = fmt.Sprintf("%v\n\n", kverr.Cause()) kvs := make([]string, 0, len(kverr.KeyVals)) for k, v := range kverr.KeyVals { kvs = append(kvs, fmt.Sprintf("%v: %v", k, v)) } errstr = errstr + strings.Join(kvs, "\n") } else { errstr = fmt.Sprintf("%v", err) } app.Run(gowid.RunFunction(func(app gowid.IApp) { OpenError(errstr, app) })) } } func initStreamWidgetCache() { widgetCacheSize := termshark.ConfInt("main.stream-cache-size", 100) var err error streamWidgets, err = lru.New(widgetCacheSize) if err != nil { log.Fatal(err) } log.Infof("Initialized stream widget cache with %d entries.", widgetCacheSize) } func clearStreamState() { initStreamWidgetCache() currentStreamKey = nil } type streamClicker struct{} var _ streamwidget.IChunkClicked = streamClicker{} func (c streamClicker) OnPacketClicked(pkt int, app gowid.IApp) error { if packetListView != nil { coords, err := packetListView.FocusXY() if err == nil { // Need to go from row identifier ("9th packet") to display order which might be sorted. // Note that for our pcap table, the row identifier (logical id) for each table row is // itself an int i.e. packet #0, packet #1 (although the packet's *frame number* might // be different if there's a filter). When OnChunkClicked() is called, it means react // to a click on the logical packet #N (where the stream loader tracks pcap packet -> // packet-with-payload). So // // - user clicks on 9th item in stream view // - the stream loader translates this to the 15th packet in the pcap (rest are SYN, etc) // - the inverted table model translates this to display row #5 in the table (because it's sorted) // - then we set the display row and switch to the data/away from the table header // if row, ok := packetListView.InvertedModel().IdentifierToRow(table.RowId(pkt)); !ok { OpenError(fmt.Sprintf("Unexpected error looking up packet %v.", pkt), app) } else { coords.Row = row // cast to int - we want row == #item in list packetListView.SetFocusXY(app, coords) packetListTable.SetFocusOnData(app) packetListTable.GoToMiddle(app) setFocusOnPacketList(app) packetListData := packetListView.Model().(table.ISimpleDataProvider).GetData() // This condition should always be true. I just feel cautious because accessing the psml data // in this way feels fragile. Also, take note: there's an open issue to make it possible to // customize the packet headers, in which case the item at index 0 in the psml might not be // the frame number (though this check doesn't guard against that...). It's more useful // to display the actual frame number if possible, so do that if we can, otherwise just // display which segment of the stream this is. if len(packetListData) > row && len(packetListData[row]) > 0 { OpenMessage(fmt.Sprintf("Selected packet %s.", packetListData[row][0]), appView, app) } else { OpenMessage(fmt.Sprintf("Selected segment %d.", pkt+1), appView, app) } } } } return nil } func (c streamClicker) HandleError(row table.RowId, err error, app gowid.IApp) { OpenError(fmt.Sprintf("Packet at row %v is not loaded yet. Try again in a few seconds.", row), app) } //====================================================================== type simpleOnError struct{} func (s simpleOnError) OnError(msg string, app gowid.IApp) { OpenError(msg, app) } func makeStreamWidget(previousFilter string, filter string, cap string, proto streams.Protocol) *streamwidget.Widget { return streamwidget.New(filter, cap, proto, conversationMenu, conversationMenuHolder, &keyState, streamwidget.Options{ DefaultDisplay: func() streamwidget.DisplayFormat { view := streamwidget.Hex choice := termshark.ConfString("main.stream-view", "hex") switch choice { case "raw": view = streamwidget.Raw case "ascii": view = streamwidget.Ascii } return view }, PreviousFilter: previousFilter, FilterOutFunc: func(w streamwidget.IFilterOut, app gowid.IApp) { closeStreamUi(app, true) var newFilter string if w.PreviousFilter() == "" { newFilter = fmt.Sprintf("!(%s)", w.DisplayFilter()) } else { newFilter = fmt.Sprintf("%s and !(%s)", w.PreviousFilter(), w.DisplayFilter()) } FilterWidget.SetValue(newFilter, app) //Loader.NewFilter(newFilter, MakePacketViewUpdater(), app) RequestNewFilter(newFilter, app) }, CopyModeWidget: CopyModeWidget, ChunkClicker: streamClicker{}, ErrorHandler: simpleOnError{}, }) } //====================================================================== func openStreamUi(swid *streamwidget.Widget, app gowid.IApp) { streamViewNoKeysHolder.SetSubWidget(swid, app) appViewNoKeys.SetSubWidget(streamView, app) // When opening, put focus on the list of stream chunks. There may be none in which case // this won't work. But when UI is constructed, there are no chunks, even though it's not // open yet, so focus on the pile goes to the bottom, even when the chunk table becomes populated. // That's not ideal. swid.SetFocusOnChunksIfPossible(app) } func closeStreamUi(app gowid.IApp, refocus bool) { appViewNoKeys.SetSubWidget(mainView, app) // Do this if the user starts reassembly from the menu - better UX if refocus { setFocusOnPacketList(app) } } //====================================================================== func buildStreamUi() { conversationMenuHolder, conversationMenu = streamwidget.MakeConvMenu() streamViewNoKeysHolder = holder.New(null.New()) streamView = appkeys.New( appkeys.New( appkeys.New( streamViewNoKeysHolder, streamKeyPress, ), copyModeExitKeys20, appkeys.Options{ ApplyBefore: true, }, ), copyModeEnterKeys, ) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.2.0/ui/tableutil/000077500000000000000000000000001377442047300161305ustar00rootroot00000000000000termshark-2.2.0/ui/tableutil/tableutil.go000066400000000000000000000043701377442047300204500ustar00rootroot00000000000000// Copyright 2019-2021 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 tableutil contains user-interface functions and helpers for termshark's // tables - in particular, helpers for vim key sequences like 5gg and G package tableutil import ( "github.com/gcla/gowid" "github.com/gcla/gowid/widgets/table" "github.com/gcla/termshark/v2" "github.com/gcla/termshark/v2/widgets/appkeys" "github.com/gdamore/tcell" ) //====================================================================== type GoToAdapter struct { *table.BoundedWidget *termshark.KeyState } var _ IGoToLineRequested = (*GoToAdapter)(nil) func (t *GoToAdapter) GoToLineOrTop(evk *tcell.EventKey) (bool, int) { num := -1 if t.NumberPrefix != -1 { num = t.NumberPrefix - 1 } return evk.Key() == tcell.KeyRune && evk.Rune() == 'g' && t.PartialgCmd, num } func (t *GoToAdapter) GoToLineOrBottom(evk *tcell.EventKey) (bool, int) { num := -1 if t.NumberPrefix != -1 { num = t.NumberPrefix - 1 } return evk.Key() == tcell.KeyRune && evk.Rune() == 'G', num } type IGoToLineRequested interface { GoToLineOrTop(evk *tcell.EventKey) (bool, int) // -1 means top GoToLineOrBottom(evk *tcell.EventKey) (bool, int) // -1 means bottom GoToFirst(gowid.IApp) bool GoToLast(gowid.IApp) bool GoToNth(gowid.IApp, int) bool } // GotoHander retrusn a function suitable for the appkeys widget - it will // check to see if the key represents a supported action on the table and // then runs the action if so. func GotoHandler(t IGoToLineRequested) appkeys.KeyInputFn { return func(evk *tcell.EventKey, app gowid.IApp) bool { handled := false if t != nil { handled = true if doit, line := t.GoToLineOrTop(evk); doit { if line == -1 { t.GoToFirst(app) } else { // psml starts counting at 1 t.GoToNth(app, line) } } else if doit, line := t.GoToLineOrBottom(evk); doit { if line == -1 { t.GoToLast(app) } else { // psml starts counting at 1 t.GoToNth(app, line) } } else { handled = false } } return handled } } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.2.0/ui/ui.go000066400000000000000000003046021377442047300151140ustar00rootroot00000000000000// Copyright 2019-2021 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 ui contains user-interface functions and helpers for termshark. package ui import ( "encoding/xml" "fmt" "math" "os" "reflect" "runtime" "strconv" "strings" "sync" "time" "github.com/gcla/deep" "github.com/gcla/gowid" "github.com/gcla/gowid/gwutil" "github.com/gcla/gowid/widgets/button" "github.com/gcla/gowid/widgets/clicktracker" "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/list" "github.com/gcla/gowid/widgets/menu" "github.com/gcla/gowid/widgets/null" "github.com/gcla/gowid/widgets/overlay" "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/v2" "github.com/gcla/termshark/v2/pcap" "github.com/gcla/termshark/v2/pdmltree" "github.com/gcla/termshark/v2/psmlmodel" "github.com/gcla/termshark/v2/system" "github.com/gcla/termshark/v2/theme" "github.com/gcla/termshark/v2/ui/menuutil" "github.com/gcla/termshark/v2/ui/tableutil" "github.com/gcla/termshark/v2/widgets" "github.com/gcla/termshark/v2/widgets/appkeys" "github.com/gcla/termshark/v2/widgets/copymodetree" "github.com/gcla/termshark/v2/widgets/enableselected" "github.com/gcla/termshark/v2/widgets/expander" "github.com/gcla/termshark/v2/widgets/filter" "github.com/gcla/termshark/v2/widgets/hexdumper2" "github.com/gcla/termshark/v2/widgets/ifwidget" "github.com/gcla/termshark/v2/widgets/mapkeys" "github.com/gcla/termshark/v2/widgets/minibuffer" "github.com/gcla/termshark/v2/widgets/resizable" "github.com/gcla/termshark/v2/widgets/rossshark" "github.com/gcla/termshark/v2/widgets/withscrollbar" "github.com/gdamore/tcell" lru "github.com/hashicorp/golang-lru" "github.com/pkg/errors" log "github.com/sirupsen/logrus" ) //====================================================================== var Goroutinewg *sync.WaitGroup // Global so that we can change the displayed packet in the struct view, etc // test var appViewNoKeys *holder.Widget var appView *holder.Widget var mbView *holder.Widget var mainViewNoKeys *holder.Widget var mainView *appkeys.KeyWidget var pleaseWaitSpinner *spinner.Widget var mainviewRows *resizable.PileWidget var mainview gowid.IWidget var altview1 gowid.IWidget var altview1OuterRows *resizable.PileWidget var altview1Pile *resizable.PileWidget var altview1Cols *resizable.ColumnsWidget var altview2 gowid.IWidget var altview2OuterRows *resizable.PileWidget var altview2Pile *resizable.PileWidget var altview2Cols *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 altview1Paths [][]interface{} var altview2Paths [][]interface{} var maxViewPath []interface{} var filterPathMain []interface{} var filterPathAlt []interface{} var filterPathMax []interface{} var menuPathMain []interface{} var menuPathAlt []interface{} var menuPathMax []interface{} var view1idx int var view2idx int var generalMenu *menu.Widget var analysisMenu *menu.Widget var savedMenu *menu.Widget var FilterWidget *filter.Widget var Fin *rossshark.Widget var CopyModeWidget gowid.IWidget var CopyModePredicate ifwidget.Predicate var openMenuSite *menu.SiteWidget var openAnalysisSite *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 savedListBoxWidgetHolder *holder.Widget var singlePacketViewMsgHolder *holder.Widget // either empty or "loading..." var keyMapper *mapkeys.Widget var tabViewsForward map[gowid.IWidget]gowid.IWidget var tabViewsBackward map[gowid.IWidget]gowid.IWidget var currentCapture *text.Widget var currentCaptureWidget *columns.Widget var currentCaptureWidgetHolder *holder.Widget var nullw *null.Widget // empty var fillSpace *fill.Widget var fillVBar *fill.Widget var colSpace *gowid.ContainerWidget var curPacketStructWidget *copymodetree.Widget var packetHexWidgets *lru.Cache var packetListView *psmlTableRowWidget var Loadingw gowid.IWidget // "loading..." var MissingMsgw gowid.IWidget // centered, holding singlePacketViewMsgHolder var EmptyStructViewTimer *time.Timer var EmptyHexViewTimer *time.Timer var curExpandedStructNodes pdmltree.ExpandedPaths // a path to each expanded node in the packet, preserved while navigating var curStructPosition tree.IPos // e.g. [0, 2, 1] -> the indices of the expanded nodes var curPdmlPosition []string // e.g. [ , tcp, tcp.srcport ] -> the path from focus to root in the current struct var curStructWidgetState interface{} // e.g. {linesFromTop: 1, ...} -> the positioning of the current struct widget var CacheRequests []pcap.LoadPcapSlice var CacheRequestsChan chan struct{} // false means started, true means finished var QuitRequestedChan chan struct{} var StartUIChan chan struct{} var StartUIOnce sync.Once // Store this for vim-like keypresses that are a sequence e.g. "ZZ" var keyState termshark.KeyState var marksMap map[rune]termshark.JumpPos var globalMarksMap map[rune]termshark.GlobalJumpPos var lastJumpPos int var NoGlobalJump termshark.GlobalJumpPos // leave as default, like a placeholder var Loader *pcap.PacketLoader 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 var AutoScroll bool // true if the packet list should auto-scroll when listening on an interface. var newPacketsArrived bool // true if current updates are due to new packets when listening on an interface. var reenableAutoScroll bool // set to true by keypress processing widgets - used with newPacketsArrived var Running bool // true if gowid/tcell is controlling the terminal var QuitRequested bool // true if a quit has been issued, but not yet processed. Stops some handlers displaying errors. //====================================================================== func init() { curExpandedStructNodes = make(pdmltree.ExpandedPaths, 0, 20) 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) // Buffered because I might send something in this goroutine StartUIChan = make(chan struct{}, 1) keyState.NumberPrefix = -1 // 0 might be meaningful marksMap = make(map[rune]termshark.JumpPos) globalMarksMap = make(map[rune]termshark.GlobalJumpPos) lastJumpPos = -1 EnsureTemplateData() TemplateData["Marks"] = marksMap TemplateData["GlobalMarks"] = globalMarksMap TemplateData["Maps"] = getMappings{} } type globalJump struct { file string pos int } type getMappings struct{} func (g getMappings) Get() []termshark.KeyMapping { return termshark.LoadKeyMappings() } func (g getMappings) None() bool { return len(termshark.LoadKeyMappings()) == 0 } func RequestQuit() { select { case QuitRequestedChan <- struct{}{}: default: // Ok for the send not to succeed - there is a buffer of one, and it only // needs one message to start the shutdown sequence. So this means a // message has already been sent (before the main loop gets round to processing // this channel) } } // Runs in app goroutine func UpdateProgressBarForInterface(c *pcap.InterfaceLoader, app gowid.IApp) { SetProgressIndeterminate(app) loadSpinner.Update() } // Runs in app goroutine func UpdateProgressBarForFile(c *pcap.PacketLoader, prevRatio float64, app gowid.IApp) float64 { SetProgressDeterminate(app) psmlProg := Prog{0, 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 { pktsPerLoad := c.PacketsPerLoad() currentRow = int(foo) currentRowMod = int64(currentRow % pktsPerLoad) currentRowDiv = (currentRow / pktsPerLoad) * pktsPerLoad c.PsmlLoader.Lock() curRowProg.cur, curRowProg.max = int64(currentRow), int64(len(c.PsmlData())) c.PsmlLoader.Unlock() } } } // Progress determined by how many of the (up to) pktsPerLoad 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.PdmlLoader.IsLoading() { if c.LoadingRow() == currentRowDiv { if x, err = c.LengthOfPdmlCacheEntry(c.LoadingRow()); 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.PdmlLoader.Lock() c2, m, err = system.ProcessProgress(c.PdmlPid, c.PcapPdml) c.PdmlLoader.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) pktsPerLoad pcap packets are read if x, err = c.LengthOfPcapCacheEntry(c.LoadingRow()); 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.PdmlLoader.Lock() c2, m, err = system.ProcessProgress(c.PcapPid, c.PcapPcap) c.PdmlLoader.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.PsmlLoader.IsLoading() { c.PsmlLoader.Lock() c2, m, err = system.ProcessProgress(termshark.SafePid(c.PsmlCmd), psml) c.PsmlLoader.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 { case c.PsmlLoader.IsLoading() && c.PdmlLoader.IsLoading() && c.PdmlLoader.LoadIsVisible(): select { case <-c.StartStage2ChanFn(): prog = psmlProg.Add( progMax(pcapPacketProg, pcapIdxProg).Add( progMax(pdmlPacketProg, pdmlIdxProg), ), ) default: prog = psmlProg.Div(2) // temporarily divide in 2. Leave original for case above - so that the 50% } case c.PsmlLoader.IsLoading(): prog = psmlProg case c.PdmlLoader.IsLoading() && c.PdmlLoader.LoadIsVisible(): prog = progMax(pcapPacketProg, pcapIdxProg).Add( progMax(pdmlPacketProg, pdmlIdxProg), ) } curRatio := float64(prog.cur) / float64(prog.max) if !prog.Complete() { if prevRatio < curRatio { loadProgress.SetTarget(app, int(prog.max)) loadProgress.SetProgress(app, int(prog.cur)) } } return math.Max(prevRatio, curRatio) } //====================================================================== // psmlSummary is used to generate a summary for the marks dialog type psmlSummary []string func (p psmlSummary) String() string { // Skip packet number return strings.Join([]string(p)[1:], " : ") } //====================================================================== 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} } //====================================================================== // run in app goroutine func clearPacketViews(app gowid.IApp) { 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 our 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()) })) })) expandContractKeys := appkeys.New( bn, func(ev *tcell.EventKey, app gowid.IApp) bool { handled := false switch ev.Key() { case tcell.KeyLeft: if !ct.IsCollapsed() { ct.SetCollapsed(app, true) handled = true } case tcell.KeyRight: if ct.IsCollapsed() { ct.SetCollapsed(app, false) handled = true } } return handled }, ) cwidgets = append(cwidgets, &gowid.ContainerWidget{ IWidget: expandContractKeys, 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("packet-struct-selected")), styled.New(res, gowid.MakePaletteRef("packet-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 } //====================================================================== // An ugly interface that captures what sort of type will be suitable // as a table widget to which a row focus can be applied. type iRowFocusTableWidgetNeeds interface { gowid.IWidget list.IBoundedWalker table.IFocus table.IGoToMiddle table.ISetFocus list.IWalkerHome list.IWalkerEnd SetPos(pos list.IBoundedWalkerPosition, app gowid.IApp) FocusXY() (table.Coords, error) SetFocusXY(gowid.IApp, table.Coords) SetModel(table.IModel, gowid.IApp) Lower() *table.ListWithPreferedColumn SetFocusOnData(app gowid.IApp) bool OnFocusChanged(f gowid.IWidgetChangedCallback) } // rowFocusTableWidget provides a table that highlights the selected row or // focused row. type rowFocusTableWidget struct { iRowFocusTableWidgetNeeds rowSelected string rowFocus string } func NewRowFocusTableWidget(w iRowFocusTableWidgetNeeds, rs string, rf string) *rowFocusTableWidget { res := &rowFocusTableWidget{ iRowFocusTableWidgetNeeds: w, rowSelected: rs, rowFocus: rf, } res.Lower().IWidget = list.NewBounded(res) return res } var _ gowid.IWidget = (*rowFocusTableWidget)(nil) func (t *rowFocusTableWidget) SubWidget() gowid.IWidget { return t.iRowFocusTableWidgetNeeds } func (t *rowFocusTableWidget) InvertedModel() table.IInvertible { return t.Model().(table.IInvertible) } func (t *rowFocusTableWidget) Rows() int { return t.Model().(table.IBoundedModel).Rows() } // Implement withscrollbar.IScrollValues func (t *rowFocusTableWidget) ScrollLength() int { return t.Rows() } // Implement withscrollbar.IScrollValues func (t *rowFocusTableWidget) ScrollPosition() int { return t.CurrentRow() } func (t *rowFocusTableWidget) Up(lines int, size gowid.IRenderSize, app gowid.IApp) { for i := 0; i < lines; i++ { t.UserInput(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++ { t.UserInput(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++ { t.UserInput(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++ { t.UserInput(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.AtRow(pos) if w == nil { return nil } // Composite so it passes through prefered column var res gowid.IWidget = &selectedComposite{ Widget: isselected.New(w, styled.New(w, gowid.MakePaletteRef(t.rowSelected)), styled.New(w, gowid.MakePaletteRef(t.rowFocus)), ), } return res } // 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) } //====================================================================== // A rowFocusTableWidget that adds colors to rows type psmlTableRowWidget struct { *rowFocusTableWidget // set to true after the first time we move focus from the table header to the data. We do this // once and that this happens quickly, but then assume the user might want to move back to the // table header manually, and it would be strange if the table keeps jumping back to the data... didFirstAutoFocus bool colors []pcap.PacketColors } func NewPsmlTableRowWidget(w *rowFocusTableWidget, c []pcap.PacketColors) *psmlTableRowWidget { res := &psmlTableRowWidget{ rowFocusTableWidget: w, colors: c, } res.Lower().IWidget = list.NewBounded(res) return res } func (t *psmlTableRowWidget) At(lpos list.IWalkerPosition) gowid.IWidget { res := t.rowFocusTableWidget.At(lpos) if res == nil { return nil } pos := int(lpos.(table.Position)) // Check the color array length because it might not yet be adequately // populated from the arriving psml. if pos >= 0 && PacketColors && pos < len(t.colors) { res = styled.New(res, gowid.MakePaletteEntry(t.colors[pos].FG, t.colors[pos].BG), ) } return res } func (t *psmlTableRowWidget) Focus() list.IWalkerPosition { return table.Focus(t) } //====================================================================== type pleaseWaitCallbacks struct { w *spinner.Widget app gowid.IApp open bool } func (s *pleaseWaitCallbacks) ProcessWaitTick() error { s.app.Run(gowid.RunFunction(func(app gowid.IApp) { s.w.Update() if !s.open { OpenPleaseWait(appView, s.app) s.open = true } })) return nil } // Call in app context func (s *pleaseWaitCallbacks) closeWaitDialog(app gowid.IApp) { if s.open { ClosePleaseWait(app) s.open = false } } func (s *pleaseWaitCallbacks) ProcessCommandDone() { s.app.Run(gowid.RunFunction(func(app gowid.IApp) { s.closeWaitDialog(app) })) } //====================================================================== // Wait until the copy command has finished, then open up a dialog with the results. type urlCopiedCallbacks struct { app gowid.IApp tmplName string *pleaseWaitCallbacks } var ( _ termshark.ICommandOutput = urlCopiedCallbacks{} _ termshark.ICommandError = urlCopiedCallbacks{} _ termshark.ICommandDone = urlCopiedCallbacks{} _ termshark.ICommandKillError = urlCopiedCallbacks{} _ termshark.ICommandTimeout = urlCopiedCallbacks{} _ termshark.ICommandWaitTicker = urlCopiedCallbacks{} ) func (h urlCopiedCallbacks) displayDialog(output string) { TemplateData["CopyCommandMessage"] = output h.app.Run(gowid.RunFunction(func(app gowid.IApp) { h.closeWaitDialog(app) OpenTemplatedDialog(appView, h.tmplName, app) delete(TemplateData, "CopyCommandMessage") })) } func (h urlCopiedCallbacks) ProcessOutput(output string) error { var msg string if len(output) == 0 { msg = "URL copied to clipboard." } else { msg = output } h.displayDialog(msg) return nil } func (h urlCopiedCallbacks) ProcessCommandTimeout() error { h.displayDialog("") return nil } func (h urlCopiedCallbacks) ProcessCommandError(err error) error { h.displayDialog("") return nil } func (h urlCopiedCallbacks) ProcessKillError(err error) error { h.displayDialog("") return nil } //====================================================================== type userCopiedCallbacks struct { app gowid.IApp copyCmd []string *pleaseWaitCallbacks } var ( _ termshark.ICommandOutput = userCopiedCallbacks{} _ termshark.ICommandError = userCopiedCallbacks{} _ termshark.ICommandDone = userCopiedCallbacks{} _ termshark.ICommandKillError = userCopiedCallbacks{} _ termshark.ICommandTimeout = userCopiedCallbacks{} _ termshark.ICommandWaitTicker = userCopiedCallbacks{} ) func (h userCopiedCallbacks) ProcessCommandTimeout() error { h.app.Run(gowid.RunFunction(func(app gowid.IApp) { h.closeWaitDialog(app) OpenError(fmt.Sprintf("Copy command \"%v\" timed out", strings.Join(h.copyCmd, " ")), app) })) return nil } func (h userCopiedCallbacks) ProcessCommandError(err error) error { h.app.Run(gowid.RunFunction(func(app gowid.IApp) { h.closeWaitDialog(app) OpenError(fmt.Sprintf("Copy command \"%v\" failed: %v", strings.Join(h.copyCmd, " "), err), app) })) return nil } func (h userCopiedCallbacks) ProcessKillError(err error) error { h.app.Run(gowid.RunFunction(func(app gowid.IApp) { h.closeWaitDialog(app) OpenError(fmt.Sprintf("Timed out, but could not kill copy command: %v", err), app) })) return nil } func (h userCopiedCallbacks) ProcessOutput(output string) error { h.app.Run(gowid.RunFunction(func(app gowid.IApp) { h.closeWaitDialog(app) if len(output) == 0 { OpenMessage(" Copied! ", appView, app) } else { OpenMessage(fmt.Sprintf("Copied! Output was:\n%s\n", output), appView, app) } })) return nil } //====================================================================== func OpenError(msgt string, app gowid.IApp) *dialog.Widget { // the same, for now return OpenMessage(msgt, appView, app) } func OpenLongError(msgt string, app gowid.IApp) *dialog.Widget { // the same, for now return OpenLongMessage(msgt, appView, app) } func openResultsAfterCopy(tmplName string, tocopy string, app gowid.IApp) { v := urlCopiedCallbacks{ app: app, tmplName: tmplName, pleaseWaitCallbacks: &pleaseWaitCallbacks{ w: pleaseWaitSpinner, app: app, }, } termshark.CopyCommand(strings.NewReader(tocopy), v) } func processCopyChoices(copyLen int, app gowid.IApp) { var cc *dialog.Widget copyCmd := termshark.ConfStringSlice( "main.copy-command", system.CopyToClipboard, ) if len(copyCmd) == 0 { OpenError("Config file has an invalid copy-command entry! Please remove it.", app) return } clips := app.Clips() // No need to display a choice dialog with one choice - just copy right away if len(clips) == 1 { app.InCopyMode(false) termshark.CopyCommand(strings.NewReader(clips[0].ClipValue()), userCopiedCallbacks{ app: app, copyCmd: copyCmd, pleaseWaitCallbacks: &pleaseWaitCallbacks{ w: pleaseWaitSpinner, app: app, }, }) return } cws := make([]gowid.IWidget, 0, len(clips)) for _, clip := range clips { c2 := clip lbl := text.New(clip.ClipName() + ":") btxt1 := clip.ClipValue() if copyLen > 0 { blines := strings.Split(btxt1, "\n") if len(blines) > copyLen { blines[copyLen-1] = "..." blines = blines[0:copyLen] } btxt1 = strings.Join(blines, "\n") } btn := button.NewBare(text.New(btxt1, text.Options{ Wrap: text.WrapClip, ClipIndicator: "...", })) btn.OnClick(gowid.MakeWidgetCallback("cb", gowid.WidgetChangedFunction(func(app gowid.IApp, w gowid.IWidget) { cc.Close(app) app.InCopyMode(false) termshark.CopyCommand(strings.NewReader(c2.ClipValue()), userCopiedCallbacks{ app: app, copyCmd: copyCmd, pleaseWaitCallbacks: &pleaseWaitCallbacks{ w: pleaseWaitSpinner, app: app, }, }) }))) 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) cc = dialog.New(view1, dialog.Options{ Buttons: dialog.CloseOnly, NoShadow: true, BackgroundStyle: gowid.MakePaletteRef("dialog"), BorderStyle: gowid.MakePaletteRef("dialog"), ButtonStyle: gowid.MakePaletteRef("dialog-button"), FocusOnWidget: true, }, ) cc.OnOpenClose(gowid.MakeWidgetCallback("cb", gowid.WidgetChangedFunction(func(app gowid.IApp, w gowid.IWidget) { if !cc.IsOpen() { app.InCopyMode(false) } }))) dialog.OpenExt(cc, appView, 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) { RequestQuit() }, }, dialog.Cancel, }, NoShadow: true, BackgroundStyle: gowid.MakePaletteRef("dialog"), BorderStyle: gowid.MakePaletteRef("dialog"), ButtonStyle: gowid.MakePaletteRef("dialog-button"), }, ) YesNo.Open(appView, units(len(msgt)+20), app) } func lastLineMode(app gowid.IApp) { MiniBuffer = minibuffer.New() MiniBuffer.Register("quit", minibufferFn(func(gowid.IApp, ...string) error { reallyQuit(app) return nil })) // force quit MiniBuffer.Register("q!", quietMinibufferFn(func(gowid.IApp, ...string) error { RequestQuit() return nil })) MiniBuffer.Register("help", minibufferFn(func(gowid.IApp, ...string) error { OpenTemplatedDialog(appView, "UIHelp", app) return nil })) MiniBuffer.Register("no-theme", minibufferFn(func(app gowid.IApp, s ...string) error { mode := theme.Mode(app.GetColorMode()).String() // more concise termshark.DeleteConf(fmt.Sprintf("main.theme-%s", mode)) theme.Load("default", app) SetupColors() OpenMessage(fmt.Sprintf("Cleared theme for terminal mode %v.", app.GetColorMode()), appView, app) return nil })) MiniBuffer.Register("convs", minibufferFn(func(gowid.IApp, ...string) error { openConvsUi(app) return nil })) MiniBuffer.Register("streams", minibufferFn(func(gowid.IApp, ...string) error { startStreamReassembly(app) return nil })) MiniBuffer.Register("capinfo", minibufferFn(func(gowid.IApp, ...string) error { startCapinfo(app) return nil })) MiniBuffer.Register("menu", minibufferFn(func(gowid.IApp, ...string) error { openGeneralMenu(app) return nil })) MiniBuffer.Register("clear-packets", minibufferFn(func(gowid.IApp, ...string) error { reallyClear(app) return nil })) MiniBuffer.Register("clear-filter", minibufferFn(func(gowid.IApp, ...string) error { FilterWidget.SetValue("", app) RequestNewFilter(FilterWidget.Value(), app) return nil })) MiniBuffer.Register("marks", minibufferFn(func(gowid.IApp, ...string) error { OpenTemplatedDialogExt(appView, "Marks", fixed, ratio(0.6), app) return nil })) if runtime.GOOS != "windows" { MiniBuffer.Register("logs", minibufferFn(func(gowid.IApp, ...string) error { openLogsUi(app) return nil })) } MiniBuffer.Register("set", setCommand{}) // read new pcap MiniBuffer.Register("r", readCommand{complete: false}) MiniBuffer.Register("e", readCommand{complete: false}) MiniBuffer.Register("load", readCommand{complete: true}) MiniBuffer.Register("recents", recentsCommand{}) MiniBuffer.Register("filter", filterCommand{}) MiniBuffer.Register("theme", themeCommand{}) MiniBuffer.Register("map", mapCommand{w: keyMapper}) MiniBuffer.Register("unmap", unmapCommand{w: keyMapper}) MiniBuffer.Register("help", helpCommand{}) minibuffer.Open(MiniBuffer, mbView, ratio(1.0), fixed, app) } //====================================================================== // getCurrentStructModel will return a termshark model of a packet section of PDML given a row number, // or nil if there is no model for the given row. func getCurrentStructModel(row int) *pdmltree.Model { var res *pdmltree.Model pktsPerLoad := Loader.PacketsPerLoad() row2 := (row / pktsPerLoad) * pktsPerLoad Loader.PsmlLoader.Lock() defer Loader.PsmlLoader.Unlock() if ws, ok := Loader.PacketCache.Get(row2); ok { srca := ws.(pcap.CacheEntry).Pdml if len(srca) > row%pktsPerLoad { data, err := xml.Marshal(srca[row%pktsPerLoad].Packet()) if err != nil { log.Fatal(err) } res = pdmltree.DecodePacket(data) } } return res } //====================================================================== 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) Loader.ClearPcap( pcap.HandlerList{ SimpleErrors{}, MakePacketViewUpdater(), MakeUpdateCurrentCaptureInTitle(), ManageStreamCache{}, ManageCapinfoCache{}, SetStructWidgets{Loader}, // for OnClear ClearMarksHandler{}, CancelledMessage{}, }, ) }, }, dialog.Cancel, }, NoShadow: true, BackgroundStyle: gowid.MakePaletteRef("dialog"), BorderStyle: gowid.MakePaletteRef("dialog"), ButtonStyle: gowid.MakePaletteRef("dialog-button"), }, ) YesNo.Open(mainViewNoKeys, units(len(msgt)+28), app) } //====================================================================== func appKeysResize1(evk *tcell.EventKey, app gowid.IApp) bool { handled := true if evk.Rune() == '+' { mainviewRows.AdjustOffset(2, 6, resizable.Add1, app) } else if evk.Rune() == '-' { mainviewRows.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() == '+' { mainviewRows.AdjustOffset(4, 6, resizable.Add1, app) } else if evk.Rune() == '-' { mainviewRows.AdjustOffset(4, 6, resizable.Subtract1, app) } else { handled = false } return handled } func altview1ColsKeyPress(evk *tcell.EventKey, app gowid.IApp) bool { handled := true if evk.Rune() == '>' { altview1Cols.AdjustOffset(0, 2, resizable.Add1, app) } else if evk.Rune() == '<' { altview1Cols.AdjustOffset(0, 2, resizable.Subtract1, app) } else { handled = false } return handled } func altview1PileKeyPress(evk *tcell.EventKey, app gowid.IApp) bool { handled := true if evk.Rune() == '+' { altview1Pile.AdjustOffset(0, 2, resizable.Add1, app) } else if evk.Rune() == '-' { altview1Pile.AdjustOffset(0, 2, resizable.Subtract1, app) } else { handled = false } return handled } func altview2ColsKeyPress(evk *tcell.EventKey, app gowid.IApp) bool { handled := true if evk.Rune() == '>' { altview2Cols.AdjustOffset(0, 2, resizable.Add1, app) } else if evk.Rune() == '<' { altview2Cols.AdjustOffset(0, 2, resizable.Subtract1, app) } else { handled = false } return handled } func altview2PileKeyPress(evk *tcell.EventKey, app gowid.IApp) bool { handled := true if evk.Rune() == '+' { altview2Pile.AdjustOffset(0, 2, resizable.Add1, app) } else if evk.Rune() == '-' { altview2Pile.AdjustOffset(0, 2, resizable.Subtract1, app) } else { handled = false } return handled } func copyModeExitKeys(evk *tcell.EventKey, app gowid.IApp) bool { return copyModeExitKeysClipped(evk, 0, app) } // Used for limiting samples of reassembled streams func copyModeExitKeys20(evk *tcell.EventKey, app gowid.IApp) bool { return copyModeExitKeysClipped(evk, 20, app) } func copyModeExitKeysClipped(evk *tcell.EventKey, copyLen int, 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 '?': OpenTemplatedDialog(appView, "CopyModeHelp", app) } case tcell.KeyEscape: app.InCopyMode(false) case tcell.KeyCtrlC: processCopyChoices(copyLen, 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() } } } return handled } func copyModeEnterKeys(evk *tcell.EventKey, app gowid.IApp) bool { handled := false if !app.InCopyMode() { switch evk.Key() { case tcell.KeyRune: switch evk.Rune() { case 'c': app.InCopyMode(true) handled = true } } } return handled } func setFocusOnPacketList(app gowid.IApp) { gowid.SetFocusPath(mainview, mainviewPaths[0], app) gowid.SetFocusPath(altview1, altview1Paths[0], app) gowid.SetFocusPath(altview2, altview2Paths[0], app) gowid.SetFocusPath(viewOnlyPacketList, maxViewPath, app) } func setFocusOnPacketStruct(app gowid.IApp) { gowid.SetFocusPath(mainview, mainviewPaths[1], app) gowid.SetFocusPath(altview1, altview1Paths[1], app) gowid.SetFocusPath(altview2, altview2Paths[1], app) gowid.SetFocusPath(viewOnlyPacketStructure, maxViewPath, app) } func setFocusOnPacketHex(app gowid.IApp) { gowid.SetFocusPath(mainview, mainviewPaths[2], app) gowid.SetFocusPath(altview1, altview1Paths[2], app) gowid.SetFocusPath(altview2, altview2Paths[2], app) gowid.SetFocusPath(viewOnlyPacketHex, maxViewPath, app) } func setFocusOnDisplayFilter(app gowid.IApp) { gowid.SetFocusPath(mainview, filterPathMain, app) gowid.SetFocusPath(altview1, filterPathAlt, app) gowid.SetFocusPath(altview2, filterPathAlt, app) gowid.SetFocusPath(viewOnlyPacketList, filterPathMax, app) gowid.SetFocusPath(viewOnlyPacketStructure, filterPathMax, app) gowid.SetFocusPath(viewOnlyPacketHex, filterPathMax, app) } func clearOffsets(app gowid.IApp) { if mainViewNoKeys.SubWidget() == mainview { mainviewRows.SetOffsets([]resizable.Offset{}, app) } else if mainViewNoKeys.SubWidget() == altview1 { altview1Cols.SetOffsets([]resizable.Offset{}, app) altview1Pile.SetOffsets([]resizable.Offset{}, app) } else { altview2Cols.SetOffsets([]resizable.Offset{}, app) altview2Pile.SetOffsets([]resizable.Offset{}, app) } } func packetNumberFromCurrentTableRow() (termshark.JumpPos, error) { tablePos, err := packetListView.FocusXY() // e.g. table position 5 if err != nil { return termshark.JumpPos{}, fmt.Errorf("No packet in focus: %v", err) } return packetNumberFromTableRow(tablePos.Row) } func tableRowFromPacketNumber(savedPacket int) (int, error) { // Map e.g. packet number #123 to the index in the PSML array - e.g. index 10 (order of psml load) packetRowId, ok := Loader.PacketNumberMap[savedPacket] if !ok { return -1, fmt.Errorf("Error finding packet %v", savedPacket) } // This psml order is also the table RowId order. The table might be sorted though, so // map this RowId to the actual table row, so we can change focus to it tableRow, ok := packetListView.InvertedModel().IdentifierToRow(table.RowId(packetRowId)) if !ok { return -1, fmt.Errorf("Error looking up packet %v", packetRowId) } return tableRow, nil } func packetNumberFromTableRow(tableRow int) (termshark.JumpPos, error) { packetRowId, ok := packetListView.Model().RowIdentifier(tableRow) if !ok { return termshark.JumpPos{}, fmt.Errorf("Error looking up packet at row %v", tableRow) } // e.g. packet #123 var summary string if len(Loader.PsmlData()) > int(packetRowId) { summary = psmlSummary(Loader.PsmlData()[packetRowId]).String() } packetNum, err := strconv.Atoi(Loader.PsmlData()[packetRowId][0]) if err != nil { return termshark.JumpPos{}, fmt.Errorf("Unexpected error determining no. of packet %d: %v.", tableRow, err) } return termshark.JumpPos{ Pos: packetNum, Summary: summary, }, nil } // These only apply to the traditional wireshark-like main view func vimKeysMainView(evk *tcell.EventKey, app gowid.IApp) bool { handled := true if evk.Key() == tcell.KeyCtrlW && keyState.PartialCtrlWCmd { cycleView(app, true, tabViewsForward) } else if evk.Key() == tcell.KeyRune && evk.Rune() == '=' && keyState.PartialCtrlWCmd { clearOffsets(app) } else if evk.Key() == tcell.KeyRune && evk.Rune() >= 'a' && evk.Rune() <= 'z' && keyState.PartialmCmd { if packetListView != nil { tablePos, err := packetListView.FocusXY() // e.g. table position 5 if err != nil { OpenError(fmt.Sprintf("No packet in focus: %v", err), app) } else { jpos, err := packetNumberFromTableRow(tablePos.Row) if err != nil { OpenError(err.Error(), app) } else { marksMap[evk.Rune()] = jpos OpenMessage(fmt.Sprintf("Local mark '%c' set to packet %v.", evk.Rune(), jpos.Pos), appView, app) } } } } else if evk.Key() == tcell.KeyRune && evk.Rune() >= 'A' && evk.Rune() <= 'Z' && keyState.PartialmCmd { if Loader != nil { if Loader.Pcap() != "" { if packetListView != nil { tablePos, err := packetListView.FocusXY() if err != nil { OpenError(fmt.Sprintf("No packet in focus: %v", err), app) } else { jpos, err := packetNumberFromTableRow(tablePos.Row) if err != nil { OpenError(err.Error(), app) } else { globalMarksMap[evk.Rune()] = termshark.GlobalJumpPos{ JumpPos: jpos, Filename: Loader.Pcap(), } termshark.SaveGlobalMarks(globalMarksMap) OpenMessage(fmt.Sprintf("Global mark '%c' set to packet %v.", evk.Rune(), jpos.Pos), appView, app) } } } } } } else if evk.Key() == tcell.KeyRune && evk.Rune() >= 'a' && evk.Rune() <= 'z' && keyState.PartialQuoteCmd { if packetListView != nil { markedPacket, ok := marksMap[evk.Rune()] if ok { tableRow, err := tableRowFromPacketNumber(markedPacket.Pos) if err != nil { OpenError(err.Error(), app) } else { tableCol := 0 curTablePos, err := packetListView.FocusXY() if err == nil { tableCol = curTablePos.Column } pn, _ := packetNumberFromCurrentTableRow() // save for '' lastJumpPos = pn.Pos packetListView.SetFocusXY(app, table.Coords{Column: tableCol, Row: tableRow}) } } } } else if evk.Key() == tcell.KeyRune && evk.Rune() >= 'A' && evk.Rune() <= 'Z' && keyState.PartialQuoteCmd { markedPacket, ok := globalMarksMap[evk.Rune()] if !ok { OpenError("Mark not found.", app) } else { if Loader.Pcap() != markedPacket.Filename { RequestLoadPcapWithCheck(markedPacket.Filename, FilterWidget.Value(), markedPacket, app) } else { if packetListView != nil { tableRow, err := tableRowFromPacketNumber(markedPacket.Pos) if err != nil { OpenError(err.Error(), app) } else { tableCol := 0 curTablePos, err := packetListView.FocusXY() if err == nil { tableCol = curTablePos.Column } pn, _ := packetNumberFromCurrentTableRow() // save for '' lastJumpPos = pn.Pos packetListView.SetFocusXY(app, table.Coords{Column: tableCol, Row: tableRow}) } } } } } else if evk.Key() == tcell.KeyRune && evk.Rune() == '\'' && keyState.PartialQuoteCmd { if packetListView != nil { tablePos, err := packetListView.FocusXY() if err != nil { OpenError(fmt.Sprintf("No packet in focus: %v", err), app) } else { // which packet number was saved as a mark savedPacket := lastJumpPos if savedPacket != -1 { // Map that packet number #123 to the index in the PSML array - e.g. index 10 (order of psml load) if packetRowId, ok := Loader.PacketNumberMap[savedPacket]; !ok { OpenError(fmt.Sprintf("Error finding packet %v", savedPacket), app) } else { // This psml order is also the table RowId order. The table might be sorted though, so // map this RowId to the actual table row, so we can change focus to it if tableRow, ok := packetListView.InvertedModel().IdentifierToRow(table.RowId(packetRowId)); !ok { OpenError(fmt.Sprintf("Error looking up packet %v", packetRowId), app) } else { pn, _ := packetNumberFromCurrentTableRow() // save for '' lastJumpPos = pn.Pos packetListView.SetFocusXY(app, table.Coords{Column: tablePos.Column, Row: tableRow}) } } } } } } else { handled = false } return handled } // Move focus among the packet list view, structure view and hex view func cycleView(app gowid.IApp, forward bool, tabMap map[gowid.IWidget]gowid.IWidget) { if v, ok := tabMap[mainViewNoKeys.SubWidget()]; ok { mainViewNoKeys.SetSubWidget(v, app) } gowid.SetFocusPath(viewOnlyPacketList, maxViewPath, app) gowid.SetFocusPath(viewOnlyPacketStructure, maxViewPath, app) gowid.SetFocusPath(viewOnlyPacketHex, maxViewPath, app) if packetStructureViewHolder.SubWidget() == MissingMsgw { setFocusOnPacketList(app) } else { newidx := -1 if mainViewNoKeys.SubWidget() == mainview { v1p := gowid.FocusPath(mainview) if deep.Equal(v1p, mainviewPaths[0]) == nil { newidx = gwutil.If(forward, 1, 2).(int) } else if deep.Equal(v1p, mainviewPaths[1]) == nil { newidx = gwutil.If(forward, 2, 0).(int) } else { newidx = gwutil.If(forward, 0, 1).(int) } } else if mainViewNoKeys.SubWidget() == altview1 { v2p := gowid.FocusPath(altview1) if deep.Equal(v2p, altview1Paths[0]) == nil { newidx = gwutil.If(forward, 1, 2).(int) } else if deep.Equal(v2p, altview1Paths[1]) == nil { newidx = gwutil.If(forward, 2, 0).(int) } else { newidx = gwutil.If(forward, 0, 1).(int) } } else if mainViewNoKeys.SubWidget() == altview2 { v3p := gowid.FocusPath(altview2) if deep.Equal(v3p, altview2Paths[0]) == nil { newidx = gwutil.If(forward, 1, 2).(int) } else if deep.Equal(v3p, altview2Paths[1]) == nil { newidx = gwutil.If(forward, 2, 0).(int) } else { newidx = gwutil.If(forward, 0, 1).(int) } } if newidx != -1 { // Keep the views in sync gowid.SetFocusPath(mainview, mainviewPaths[newidx], app) gowid.SetFocusPath(altview1, altview1Paths[newidx], app) gowid.SetFocusPath(altview2, altview2Paths[newidx], app) } } } // Keys for the main view - packet list, structure, etc func mainKeyPress(evk *tcell.EventKey, app gowid.IApp) bool { handled := true isrune := evk.Key() == tcell.KeyRune if evk.Key() == tcell.KeyCtrlC && Loader.PsmlLoader.IsLoading() { Loader.StopLoadPsmlAndIface(NoHandlers{}) // iface and psml } else if evk.Key() == tcell.KeyTAB || evk.Key() == tcell.KeyBacktab { isTab := (evk.Key() == tcell.KeyTab) var tabMap map[gowid.IWidget]gowid.IWidget if isTab { tabMap = tabViewsForward } else { tabMap = tabViewsBackward } cycleView(app, isTab, tabMap) } else if isrune && evk.Rune() == '|' { if mainViewNoKeys.SubWidget() == mainview { mainViewNoKeys.SetSubWidget(altview1, app) termshark.SetConf("main.layout", "altview1") } else if mainViewNoKeys.SubWidget() == altview1 { mainViewNoKeys.SetSubWidget(altview2, app) termshark.SetConf("main.layout", "altview2") } else { mainViewNoKeys.SetSubWidget(mainview, app) termshark.SetConf("main.layout", "mainview") } } else if isrune && evk.Rune() == '\\' { w := mainViewNoKeys.SubWidget() fp := gowid.FocusPath(w) if w == viewOnlyPacketList || w == viewOnlyPacketStructure || w == viewOnlyPacketHex { switch termshark.ConfString("main.layout", "mainview") { case "altview1": mainViewNoKeys.SetSubWidget(altview1, app) case "altview2": mainViewNoKeys.SetSubWidget(altview2, app) default: mainViewNoKeys.SetSubWidget(mainview, app) } if deep.Equal(fp, maxViewPath) == nil { switch w { case viewOnlyPacketList: setFocusOnPacketList(app) case viewOnlyPacketStructure: setFocusOnPacketStruct(app) case viewOnlyPacketHex: setFocusOnPacketList(app) } } } else { gotov := 0 if mainViewNoKeys.SubWidget() == mainview { v1p := gowid.FocusPath(mainview) if deep.Equal(v1p, mainviewPaths[0]) == nil { gotov = 0 } else if deep.Equal(v1p, mainviewPaths[1]) == nil { gotov = 1 } else { gotov = 2 } } else if mainViewNoKeys.SubWidget() == altview1 { v2p := gowid.FocusPath(altview1) if deep.Equal(v2p, altview1Paths[0]) == nil { gotov = 0 } else if deep.Equal(v2p, altview1Paths[1]) == nil { gotov = 1 } else { gotov = 2 } } else if mainViewNoKeys.SubWidget() == altview2 { v3p := gowid.FocusPath(altview2) if deep.Equal(v3p, altview2Paths[0]) == nil { gotov = 0 } else if deep.Equal(v3p, altview2Paths[1]) == nil { gotov = 1 } else { gotov = 2 } } switch gotov { case 0: mainViewNoKeys.SetSubWidget(viewOnlyPacketList, app) if deep.Equal(fp, maxViewPath) == nil { gowid.SetFocusPath(viewOnlyPacketList, maxViewPath, app) } case 1: mainViewNoKeys.SetSubWidget(viewOnlyPacketStructure, app) if deep.Equal(fp, maxViewPath) == nil { gowid.SetFocusPath(viewOnlyPacketStructure, maxViewPath, app) } case 2: mainViewNoKeys.SetSubWidget(viewOnlyPacketHex, app) if deep.Equal(fp, maxViewPath) == nil { gowid.SetFocusPath(viewOnlyPacketHex, maxViewPath, app) } } } } else if isrune && evk.Rune() == '/' { setFocusOnDisplayFilter(app) } else { handled = false } return handled } func focusOnMenuButton(app gowid.IApp) { gowid.SetFocusPath(mainview, menuPathMain, app) gowid.SetFocusPath(altview1, menuPathAlt, app) gowid.SetFocusPath(altview2, menuPathAlt, app) gowid.SetFocusPath(viewOnlyPacketList, menuPathMax, app) gowid.SetFocusPath(viewOnlyPacketStructure, menuPathMax, app) gowid.SetFocusPath(viewOnlyPacketHex, menuPathMax, app) } func openGeneralMenu(app gowid.IApp) { focusOnMenuButton(app) generalMenu.Open(openMenuSite, app) } // Keys for the whole app, applicable whichever view is frontmost func appKeyPress(evk *tcell.EventKey, app gowid.IApp) bool { handled := true isrune := evk.Key() == tcell.KeyRune if evk.Key() == tcell.KeyCtrlC { reallyQuit(app) } else if evk.Key() == tcell.KeyCtrlL { app.Sync() } else if isrune && (evk.Rune() == 'q' || evk.Rune() == 'Q') { reallyQuit(app) } else if isrune && evk.Rune() == ':' { lastLineMode(app) } else if evk.Key() == tcell.KeyEscape { focusOnMenuButton(app) } else if isrune && evk.Rune() == '?' { OpenTemplatedDialog(appView, "UIHelp", app) } else if isrune && evk.Rune() == 'Z' && keyState.PartialZCmd { RequestQuit() } else if isrune && evk.Rune() == 'Z' { keyState.PartialZCmd = true } else if isrune && evk.Rune() == 'm' { keyState.PartialmCmd = true } else if isrune && evk.Rune() == '\'' { keyState.PartialQuoteCmd = true } else if isrune && evk.Rune() == 'g' { keyState.PartialgCmd = true } else if evk.Key() == tcell.KeyCtrlW { keyState.PartialCtrlWCmd = true } else if isrune && evk.Rune() >= '0' && evk.Rune() <= '9' { if keyState.NumberPrefix == -1 { keyState.NumberPrefix = int(evk.Rune() - '0') } else { keyState.NumberPrefix = (10 * keyState.NumberPrefix) + (int(evk.Rune() - '0')) } } 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("button"), gowid.MakePaletteRef("button-focus")) stop.OnClick(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, w gowid.IWidget) { Loader.StopLoadPsmlAndIface(NoHandlers{}) // psml and iface })) 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) } // setLowerWidgets will set the packet structure and packet hex views, if there // is suitable data to display. If not, they are left as-is. func setLowerWidgets(app gowid.IApp) { var sw1 gowid.IWidget var sw2 gowid.IWidget if packetListView != nil { if fxy, err := packetListView.FocusXY(); err == nil { row2, _ := packetListView.Model().RowIdentifier(fxy.Row) row := int(row2) hex := getHexWidgetToDisplay(row) if hex != nil { sw1 = enableselected.New( withscrollbar.New( hex, withscrollbar.Options{ HideIfContentFits: true, }, ), ) } str := getStructWidgetToDisplay(row, app) if str != nil { sw2 = enableselected.New(str) } } } if sw1 != nil { packetHexViewHolder.SetSubWidget(sw1, app) StopEmptyHexViewTimer() } else { // If autoscroll is on, it's annoying to see the constant loading message, so // suppress and just remain on the last displayed hex timer := false if AutoScroll { // Only displaying loading if the current panel is blank. If it's data, leave the data if packetHexViewHolder.SubWidget() == nullw { timer = true } } else { if packetHexViewHolder.SubWidget() != MissingMsgw { timer = true } } if timer { if EmptyHexViewTimer == nil { EmptyHexViewTimer = time.AfterFunc(time.Duration(1000)*time.Millisecond, func() { app.Run(gowid.RunFunction(func(app gowid.IApp) { singlePacketViewMsgHolder.SetSubWidget(Loadingw, app) packetHexViewHolder.SetSubWidget(MissingMsgw, app) })) }) } } } if sw2 != nil { packetStructureViewHolder.SetSubWidget(sw2, app) StopEmptyStructViewTimer() } else { timer := false if AutoScroll { if packetStructureViewHolder.SubWidget() == nullw { timer = true } } else { if packetStructureViewHolder.SubWidget() != MissingMsgw { timer = true } } // If autoscroll is on, it's annoying to see the constant loading message, so // suppress and just remain on the last displayed hex if timer { if EmptyStructViewTimer == nil { EmptyStructViewTimer = time.AfterFunc(time.Duration(1000)*time.Millisecond, func() { app.Run(gowid.RunFunction(func(app gowid.IApp) { singlePacketViewMsgHolder.SetSubWidget(Loadingw, app) packetStructureViewHolder.SetSubWidget(MissingMsgw, app) })) }) } } } } func makePacketListModel(psml psmlInfo, app gowid.IApp) *psmlmodel.Model { packetPsmlTableModel := table.NewSimpleModel( psml.PsmlHeaders(), psml.PsmlData(), table.SimpleOptions{ Style: table.StyleOptions{ VerticalSeparator: fill.New(' '), HeaderStyleProvided: true, HeaderStyleFocus: gowid.MakePaletteRef("packet-list-cell-focus"), CellStyleProvided: true, CellStyleSelected: gowid.MakePaletteRef("packet-list-cell-selected"), CellStyleFocus: gowid.MakePaletteRef("packet-list-cell-focus"), }, Layout: table.LayoutOptions{ Widths: []gowid.IWidgetDimension{ weightupto(6, 10), weightupto(8, 24), weightupto(14, 32), weightupto(14, 32), weightupto(12, 32), weightupto(8, 8), weight(40), }, }, }, ) expandingModel := psmlmodel.New( packetPsmlTableModel, gowid.MakePaletteRef("packet-list-row-focus"), ) if len(expandingModel.Comparators) > 0 { expandingModel.Comparators[0] = table.IntCompare{} expandingModel.Comparators[5] = table.IntCompare{} } return expandingModel } func updatePacketListWithData(psml psmlInfo, app gowid.IApp) { packetListView.colors = psml.PsmlColors() // otherwise this isn't updated model := makePacketListModel(psml, app) newPacketsArrived = true packetListTable.SetModel(model, app) newPacketsArrived = false if AutoScroll { coords, err := packetListView.FocusXY() if err == nil { coords.Row = packetListTable.Length() - 1 newPacketsArrived = true // Set focus on the last item in the view, then... packetListView.SetFocusXY(app, coords) newPacketsArrived = false } // ... adjust the widget so it is rendering with the last item at the bottom. packetListTable.GoToBottom(app) } // Only do this once, the first time. if !packetListView.didFirstAutoFocus && len(psml.PsmlData()) > 0 { packetListView.SetFocusOnData(app) packetListView.didFirstAutoFocus = true } } // don't claim the keypress func ApplyAutoScroll(ev *tcell.EventKey, app gowid.IApp) bool { doit := false reenableAutoScroll = false switch ev.Key() { case tcell.KeyRune: if ev.Rune() == 'G' { doit = true } case tcell.KeyEnd: doit = true } if doit { if termshark.ConfBool("main.auto-scroll", true) { AutoScroll = true reenableAutoScroll = true // when packet updates come, helps // understand that AutoScroll should not be disabled again } } return false } type psmlInfo interface { PsmlData() [][]string PsmlHeaders() []string PsmlColors() []pcap.PacketColors } func setPacketListWidgets(psml psmlInfo, app gowid.IApp) { expandingModel := makePacketListModel(psml, app) packetListTable = &table.BoundedWidget{Widget: table.New(expandingModel)} packetListView = NewPsmlTableRowWidget( NewRowFocusTableWidget( packetListTable, "packet-list-row-selected", "packet-list-row-focus", ), psml.PsmlColors(), ) packetListView.OnFocusChanged(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, w gowid.IWidget) { fxy, err := packetListView.FocusXY() if err != nil { return } if !newPacketsArrived && !reenableAutoScroll { // this focus change must've been user-initiated, so stop auto-scrolling with new packets. // This mimics Wireshark's behavior. Note that if the user hits the end key, this may // update the view and run this callback, but end means to resume auto-scrolling if it's // enabled, so we should not promptly disable it again AutoScroll = false } row2 := fxy.Row row3, gotrow := packetListView.Model().RowIdentifier(row2) row := int(row3) if gotrow && row >= 0 { pktsPerLoad := Loader.PacketsPerLoad() rowm := row % pktsPerLoad CacheRequests = CacheRequests[:0] CacheRequests = append(CacheRequests, pcap.LoadPcapSlice{ Row: (row / pktsPerLoad) * pktsPerLoad, CancelCurrent: true, }) if rowm > pktsPerLoad/2 { // Optimistically load the batch below this one CacheRequests = append(CacheRequests, pcap.LoadPcapSlice{ Row: ((row / pktsPerLoad) + 1) * pktsPerLoad, }) } else { // Optimistically load the batch above this one row2 := ((row / pktsPerLoad) - 1) * pktsPerLoad if row2 < 0 { row2 = 0 } CacheRequests = append(CacheRequests, pcap.LoadPcapSlice{ Row: row2, }) } CacheRequestsChan <- struct{}{} } // When the focus changes, update the hex and struct view. If they cannot // be populated, display a loading message setLowerWidgets(app) })) withScrollbar := withscrollbar.New(packetListView, withscrollbar.Options{ HideIfContentFits: true, }) selme := enableselected.New(withScrollbar) keys := appkeys.New( selme, tableutil.GotoHandler(&tableutil.GoToAdapter{ BoundedWidget: packetListTable, KeyState: &keyState, }), ) packetListViewHolder.SetSubWidget(keys, app) } func expandStructWidgetAtPosition(row int, pos int, app gowid.IApp) { if curPacketStructWidget != nil { walker := curPacketStructWidget.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.SetCollapsed(app, false) finalPos = append(finalPos, chosenIdx+hack) curTree = chosenTree hack = 0 } else { // didn't find any break Out } } if len(finalPos) > 0 { curStructPosition = 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(curStructPosition, app) curPacketStructWidget.GoToMiddle(app) curStructWidgetState = curPacketStructWidget.State() updateCurrentPdmlPosition(walker.Tree()) } } } func updateCurrentPdmlPosition(tr tree.IModel) { treeAtCurPos := curStructPosition.GetSubStructure(tr) // Save [/, tcp, tcp.srcport] - so we can apply if user moves in packet list curPdmlPosition = treeAtCurPos.(*pdmltree.Model).PathToRoot() } func getLayersFromStructWidget(row int, pos int) []hexdumper2.LayerStyler { layers := make([]hexdumper2.LayerStyler, 0) model := getCurrentStructModel(row) if model != nil { layers = model.HexLayers(pos, false) } return layers } func getHexWidgetKey(row int) []byte { return []byte(fmt.Sprintf("p%d", row)) } // Can return nil func getHexWidgetToDisplay(row int) *hexdumper2.Widget { var res2 *hexdumper2.Widget if val, ok := packetHexWidgets.Get(row); ok { res2 = val.(*hexdumper2.Widget) } else { pktsPerLoad := Loader.PacketsPerLoad() row2 := (row / pktsPerLoad) * pktsPerLoad if ws, ok := Loader.PacketCache.Get(row2); ok { srca := ws.(pcap.CacheEntry).Pcap if len(srca) > row%pktsPerLoad { src := srca[row%pktsPerLoad] b := make([]byte, len(src)) copy(b, src) layers := getLayersFromStructWidget(row, 0) res2 = hexdumper2.New(b, hexdumper2.Options{ StyledLayers: layers, CursorUnselected: "hex-byte-unselected", CursorSelected: "hex-byte-selected", LineNumUnselected: "hex-interval-unselected", LineNumSelected: "hex-interval-selected", PaletteIfCopying: "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 mainViewNoKeys.SubWidget() == mainview { v1p := gowid.FocusPath(mainview) if deep.Equal(v1p, mainviewPaths[2]) != nil { // it's not hex return } } else if mainViewNoKeys.SubWidget() == altview1 { v2p := gowid.FocusPath(altview1) if deep.Equal(v2p, altview1Paths[2]) != nil { // it's not hex return } } else { // altview2 v3p := gowid.FocusPath(altview2) if deep.Equal(v3p, altview2Paths[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 // Note - returns nil if one can't be found func getStructWidgetToDisplay(row int, app gowid.IApp) gowid.IWidget { var res gowid.IWidget model := getCurrentStructModel(row) if model != nil { // Apply expanded paths from previous packet model.ApplyExpandedPaths(&curExpandedStructNodes) model.Expanded = true var pos tree.IPos = tree.NewPos() pos = tree.NextPosition(pos, model) // Start ahead by one, then never go back rwalker := tree.NewWalker(model, pos, tree.NewCachingMaker(tree.WidgetMakerFunction(makeStructNodeWidget)), tree.NewCachingDecorator(tree.DecoratorFunction(makeStructNodeDecoration))) // Without the caching layer, clicking on a button has no effect walker := termshark.NewNoRootWalker(rwalker) // 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 hexdumper2 // 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) } } } } tb := copymodetree.New(tree.New(walker), copyModePalette{}) res = tb // Save this in case the hex layer needs to change it curPacketStructWidget = tb // if not nil, it means the user has interacted with some struct widget at least once causing // a focus change. We track the current focus e.g. [0, 2, 1] - the indices through the tree leading // to the focused item. We programatically adjust the focus widget of the new struct (e.g. after // navigating down one in the packet list), but only if we can move focus to the same PDML field // as the old struct. For example, if we are on tcp.srcport in the old packet, and we can // open up tcp.srcport in the new packet, then we do so. This is not perfect, because I use the old // pdml tre eposition, which is a sequence of integer indices. This means if the next packet has // an extra layer before TCP, say some encapsulation, then I could still open up tcp.srcport, but // I don't find it because I find the candidate focus widget using the list of integer indices. if curStructPosition != nil { curPos := curStructPosition // e.g. [0, 2, 1] treeAtCurPos := curPos.GetSubStructure(walker.Tree()) // e.g. the TCP *pdmltree.Model if treeAtCurPos != nil && deep.Equal(curPdmlPosition, treeAtCurPos.(*pdmltree.Model).PathToRoot()) == nil { // if the newly selected struct has a node at [0, 2, 1] and it maps to tcp.srcport via the same path, // set the focus widget of the new struct i.e. which leaf has focus walker.SetFocus(curPos, app) if curStructWidgetState != nil { // we scrolled the previous struct a bit, apply it to the new one too tb.SetState(curStructWidgetState, app) } else { // First change by the user, so remember it and use it when navigating to the next curStructWidgetState = tb.State() } } } else { curStructPosition = walker.Focus().(tree.IPos) } tb.OnFocusChanged(gowid.MakeWidgetCallback("cb", gowid.WidgetChangedFunction(func(app gowid.IApp, w gowid.IWidget) { curStructWidgetState = tb.State() }))) walker.OnFocusChanged(tree.MakeCallback("cb", func(app gowid.IApp, twalker tree.ITreeWalker) { updateHex(app, twalker) // need to save the position, so it can be applied to the next struct widget // if brought into focus by packet list navigation curStructPosition = walker.Focus().(tree.IPos) updateCurrentPdmlPosition(walker.Tree()) })) // Update hex at the end, having set up callbacks. We want to make sure that // navigating around the hext view expands the struct view in such a way as to // preserve these changes when navigating the packet view updateHex(app, walker) } 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, }, ) } //====================================================================== func RequestLoadInterfaces(psrcs []pcap.IPacketSource, captureFilter string, displayFilter string, tmpfile string, app gowid.IApp) { Loader.Renew() Loader.LoadInterfaces(psrcs, captureFilter, displayFilter, tmpfile, pcap.HandlerList{ StartUIWhenThereArePackets{}, SimpleErrors{}, MakeSaveRecents("", displayFilter), MakePacketViewUpdater(), MakeUpdateCurrentCaptureInTitle(), ManageStreamCache{}, ManageCapinfoCache{}, SetStructWidgets{Loader}, // for OnClear ClearMarksHandler{}, CancelledMessage{}, }, app, ) } //====================================================================== // Call from app goroutine context func RequestLoadPcapWithCheck(pcapf string, displayFilter string, jump termshark.GlobalJumpPos, app gowid.IApp) { handlers := pcap.HandlerList{ SimpleErrors{}, MakeSaveRecents(pcapf, displayFilter), MakePacketViewUpdater(), MakeUpdateCurrentCaptureInTitle(), ManageStreamCache{}, ManageCapinfoCache{}, SetStructWidgets{Loader}, // for OnClear MakeCheckGlobalJumpAfterPsml(jump), ClearMarksHandler{}, CancelledMessage{}, } if _, err := os.Stat(pcapf); os.IsNotExist(err) { pcap.HandleError(pcap.NoneCode, app, err, handlers) } else { // no auto-scroll when reading a file AutoScroll = false Loader.LoadPcap(pcapf, displayFilter, handlers, app) } } //====================================================================== func RequestNewFilter(displayFilter string, app gowid.IApp) { handlers := pcap.HandlerList{ SimpleErrors{}, MakePacketViewUpdater(), MakeUpdateCurrentCaptureInTitle(), SetStructWidgets{Loader}, // for OnClear ClearMarksHandler{}, // Don't use this one - we keep the cancelled flag set so that we // don't restart live captures on clear if ctrl-c has been issued // so we don't want this handler on a new filter because we don't // want to be told again after applying the filter that the load // was cancelled //MakeCancelledMessage(), } Loader.NewFilter(displayFilter, handlers, app) } //====================================================================== // 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 (p Prog) Div(y int64) Prog { p.cur /= y return p } func (p Prog) Add(y Prog) Prog { return Prog{cur: p.cur + y.cur, max: p.max + y.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 } } //====================================================================== func makeRecentMenuWidget() gowid.IWidget { savedItems := make([]menuutil.SimpleMenuItem, 0) cfiles := termshark.ConfStringSlice("main.recent-files", []string{}) if cfiles != nil { for i, s := range cfiles { scopy := s savedItems = append(savedItems, menuutil.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(), NoGlobalJump, app) }, }, ) } } savedListBox := menuutil.MakeMenuWithHotKeys(savedItems) return savedListBox } func UpdateRecentMenu(app gowid.IApp) { savedListBox := makeRecentMenuWidget() savedListBoxWidgetHolder.SetSubWidget(savedListBox, app) } //====================================================================== type savedCompleterCallback struct { prefix string comp termshark.IPrefixCompleterCallback } var _ termshark.IPrefixCompleterCallback = (*savedCompleterCallback)(nil) func (s *savedCompleterCallback) Call(orig []string) { if s.prefix == "" { comps := termshark.ConfStrings("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) } //====================================================================== func StopEmptyStructViewTimer() { if EmptyStructViewTimer != nil { EmptyStructViewTimer.Stop() EmptyStructViewTimer = nil } } func StopEmptyHexViewTimer() { if EmptyHexViewTimer != nil { EmptyHexViewTimer.Stop() EmptyHexViewTimer = nil } } //====================================================================== func assignTo(wp interface{}, w gowid.IWidget) gowid.IWidget { reflect.ValueOf(wp).Elem().Set(reflect.ValueOf(w)) return w } //====================================================================== // prefixKeyWidget wraps a widget, and adjusts the state of the variables tracking // "partial" key chords e.g. the first Z in ZZ, the first g in gg. It also resets // the number prefix (which some commands use) - this is done if they key is not // a number, and the last keypress wasn't the start of a key chord. type prefixKeyWidget struct { gowid.IWidget } func (w *prefixKeyWidget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { // Save these first. If they are enabled now, any key should cancel them, so cancel // at the end. startingKeyState := keyState handled := w.IWidget.UserInput(ev, size, focus, app) switch ev := ev.(type) { case *tcell.EventKey: // If it was set this time around, whatever key was pressed resets it if startingKeyState.PartialgCmd { keyState.PartialgCmd = false } if startingKeyState.PartialZCmd { keyState.PartialZCmd = false } if startingKeyState.PartialCtrlWCmd { keyState.PartialCtrlWCmd = false } if startingKeyState.PartialmCmd { keyState.PartialmCmd = false } if startingKeyState.PartialQuoteCmd { keyState.PartialQuoteCmd = false } if ev.Key() != tcell.KeyRune || ev.Rune() < '0' || ev.Rune() > '9' { if !keyState.PartialZCmd && !keyState.PartialgCmd && !keyState.PartialCtrlWCmd { keyState.NumberPrefix = -1 } } } return handled } //====================================================================== func Build() (*gowid.App, error) { var err error var app *gowid.App widgetCacheSize := termshark.ConfInt("main.ui-cache-size", 1000) if widgetCacheSize < 64 { widgetCacheSize = 64 } packetHexWidgets, err = lru.New(widgetCacheSize) if err != nil { return nil, gowid.WithKVs(termshark.InternalErr, map[string]interface{}{ "err": err, }) } nullw = null.New() Loadingw = text.New("Loading, please wait...") singlePacketViewMsgHolder = holder.New(nullw) 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(singlePacketViewMsgHolder, 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"), BorderStyle: gowid.MakePaletteRef("dialog"), ButtonStyle: gowid.MakePaletteRef("dialog-button"), }, ) title := styled.New(text.New(termshark.TemplateToString(Templates, "NameVer", TemplateData)), gowid.MakePaletteRef("title")) currentCapture = text.New("") currentCaptureStyled := styled.New( currentCapture, gowid.MakePaletteRef("current-capture"), ) sp := text.New(" ") currentCaptureWidget = columns.NewFixed( sp, &gowid.ContainerWidget{ IWidget: fill.New('|'), D: gowid.MakeRenderBox(1, 1), }, sp, currentCaptureStyled, ) currentCaptureWidgetHolder = holder.New(nullw) CopyModePredicate = func() bool { return app != nil && app.InCopyMode() } CopyModeWidget = styled.New( ifwidget.New( text.New(" COPY-MODE "), null.New(), CopyModePredicate, ), gowid.MakePaletteRef("copy-mode-label"), ) //====================================================================== openMenu := button.NewBare(text.New(" Misc ")) openMenu2 := clicktracker.New( styled.NewExt( openMenu, gowid.MakePaletteRef("button"), gowid.MakePaletteRef("button-focus"), ), ) openMenuSite = menu.NewSite(menu.SiteOptions{YOffset: 1}) openMenu.OnClick(gowid.MakeWidgetCallback(gowid.ClickCB{}, func(app gowid.IApp, target gowid.IWidget) { generalMenu.Open(openMenuSite, app) })) //====================================================================== generalMenuItems := make([]menuutil.SimpleMenuItem, 0) generalMenuItems = append(generalMenuItems, []menuutil.SimpleMenuItem{ menuutil.SimpleMenuItem{ Txt: "Refresh Screen", Key: gowid.MakeKeyExt2(0, tcell.KeyCtrlL, ' '), CB: func(app gowid.IApp, w gowid.IWidget) { generalMenu.Close(app) app.Sync() }, }, // Put 2nd so a simple menu click, down, enter without thinking doesn't toggle dark mode (annoying...) menuutil.SimpleMenuItem{ Txt: "Toggle Dark Mode", Key: gowid.MakeKey('d'), CB: func(app gowid.IApp, w gowid.IWidget) { generalMenu.Close(app) DarkMode = !DarkMode termshark.SetConf("main.dark-mode", DarkMode) }, }, menuutil.MakeMenuDivider(), menuutil.SimpleMenuItem{ Txt: "Clear Packets", Key: gowid.MakeKeyExt2(0, tcell.KeyCtrlW, ' '), CB: func(app gowid.IApp, w gowid.IWidget) { generalMenu.Close(app) reallyClear(app) }, }}...) if runtime.GOOS != "windows" { generalMenuItems = append(generalMenuItems, menuutil.SimpleMenuItem{ Txt: "Show Log", Key: gowid.MakeKey('l'), CB: func(app gowid.IApp, w gowid.IWidget) { analysisMenu.Close(app) openLogsUi(app) }, }) } generalMenuItems = append(generalMenuItems, []menuutil.SimpleMenuItem{ menuutil.MakeMenuDivider(), menuutil.SimpleMenuItem{ Txt: "Help", Key: gowid.MakeKey('?'), CB: func(app gowid.IApp, w gowid.IWidget) { generalMenu.Close(app) OpenTemplatedDialog(appView, "UIHelp", app) }, }, menuutil.SimpleMenuItem{ Txt: "User Guide", Key: gowid.MakeKey('u'), CB: func(app gowid.IApp, w gowid.IWidget) { generalMenu.Close(app) if !termshark.RunningRemotely() { termshark.BrowseUrl(termshark.UserGuideURL) } openResultsAfterCopy("UIUserGuide", termshark.UserGuideURL, app) }, }, menuutil.SimpleMenuItem{ Txt: "FAQ", Key: gowid.MakeKey('f'), CB: func(app gowid.IApp, w gowid.IWidget) { generalMenu.Close(app) if !termshark.RunningRemotely() { termshark.BrowseUrl(termshark.FAQURL) } openResultsAfterCopy("UIFAQ", termshark.FAQURL, app) }, }, menuutil.MakeMenuDivider(), menuutil.SimpleMenuItem{ Txt: "Found a Bug?", Key: gowid.MakeKey('B'), CB: func(app gowid.IApp, w gowid.IWidget) { generalMenu.Close(app) if !termshark.RunningRemotely() { termshark.BrowseUrl(termshark.BugURL) } openResultsAfterCopy("UIBug", termshark.BugURL, app) }, }, menuutil.SimpleMenuItem{ Txt: "Feature Request?", Key: gowid.MakeKey('F'), CB: func(app gowid.IApp, w gowid.IWidget) { generalMenu.Close(app) if !termshark.RunningRemotely() { termshark.BrowseUrl(termshark.FeatureURL) } openResultsAfterCopy("UIFeature", termshark.FeatureURL, app) }, }, menuutil.MakeMenuDivider(), menuutil.SimpleMenuItem{ Txt: "Quit", Key: gowid.MakeKey('q'), CB: func(app gowid.IApp, w gowid.IWidget) { generalMenu.Close(app) reallyQuit(app) }, }, }...) if PacketColorsSupported { generalMenuItems = append( generalMenuItems[0:2], append( []menuutil.SimpleMenuItem{ menuutil.SimpleMenuItem{ Txt: "Toggle Packet Colors", Key: gowid.MakeKey('c'), CB: func(app gowid.IApp, w gowid.IWidget) { generalMenu.Close(app) PacketColors = !PacketColors termshark.SetConf("main.packet-colors", PacketColors) }, }, }, generalMenuItems[2:]..., )..., ) } generalMenuListBox := menuutil.MakeMenuWithHotKeys(generalMenuItems) var generalNext menuutil.NextMenu generalMenuListBoxWithKeys := appkeys.New( generalMenuListBox, menuutil.MakeMenuNavigatingKeyPress( &generalNext, nil, ), ) generalMenu = menu.New("main", generalMenuListBoxWithKeys, fixed, menu.Options{ Modal: true, CloseKeysProvided: true, CloseKeys: []gowid.IKey{ gowid.MakeKeyExt(tcell.KeyEscape), gowid.MakeKeyExt(tcell.KeyCtrlC), }, }) //====================================================================== openAnalysis := button.NewBare(text.New(" Analysis ")) openAnalysis2 := clicktracker.New( styled.NewExt( openAnalysis, gowid.MakePaletteRef("button"), gowid.MakePaletteRef("button-focus"), ), ) openAnalysisSite = menu.NewSite(menu.SiteOptions{YOffset: 1}) openAnalysis.OnClick(gowid.MakeWidgetCallback(gowid.ClickCB{}, func(app gowid.IApp, target gowid.IWidget) { analysisMenu.Open(openAnalysisSite, app) })) analysisMenuItems := []menuutil.SimpleMenuItem{ menuutil.SimpleMenuItem{ Txt: "Capture file properties", Key: gowid.MakeKey('p'), CB: func(app gowid.IApp, w gowid.IWidget) { analysisMenu.Close(app) startCapinfo(app) }, }, menuutil.SimpleMenuItem{ Txt: "Reassemble stream", Key: gowid.MakeKey('f'), CB: func(app gowid.IApp, w gowid.IWidget) { analysisMenu.Close(app) startStreamReassembly(app) }, }, menuutil.SimpleMenuItem{ Txt: "Conversations", Key: gowid.MakeKey('c'), CB: func(app gowid.IApp, w gowid.IWidget) { analysisMenu.Close(app) openConvsUi(app) }, }, } analysisMenuListBox := menuutil.MakeMenuWithHotKeys(analysisMenuItems) var analysisNext menuutil.NextMenu analysisMenuListBoxWithKeys := appkeys.New( analysisMenuListBox, menuutil.MakeMenuNavigatingKeyPress( nil, &analysisNext, ), ) analysisMenu = menu.New("analysis", analysisMenuListBoxWithKeys, fixed, menu.Options{ Modal: true, CloseKeysProvided: true, CloseKeys: []gowid.IKey{ gowid.MakeKey('q'), 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), }, }) var titleCols *columns.Widget // If anything gets added or removed here, see [[generalmenu1]] // and [[generalmenu2]] and [[generalmenu3]] titleView := overlay.New( hpadding.New(CopyModeWidget, gowid.HAlignMiddle{}, fixed), assignTo(&titleCols, columns.NewFixed( title, &gowid.ContainerWidget{ IWidget: currentCaptureWidgetHolder, D: weight(10), // give it priority when the window isn't wide enough }, &gowid.ContainerWidget{ IWidget: fill.New(' '), D: weight(1), }, openAnalysisSite, openAnalysis2, openMenuSite, openMenu2, )), gowid.VAlignTop{}, gowid.RenderWithRatio{R: 1}, gowid.HAlignMiddle{}, gowid.RenderWithRatio{R: 1}, overlay.Options{ BottomGetsFocus: true, TopGetsNoFocus: true, BottomGetsCursor: true, }, ) // Fill this in once generalMenu is defined and titleView is defined // <> generalNext.Cur = generalMenu generalNext.Next = analysisMenu generalNext.Site = openAnalysisSite generalNext.Container = titleCols generalNext.Focus = 4 // should really find by ID // <> analysisNext.Cur = analysisMenu analysisNext.Next = generalMenu analysisNext.Site = openMenuSite analysisNext.Container = titleCols analysisNext.Focus = 6 // should really find by ID packetListViewHolder = holder.New(nullw) packetStructureViewHolder = holder.New(nullw) packetHexViewHolder = holder.New(nullw) progressHolder = holder.New(nullw) applyw := button.New(text.New("Apply")) applyWidget := disable.NewEnabled( clicktracker.New( styled.NewExt( applyw, gowid.MakePaletteRef("button"), gowid.MakePaletteRef("button-focus"), ), ), ) FilterWidget = filter.New(filter.Options{ Completer: savedCompleter{def: termshark.NewFields()}, }) validFilterCb := gowid.MakeWidgetCallback("cb", func(app gowid.IApp, w gowid.IWidget) { if Loader.DisplayFilter() == FilterWidget.Value() { OpenError("Same filter - nothing to do", app) } else { RequestNewFilter(FilterWidget.Value(), app) } }) // Will only be enabled to click if filter is valid applyw.OnClick(validFilterCb) // Will only fire OnSubmit if filter is valid FilterWidget.OnSubmit(validFilterCb) 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 := clicktracker.New( styled.NewExt( savedw, gowid.MakePaletteRef("button"), gowid.MakePaletteRef("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( appkeys.New( packetListViewHolder, ApplyAutoScroll, appkeys.Options{ ApplyBefore: true, }, ), appKeysResize1, ), widgets.SwallowMovementKeys, ), widgets.SwallowMouseScroll, ) packetStructureViewWithKeys := appkeys.New( appkeys.New( appkeys.NewMouse( appkeys.New( appkeys.New( packetStructureViewHolder, appKeysResize2, ), widgets.SwallowMovementKeys, ), widgets.SwallowMouseScroll, ), copyModeEnterKeys, appkeys.Options{ ApplyBefore: true, }, ), copyModeExitKeys, appkeys.Options{ ApplyBefore: true, }, ) packetHexViewHolderWithKeys := appkeys.New( appkeys.New( appkeys.NewMouse( appkeys.New( packetHexViewHolder, widgets.SwallowMovementKeys, ), widgets.SwallowMouseScroll, ), copyModeEnterKeys, appkeys.Options{ ApplyBefore: true, }, ), copyModeExitKeys, appkeys.Options{ ApplyBefore: true, }, ) mainviewRows = 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), }, }) mainviewRows.OnOffsetsSet(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, w gowid.IWidget) { termshark.SaveOffsetToConfig("mainview", mainviewRows.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), }, }) tabViewsForward = make(map[gowid.IWidget]gowid.IWidget) tabViewsBackward = make(map[gowid.IWidget]gowid.IWidget) tabViewsForward[viewOnlyPacketList] = viewOnlyPacketStructure tabViewsForward[viewOnlyPacketStructure] = viewOnlyPacketHex tabViewsForward[viewOnlyPacketHex] = viewOnlyPacketList tabViewsBackward[viewOnlyPacketList] = viewOnlyPacketHex tabViewsBackward[viewOnlyPacketStructure] = viewOnlyPacketList tabViewsBackward[viewOnlyPacketHex] = viewOnlyPacketStructure //====================================================================== altview1Pile = resizable.NewPile([]gowid.IContainerWidget{ &gowid.ContainerWidget{ IWidget: packetListViewWithKeys, D: weight(1), }, &gowid.ContainerWidget{ IWidget: divider.NewUnicode(), D: flow, }, &gowid.ContainerWidget{ IWidget: packetStructureViewWithKeys, D: weight(1), }, }) altview1Pile.OnOffsetsSet(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, w gowid.IWidget) { termshark.SaveOffsetToConfig("altviewleft", altview1Pile.GetOffsets()) })) altview1PileAndKeys := appkeys.New(altview1Pile, altview1PileKeyPress) altview1Cols = resizable.NewColumns([]gowid.IContainerWidget{ &gowid.ContainerWidget{ IWidget: altview1PileAndKeys, D: weight(1), }, &gowid.ContainerWidget{ IWidget: fillVBar, D: units(1), }, &gowid.ContainerWidget{ IWidget: packetHexViewHolderWithKeys, D: weight(1), }, }) altview1Cols.OnOffsetsSet(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, w gowid.IWidget) { termshark.SaveOffsetToConfig("altviewright", altview1Cols.GetOffsets()) })) altview1ColsAndKeys := appkeys.New(altview1Cols, altview1ColsKeyPress) altview1OuterRows = resizable.NewPile([]gowid.IContainerWidget{ &gowid.ContainerWidget{ IWidget: titleView, D: units(1), }, &gowid.ContainerWidget{ IWidget: filterView, D: units(3), }, &gowid.ContainerWidget{ IWidget: altview1ColsAndKeys, D: weight(1), }, }) //====================================================================== altview2ColsAndKeys := appkeys.New( assignTo(&altview2Cols, resizable.NewColumns([]gowid.IContainerWidget{ &gowid.ContainerWidget{ IWidget: packetStructureViewWithKeys, D: weight(1), }, &gowid.ContainerWidget{ IWidget: fillVBar, D: units(1), }, &gowid.ContainerWidget{ IWidget: packetHexViewHolderWithKeys, D: weight(1), }, }), ), altview2ColsKeyPress, ) altview2Cols.OnOffsetsSet(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, w gowid.IWidget) { termshark.SaveOffsetToConfig("altview2vertical", altview2Cols.GetOffsets()) })) altview2PileAndKeys := appkeys.New( assignTo(&altview2Pile, resizable.NewPile([]gowid.IContainerWidget{ &gowid.ContainerWidget{ IWidget: packetListViewWithKeys, D: weight(1), }, &gowid.ContainerWidget{ IWidget: divider.NewUnicode(), D: flow, }, &gowid.ContainerWidget{ IWidget: altview2ColsAndKeys, D: weight(1), }, }), ), altview2PileKeyPress, ) altview2Pile.OnOffsetsSet(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, w gowid.IWidget) { termshark.SaveOffsetToConfig("altview2horizontal", altview2Pile.GetOffsets()) })) altview2OuterRows = resizable.NewPile([]gowid.IContainerWidget{ &gowid.ContainerWidget{ IWidget: titleView, D: units(1), }, &gowid.ContainerWidget{ IWidget: filterView, D: units(3), }, &gowid.ContainerWidget{ IWidget: altview2PileAndKeys, D: weight(1), }, }) //====================================================================== maxViewPath = []interface{}{2, 0} // list, structure or hex - whichever one is selected mainviewPaths = [][]interface{}{ {2}, // packet list {4}, // packet structure {6}, // packet hex } altview1Paths = [][]interface{}{ {2, 0, 0}, // packet list {2, 0, 2}, // packet structure {2, 2}, // packet hex } altview2Paths = [][]interface{}{ {2, 0}, // packet list {2, 2, 0}, // packet structure {2, 2, 2}, // packet hex } filterPathMain = []interface{}{1, 1} filterPathAlt = []interface{}{1, 1} filterPathMax = []interface{}{1, 1} mainview = mainviewRows altview1 = altview1OuterRows altview2 = altview2OuterRows mainViewNoKeys = holder.New(mainview) defaultLayout := termshark.ConfString("main.layout", "") switch defaultLayout { case "altview1": mainViewNoKeys = holder.New(altview1) case "altview2": mainViewNoKeys = holder.New(altview2) } // <> menuPathMain = []interface{}{0, 6} menuPathAlt = []interface{}{0, 6} menuPathMax = []interface{}{0, 6} buildStreamUi() buildFilterConvsMenu() mainView = appkeys.New( appkeys.New( mainViewNoKeys, mainKeyPress, ), vimKeysMainView, appkeys.Options{ ApplyBefore: true, }, ) //====================================================================== palette := PaletteSwitcher{ P1: &DarkModePalette, P2: &RegularPalette, ChooseOne: &DarkMode, } appViewWithKeys := &prefixKeyWidget{ IWidget: appkeys.New( assignTo(&appViewNoKeys, holder.New(mainView)), appKeyPress, ), } // For minibuffer mbView = holder.New(appViewWithKeys) Fin = rossshark.New(mbView) if !termshark.ConfBool("main.disable-shark-fin", false) { steerableFin := appkeys.NewMouse( appkeys.New( Fin, func(evk *tcell.EventKey, app gowid.IApp) bool { if Fin.Active() { switch evk.Key() { case tcell.KeyLeft: Fin.Dir = rossshark.Backward case tcell.KeyRight: Fin.Dir = rossshark.Forward default: Fin.Deactivate() } return true } return false }, appkeys.Options{ ApplyBefore: true, }, ), func(evm *tcell.EventMouse, app gowid.IApp) bool { if Fin.Active() { Fin.Deactivate() return true } return false }, appkeys.Options{ ApplyBefore: true, }, ) appView = holder.New(steerableFin) } else { appView = holder.New(mbView) } var lastMenu gowid.IWidget = appView menus := []gowid.IMenuCompatible{ savedMenu, analysisMenu, generalMenu, conversationMenu, filterConvsMenu1, filterConvsMenu2, } menus = append(menus, FilterWidget.Menus()...) for _, w := range menus { w.SetSubWidget(lastMenu, app) lastMenu = w } keyMapper = mapkeys.New(lastMenu) keyMappings := termshark.LoadKeyMappings() for _, km := range keyMappings { log.Infof("Applying keymapping %v --> %v", km.From, km.To) keyMapper.AddMapping(km.From, km.To, app) } if err = termshark.LoadGlobalMarks(globalMarksMap); err != nil { // Not fatal log.Error(err) } // Create app, etc, but don't init screen which sets ICANON, etc app, err = gowid.NewApp(gowid.AppArgs{ View: keyMapper, Palette: palette, DontActivate: true, Log: log.StandardLogger(), }) if err != nil { return nil, err } gowid.SetFocusPath(mainview, mainviewPaths[0], app) gowid.SetFocusPath(altview1, altview1Paths[0], app) gowid.SetFocusPath(altview2, altview2Paths[0], app) if offs, err := termshark.LoadOffsetFromConfig("mainview"); err == nil { mainviewRows.SetOffsets(offs, app) } if offs, err := termshark.LoadOffsetFromConfig("altviewleft"); err == nil { altview1Pile.SetOffsets(offs, app) } if offs, err := termshark.LoadOffsetFromConfig("altviewright"); err == nil { altview1Cols.SetOffsets(offs, app) } if offs, err := termshark.LoadOffsetFromConfig("altview2horizontal"); err == nil { altview2Pile.SetOffsets(offs, app) } if offs, err := termshark.LoadOffsetFromConfig("altview2vertical"); err == nil { altview2Cols.SetOffsets(offs, app) } return app, err } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.2.0/utils.go000066400000000000000000000667531377442047300152360ustar00rootroot00000000000000// Copyright 2019-2021 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/json" "encoding/xml" "fmt" "io" "io/ioutil" "net" "os" "os/exec" "path" "path/filepath" "regexp" "runtime" "strconv" "strings" "sync" "syscall" "text/template" "time" "unicode" "github.com/adam-hanna/arrayOperations" "github.com/blang/semver" "github.com/gcla/gowid" "github.com/gcla/gowid/gwutil" "github.com/gcla/gowid/vim" "github.com/gcla/termshark/v2/system" "github.com/gcla/termshark/v2/widgets/resizable" "github.com/gdamore/tcell" "github.com/mattn/go-isatty" "github.com/pkg/errors" "github.com/shibukawa/configdir" log "github.com/sirupsen/logrus" "github.com/spf13/viper" "github.com/tevino/abool" ) //====================================================================== type BadStateError struct{} var _ error = BadStateError{} func (e BadStateError) Error() string { return "Bad state" } var BadState = BadStateError{} //====================================================================== type BadCommandError struct{} var _ error = BadCommandError{} func (e BadCommandError) Error() string { return "Error running command" } var BadCommand = BadCommandError{} //====================================================================== type ConfigError struct{} var _ error = ConfigError{} func (e ConfigError) Error() string { return "Configuration error" } var ConfigErr = ConfigError{} //====================================================================== type InternalError struct{} var _ error = InternalError{} func (e InternalError) Error() string { return "Internal error" } var InternalErr = InternalError{} //====================================================================== var ( UserGuideURL string = "https://termshark.io/userguide" FAQURL string = "https://termshark.io/faq" BugURL string = "https://github.com/gcla/termshark/issues/new?assignees=&labels=&template=bug_report.md&title=" FeatureURL string = "https://github.com/gcla/termshark/issues/new?assignees=&labels=&template=feature_request.md&title=" ) //====================================================================== 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) } //====================================================================== // The config is accessed by the main goroutine and pcap loading goroutines. So this // is an attempt to prevent warnings with the -race flag (though they are very likely // harmless) var confMutex sync.Mutex func ConfKeyExists(name string) bool { return viper.Get(name) != nil } func ConfString(name string, def string) string { confMutex.Lock() defer confMutex.Unlock() if viper.Get(name) != nil { return viper.GetString(name) } else { return def } } func SetConf(name string, val interface{}) { confMutex.Lock() defer confMutex.Unlock() viper.Set(name, val) viper.WriteConfig() } func ConfStrings(name string) []string { confMutex.Lock() defer confMutex.Unlock() return viper.GetStringSlice(name) } func DeleteConf(name string) { confMutex.Lock() defer confMutex.Unlock() viper.Set(name, "") viper.WriteConfig() } func ConfInt(name string, def int) int { confMutex.Lock() defer confMutex.Unlock() if viper.Get(name) != nil { return viper.GetInt(name) } else { return def } } func ConfBool(name string, def ...bool) bool { confMutex.Lock() defer confMutex.Unlock() if viper.Get(name) != nil { return viper.GetBool(name) } else { if len(def) > 0 { return def[0] } else { return false } } } func ConfStringSlice(name string, def []string) []string { confMutex.Lock() defer confMutex.Unlock() res := viper.GetStringSlice(name) if res == nil { res = def } return res } //====================================================================== 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)) } // Depends on empty.pcap being present func TSharkSupportsColor(tshark string) (bool, error) { exitCode, err := RunForExitCode( tshark, []string{"-r", CacheFile("empty.pcap"), "-T", "psml", "--color"}, nil, ) return exitCode == 0, err } // TSharkPath will return the full path of the tshark binary, if it's found in the path, otherwise an error func TSharkPath() (string, *gowid.KeyValueError) { tsharkBin := ConfString("main.tshark", "") if tsharkBin != "" { confirmedTshark := false if _, err := os.Stat(tsharkBin); err == nil { confirmedTshark = true } else if IsCommandInPath(tsharkBin) { confirmedTshark = true } // This message is for a configured tshark binary that is invalid if !confirmedTshark { err := gowid.WithKVs(ConfigErr, map[string]interface{}{ "msg": fmt.Sprintf("Could not run tshark binary '%s'. The tshark binary is required to run termshark.\n", tsharkBin) + fmt.Sprintf("Check your config file %s\n", ConfFile("toml")), }) return "", &err } } else { tsharkBin = "tshark" if !IsCommandInPath(tsharkBin) { // This message is for an unconfigured tshark bin (via PATH) that is invalid errstr := fmt.Sprintf("Could not find tshark in your PATH. The tshark binary is required to run termshark.\n") if strings.Contains(os.Getenv("PREFIX"), "com.termux") { errstr += fmt.Sprintf("Try installing with: pkg install root-repo && pkg install tshark") } else if IsCommandInPath("apt") { errstr += fmt.Sprintf("Try installing with: apt install tshark") } else if IsCommandInPath("apt-get") { errstr += fmt.Sprintf("Try installing with: apt-get install tshark") } else if IsCommandInPath("yum") { errstr += fmt.Sprintf("Try installing with: yum install wireshark") } else if IsCommandInPath("brew") { errstr += fmt.Sprintf("Try installing with: brew install wireshark") } errstr += "\n" err := gowid.WithKVs(ConfigErr, map[string]interface{}{ "msg": errstr, }) return "", &err } } // Here we know it's in PATH tsharkBin = DirOfPathCommandUnsafe(tsharkBin) return tsharkBin, nil } func RunForExitCode(prog string, args []string, env []string) (int, error) { var err error exitCode := -1 // default bad cmd := exec.Command(prog, args...) if env != nil { cmd.Env = env } cmd.Stdout = ioutil.Discard cmd.Stderr = ioutil.Discard 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 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 } // A separate dir from CacheDir because I need to use inotify under some // 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 { return path.Join(CacheDir(), "pcaps") } func TSharkBin() string { return ConfString("main.tshark", "tshark") } func DumpcapBin() string { return ConfString("main.dumpcap", "dumpcap") } func CapinfosBin() string { return ConfString("main.capinfos", "capinfos") } // CaptureBin is the binary the user intends to use to capture // packets i.e. with the -i switch. This might be distinct from // DumpcapBin because dumpcap can't capture on extcap interfaces // like randpkt, but while tshark can, it can drop packets more // readily than dumpcap. This value is interpreted as the name // of a binary, resolved against PATH. Note that the default is // termshark - this invokes termshark in a special mode where it // first tries DumpcapBin, then if that fails, TSharkBin - for // the best of both worlds. To detect this, termshark will run // CaptureBin with TERMSHARK_CAPTURE_MODE=1 in the environment, // so when termshark itself is invoked with this in the environment, // it switches to capture mode. func CaptureBin() string { if runtime.GOOS == "windows" { return ConfString("main.capture-command", DumpcapBin()) } else { return ConfString("main.capture-command", os.Args[0]) } } // PrivilegedBin returns a capture binary that may require setcap // privileges on Linux. This is a simple UI to cover the fact that // termshark's default capture method is to run dumpcap and tshark // as a fallback. I don't want to tell the user the capture binary // is termshark - that'd be confusing. We know that on Linux, termshark // will run dumpcap first, then fall back to tshark if needed. Only // dumpcap should need access to live interfaces; tshark is needed // for extcap interfaces only. This is used to provide advice to // the user if packet capture fails. func PrivilegedBin() string { cap := CaptureBin() if cap == "termshark" { return DumpcapBin() } else { return cap } } func TailCommand() []string { def := []string{"tail", "-f", "-c", "+0"} if runtime.GOOS == "windows" { def = []string{os.Args[0], "--tail"} } return ConfStringSlice("main.tail-command", def) } func KeyPressIsPrintable(key gowid.IKey) bool { return unicode.IsPrint(key.Rune()) && key.Modifiers() & ^tcell.ModShift == 0 } type KeyMapping struct { From vim.KeyPress To vim.KeySequence } func AddKeyMapping(km KeyMapping) { mappings := LoadKeyMappings() newMappings := make([]KeyMapping, 0) for _, mapping := range mappings { if mapping.From != km.From { newMappings = append(newMappings, mapping) } } newMappings = append(newMappings, km) SaveKeyMappings(newMappings) } func RemoveKeyMapping(kp vim.KeyPress) { mappings := LoadKeyMappings() newMappings := make([]KeyMapping, 0) for _, mapping := range mappings { if mapping.From != kp { newMappings = append(newMappings, mapping) } } SaveKeyMappings(newMappings) } func LoadKeyMappings() []KeyMapping { mappings := ConfStringSlice("main.key-mappings", []string{}) res := make([]KeyMapping, 0) for _, mapping := range mappings { pair := strings.Split(mapping, " ") if len(pair) != 2 { log.Warnf("Could not parse vim key mapping (missing separator?): %s", mapping) continue } from := vim.VimStringToKeys(pair[0]) if len(from) != 1 { log.Warnf("Could not parse 'source' vim keypress: %s", pair[0]) continue } to := vim.VimStringToKeys(pair[1]) if len(to) < 1 { log.Warnf("Could not parse 'target' vim keypresses: %s", pair[1]) continue } res = append(res, KeyMapping{From: from[0], To: to}) } return res } func SaveKeyMappings(mappings []KeyMapping) { ser := make([]string, 0, len(mappings)) for _, mapping := range mappings { ser = append(ser, fmt.Sprintf("%v %v", mapping.From, vim.KeySequence(mapping.To))) } SetConf("main.key-mappings", ser) } 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() gr, err := gzip.NewReader(file) if err != nil { return err } defer gr.Close() 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() if errProcessAlreadyFinished(err) { return nil } else { return err } } func errProcessAlreadyFinished(err error) bool { if err == nil { return false } // Terrible hack - but the error isn't published return err.Error() == "os: process already finished" } func SafePid(p IProcess) int { if p == nil { return -1 } return p.Pid() } func SetConvTypes(convs []string) { SetConf("main.conv-types", convs) } func ConvTypes() []string { defs := []string{"eth", "ip", "ipv6", "tcp", "udp"} ctypes := ConfStrings("main.conv-types") if len(ctypes) > 0 { z, ok := arrayOperations.Intersect(defs, ctypes) if ok { res, ok := z.Interface().([]string) if ok { return res } } } return defs } func AddToRecentFiles(pcap string) { comps := ConfStrings("main.recent-files") if len(comps) == 0 || comps[0] != pcap { comps = RemoveFromStringSlice(pcap, comps) if len(comps) > 16 { comps = comps[0 : 16-1] } SetConf("main.recent-files", comps) } } func AddToRecentFilters(val string) { comps := ConfStrings("main.recent-filters") if (len(comps) == 0 || comps[0] != val) && strings.TrimSpace(val) != "" { comps = RemoveFromStringSlice(val, comps) if len(comps) > 64 { comps = comps[0 : 64-1] } SetConf("main.recent-filters", comps) } } func LoadOffsetFromConfig(name string) ([]resizable.Offset, error) { offsStr := ConfString("main."+name, "") if offsStr == "" { return nil, errors.WithStack(gowid.WithKVs(ConfigErr, map[string]interface{}{ "name": name, "msg": "No offsets found", })) } res := make([]resizable.Offset, 0) err := json.Unmarshal([]byte(offsStr), &res) if err != nil { return nil, errors.WithStack(gowid.WithKVs(ConfigErr, map[string]interface{}{ "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 { DeleteConf("main." + name) } else { offs, err := json.Marshal(offsets) if err != nil { log.Fatal(err) } SetConf("main."+name, string(offs)) } // Hack to make viper save if I only deleted from the map SetConf("main.lastupdate", time.Now().String()) } //====================================================================== // Need to publish fields for template use type JumpPos struct { Summary string `json:"summary"` Pos int `json:"position"` } type GlobalJumpPos struct { JumpPos Filename string `json:"filename"` } // For ease of use in the template func (g GlobalJumpPos) Base() string { return filepath.Base(g.Filename) } type globalJumpPosMapping struct { Key rune `json:"key"` GlobalJumpPos // embedding without a field name makes the json more concise } func LoadGlobalMarks(m map[rune]GlobalJumpPos) error { marksStr := ConfString("main.marks", "") if marksStr == "" { return nil } mappings := make([]globalJumpPosMapping, 0) err := json.Unmarshal([]byte(marksStr), &mappings) if err != nil { return errors.WithStack(gowid.WithKVs(ConfigErr, map[string]interface{}{ "name": "marks", "msg": "Could not unmarshal marks", })) } for _, mapping := range mappings { m[mapping.Key] = mapping.GlobalJumpPos } return nil } func SaveGlobalMarks(m map[rune]GlobalJumpPos) { marks := make([]globalJumpPosMapping, 0) for k, v := range m { marks = append(marks, globalJumpPosMapping{Key: k, GlobalJumpPos: v}) } if len(marks) == 0 { DeleteConf("main.marks") } else { marksJ, err := json.Marshal(marks) if err != nil { log.Fatal(err) } SetConf("main.marks", string(marksJ)) } // Hack to make viper save if I only deleted from the map SetConf("main.lastupdate", time.Now().String()) } //====================================================================== var cpuProfileRunning *abool.AtomicBool func init() { cpuProfileRunning = abool.New() } // Down to the second for profiling, etc func DateStringForFilename() string { return time.Now().Format("2006-01-02--15-04-05") } func ProfileCPUFor(secs int) bool { if !cpuProfileRunning.SetToIf(false, true) { log.Infof("CPU profile already running.") return false } file := filepath.Join(CacheDir(), fmt.Sprintf("cpu-%s.prof", DateStringForFilename())) log.Infof("Starting CPU profile for %d seconds in %s", secs, file) gwutil.StartProfilingCPU(file) go func() { time.Sleep(time.Duration(secs) * time.Second) log.Infof("Stopping CPU profile") gwutil.StopProfilingCPU() cpuProfileRunning.UnSet() }() return true } func ProfileHeap() { file := filepath.Join(CacheDir(), fmt.Sprintf("mem-%s.prof", DateStringForFilename())) log.Infof("Creating memory profile in %s", file) gwutil.ProfileHeap(file) } func LocalIPs() []string { res := make([]string, 0) addrs, err := net.InterfaceAddrs() if err != nil { return res } for _, addr := range addrs { if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { res = append(res, ipnet.IP.String()) } } return res } //====================================================================== // 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() (map[int][]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) (map[int][]string, error) { re := regexp.MustCompile(`^(?P[0-9]+)\.\s+(?P[^\s]+)(\s*\((?P[^)]+)\))?`) res := make(map[int][]string) scanner := bufio.NewScanner(reader) for scanner.Scan() { line := scanner.Text() match := re.FindStringSubmatch(line) if len(match) < 2 { return nil, gowid.WithKVs(UnexpectedOutput, map[string]interface{}{"Output": line}) } result := make(map[string]string) for i, name := range re.SubexpNames() { if i != 0 && match[i] != "" { result[name] = match[i] } } i, err := strconv.ParseInt(result["index"], 10, 32) if err != nil { return nil, gowid.WithKVs(UnexpectedOutput, map[string]interface{}{"Output": line}) } val := make([]string, 0) val = append(val, result["name1"]) if name2, ok := result["name2"]; ok { val = append([]string{name2}, val...) } res[int(i)] = val } return res, nil } //====================================================================== func IsTerminal(fd uintptr) bool { return isatty.IsTerminal(fd) || isatty.IsCygwinTerminal(fd) } //====================================================================== 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) } //====================================================================== type iWrappedError interface { Cause() error } func RootCause(err error) error { res := err for { if cerr, ok := res.(iWrappedError); ok { res = cerr.Cause() } else { break } } return res } //====================================================================== func RunningRemotely() bool { return os.Getenv("SSH_TTY") != "" } // ApplyArguments turns ["echo", "hello", "$2"] + ["big", "world"] into // ["echo", "hello", "world"] func ApplyArguments(cmd []string, args []string) ([]string, int) { total := 0 re := regexp.MustCompile("^\\$([1-9][0-9]{0,4})$") res := make([]string, len(cmd)) for i, c := range cmd { changed := false matches := re.FindStringSubmatch(c) if len(matches) > 1 { unum, _ := strconv.ParseUint(matches[1], 10, 32) num := int(unum) num -= 1 // 1 indexed if num < len(args) { res[i] = args[num] changed = true total += 1 } } if !changed { res[i] = c } } return res, total } func BrowseUrl(url string) error { urlCmd := ConfStringSlice( "main.browse-command", system.OpenURL, ) if len(urlCmd) == 0 { return errors.WithStack(gowid.WithKVs(BadCommand, map[string]interface{}{"message": "browse command is nil"})) } urlCmdPP, changed := ApplyArguments(urlCmd, []string{url}) if changed == 0 { urlCmdPP = append(urlCmd, url) } cmd := exec.Command(urlCmdPP[0], urlCmdPP[1:]...) return cmd.Run() } //====================================================================== type KeyState struct { NumberPrefix int PartialgCmd bool PartialZCmd bool PartialCtrlWCmd bool PartialmCmd bool PartialQuoteCmd bool } //====================================================================== type ICommandOutput interface { ProcessOutput(output string) error } type ICommandError interface { ProcessCommandError(err error) error } type ICommandDone interface { ProcessCommandDone() } type ICommandKillError interface { ProcessKillError(err error) error } type ICommandTimeout interface { ProcessCommandTimeout() error } type ICommandWaitTicker interface { ProcessWaitTick() error } func CopyCommand(input io.Reader, cb interface{}) error { var err error copyCmd := ConfStringSlice( "main.copy-command", system.CopyToClipboard, ) if len(copyCmd) == 0 { return errors.WithStack(gowid.WithKVs(BadCommand, map[string]interface{}{"message": "copy command is nil"})) } cmd := exec.Command(copyCmd[0], copyCmd[1:]...) cmd.Stdin = input outBuf := bytes.Buffer{} cmd.Stdout = &outBuf cmdTimeout := ConfInt("main.copy-command-timeout", 5) if err := cmd.Start(); err != nil { return errors.WithStack(gowid.WithKVs(BadCommand, map[string]interface{}{"err": err})) } TrackedGo(func() { defer func() { if po, ok := cb.(ICommandDone); ok { po.ProcessCommandDone() } }() 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: if po, ok := cb.(ICommandWaitTicker); ok { err = po.ProcessWaitTick() if err != nil { break Loop } } case <-tchan: if err := cmd.Process.Kill(); err != nil { if po, ok := cb.(ICommandKillError); ok { err = po.ProcessKillError(err) if err != nil { break Loop } } } else { if po, ok := cb.(ICommandTimeout); ok { err = po.ProcessCommandTimeout() if err != nil { break Loop } } } break Loop case err := <-done: if err != nil { if po, ok := cb.(ICommandError); ok { po.ProcessCommandError(err) } } else { if po, ok := cb.(ICommandOutput); ok { outStr := outBuf.String() po.ProcessOutput(outStr) } } break Loop } } }) return nil } // Returns true if error, too func FileSizeDifferentTo(filename string, cur int64) (int64, bool) { var newSize int64 diff := true fi, err := os.Stat(filename) if err == nil { newSize = fi.Size() if cur == newSize { diff = false } } return newSize, diff } //====================================================================== // Local Variables: // mode: Go // fill-column: 78 // End: termshark-2.2.0/utils_test.go000066400000000000000000000106011377442047300162520ustar00rootroot00000000000000// Copyright 2019-2021 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/gcla/termshark/v2/format" "github.com/stretchr/testify/assert" ) //====================================================================== func TestApplyArgs(t *testing.T) { cmd := []string{"echo", "something", "$3", "else", "$1", "$3"} args := []string{"a1", "a2"} eres := []string{"echo", "something", "$3", "else", "a1", "$3"} res, total := ApplyArguments(cmd, args) assert.Equal(t, eres, res) assert.Equal(t, total, 1) args = []string{"a1", "a2", "a3"} eres = []string{"echo", "something", "a3", "else", "a1", "a3"} res, total = ApplyArguments(cmd, args) assert.Equal(t, eres, res) assert.Equal(t, total, 3) } 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_{84E7CAE6-E96F-4F31-96FD-170B0F514AB2} (Npcap Loopback Adapter) 4. \Device\NPF_NdisWanIpv6 (NdisWan Adapter) 5. \Device\NPF_{503E1F71-C57C-438D-B004-EA5563723C16} (Local Area Connection 5) 6. \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, 6, len(interfaces)) v := interfaces[2] assert.Equal(t, `\Device\NPF_{78032B7E-4968-42D3-9F37-287EA86C0AAA}`, v[1]) assert.Equal(t, `Local Area Connection* 10`, v[0]) } func TestInterfaces2(t *testing.T) { out1 := ` 1. eth0 2. ham0 3. docker0 4. vethd45103d 5. lo (Loopback) 6. mpqemubr0-dummy 7. nflog 8. nfqueue 9. bluetooth0 10. virbr0-nic 11. vboxnet0 12. ciscodump (Cisco remote capture) 13. dpauxmon (DisplayPort AUX channel monitor capture) 14. randpkt (Random packet generator) 15. sdjournal (systemd Journal Export) 16. sshdump (SSH remote capture) 17. udpdump (UDP Listener remote capture) `[1:] interfaces, err := interfacesFrom(bytes.NewReader([]byte(out1))) assert.NoError(t, err) assert.Equal(t, 17, len(interfaces)) v := interfaces[3] assert.Equal(t, `docker0`, v[0]) v = interfaces[12] assert.Equal(t, `Cisco remote capture`, v[0]) assert.Equal(t, `ciscodump`, v[1]) } func TestConv1(t *testing.T) { var tests = []struct { arg string res string }{ {"hello\x41world\x42", "helloAworldB"}, {"80 \xe2\x86\x92 53347", "80 → 53347"}, {"hello\x41world\x42 foo \\000 bar", "helloAworldB foo \\000 bar"}, } for _, test := range tests { outs := format.TranslateHexCodes([]byte(test.arg)) assert.Equal(t, string(outs), test.res) } } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.2.0/version.go000066400000000000000000000005631377442047300155460ustar00rootroot00000000000000// Copyright 2019-2021 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 = "v2.2.0" //====================================================================== // Local Variables: // indent-tabs-mode: nil // tab-width: 4 // fill-column: 78 // End: termshark-2.2.0/widgets/000077500000000000000000000000001377442047300151745ustar00rootroot00000000000000termshark-2.2.0/widgets/appkeys/000077500000000000000000000000001377442047300166505ustar00rootroot00000000000000termshark-2.2.0/widgets/appkeys/appkeys.go000066400000000000000000000101301377442047300206460ustar00rootroot00000000000000// Copyright 2019-2021 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 = w.SubWidget().UserInput(ev, size, focus, app) } } else { res = w.SubWidget().UserInput(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 w.SubWidget().Render(size, focus, app) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.2.0/widgets/copymodetable/000077500000000000000000000000001377442047300200235ustar00rootroot00000000000000termshark-2.2.0/widgets/copymodetable/copymodetable.go000066400000000000000000000111021377442047300231740ustar00rootroot00000000000000// Copyright 2019-2021 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 copymodetable provides a wrapper around a table that supports copy mode. // The implementation currently supports clipping a whole row and also the whole // table by providing these as interfaces to the New function. It's easy to imagine // supporting narrowing the copy selection to a single column, but I don't need // that yet... package copymodetable import ( "github.com/gcla/gowid" "github.com/gcla/gowid/widgets/list" "github.com/gcla/gowid/widgets/table" "github.com/gcla/termshark/v2/widgets/withscrollbar" lru "github.com/hashicorp/golang-lru" ) //====================================================================== type IRowCopier interface { CopyRow(id table.RowId) []gowid.ICopyResult } type ITableCopier interface { CopyTable() []gowid.ICopyResult } type ICopyModeTableNeeds interface { gowid.IWidget list.IWalker table.IGoToMiddle withscrollbar.IScrollOneLine withscrollbar.IScrollOnePage CurrentRow() int SetCurrentRow(table.Position) Model() table.IModel SetModel(table.IModel, gowid.IApp) Cache() *lru.Cache OnFocusChanged(gowid.IWidgetChangedCallback) } type Widget struct { ICopyModeTableNeeds RowClip IRowCopier // Knows how to make a clip result set given a row AllClip ITableCopier // Knows how to make a clip result set from the whole table name string // for widget "id" clip gowid.IClipboardSelected // function to modify selected widget for copying } type idstring string // Needed to satisfy copy mode func (i idstring) ID() interface{} { return i } func New(wrapped ICopyModeTableNeeds, rowClip IRowCopier, allClip ITableCopier, name string, clip gowid.IClipboardSelected) *Widget { return &Widget{ ICopyModeTableNeeds: wrapped, RowClip: rowClip, AllClip: allClip, name: name, 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 { row := w.CurrentRow() if app.CopyModeClaimedAt() == 0 { row = -1 // all rows } origModel := w.Model() model := copyModeTableModel{ IModel: origModel, clip: w.clip, app: app, row: row, } w.SetModel(model, app) res := w.ICopyModeTableNeeds.Render(size, focus, app) w.SetModel(origModel, app) return res } else { return w.ICopyModeTableNeeds.Render(size, focus, app) } } // The app stores which widget claims copy mode, and so each widget must check whether it's the // one when it render itself. func (w *Widget) ID() interface{} { return idstring(w.name) } func (w *Widget) SubWidget() gowid.IWidget { return w.ICopyModeTableNeeds } func (w *Widget) CopyModeLevels() int { return 1 // one row, all rows } 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 { // 1 is whole table // 0 is just row diff := w.CopyModeLevels() - (app.CopyModeClaimedAt() - app.CopyLevel()) var rd []gowid.ICopyResult if diff == 0 { cur := w.CurrentRow() rid, ok := w.Model().RowIdentifier(cur) if ok { rd = w.RowClip.CopyRow(rid) } } else { rd = w.AllClip.CopyTable() } return rd } //====================================================================== // copyModeTableModel exists solely to provide an "overridden" implementation of CellWidgets e.g. to color the // selected row yellow. To do this, it needs clip for the AlterWidget function, and the row to alter (or // all). This model is set on the underlying table before Render() is called on the underlying table. type copyModeTableModel struct { table.IModel clip gowid.IClipboardSelected app gowid.IApp row int } var _ table.IModel = copyModeTableModel{} func (c copyModeTableModel) CellWidgets(row table.RowId) []gowid.IWidget { res := c.IModel.CellWidgets(row) dothisrow := false if c.row == -1 { dothisrow = true // do every row i.e. every call to CellWidgets() } else { rid, ok := c.IModel.RowIdentifier(c.row) if ok && (row == rid) { dothisrow = true } } if dothisrow { for col := 0; col < len(res); col++ { res[col] = c.clip.AlterWidget(res[col], c.app) } } return res } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.2.0/widgets/copymodetree/000077500000000000000000000000001377442047300176735ustar00rootroot00000000000000termshark-2.2.0/widgets/copymodetree/copymodetree.go000066400000000000000000000075011377442047300227240ustar00rootroot00000000000000// Copyright 2019-2021 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/v2" "github.com/gcla/termshark/v2/pdmltree" ) //====================================================================== type Widget struct { *list.Widget clip gowid.IClipboardSelected } type ITreeAndListWalker interface { list.IWalker Decorator() tree.IDecorator Maker() tree.IWidgetMaker Tree() tree.IModel } // Note that tree.New() returns a *list.Widget - that's how it's implemented. So this // uses a list widget too. 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 := w.Widget.Render(size, focus, app) w.SetWalker(walk, app) return res } else { return w.Widget.Render(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: termshark-2.2.0/widgets/enableselected/000077500000000000000000000000001377442047300201335ustar00rootroot00000000000000termshark-2.2.0/widgets/enableselected/enableselected.go000066400000000000000000000032331377442047300234220ustar00rootroot00000000000000// Copyright 2019-2021 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 w.IWidget.Render(size, focus, app) } func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { focus.Selected = true return w.IWidget.UserInput(ev, size, focus, app) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.2.0/widgets/expander/000077500000000000000000000000001377442047300170025ustar00rootroot00000000000000termshark-2.2.0/widgets/expander/expander.go000066400000000000000000000035441377442047300211450ustar00rootroot00000000000000// Copyright 2019-2021 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 w.orig.Render(size, focus, app) } else { return w.w.Render(size, focus, app) } } func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { if focus.Selected { return w.orig.UserInput(ev, size, focus, app) } else { return w.w.UserInput(ev, size, focus, app) } } func (w *Widget) Selectable() bool { return w.w.Selectable() } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.2.0/widgets/filter/000077500000000000000000000000001377442047300164615ustar00rootroot00000000000000termshark-2.2.0/widgets/filter/filter.go000066400000000000000000000456511377442047300203100ustar00rootroot00000000000000// Copyright 2019-2021 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" "unicode" "github.com/gcla/gowid" "github.com/gcla/gowid/gwutil" "github.com/gcla/gowid/vim" "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/v2" "github.com/gcla/termshark/v2/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 edCtxLock sync.Mutex fields termshark.IPrefixCompleter // provides completions, given a prefix completionsList *list.Widget // the filter widget replaces the list walker when new completions are generated completionsActivator *activatorWidget // used to disable focus going to drop down completions []string // the current set of completions, used when rendering runthisfilterchan chan *filtStruct filterchangedchan chan *filtStruct quitchan chan struct{} readytorunchan chan struct{} temporarilyDisabled *bool // set to true right after submitting a new filter, so the menu disappears enterPending bool // set to true if the user has hit enter; process if the filter goes to valid before another change. For slow validity processing. *gowid.Callbacks gowid.IsSelectable } var _ gowid.IWidget = (*Widget)(nil) var _ io.Closer = (*Widget)(nil) type IntermediateCB struct{} type ValidCB struct{} type InvalidCB struct{} type SubmitCB struct{} type Options struct { Completer termshark.IPrefixCompleter MaxCompletions int } func New(opt Options) *Widget { res := &Widget{} ed := edit.New() ed.OnTextSet(gowid.WidgetCallback{"cb", func(app gowid.IApp, w gowid.IWidget) { // every time the filter changes, drop any pending enter - we don't want to // apply a filter to a stale value res.enterPending = false }}) fixed := gowid.RenderFixed{} filterList := list.New(list.NewSimpleListWalker([]gowid.IWidget{})) filterActivator := &activatorWidget{ IWidget: filterList, } if opt.MaxCompletions == 0 { opt.MaxCompletions = 20 } menuListBox2 := styled.New( framed.NewUnicode(cellmod.Opaque(filterActivator)), gowid.MakePaletteRef("filter-menu"), ) ign := make([]gowid.IKey, 0, len(vim.AllDownKeys)+len(vim.AllUpKeys)) for _, k := range vim.AllDownKeys { if !termshark.KeyPressIsPrintable(gowid.Key(k)) { ign = append(ign, gowid.Key(k)) } } for _, k := range vim.AllUpKeys { if !termshark.KeyPressIsPrintable(gowid.Key(k)) { ign = append(ign, gowid.Key(k)) } } drop := menu.New("filter", menuListBox2, gowid.RenderWithUnits{U: opt.MaxCompletions + 2}, menu.Options{ IgnoreKeysProvided: true, IgnoreKeys: ign, CloseKeysProvided: true, CloseKeys: []gowid.IKey{}, }, ) site := menu.NewSite(menu.SiteOptions{ YOffset: 1, }) cb := gowid.NewCallbacks() onelineEd := appkeys.New(ed, handleEnter(cb, res), 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: filterList, completionsActivator: filterActivator, completions: []string{}, filterchangedchan: filterchangedchan, runthisfilterchan: runthisfilterchan, quitchan: quitchan, readytorunchan: readytorunchan, temporarilyDisabled: new(bool), Callbacks: cb, } 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) if res.enterPending { var dummy gowid.IWidget gowid.RunWidgetCallbacks(cb, SubmitCB{}, app, dummy) *res.temporarilyDisabled = true res.enterPending = false } })) }, } 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) res.enterPending = false })) }, } 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) res.enterPending = false })) }, } 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. select { case res.readytorunchan <- struct{}{}: case <-res.quitchan: break CL } 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.OnCursorPosSet(gowid.MakeWidgetCallback("cb", gowid.WidgetChangedFunction(func(app gowid.IApp, ew gowid.IWidget) { res.UpdateCompletions(app) }))) ed.OnTextSet(gowid.MakeWidgetCallback("cb", gowid.WidgetChangedFunction(func(app gowid.IApp, ew gowid.IWidget) { res.UpdateCompletions(app) }))) 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) } } } } type iFilterEnter interface { setDisabled() setEnterPending() isValid() bool } // if the filter is valid when enter is pressed, submit the SubmitCB callback. Those // registered will be able to respond e.g. start handling the valid filter value. func handleEnter(cb *gowid.Callbacks, fe iFilterEnter) appkeys.KeyInputFn { return func(evk *tcell.EventKey, app gowid.IApp) bool { handled := false switch evk.Key() { case tcell.KeyEnter: if fe.isValid() { var dummy gowid.IWidget gowid.RunWidgetCallbacks(cb, SubmitCB{}, app, dummy) fe.setDisabled() } else { fe.setEnterPending() // remember in case the filter goes valid shortly } handled = true } return handled } } func isValidFilterRune(r rune) bool { res := true switch { case unicode.IsLetter(r): case unicode.IsNumber(r): case r == '-': case r == '_': case r == '.': default: res = false } return res } 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{}, SelectKeys: []gowid.IKey{gowid.MakeKeyExt(tcell.KeyEnter)}, }, ) clickmeStyled := styled.NewInvertedFocus(clickme, gowid.MakePaletteRef("filter-menu")) clickme.OnClick(gowid.MakeWidgetCallback(gowid.ClickCB{}, func(app gowid.IApp, target gowid.IWidget) { txt := ed.Text() end := ed.CursorPos() start := end Loop1: for { if start == 0 { break } start-- if !isValidFilterRune(rune(txt[start])) { start++ break Loop1 } } Loop2: for { if end == len(txt) { break } if !isValidFilterRune(rune(txt[end])) { break Loop2 } end++ } 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) setDisabled() { *w.temporarilyDisabled = true } func (w *Widget) setEnterPending() { w.enterPending = true } // isCurrentlyValid returns true if the current state of the filter is valid (green) func (w *Widget) isValid() bool { return w.validitySite.SubWidget() == w.valid } // Start an asynchronous routine to update the drop-down menu with completion // options. Runs on a small delay so it can be cancelled and restarted if the // user is typing quickly. func (w *Widget) UpdateCompletions(app gowid.IApp) { app.Run(gowid.RunFunction(func(app gowid.IApp) { if w.ed.Text() != "" { w.validitySite.SetSubWidget(w.intermediate, app) gowid.RunWidgetCallbacks(w.Callbacks, IntermediateCB{}, app, w) } })) // UpdateCompletions can be called outside of the app goroutine, so we // need to protect the context w.edCtxLock.Lock() defer w.edCtxLock.Unlock() if w.edCancelFn != nil { w.edCancelFn() } w.edCtx, w.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 } // Send the value to be run by tshark. This will kill any other one in progress. w.filterchangedchan <- &filtStruct{w.ed.Text(), app} app.Run(gowid.RunFunction(func(app gowid.IApp) { _, y := app.GetScreen().Size() txt := w.ed.Text() end := w.ed.CursorPos() start := end Loop: for { if start == 0 { break } start-- if !isValidFilterRune(rune(txt[start])) { start++ break Loop } } makeCompletions(w.fields, txt[start:end], y, app, func(completions []string, app gowid.IApp) { app.Run(gowid.RunFunction(func(app gowid.IApp) { w.processCompletions(completions, app) })) }) })) }(w.edCtx) } 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) // whenever there's an update, take focus away from drop down. This means enter // can be used to submit a new filter. w.completionsActivator.active = false 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) OnSubmit(f gowid.IWidgetChangedCallback) { gowid.AddWidgetCallback(w, SubmitCB{}, f) } 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) w.ed.SetCursorPos(len(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 { // It can be disabled if e.g. the user's last input caused the filter value to // be submitted. Then the best UX is to not display the drop down until further input // or cursor movement. if focus.Focus && len(w.completions) > 0 && !*w.temporarilyDisabled { 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. Reject down because // accepting it triggers the OnCursorSet callback, which re-evaluates the filter value - the user sees // it go orange briefly, which is unpleasant. func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { if evk, ok := ev.(*tcell.EventKey); ok { if evk.Key() == tcell.KeyTAB || (vim.KeyIn(evk, vim.AllDownKeys) && !termshark.KeyPressIsPrintable(evk)) { return false } } *w.temporarilyDisabled = false // any input should start the appearance of the drop down again return w.wrapped.UserInput(ev, size, focus, app) } //====================================================================== // activatorWidget is intended to wrap a ListBox, and will suppress focus to the listbox by // default, which has the effect of not highlighting any listbox items. The intended effect // is for the cursor to be "above" the first item. When the user hits down, then focus // is passed through, so the top item is highlighted. If the key pressed is up, and the // listbox doesn't handle it, that must mean it's at the top of its range, so the effect is // start suppressing focus again. type activatorWidget struct { gowid.IWidget active bool } func (w *activatorWidget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { if ev, ok := ev.(*tcell.EventKey); ok && !w.active { if vim.KeyIn(ev, vim.AllDownKeys) && !termshark.KeyPressIsPrintable(ev) { w.active = true return true } else { return false } } res := w.IWidget.UserInput(ev, size, focus, app) if !res { if ev, ok := ev.(*tcell.EventKey); ok && w.active { if vim.KeyIn(ev, vim.AllUpKeys) && !termshark.KeyPressIsPrintable(ev) { w.active = false return true } else { return false } } } return res } func (w *activatorWidget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { newf := focus if !w.active { newf = gowid.NotSelected } return w.IWidget.Render(size, newf, app) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.2.0/widgets/framefocus/000077500000000000000000000000001377442047300173265ustar00rootroot00000000000000termshark-2.2.0/widgets/framefocus/framefocus.go000066400000000000000000000035441377442047300220150ustar00rootroot00000000000000// Copyright 2019-2021 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 framefocus provides a very specific widget to apply a frame around the widget in focus // and an empty frame if not. package framefocus import ( "runtime" "github.com/gcla/gowid" "github.com/gcla/gowid/widgets/framed" "github.com/gcla/gowid/widgets/holder" "github.com/gcla/gowid/widgets/isselected" ) //====================================================================== type Widget struct { *isselected.Widget h *holder.Widget } func New(w gowid.IWidget) *Widget { h := holder.New(w) noFocusFrame := framed.SpaceFrame noFocusFrame.T = 0 noFocusFrame.B = 0 return &Widget{ Widget: isselected.New( framed.New(h, framed.Options{ Frame: noFocusFrame, }), framed.NewUnicodeAlt2(h), framed.NewUnicode(h), ), h: h, } } func NewSlim(w gowid.IWidget) *Widget { h := holder.New(w) noFocusFrame := framed.SpaceFrame selectedFrame := framed.UnicodeAlt2Frame focusFrame := framed.UnicodeFrame if runtime.GOOS == "windows" { selectedFrame = framed.UnicodeFrame focusFrame = framed.UnicodeAlt2Frame } noFocusFrame.T = 0 noFocusFrame.B = 0 selectedFrame.T = 0 selectedFrame.B = 0 focusFrame.T = 0 focusFrame.B = 0 return &Widget{ Widget: isselected.New( framed.New(h, framed.Options{ Frame: noFocusFrame, }), framed.New(h, framed.Options{ Frame: selectedFrame, }), framed.New(h, framed.Options{ Frame: focusFrame, }), ), h: h, } } func (w *Widget) SubWidget() gowid.IWidget { return w.h.SubWidget() } func (w *Widget) SetSubWidget(wi gowid.IWidget, app gowid.IApp) { w.h.SetSubWidget(wi, app) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.2.0/widgets/hexdumper/000077500000000000000000000000001377442047300171755ustar00rootroot00000000000000termshark-2.2.0/widgets/hexdumper/hexdumper.go000066400000000000000000000347311377442047300215350ustar00rootroot00000000000000// Copyright 2019-2021 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 ( "encoding/hex" "fmt" "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/v2" "github.com/gcla/termshark/v2/format" "github.com/gcla/termshark/v2/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 Options struct { StyledLayers []LayerStyler CursorUnselected string CursorSelected string LineNumUnselected string LineNumSelected string PaletteIfCopying string } 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, opts ...Options) *Widget { var opt Options if len(opts) > 0 { opt = opts[0] } res := &Widget{ data: data, layers: opt.StyledLayers, cursorUnselected: opt.CursorUnselected, cursorSelected: opt.CursorSelected, lineNumUnselected: opt.LineNumUnselected, lineNumSelected: opt.LineNumSelected, paletteIfCopying: opt.PaletteIfCopying, UsePaletteIfSelectedForCopy: styled.UsePaletteIfSelectedForCopy{Entry: opt.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: opt.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 d.w.Render(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 wa.Render(size, focus, app) } else { return w.w.Render(size, focus, app) } } func clipsForBytes(data []byte, start int, end int) []gowid.ICopyResult { dump := hex.Dump(data[start:end]) dump2 := format.MakeEscapedString(data[start:end]) dump3 := format.MakePrintableString(data[start:end]) dump4 := format.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 = w.w.UserInput(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 = w.w.UserInput(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: termshark-2.2.0/widgets/hexdumper/hexdumper_test.go000066400000000000000000000021241377442047300225630ustar00rootroot00000000000000// Copyright 2019-2021 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")) //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("")) 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: termshark-2.2.0/widgets/hexdumper2/000077500000000000000000000000001377442047300172575ustar00rootroot00000000000000termshark-2.2.0/widgets/hexdumper2/hexdumper2.go000066400000000000000000000435251377442047300217020ustar00rootroot00000000000000// Copyright 2019-2021 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 hexdumper2 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 hexdumper2 import ( "encoding/hex" "fmt" "io" "github.com/gcla/gowid" "github.com/gcla/gowid/gwutil" "github.com/gcla/gowid/vim" "github.com/gcla/gowid/widgets/list" "github.com/gcla/gowid/widgets/styled" "github.com/gcla/termshark/v2/format" "github.com/gdamore/tcell" ) //====================================================================== type LayerStyler struct { Start int End int ColUnselected string ColSelected string } type PositionChangedCB struct{} //====================================================================== type Options struct { StyledLayers []LayerStyler CursorUnselected string CursorSelected string LineNumUnselected string LineNumSelected string PaletteIfCopying string DownKeys []vim.KeyPress UpKeys []vim.KeyPress LeftKeys []vim.KeyPress RightKeys []vim.KeyPress } type Widget struct { data []byte layers []LayerStyler position int cursorUnselected string cursorSelected string lineNumUnselected string lineNumSelected string offset int // scroll position, bit of a hack paletteIfCopying string opt Options 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, opts ...Options) *Widget { var opt Options if len(opts) > 0 { opt = opts[0] } if opt.DownKeys == nil { opt.DownKeys = vim.AllDownKeys } if opt.UpKeys == nil { opt.UpKeys = vim.AllUpKeys } if opt.LeftKeys == nil { opt.LeftKeys = vim.AllLeftKeys } if opt.RightKeys == nil { opt.RightKeys = vim.AllRightKeys } res := &Widget{ data: data, layers: opt.StyledLayers, cursorUnselected: opt.CursorUnselected, cursorSelected: opt.CursorSelected, lineNumUnselected: opt.LineNumUnselected, lineNumSelected: opt.LineNumSelected, paletteIfCopying: opt.PaletteIfCopying, UsePaletteIfSelectedForCopy: styled.UsePaletteIfSelectedForCopy{Entry: opt.PaletteIfCopying}, opt: opt, Callbacks: gowid.NewCallbacks(), } 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 "hexdump2" } 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 } func (w *Widget) Data() []byte { return w.data } func (w *Widget) SetData(data []byte, app gowid.IApp) { w.data = data } func (w *Widget) Position() int { return w.position } func (w *Widget) SetPosition(pos int, app gowid.IApp) { curpos := w.Position() w.position = pos if curpos != pos { gowid.RunWidgetCallbacks(w.Callbacks, PositionChangedCB{}, app, w) } } func (w *Widget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { // 1<-4><3><----------(8 * 3)-1---<2<-------(8 * 3)-1-----><3><---8-->1<---8--> var cols int if sz, ok := size.(gowid.IColumns); ok { cols = sz.Columns() } else { cols = 1 + 4 + 3 + ((8 * 3) - 1) + 2 + ((8 * 3) - 1) + 3 + 8 + 1 + 8 } var rows int if box, ok := size.(gowid.IRows); ok { rows = box.Rows() } else { rows = (len(w.data) + 15) / 16 } return gowid.MakeRenderBox(cols, rows) } // 1<-4><3><----------(8 * 3)-1---<2<-------(8 * 3)-1-----><3><---8-->1<---8--> // 0660 72 6f 72 73 2e 57 69 74 68 53 74 61 63 6b 28 67 rors.Wit hStack(g // 0670 6f 77 69 64 2e 57 69 74 68 4b 56 73 28 4f 70 65 owid.Wit hKVs(Ope // 0680 6e 45 72 72 6f 72 2c 20 6d 61 70 5b 73 74 72 69 nError, map[stri // 0690 6e 67 5d 69 6e 74 65 72 66 61 63 65 7b 7d 7b 0a ng]inter face{}{. // 06a0 09 09 09 09 22 64 65 73 63 72 69 70 74 6f 72 22 ...."des criptor" // 06b0 3a 20 6e 65 77 73 74 64 69 6e 2c 0a 09 09 09 09 : newstd in,..... // func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { var canvasRows int if box, ok := size.(gowid.IRows); ok { canvasRows = box.Rows() } else { canvasRows = (len(w.data) + 15) / 16 } if canvasRows == 0 { return gowid.NewCanvas() } // -1 means not copy mode. 0 means the whole hexdump. 2 means the smallest layer, 1 the next biggest diff := -1 if app.InCopyMode() && app.CopyModeClaimedBy().ID() == w.ID() && focus.Focus { diff = app.CopyModeClaimedAt() - app.CopyLevel() } cols := 1 + 4 + 3 + ((8 * 3) - 1) + 2 + ((8 * 3) - 1) + 3 + 8 + 1 + 8 ccols := cols if sz, ok := size.(gowid.IColumns); ok { ccols = gwutil.Max(ccols, sz.Columns()) } c := gowid.NewCanvasOfSize(ccols, canvasRows) // Adjust in case it's been set too high e.g. via a scrollbar drows := (len(w.data) + 15) / 16 if w.offset >= drows { w.offset = drows } rows := gwutil.Min(canvasRows, drows) if w.Position() < w.offset*16 { w.SetPosition(w.Position()%16+(w.offset*16), app) } if w.Position() >= ((canvasRows + w.offset) * 16) { w.SetPosition(w.Position()%16+((canvasRows+w.offset-1)*16), app) } var lineNumStyle convertedStyle var cursorStyle convertedStyle var copyModeStyle convertedStyle if focus.Focus { lineNumStyle = convertStyle(gowid.MakePaletteRef(w.LineNumSelected()), app) cursorStyle = convertStyle(gowid.MakePaletteRef(w.CursorSelected()), app) } else { lineNumStyle = convertStyle(gowid.MakePaletteRef(w.LineNumUnselected()), app) cursorStyle = convertStyle(gowid.MakePaletteRef(w.CursorUnselected()), app) } copyModeStyle = convertStyle(gowid.MakePaletteRef(w.Entry), app) twoByteWriter := CanvasSlice{ C: c, } asciiWriter := CanvasSlice{ C: c, } var active *convertedStyle // for styling the hex data "41" and the ascii "A" var spactive *convertedStyle // for styling the spaces between data e.g. "41 42" // nil // [1, 5] // [2, 3] // // Deliberately add a blank layer at the beginning for index 0 layers := w.Layers() var layer *LayerStyler layerStyles := make([]convertedLayer, len(layers)) for i := 0; i < len(layers); i++ { layerStyles[i].u = convertStyle(gowid.MakePaletteRef(layers[i].ColUnselected), app) layerStyles[i].s = convertStyle(gowid.MakePaletteRef(layers[i].ColSelected), app) } var i int Loop: for row := w.offset; row < rows+w.offset; row++ { twoByteWriter.XOffset = 1 + 4 + 3 asciiWriter.XOffset = 1 + 4 + 3 + ((8 * 3) - 1) + 2 + ((8 * 3) - 1) + 3 for col := 0; col < 16; col++ { i = (row * 16) + col if i == len(w.data) { break Loop } active = nil spactive = nil if w.Position() == i { if diff != -1 { active = ©ModeStyle } else { active = &cursorStyle } } else { for j := 0; j < len(layers); j++ { layer = &layers[j] if i >= layer.Start && i < layer.End { if j+1 == diff { active = ©ModeStyle break } else { if focus.Focus { active = &layerStyles[j].s } else { active = &layerStyles[j].u } } } } } for j := 0; j < len(layers); j++ { layer = &layers[j] if i >= layer.Start && i < layer.End-1 { if j+1 == diff { spactive = ©ModeStyle break } else { if focus.Focus { spactive = &layerStyles[j].s } else { spactive = &layerStyles[j].u } } } } fmt.Fprintf(twoByteWriter, "%02x", w.data[i]) if active != nil { styleAt(c, twoByteWriter.XOffset, twoByteWriter.YOffset, *active) styleAt(c, twoByteWriter.XOffset+1, twoByteWriter.YOffset, *active) } if spactive != nil { styleAt(c, twoByteWriter.XOffset+2, twoByteWriter.YOffset, *spactive) if col == 7 { styleAt(c, twoByteWriter.XOffset+3, twoByteWriter.YOffset, *spactive) } } twoByteWriter.XOffset += 3 if col == 7 { twoByteWriter.XOffset += 1 } ch := '.' r := w.data[i] if r >= 32 && r <= 126 { ch = rune(byte(r)) } fmt.Fprintf(asciiWriter, "%c", ch) if active != nil { styleAt(c, asciiWriter.XOffset, asciiWriter.YOffset, *active) } if spactive != nil && col == 7 { styleAt(c, asciiWriter.XOffset+1, asciiWriter.YOffset, *spactive) } asciiWriter.XOffset += 1 if col == 7 { asciiWriter.XOffset += 1 } } twoByteWriter.YOffset += 1 asciiWriter.YOffset += 1 } lineNumWriter := CanvasSlice{ C: c, XOffset: 1, YOffset: 0, } Loop2: for k, rk := w.offset, 0; k < rows+w.offset; k, rk = k+1, rk+1 { if k*16 >= len(w.data) { break Loop2 } fmt.Fprintf(lineNumWriter, "%04x", k*16) lineNumWriter.YOffset += 1 active := false for _, layer := range layers { if (k+1)*16 >= layer.Start && k*16 < layer.End { active = true break } } if active { for x := 0; x < 6; x++ { styleAt(c, x, rk, lineNumStyle) } } } if sz, ok := size.(gowid.IColumns); ok { c.TrimRight(sz.Columns()) } if diff == 0 { gowid.RangeOverCanvas(c, gowid.CellRangeFunc(func(cell gowid.Cell) gowid.Cell { return cell.WithBackgroundColor(copyModeStyle.b).WithForegroundColor(copyModeStyle.f).WithStyle(copyModeStyle.s) })) } return c } func clipsForBytes(data []byte, start int, end int) []gowid.ICopyResult { dump := hex.Dump(data[start:end]) dump2 := format.MakeEscapedString(data[start:end]) dump3 := format.MakePrintableString(data[start:end]) dump4 := format.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) CopyModeLevels() int { return len(w.layers) } 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) } } func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { return gowid.CopyModeUserInput(forCopyModeWidget{forUserInputWidget{Widget: w}}, ev, size, focus, app) } type forCopyModeWidget struct { forUserInputWidget } // CopyModeUserInput calls UserInput() on w.SubWidget() - which is this. Then... func (w forCopyModeWidget) SubWidget() gowid.IWidget { return w.forUserInputWidget } type forUserInputWidget struct { *Widget } // ... UserInput is sent to the hexdumper's UserInput logic. func (w forUserInputWidget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { return w.Widget.realUserInput(ev, size, focus, app) } // Reject tab because I want it to switch views. Not intended to be transferable. func (w *Widget) realUserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { res := false scrollDown := false scrollUp := false switch ev := ev.(type) { case *tcell.EventKey: switch { case vim.KeyIn(ev, w.opt.RightKeys): //res = Scroll(w, 1, w.Wrap(), app) pos := w.Position() if pos < len(w.data) { w.SetPosition(pos+1, app) res = true } case vim.KeyIn(ev, w.opt.LeftKeys): pos := w.Position() if pos > 0 { w.SetPosition(pos-1, app) res = true } case vim.KeyIn(ev, w.opt.DownKeys): scrollDown = true case vim.KeyIn(ev, w.opt.UpKeys): scrollUp = true } case *tcell.EventMouse: switch ev.Buttons() { case tcell.WheelDown: scrollDown = true case tcell.WheelUp: scrollUp = true case tcell.Button1: xp := -1 mx, my := ev.Position() // 1<-4><3><----------(8 * 3)-1---<2<-------(8 * 3)-1-----><3><---8-->1<---8--> // 0660 72 6f 72 73 2e 57 69 74 68 53 74 61 63 6b 28 67 rors.Wit hStack(g // 0670 6f 77 69 64 2e 57 69 74 68 4b 56 73 28 4f 70 65 owid.Wit hKVs(Ope switch { case mx >= 1+4+3 && mx < 1+4+3+((8*3)-1): xp = (mx - (1 + 4 + 3)) / 3 case mx >= 1+4+3+((8*3)-1)+2 && mx < 1+4+3+((8*3)-1)+2+((8*3)-1): xp = ((mx - (1 + 4 + 3 + ((8 * 3) - 1) + 2)) / 3) + 8 case mx >= 1+4+3+((8*3)-1)+2+((8*3)-1)+3 && mx < 1+4+3+((8*3)-1)+2+((8*3)-1)+3+8: xp = mx - (1 + 4 + 3 + ((8 * 3) - 1) + 2 + ((8 * 3) - 1) + 3) case mx >= 1+4+3+((8*3)-1)+2+((8*3)-1)+3+8+1 && mx < 1+4+3+((8*3)-1)+2+((8*3)-1)+3+8+1+8: xp = mx - (1 + 4 + 3 + ((8 * 3) - 1) + 2 + ((8 * 3) - 1) + 3 + 8 + 1) + 8 } if xp != -1 { pos := (my * 16) + xp if pos < len(w.data) { w.SetPosition(pos, app) res = true } } } } pos := w.Position() atBottom := false atTop := false var canvasRows int if box, ok := size.(gowid.IRows); ok { canvasRows = box.Rows() } else { canvasRows = (len(w.data) + 15) / 16 } atTop = ((pos / 16) == w.offset) atBottom = ((pos / 16) == (w.offset + canvasRows - 1)) if scrollDown { if pos+16 < len(w.data) { w.SetPosition(pos+16, app) if atBottom { w.offset += 1 } res = true } } else if scrollUp { if pos-16 >= 0 { w.SetPosition(pos-16, app) if atTop { w.offset -= 1 } res = true } } return res } // Implement withscrollbar.IScrollValues func (t *Widget) ScrollLength() int { return (len(t.data) + 15) / 16 } // Implement withscrollbar.IScrollValues func (t *Widget) ScrollPosition() int { return t.Position() / 16 } // Implements withscrollbar.iSetPosition. Note that position is a row. func (t *Widget) SetPos(pos list.IBoundedWalkerPosition, app gowid.IApp) { t.offset = pos.ToInt() } // Can leave the cursor out of sight func (t *Widget) Up(lines int, size gowid.IRenderSize, app gowid.IApp) { t.offset -= lines if t.offset < 0 { t.offset = 0 } } // Adjusts cursor pos too func (t *Widget) GoHome(size gowid.IRenderSize, app gowid.IApp) { t.offset = 0 t.SetPosition(0, app) } func (t *Widget) GoToEnd(size gowid.IRenderSize, app gowid.IApp) { var canvasRows int if box, ok := size.(gowid.IRows); ok { canvasRows = box.Rows() } else { canvasRows = (len(t.data) + 15) / 16 } dataRows := (len(t.data) + 15) / 16 t.offset += gwutil.Max(0, dataRows-(canvasRows+t.offset)) t.SetPosition(len(t.data)-1, app) } // Can leave the cursor out of sight func (t *Widget) Down(lines int, size gowid.IRenderSize, app gowid.IApp) { var canvasRows int if box, ok := size.(gowid.IRows); ok { canvasRows = box.Rows() } else { canvasRows = (len(t.data) + 15) / 16 } dataRows := (len(t.data) + 15) / 16 t.offset += gwutil.Max(0, gwutil.Min(lines, dataRows-(canvasRows+t.offset))) } func (t *Widget) DownPage(num int, size gowid.IRenderSize, app gowid.IApp) { units := 1 if size, ok := size.(gowid.IRows); ok { units = size.Rows() } for i := 0; i < num; i++ { t.Down(units, size, app) } } func (t *Widget) UpPage(num int, size gowid.IRenderSize, app gowid.IApp) { units := 1 if size, ok := size.(gowid.IRows); ok { units = size.Rows() } for i := 0; i < num; i++ { t.Up(units, size, app) } } //====================================================================== // Optimization - convert the styles for use in the canvas once per call // to Render() type convertedStyle struct { f gowid.TCellColor b gowid.TCellColor s gowid.StyleAttrs } type convertedLayer struct { u convertedStyle s convertedStyle } func convertStyle(style gowid.ICellStyler, app gowid.IApp) convertedStyle { f, b, s := style.GetStyle(app) f1 := gowid.IColorToTCell(f, gowid.ColorNone, app.GetColorMode()) b1 := gowid.IColorToTCell(b, gowid.ColorNone, app.GetColorMode()) return convertedStyle{ f: f1, b: b1, s: s, } } //====================================================================== type CanvasSlice struct { C *gowid.Canvas XOffset int YOffset int } var _ io.Writer = CanvasSlice{} var _ gowid.IRangeOverCanvas = CanvasSlice{} func NewCanvasSlice(c *gowid.Canvas, xoff, yoff int) CanvasSlice { res := CanvasSlice{ C: c, XOffset: xoff, YOffset: yoff, } return res } func (c CanvasSlice) Write(p []byte) (n int, err error) { return gowid.WriteToCanvas(c, p) } func (c CanvasSlice) ImplementsWidgetDimension() {} func (c CanvasSlice) BoxColumns() int { return c.C.BoxColumns() } func (c CanvasSlice) BoxRows() int { return c.C.BoxRows() } func (c CanvasSlice) CellAt(col, row int) gowid.Cell { return c.C.CellAt(col+c.XOffset, row+c.YOffset) } func (c CanvasSlice) SetCellAt(col, row int, cell gowid.Cell) { c.C.SetCellAt(col+c.XOffset, row+c.YOffset, cell) } func styleAt(canvas gowid.ICanvas, col int, row int, st convertedStyle) { c := canvas.CellAt(col, row) c = c.MergeDisplayAttrsUnder(c.WithForegroundColor(st.f).WithBackgroundColor(st.b).WithStyle(st.s)) canvas.SetCellAt(col, row, c) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.2.0/widgets/ifwidget/000077500000000000000000000000001377442047300167765ustar00rootroot00000000000000termshark-2.2.0/widgets/ifwidget/ifwidget.go000066400000000000000000000042231377442047300211300ustar00rootroot00000000000000// Copyright 2019-2021 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 w.wtrue.UserInput(ev, size, focus, app) } else { return w.wfalse.UserInput(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 w.wtrue.Render(size, focus, app) } else { return w.wfalse.Render(size, focus, app) } } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.2.0/widgets/keepselected/000077500000000000000000000000001377442047300176315ustar00rootroot00000000000000termshark-2.2.0/widgets/keepselected/keepselected.go000066400000000000000000000027211377442047300226170ustar00rootroot00000000000000// Copyright 2019-2021 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 keepselected turns on the selected bit when Render or UserInput is called. package keepselected import "github.com/gcla/gowid" // A widget to ensure that its subwidget is always rendered as "selected", even if it's // not in focus. This allows a composite widget to style its selected child even without // focus so the user can see which child is active. type Widget struct { sub gowid.IWidget } func New(w gowid.IWidget) *Widget { return &Widget{ sub: w, } } func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { return w.sub.Render(size, focus.SelectIf(true), app) } func (w *Widget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { return w.sub.RenderSize(size, focus.SelectIf(true), app) } func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { return w.sub.UserInput(ev, size, focus.SelectIf(true), app) } func (w *Widget) Selectable() bool { return w.sub.Selectable() } func (w *Widget) SubWidget() gowid.IWidget { return w.sub } func (w *Widget) SetSubWidget(wi gowid.IWidget, app gowid.IApp) { w.sub = wi } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.2.0/widgets/logviewer/000077500000000000000000000000001377442047300171775ustar00rootroot00000000000000termshark-2.2.0/widgets/logviewer/logviewer.go000066400000000000000000000035711377442047300215370ustar00rootroot00000000000000// Copyright 2019-2021 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 !windows // Package logviewer provides a widget to view termshark's log file in a terminal // via a pager program. package logviewer import ( "fmt" "os" "github.com/gcla/gowid" "github.com/gcla/gowid/widgets/hpadding" "github.com/gcla/gowid/widgets/null" "github.com/gcla/gowid/widgets/pile" "github.com/gcla/gowid/widgets/terminal" "github.com/gcla/gowid/widgets/text" "github.com/gcla/termshark/v2" ) //====================================================================== type Widget struct { gowid.IWidget } // New - a bit clumsy, UI will always be legit, but error represents terminal failure func New(cb gowid.IWidgetChangedCallback) (*Widget, error) { logfile := termshark.CacheFile("termshark.log") var args []string pager := termshark.ConfString("main.pager", "") if pager == "" { pager = os.Getenv("PAGER") } if pager == "" { args = []string{"less", "+G", logfile} } else { args = []string{"sh", "-c", fmt.Sprintf("%s %s", pager, logfile)} } var term gowid.IWidget var termC *terminal.Widget var errTerm error termC, errTerm = terminal.New(args) if errTerm != nil { term = null.New() } else { termC.OnProcessExited(cb) term = termC } header := hpadding.New( text.New(fmt.Sprintf("Logs - %s", logfile)), gowid.HAlignMiddle{}, gowid.RenderFixed{}, ) main := pile.New([]gowid.IContainerWidget{ &gowid.ContainerWidget{ IWidget: header, D: gowid.RenderWithUnits{U: 2}, }, &gowid.ContainerWidget{ IWidget: term, D: gowid.RenderWithWeight{W: 1.0}, }, }) res := &Widget{ IWidget: main, } return res, errTerm } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.2.0/widgets/mapkeys/000077500000000000000000000000001377442047300166455ustar00rootroot00000000000000termshark-2.2.0/widgets/mapkeys/mapkeys.go000066400000000000000000000042521377442047300206500ustar00rootroot00000000000000// Copyright 2019-2021 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 mapkeys provides a widget that can map one keypress to a sequence of // keypresses. If the user pnovides as input a key that is mapped, the sequence of // resulting keypresses is played to the subwidget before control returns. If the key is // not mapped, it is passed through as normal. I'm going to use this to provide a vim-like // macro feature in termshark. package mapkeys import ( "github.com/gcla/gowid" "github.com/gcla/gowid/vim" "github.com/gdamore/tcell" ) //====================================================================== type Widget struct { gowid.IWidget kmap map[vim.KeyPress]vim.KeySequence } var _ gowid.IWidget = (*Widget)(nil) func New(w gowid.IWidget) *Widget { res := &Widget{ IWidget: w, kmap: make(map[vim.KeyPress]vim.KeySequence), } return res } func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { switch ev := ev.(type) { case *tcell.EventKey: kp := vim.KeyPressFromTcell(ev) if seq, ok := w.kmap[kp]; ok { var res bool for _, vk := range seq { k := gowid.Key(vk) // What should the handled value be?? res = w.IWidget.UserInput(tcell.NewEventKey(k.Key(), k.Rune(), k.Modifiers()), size, focus, app) } return res } else { return w.IWidget.UserInput(ev, size, focus, app) } default: return w.IWidget.UserInput(ev, size, focus, app) } } func (w *Widget) AddMapping(from vim.KeyPress, to vim.KeySequence, app gowid.IApp) { w.kmap[from] = to } func (w *Widget) RemoveMapping(from vim.KeyPress, app gowid.IApp) { delete(w.kmap, from) } // ClearMappings will remove all mappings. I deliberately preserve the same dictionary, // though in case I decide in the future it's useful to let clients have direct access to // the map (and so maybe store it somewhere). func (w *Widget) ClearMappings(app gowid.IApp) { for k := range w.kmap { delete(w.kmap, k) } } //====================================================================== // Local Variables: // mode: Go // fill-column: 90 // End: termshark-2.2.0/widgets/minibuffer/000077500000000000000000000000001377442047300173225ustar00rootroot00000000000000termshark-2.2.0/widgets/minibuffer/minibuffer.go000066400000000000000000000321731377442047300220050ustar00rootroot00000000000000// Copyright 2019-2021 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 minibuffer todo package minibuffer import ( "regexp" "sort" "strings" "github.com/gcla/gowid" "github.com/gcla/gowid/widgets/button" "github.com/gcla/gowid/widgets/dialog" "github.com/gcla/gowid/widgets/edit" "github.com/gcla/gowid/widgets/holder" "github.com/gcla/gowid/widgets/hpadding" "github.com/gcla/gowid/widgets/isselected" "github.com/gcla/gowid/widgets/list" "github.com/gcla/gowid/widgets/null" "github.com/gcla/gowid/widgets/overlay" "github.com/gcla/gowid/widgets/pile" "github.com/gcla/gowid/widgets/styled" "github.com/gcla/gowid/widgets/text" "github.com/gcla/termshark/v2/widgets/keepselected" "github.com/gdamore/tcell" ) //====================================================================== // Widget represents a termshark-specific "minibuffer" widget, expected to be opened // as a dialog near the bottom of the screen. It allows the user to type commands and // supports tab completion and listing completions. type Widget struct { *dialog.Widget compl *holder.Widget selections *list.Widget ed *edit.Widget pl *pile.Widget showAll bool // true if the user hits tab with nothing in the minibuffer. I don't // want to display all completions if the buffer is empty because it fills the screen // and looks ugly. So this is a hack to allow the completions to be displayed // via the tab key actions map[string]IAction } var _ gowid.IWidget = (*Widget)(nil) var nullw *null.Widget var wordExp *regexp.Regexp func init() { nullw = null.New() wordExp = regexp.MustCompile(`( *)((?:")[^"]*(?:")|[^\s]*)`) } // IAction represents a command that can be run in the minibuffer e.g. "set". It // can decide whether or not to show in the list of completions e.g. if the user // types "s". type IAction interface { Run(gowid.IApp, ...string) error // nil means success Arguments([]string, gowid.IApp) []IArg OfferCompletion() bool } // IArg represents an argument to a minibuffer command e.g. "dark-mode" in the // command "set dark-mode on". type IArg interface { OfferCompletion() bool Completions() []string } type partial struct { word string qword string before string after string } func (p *partial) Line() string { return p.before + p.qword + p.after } func (p *partial) CursorPos() int { return len(p.before + p.qword) } // keysWidget redirects printable characters to the edit widget (the lower), but navigational // commands like up/down will control the selection widget. type keysWidget struct { *pile.Widget top gowid.IWidget bottom gowid.IWidget outer *Widget } var _ gowid.IWidget = (*keysWidget)(nil) func (w *keysWidget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { res := false switch ev := ev.(type) { case *tcell.EventKey: switch ev.Key() { case tcell.KeyRune: res = w.bottom.UserInput(ev, size, focus, app) case tcell.KeyDown, tcell.KeyCtrlN, tcell.KeyUp, tcell.KeyCtrlP: res = w.top.UserInput(ev, size, focus, app) case tcell.KeyTAB, tcell.KeyEnter: w.outer.handleSelection(ev.Key() == tcell.KeyEnter, app) res = true case tcell.KeyBackspace, tcell.KeyBackspace2: if w.outer.ed.Text() == "" { if w.outer.IsOpen() { w.outer.Close(app) } res = true } } } if !res { res = w.Widget.UserInput(ev, size, focus, app) } w.Widget.SetFocus(app, 1) return res } func New() *Widget { res := &Widget{} editW := edit.New(edit.Options{ Caption: ":", }) editW.OnTextSet(gowid.MakeWidgetCallback("cb", gowid.WidgetChangedFunction(func(app gowid.IApp, ew gowid.IWidget) { res.updateCompletions(app) }))) // If the cursor pos changes, we might not be displaying the right set of completions editW.OnCursorPosSet(gowid.MakeWidgetCallback("cb", gowid.WidgetChangedFunction(func(app gowid.IApp, ew gowid.IWidget) { res.updateCompletions(app) }))) top := holder.New(nullw) bottom := hpadding.New(editW, gowid.HAlignLeft{}, gowid.RenderFlow{}) bufferW := pile.New( []gowid.IContainerWidget{ &gowid.ContainerWidget{ IWidget: top, D: gowid.RenderFlow{}, }, &gowid.ContainerWidget{ IWidget: bottom, D: gowid.RenderFlow{}, }, }, pile.Options{ StartRow: 1, }, ) keys := &keysWidget{ Widget: bufferW, top: top, bottom: bottom, outer: res, } *res = Widget{ Widget: dialog.New( keys, dialog.Options{ Buttons: []dialog.Button{}, NoShadow: true, NoFrame: false, BackgroundStyle: gowid.MakePaletteRef("cmdline"), ButtonStyle: gowid.MakePaletteRef("cmdline-button"), BorderStyle: gowid.MakePaletteRef("cmdline-border"), }, ), compl: top, ed: editW, pl: bufferW, actions: make(map[string]IAction), } return res } func (w *Widget) handleSelection(keyIsEnter bool, app gowid.IApp) { // Break edit text up into "words" // gcla later todo - need to check it's not nil selectedIdx := 0 if w.selections != nil { selectedIdx = int(w.selections.Walker().Focus().(list.ListPos)) } wordMatchesS := wordExp.FindAllStringSubmatch(w.ed.Text(), -1) words := make([]string, 0, len(wordMatchesS)) for _, m := range wordMatchesS { if m[2] != "" { words = append(words, strings.TrimPrefix(strings.TrimSuffix(m[2], "\""), "\"")) // make a list of the words in the minibuffer } } switch { // how many words are in the edit box case len(words) > 1: // a command with args, so command itself must be provided in full. partials := w.getPartialsCompletions(false, app) switch len(partials) { // case 1: case 0: // "load /tmp/foo" and [] - just run what the user typed if the key was enter if act, ok := w.actions[words[0]]; ok && keyIsEnter { err := act.Run(app, words...) if err == nil { // Run the command, let it handle errors if w.IsOpen() { w.Close(app) } } } default: // if the last word exactly equals the one selected in the partials, just run on enter if words[len(words)-1] == partials[selectedIdx].word && keyIsEnter { // "load /tmp/foo.pcap" and ["/tmp/foo.pcap"] if act, ok := w.actions[words[0]]; ok { err := act.Run(app, words...) if err == nil { // Run the command, let it handle errors if w.IsOpen() { w.Close(app) } } } } else { // Otherwise, tab complete // // "load /tmp/foo" and ["/tmp/foo2.pcap", "/tmp/foo.pcap"] // ^^^^^^^^^^^^^ w.ed.SetText(partials[selectedIdx].Line(), app) w.ed.SetCursorPos(partials[selectedIdx].CursorPos(), app) } //default: } case len(words) == 1: // command itself may be partially provided. If there is only // one way for the command to be completed, allow it to be run. // User types "cl", partials would be ["clear-packets", "clear-filter"] partials := w.getPartialsCompletions(false, app) switch len(partials) { case 0: if act, ok := w.actions[words[0]]; ok && keyIsEnter { err := act.Run(app, words...) if err == nil { // Run the command, let it handle errors if w.IsOpen() { w.Close(app) } } } default: if words[len(words)-1] == partials[selectedIdx].word && keyIsEnter { act := w.actions[partials[selectedIdx].word] if len(act.Arguments([]string{}, app)) == 0 { err := w.actions[partials[selectedIdx].word].Run(app, partials[selectedIdx].word) if err == nil { if w.IsOpen() { w.Close(app) } } } } else { if keyIsEnter { w.ed.SetText(partials[selectedIdx].Line(), app) w.ed.SetCursorPos(partials[selectedIdx].CursorPos(), app) } else { // This is tab - complete to the longest common prefix extraPrefix := "" loop: for j := len(words[len(words)-1]); true; j += 1 { var c rune for _, partial := range partials { if len(partial.word) <= j { break loop } if c == 0 { c = rune(partial.word[j]) } else { if c != rune(partial.word[j]) { break loop } } } extraPrefix += string(c) } longestPrefixPartial := partials[selectedIdx] // e.g. "cl" + "ear-" from ["clear-packets", "clear-filter"] longestPrefixPartial.qword = words[len(words)-1] + extraPrefix w.ed.SetText(longestPrefixPartial.Line(), app) w.ed.SetCursorPos(longestPrefixPartial.CursorPos(), app) } } } default: if w.selections == nil { w.showAll = true w.updateCompletions(app) } else if keyIsEnter { // if the selections are being displayed and enter is hit, complete that selection partials := w.getPartialsCompletions(true, app) w.ed.SetText(partials[selectedIdx].Line(), app) w.ed.SetCursorPos(partials[selectedIdx].CursorPos(), app) } } } // Not thread-safe, manage via App perhaps func (w *Widget) Register(name string, action IAction) { w.actions[name] = action } func (w *Widget) getPartialsCompletions(checkOffer bool, app gowid.IApp) []partial { txt := w.ed.Text() partials := make([]partial, 0) // e.g. "demo false" -> [[0 4 0 0 0 4] [4 10 4 5 5 10]] wordMatches := wordExp.FindAllStringSubmatchIndex(txt, -1) wordMatchesS := wordExp.FindAllStringSubmatch(txt, -1) wordIdx := 0 wordStart := 0 wordEnd := 0 cp := w.ed.CursorPos() // for : prompt? for mIdx, wordMatch := range wordMatches { wordIdx = mIdx wordStart = wordMatch[4] wordEnd = wordMatch[5] if wordMatch[2] <= cp && cp <= wordMatch[5] { // within the range of whitespace+word if wordMatch[2] < cp && cp < wordMatch[3] { // within the range of whitespace only // fake match - this is so the correct completion is displayed in the following situation: // "set dark-mode" // " ^ " wordMatchesS = append(wordMatchesS[0:wordIdx], append([][]string{{"", "", ""}}, wordMatchesS[wordIdx:len(wordMatchesS)]...)...) wordStart = cp wordEnd = cp } break } } toks := make([]string, 0) for _, s := range wordMatchesS { toks = append(toks, s[2]) } if wordIdx > 0 { argIdx := wordIdx - 1 // first argument to command if word, ok := w.actions[wordMatchesS[0][2]]; ok { // wordArgs := word.Arguments(toks[1:], app) if argIdx < len(wordArgs) { if !checkOffer || wordArgs[argIdx].OfferCompletion() { for _, complV := range wordArgs[argIdx].Completions() { // to bind properly compl := complV qcompl := compl if strings.Contains(qcompl, " ") { qcompl = "\"" + qcompl + "\"" } partials = append(partials, partial{ word: compl, qword: qcompl, before: txt[0:wordStart], after: txt[wordEnd:len(txt)], }) } } } } } else { // This is the first word matching keys := make([]string, 0, len(w.actions)) for k, _ := range w.actions { keys = append(keys, k) } sort.Strings(keys) for _, keyV := range keys { key := keyV act := w.actions[key] if (!checkOffer || act.OfferCompletion()) && strings.HasPrefix(key, txt) { partials = append(partials, partial{ word: key, qword: key, before: "", after: "", }) } } } return partials } func (w *Widget) updateCompletions(app gowid.IApp) { txt := w.ed.Text() complWidgets := make([]gowid.IWidget, 0) partials := make([]partial, 0) if txt != "" || w.showAll { partials = w.getPartialsCompletions(true, app) } w.showAll = false for _, partialV := range partials { partial := partialV // avoid gotcha compBtn := button.NewBare(text.New(partial.word)) compBtn.OnClick(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, widget gowid.IWidget) { w.ed.SetText(partial.Line(), app) w.ed.SetCursorPos(partial.CursorPos(), app) w.pl.SetFocus(app, 1) })) complWidgets = append(complWidgets, isselected.New( compBtn, styled.New(compBtn, gowid.MakePaletteRef("cmdline-button")), styled.New(compBtn, gowid.MakePaletteRef("cmdline-button")), ), ) } walker := list.NewSimpleListWalker(complWidgets) if len(complWidgets) > 0 { walker.SetFocus(walker.Last(), app) selections := list.New(walker) sl2 := keepselected.New(selections) w.compl.SetSubWidget(sl2, app) w.selections = selections } else { // don't want anything to take focus if there are no completions w.compl.SetSubWidget(nullw, app) w.selections = nil } } func Open(w dialog.IOpenExt, container gowid.ISettableComposite, width gowid.IWidgetDimension, height gowid.IWidgetDimension, app gowid.IApp) { ov := overlay.New(w, container.SubWidget(), gowid.VAlignBottom{}, height, // Intended to mean use as much vertical space as you need gowid.HAlignLeft{Margin: 5, MarginRight: 5}, width) if _, ok := width.(gowid.IRenderFixed); ok { w.SetContentWidth(gowid.RenderFixed{}, app) // fixed or weight:1, ratio:0.5 } else { w.SetContentWidth(gowid.RenderWithWeight{W: 1}, app) // fixed or weight:1, ratio:0.5 } w.SetSavedSubWidget(container.SubWidget(), app) w.SetSavedContainer(container, app) container.SetSubWidget(ov, app) w.SetOpen(true, app) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.2.0/widgets/regexstyle/000077500000000000000000000000001377442047300173675ustar00rootroot00000000000000termshark-2.2.0/widgets/regexstyle/regexstyle.go000066400000000000000000000055501377442047300221160ustar00rootroot00000000000000// Copyright 2019-2021 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 regexstyle provides a widget that highlights the content of its subwidget according to a regular // expression. The widget is also given an occurrence parameter which determines which instance of the regex // match is highlighted, or if -1 is supplied, all instances are highlighted. The widget currently wraps a // text widget only since it depends on that widget being able to clone its content. package regexstyle import ( "regexp" "github.com/gcla/gowid" "github.com/gcla/gowid/widgets/text" ) //====================================================================== // This is the type of subwidget supported by regexstyle type ContentWidget interface { gowid.IWidget Content() text.IContent SetContent(gowid.IApp, text.IContent) } type Widget struct { ContentWidget Highlight } type Highlight struct { Re *regexp.Regexp Occ int Style gowid.ICellStyler } func New(w ContentWidget, hl Highlight) *Widget { res := &Widget{ ContentWidget: w, Highlight: hl, } return res } func (w *Widget) SetRegexOccurrence(i int) { w.Occ = i } func (w *Widget) SetRegex(re *regexp.Regexp) { w.Re = re } func (w *Widget) RegexMatches() int { return len(w.regexMatches(w.Content())) } func (w *Widget) regexMatches(content text.IContent) [][]int { if w.Re == nil || w.Style == nil { return [][]int{} } runes := make([]rune, 0, content.Length()) for i := 0; i < w.Content().Length(); i++ { runes = append(runes, w.Content().ChrAt(i)) } return w.Re.FindAllStringIndex(string(runes), -1) } func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { if w.Re == nil || w.Style == nil { return w.ContentWidget.Render(size, focus, app) } // save orig so it can be restored before end of render content := w.Content() if clonableContent, ok := content.(text.ICloneContent); !ok { return w.ContentWidget.Render(size, focus, app) } else { dup := clonableContent.Clone() if textContent, ok := content.(*text.Content); !ok { return w.ContentWidget.Render(size, focus, app) } else { indices := w.regexMatches(content) if len(indices) == 0 { return w.ContentWidget.Render(size, focus, app) } for i := 0; i < len(indices); i++ { if w.Occ == i || w.Occ == -1 { for j := indices[i][0]; j < indices[i][1]; j++ { (*textContent)[j].Attr = w.Style } } } // Let the underlying text widget layout the text in the way it's configured to // (line breaks, justification, etc) canvas := w.ContentWidget.Render(size, focus, app) w.SetContent(app, dup) return canvas } } } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.2.0/widgets/renderfocused/000077500000000000000000000000001377442047300200245ustar00rootroot00000000000000termshark-2.2.0/widgets/renderfocused/renderfocused.go000066400000000000000000000030231377442047300232010ustar00rootroot00000000000000// Copyright 2019-2021 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 w.IWidget.Render(size, gowid.Focused, app) } func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { return w.IWidget.UserInput(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: termshark-2.2.0/widgets/resizable/000077500000000000000000000000001377442047300171545ustar00rootroot00000000000000termshark-2.2.0/widgets/resizable/resizable.go000066400000000000000000000156001377442047300214650ustar00rootroot00000000000000// Copyright 2019-2021 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), } return res } func (w *ColumnsWidget) GetOffsets() []Offset { return w.Offsets } func (w *ColumnsWidget) SetOffsets(offs []Offset, app gowid.IApp) { w.Offsets = offs gowid.RunWidgetCallbacks(w.Callbacks, OffsetsCB{}, app, w) } func (w *ColumnsWidget) OnOffsetsSet(cb gowid.IWidgetChangedCallback) { if w.Callbacks == nil { w.Callbacks = gowid.NewCallbacks() } gowid.AddWidgetCallback(w.Callbacks, OffsetsCB{}, cb) } func (w *ColumnsWidget) RemoveOnOffsetsSet(cb gowid.IIdentity) { if w.Callbacks == nil { w.Callbacks = gowid.NewCallbacks() } 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 gowid.RunWidgetCallbacks(w.Callbacks, OffsetsCB{}, app, w) } 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: termshark-2.2.0/widgets/resizable/resizable_test.go000066400000000000000000000017501377442047300225250ustar00rootroot00000000000000// Copyright 2019-2021 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: termshark-2.2.0/widgets/rossshark/000077500000000000000000000000001377442047300172135ustar00rootroot00000000000000termshark-2.2.0/widgets/rossshark/rossshark.go000066400000000000000000000101501377442047300215560ustar00rootroot00000000000000// Copyright 2019-2021 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 rossshark provides a widget that draws a hi-tech shark fin over the // background and allows it to move across the screen. I hope this is faithful // to Ross Jacobs' vision :-) package rossshark import ( "math/rand" "time" "github.com/gcla/gowid" "github.com/gcla/gowid/gwutil" ) //====================================================================== var maskF []string var maskB = []string{ "00000000000000000000000000111111111", "00000000000000000000011111111111110", "00000000000000000111111111111111100", "00000000000000111111111111111111000", "00000000000011111111111111111110000", "00000000001111111111111111111110000", "00000000111111111111111111111100000", "00000001111111111111111111111100000", "00000011111111111111111111111100000", "00000111111111111111111111111100000", "00001111111111111111111111111100000", "00011111111111111111111111111100000", "00111111111111111111111111111100000", "01111111111111111111111111111100000", "01111111111111111111111111111110000", "11111111111111111111111111111110000", "11111111111111111111111111111111000", "11111111111111111111111111111111100", } func init() { maskF = make([]string, 0, len(maskB)) for _, line := range maskB { maskF = append(maskF, reverseString(line)) } } type Direction int const ( Backward Direction = 0 Forward Direction = iota ) type Widget struct { gowid.IWidget Dir Direction active bool xOffset int mask [][]string backg []string ticker *time.Ticker } var _ gowid.IWidget = (*Widget)(nil) func New(w gowid.IWidget) *Widget { backg := make([]string, 0, 48) for i := 0; i < cap(backg); i++ { backg = append(backg, randomString(110)) } res := &Widget{ IWidget: w, mask: [][]string{maskB, maskF}, backg: backg, xOffset: 100000, } return res } func (w *Widget) Advance() { switch w.Dir { case Backward: w.xOffset -= 1 if w.xOffset <= -len(w.mask[0]) { w.xOffset = 100000 // big enough } case Forward: w.xOffset += 1 } } func (w *Widget) Activate() { w.ticker = time.NewTicker(time.Duration(150) * time.Millisecond) } func (w *Widget) Deactivate() { w.ticker = nil } func (w *Widget) Active() bool { return w.ticker != nil } func (w *Widget) C() <-chan time.Time { return w.ticker.C } func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { c := w.IWidget.Render(size, focus, app) if w.Active() { // Adjust here to account for the fact the screen can be resized if w.xOffset >= c.BoxColumns() { switch w.Dir { case Backward: w.xOffset = c.BoxColumns() - 1 case Forward: w.xOffset = -len(w.mask[0]) } } mask := w.mask[w.Dir] yOffset := c.BoxRows()/2 - len(mask)/2 // in the middle for y, sy := gwutil.Max(0, yOffset), gwutil.Max(0, -yOffset); y < c.BoxRows() && sy < len(mask); y, sy = y+1, sy+1 { for x, sx := gwutil.Max(0, w.xOffset), gwutil.Max(0, -w.xOffset); x < c.BoxColumns() && sx < len(mask[0]); x, sx = x+1, sx+1 { if mask[sy][sx] == '1' { cell := c.CellAt(x, y) r := w.backg[y%len(w.backg)][x%len(w.backg[0])] c.SetCellAt(x, y, cell.WithRune(rune(r))) } } } } return c } //====================================================================== // Use charset [a-f0-9] to mirror tshark -x/xxd hex output const charset = "abcdef0123456789" var seededRand *rand.Rand = rand.New(rand.NewSource(time.Now().UnixNano())) func randomStringWithCharset(length int, charset string) string { b := make([]byte, length) for i := range b { b[i] = charset[seededRand.Intn(len(charset))] } return string(b) } func randomString(length int) string { return randomStringWithCharset(length, charset) } // Plagiarized from https://stackoverflow.com/a/4965535 - the most straightforward answer func reverseString(s string) (result string) { for _, v := range s { result = string(v) + result } return } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.2.0/widgets/scrollabletable/000077500000000000000000000000001377442047300203265ustar00rootroot00000000000000termshark-2.2.0/widgets/scrollabletable/scrollabletable.go000066400000000000000000000024141377442047300240100ustar00rootroot00000000000000// Copyright 2019-2021 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 scrollabletable makes a widget that some scrollbar interfaces // suitable for passing to withscrollbar.New() package scrollabletable import ( "github.com/gcla/gowid" "github.com/gcla/gowid/widgets/table" "github.com/gcla/termshark/v2/widgets/withscrollbar" ) //====================================================================== type IScrollableTable interface { gowid.IWidget withscrollbar.IScrollOneLine withscrollbar.IScrollOnePage CurrentRow() int Model() table.IModel } // To implement withscrollbar.IScrollValues type Widget struct { IScrollableTable } // makes a IScrollableTable suitable for passing to withscrollbar.New() var _ withscrollbar.IScrollSubWidget = Widget{} var _ withscrollbar.IScrollSubWidget = (*Widget)(nil) func New(t IScrollableTable) *Widget { return &Widget{ IScrollableTable: t, } } func (s Widget) ScrollLength() int { return s.Model().(table.IBoundedModel).Rows() } func (s Widget) ScrollPosition() int { return s.CurrentRow() } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.2.0/widgets/scrollabletext/000077500000000000000000000000001377442047300202235ustar00rootroot00000000000000termshark-2.2.0/widgets/scrollabletext/scrollabletext.go000066400000000000000000000071701377442047300236060ustar00rootroot00000000000000// Copyright 2019-2021 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 scrollabletext provides a text widget that can be placed inside // withscrollbar.Widget package scrollabletext import ( "strings" "github.com/gcla/gowid" "github.com/gcla/gowid/gwutil" "github.com/gcla/gowid/widgets/selectable" "github.com/gcla/gowid/widgets/text" "github.com/gdamore/tcell" ) //====================================================================== // Widget constructs a text widget and allows it to be scrolled. But this widget is limited - it assumes no // line will wrap. To make this happen it ensures that any lines that are too long are clipped. It makes this // assumption because my scrollbar APIs are not well designed, and functions like ScrollPosition and // ScrollLength don't understand the current rendering context. That means if the app is resized, and a line // now takes two screen lines to render and not one, the scrollbar can't be built accurately. Until I design // a better scrollbar API, this will work - I'm only using it for limited information dialogs at the moment. type Widget struct { *selectable.Widget splitText []string linesFromTop int // how many lines down we are cachedLength int } var _ gowid.IWidget = (*Widget)(nil) func New(txt string) *Widget { splitText := strings.Split(txt, "\n") res := &Widget{ splitText: splitText, cachedLength: len(splitText), } res.makeText() return res } func (w *Widget) makeText() { w.Widget = selectable.New( text.New( strings.Join(w.splitText[w.linesFromTop:], "\n"), text.Options{ Wrap: text.WrapClip, ClipIndicator: "...", }, ), ) } func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { handled := true linesFromTop := w.linesFromTop switch ev := ev.(type) { case *tcell.EventKey: switch ev.Key() { case tcell.KeyPgUp: w.UpPage(1, size, app) case tcell.KeyUp, tcell.KeyCtrlP: w.Up(1, size, app) case tcell.KeyDown, tcell.KeyCtrlN: w.Down(1, size, app) case tcell.KeyPgDn: w.DownPage(1, size, app) default: handled = false } } if handled && linesFromTop == w.linesFromTop { handled = false } if !handled { handled = w.Widget.UserInput(ev, size, focus, app) } return handled } // Implement functions for withscrollbar.Widget func (w *Widget) ScrollPosition() int { return w.linesFromTop } func (w *Widget) ScrollLength() int { return w.cachedLength } func (w *Widget) Up(lines int, size gowid.IRenderSize, app gowid.IApp) { pos := w.linesFromTop w.linesFromTop = gwutil.Max(0, w.linesFromTop-lines) if pos != w.linesFromTop { w.makeText() } } func (w *Widget) Down(lines int, size gowid.IRenderSize, app gowid.IApp) { pos := w.linesFromTop w.linesFromTop = gwutil.Min(w.cachedLength-1, w.linesFromTop+lines) if pos != w.linesFromTop { w.makeText() } } func (w *Widget) UpPage(num int, size gowid.IRenderSize, app gowid.IApp) { pos := w.linesFromTop pg := 1 if size, ok := size.(gowid.IRows); ok { pg = size.Rows() } w.linesFromTop = gwutil.Max(0, w.linesFromTop-(pg*num)) if pos != w.linesFromTop { w.makeText() } } func (w *Widget) DownPage(num int, size gowid.IRenderSize, app gowid.IApp) { pos := w.linesFromTop pg := 1 if size, ok := size.(gowid.IRows); ok { pg = size.Rows() } w.linesFromTop = gwutil.Min(w.cachedLength-1, w.linesFromTop+(pg*num)) if pos != w.linesFromTop { w.makeText() } } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.2.0/widgets/streamwidget/000077500000000000000000000000001377442047300176735ustar00rootroot00000000000000termshark-2.2.0/widgets/streamwidget/streamwidget.go000066400000000000000000001050071377442047300227240ustar00rootroot00000000000000// Copyright 2019-2021 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 streamwidget provides a very specific stream reassembly termshark widget. // This is probably not much use generally, but is separated out to ease testing. It // is intended to render as mostly full screen, with a title bar, the main view showing // the reassembled stream, and controls at the bottom. package streamwidget import ( "fmt" "regexp" "strings" "github.com/gcla/gowid" "github.com/gcla/gowid/gwutil" "github.com/gcla/gowid/widgets/button" "github.com/gcla/gowid/widgets/checkbox" "github.com/gcla/gowid/widgets/clicktracker" "github.com/gcla/gowid/widgets/columns" "github.com/gcla/gowid/widgets/divider" "github.com/gcla/gowid/widgets/edit" "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/list" "github.com/gcla/gowid/widgets/menu" "github.com/gcla/gowid/widgets/null" "github.com/gcla/gowid/widgets/overlay" "github.com/gcla/gowid/widgets/pile" "github.com/gcla/gowid/widgets/radio" "github.com/gcla/gowid/widgets/selectable" "github.com/gcla/gowid/widgets/styled" "github.com/gcla/gowid/widgets/table" "github.com/gcla/gowid/widgets/text" "github.com/gcla/termshark/v2" "github.com/gcla/termshark/v2/format" "github.com/gcla/termshark/v2/streams" "github.com/gcla/termshark/v2/ui/menuutil" "github.com/gcla/termshark/v2/ui/tableutil" "github.com/gcla/termshark/v2/widgets" "github.com/gcla/termshark/v2/widgets/appkeys" "github.com/gcla/termshark/v2/widgets/copymodetable" "github.com/gcla/termshark/v2/widgets/framefocus" "github.com/gcla/termshark/v2/widgets/keepselected" "github.com/gcla/termshark/v2/widgets/regexstyle" "github.com/gcla/termshark/v2/widgets/scrollabletable" "github.com/gcla/termshark/v2/widgets/trackfocus" "github.com/gcla/termshark/v2/widgets/withscrollbar" "github.com/gdamore/tcell" ) //====================================================================== var fixed gowid.RenderFixed var indentRe *regexp.Regexp func init() { indentRe = regexp.MustCompile(`(?m)^(.+)$`) // do each line } var PacketRowNotLoadedError = fmt.Errorf("The packet is not yet loaded.") //====================================================================== type DisplayFormat int const ( Hex DisplayFormat = 0 Ascii DisplayFormat = iota Raw DisplayFormat = iota ) type ConversationFilter int const ( Entire ConversationFilter = 0 ClientOnly ConversationFilter = iota ServerOnly ConversationFilter = iota ) type streamStats struct { started bool lastTurn streams.Direction turns int // how many times the conversation flips clientPackets int clientBytes int serverPackets int serverBytes int } type Options struct { DefaultDisplay func() DisplayFormat // Start in ascii, hex, raw FilterOutFunc func(IFilterOut, gowid.IApp) // UI changes to run when user clicks "filter out" button PreviousFilter string // so if we filter out, we can do "Previous and (! tcp.stream eq 0)" ChunkClicker IChunkClicked // UI changes to make when stream chunk in table is clicked ErrorHandler IOnError // UI action to take on error CopyModeWidget gowid.IWidget // What to display when copy-mode is started. } //====================================================================== type Widget struct { gowid.IWidget opt Options tblWidgets []*copymodetable.Widget // the table for all data, client data, server data viewWidgets []gowid.IWidget // the table for all data, client data, server data selectedConv ConversationFilter // both sides (entire), client only, server only data *Data // the rest of the data from tshark -z follow stats streamStats // track turns, client packets, bytes, etc streamHeader streams.FollowHeader // first chunk of data back from tshark -z follow displayAs DisplayFormat // display as hex, ascii, raw captureDevice string // it's a very feature-specific widget so I don't care about supporting callbacks displayFilter string // "tcp.stream eq 1" Proto streams.Protocol // TCP, UDP tableHolder *holder.Widget // hold the chunk UI table convBtn *button.Widget // "Entire conversation" -> click this to open conv menu turnTxt *text.Widget // "26 clients pkts, 0 server pkts, 5 turns" sections *pile.Widget // the vertical ui layout convMenuHolder *holder.Widget // actually holds the listbox used for the open "menu" - entire, client, server convMenu *menu.Widget // the menu that opens when you hit the conversation button (entire, client, server) clickActive bool // if true, clicking in stream list will display packet selected keyState *termshark.KeyState // for vim key chords that are intended for table navigation searchState // track the current highlighted search term } func New(displayFilter string, captureDevice string, proto streams.Protocol, convMenu *menu.Widget, convMenuHolder *holder.Widget, keyState *termshark.KeyState, opts ...Options) *Widget { var opt Options if len(opts) > 0 { opt = opts[0] } var mode DisplayFormat if opt.DefaultDisplay != nil { mode = opt.DefaultDisplay() } res := &Widget{ opt: opt, displayFilter: displayFilter, captureDevice: captureDevice, Proto: proto, displayAs: mode, convMenu: convMenu, convMenuHolder: convMenuHolder, tblWidgets: make([]*copymodetable.Widget, 3), viewWidgets: make([]gowid.IWidget, 3), clickActive: true, keyState: keyState, } res.construct() return res } var _ gowid.IWidget = (*Widget)(nil) var _ iHighlight = (*Widget)(nil) func (w *Widget) PreviousFilter() string { return w.opt.PreviousFilter } func (w *Widget) DisplayFilter() string { return w.displayFilter } func (w *Widget) clickIsActive() bool { return w.clickActive } // Used by the stream chunk structures, which act as table models; they apply the returned highlight structure // to a a regexstyle which wraps the contents of each chunk displayed in the table. If this row is not the row // currently being searched (e.g. chunk #5 instead of chunk #2), then a default Highlight is returned which // will have no effect on the rendering of the stream chunk. func (w *Widget) highlightThis(pos table.Position) regexstyle.Highlight { if pos == w.searchRow { return regexstyle.Highlight{ Re: w.searchRe, Occ: w.searchOccurrence, Style: gowid.MakePaletteRef("stream-match"), } } return regexstyle.Highlight{} } // The widget displayed in the first line of the stream reassembly UI. func (w *Widget) makeHeaderWidget() gowid.IWidget { var headerText string var headerText1 string var headerText2 string var headerText3 string if w.Proto != streams.Unspecified { headerText1 = fmt.Sprintf("Follow %s Stream", w.Proto) } if w.displayFilter != "" { headerText2 = fmt.Sprintf("(%s)", w.displayFilter) } if w.captureDevice != "" { headerText3 = fmt.Sprintf("- %s", w.captureDevice) } headerText = strings.Join([]string{headerText1, headerText2, headerText3}, " ") headerView := overlay.New( hpadding.New(w.opt.CopyModeWidget, gowid.HAlignMiddle{}, fixed), hpadding.New( text.New(headerText), gowid.HAlignMiddle{}, fixed, ), gowid.VAlignTop{}, gowid.RenderWithRatio{R: 1}, gowid.HAlignMiddle{}, gowid.RenderWithRatio{R: 1}, overlay.Options{ BottomGetsFocus: true, TopGetsNoFocus: true, BottomGetsCursor: true, }, ) return headerView } func MakeConvMenu() (*holder.Widget, *menu.Widget) { convListBoxWidgetHolder := holder.New(null.New()) convMenu := menu.New("conv", convListBoxWidgetHolder, fixed, menu.Options{ Modal: true, CloseKeysProvided: true, CloseKeys: []gowid.IKey{ gowid.MakeKey('q'), gowid.MakeKeyExt(tcell.KeyLeft), gowid.MakeKeyExt(tcell.KeyEscape), gowid.MakeKeyExt(tcell.KeyCtrlC), }, }) return convListBoxWidgetHolder, convMenu } func (w *Widget) updateConvMenuWidget(app gowid.IApp) { convListBox := w.makeConvMenuWidget() w.convMenuHolder.SetSubWidget(convListBox, app) w.setConvButtonText(app) w.setTurnText(app) } func (w *Widget) makeConvMenuWidget() gowid.IWidget { savedItems := make([]menuutil.SimpleMenuItem, 0) savedItems = append(savedItems, menuutil.SimpleMenuItem{ Txt: w.getConvButtonText(Entire), Key: gowid.MakeKey('e'), CB: func(app gowid.IApp, w2 gowid.IWidget) { w.convMenu.Close(app) w.selectedConv = Entire w.tableHolder.SetSubWidget(w.viewWidgets[w.selectedConv], app) w.setConvButtonText(app) }, }, ) // Ensure we have valid header data. This should always be true if w.streamHeader.Node0 != "" && w.streamHeader.Node1 != "" { savedItems = append(savedItems, menuutil.SimpleMenuItem{ Txt: w.getConvButtonText(ClientOnly), Key: gowid.MakeKey('c'), CB: func(app gowid.IApp, w2 gowid.IWidget) { w.convMenu.Close(app) w.selectedConv = ClientOnly w.tableHolder.SetSubWidget(w.viewWidgets[w.selectedConv], app) w.setConvButtonText(app) }, }, ) savedItems = append(savedItems, menuutil.SimpleMenuItem{ Txt: w.getConvButtonText(ServerOnly), Key: gowid.MakeKey('s'), CB: func(app gowid.IApp, w2 gowid.IWidget) { w.convMenu.Close(app) w.selectedConv = ServerOnly w.tableHolder.SetSubWidget(w.viewWidgets[w.selectedConv], app) w.setConvButtonText(app) }, }, ) } convListBox := menuutil.MakeMenuWithHotKeys(savedItems) return convListBox } // Turns an array of stream chunks into a pair of (a) a scrollable table // widget to be displayed, and (b) the underlying table so that its model // can be manipulated. func (w *Widget) makeTable(i int) (gowid.IWidget, *copymodetable.Widget) { data := w.data.vdata[i].hexChunks btbl := &table.BoundedWidget{Widget: table.New(data)} cmtbl := copymodetable.New( btbl, data, data, "streamtable", copyModePalette{}, ) sc := appkeys.New( keepselected.New( withscrollbar.New( scrollabletable.New(cmtbl), withscrollbar.Options{ HideIfContentFits: true, }, ), ), tableutil.GotoHandler(&tableutil.GoToAdapter{ BoundedWidget: btbl, KeyState: w.keyState, }), ) return sc, cmtbl } // "26 clients pkts, 0 server pkts, 5 turns" func (w *Widget) getTurnContent() *text.Content { cpkts := gwutil.If(w.stats.clientPackets == 1, "pkt", "pkts").(string) spkts := gwutil.If(w.stats.serverPackets == 1, "pkt", "pkts").(string) turns := gwutil.If(w.stats.turns == 1, "turn", "turns").(string) return text.NewContent([]text.ContentSegment{ text.StyledContent(fmt.Sprintf("%d client %s", w.stats.clientPackets, cpkts), gowid.MakePaletteRef("stream-client")), text.StringContent(", "), text.StyledContent(fmt.Sprintf("%d server %s", w.stats.serverPackets, spkts), gowid.MakePaletteRef("stream-server")), text.StringContent(fmt.Sprintf(", %d %s", w.stats.turns, turns)), }) } func (w *Widget) setTurnText(app gowid.IApp) { w.turnTxt.SetContent(app, w.getTurnContent()) } func (w *Widget) getConvButtonText(typ ConversationFilter) string { var txt string switch typ { case Entire: txt = fmt.Sprintf("Entire conversation (%d bytes)", w.stats.clientBytes+w.stats.serverBytes) case ClientOnly: txt = fmt.Sprintf("%s → %s (%d bytes)", w.streamHeader.Node0, w.streamHeader.Node1, w.stats.clientBytes) case ServerOnly: txt = fmt.Sprintf("%s → %s (%d bytes)", w.streamHeader.Node1, w.streamHeader.Node0, w.stats.serverBytes) } return txt } // Set the text of the button showing "entire conversation", client only, server only func (w *Widget) setConvButtonText(app gowid.IApp) { w.convBtn.SetSubWidget(text.New(w.getConvButtonText(w.selectedConv)), app) } func (w *Widget) construct() { w.data = newData(w.opt.ChunkClicker, w, w, w) fixed := fixed rbgroup := make([]radio.IWidget, 0) rb1 := radio.New(&rbgroup) rbt1 := text.New(" hex ") rb2 := radio.New(&rbgroup) rbt2 := text.New(" ascii ") rb3 := radio.New(&rbgroup) rbt3 := text.New(" raw ") switch w.displayAs { case Hex: rb1.Select(nil) case Ascii: rb2.Select(nil) default: rb3.Select(nil) } c2cols := []gowid.IContainerWidget{ &gowid.ContainerWidget{rb1, fixed}, &gowid.ContainerWidget{rbt1, fixed}, &gowid.ContainerWidget{rb2, fixed}, &gowid.ContainerWidget{rbt2, fixed}, &gowid.ContainerWidget{rb3, fixed}, &gowid.ContainerWidget{rbt3, fixed}, } cols2 := columns.New(c2cols) rb1.OnClick(gowid.WidgetCallback{"cb", func(app gowid.IApp, w2 gowid.IWidget) { if rb1.Selected { w.displayAs = Hex for i := 0; i < len(w.tblWidgets); i++ { w.updateChunkModel(i, w.displayAs, app) } termshark.SetConf("main.stream-view", "hex") } }}) rb2.OnClick(gowid.WidgetCallback{"cb", func(app gowid.IApp, w2 gowid.IWidget) { if rb2.Selected { w.displayAs = Ascii for i := 0; i < len(w.tblWidgets); i++ { w.updateChunkModel(i, w.displayAs, app) } termshark.SetConf("main.stream-view", "ascii") } }}) rb3.OnClick(gowid.WidgetCallback{"cb", func(app gowid.IApp, w2 gowid.IWidget) { if rb3.Selected { w.displayAs = Raw for i := 0; i < len(w.tblWidgets); i++ { w.updateChunkModel(i, w.displayAs, app) } termshark.SetConf("main.stream-view", "raw") } }}) filterOutBtn := button.New(text.New("Filter stream out")) filterOutBtn.OnClick(gowid.WidgetCallback{"cb", func(app gowid.IApp, w2 gowid.IWidget) { w.opt.FilterOutFunc(w, app) }}) w.turnTxt = text.NewFromContent(w.getTurnContent()) // Hardcoded for 3 lines + frame - yuck convBtnSite := menu.NewSite(menu.SiteOptions{YOffset: -5}) w.convBtn = button.New(text.New(w.getConvButtonText(Entire))) w.convBtn.OnClick(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, w2 gowid.IWidget) { w.convMenu.Open(convBtnSite, app) })) //styledConvBtn := styled.NewInvertedFocus(w.convBtn, gowid.MakePaletteRef("default")) styledConvBtn := styled.NewExt( w.convBtn, gowid.MakePaletteRef("button"), gowid.MakePaletteRef("button-focus"), ) // After making button w.updateConvMenuWidget(nil) convCols := columns.NewFixed(convBtnSite, styledConvBtn) searchBox := edit.New(edit.Options{ Caption: "Find: ", }) reCheck := checkbox.New(false) caseCheck := checkbox.New(true) nextClick := func(app gowid.IApp, w2 gowid.IWidget) { txt := searchBox.Text() if txt == "" { w.opt.ErrorHandler.OnError("Enter a search string.", app) return } if !reCheck.IsChecked() { txt = regexp.QuoteMeta(txt) } if !caseCheck.IsChecked() { txt = fmt.Sprintf("(?i)%s", txt) } newre, err := regexp.Compile(txt) if err != nil { w.opt.ErrorHandler.OnError(fmt.Sprintf("Invalid regex: %s", searchBox.Text()), app) return } if w.searchReTxt != txt { w.initSearch(newre, txt) } i := w.selectedConv w.tblWidgets[i].Cache().Purge() // Start from current table focus position w.searchRow = table.Position(w.tblWidgets[i].Focus().(list.IBoundedWalkerPosition).ToInt()) origstate := w.searchState searchDone := false fixTablePosition := false for !searchDone { rem := findMatcher(w.tblWidgets[i].At(w.searchRow)) if rem == nil { searchDone = true w.searchState = origstate // maintain old position } else { if w.maxOccurrences.IsNone() { w.maxOccurrences = gwutil.SomeInt(rem.RegexMatches()) if w.maxOccurrences.Value() == 0 { w.goToNextSearchRow() fixTablePosition = true continue } w.searchOccurrence = 0 searchDone = true } else if w.searchOccurrence < w.maxOccurrences.Val()-1 { w.searchOccurrence += 1 searchDone = true } else { w.goToNextSearchRow() fixTablePosition = true continue } } } w.tblWidgets[i].Cache().Purge() if fixTablePosition { w.tblWidgets[i].SetCurrentRow(w.searchRow) } } searchBox2 := appkeys.New( searchBox, func(ev *tcell.EventKey, app gowid.IApp) bool { res := false switch ev.Key() { case tcell.KeyEnter: nextClick(app, nil) res = true } return res }, appkeys.Options{ ApplyBefore: true, }, ) searchBoxStyled := styled.New(searchBox2, gowid.MakePaletteRef("stream-search"), ) nextBtn := button.New(text.New("Next")) nextBtn.OnClick(gowid.MakeWidgetCallback("cb", nextClick)) pad := text.New(" ") hpad := hpadding.New(pad, gowid.HAlignLeft{}, fixed) vline := &gowid.ContainerWidget{ IWidget: fill.New('|'), D: gowid.RenderWithUnits{U: 1}, } streamsFooter1 := columns.NewWithDim( fixed, hpad, hpadding.New( w.turnTxt, gowid.HAlignLeft{}, fixed, ), hpad, vline, hpad, &gowid.ContainerWidget{ IWidget: searchBoxStyled, D: gowid.RenderWithWeight{W: 1}, }, hpad, hpadding.New( clicktracker.New( styled.NewExt( nextBtn, gowid.MakePaletteRef("button"), gowid.MakePaletteRef("button-focus"), ), ), gowid.HAlignLeft{}, fixed, ), hpad, vline, hpad, reCheck, hpad, text.New("Regex"), hpad, caseCheck, hpad, text.New("Case"), hpad, ) streamsFooter := columns.NewWithDim( gowid.RenderWithWeight{1}, hpadding.New( convCols, gowid.HAlignMiddle{}, fixed, ), hpadding.New( cols2, gowid.HAlignMiddle{}, fixed, ), hpadding.New( //styled.NewInvertedFocus(filterOutBtn, gowid.MakePaletteRef("default")), styled.NewExt( filterOutBtn, gowid.MakePaletteRef("button"), gowid.MakePaletteRef("button-focus"), ), gowid.HAlignMiddle{}, fixed, ), ) // In case it's not made header := w.makeHeaderWidget() //w.headerHolder = holder.New(w.header) streamsHeader := columns.NewWithDim( gowid.RenderWithWeight{1}, //w.headerHolder, header, ) w.tableHolder = holder.New(null.New()) mainpane := trackfocus.New( styled.New( framed.NewUnicode( w.tableHolder, ), gowid.MakePaletteRef("mainpane"), ), ) // Track whether the stream chunk list has focus. If it is clicked when it doesn't have focus, // don't annoy the user by displaying the selected packet in the underlying packet list. We // assume the user is just clicking to change the focus. mainpane.OnFocusLost(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, w2 gowid.IWidget) { w.clickActive = false })) mainpane.OnFocusGained(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, w2 gowid.IWidget) { w.clickActive = true })) mainPaneWithKeys := appkeys.NewMouse( appkeys.New( mainpane, widgets.SwallowMovementKeys, ), widgets.SwallowMouseScroll, // So we don't scroll out of the stream chunk list - it's annoying. Use tab instead. ) streamView := pile.New( []gowid.IContainerWidget{ &gowid.ContainerWidget{ streamsHeader, gowid.RenderWithUnits{U: 1}, }, &gowid.ContainerWidget{ mainPaneWithKeys, gowid.RenderWithWeight{W: 1}, }, &gowid.ContainerWidget{ streamsFooter1, gowid.RenderWithUnits{U: 1}, }, &gowid.ContainerWidget{ divider.NewUnicode(), gowid.RenderFlow{}, }, &gowid.ContainerWidget{ streamsFooter, gowid.RenderWithUnits{U: 1}, }, }, ) streamViewWithKeysAfter := appkeys.New( streamView, func(ev *tcell.EventKey, app gowid.IApp) bool { return streamViewKeyPressAfter(streamView, ev, app) }, appkeys.Options{ ApplyBefore: true, }, ) w.sections = streamView w.IWidget = streamViewWithKeysAfter for i := 0; i < len(w.tblWidgets); i++ { j := i // avoid loop variable gotcha w.viewWidgets[i], w.tblWidgets[i] = w.makeTable(i) w.tblWidgets[i].OnFocusChanged(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, w2 gowid.IWidget) { // reset search on manual moving of table w.goToSearchRow(table.Position(w.tblWidgets[j].Focus().(list.IBoundedWalkerPosition).ToInt())) })) w.updateChunkModel(i, w.displayAs, nil) } w.tableHolder.SetSubWidget(w.viewWidgets[w.selectedConv], nil) } func (w *Widget) updateChunkModel(i int, f DisplayFormat, app gowid.IApp) { switch f { case Hex: w.tblWidgets[i].SetModel(w.data.vdata[i].hexChunks, app) w.tblWidgets[i].RowClip = w.data.vdata[i].hexChunks w.tblWidgets[i].AllClip = w.data.vdata[i].hexChunks case Ascii: w.tblWidgets[i].SetModel(w.data.vdata[i].asciiChunks, app) w.tblWidgets[i].RowClip = w.data.vdata[i].asciiChunks w.tblWidgets[i].AllClip = w.data.vdata[i].asciiChunks case Raw: w.tblWidgets[i].SetModel(w.data.vdata[i].rawChunks, app) w.tblWidgets[i].RowClip = w.data.vdata[i].rawChunks w.tblWidgets[i].AllClip = w.data.vdata[i].rawChunks } } //====================================================================== type iMatcher interface { RegexMatches() int // the number of times the regex matchesd in the data SetRegexOccurrence(i int) // highlight the ith occurrence of the match SetRegex(re *regexp.Regexp) // use this regular expression } // A utility to find the regex matching widget within the stream reassembly table // widget hierarchy. func findMatcher(w gowid.IWidget) iMatcher { res := gowid.FindInHierarchy(w, true, gowid.WidgetPredicate(func(w gowid.IWidget) bool { var res bool if _, ok := w.(iMatcher); ok { res = true } return res })) if res == nil { return nil } else { return res.(iMatcher) } } func (w *Widget) TrackPayloadPacket(packet int) { w.data.pktIndices = append(w.data.pktIndices, packet) } func (w *Widget) NumChunks() int { return len(w.data.vdata[Entire].hexChunks.chunks) } func (w *Widget) Finished() bool { return w.data.finished } func (w *Widget) SetFinished(f bool) { w.data.finished = f } func (w *Widget) SetFocusOnChunksIfPossible(app gowid.IApp) { if w.NumChunks() == 0 { w.sections.SetFocus(app, 4) } else { w.sections.SetFocus(app, 1) } } func (w *Widget) SetCurrentRow(row table.Position) { w.tblWidgets[w.selectedConv].SetCurrentRow(row) } func (w *Widget) GoToMiddle(app gowid.IApp) { w.tblWidgets[w.selectedConv].GoToMiddle(app) } // Hardcoded - yuck! func setFocusOnSearchBox(app gowid.IApp, view gowid.IWidget) { gowid.SetFocusPath(view, []interface{}{2, 5}, app) } func streamViewKeyPressAfter(sections *pile.Widget, evk *tcell.EventKey, app gowid.IApp) bool { handled := false if evk.Key() == tcell.KeyTAB { if next, ok := sections.FindNextSelectable(gowid.Forwards, true); ok { sections.SetFocus(app, next) handled = true } } else if evk.Key() == tcell.KeyBacktab { if next, ok := sections.FindNextSelectable(gowid.Backwards, true); ok { sections.SetFocus(app, next) handled = true } } else if evk.Rune() == '/' { setFocusOnSearchBox(app, sections) handled = true } return handled } func (w *Widget) String() string { return "streamreassembly" } func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { return w.IWidget.UserInput(ev, size, focus, app) } func (w *Widget) AddHeader(hdr streams.FollowHeader) { w.streamHeader = hdr } func (w *Widget) MapChunkToTableRow(chunk int) (int, error) { if chunk < len(w.data.vdata[w.selectedConv].subIndices) { gchunk := w.data.vdata[w.selectedConv].subIndices[chunk] if gchunk < len(w.data.pktIndices) { return w.data.pktIndices[gchunk], nil } } err := gowid.WithKVs(PacketRowNotLoadedError, map[string]interface{}{ "row": chunk, }) return -1, err } func (w *Widget) AddChunkEntire(ch streams.IChunk, app gowid.IApp) { dir := ch.Direction() if w.stats.lastTurn != dir && w.stats.started { w.stats.turns++ } w.stats.lastTurn = dir w.stats.started = true switch dir { case streams.Client: w.stats.clientPackets++ w.stats.clientBytes += len(ch.StreamData()) case streams.Server: w.stats.serverPackets++ w.stats.serverBytes += len(ch.StreamData()) } w.data.vdata[Entire].hexChunks.chunks = append(w.data.vdata[Entire].hexChunks.chunks, ch) w.data.vdata[Entire].update() w.data.vdata[Entire].subIndices = append(w.data.vdata[Entire].subIndices, w.data.currentChunk) switch dir { case streams.Client: w.data.vdata[ClientOnly].hexChunks.chunks = append(w.data.vdata[ClientOnly].hexChunks.chunks, ch) w.data.vdata[ClientOnly].update() w.data.vdata[ClientOnly].subIndices = append(w.data.vdata[ClientOnly].subIndices, w.data.currentChunk) case streams.Server: w.data.vdata[ServerOnly].hexChunks.chunks = append(w.data.vdata[ServerOnly].hexChunks.chunks, ch) w.data.vdata[ServerOnly].update() w.data.vdata[ServerOnly].subIndices = append(w.data.vdata[ServerOnly].subIndices, w.data.currentChunk) } // Update the copymodetable's data - otherwise the slice is stale for i := 0; i < len(w.tblWidgets); i++ { // Loop over all conmv views - entire, client, server w.updateChunkModel(i, w.displayAs, app) } w.updateConvMenuWidget(app) w.data.currentChunk++ } //====================================================================== type iClickIsActive interface { clickIsActive() bool } type iHighlight interface { highlightThis(pos table.Position) regexstyle.Highlight } type IFilterOut interface { PreviousFilter() string DisplayFilter() string } type IOnError interface { OnError(msg string, app gowid.IApp) } type iMapChunkToTableRow interface { MapChunkToTableRow(chunk int) (int, error) } // Supplied by user of widget - what UI changes to make when packet is clicked type IChunkClicked interface { OnPacketClicked(pkt int, app gowid.IApp) error HandleError(row table.RowId, err error, app gowid.IApp) } // Used by widget - first map table click to packet number, then use IChunkClicked type iChunkClicker interface { IChunkClicked iClickIsActive iMapChunkToTableRow iHighlight } type chunkList struct { clicker iChunkClicker chunks []streams.IChunk } type asciiChunkList struct { *chunkList } type rawChunkList struct { *chunkList } var _ table.IBoundedModel = chunkList{} var _ table.IBoundedModel = asciiChunkList{} var _ table.IBoundedModel = rawChunkList{} var _ copymodetable.IRowCopier = chunkList{} var _ copymodetable.IRowCopier = asciiChunkList{} var _ copymodetable.IRowCopier = rawChunkList{} var _ copymodetable.ITableCopier = chunkList{} var _ copymodetable.ITableCopier = asciiChunkList{} var _ copymodetable.ITableCopier = rawChunkList{} // CopyTable is here to implement copymodetable.IRowCopier func (c chunkList) CopyRow(rowid table.RowId) []gowid.ICopyResult { hexd := format.HexDump(c.chunks[int(rowid)].StreamData()) return []gowid.ICopyResult{ gowid.CopyResult{ Name: "Copy hexdump", Val: hexd, }, } } func (c asciiChunkList) CopyRow(rowid table.RowId) []gowid.ICopyResult { prt := format.MakePrintableStringWithNewlines(c.chunks[int(rowid)].StreamData()) return []gowid.ICopyResult{ gowid.CopyResult{ Name: "Copy ascii", Val: prt, }, } } func (c rawChunkList) CopyRow(rowid table.RowId) []gowid.ICopyResult { raw := format.MakeHexStream(c.chunks[int(rowid)].StreamData()) return []gowid.ICopyResult{ gowid.CopyResult{ Name: "Copy raw", Val: raw, }, } } // CopyTable is here to implement copymodetable.ITableCopier func (c asciiChunkList) CopyTable() []gowid.ICopyResult { prtl := make([]string, 0, len(c.chunks)) for i := 0; i < len(c.chunks); i++ { prtl = append(prtl, format.MakePrintableStringWithNewlines(c.chunks[i].StreamData())) } prt := strings.Join(prtl, "\n") return []gowid.ICopyResult{ gowid.CopyResult{ Name: "Copy ascii", Val: prt, }, } } // CopyTable is here to implement copymodetable.ITableCopier func (c chunkList) CopyTable() []gowid.ICopyResult { hexdl := make([]string, 0, len(c.chunks)) for i := 0; i < len(c.chunks); i++ { hex := format.HexDump(c.chunks[i].StreamData()) if c.chunks[i].Direction() == streams.Server { hex = indentRe.ReplaceAllString(hex, ` $1`) } hexdl = append(hexdl, hex) } hexd := strings.Join(hexdl, "\n") return []gowid.ICopyResult{ gowid.CopyResult{ Name: "Copy hexdump", Val: hexd, }, } } // CopyTable is here to implement copymodetable.ITableCopier func (c rawChunkList) CopyTable() []gowid.ICopyResult { rawl := make([]string, 0, len(c.chunks)) for i := 0; i < len(c.chunks); i++ { rawl = append(rawl, format.MakeHexStream(c.chunks[i].StreamData())) } raw := strings.Join(rawl, "\n") return []gowid.ICopyResult{ gowid.CopyResult{ Name: "Copy raw", Val: raw, }, } } // makeButton constructs a row for the stream list that if clicked will select the // appropriate packet in the packet list func (c chunkList) makeButton(row table.RowId, ch gowid.IWidget) *button.Widget { btn := button.NewBare(ch) //btn.OnClickDown(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, widget gowid.IWidget) { btn.OnClick(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, widget gowid.IWidget) { if c.clicker != nil && c.clicker.clickIsActive() { if irow, err := c.clicker.MapChunkToTableRow(int(row)); err != nil { c.clicker.HandleError(row, err, app) } else { c.clicker.OnPacketClicked(irow, app) } } }), ) return btn } func (c chunkList) CellWidgets(row table.RowId) []gowid.IWidget { res := make([]gowid.IWidget, 1) var ch gowid.IWidget // not sorted hilite := c.clicker.highlightThis(table.Position(row)) datastr := format.HexDump(c.chunks[row].StreamData()) if c.chunks[row].Direction() == streams.Server { datastr = indentRe.ReplaceAllString(datastr, ` $1`) } dataw := framefocus.New( selectable.New( regexstyle.New( text.New(datastr), hilite, ), ), ) if c.chunks[row].Direction() == streams.Client { ch = styled.New( dataw, gowid.MakePaletteRef("stream-client"), ) } else { ch = styled.New( dataw, gowid.MakePaletteRef("stream-server"), ) } res[0] = c.makeButton(row, ch) return res } //====================================================================== func (c asciiChunkList) CellWidgets(row table.RowId) []gowid.IWidget { res := make([]gowid.IWidget, 1) hl := c.clicker.highlightThis(table.Position(row)) str := framefocus.NewSlim( selectable.New( regexstyle.New( text.New(strings.TrimSuffix(format.MakePrintableStringWithNewlines((*c.chunkList).chunks[row].StreamData()), "\n")), hl, ), ), ) var ch gowid.IWidget if (*c.chunkList).chunks[row].Direction() == streams.Client { ch = styled.New( str, gowid.MakePaletteRef("stream-client"), ) } else { ch = styled.New( str, gowid.MakePaletteRef("stream-server"), ) } res[0] = c.makeButton(row, ch) return res } func (c rawChunkList) CellWidgets(row table.RowId) []gowid.IWidget { res := make([]gowid.IWidget, 1) hl := c.clicker.highlightThis(table.Position(row)) str := framefocus.New( selectable.New( regexstyle.New( text.New(format.MakeHexStream((*c.chunkList).chunks[row].StreamData())), hl, ), ), ) var ch gowid.IWidget if (*c.chunkList).chunks[row].Direction() == streams.Client { ch = styled.New( str, gowid.MakePaletteRef("stream-client"), ) } else { ch = styled.New( str, gowid.MakePaletteRef("stream-server"), ) } res[0] = c.makeButton(row, ch) return res } func (c asciiChunkList) Widths() []gowid.IWidgetDimension { return []gowid.IWidgetDimension{gowid.RenderWithWeight{W: 1}} } func (c chunkList) Widths() []gowid.IWidgetDimension { return []gowid.IWidgetDimension{gowid.RenderWithWeight{W: 1}} } func (c chunkList) Columns() int { return 1 } func (c chunkList) Rows() int { return len(c.chunks) } func (c chunkList) HorizontalSeparator() gowid.IWidget { return nil } func (c chunkList) HeaderSeparator() gowid.IWidget { return nil } func (c chunkList) HeaderWidgets() []gowid.IWidget { return nil } func (c chunkList) VerticalSeparator() gowid.IWidget { return nil } func (c chunkList) RowIdentifier(row int) (table.RowId, bool) { if row < 0 || row >= len(c.chunks) { return -1, false } return table.RowId(row), true } //====================================================================== // TODO - duplicated from termshark 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 searchState struct { searchReTxt string searchRe *regexp.Regexp searchRow table.Position searchOccurrence int maxOccurrences gwutil.IntOption } func (s *searchState) initSearch(re *regexp.Regexp, txt string) { s.searchReTxt = txt s.searchRe = re s.searchRow = 0 } func (s *searchState) goToSearchRow(row table.Position) { s.searchRow = row s.searchOccurrence = 0 s.maxOccurrences = gwutil.NoneInt() } func (s *searchState) goToNextSearchRow() { s.goToSearchRow(s.searchRow + 1) } func (w searchState) String() string { return fmt.Sprintf("[re='%s' row=%d occ=%d maxocc=%v]", w.searchReTxt, w.searchRow, w.searchOccurrence, w.maxOccurrences) } //====================================================================== // Represents the view of the data from either both sides, client side or server side type ViewData struct { subIndices []int // [0,1,2,3,...] - index into pktIndices hexChunks chunkList asciiChunks asciiChunkList rawChunks rawChunkList } func newViewData(clicker IChunkClicked, ca iClickIsActive, mapper iMapChunkToTableRow, hiliter iHighlight) *ViewData { clickMapper := struct { IChunkClicked iClickIsActive iMapChunkToTableRow iHighlight }{ IChunkClicked: clicker, iClickIsActive: ca, iMapChunkToTableRow: mapper, iHighlight: hiliter, } res := &ViewData{ subIndices: make([]int, 0, 16), hexChunks: chunkList{ clicker: clickMapper, chunks: make([]streams.IChunk, 0, 16), }, } res.update() return res } func (v *ViewData) update() { v.asciiChunks = asciiChunkList{ chunkList: &v.hexChunks, } v.rawChunks = rawChunkList{ chunkList: &v.hexChunks, } } //====================================================================== // Represents all the streamed data type Data struct { pktIndices []int // [0,2,5,12...] - frame numbers (-1) for each packet of this stream vdata []*ViewData // for each of (a) whole view (b) client (c) server currentChunk int // add to client or server view finished bool } func newData(clicker IChunkClicked, ca iClickIsActive, mapper iMapChunkToTableRow, hiliter iHighlight) *Data { vdata := make([]*ViewData, 0, 3) for i := 0; i < 3; i++ { vdata = append(vdata, newViewData(clicker, ca, mapper, hiliter)) } res := &Data{ pktIndices: make([]int, 0, 16), vdata: vdata, } return res } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.2.0/widgets/trackfocus/000077500000000000000000000000001377442047300173405ustar00rootroot00000000000000termshark-2.2.0/widgets/trackfocus/trackfocus.go000066400000000000000000000040701377442047300220340ustar00rootroot00000000000000// Copyright 2019-2021 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 trackfocus provides a widget that issues callbacks when a widget loses or gains the focus. package trackfocus import ( "github.com/gcla/gowid" ) //====================================================================== type Widget struct { gowid.IWidget init bool last bool cb *gowid.Callbacks } func New(w gowid.IWidget) *Widget { return &Widget{ IWidget: w, cb: gowid.NewCallbacks(), } } // Markers to track the callbacks being added. These just need to be distinct // from other markers. type FocusLostCB struct{} type FocusGainedCB struct{} // Boilerplate to make the widget provide methods to add and remove callbacks. func (w *Widget) OnFocusLost(f gowid.IWidgetChangedCallback) { gowid.AddWidgetCallback(w.cb, FocusLostCB{}, f) } func (w *Widget) RemoveOnFocusLost(f gowid.IIdentity) { gowid.RemoveWidgetCallback(w.cb, FocusLostCB{}, f) } func (w *Widget) OnFocusGained(f gowid.IWidgetChangedCallback) { gowid.AddWidgetCallback(w.cb, FocusGainedCB{}, f) } func (w *Widget) RemoveOnFocusGained(f gowid.IIdentity) { gowid.RemoveWidgetCallback(w.cb, FocusGainedCB{}, f) } func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { res := w.IWidget.Render(size, focus, app) if w.init && focus.Focus != w.last { if focus.Focus { gowid.RunWidgetCallbacks(w.cb, FocusGainedCB{}, app, w) } else { gowid.RunWidgetCallbacks(w.cb, FocusLostCB{}, app, w) } } w.init = true w.last = focus.Focus return res } // Provide IComposite and ISettableComposite. This makes the widget cooperate with general // utilities that walk the widget hierarchy, like FocusPath(). func (w *Widget) SubWidget() gowid.IWidget { return w.IWidget } func (w *Widget) SetSubWidget(wi gowid.IWidget, app gowid.IApp) { w.IWidget = wi } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.2.0/widgets/trackfocus/trackfocus_test.go000066400000000000000000000022671377442047300231010ustar00rootroot00000000000000// Copyright 2019-2021 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 trackfocus import ( "testing" "github.com/gcla/gowid" "github.com/gcla/gowid/gwtest" "github.com/gcla/gowid/widgets/text" "github.com/stretchr/testify/assert" ) func TestTrackFocus1(t *testing.T) { tw := text.New("foobar") ftw := New(tw) c := ftw.Render(gowid.RenderFixed{}, gowid.Focused, gwtest.D) cbran := false ftw.OnFocusLost(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, w gowid.IWidget) { cbran = true })) assert.Equal(t, "foobar", c.String()) ftw.Render(gowid.RenderFixed{}, gowid.Focused, gwtest.D) assert.Equal(t, false, cbran) ftw.Render(gowid.RenderFixed{}, gowid.NotSelected, gwtest.D) assert.Equal(t, true, cbran) cbran = false ftw.Render(gowid.RenderFixed{}, gowid.Focused, gwtest.D) assert.Equal(t, false, cbran) ftw.RemoveOnFocusLost(gowid.CallbackID{"cb"}) ftw.Render(gowid.RenderFixed{}, gowid.NotSelected, gwtest.D) assert.Equal(t, false, cbran) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.2.0/widgets/utils.go000066400000000000000000000017361377442047300166720ustar00rootroot00000000000000// Copyright 2019-2021 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 widgets import ( "github.com/gcla/gowid" "github.com/gdamore/tcell" ) //====================================================================== 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 } 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 case tcell.KeyRune: switch ev.Rune() { case 'h', 'j', 'k', 'l': res = true } } return res } //====================================================================== // Local Variables: // mode: Go // fill-column: 78 // End: termshark-2.2.0/widgets/withscrollbar/000077500000000000000000000000001377442047300200535ustar00rootroot00000000000000termshark-2.2.0/widgets/withscrollbar/withscrollbar.go000066400000000000000000000145001377442047300232610ustar00rootroot00000000000000// Copyright 2019-2021 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/list" "github.com/gcla/gowid/widgets/selectable" "github.com/gcla/gowid/widgets/table" "github.com/gcla/gowid/widgets/vscroll" "github.com/gdamore/tcell" ) //====================================================================== type Widget struct { always *columns.Widget // use if scrollbar is to be shown w IScrollSubWidget sb *vscroll.Widget goUpDown int // positive means down pgUpDown int // positive means down frac float32 // positive means down fracSet bool opt Options } var _ gowid.IWidget = (*Widget)(nil) type Options struct { HideIfContentFits bool } type IScrollValues interface { ScrollPosition() int ScrollLength() int } // Implemented by widgets that can scroll type IScrollOneLine interface { Up(lines int, size gowid.IRenderSize, app gowid.IApp) Down(lines int, size gowid.IRenderSize, app gowid.IApp) } type IScrollOnePage interface { UpPage(num int, size gowid.IRenderSize, app gowid.IApp) DownPage(num int, size gowid.IRenderSize, app gowid.IApp) } type IScrollHome interface { GoHome(size gowid.IRenderSize, app gowid.IApp) } type IScrollToEnd interface { GoToEnd(size gowid.IRenderSize, app gowid.IApp) } type IScrollSubWidget interface { gowid.IWidget IScrollValues IScrollOneLine IScrollOnePage } func New(w IScrollSubWidget, opts ...Options) *Widget { var opt Options if len(opts) > 0 { opt = opts[0] } sb := vscroll.NewExt(vscroll.VerticalScrollbarUnicodeRunes) res := &Widget{ always: 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, opt: opt, } sb.OnClickAbove(gowid.MakeWidgetCallback("cb", res.clickUp)) sb.OnClickBelow(gowid.MakeWidgetCallback("cb", res.clickDown)) sb.OnRightClick(gowid.MakeWidgetCallbackExt("cb", res.rightClick)) 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) rightClick(app gowid.IApp, w gowid.IWidget, data ...interface{}) { frac := data[0].(float32) e.frac = frac e.fracSet = true } 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 } // Don't attempt to calculate actual rendered rows - it's terribly slow, and O(n) rows. func CalculateMenuRows(vals IScrollValues, rows int, focus gowid.Selector, app gowid.IApp) (int, int, int) { return vals.ScrollPosition(), 1, vals.ScrollLength() - (vals.ScrollPosition() + 1) } func (w *Widget) contentFits(size gowid.IRenderSize) bool { res := true if rower, ok := size.(gowid.IRows); ok { res = (w.w.ScrollLength() <= rower.Rows()) } return res } func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { if w.opt.HideIfContentFits && w.contentFits(size) { return w.w.UserInput(ev, size, focus, app) } box, ok := size.(gowid.IRenderBox) if !ok { panic(gowid.WidgetSizeError{Widget: w, Size: size, Required: "gowid.IRenderBox"}) } x, y, z := CalculateMenuRows(w.w, box.BoxRows(), focus, app) w.sb.Top = x w.sb.Middle = y w.sb.Bottom = z if ws, ok := w.w.(IScrollOnePage); ok { if ev, ok := ev.(*tcell.EventKey); ok { switch ev.Key() { case tcell.KeyPgUp: ws.UpPage(1, size, app) return true case tcell.KeyPgDn: ws.DownPage(1, size, app) return true } } } if ws, ok := w.w.(IScrollHome); ok { if ev, ok := ev.(*tcell.EventKey); ok { switch ev.Key() { case tcell.KeyHome: ws.GoHome(size, app) return true } } } if ws, ok := w.w.(IScrollToEnd); ok { if ev, ok := ev.(*tcell.EventKey); ok { switch ev.Key() { case tcell.KeyEnd: ws.GoToEnd(size, app) return true } } } res := w.always.UserInput(ev, size, focus, app) if res { w.always.SetFocus(app, 0) } return res } type iSetPosition interface { SetPos(pos list.IBoundedWalkerPosition, app gowid.IApp) } func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { if w.opt.HideIfContentFits && w.contentFits(size) { return w.w.Render(size, focus, app) } var box gowid.IRenderBox var ok bool box, ok = size.(gowid.IRenderBox) if !ok { box = w.always.Render(size, focus, app) } ecols := box.BoxColumns() - 1 var x int var y int var z int if ecols >= 1 { ebox := gowid.MakeRenderBox(ecols, box.BoxRows()) if w.goUpDown != 0 || w.pgUpDown != 0 || w.fracSet { 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) } if w.fracSet { if wp, ok := w.w.(iSetPosition); ok { wp.SetPos(table.Position(int(float32(w.w.ScrollLength()-1)*w.frac)), app) } w.fracSet = false } } w.goUpDown = 0 w.pgUpDown = 0 x, y, z = CalculateMenuRows(w.w, box.BoxRows(), focus, app) } w.sb.Top = x w.sb.Middle = y w.sb.Bottom = z canvas := w.always.Render(size, focus, app) return canvas } func (w *Widget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { if w.opt.HideIfContentFits && w.contentFits(size) { return w.w.RenderSize(size, focus, app) } return w.always.RenderSize(size, focus, app) } func (w *Widget) Selectable() bool { return w.w.Selectable() } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.2.0/widgets/withscrollbar/withscrollbar_test.go000066400000000000000000000034541377442047300243260ustar00rootroot00000000000000package withscrollbar import ( "fmt" "strings" "testing" "github.com/gcla/gowid" "github.com/gcla/gowid/gwtest" "github.com/gcla/gowid/widgets/button" "github.com/gcla/gowid/widgets/list" "github.com/gcla/gowid/widgets/text" "github.com/stretchr/testify/assert" ) type scrollingListBox struct { *list.Widget } func (t *scrollingListBox) Up(lines int, size gowid.IRenderSize, app gowid.IApp) {} func (t *scrollingListBox) Down(lines int, size gowid.IRenderSize, app gowid.IApp) {} func (t *scrollingListBox) UpPage(num int, size gowid.IRenderSize, app gowid.IApp) {} func (t *scrollingListBox) DownPage(num int, size gowid.IRenderSize, app gowid.IApp) {} func (t *scrollingListBox) ScrollLength() int { return 8 } func (t *scrollingListBox) ScrollPosition() int { return 0 } func Test1(t *testing.T) { bws := make([]gowid.IWidget, 8) for i := 0; i < len(bws); i++ { bws[i] = button.NewBare(text.New(fmt.Sprintf("%03d", i))) } walker := list.NewSimpleListWalker(bws) lbox := &scrollingListBox{Widget: list.New(walker)} sbox := New(lbox) canvas1 := sbox.Render(gowid.MakeRenderBox(4, 8), gowid.NotSelected, gwtest.D) res := strings.Join([]string{ "000▲", "001█", "002 ", "003 ", "004 ", "005 ", "006 ", "007▼", }, "\n") assert.Equal(t, res, canvas1.String()) sbox = New(lbox, Options{ HideIfContentFits: true, }) canvas1 = sbox.Render(gowid.MakeRenderBox(4, 8), gowid.NotSelected, gwtest.D) res = strings.Join([]string{ "000 ", "001 ", "002 ", "003 ", "004 ", "005 ", "006 ", "007 ", }, "\n") assert.Equal(t, res, canvas1.String()) canvas1 = sbox.Render(gowid.MakeRenderBox(4, 5), gowid.NotSelected, gwtest.D) res = strings.Join([]string{ "000▲", "001█", "002 ", "003 ", "004▼", }, "\n") assert.Equal(t, res, canvas1.String()) }