pax_global_header00006660000000000000000000000064136004416300014507gustar00rootroot0000000000000052 comment=802bae4ea015e0cecb309b4f57dcbf4f60ac4b5d termshark-2.0.3/000077500000000000000000000000001360044163000135115ustar00rootroot00000000000000termshark-2.0.3/.all-contributorsrc000066400000000000000000000215221360044163000173440ustar00rootroot00000000000000{ "projectName": "termshark", "projectOwner": "gcla", "repoType": "github", "repoHost": "https://github.com", "files": [ "README.md" ], "imageSize": 100, "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" ] }, { "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" ] } ], "contributorsPerLine": 7 } termshark-2.0.3/.github/000077500000000000000000000000001360044163000150515ustar00rootroot00000000000000termshark-2.0.3/.github/ISSUE_TEMPLATE/000077500000000000000000000000001360044163000172345ustar00rootroot00000000000000termshark-2.0.3/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000022171360044163000217300ustar00rootroot00000000000000--- 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.0.3/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000011251360044163000227600ustar00rootroot00000000000000--- 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.0.3/.github/workflows/000077500000000000000000000000001360044163000171065ustar00rootroot00000000000000termshark-2.0.3/.github/workflows/go.yml000066400000000000000000000014051360044163000202360ustar00rootroot00000000000000name: 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.0.3/.gitignore000066400000000000000000000000701360044163000154760ustar00rootroot00000000000000dist/ .vscode/ *~ /cmd/termshark/termshark /typescript termshark-2.0.3/.goreleaser.yml000066400000000000000000000015411360044163000164430ustar00rootroot00000000000000# This is an example goreleaser.yaml file with some sane defaults. # Make sure to check the documentation at http://goreleaser.com before: hooks: builds: - env: - CGO_ENABLED=0 - GO111MODULE=on main: ./cmd/termshark/termshark.go goos: - freebsd - windows - linux - darwin goarch: - arm - amd64 ignore: - goos: darwin goarch: arm - goos: freebsd goarch: arm - goos: windows goarch: arm archives: - replacements: darwin: macOS linux: linux windows: windows amd64: x64 wrap_in_directory: true format_overrides: - goos: windows format: zip files: - none* sign: artifacts: checksum checksum: name_template: 'checksums.txt' snapshot: name_template: "{{ .Env.TERMSHARK_GIT_DESCRIBE }}" changelog: sort: asc filters: exclude: - '^docs:' - '^test:' termshark-2.0.3/.travis.yml000066400000000000000000000060031360044163000156210ustar00rootroot00000000000000language: 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.11.x - 1.12.x - 1.13.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 addons: apt: update: true script: - go test -v ./... 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.13.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.13.x" termshark-2.0.3/CHANGELOG.md000066400000000000000000000050641360044163000153270ustar00rootroot00000000000000# Changelog ## [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.0.0...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 termshark-2.0.3/LICENSE000066400000000000000000000020701360044163000145150ustar00rootroot00000000000000The 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.0.3/README.md000066400000000000000000000341301360044163000147710ustar00rootroot00000000000000[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 is out now with stream reassembly, dark-mode and more! Here's the [ChangeLog](CHANGELOG.md#changelog).** ![demo2](https://drive.google.com/uc?export=view&id=1EmqYrOPwLXanoi7o74PQMOX1KSgOqhNr) If you're debugging on a remote machine with a large pcap and no desire to scp it back to your desktop, termshark can help! ## Features - Read pcap files or sniff live interfaces (where tshark is permitted). - Inspect each packet using familiar Wireshark-inspired views - Filter pcaps or live captures using Wireshark's display filters - Reassemble and inspect TCP and UDP flows - 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.11 or higher. Set `GO111MODULE=on` then run: ```bash go install 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). ## User Guide See the [termshark user guide](docs/UserGuide.md) (and my best guess at some [FAQs](docs/FAQ.md)) ## Dependencies Termshark depends on these open-source packages: - [tshark](https://www.wireshark.org/docs/man-pages/tshark.html) - command-line network protocol analyzer, part of [Wireshark](https://wireshark.org) - [tcell](https://github.com/gdamore/tcell) - a cell based terminal handling package, inspired by termbox - [gowid](https://github.com/gcla/gowid) - compositional terminal UI widgets, inspired by [urwid](http://urwid.org), built on [tcell](https://github.com/gdamore/tcell) Note that tshark is a run-time dependency, and must be in your ```PATH``` for termshark to function. Version 1.10.2 or higher is required (approx 2013). ## Contributors Thanks to everyone that's contributed ports, patches and effort!
Ross Jacobs
Ross Jacobs

💻 🐛 📓
Hongarc
Hongarc

📖
Ryan Steinmetz
Ryan Steinmetz

📦
Nicolai Søborg
Nicolai Søborg

📦
Elliott Sales de Andrade
Elliott Sales de Andrade

💻
Romanos
Romanos

💻
Denys
Denys

🐛
jerry73204
jerry73204

📦
Jon Knapp
Jon Knapp

📦
Mario Harjac
Mario Harjac

📦
Andrew Benson
Andrew Benson

🐛
sagis-tikal
sagis-tikal

🐛
punkymaniac
punkymaniac

🐛
msenturk
msenturk

🐛
Sandor Szücs
Sandor Szücs

🐛
Dawid Dziurla
Dawid Dziurla

🐛
jJit0
jJit0

🐛
inzel
inzel

🐛
thejerrod
thejerrod

🤔
gdluca
gdluca

🐛
Patrick Winter
Patrick Winter

📦
Robert Larsen
Robert Larsen

🤔 📓
MinJae Kwon
MinJae Kwon

🐛
the-c0d3r
the-c0d3r

🤔
Gisle Vanem
Gisle Vanem

🐛
hook
hook

🐛
Lennart Koopmann
Lennart Koopmann

🤔
Fernandez, ReK2
Fernandez, ReK2

🐛
mazball
mazball

🤔
wfailla
wfailla

🤔
荣怡
荣怡

🤔
thebyrdman-git
thebyrdman-git

🐛
Clemens Mosig
Clemens Mosig

🐛
Michael Rash
Michael Rash

📓
joelparker
joelparker

📓
Dragos Maftei
Dragos Maftei

🤔
## 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.0.3/cli/000077500000000000000000000000001360044163000142605ustar00rootroot00000000000000termshark-2.0.3/cli/all.go000066400000000000000000000063001360044163000153560ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // package 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 { Iface string `value-name:"" short:"i" description:"Interface to read."` Pcap flags.Filename `value-name:"" short:"r" description:"Pcap file to read."` DecodeAs []string `short:"d" description:"Specify dissection of layer type." value-name:"==,"` 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 string `long:"debug" default:"false" hidden:"true" optional:"true" optional-value:"true" choice:"true" choice:"false" 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 { FilterOrFile string `value-name:"" description:"Filter (capture for iface, display for pcap), or pcap file to read."` } `positional-args:"yes"` } // If args are passed through to tshark (e.g. stdout not a tty), then // strip these out so tshark doesn't fail. 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.0.3/cli/flags.go000066400000000000000000000014161360044163000157050ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // // +build !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.0.3/cli/flags_windows.go000066400000000000000000000014251360044163000174570ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. package 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.0.3/cmd/000077500000000000000000000000001360044163000142545ustar00rootroot00000000000000termshark-2.0.3/cmd/termshark/000077500000000000000000000000001360044163000162545ustar00rootroot00000000000000termshark-2.0.3/cmd/termshark/termshark.go000066400000000000000000001111271360044163000206060ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. package main import ( "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/cli" "github.com/gcla/termshark/v2/pcap" "github.com/gcla/termshark/v2/streams" "github.com/gcla/termshark/v2/system" "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" "gopkg.in/fsnotify.v1" "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 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...") } // 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 != "" { os.Setenv("TERM", termVar) } var psrc pcap.IPacketSource defer func() { 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 == "" && opts.Iface == "" { pcapf = string(opts.Args.FilterOrFile) // `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. Not that termux // makes a copy, so we ought to clean that up when termshark terminates. psrc = 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 psrc = 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 - psrc = pcap.FileSource{Filename: "-"} } } } else { // Add it to filter args. Figure out later if they're capture or display. filterArgs = append(filterArgs, opts.Args.FilterOrFile) } if pcapf != "" && opts.Iface != "" { fmt.Fprintf(os.Stderr, "Please supply either a pcap or an interface.\n") return 1 } // Invariant: pcap != "" XOR opts.Iface != "" if psrc == nil { switch { case pcapf != "": psrc = pcap.FileSource{Filename: pcapf} case opts.Iface != "": psrc = pcap.InterfaceSource{Iface: opts.Iface} } } // go-flags returns [""] when no extra args are provided, so I can't just // test the length of this slice argsFilter := strings.Join(filterArgs, " ") // Work out capture filter afterwards because we need to determine first // whether any potential first argument is intended as a pcap file instead of // a capture filter. captureFilter := opts.CaptureFilter if psrc.IsInterface() && 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 psrc.IsFile() { 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 } } // - means read from stdin. But termshark uses stdin for interacting with the UI. So if the // iface is -, then dup stdin to a free descriptor, adjust iface to read from that descriptor, // then open /dev/tty on stdin. newinputfd := -1 if psrc.Name() == "-" { if termshark.IsTerminal(os.Stdin.Fd()) { fmt.Fprintf(os.Stderr, "Requested pcap source is %v (\"stdin\") but stdin is a tty.\n", opts.Iface) fmt.Fprintf(os.Stderr, "Perhaps you intended to pipe packet input to termshark?\n") return 1 } if runtime.GOOS != "windows" { newinputfd, err = system.MoveStdin() if err != nil { fmt.Fprintf(os.Stderr, "%v\n", err) return 1 } psrc = pcap.PipeSource{Descriptor: fmt.Sprintf("/dev/fd/%d", newinputfd), Fd: newinputfd} } else { fmt.Fprintf(os.Stderr, "Sorry, termshark does not yet support piped input on Windows.\n") return 1 } } // Better to do a command-line error if file supplied at command-line is not found. File // won't be "-" at this point because above we switch to -i if input is "-" // We haven't distinguished between file sources and fifo sources yet. So IsFile() will be true // even if argument is a fifo if psrc.IsFile() { stat, err := os.Stat(psrc.Name()) if err != nil { fmt.Fprintf(os.Stderr, "Error reading file %s: %v.\n", psrc.Name(), err) return 1 } 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) psrc = 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() } } } // Here we now have an accurate view of psrc - 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) } if cli.FlagIsTrue(opts.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 opts.Iface is provided as a number, it's meant as the index of the interfaces as // per the order returned by the OS. useIface will always be the name of the interface. // See if the interface argument is an integer 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 { ifaces, 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 for i, n := range ifaces { // ("NDIS_...", 7) if i == psrc.Name() || n == ifaceIdx { gotit = true canonicalName = i break } } 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. psrc = 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 (psrc.IsInterface() || psrc.IsFifo() || psrc.IsPipe()) && startedSuccessfully { fmt.Printf("Packets read from %s have been saved in %s\n", psrc.Name(), 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", psrc.Name()) 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.DumpcapBin()) 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 } ui.PcapScheduler = pcap.NewScheduler( pcap.MakeCommands(opts.DecodeAs, tsharkArgs, pdmlArgs, psmlArgs, ui.PacketColors), pcap.Options{ CacheSize: cacheSize, PacketsPerLoad: bundleSize, }, ) ui.Loader = ui.PcapScheduler.Loader // Buffered because I might send something in this goroutine startUIChan := make(chan struct{}, 1) // Used to cancel the display of a message telling the user why there is no UI yet. detectMsgChan := make(chan struct{}, 1) var iwatcher *fsnotify.Watcher var ifaceTmpFile string if psrc.IsInterface() || psrc.IsFifo() || psrc.IsPipe() { ifaceTmpFile = pcap.TempPcapFile(psrc.Name()) iwatcher, err = fsnotify.NewWatcher() if err != nil { fmt.Fprintf(os.Stderr, "Could not start filesystem watcher: %v\n", err) return 1 } defer func() { if iwatcher != nil { iwatcher.Close() } }() // Don't start the UI until this file is created. When listening on a pipe, // termshark will start a process similar to: // // dumpcap -i /dev/fd/3 -w ~/.cache/pcaps/tmp123.pcap // // dumpcap will not actually create that file until it has data to write to it. // So we watch for the creation of that file, and until then, don't launch the UI. // Then if the feeding process needs input first e.g. sudo tcpdump needs password, // there won't be a conflict for reading /dev/tty. // if err := iwatcher.Add(termshark.PcapDir()); err != nil { //&& !os.IsNotExist(err) { fmt.Fprintf(os.Stderr, "Could not set up watcher for %s: %v\n", termshark.PcapDir(), err) return 1 } fmt.Printf("(The termshark UI will start when packets are detected...)\n") } else { // Start UI right away, reading from a file startUIChan <- struct{}{} } // 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() // 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.QuitRequestedChan <- struct{}{} } else { app.Run(gowid.RunFunction(func(app gowid.IApp) { ui.OpenError(fmt.Sprintf("Invalid filter: %s", displayFilter), app) })) } }, }, } if psrc.IsFile() { 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, app) } validator.Valid = &filter.ValidateCB{Fn: doit, App: app} validator.Validate(displayFilter) // no auto-scroll when reading a file ui.AutoScroll = false } else if psrc.IsInterface() || psrc.IsFifo() || psrc.IsPipe() { // Verifies whether or not we will be able to read from the interface (hopefully) ifaceExitCode = 0 if psrc.IsInterface() { if ifaceExitCode, ifaceErr = termshark.RunForExitCode(termshark.DumpcapBin(), "-i", psrc.Name(), "-a", "duration:1", "-w", os.DevNull); ifaceExitCode != 0 { return 1 } } ifValid := func(app gowid.IApp) { app.Run(gowid.RunFunction(func(app gowid.IApp) { ui.FilterWidget.SetValue(displayFilter, app) })) ifacePcapFilename = ifaceTmpFile ui.PcapScheduler.RequestLoadInterface(psrc, captureFilter, displayFilter, ifaceTmpFile, pcap.HandlerList{ ui.MakeSaveRecents("", displayFilter, app), ui.MakePacketViewUpdater(app), ui.MakeUpdateCurrentCaptureInTitle(app), ui.ManageStreamCache{}, }, ) } validator.Valid = &filter.ValidateCB{Fn: ifValid, App: app} validator.Validate(displayFilter) } quitRequested := false quitIssuedToApp := false prevstate := ui.Loader.State() var prev float64 progTicker := time.NewTicker(time.Duration(200) * time.Millisecond) loaderPsmlFinChan := ui.Loader.PsmlFinishedChan loaderIfaceFinChan := ui.Loader.IfaceFinishedChan loaderPdmlFinChan := ui.Loader.Stage2FinishedChan ctrlzLineDisc := tty.TerminalSignals{} Loop: for { var opsChan <-chan pcap.RunFn var tickChan <-chan time.Time var emptyStructViewChan <-chan time.Time var emptyHexViewChan <-chan time.Time var psmlFinChan <-chan struct{} var ifaceFinChan <-chan struct{} var pdmlFinChan <-chan struct{} var tmpPcapWatcherChan <-chan fsnotify.Event var tmpPcapWatcherErrorsChan <-chan error var tcellEvents <-chan tcell.Event 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, if ui.EmptyStructViewTimer != nil { emptyStructViewChan = ui.EmptyStructViewTimer.C } // For setting hex views empty if ui.EmptyHexViewTimer != nil { emptyHexViewChan = ui.EmptyHexViewTimer.C } // This should really be moved to a handler... if ui.Loader.State() == 0 { if prevstate != 0 { // 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) prev = 0.0 } if 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 } } } if ui.Loader.State()&(pcap.LoadingPdml|pcap.LoadingPsml) != 0 { tickChan = progTicker.C // progress is only enabled when a pcap may be loading } if ui.Loader.State()&pcap.LoadingPdml != 0 { pdmlFinChan = loaderPdmlFinChan } if ui.Loader.State()&pcap.LoadingPsml != 0 { psmlFinChan = loaderPsmlFinChan } if ui.Loader.State()&pcap.LoadingIface != 0 { ifaceFinChan = loaderIfaceFinChan } // (User) operations are enabled by default (the test predicate is nil), or if the predicate returns true // meaning the operation has reached its desired state. Only one operation can be in progress at a time. if ui.PcapScheduler.IsEnabled() { opsChan = ui.PcapScheduler.OperationsChan } // This tracks a temporary pcap file which is populated by dumpcap when termshark is // reading from a fifo. If iwatcher is nil, it means we've got data and don't need to // monitor any more. if iwatcher != nil { tmpPcapWatcherChan = iwatcher.Events tmpPcapWatcherErrorsChan = iwatcher.Errors } // Only process tcell and gowid events if the UI is running. if ui.Running { tcellEvents = app.TCellEvents } afterRenderEvents = app.AfterRenderEvents prevstate = ui.Loader.State() select { case we := <-tmpPcapWatcherChan: if strings.Contains(we.Name, ifaceTmpFile) { log.Infof("Pcap file %v has appeared - launching UI", we.Name) iwatcher.Close() iwatcher = nil startUIChan <- struct{}{} } case err := <-tmpPcapWatcherErrorsChan: fmt.Fprintf(os.Stderr, "Unexpected watcher error for %s: %v", ifaceTmpFile, err) return 1 case <-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 } // 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 close(startUIChan) startUIChan = nil // make sure it's not triggered again close(detectMsgChan) // don't display the message about waiting for the UI 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 if ui.StreamLoader != nil { ui.StreamLoader.SuppressErrors = true } ui.Loader.Close() appRunner.Stop() app.Close() ui.Running = false }() case <-ui.QuitRequestedChan: quitRequested = true if ui.Loader.State() != 0 { // 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. ui.PcapScheduler.RequestStopLoad(ui.NoHandlers{}) } 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 cli.FlagIsTrue(opts.Debug) { termshark.ProfileCPUFor(20) } else { log.Infof("SIGUSR1 ignored by termshark - see the --debug flag") } } else if system.IsSigUSR2(sig) { if cli.FlagIsTrue(opts.Debug) { termshark.ProfileHeap() } else { log.Infof("SIGUSR2 ignored by termshark - see the --debug flag") } } else { log.Infof("Starting termination via signal %v", sig) ui.QuitRequestedChan <- struct{}{} } case fn := <-opsChan: // We run the requested operation - because operations are now enabled, since this channel // is listening - and the result tells us when operations can be re-enabled (i.e. the target // state of the operation just started, for example). This means we can let an operation // "complete", moving through a sequence of states to the final state, befpre accepting // another request. fn() case <-ui.CacheRequestsChan: ui.CacheRequests = pcap.ProcessPdmlRequests(ui.CacheRequests, ui.Loader, struct { ui.SetNewPdmlRequests ui.SetStructWidgets }{ ui.SetNewPdmlRequests{ui.PcapScheduler}, ui.SetStructWidgets{ui.Loader, app}, }) case <-ifaceFinChan: // this state change only happens if the load from the interface is explicitly // stopped by the user (e.g. the stop button). When the current data has come // from loading from an interface, when stopped we still want to be able to filter // on that data. So the load routines should treat it like a regular pcap // (until the interface is started again). That means the psml reader should read // from the file and not the fifo. loaderIfaceFinChan = ui.Loader.IfaceFinishedChan ui.Loader.SetState(ui.Loader.State() & ^pcap.LoadingIface) case <-psmlFinChan: if ui.Loader.LoadWasCancelled { // Don't reset cancel state here. If, after stopping an interface load, I // apply a filter, I need to know if the load was cancelled previously because // if it was cancelled, I need to load from the temp pcap; if not cancelled, // (meaning still running), then I just apply a new filter and have the pcap // reader read from the fifo. Only do this if the user isn't quitting the app, // otherwise it looks clumsy. if !quitRequested { app.Run(gowid.RunFunction(func(app gowid.IApp) { ui.OpenError("Loading was cancelled.", app) })) } } // Reset loaderPsmlFinChan = ui.Loader.PsmlFinishedChan ui.Loader.SetState(ui.Loader.State() & ^pcap.LoadingPsml) case <-pdmlFinChan: loaderPdmlFinChan = ui.Loader.Stage2FinishedChan ui.Loader.SetState(ui.Loader.State() & ^pcap.LoadingPdml) case <-tickChan: if system.HaveFdinfo && (ui.Loader.State() == pcap.LoadingPdml || !ui.Loader.ReadingFromFifo()) { app.Run(gowid.RunFunction(func(app gowid.IApp) { prev = ui.UpdateProgressBarForFile(ui.Loader, prev, app) })) } else { app.Run(gowid.RunFunction(func(app gowid.IApp) { ui.UpdateProgressBarForInterface(ui.Loader, app) })) } case <-emptyStructViewChan: app.Run(gowid.RunFunction(func(app gowid.IApp) { ui.SetStructViewMissing(app) ui.StopEmptyStructViewTimer() })) case <-emptyHexViewChan: app.Run(gowid.RunFunction(func(app gowid.IApp) { ui.SetHexViewMissing(app) ui.StopEmptyHexViewTimer() })) case ev := <-tcellEvents: app.HandleTCellEvent(ev, gowid.IgnoreUnhandledInput) 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(): ui.UpdateRecentMenu(app) } } return 0 } //====================================================================== // Local Variables: // mode: Go // fill-column: 78 // End: termshark-2.0.3/configs/000077500000000000000000000000001360044163000151415ustar00rootroot00000000000000termshark-2.0.3/configs/termshark-dd01307f2423.json.enc000066400000000000000000000044001360044163000222370ustar00rootroot00000000000000c*Ƅ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 limited... By default, termshark respects the ```TERM``` environment variable and chooses a color scheme based on what it thinks the terminal is capable of, via the excellent [tcell](https://github.com/gdamore/tcell) package. You might be running on a terminal that can display more colors than ```TERM``` reports - so you can try adjusting your ```TERM``` variable e.g. if ```TERM``` is ```xterm```, try ```bash export TERM=xterm-256color ``` or even ```bash export TERM=xterm-truecolor ``` then re-run termshark. tcell makes use of the environment variable ```COLORTERM``` when determining how to emit color codes. If ```COLORTERM``` is set to ```truecolor```, then tcell will emit truecolor color codes when the application changes the foreground or background color. If you connect to a remote machine with ssh to run termshark, the ```COLORTERM``` variable will not be forwarded. If that leaves you with ```TERM=xterm``` for example, then termshark, via tcell, will fall back to 8-color support. Here again you can change ```TERM``` or add a setting for ```COLORTERM``` to your remote ```.bashrc``` file. If you run termshark under tmux or screen and always have ```TERM``` set in a way that doesn't make full use of your terminal emulator, you can configure termshark to always override it. Add the following to your `termshark.toml` file: ```toml [main] term = "screen-256color" ``` ## 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](https://drive.google.com/uc?export=view&id=1tYTiSdcQtsSRFmw0nw7awmL9MOZiUinM) ## How does termshark use tshark? Termshark uses tshark to provide all the data it displays, and to validate display filter expressions. When you give termshark a pcap file, it will run ```bash tshark -T psml -r my.pcap -Y '' -o gui.column.format:\"...\"``` ``` to generate the packet list data. Note that the columns are currently unconfigurable (future work...) Let's say the user is focused on packet number 1234. Then termshark will load packet structure and hex/byte data using commands like: ```bash tshark -T pdml -r my.pcap -Y ' and frame.number >= 1000 and frame.number < 2000' tshark -F pcap -r my.pcap -Y ' and frame.number >= 1000 and frame.number < 2000' -w - ``` If the user is reading from an interface, some extra processes are needed. To capture the data, termshark runs ```bash dumpcap -P -i eth0 -f -w ``` This process runs until the user hits `ctrl-c` or clicks the "Stop" button in the UI. The path to ```tmpfile``` is printed out to the user when termshark exits. Then to feed data continually to termshark, another process is started: ```bash tail -f -c +0 tmpfile ``` The stdout of the ```tail``` command is connected to the stdin of the PSML reading command, which is adjusted to: ```bash tshark -T psml -i - -l -Y '' -o gui.column.format:\"...\"``` ``` The ```-l``` switch might push the data to the UI more quickly... The PDML and byte/hex generating commands read directly from `tmpfile`, since they don't need to provide continual updates (they load data in batches as the user moves around). When the user types in termshark's display filter widget, termshark issues the following command for each change: ```bash tshark -Y '' -r empty.pcap ``` and checks the return code of the process. If it's zero, termshark assumes the filter expression is valid, and turns the widget green. If the return code is non-zero, termshark assumes the expression is invalid and turns the widget red. The file `empty.pcap` is generated once on startup and cached in ```$XDG_CONFIG_CACHE/empty.pcap``` (on Linux, ```~/.cache/termshark/empty.pcap```) On slower systems like the Raspberry Pi, you might see this widget go orange for a couple of seconds while termshark waits for tshark to finish. 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: ```console 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: ```console 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. Finally, termshark uses tshark in one more way - to generate the possible completions for prefixes of display filter terms. If you type ```tcp.``` in the filter widget, termshark will show a drop-down menu of possible completions. This is generated once at startup by running ```bash termshark -G fields ``` then parsing the output into a nested collection of Go maps, and serializing it to ```$XDG_CONFIG_CACHE/tsharkfieldsv2.gob.gz```. ## 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 1. 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. 2. 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. ## 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_CONFIG_CACHE` (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: - Show pcap statistics, conversation statics, etc - expose all tshark's ```-z``` options - Colorize the packets in the packet list view using Wireshark's coloring rules - 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.0.3/docs/Packages.md000066400000000000000000000051271360044163000165060ustar00rootroot00000000000000# 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](https://drive.google.com/uc?export=view&id=1RzilBvj5YFsSqv72kO6yOD0Oil88mwp3) ## Ubuntu If you are running Ubuntu 19.10 (eoan), 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.0.3/docs/UserGuide.md000066400000000000000000000450761360044163000166730ustar00rootroot00000000000000# User Guide Termshark provides a terminal-based user interface for analyzing packet captures. It's inspired by Wireshark, and depends on tshark for all its intelligence. Termshark is run from the command-line. You can see its options with ```bash $ termshark -h ``` ```console termshark v2.0.0 A wireshark-inspired terminal user interface for tshark. Analyze network traffic interactively from your terminal. See https://termshark.io for more information. Usage: termshark [FilterOrFile] Application Options: -i= Interface to read. -r= Pcap file to read. -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. --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: FilterOrFile: Filter (capture for iface, display for pcap), or pcap file to read. If --pass-thru is true (or auto, and stdout is not a tty), tshark will be executed with the supplied command-line flags. You can provide tshark-specific flags and they will be passed through to tshark (-n, -d, -T, etc). For example: $ termshark -r file.pcap -T psml -n | less ``` By default, termshark will launch an ncurses-like application in your terminal window, but if your standard output is not a tty, termshark will simply defer to tshark and pass its options through: ```console $ termshark -r test.pcap | cat 1 0.000000 192.168.44.123 → 192.168.44.213 TFTP 77 Read Request, File: C:\IBMTCPIP\lccm.1, Transfer type: octet 2 0.000000 192.168.44.123 → 192.168.44.213 TFTP 77 Read Request, File: C:\IBMTCPIP\lccm.1, Transfer type: octet ``` ## Read a pcap file Launch termshark like this to inspect a file: ```bash termshark -r test.pcap ``` You can also apply a display filter directly from the command-line: ```bash termshark -r test.pcap icmp ``` Note that when reading a file, the filter will be interpreted as a [display filter](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](https://drive.google.com/uc?export=view&id=1DOZEAlP5xiNAoCKrZoIhWJ9Zz3gX0gJf) ## Filtering Press `/` to focus on the display filter. Now you can type in a Wireshark display filter expression. The UI will update in real-time to display the validity of the current expression. If the expression is invalid, the filter widget will change color to red. As you type, termshark presents a drop-down menu with possible completions for the current term: ![filterbad](https://drive.google.com/uc?export=view&id=1KobuhX7KfA_i2VU-lCllPc3FkLUBEmQi) When the filter widget is green, you can hit the "Apply" button to make its value take effect. Termshark will then reload the packets with the new display filter applied. ![filterbad](https://drive.google.com/uc?export=view&id=10AVIaRtLWgqJ_fi0kWS_PI-vOogZTVv-) ## Changing Files Termshark provides a "Recent" button which will open a menu with your most recently-loaded pcap files. Each invocation of termshark with the ```-r``` flag will add a pcap to the start of this list: ![recent](https://drive.google.com/uc?export=view&id=1jnENk7ANqo2TZeqA-4hujHDWfDko_isT) ## Changing Views Press `tab` to move between the three packet views. You can also use the mouse to move views by clicking with the left mouse button. When focus is in any of these three views, hit the `\` key to maximize that view: ![max](https://drive.google.com/uc?export=view&id=143PHT2YDEuDig2QqFIGcZTjNg9TA7awB) Press `\` to restore the original layout. Press `|` to move the hex view to the right-hand side: ![altview](https://drive.google.com/uc?export=view&id=1RinO3imTgboVYKLWblaLOqwjhu7OcUt4) You can also press `<`,`>`,`+` and `-` to change the relative size of each view. ## Packet List View Termshark's top-most view is a list of packets read from the capture (or interface). Termshark generates the data by running `tshark` on the input with the `-T psml` options, and parsing the resulting XML. Currently the columns displayed cannot be configured, and are the same as Wireshark's defaults. When the source is a pcap file, the list can be sorted by column by clicking the button next to each column header: ![sortcol](https://drive.google.com/uc?export=view&id=1UaXNRUp8UtR728j_CPTRTb0hpVy6EUte) You can hit `home` to jump to the top of the list or `end` to jump to the bottom. Sometimes, especially if running on a small terminal, the values in a column will be truncated (e.g. long IPv6 addresses). To see the full value, move the purple cursor over the value: ![ipv6](https://drive.google.com/uc?export=view&id=1LXLz0gFieOf3mZEiP9QzwKSzSJL1FLT6) ## Packet Structure View Termshark's middle view shows the structure of the packet selected in the list view. You can expand and contract the structure using the `[+]` and `[-]` buttons, the 'enter' key, or the right and left cursor keys: ![structure](https://drive.google.com/uc?export=view&id=1Tv7kvLxXe5a2tbsvkWR6U8K6nhEBqk8D) As you navigate the packet structure, different sections of the bottom view - a hex representation of the packet - will be highlighted. ## Packet Hex View Termshark's bottom view shows the bytes that the packet comprises. Like Wireshark, they are displayed in a hexdump-like format. As you move around the bytes, the middle (structure) view will update to show you where you are in the packet's structure. ## Reading from an Interface Launch termshark like this to read from an interface: ```bash termshark -i eth0 ``` You can also apply a capture filter directly from the command-line: ```bash termshark -i eth0 tcp ``` Termshark will apply the capture filter as it reads, but the UI currently does not provide any indication of the capture filter that is in effect. Termshark's UI will launch and the packet views will update as packets are read: ![readiface](https://drive.google.com/uc?export=view&id=1UPD6KaNGsFrQ9lW-_dx_0SXhTbWBX4vn) You can apply a display filter while the packet capture process is ongoing - termshark will dynamically apply the filter without restarting the capture. Press `ctrl-c` to stop the capture process. When you exit termshark, it will print a message with the location of the pcap file that was captured: ```console $ termshark -i eth0 Packets read from interface eth0 have been saved in /home/gcla/.cache/termshark/eth0-657695279.pcap ``` ## 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: ```bash $ 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. ```bash $ 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. ## Copy Mode Both the structure and hex view support "copy mode" a feature which lets you copy ranges of data from the currently selected packet. First, move focus to the part of the packet you wish to copy. Now hit the `c` key - a section of the packet will be highlighted in yellow: ![copymode1](https://drive.google.com/uc?export=view&id=1EE9zNYyzi3vLz6FBEgFfU0gRkkWsX1Dz) You can hit the `left` and `right` arrow keys to expand or contract the selected region. Now hit `ctrl-c` to copy. Termshark will display a dialog showing you the format in which you can copy the data: ![copymode2](https://drive.google.com/uc?export=view&id=1EJW7DE1ycm9MbQkBFGOdDryoo5wlBgnZ) Select the format you want and hit `enter` (or click). Copy mode is available in the packet structure and packet hex views. This feature comes with a caveat! If you are connected to a remote machine e.g. via ssh, then you should use the `-X` flag to forward X11. On Linux, the default copy command is `xsel`. If you forward X11 with ssh, then the packet data will be copied to your desktop machine's clipboard. You can customize the copy command using termshark's [config file](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. ## 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](https://drive.google.com/uc?export=view&id=1ss09_QwHnjONa1wQkhuc34RWJv2ZSbvp) 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](https://drive.google.com/uc?export=view&id=1IjD5L0QgBWNpZ_j8KY_AYhELxwoPoSI7) 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](https://drive.google.com/uc?export=view&id=1UsVIKEFMqHBRjWqxfobJtWczc1Y7iD09) Like Wireshark, you can filter the displayed data to show only the client-side or only the server-side of the conversation: ![streams4](https://drive.google.com/uc?export=view&id=1GKE28J0j-OFrAgxqc0yhc8XnEBR3kaAv) 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](https://drive.google.com/uc?export=view&id=18rv298lPYiAiXFwVhBjogZ43qfj6DYd4) 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. ## 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](https://drive.google.com/uc?export=view&id=1bkwdKL2pHwJYpiwvyazEQ1ACtG50ZHI7) Your choice is stored in the termshark [config file](UserGuide.md#config-file). Dark-mode is supported throughout the termshark user-interface. ## Problems 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 (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_CONFIG_CACHE` (e.g. `~/.cache/termshark/`). If you open a termshark issue on github, these profiles will be useful for debugging. ## Config File Termshark reads options from a TOML configuration file saved in ```$XDG_CONFIG_HOME/termshark.toml``` (e.g. ```~/.config/termshark/termshark.toml``` on Linux). All options are saved under the ```[main]``` section. The available options are: - ```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"] ``` - ```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. - ```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. - ```dumpcap``` (string) - make termshark use this specific ```dumpcap``` (used when reading from an interface). - ```packet-colors``` (bool) - if true (or missing), termshark will colorize packets according to Wireshark's rules. - ```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 in a special hidden tail-mode. 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" ``` - ```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). termshark-2.0.3/fields.go000066400000000000000000000072721360044163000153160ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. package termshark import ( "bufio" "os" "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.0.3/fields_test.go000066400000000000000000000013151360044163000163450ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. package termshark import ( "testing" "github.com/stretchr/testify/assert" ) //====================================================================== func TestFields1(t *testing.T) { fields := NewFields() err := fields.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.0.3/format/000077500000000000000000000000001360044163000150015ustar00rootroot00000000000000termshark-2.0.3/format/hexdump.go000066400000000000000000000025311360044163000170030ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package 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.0.3/format/hexdump_test.go000066400000000000000000000017321360044163000200440ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. package 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.0.3/format/printable.go000066400000000000000000000036601360044163000173150ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package 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.0.3/go.mod000066400000000000000000000027061360044163000146240ustar00rootroot00000000000000module github.com/gcla/termshark/v2 require ( 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/gcla/deep v1.0.2 github.com/gcla/gowid v1.0.1-0.20191116181933-e43461853f37 github.com/gcla/tail v1.0.1-0.20190505190527-650e90873359 github.com/gcla/termshark v1.0.0 github.com/gdamore/tcell v1.2.1-0.20190805162843-ae1dc54d2c70 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/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/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.3.0 github.com/tevino/abool v0.0.0-20170917061928-9b9efcf221b5 golang.org/x/net v0.0.0-20190620200207-3b0461eec859 // indirect golang.org/x/sys v0.0.0-20191010194322-b09406accb47 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect gopkg.in/fsnotify.v1 v1.4.7 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.20191105032631-bdf184f0c937 replace github.com/pkg/term => github.com/gcla/term v0.0.0-20191015020247-31cba2f9f402 termshark-2.0.3/go.sum000066400000000000000000000343151360044163000146520ustar00rootroot00000000000000github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/DATA-DOG/go-sqlmock v1.3.3 h1:CWUqKXe0s8A2z6qCgkP4Kru7wC11YoAnoupUKFDnH08= github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/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.0.0/go.mod h1:Th3cr14AYIbbSAVZO/uBtswds/4iJGIqYsRmOo59X54= github.com/gcla/gowid v1.0.1-0.20191109013850-ef2a6ba3a3a6 h1:y+wiGYjZZRTkrpYVYTfFmwWywCE/jIB2rYnmj20Jcag= github.com/gcla/gowid v1.0.1-0.20191109013850-ef2a6ba3a3a6/go.mod h1:0ho5JHTdYdJWIWNoFPcsao5CzvWUq4uqCXmMK7kBLn8= github.com/gcla/gowid v1.0.1-0.20191109234239-2f8dae05ca56 h1:dwVDbW5freG4hOJopA/i10YRgF57vcoFprj3u4OnOlk= github.com/gcla/gowid v1.0.1-0.20191109234239-2f8dae05ca56/go.mod h1:0ho5JHTdYdJWIWNoFPcsao5CzvWUq4uqCXmMK7kBLn8= github.com/gcla/gowid v1.0.1-0.20191116181933-e43461853f37 h1:v3X2SbD5NdXZiyljkiebiH8/DzQyMM6WhpEGSSt5wT4= github.com/gcla/gowid v1.0.1-0.20191116181933-e43461853f37/go.mod h1:0ho5JHTdYdJWIWNoFPcsao5CzvWUq4uqCXmMK7kBLn8= 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.20191105032631-bdf184f0c937 h1:n7MlLGilok8bWfjitwyBDtBYWK2lz4LoXAjAbTzfE4I= github.com/gcla/tcell v1.1.2-0.20191105032631-bdf184f0c937/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM= 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/gcla/termshark v1.0.0/go.mod h1:vqwKjki5plWU7uU/43JI7XBzXk3HOkQJ683RVcfhRxQ= 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/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/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.2 h1:mCMFu6PgSozg9tDNMMK3g18oJBX7oYGrC09mS6CXfO4= github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s= github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 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.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/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/go.mod h1:OEi9wJV/fMUAGx1eNjq75DKDsJVuEv1U0oYdX6GX8Zs= github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0 h1:Xuk8ma/ibJ1fOy4Ee11vHhUFHQNpHhrBneOCNHVXS5w= github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0/go.mod h1:7AwjWCpdPhkSmNAgUv5C7EJ4AbmjEB3r047r3DXWu3Y= github.com/sirupsen/logrus v1.4.0 h1:yKenngtzGh+cUSSh6GWbxW2abRqhYUSR/t/6+2QqNvE= github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= 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/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-20180904163835-0709b304e793 h1:u+LnwYTOOW7Ukr/fppxEb1Nwz0AtPflrblfvUudpo+I= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 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-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTuEGr4PN7F4XJ1p4E3Y8= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190322080309-f49334f85ddc/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190416152802-12500544f89f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 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.1-0.20180807135948-17ff2d5776d2/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.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 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.0.3/modeswap/000077500000000000000000000000001360044163000153305ustar00rootroot00000000000000termshark-2.0.3/modeswap/modeswap.go000066400000000000000000000016771360044163000175110ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // package modeswap provides an IColor-conforming type Color that renders differently // if in low-color mode package modeswap import ( "github.com/gcla/gowid" ) //====================================================================== type Color struct { modeReg gowid.IColor modeLow gowid.IColor } var _ gowid.IColor = (*Color)(nil) func New(reg, lofi gowid.IColor) *Color { return &Color{ modeReg: reg, modeLow: lofi, } } func (c *Color) ToTCellColor(mode gowid.ColorMode) (gowid.TCellColor, bool) { var col gowid.IColor = c.modeLow switch mode { case gowid.Mode256Colors, gowid.Mode24BitColors: col = c.modeReg } return col.ToTCellColor(mode) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.0.3/noroot.go000066400000000000000000000017431360044163000153650ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. package termshark import ( "github.com/gcla/gowid/widgets/list" "github.com/gcla/gowid/widgets/tree" ) //====================================================================== type NoRootWalker struct { *tree.TreeWalker } func NewNoRootWalker(w *tree.TreeWalker) *NoRootWalker { return &NoRootWalker{ TreeWalker: w, } } // for omitting top level node func (f *NoRootWalker) Next(pos list.IWalkerPosition) list.IWalkerPosition { return tree.WalkerNext(f, pos) } func (f *NoRootWalker) Previous(pos list.IWalkerPosition) list.IWalkerPosition { fc := pos.(tree.IPos) pp := tree.PreviousPosition(fc, f.Tree()) if pp.Equal(tree.NewPos()) { return nil } return tree.WalkerPrevious(f, pos) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.0.3/pcap/000077500000000000000000000000001360044163000144345ustar00rootroot00000000000000termshark-2.0.3/pcap/cmds.go000066400000000000000000000116651360044163000157220ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. package pcap import ( "fmt" "io" "os" "os/exec" "runtime" "strings" "sync" "github.com/gcla/termshark/v2" "github.com/pkg/errors" log "github.com/sirupsen/logrus" ) //====================================================================== type ProcessNotStarted struct { Command *exec.Cmd } var _ error = ProcessNotStarted{} func (e ProcessNotStarted) Error() string { return fmt.Sprintf("Process %v [%v] not started yet", e.Command.Path, strings.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, c.Cmd.Args) } func (c *Command) Start() error { c.Lock() defer c.Unlock() c.Cmd.Stderr = log.StandardLogger().Writer() res := c.Cmd.Start() return res } func (c *Command) Wait() error { return c.Cmd.Wait() } func (c *Command) 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) Kill() error { c.Lock() defer c.Unlock() if c.Cmd.Process == nil { return errors.WithStack(ProcessNotStarted{Command: c.Cmd}) } if runtime.GOOS == "windows" { return c.Cmd.Process.Kill() } else { return c.Cmd.Process.Signal(os.Interrupt) } } func (c *Command) Pid() int { c.Lock() defer c.Unlock() if c.Cmd.Process == nil { return -1 } return c.Cmd.Process.Pid } //====================================================================== type Commands struct { DecodeAs []string Args []string PdmlArgs []string PsmlArgs []string 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(iface string, captureFilter string, tmpfile string) IBasicCommand { args := []string{"-P", "-i", iface, "-w", tmpfile} if captureFilter != "" { args = append(args, "-f", captureFilter) } return &Command{Cmd: exec.Command(termshark.DumpcapBin(), args...)} } func (c Commands) Tail(tmpfile string) ITailCommand { args := termshark.TailCommand() args = append(args, tmpfile) return &Command{Cmd: exec.Command(args[0], args[1:]...)} } func (c Commands) Psml(pcap interface{}, displayFilter string) IPcapCommand { fifo := true switch pcap.(type) { case string: fifo = false } args := []string{ // "-i", // "0", // "-o", // "0", //"-f", "-o", fmt.Sprintf("/tmp/foo-%d", delme), "-s", "256", "-tt", //termshark.TSharkBin(), "-T", "psml", "-o", "gui.column.format:\"No.\",\"%m\",\"Time\",\"%t\",\"Source\",\"%s\",\"Destination\",\"%d\",\"Protocol\",\"%p\",\"Length\",\"%L\",\"Info\",\"%i\"", } if !fifo { // read from cmdline file args = append(args, "-r", pcap.(string)) } else { args = append(args, "-i", "-") args = append(args, "-l") // provide data sooner to decoder routine in termshark } if displayFilter != "" { args = append(args, "-Y", displayFilter) } for _, arg := range c.DecodeAs { args = append(args, "-d", arg) } 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{"-F", "pcap", "-r", pcap, "-w", "-"} if displayFilter != "" { args = append(args, "-Y", displayFilter) } args = append(args, c.Args...) return &Command{Cmd: exec.Command(termshark.TSharkBin(), args...)} } func (c Commands) Pdml(pcap string, displayFilter string) IPcapCommand { args := []string{"-T", "pdml", "-r", pcap} if 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.0.3/pcap/loader.go000066400000000000000000001554421360044163000162440ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. package pcap import ( "context" "encoding/xml" "fmt" "io" "os" "os/exec" "path/filepath" "regexp" "strconv" "strings" "sync" "sync/atomic" "time" "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 Goroutinewg *sync.WaitGroup type RunFn func() type whenFn func() bool type runFnInState struct { when whenFn doit RunFn } //====================================================================== type LoaderState int const ( LoadingPsml LoaderState = 1 << iota // pcap+pdml might be finished, but this is what was initiated LoadingPdml // from a cache request LoadingIface // copying from iface to temp pcap ) func (c *Loader) State() LoaderState { return c.state } // Repeatedly go back to the start if anything is triggered. // Call only on the main thread (running the select loop) func (c *Loader) SetState(st LoaderState) { c.state = st Outer: for { Inner: for i, sc := range c.onStateChange { if sc.when() { c.onStateChange = append(c.onStateChange[:i], c.onStateChange[i+1:]...) sc.doit() break Inner } } break Outer } } func (t LoaderState) String() string { s := make([]string, 0, 3) if t&LoadingPsml != 0 { s = append(s, "psml") } if t&LoadingPdml != 0 { s = append(s, "pdml") } if t&LoadingIface != 0 { s = append(s, "iface") } if len(s) == 0 { return fmt.Sprintf("idle") } else { return strings.Join(s, "+") } } //====================================================================== type IBasicCommand interface { 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(iface string, captureFilter string, tmpfile string) IBasicCommand Tail(tmpfile string) ITailCommand Psml(pcap interface{}, displayFilter string) IPcapCommand Pcap(pcap string, displayFilter string) IPcapCommand Pdml(pcap string, displayFilter string) IPcapCommand } type Loader struct { cmds ILoaderCmds state LoaderState // which pieces are currently loading suppressErrors bool // true if loader is in a transient state due to a user operation e.g. stop, reload, etc psrc IPacketSource // The canonical struct for the loader's current packet source. ifaceFile string // The temp pcap file that is created by reading from the interface displayFilter string captureFilter string PcapPsml interface{} // Pcap file source for the psml reader - fifo if iface+!stopped; tmpfile if iface+stopped; pcap otherwise PcapPdml string // Pcap file source for the pdml reader - tmpfile if iface; pcap otherwise PcapPcap string // Pcap file source for the pcap reader - tmpfile if iface; pcap otherwise mainCtx context.Context // cancelling this cancels the dependent contexts - used to close whole loader. mainCancelFn context.CancelFunc thisSrcCtx context.Context // cancelling this cancels the dependent contexts - used to stop current load. thisSrcCancelFn context.CancelFunc psmlCtx context.Context // cancels the psml loading process psmlCancelFn context.CancelFunc stage2Ctx context.Context // cancels the pcap/pdml loading process stage2CancelFn context.CancelFunc ifaceCtx context.Context // cancels the iface reader process ifaceCancelFn context.CancelFunc //psmlDecodingProcessChan chan struct{} // signalled by psml load stage when the XML decoding is complete - signals rest of stage 1 to shut down stage2GoroutineDoneChan chan struct{} // signalled by a goroutine in stage 2 for pcap/pdml - always signalled at end. When x2, signals rest of stage 2 to shut down //stage1Wg sync.WaitGroup stage2Wg sync.WaitGroup // Signalled when the psml is fully loaded (or already loaded) - to tell // the pdml and pcap reader goroutines to start - they can then map table // row -> frame number StartStage2Chan chan struct{} // Signalled to start the pdml reader. Will start concurrent with psml if // psml loaded already or if filter == "" (then table row == frame number) startPdmlChan chan struct{} startPcapChan chan struct{} PsmlFinishedChan chan struct{} // closed when entire psml load process is done Stage2FinishedChan chan struct{} // closed when entire pdml+pcap load process is done IfaceFinishedChan chan struct{} // closed when interface reader process has shut down (e.g. stopped) ifaceCmd IBasicCommand tailCmd ITailCommand PsmlCmd IPcapCommand PcapCmd IPcapCommand PdmlCmd IPcapCommand sync.Mutex PacketPsmlData [][]string PacketPsmlColors []PacketColors PacketPsmlHeaders []string PacketCache *lru.Cache // i -> [pdml(i * 1000)..pdml(i+1*1000)] onStateChange []runFnInState LoadWasCancelled bool // True if the last load (iface or file) was halted by the stop button RowCurrentlyLoading int // set by the pdml loading stage highestCachedRow int KillAfterReadingThisMany int // A shortcut - tell pcap/pdml to read one // 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 opt Options } type PacketColors struct { FG gowid.IColor BG gowid.IColor } type Options struct { CacheSize int PacketsPerLoad int } func NewPcapLoader(cmds ILoaderCmds, opts ...Options) *Loader { var opt Options if len(opts) > 0 { opt = opts[0] } if opt.CacheSize == 0 { opt.CacheSize = 32 } if opt.PacketsPerLoad == 0 { opt.PacketsPerLoad = 1000 // default } else if opt.PacketsPerLoad < 100 { opt.PacketsPerLoad = 100 // minimum } res := &Loader{ cmds: cmds, IfaceFinishedChan: make(chan struct{}), stage2GoroutineDoneChan: make(chan struct{}), PsmlFinishedChan: make(chan struct{}), Stage2FinishedChan: make(chan struct{}), onStateChange: make([]runFnInState, 0), RowCurrentlyLoading: -1, highestCachedRow: -1, opt: opt, } res.resetData() res.mainCtx, res.mainCancelFn = context.WithCancel(context.Background()) return res } func (c *Loader) resetData() { c.Lock() defer c.Unlock() c.PacketPsmlData = make([][]string, 0) c.PacketPsmlColors = make([]PacketColors, 0) c.PacketPsmlHeaders = make([]string, 0, 10) packetCache, err := lru.New(c.opt.CacheSize) if err != nil { log.Fatal(err) } c.PacketCache = packetCache } func (c *Loader) PacketsPerLoad() int { c.Lock() defer c.Unlock() return c.opt.PacketsPerLoad } // Close shuts down the whole loader, including progress monitoring goroutines. Use this only // when about to load a new pcap (use a new loader) func (c *Loader) Close() error { if c.mainCancelFn != nil { c.mainCancelFn() } return nil } func (c *Loader) MainContext() context.Context { return c.mainCtx } func (c *Loader) SourceContext() context.Context { return c.thisSrcCtx } func (c *Loader) stopLoadIface() { if c.ifaceCancelFn != nil { c.ifaceCancelFn() } } func (c *Loader) stopLoadPsml() { if c.psmlCancelFn != nil { c.psmlCancelFn() } } func (c *Loader) stopLoadPdml() { if c.stage2CancelFn != nil { c.stage2CancelFn() } } func (c *Loader) stopLoadCurrentSource() { if c.thisSrcCancelFn != nil { c.thisSrcCancelFn() } } func (c *Loader) PsmlData() [][]string { return c.PacketPsmlData } func (c *Loader) PsmlHeaders() []string { return c.PacketPsmlHeaders } func (c *Loader) PsmlColors() []PacketColors { return c.PacketPsmlColors } //====================================================================== type Scheduler struct { *Loader OperationsChan chan RunFn disabled bool } func NewScheduler(cmds ILoaderCmds, opts ...Options) *Scheduler { return &Scheduler{ OperationsChan: make(chan RunFn, 1000), Loader: NewPcapLoader(cmds, opts...), } } func (c *Scheduler) IsEnabled() bool { return !c.disabled } func (c *Scheduler) Enable() { c.disabled = false c.suppressErrors = false } func (c *Scheduler) Disable() { c.disabled = true c.suppressErrors = true } func (c *Scheduler) RequestClearPcap(cb interface{}) { log.Infof("Scheduler requested clear pcap") c.OperationsChan <- func() { c.Disable() c.doClearPcapOperation(cb, func() { c.Enable() }) } } func (c *Scheduler) RequestStopLoadStage1(cb interface{}) { log.Infof("Scheduler requested stop psml + iface") c.OperationsChan <- func() { c.Disable() c.doStopLoadStage1Operation(cb, func() { c.Enable() }) } } func (c *Scheduler) RequestStopLoad(cb interface{}) { log.Infof("Scheduler requested stop pcap load") c.OperationsChan <- func() { c.Disable() c.doStopLoadOperation(cb, func() { c.Enable() }) } } func (c *Scheduler) RequestNewFilter(newfilt string, cb interface{}) { log.Infof("Scheduler requested application of display filter '%v'", newfilt) c.OperationsChan <- func() { c.Disable() c.doNewFilterOperation(newfilt, cb, c.Enable) } } func (c *Scheduler) RequestLoadInterface(psrc IPacketSource, captureFilter string, displayFilter string, tmpfile string, cb interface{}) { log.Infof("Scheduler requested interface/fifo load for '%v'", psrc.Name()) c.OperationsChan <- func() { c.Disable() c.doLoadInterfaceOperation(psrc, captureFilter, displayFilter, tmpfile, cb, func() { c.Enable() }) } } func (c *Scheduler) RequestLoadPcap(pcap string, displayFilter string, cb interface{}) { log.Infof("Scheduler requested pcap file load for '%v'", pcap) c.OperationsChan <- func() { c.Disable() c.doLoadPcapOperation(pcap, displayFilter, cb, func() { c.Enable() }) } } //====================================================================== // Clears the currently loaded data. If the loader is currently reading from an // interface, the loading continues after the current data has been discarded. If // the loader is currently reading from a file, the loading *stops*. func (c *Loader) doClearPcapOperation(cb interface{}, fn RunFn) { if c.State() == 0 { c.resetData() handleClear(cb) fn() } else { // If we are reading from an interface when the clear operation is issued, we should // continue again afterwards. If we're reading from a file, the clear stops the read. // Track this state. startIfaceAgain := false if c.State()&LoadingIface != 0 { startIfaceAgain = CanRestart(c.psrc) // Only try to restart if the packet source allows } c.stopLoadCurrentSource() c.When(c.IdleState, func() { // Document why this needs to be delayed again, since runWhenReadyFn // will run in app goroutine c.doClearPcapOperation(cb, func() { if startIfaceAgain { c.doLoadInterfaceOperation( c.psrc, c.CaptureFilter(), c.DisplayFilter(), c.InterfaceFile(), cb, fn, ) } else { fn() } }) }) } } func (c *Loader) IdleState() bool { return c.State() == 0 } func (c *Loader) When(pred whenFn, doit RunFn) { c.onStateChange = append(c.onStateChange, runFnInState{pred, doit}) } func (c *Loader) doStopLoadOperation(cb interface{}, fn RunFn) { c.LoadWasCancelled = true HandleBegin(cb) if c.State() != 0 { c.stopLoadCurrentSource() c.When(c.IdleState, func() { c.doStopLoadOperation(cb, fn) }) } else { fn() HandleEnd(cb) } } func (c *Loader) doStopLoadStage1Operation(cb interface{}, fn RunFn) { c.LoadWasCancelled = true HandleBegin(cb) if c.State()&(LoadingPsml|LoadingIface) != 0 { if c.State()&LoadingPsml != 0 { c.stopLoadPsml() } if c.State()&LoadingIface != 0 { c.stopLoadIface() } c.When(func() bool { return c.State()&(LoadingIface|LoadingPsml) == 0 }, func() { c.doStopLoadStage1Operation(cb, fn) }) } else { fn() HandleEnd(cb) } } // Issued e.g. if a new filter is applied while loading from an interface. We need // to stop the psml (and fifo) and pdml reading processes, but keep alive the spooling // process from iface -> temp file. When the current state is simply Loadingiface then // the next operation can commence (e.g. applying the new filter value) func (c *Loader) doStopLoadToIfaceOperation(fn RunFn) { c.stopLoadPsml() c.stopLoadPdml() c.When(func() bool { return c.State() == LoadingIface }, fn) } // Called when state is appropriate func (c *Loader) doNewFilterOperation(newfilt string, cb interface{}, fn RunFn) { //var res EnableOperationsWhen if c.DisplayFilter() == newfilt { log.Infof("No operation - same filter applied.") fn() } else if c.State() == 0 || c.State() == LoadingIface { handleClear(cb) c.startLoadNewFilter(newfilt, cb) c.When(func() bool { return c.State()&LoadingPsml == LoadingPsml }, fn) c.SetState(c.State() | LoadingPsml) } else { if c.State()&LoadingPsml != 0 { c.stopLoadPsml() } if c.State()&LoadingPdml != 0 { c.stopLoadPdml() } c.When(func() bool { return c.State() == 0 || c.State() == LoadingIface }, func() { c.doNewFilterOperation(newfilt, cb, fn) }) } } type IClear interface { OnClear(closeMe chan<- struct{}) } type INewSource interface { OnNewSource(closeMe chan<- struct{}) } type IOnError interface { OnError(err error, closeMe chan<- struct{}) } type IBeforeBegin interface { BeforeBegin(closeMe chan<- struct{}) } type IAfterEnd interface { AfterEnd(closeMe chan<- struct{}) } type IUnpack interface { Unpack() []interface{} } type HandlerList []interface{} func (h HandlerList) Unpack() []interface{} { return h } func (c *Loader) doLoadInterfaceOperation(psrc IPacketSource, captureFilter string, displayFilter string, tmpfile string, cb interface{}, fn RunFn) { // The channel is unbuffered, and monitored from the same goroutine, so this would block // unless we start a new goroutine //var res EnableOperationsWhen // If we're already loading, but the request is for the same, then ignore. If we were stopped, then // process the request, because it implicitly means start reading from the interface again (and we // are stopped) if c.State()&LoadingPsml != 0 && c.Interface() == psrc.Name() && c.DisplayFilter() == displayFilter && c.CaptureFilter() == captureFilter { log.Infof("No operation - same interface and filters.") fn() } else if c.State() == 0 { handleClear(cb) handleNewSource(cb) if err := c.startLoadInterfaceNew(psrc, captureFilter, displayFilter, tmpfile, cb); err == nil { c.When(func() bool { return c.State()&(LoadingIface|LoadingPsml) == LoadingIface|LoadingPsml }, fn) c.SetState(c.State() | LoadingIface | LoadingPsml) } else { HandleError(err, cb) } } else if c.State() == LoadingIface && psrc.Name() == c.Interface() { //if iface == c.Interface() { // same interface, so just start it back up - iface spooler still running handleClear(cb) c.startLoadNewFilter(displayFilter, cb) c.When(func() bool { return c.State()&(LoadingIface|LoadingPsml) == LoadingIface|LoadingPsml }, fn) c.SetState(c.State() | LoadingPsml) } else { // State contains Loadingpdml and/or Loadingpdml. Need to stop those first. OR state contains // Loadingiface but the interface requested is different. if c.State()&LoadingIface != 0 && psrc.Name() != c.Interface() { c.doStopLoadOperation(cb, func() { c.doLoadInterfaceOperation(psrc, captureFilter, displayFilter, tmpfile, cb, fn) }) // returns an enable function when idle } else { c.doStopLoadToIfaceOperation(func() { c.doLoadInterfaceOperation(psrc, captureFilter, displayFilter, tmpfile, cb, fn) }) } } } // Call from app goroutine context func (c *Loader) doLoadPcapOperation(pcap string, displayFilter string, cb interface{}, fn RunFn) { curDisplayFilter := displayFilter // The channel is unbuffered, and monitored from the same goroutine, so this would block // unless we start a new goroutine if c.Pcap() == pcap && c.DisplayFilter() == curDisplayFilter { log.Infof("No operation - same pcap and filter.") fn() } else if c.State() == 0 { handleClear(cb) handleNewSource(cb) c.startLoadNewFile(pcap, curDisplayFilter, cb) c.When(func() bool { return c.State()&LoadingPsml == LoadingPsml }, fn) c.SetState(c.State() | LoadingPsml) } else { // First, wait until everything is stopped c.doStopLoadOperation(cb, func() { c.doLoadPcapOperation(pcap, displayFilter, cb, fn) }) } } func (c *Loader) ReadingFromFifo() bool { // 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 } type unpackedHandlerFunc func(interface{}, ...chan struct{}) bool func HandleUnpack(cb interface{}, handler unpackedHandlerFunc, chs ...chan struct{}) bool { if c, ok := cb.(IUnpack); ok { ch := getCbChan(chs...) var wg sync.WaitGroup handlers := c.Unpack() wg.Add(len(handlers)) for _, cb := range handlers { cbcopy := cb // don't fall into loop variable non-capture trap termshark.TrackedGo(func() { ch2 := make(chan struct{}) handler(cbcopy, ch2) // will wait on channel if it has to, doesn't matter if not wg.Done() }, Goroutinewg) } termshark.TrackedGo(func() { wg.Wait() close(ch) }, Goroutinewg) <-ch return true } return false } func getCbChan(chs ...chan struct{}) chan struct{} { if len(chs) > 0 { return chs[0] } else { return make(chan struct{}) } } func HandleBegin(cb interface{}, chs ...chan struct{}) bool { res := false if !HandleUnpack(cb, HandleBegin) { if c, ok := cb.(IBeforeBegin); ok { ch := getCbChan(chs...) c.BeforeBegin(ch) <-ch res = true } } return res } func HandleEnd(cb interface{}, chs ...chan struct{}) bool { res := false if !HandleUnpack(cb, HandleEnd, chs...) { if c, ok := cb.(IAfterEnd); ok { ch := getCbChan(chs...) c.AfterEnd(ch) <-ch res = true } } return res } func HandleError(err error, cb interface{}, chs ...chan struct{}) bool { res := false if !HandleUnpack(cb, func(cb2 interface{}, chs2 ...chan struct{}) bool { return HandleError(err, cb2, chs2...) }, chs...) { if ec, ok := cb.(IOnError); ok { ch := getCbChan(chs...) ec.OnError(err, ch) <-ch res = true } } return res } func handleClear(cb interface{}, chs ...chan struct{}) bool { res := false if !HandleUnpack(cb, handleClear) { if c, ok := cb.(IClear); ok { ch := getCbChan(chs...) c.OnClear(ch) <-ch res = true } } return res } func handleNewSource(cb interface{}, chs ...chan struct{}) bool { res := false if !HandleUnpack(cb, handleNewSource) { if c, ok := cb.(INewSource); ok { ch := getCbChan(chs...) c.OnNewSource(ch) <-ch res = true } } return res } // https://stackoverflow.com/a/28005931/784226 func TempPcapFile(token string) string { re := regexp.MustCompile(`[^a-zA-Z0-9.-]`) tokenClean := re.ReplaceAllString(token, "_") return filepath.Join(termshark.PcapDir(), fmt.Sprintf("%s--%s.pcap", tokenClean, termshark.DateStringForFilename(), )) } func (c *Loader) makeNewSourceContext() { c.thisSrcCtx, c.thisSrcCancelFn = context.WithCancel(c.mainCtx) } // Save the file first // 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 *Loader) startLoadInterfaceNew(psrc IPacketSource, captureFilter string, displayFilter string, tmpfile string, cb interface{}) error { c.PcapPsml = nil c.PcapPdml = tmpfile c.PcapPcap = tmpfile c.psrc = psrc // 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 c.makeNewSourceContext() // 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 } log.Infof("Starting new interface/fifo load '%v'", psrc.Name()) c.startLoadPsml(cb) termshark.TrackedGo(func() { c.loadIfaceAsync(cb) }, Goroutinewg) return nil } func (c *Loader) startLoadNewFilter(displayFilter string, cb interface{}) { c.displayFilter = displayFilter c.makeNewSourceContext() log.Infof("Applying new display filter '%s'", displayFilter) c.startLoadPsml(cb) } func (c *Loader) startLoadNewFile(pcap string, displayFilter string, cb interface{}) { c.psrc = FileSource{Filename: pcap} c.ifaceFile = "" c.PcapPsml = pcap c.PcapPdml = pcap c.PcapPcap = pcap c.displayFilter = displayFilter c.makeNewSourceContext() log.Infof("Starting new pcap file load '%s'", pcap) c.startLoadPsml(cb) } func (c *Loader) startLoadPsml(cb interface{}) { c.Lock() c.PacketCache.Purge() c.Unlock() termshark.TrackedGo(func() { c.loadPsmlAsync(cb) }, Goroutinewg) } // assumes no pcap is being loaded func (c *Loader) startLoadPdml(row int, cb interface{}) { c.Lock() c.RowCurrentlyLoading = row c.Unlock() termshark.TrackedGo(func() { c.loadPcapAsync(row, cb) }, Goroutinewg) } // if done==true, then this cache entry is complete func (c *Loader) updateCacheEntryWithPdml(row int, pdml []IPdmlPacket, done bool) { var ce CacheEntry c.Lock() defer c.Unlock() if ce2, ok := c.PacketCache.Get(row); ok { ce = ce2.(CacheEntry) } ce.Pdml = pdml ce.PdmlComplete = done c.PacketCache.Add(row, ce) } func (c *Loader) updateCacheEntryWithPcap(row int, pcap [][]byte, done bool) { var ce CacheEntry c.Lock() defer c.Unlock() if ce2, ok := c.PacketCache.Get(row); ok { ce = ce2.(CacheEntry) } ce.Pcap = pcap ce.PcapComplete = done c.PacketCache.Add(row, ce) } func (c *Loader) LengthOfPdmlCacheEntry(row int) (int, error) { c.Lock() defer c.Unlock() if ce, ok := c.PacketCache.Get(row); ok { ce2 := ce.(CacheEntry) return len(ce2.Pdml), nil } return -1, fmt.Errorf("No cache entry found for row %d", row) } func (c *Loader) LengthOfPcapCacheEntry(row int) (int, error) { c.Lock() defer c.Unlock() if ce, ok := c.PacketCache.Get(row); ok { ce2 := ce.(CacheEntry) return len(ce2.Pcap), nil } return -1, fmt.Errorf("No cache entry found for row %d", row) } type ISimpleCache interface { Complete() bool } var _ ISimpleCache = CacheEntry{} type iPcapLoader interface { Interface() string InterfaceFile() string DisplayFilter() string CaptureFilter() string NumLoaded() int CacheAt(int) (ISimpleCache, bool) LoadingRow() int } var _ iPcapLoader = (*Loader)(nil) var _ fmt.Stringer = (*Loader)(nil) func (c *Loader) String() string { switch { case c.psrc.IsFile() || c.psrc.IsFifo(): return filepath.Base(c.psrc.Name()) case c.psrc.IsPipe(): return "" case c.psrc.IsInterface(): return c.psrc.Name() default: return "(no packet source)" } } func (c *Loader) Pcap() string { if c.psrc == nil { return "" } else if c.psrc.IsFile() { return c.psrc.Name() } else { return "" } } func (c *Loader) Interface() string { if c.psrc == nil { return "" } else if !c.psrc.IsFile() { return c.psrc.Name() } else { return "" } } func (c *Loader) InterfaceFile() string { return c.ifaceFile } func (c *Loader) DisplayFilter() string { return c.displayFilter } func (c *Loader) CaptureFilter() string { return c.captureFilter } func (c *Loader) NumLoaded() int { c.Lock() defer c.Unlock() return len(c.PacketPsmlData) } func (c *Loader) LoadingRow() int { c.Lock() defer c.Unlock() return c.RowCurrentlyLoading } func (c *Loader) CacheAt(row int) (ISimpleCache, bool) { if ce, ok := c.PacketCache.Get(row); ok { return ce.(CacheEntry), ok } return CacheEntry{}, false } func (c *Loader) 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 } else if c.LoadingRow() == ev.Row { res = false } return res } func (c *Loader) signalStage2Done(cb interface{}) { c.Lock() ch := c.Stage2FinishedChan c.Stage2FinishedChan = make(chan struct{}) c.Unlock() HandleEnd(cb, ch) } func (c *Loader) signalStage2Starting(cb interface{}) { HandleBegin(cb) } // Call from any goroutine - avoid calling in render, don't block it // Procedure: // - caller passes context, keeps cancel function // - create a derived context for pcap reading processes // - run them in goroutines // - for each pcap process, // - defer signal pcapchan when done // - check for ctx2.Err to break // - if err, then break // - run goroutine to update UI with latest data on ticker // - if ctxt2 done, then break // - run controller watching for // - if original ctxt done, then break (ctxt2 automatically cancelled) // - if both processes done, then // - cancel ticker with ctxt2 // - wait for all to shut down // - final UI update //func loadPcapAsync(ctx context.Context, pcapFile string, filter string, app gowid.IApp) error { func (c *Loader) loadPcapAsync(row int, cb interface{}) { // Used to cancel the tickers below which update list widgets with the latest data and // update the progress meter. Note that if ctx is cancelled, then this context is cancelled // too. When the 2/3 data loading processes are done, a goroutine will then run uiCtxCancel() // to stop the UI updates. c.Lock() c.stage2Ctx, c.stage2CancelFn = context.WithCancel(c.thisSrcCtx) c.Unlock() intStage2Ctx, intStage2CancelFn := context.WithCancel(context.Background()) // Set to true by a goroutine started within here if ctxCancel() is called i.e. the outer context var stageIsCancelled int32 c.startPdmlChan = make(chan struct{}) c.startPcapChan = make(chan struct{}) // Returns true if it's an error we should bring to user's attention unexpectedError := func(err error) bool { cancelled := atomic.LoadInt32(&stageIsCancelled) if cancelled == 0 { if err != io.EOF { if err, ok := err.(*xml.SyntaxError); !ok || err.Msg != "unexpected EOF" { return true } } } return false } setCancelled := func() { atomic.CompareAndSwapInt32(&stageIsCancelled, 0, 1) } //====================================================================== var displayFilterStr string sidx := -1 eidx := -1 // When we start a command (in service of loading pcaps), add it to this list. Then we wait // for finished signals on a channel - //procs := []ICommand{} // signal to updater that we're about to start. This will block until cb completes c.signalStage2Starting(cb) // This should correctly wait for all resources, no matter where in the process of creating them // an interruption or error occurs defer func() { procsDoneCount := 0 L: for { // pdml and psml make 2 select { // Don't need to wait for ctx.Done. if that gets cancelled, then it will propagate // to context2. The two tshark processes will wait on context2.Done, and complete - // then their defer blocks will send procDoneChan messages. When the count hits 2, this // select block will exit. Note that we also issue a cancel if count==2 because it might // just be that the tshark processes finish normally - then we need to stop the other // goroutines using ctxt2. case <-c.stage2GoroutineDoneChan: procsDoneCount++ if procsDoneCount == 2 { intStage2CancelFn() // stop the ticker break L } } } // Wait for all other goroutines to complete c.stage2Wg.Wait() c.Lock() c.RowCurrentlyLoading = -1 c.Unlock() c.signalStage2Done(cb) }() // // Goroutine to set mapping between table rows and frame numbers // termshark.TrackedGo(func() { select { case <-c.StartStage2Chan: break case <-c.stage2Ctx.Done(): setCancelled() return case <-intStage2Ctx.Done(): return // shutdown signalled - don't start the pdml/pcap processes } // Do this - but if we're cancelled first (stage2Ctx.Done), then they // don't need to be signalled because the other selects waiting on these // channels will be cancelled too. defer func() { // Signal the pdml and pcap reader to start. for _, ch := range []chan struct{}{c.startPdmlChan, c.startPcapChan} { select { case <-ch: // it will be closed if the psml has loaded already, and this e.g. a cached load default: close(ch) } } }() // If there's no filter, psml, pdml and pcap run concurrently for speed. Therefore the pdml and pcap // don't know how large the psml will be. So we set numToRead to 1000. This might be too high, but // we only use this to determine when we can kill the reading processes early. The result will be // correct if we don't kill the processes, it just might load for longer. c.KillAfterReadingThisMany = c.opt.PacketsPerLoad var err error if c.displayFilter == "" { sidx = row + 1 // +1 for frame.number being 1-based; +1 to read past the end so that // the XML decoder doesn't stall and I can kill after abcdex eidx = row + c.opt.PacketsPerLoad + 1 + 1 } else { c.Lock() if len(c.PacketPsmlData) > row { sidx, err = strconv.Atoi(c.PacketPsmlData[row][0]) if err != nil { log.Fatal(err) } if len(c.PacketPsmlData) > row+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(c.PacketPsmlData[row+c.opt.PacketsPerLoad+1][0]) if err != nil { log.Fatal(err) } } else { eidx, err = strconv.Atoi(c.PacketPsmlData[len(c.PacketPsmlData)-1][0]) if err != nil { log.Fatal(err) } eidx += 1 // beyond end of last frame c.KillAfterReadingThisMany = len(c.PacketPsmlData) - row } } c.Unlock() } if c.displayFilter != "" { displayFilterStr = fmt.Sprintf("(%s) and (frame.number >= %d) and (frame.number < %d)", c.displayFilter, sidx, eidx) } else { displayFilterStr = fmt.Sprintf("(frame.number >= %d) and (frame.number < %d)", sidx, eidx) } }, &c.stage2Wg, Goroutinewg) //====================================================================== // // Goroutine to run pdml process // termshark.TrackedGo(func() { defer func() { c.stage2GoroutineDoneChan <- struct{}{} }() // Wait for stage 2 to be kicked off (potentially by psml load, then mapping table row to frame num); or // quit if that happens first select { case <-c.startPdmlChan: case <-c.stage2Ctx.Done(): setCancelled() return case <-intStage2Ctx.Done(): return } c.Lock() c.PdmlCmd = c.cmds.Pdml(c.PcapPdml, displayFilterStr) c.Unlock() pdmlOut, err := c.PdmlCmd.StdoutReader() if err != nil { HandleError(err, cb) return } err = c.PdmlCmd.Start() if err != nil { err = fmt.Errorf("Error starting PDML process %v: %v", c.PdmlCmd, err) HandleError(err, cb) return } log.Infof("Started PDML command %v with pid %d", c.PdmlCmd, c.PdmlCmd.Pid()) defer func() { c.PdmlCmd.Wait() }() d := xml.NewDecoder(pdmlOut) packets := make([]IPdmlPacket, 0, c.opt.PacketsPerLoad) issuedKill := false var packet PdmlPacket var cpacket IPdmlPacket Loop: for { tok, err := d.Token() if err != nil { if unexpectedError(err) { err = fmt.Errorf("Could not read PDML data: %v", err) HandleError(err, cb) } break } switch tok := tok.(type) { case xml.StartElement: switch tok.Name.Local { case "packet": err := d.DecodeElement(&packet, &tok) if err != nil { if !issuedKill && unexpectedError(err) { err = fmt.Errorf("Could not decode PDML data: %v", err) HandleError(err, cb) } break Loop } // Enabled for now - do something more subtle perhaps in the future if true { cpacket = SnappyPdmlPacket(packet) } else { cpacket = packet } packets = append(packets, cpacket) c.updateCacheEntryWithPdml(row, packets, false) //if len(pdml2) == abcdex { if len(packets) == c.KillAfterReadingThisMany { // Shortcut - we never take more than abcdex - so just kill here issuedKill = true err = termshark.KillIfPossible(c.PdmlCmd) if err != nil { log.Infof("Did not kill pdml process: %v", err) } } } } } // Want to preserve invariant - for simplicity - that we only add full loads // to the cache cancelled := atomic.LoadInt32(&stageIsCancelled) if cancelled == 0 { // never evict row 0 c.PacketCache.Get(0) c.Lock() if c.highestCachedRow != -1 { // try not to evict "end" c.PacketCache.Get(c.highestCachedRow) } c.Unlock() // 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. c.updateCacheEntryWithPdml(row, packets, !c.ReadingFromFifo()) if row > c.highestCachedRow { c.Lock() c.highestCachedRow = row c.Unlock() } } }, &c.stage2Wg, Goroutinewg) //====================================================================== // // Goroutine to run pcap process // termshark.TrackedGo(func() { defer func() { c.stage2GoroutineDoneChan <- struct{}{} }() // Wait for stage 2 to be kicked off (potentially by psml load, then mapping table row to frame num); or // quit if that happens first select { case <-c.startPcapChan: case <-c.stage2Ctx.Done(): setCancelled() return case <-intStage2Ctx.Done(): return } c.Lock() c.PcapCmd = c.cmds.Pcap(c.PcapPcap, displayFilterStr) c.Unlock() pcapOut, err := c.PcapCmd.StdoutReader() if err != nil { HandleError(err, cb) return } err = c.PcapCmd.Start() if err != nil { // e.g. on the pi err = fmt.Errorf("Error starting PCAP process %v: %v", c.PcapCmd, err) HandleError(err, cb) return } log.Infof("Started pcap command %v with pid %d", c.PcapCmd, c.PcapCmd.Pid()) defer func() { c.PcapCmd.Wait() }() packets := make([][]byte, 0, c.opt.PacketsPerLoad) globalHdr := [24]byte{} pktHdr := [16]byte{} _, err = io.ReadFull(pcapOut, globalHdr[:]) if err != nil { if unexpectedError(err) { err = fmt.Errorf("Could not read PCAP header: %v", err) HandleError(err, cb) } return } issuedKill := false for { _, err = io.ReadFull(pcapOut, pktHdr[:]) if err != nil { if unexpectedError(err) { err = fmt.Errorf("Could not read PCAP packet header: %v", err) HandleError(err, cb) } break } var value uint32 value |= uint32(pktHdr[8]) value |= uint32(pktHdr[9]) << 8 value |= uint32(pktHdr[10]) << 16 value |= uint32(pktHdr[11]) << 24 packet := make([]byte, int(value)) _, err = io.ReadFull(pcapOut, packet) if err != nil { if !issuedKill && unexpectedError(err) { err = fmt.Errorf("Could not read PCAP packet: %v", err) HandleError(err, cb) } break } packets = append(packets, packet) readEnough := (len(packets) >= c.KillAfterReadingThisMany) c.updateCacheEntryWithPcap(row, packets, false) if readEnough { // Shortcut - we never take more than abcdex - so just kill here issuedKill = true err = termshark.KillIfPossible(c.PcapCmd) if err != nil { log.Infof("Did not kill pdml process: %v", err) } } } // I just want to ensure I read it from ram, obviously this is racey cancelled := atomic.LoadInt32(&stageIsCancelled) if cancelled == 0 { // never evict row 0 c.PacketCache.Get(0) if c.highestCachedRow != -1 { // try not to evict "end" c.PacketCache.Get(c.highestCachedRow) } c.updateCacheEntryWithPcap(row, packets, !c.ReadingFromFifo()) } }, &c.stage2Wg, Goroutinewg) // // Goroutine to track an external shutdown - kills processes in case the external // shutdown comes first. If it's an internal shutdown, no need to kill because // that would only be triggered once processes are dead // termshark.TrackedGo(func() { select { case <-c.stage2Ctx.Done(): setCancelled() err := termshark.KillIfPossible(c.PcapCmd) if err != nil { log.Infof("Did not kill pcap process: %v", err) } err = termshark.KillIfPossible(c.PdmlCmd) if err != nil { log.Infof("Did not kill pdml process: %v", err) } case <-intStage2Ctx.Done(): } }, Goroutinewg) } func (c *Loader) TurnOffPipe() { // Switch over to the temp pcap file. If a new filter is applied // after stopping, we should read from the temp file and not the fifo // because nothing will be feeding the fifo. if c.PcapPsml != c.PcapPdml { log.Infof("Switching from interface/fifo mode to file mode") c.PcapPsml = c.PcapPdml } } func (c *Loader) signalPsmlStarting(cb interface{}) { HandleBegin(cb) } func (c *Loader) signalPsmlDone(cb interface{}) { ch := c.PsmlFinishedChan c.PsmlFinishedChan = make(chan struct{}) HandleEnd(cb, ch) } // 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 *Loader callback interface{} } func (r *tailReadTracker) Read(p []byte) (int, error) { n, err := r.tailReader.Read(p) 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.checkAllBytesRead(r.callback) 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 } } // 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 (c *Loader) checkAllBytesRead(cb interface{}) { cancel := false if !c.totalFifoBytesWritten.IsNone() && !c.totalFifoBytesRead.IsNone() { if c.totalFifoBytesRead.Val() == c.totalFifoBytesWritten.Val() { cancel = true } } if c.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 c.fifoError != nil { err := fmt.Errorf("Fifo error: %v", c.fifoError) HandleError(err, cb) } if c.tailCmd != nil { c.totalFifoBytesWritten = gwutil.NoneInt64() c.totalFifoBytesRead = gwutil.NoneInt64() err := termshark.KillIfPossible(c.tailCmd) if err != nil { log.Infof("Did not kill tail process: %v", err) } else { c.tailCmd.Wait() // this will block the exit of this function until the command is killed // We need to explicitly close the write side of the pipe. Without this, // the PSML process Wait() function won't complete, because golang won't // complete termination until IO has finished, and io.Copy() will be stuck // in a loop. c.tailCmd.Close() } } } } func (c *Loader) loadPsmlAsync(cb interface{}) { // Used to cancel the tickers below which update list widgets with the latest data and // update the progress meter. Note that if ctx is cancelled, then this context is cancelled // too. When the 2/3 data loading processes are done, a goroutine will then run uiCtxCancel() // to stop the UI updates. c.psmlCtx, c.psmlCancelFn = context.WithCancel(c.thisSrcCtx) intPsmlCtx, intPsmlCancelFn := context.WithCancel(context.Background()) // signalling psml done to the goroutine that started //====================================================================== // Make sure data is cleared before we signal we're starting. This gives callbacks a clean // view, not the old view of a loader with old data. c.Lock() c.PacketPsmlData = make([][]string, 0) c.PacketPsmlColors = make([]PacketColors, 0) c.PacketPsmlHeaders = make([]string, 0, 10) c.PacketCache.Purge() c.LoadWasCancelled = false c.StartStage2Chan = make(chan struct{}) // do this before signalling start c.Unlock() // signal to updater that we're about to start. This will block until cb completes c.signalPsmlStarting(cb) defer func() { c.signalPsmlDone(cb) }() //====================================================================== var psmlOut io.ReadCloser // Only start this process if we are in interface mode var err error var pr *os.File var pw *os.File //====================================================================== // Make sure we start the goroutine that monitors for shutdown early - so if/when // a shutdown happens, and we get blocked in the XML parser, this will be able to // respond termshark.TrackedGo(func() { select { case <-c.psmlCtx.Done(): intPsmlCancelFn() // start internal shutdown case <-intPsmlCtx.Done(): } if c.tailCmd != nil { err := termshark.KillIfPossible(c.tailCmd) if err != nil { log.Infof("Did not kill tail process: %v", err) } } if c.PsmlCmd != nil { err := termshark.KillIfPossible(c.PsmlCmd) if err != nil { log.Infof("Did not kill psml process: %v", err) } } if psmlOut != nil { psmlOut.Close() // explicitly close else this goroutine can block } }, Goroutinewg) //====================================================================== // Set to true by a goroutine started within here if ctxCancel() is called i.e. the outer context if c.displayFilter == "" || c.ReadingFromFifo() { // don't hold up pdml and pcap generators. If the filter is "", then the frame numbers // equal the row numbers, so we don't need the psml to map from row -> frame. // // And, if we are in interface mode, we won't reach the end of the psml anyway. // close(c.StartStage2Chan) } //====================================================================== closePipe := func() { if pw != nil { pw.Close() pw = nil } if pr != nil { pr.Close() pr = nil } } if c.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. // pr, pw, err = os.Pipe() if err != nil { err = fmt.Errorf("Could not create pipe: %v", err) HandleError(err, cb) intPsmlCancelFn() return } // pw is used as Stdout for the tail command, which unwinds in this // goroutine - so we can close at this point in the unwinding. pr // is used as stdin for the psml command, which also runs in this // goroutine. defer closePipe() // 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. c.PcapPsml = &tailReadTracker{ tailReader: pr, loader: c, callback: cb, } } c.Lock() c.PsmlCmd = c.cmds.Psml(c.PcapPsml, c.displayFilter) c.Unlock() psmlOut, err = c.PsmlCmd.StdoutReader() if err != nil { err = fmt.Errorf("Could not access pipe output: %v", err) HandleError(err, cb) intPsmlCancelFn() return } err = c.PsmlCmd.Start() if err != nil { err = fmt.Errorf("Error starting PSML command %v: %v", c.PsmlCmd, err) HandleError(err, cb) intPsmlCancelFn() return } log.Infof("Started PSML command %v with pid %d", c.PsmlCmd, c.PsmlCmd.Pid()) defer func() { // These need to close so the tailreader Read() terminates so that the // PsmlCmd.Wait() below completes. closePipe() err := c.PsmlCmd.Wait() if !c.suppressErrors { if err != nil { if _, ok := err.(*exec.ExitError); ok { cerr := gowid.WithKVs(termshark.BadCommand, map[string]interface{}{ "command": c.PsmlCmd.String(), "error": err, }) HandleError(cerr, cb) } } // If the psml command generates an error, then we should stop any feed // from the interface too. if c.ifaceCancelFn != nil { c.ifaceCancelFn() } } }() //====================================================================== // If it was cancelled, then we don't need to start the tail process because // psml will read from the tmp pcap file generated by the interface reading // process. c.tailCmd = nil if c.ReadingFromFifo() { c.tailCmd = c.cmds.Tail(c.ifaceFile) c.tailCmd.SetStdout(pw) // 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. watcher, err := fsnotify.NewWatcher() if err != nil { err = fmt.Errorf("Could not create FS watch: %v", err) HandleError(err, cb) intPsmlCancelFn() return } defer watcher.Close() if err := watcher.Add(filepath.Dir(c.ifaceFile)); err != nil { err = fmt.Errorf("Could not set up watcher for %s: %v", c.ifaceFile, err) HandleError(err, cb) intPsmlCancelFn() return } else { // If it's there, touch it so watcher below is notified that everything is in order if _, err := os.Stat(c.ifaceFile); err == nil { if err = os.Chtimes(c.ifaceFile, time.Now(), time.Now()); err != nil { HandleError(err, cb) intPsmlCancelFn() return } } } defer func() { watcher.Remove(filepath.Dir(c.ifaceFile)) }() Loop: for { select { case fe := <-watcher.Events: if fe.Name == c.ifaceFile { break Loop } case err := <-watcher.Errors: err = fmt.Errorf("Unexpected watcher error for %s: %v", c.ifaceFile, err) HandleError(err, cb) intPsmlCancelFn() return case <-intPsmlCtx.Done(): return } } log.Infof("Starting Tail command: %v", c.tailCmd) err = c.tailCmd.Start() if err != nil { err = fmt.Errorf("Could not start tail command %v: %v", c.tailCmd, err) HandleError(err, cb) intPsmlCancelFn() return } // Do this in a goroutine - in a defer, it would block here before the code executes defer func() { c.tailCmd.Wait() // this will block the exit of this function until the command is killed // We need to explicitly close the write side of the pipe. Without this, // the PSML process Wait() function won't complete, because golang won't // complete termination until IO has finished, and io.Copy() will be stuck // in a loop. pw.Close() }() } //====================================================================== // // Goroutine to read psml xml and update data structures // defer func() { select { case <-c.StartStage2Chan: // already done/closed, do nothing default: close(c.StartStage2Chan) } // This will kill the tail process if there is one intPsmlCancelFn() // stop the ticker }() d := xml.NewDecoder(psmlOut) // //
1
//
0.000000
//
192.168.44.123
//
192.168.44.213
//
TFTP
//
77
//
Read Request, File: C:\IBMTCPIP\lccm.1, Transfer type: octet
//
var curPsml []string var fg string var bg string ready := false empty := true structure := false for { if intPsmlCtx.Err() != nil { break } tok, err := d.Token() if err != nil { if err != io.EOF && !c.LoadWasCancelled { err = fmt.Errorf("Could not read PSML data: %v", err) HandleError(err, cb) } break } switch tok := tok.(type) { case xml.EndElement: switch tok.Name.Local { case "structure": structure = false case "packet": c.Lock() c.PacketPsmlData = append(c.PacketPsmlData, curPsml) c.PacketPsmlColors = append(c.PacketPsmlColors, PacketColors{ FG: psmlColorToIColor(fg), BG: psmlColorToIColor(bg), }) c.Unlock() case "section": ready = false // Means we got without any char data i.e. empty
if empty { curPsml = append(curPsml, "") } } case xml.StartElement: switch tok.Name.Local { case "structure": structure = true case "packet": curPsml = make([]string, 0, 10) 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 { c.Lock() c.PacketPsmlHeaders = append(c.PacketPsmlHeaders, string(tok)) c.Unlock() } else { curPsml = append(curPsml, string(format.TranslateHexCodes(tok))) empty = false } } } } } // dumpcap -i eth0 -w /tmp/foo.pcap // dumpcap -i /dev/fd/3 -w /tmp/foo.pcap func (c *Loader) loadIfaceAsync(cb interface{}) { c.totalFifoBytesWritten = gwutil.NoneInt64() c.ifaceCtx, c.ifaceCancelFn = context.WithCancel(c.thisSrcCtx) defer func() { ch := c.IfaceFinishedChan c.IfaceFinishedChan = make(chan struct{}) close(ch) c.ifaceCtx = nil c.ifaceCancelFn = nil }() c.ifaceCmd = c.cmds.Iface(c.psrc.Name(), c.captureFilter, c.ifaceFile) log.Infof("Starting Iface command: %v", c.ifaceCmd) err := c.ifaceCmd.Start() if err != nil { err = fmt.Errorf("Error starting interface reader %v: %v", c.ifaceCmd, err) HandleError(err, cb) return } termshark.TrackedGo(func() { // Wait for external cancellation. This is the shutdown procedure. <-c.ifaceCtx.Done() err := termshark.KillIfPossible(c.ifaceCmd) if err != nil { log.Infof("Did not kill iface reader process: %v", err) } }, Goroutinewg) 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. if cl, ok := c.psrc.(io.Closer); ok { cl.Close() } }() err = c.ifaceCmd.Wait() // it definitely started, so we must wait if !c.suppressErrors && 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": c.ifaceCmd.String(), "error": err, }) HandleError(cerr, cb) } } // If something killed it, then start the internal shutdown procedure anyway to clean up // goroutines waiting on the context. This could also happen if tshark -i is reading from // a fifo and the write has stopped e.g. // // cat foo.pcap > myfifo // termshark -i myfifo // // termshark will get EOF when the cat terminates (if there are no more writers). // // 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(c.ifaceFile) 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 c.fifoError == nil && !os.IsNotExist(err) { // Ignore ENOENT because it means there was an error before dumpcap even wrote // anything to disk c.fifoError = err } } else { c.totalFifoBytesWritten = gwutil.SomeInt64(fi.Size()) } c.checkAllBytesRead(cb) c.ifaceCancelFn() } //====================================================================== 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 Cancel bool } func (m *LoadPcapSlice) String() string { if m.Cancel { return fmt.Sprintf("[loadslice: %d, cancel: %v]", m.Row, m.Cancel) } else { return fmt.Sprintf("[loadslice: %d]", m.Row) } } //====================================================================== type ICacheUpdater interface { WhenLoadingPdml() WhenNotLoadingPdml() } type ICacheLoader interface { State() LoaderState SetState(LoaderState) loadIsNecessary(ev LoadPcapSlice) bool stopLoadPdml() startLoadPdml(int, interface{}) } func ProcessPdmlRequests(requests []LoadPcapSlice, loader ICacheLoader, updater ICacheUpdater) []LoadPcapSlice { Loop: for { if len(requests) == 0 { break } else { ev := requests[0] if loader.loadIsNecessary(ev) { if loader.State()&LoadingPdml != 0 { // we are loading a piece. Do we need to cancel? If not, reschedule for when idle if ev.Cancel { loader.stopLoadPdml() } updater.WhenNotLoadingPdml() } else { loader.startLoadPdml(ev.Row, updater) loader.SetState(loader.State() | LoadingPdml) updater.WhenLoadingPdml() } break Loop } else { requests = requests[1:] } } } return requests } //====================================================================== func psmlColorToIColor(col string) gowid.IColor { if res, err := gowid.MakeRGBColorSafe(col); err != nil { return nil } else { return res } } //====================================================================== // Local Variables: // mode: Go // fill-column: 78 // End: termshark-2.0.3/pcap/loader_test.go000066400000000000000000000352161360044163000172770ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. package pcap import ( "bytes" "context" "fmt" "io" "io/ioutil" "os" "strings" "sync" "testing" "time" "net/http" _ "net/http" _ "net/http/pprof" "github.com/gcla/termshark/v2" "github.com/stretchr/testify/assert" log "github.com/sirupsen/logrus" ) //====================================================================== var ensureGoroutinesStopWG sync.WaitGroup func init() { go func() { log.Println(http.ListenAndServe("0.0.0.0:6060", nil)) }() Goroutinewg = &ensureGoroutinesStopWG } //====================================================================== type pdmlAction struct{} func newPdmlAction() *pdmlAction { return &pdmlAction{} } func (p *pdmlAction) WhenLoadingPdml() { fmt.Printf("FURTHER ACTION: when loading pdml\n") } func (p *pdmlAction) WhenNotLoadingPdml() { fmt.Printf("FURTHER ACTION: when not loading pdml\n") } //====================================================================== type iGoProc interface { StopGR() } type procReader struct { io.ReadCloser iGoProc } var _ io.Reader = (*procReader)(nil) func (r *procReader) Read(p []byte) (int, error) { n, err := r.ReadCloser.Read(p) //fmt.Printf("Read result %d, %v\n", n, err) if err != nil { r.StopGR() } return n, err } //====================================================================== type iStopLoop interface { shouldStop() error } type bodyMakerFn func() io.ReadCloser type loopReader struct { maker bodyMakerFn loops int stopper iStopLoop body io.ReadCloser numdone int } var _ io.Reader = (*loopReader)(nil) func newLoopReader(maker bodyMakerFn, loops int, stopper iStopLoop) *loopReader { res := &loopReader{ maker: maker, loops: loops, stopper: stopper, } res.body = maker() return res } func (r *loopReader) Read(p []byte) (int, error) { read := 0 for read < len(p) { if r.numdone == r.loops { return read, io.EOF } req := len(p[read:]) n, err := r.body.Read(p[read:]) read += n if err != nil { if err != io.EOF { return read, err } r.numdone += 1 // EOF r.body.Close() r.body = r.maker() if r.stopper != nil { err = r.stopper.shouldStop() if err != nil { return read, err } } } else if n < req { break } } return read, nil } //====================================================================== // Implements io.Reader - combines header, looping body and footer from disk type pcapLoopReader struct { io.Reader } var _ io.Reader = (*pcapLoopReader)(nil) func newPcapLoopReader(prefix string, suffix string, loops int, stopper iStopLoop) *pcapLoopReader { looper := newLoopReader(func() io.ReadCloser { file, err := os.Open(fmt.Sprintf("testdata/%s.%s-body", prefix, suffix)) if err != nil { panic(err) } return file }, loops, stopper) fileh, err := os.Open(fmt.Sprintf("testdata/%s.%s-header", prefix, suffix)) if err != nil { panic(err) } filef, err := os.Open(fmt.Sprintf("testdata/%s.%s-footer", prefix, suffix)) if err != nil { panic(err) } res := &pcapLoopReader{ Reader: io.MultiReader(fileh, looper, filef), } return res } //====================================================================== // Provide Tail, Pdml, etc based on files on disk type procsFromPrefix struct { prefix string } var _ ILoaderCmds = procsFromPrefix{} func makeProcsFromPrefix(pref string) procsFromPrefix { return procsFromPrefix{prefix: pref} } func (g procsFromPrefix) Iface(iface string, captureFilter string, tmpfile string) IBasicCommand { panic(fmt.Errorf("Should not need")) } func (g procsFromPrefix) Tail(tmpfile string) ITailCommand { panic(fmt.Errorf("Should not need")) } func (g procsFromPrefix) Psml(pcap interface{}, filter string) IPcapCommand { file, err := os.Open(fmt.Sprintf("testdata/%s.psml", g.prefix)) if err != nil { panic(err) } return newSimpleCmd(file) } func (g procsFromPrefix) Pcap(pcap string, filter string) IPcapCommand { file, err := os.Open(fmt.Sprintf("testdata/%s.pcap", g.prefix)) if err != nil { panic(err) } return newSimpleCmd(file) } func (g procsFromPrefix) Pdml(pcap string, filter string) IPcapCommand { file, err := os.Open(fmt.Sprintf("testdata/%s.pdml", g.prefix)) if err != nil { panic(err) } return newSimpleCmd(file) } //====================================================================== type loopingProcs struct { prefix string loops int } var _ ILoaderCmds = loopingProcs{} func makeLoopingProcs(pref string, loops int) loopingProcs { return loopingProcs{prefix: pref, loops: loops} } func (g loopingProcs) Iface(iface string, captureFilter string, tmpfile string) IBasicCommand { panic(fmt.Errorf("Should not need")) } func (g loopingProcs) Tail(tmpfile string) ITailCommand { panic(fmt.Errorf("Should not need")) } func (g loopingProcs) Psml(pcap interface{}, filter string) IPcapCommand { rd := newPcapLoopReader(g.prefix, "psml", g.loops, nil) return newSimpleCmd(rd) } func (g loopingProcs) Pcap(pcap string, filter string) IPcapCommand { rd := newPcapLoopReader(g.prefix, "pcap", g.loops, nil) return newSimpleCmd(rd) } func (g loopingProcs) Pdml(pcap string, filter string) IPcapCommand { rd := newPcapLoopReader(g.prefix, "pdml", g.loops, nil) return newSimpleCmd(rd) } //====================================================================== // A pretend external command - when started, runs a goroutine that waits until stopped type simpleCmd struct { pcap string filter string out io.Writer pipe io.ReadCloser started bool dead bool ctx context.Context // cancels the iface reader process cancel context.CancelFunc } func newSimpleCmd(rd io.Reader) *simpleCmd { res := &simpleCmd{} var rc io.ReadCloser var ok bool rc, ok = rd.(io.ReadCloser) if !ok { rc = ioutil.NopCloser(rd) } res.pipe = &procReader{ ReadCloser: rc, iGoProc: res, } return res } func (f *simpleCmd) StopGR() { f.cancel() } func (f *simpleCmd) Start() error { if f.started { return fmt.Errorf("Started already") } if f.dead { return fmt.Errorf("Started already and dead") } f.ctx, f.cancel = context.WithCancel(context.Background()) termshark.TrackedGo(func() { select { case <-f.ctx.Done(): // terminate } }, Goroutinewg) f.started = true return nil } func (f *simpleCmd) Wait() error { if !f.started { return fmt.Errorf("Not started yet") } if f.dead { return fmt.Errorf("Dead already") } select { case <-f.ctx.Done(): f.dead = true } return nil } func (f *simpleCmd) StdoutReader() (io.ReadCloser, error) { return f.pipe, nil } func (f *simpleCmd) SetStdout(w io.WriteCloser) { f.out = w } func (f *simpleCmd) Kill() error { f.cancel() return nil } func (f *simpleCmd) Signal(s os.Signal) error { f.cancel() return nil } func (f *simpleCmd) Pid() int { return 1001 } func (f *simpleCmd) String() string { return fmt.Sprintf("SimpleCmd: pcap=%s filter=%s", f.pcap, f.filter) } //====================================================================== // While tshark processes are running, signal (via close) when AfterEnd is triggered type waitForEnd struct { end chan struct{} } var _ IOnError = (*waitForEnd)(nil) var _ IClear = (*waitForEnd)(nil) var _ IBeforeBegin = (*waitForEnd)(nil) var _ IAfterEnd = (*waitForEnd)(nil) func newWaitForEnd() *waitForEnd { return &waitForEnd{ end: make(chan struct{}), } } func (p *waitForEnd) BeforeBegin(closeMe chan<- struct{}) { close(closeMe) } func (p *waitForEnd) AfterEnd(closeMe chan<- struct{}) { close(closeMe) close(p.end) } func (p *waitForEnd) OnClear(closeMe chan<- struct{}) { close(closeMe) } func (p *waitForEnd) OnError(err error, closeMe chan<- struct{}) { close(closeMe) panic(err) } //====================================================================== type waitForClear struct { end chan struct{} } var _ IOnError = (*waitForClear)(nil) var _ IClear = (*waitForClear)(nil) var _ IBeforeBegin = (*waitForClear)(nil) var _ IAfterEnd = (*waitForClear)(nil) func newWaitForClear() *waitForClear { return &waitForClear{ end: make(chan struct{}), } } func (p *waitForClear) BeforeBegin(closeMe chan<- struct{}) { close(closeMe) } func (p *waitForClear) AfterEnd(closeMe chan<- struct{}) { close(closeMe) } func (p *waitForClear) OnClear(closeMe chan<- struct{}) { close(p.end) close(closeMe) } func (p *waitForClear) OnError(err error, closeMe chan<- struct{}) { close(closeMe) panic(err) } //====================================================================== type enabler struct { val *bool } func (e enabler) EnableOperations() { *e.val = true } //====================================================================== func TestSimpleCmd(t *testing.T) { p := newSimpleCmd(bytes.NewReader([]byte("hello world"))) err := p.Start() assert.NoError(t, err) so, err := p.StdoutReader() assert.NoError(t, err) read, err := ioutil.ReadAll(so) assert.NoError(t, err) assert.Equal(t, "hello world", string(read)) err = p.Wait() assert.NoError(t, err) } func TestLoopReader1(t *testing.T) { maker := func() io.ReadCloser { return ioutil.NopCloser(strings.NewReader("hello")) } looper := newLoopReader(maker, 3, nil) read, err := ioutil.ReadAll(looper) assert.NoError(t, err) assert.Equal(t, "hellohellohello", string(read)) looper = newLoopReader(maker, 3, nil) ball := make([]byte, 0) b1 := make([]byte, 1) var n int err = nil for err != io.EOF { n, err = looper.Read(b1) if err != io.EOF { assert.Equal(t, 1, n) ball = append(ball, b1...) } } assert.Equal(t, "hellohellohello", string(ball)) } //====================================================================== // Load psml+pdml+pcap from testdata/1.pcap, validate the data func TestSinglePcap(t *testing.T) { loader := NewPcapLoader(makeProcsFromPrefix("1")) assert.NotEqual(t, nil, loader) // Save now because when psml load finishes, a new one is created psmlFinChan := loader.PsmlFinishedChan pdmlFinChan := loader.Stage2FinishedChan enabled := false updater := struct { *pdmlAction *waitForEnd enabler }{ newPdmlAction(), newWaitForEnd(), enabler{&enabled}, } done := make(chan struct{}, 1) loader.doLoadPcapOperation("abc", "def", updater, func() { close(done) }) <-updater.end <-done assert.Equal(t, 18, len(loader.PacketPsmlData)) assert.Equal(t, "192.168.86.246", loader.PacketPsmlData[0][2]) <-psmlFinChan assert.Equal(t, LoaderState(LoadingPsml), loader.State()) loader.SetState(loader.State() & ^LoadingPsml) // No pdml yet _, ok := loader.PacketCache.Get(0) assert.Equal(t, false, ok) //further := pdmlAction{} enabled = false updater = struct { *pdmlAction *waitForEnd enabler }{ newPdmlAction(), newWaitForEnd(), enabler{&enabled}, } instructions := []LoadPcapSlice{{0, false}} instructionsAfter := ProcessPdmlRequests(instructions, loader, updater) assert.Equal(t, LoaderState(LoadingPdml), loader.State()) assert.Equal(t, 1, len(instructionsAfter)) // not done yet - need to get to right state instructionsAfter = ProcessPdmlRequests(instructions, loader, updater) <-pdmlFinChan assert.Equal(t, LoaderState(LoadingPdml), loader.State()) loader.SetState(loader.State() & ^LoadingPdml) assert.Equal(t, 0, len(instructionsAfter)) cei, ok := loader.PacketCache.Get(0) assert.Equal(t, true, ok) ce := cei.(CacheEntry) assert.Equal(t, true, ce.PdmlComplete) assert.Equal(t, true, ce.PcapComplete) assert.Equal(t, 18, len(ce.Pdml)) assert.Equal(t, 18, len(ce.Pcap)) } func TestLoopingPcap(t *testing.T) { for i, loops := range []int{1, 5, 100} { // The "2" loads up testdata/2.{psml,pdml,pcap}-{header,body,footer} loader := NewPcapLoader(makeLoopingProcs("2", loops)) // Make sure we can re-use the same loader, because that's what termshark does for j, _ := range []int{1, 2} { assert.NotEqual(t, nil, loader) // Save now because when psml load finishes, a new one is created psmlFinChan := loader.PsmlFinishedChan pdmlFinChan := loader.Stage2FinishedChan // make sure each time round it tries to load a "new" pcap - otherwise the loader // returns early, and this test is set up to wait until we get the AfterEnd signal fakePcap := fmt.Sprintf("%d-%d", i, j) enabled := false updater := struct { *pdmlAction *waitForEnd enabler }{ newPdmlAction(), newWaitForEnd(), enabler{&enabled}, } done := make(chan struct{}, 1) loader.doLoadPcapOperation(fakePcap, "def", updater, func() { close(done) }) <-updater.end <-done assert.Equal(t, loops, len(loader.PacketPsmlData)) assert.Equal(t, "192.168.44.123", loader.PacketPsmlData[0][2]) <-psmlFinChan assert.Equal(t, LoaderState(LoadingPsml), loader.State()) loader.SetState(loader.State() & ^LoadingPsml) // No pdml yet _, ok := loader.PacketCache.Get(0) assert.Equal(t, false, ok) updater = struct { *pdmlAction *waitForEnd enabler }{ newPdmlAction(), newWaitForEnd(), enabler{&enabled}, } instructions := []LoadPcapSlice{{0, false}} instructionsAfter := ProcessPdmlRequests(instructions, loader, updater) assert.Equal(t, LoaderState(LoadingPdml), loader.State()) assert.Equal(t, 1, len(instructionsAfter)) // not done yet - need to get to right state instructionsAfter = ProcessPdmlRequests(instructions, loader, updater) <-pdmlFinChan assert.Equal(t, LoaderState(LoadingPdml), loader.State()) loader.SetState(loader.State() & ^LoadingPdml) assert.Equal(t, 0, len(instructionsAfter)) cei, ok := loader.PacketCache.Get(0) assert.Equal(t, true, ok) ce := cei.(CacheEntry) assert.Equal(t, true, ce.PdmlComplete) assert.Equal(t, true, ce.PcapComplete) assert.Equal(t, loops, len(ce.Pdml)) assert.Equal(t, loops, len(ce.Pcap)) assert.Equal(t, loader.State(), LoaderState(0)) fmt.Printf("about to clear\n") done = make(chan struct{}, 1) waitForClear := newWaitForClear() loader.doClearPcapOperation(waitForClear, func() { close(done) }) <-done assert.Equal(t, loader.State(), LoaderState(0)) <-waitForClear.end assert.Equal(t, 0, len(loader.PacketPsmlData)) _, ok = loader.PacketCache.Get(0) assert.Equal(t, false, ok) } } } //====================================================================== func TestKeepThisLast(t *testing.T) { fmt.Printf("Waiting for test goroutines to stop\n") done := make(chan struct{}) go func() { select { case <-done: return case <-time.After(10 * time.Second): assert.FailNow(t, "Not all test goroutines terminated in 10s") } }() Goroutinewg.Wait() close(done) fmt.Printf("Done waiting for test goroutines to stop\n") } //====================================================================== // Local Variables: // mode: Go // fill-column: 78 // End: termshark-2.0.3/pcap/loader_tshark_test.go000066400000000000000000000366061360044163000206570ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. // +build tshark package pcap import ( "bytes" "fmt" "io" "os" "regexp" "strings" "sync" "testing" "time" "github.com/gcla/termshark/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.0.3/pcap/pdml.go000066400000000000000000000051541360044163000157240ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. package pcap import ( "bytes" "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.0.3/pcap/source.go000066400000000000000000000055511360044163000162710ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. package pcap import ( "os" "github.com/gcla/termshark/v2/system" ) //====================================================================== type IPacketSource interface { Name() string IsFile() bool IsInterface() bool IsFifo() bool IsPipe() bool } //====================================================================== 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 } //====================================================================== type TemporaryFileSource struct { FileSource } type ISourceRemover interface { Remove() error } func (p TemporaryFileSource) Remove() error { return os.Remove(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 } //====================================================================== 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 } //====================================================================== 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 } //====================================================================== // Local Variables: // mode: Go // fill-column: 78 // End: termshark-2.0.3/pcap/testdata/000077500000000000000000000000001360044163000162455ustar00rootroot00000000000000termshark-2.0.3/pcap/testdata/1.pcap000066400000000000000000000044701360044163000172570ustar00rootroot00000000000000ò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.0.3/pcap/testdata/1.psml000066400000000000000000000112761360044163000173110ustar00rootroot00000000000000
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.0.3/pcap/testdata/2.pcap-body000066400000000000000000000001351360044163000202050ustar00rootroot00000000000000*9RMM@ 5+Y)E79@,{,9E#sC:\IBMTCPIP\lccm.1octettermshark-2.0.3/pcap/testdata/2.pcap-footer000066400000000000000000000000001360044163000205350ustar00rootroot00000000000000termshark-2.0.3/pcap/testdata/2.pcap-header000066400000000000000000000000301360044163000204720ustar00rootroot00000000000000òtermshark-2.0.3/pcap/testdata/2.pdml-body000066400000000000000000000252631360044163000202270ustar00rootroot00000000000000 termshark-2.0.3/pcap/testdata/2.pdml-footer000066400000000000000000000000121360044163000205510ustar00rootroot00000000000000 termshark-2.0.3/pcap/testdata/2.pdml-header000066400000000000000000000005511360044163000205130ustar00rootroot00000000000000 termshark-2.0.3/pcap/testdata/2.psml-body000066400000000000000000000004061360044163000202360ustar00rootroot00000000000000
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.0.3/pcap/testdata/2.psml-footer000066400000000000000000000000111360044163000205670ustar00rootroot00000000000000 termshark-2.0.3/pcap/testdata/2.psml-header000066400000000000000000000004441360044163000205330ustar00rootroot00000000000000
No.
Time
Source
Destination
Protocol
Length
Info
termshark-2.0.3/pdmltree/000077500000000000000000000000001360044163000153255ustar00rootroot00000000000000termshark-2.0.3/pdmltree/pdmltree.go000066400000000000000000000202361360044163000174730ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package pdmltree 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-bottom-unselected", ColSelected: "hex-bottom-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-top-unselected", ColSelected: "hex-top-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.0.3/pdmltree/pdmltree_test.go000066400000000000000000000705021360044163000205330ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. package pdmltree import ( "testing" "github.com/stretchr/testify/assert" ) //====================================================================== var p1 string = ` ` func TestPdml1(t *testing.T) { 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.0.3/psmltable/000077500000000000000000000000001360044163000154745ustar00rootroot00000000000000termshark-2.0.3/psmltable/model.go000066400000000000000000000072441360044163000171320ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. package psmltable import ( "sort" "github.com/gcla/gowid" "github.com/gcla/gowid/widgets/button" "github.com/gcla/gowid/widgets/columns" "github.com/gcla/gowid/widgets/holder" "github.com/gcla/gowid/widgets/isselected" "github.com/gcla/gowid/widgets/styled" "github.com/gcla/gowid/widgets/table" "github.com/gcla/gowid/widgets/text" "github.com/gcla/termshark/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 + " ") label = button.NewBare(label) sorters := c.Comparators if sorters != nil { sorteri := sorters[i2] if sorteri != nil { bmid := button.NewBare(text.New("-")) bfor := button.NewBare(text.New("^")) brev := button.NewBare(text.New("v")) bh := holder.New(bmid) bhs[i] = bh bms[i] = bmid action := func(rev bool, next *button.Widget, app gowid.IApp) { sorter := &table.SimpleTableByColumn{ SimpleModel: c.SimpleModel, Column: i2, } if rev { sort.Sort(sort.Reverse(sorter)) } else { sort.Sort(sorter) } bh.SetSubWidget(next, app) for j, bhj := range bhs { if j != i2 { bhj.SetSubWidget(bms[j], app) } } } bmid.OnClick(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, widget gowid.IWidget) { action(false, bfor, app) })) bfor.OnClick(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, widget gowid.IWidget) { action(true, brev, app) })) brev.OnClick(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, widget gowid.IWidget) { action(false, bfor, app) })) all = columns.NewFixed(label, styled.NewFocus(bh, gowid.MakeStyledAs(gowid.StyleReverse))) } } var w gowid.IWidget if c.Style.HeaderStyleProvided { w = isselected.New( styled.New( all, c.GetStyle().HeaderStyleNoFocus, ), styled.New( all, c.GetStyle().HeaderStyleSelected, ), styled.New( all, c.GetStyle().HeaderStyleFocus, ), ) } else { w = styled.NewExt( all, nil, gowid.MakeStyledAs(gowid.StyleReverse), ) } res = append(res, w) } } return res } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.0.3/scripts/000077500000000000000000000000001360044163000152005ustar00rootroot00000000000000termshark-2.0.3/scripts/do-release.sh000077500000000000000000000005261360044163000175620ustar00rootroot00000000000000#!/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.0.3/streams/000077500000000000000000000000001360044163000151675ustar00rootroot00000000000000termshark-2.0.3/streams/follow.go000066400000000000000000001563411360044163000170320ustar00rootroot00000000000000// 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 { ch := make(chan struct{}) cb.OnStreamChunk(dl.(Bytes), ch) <-ch } } 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 { ch := make(chan struct{}) cb.OnStreamHeader(fh, ch) <-ch } } 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.0.3/streams/follow.peg000066400000000000000000000130511360044163000171660ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // // 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.0.3/streams/follow_test.go000066400000000000000000000125061360044163000200630ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. package 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.0.3/streams/loader.go000066400000000000000000000174011360044163000167670ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. package 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 iface reader process streamCancelFn context.CancelFunc indexerCtx context.Context // cancels the stream indexer 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 } func (c *Loader) StopLoad() { if c.streamCancelFn != nil { c.streamCancelFn() } } //====================================================================== type ITrackPayload interface { TrackPayloadPacket(packet int) } type IIndexerCallbacks interface { IOnStreamChunk ITrackPayload AfterIndexEnd(success bool, closeMe chan<- struct{}) } func (c *Loader) StartLoad(pcap string, proto string, idx int, app gowid.IApp, cb IIndexerCallbacks) { 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) defer func() { c.streamCtx = nil c.streamCancelFn = nil }() c.streamCmd = c.cmds.Stream(pcapf, proto, idx) streamOut, err := c.streamCmd.StdoutReader() if err != nil { pcap.HandleError(err, cb) return } pcap.HandleBegin(cb) defer func() { pcap.HandleEnd(cb) }() err = c.streamCmd.Start() if err != nil { err = fmt.Errorf("Error starting stream reassembly %v: %v", c.streamCmd, err) pcap.HandleError(err, cb) return } log.Infof("Started stream reassembly command %v with pid %d", c.streamCmd, c.streamCmd.Pid()) defer func() { err = c.streamCmd.Wait() // it definitely started, so we must wait 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(cerr, cb) } } }() termshark.TrackedGo(func() { // Wait for external cancellation. This is the shutdown procedure. <-c.streamCtx.Done() err := termshark.KillIfPossible(c.streamCmd) if err != nil { log.Infof("Did not kill stream reassembly process: %v", err) } }, Goroutinewg) 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.StopLoad() } func (c *Loader) startStreamIndexerAsync(pcapf string, proto string, idx int, app gowid.IApp, cb IIndexerCallbacks) { res := false c.indexerCtx, c.indexerCancelFn = context.WithCancel(c.mainCtx) defer func() { c.indexerCtx = nil c.indexerCancelFn = nil }() c.indexerCmd = c.cmds.Indexer(pcapf, proto, idx) streamOut, err := c.indexerCmd.StdoutReader() if err != nil { pcap.HandleError(err, cb) return } defer func() { ch := make(chan struct{}) cb.AfterIndexEnd(res, ch) <-ch }() err = c.indexerCmd.Start() if err != nil { err = fmt.Errorf("Error starting stream indexer %v: %v", c.indexerCmd, err) pcap.HandleError(err, cb) return } log.Infof("Started stream indexer command %v with pid %d", c.indexerCmd, c.indexerCmd.Pid()) defer func() { err = c.indexerCmd.Wait() // it definitely started, so we must wait 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(cerr, cb) } } }() termshark.TrackedGo(func() { // Wait for external cancellation. This is the shutdown procedure. <-c.indexerCtx.Done() err := termshark.KillIfPossible(c.indexerCmd) if err != nil { log.Infof("Did not kill indexer process: %v", err) } // Stop main loop streamOut.Close() }, Goroutinewg) decodeStreamXml(streamOut, proto, c.indexerCtx, cb) res = true c.indexerCancelFn() } func decodeStreamXml(streamOut io.Reader, proto string, ctx context.Context, cb ITrackPayload) { inTCP := false inUDP := false curPkt := 0 curDataLen := 0 d := xml.NewDecoder(streamOut) for { if ctx.Err() != nil { break } t, tokenErr := d.Token() if tokenErr != nil { if tokenErr == io.EOF { 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 } } } } } } } } } } //====================================================================== //====================================================================== // Local Variables: // mode: Go // fill-column: 78 // End: termshark-2.0.3/streams/loader_test.go000066400000000000000000004210201360044163000200220ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. package 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.0.3/streams/parse.go000066400000000000000000000053061360044163000166340ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. package streams import ( "encoding/hex" "fmt" "strings" ) //====================================================================== type IChunk interface { Direction() Direction StreamData() []byte } type IOnStreamChunk interface { OnStreamChunk(chunk IChunk, ch chan struct{}) } type IOnStreamHeader interface { OnStreamHeader(header FollowHeader, ch chan struct{}) } //====================================================================== 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.0.3/system/000077500000000000000000000000001360044163000150355ustar00rootroot00000000000000termshark-2.0.3/system/errors.go000066400000000000000000000011001360044163000166700ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. package 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.0.3/system/extcmds.go000066400000000000000000000004471360044163000170400ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // +build !darwin,!android,!windows package system var CopyToClipboard = []string{"xsel", "-i", "-b"} var OpenURL = []string{"xdg-open"} termshark-2.0.3/system/extcmds_android.go000066400000000000000000000004631360044163000205360ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. package system var CopyToClipboard = []string{"termux-clipboard-set"} var OpenURL = []string{"am", "start", "-a", "android.intent.action.VIEW", "-d"} termshark-2.0.3/system/extcmds_darwin.go000066400000000000000000000003641360044163000204020ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. package system var CopyToClipboard = []string{"pbcopy"} var OpenURL = []string{"open"} termshark-2.0.3/system/extcmds_windows.go000066400000000000000000000003661360044163000206120ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. package system var CopyToClipboard = []string{"clip"} var OpenURL = []string{"explorer"} termshark-2.0.3/system/fd.go000066400000000000000000000037331360044163000157630ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // +build !windows package system import ( "os" "syscall" "github.com/gcla/gowid" "github.com/pkg/errors" "golang.org/x/sys/unix" ) //====================================================================== type FSError string func (e FSError) Error() string { return string(e) } var ( DupError FSError = "Error duplicating descriptor." CloseError FSError = "Error closing file descriptor." OpenError FSError = "Error opening for read." _ error = DupError _ error = CloseError _ error = OpenError ) func MoveStdin() (int, error) { newinputfd, err := syscall.Dup(int(os.Stdin.Fd())) if err != nil { err = errors.WithStack(gowid.WithKVs(DupError, map[string]interface{}{ "descriptor": os.Stdin.Fd(), "detail": err, })) return -1, err } err = syscall.Close(int(os.Stdin.Fd())) if err != nil { err = errors.WithStack(gowid.WithKVs(CloseError, map[string]interface{}{ "descriptor": os.Stdin.Fd(), "detail": err, })) return -1, err } newstdin, err := syscall.Open("/dev/tty", syscall.O_RDONLY, 0) if err != nil { err = errors.WithStack(gowid.WithKVs(OpenError, map[string]interface{}{ "name": "/dev/tty", "detail": err, })) return -1, err } if newstdin != 0 { err = unix.Dup2(newstdin, 0) if err != nil { err = errors.WithStack(gowid.WithKVs(DupError, map[string]interface{}{ "descriptor": newstdin, "detail": err, })) return -1, err } err = syscall.Close(newstdin) if err != nil { err = errors.WithStack(gowid.WithKVs(OpenError, map[string]interface{}{ "descriptor": newstdin, "detail": err, })) return -1, err } } return newinputfd, nil } func CloseDescriptor(fd int) { syscall.Close(fd) } //====================================================================== // Local Variables: // mode: Go // fill-column: 78 // End: termshark-2.0.3/system/fd_windows.go000066400000000000000000000007311360044163000175300ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // +build windows package system import ( "fmt" ) func MoveStdin() (int, error) { return -1, fmt.Errorf("MoveStdin not implemented on Windows") } func CloseDescriptor(fd int) { } //====================================================================== // Local Variables: // mode: Go // fill-column: 78 // End: termshark-2.0.3/system/fdinfo.go000066400000000000000000000032561360044163000166370ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. package 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.0.3/system/have_fdinfo.go000066400000000000000000000005451360044163000176400ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // +build !linux,!android package system const HaveFdinfo = false //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.0.3/system/have_fdinfo_linux.go000066400000000000000000000005111360044163000210500ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. package system const HaveFdinfo = true //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.0.3/system/picker.go000066400000000000000000000006141360044163000166420ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // +build !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.0.3/system/picker_android.go000066400000000000000000000041071360044163000203430ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. package 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.0.3/system/signals.go000066400000000000000000000022361360044163000170270ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // // +build !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.0.3/system/signals_windows.go000066400000000000000000000015161360044163000206010ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. package 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.0.3/tailfile.go000066400000000000000000000010451360044163000156310ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. //+build !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.0.3/tailfile_windows.go000066400000000000000000000013031360044163000174000ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. package termshark import ( "os" "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.0.3/tty/000077500000000000000000000000001360044163000143315ustar00rootroot00000000000000termshark-2.0.3/tty/tty.go000066400000000000000000000032131360044163000154770ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // // +build !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.0.3/tty/tty_windows.go000066400000000000000000000011301360044163000172450ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. package 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.0.3/ui/000077500000000000000000000000001360044163000141265ustar00rootroot00000000000000termshark-2.0.3/ui/darkmode.go000066400000000000000000000016201360044163000162420ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. package 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.0.3/ui/dialog.go000066400000000000000000000052171360044163000157210ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package 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/text" "github.com/gcla/termshark/v2" "github.com/gcla/termshark/v2/widgets/appkeys" "github.com/gdamore/tcell" ) //====================================================================== var ( fixed gowid.RenderFixed flow gowid.RenderFlow hmiddle gowid.HAlignMiddle vmiddle gowid.VAlignMiddle YesNo *dialog.Widget PleaseWait *dialog.Widget ) func OpenMessage(msgt string, openOver gowid.ISettableComposite, app gowid.IApp) { maximizer := &dialog.Maximizer{} var al gowid.IHAlignment = hmiddle if strings.Count(msgt, "\n") > 0 { al = gowid.HAlignLeft{} } var view gowid.IWidget = text.New(msgt, text.Options{ Align: al, }) view = hpadding.New( view, hmiddle, gowid.RenderFixed{}, ) view = framed.NewSpace(view) view = appkeys.New( view, func(ev *tcell.EventKey, app gowid.IApp) bool { if ev.Rune() == 'z' { // maximize/unmaximize if maximizer.Maxed { maximizer.Unmaximize(YesNo, app) } else { maximizer.Maximize(YesNo, app) } return true } return false }, appkeys.Options{ ApplyBefore: true, }, ) YesNo = dialog.New( view, dialog.Options{ Buttons: dialog.CloseOnly, NoShadow: true, BackgroundStyle: gowid.MakePaletteRef("dialog"), BorderStyle: gowid.MakePaletteRef("dialog"), ButtonStyle: gowid.MakePaletteRef("dialog-buttons"), }, ) dialog.OpenExt(YesNo, openOver, fixed, fixed, app) } func OpenTemplatedDialog(container gowid.ISettableComposite, tmplName string, app gowid.IApp) { 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-buttons"), }, ) YesNo.Open(container, ratio(0.5), app) } func OpenPleaseWait(container gowid.ISettableComposite, app gowid.IApp) { PleaseWait.Open(container, fixed, app) } func ClosePleaseWait(app gowid.IApp) { PleaseWait.Close(app) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.0.3/ui/menu.go000066400000000000000000000124511360044163000154240ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package 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.0.3/ui/menuutil/000077500000000000000000000000001360044163000157705ustar00rootroot00000000000000termshark-2.0.3/ui/menuutil/menu.go000066400000000000000000000125241360044163000172670ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package 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.0.3/ui/messages.go000066400000000000000000000066671360044163000163030ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package ui contains user-interface functions and helpers for termshark. package ui import ( "fmt" "io" "log" "text/template" "github.com/blang/semver" "github.com/gcla/termshark/v2" "github.com/jessevdk/go-flags" ) //====================================================================== var TemplateData map[string]interface{} var Templates = template.Must(template.New("Help").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 "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 't' - In bytes view, switch hex ⟷ ascii '+/-' - Adjust horizontal split '' - Adjust vertical split '?' - Display help In the filter, type a wireshark display filter expression. Most terminals will support using the mouse! Try clicking the Close button. Use shift-left-mouse to copy and shift-right-mouse to paste.{{end}} {{define "CopyModeHelp"}}{{template "NameVer" .}} termshark is in copy-mode. You can press: 'q', 'c' - Exit copy-mode ctrl-c - Copy from selected widget left - Widen selection right - Narrow selection{{end}} '?' - Display copy-mode help `)) //====================================================================== func init() { TemplateData = map[string]interface{}{ "Version": termshark.Version, "FAQURL": termshark.FAQURL, "UserGuideURL": termshark.UserGuideURL, } } 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) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.0.3/ui/palette.go000066400000000000000000000276251360044163000161270ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package ui contains user-interface functions and helpers for termshark. package ui import ( "github.com/gcla/gowid" "github.com/gcla/termshark/v2/modeswap" ) //====================================================================== var ( LightGray gowid.GrayColor = gowid.MakeGrayColor("g74") MediumGray gowid.GrayColor = gowid.MakeGrayColor("g50") DarkGray gowid.GrayColor = gowid.MakeGrayColor("g35") BrightBlue gowid.RGBColor = gowid.MakeRGBColor("#08f") BrightGreen gowid.RGBColor = gowid.MakeRGBColor("#6f2") LightRed gowid.RGBColor = gowid.MakeRGBColor("#ebb") LightBlue gowid.RGBColor = gowid.MakeRGBColor("#abf") DarkRed gowid.RGBColor = gowid.MakeRGBColor("#311") DarkBlue gowid.RGBColor = gowid.MakeRGBColor("#01f") //====================================================================== // Regular mode // // 256 color < 256 color PktListRowSelectedBgReg *modeswap.Color = modeswap.New(MediumGray, gowid.ColorBlack) PktListRowFocusBgReg *modeswap.Color = modeswap.New(BrightBlue, gowid.ColorBlue) PktListCellSelectedFgReg *modeswap.Color = modeswap.New(gowid.ColorWhite, gowid.ColorWhite) PktListCellSelectedBgReg *modeswap.Color = modeswap.New(DarkGray, gowid.ColorBlack) PktStructSelectedBgReg *modeswap.Color = modeswap.New(MediumGray, gowid.ColorBlack) PktStructFocusBgReg *modeswap.Color = modeswap.New(BrightBlue, gowid.ColorBlue) HexTopUnselectedFgReg *modeswap.Color = modeswap.New(gowid.ColorWhite, gowid.ColorWhite) HexTopUnselectedBgReg *modeswap.Color = modeswap.New(MediumGray, gowid.ColorBlack) HexTopSelectedFgReg *modeswap.Color = modeswap.New(gowid.ColorWhite, gowid.ColorWhite) HexTopSelectedBgReg *modeswap.Color = modeswap.New(BrightBlue, gowid.ColorBlue) HexBottomUnselectedFgReg *modeswap.Color = modeswap.New(gowid.ColorBlack, gowid.ColorWhite) HexBottomUnselectedBgReg *modeswap.Color = modeswap.New(LightGray, gowid.ColorBlack) HexBottomSelectedFgReg *modeswap.Color = modeswap.New(gowid.ColorBlack, gowid.ColorWhite) HexBottomSelectedBgReg *modeswap.Color = modeswap.New(LightGray, gowid.ColorBlack) HexCurUnselectedFgReg *modeswap.Color = modeswap.New(gowid.ColorWhite, gowid.ColorBlack) HexCurUnselectedBgReg *modeswap.Color = modeswap.New(gowid.ColorBlack, gowid.ColorWhite) HexLineFgReg *modeswap.Color = modeswap.New(gowid.ColorBlack, gowid.ColorWhite) HexLineBgReg *modeswap.Color = modeswap.New(LightGray, gowid.ColorBlack) FilterValidBgReg *modeswap.Color = modeswap.New(BrightGreen, gowid.ColorGreen) StreamClientFg *modeswap.Color = modeswap.New(DarkRed, gowid.ColorWhite) StreamClientBg *modeswap.Color = modeswap.New(LightRed, gowid.ColorDarkRed) StreamServerFg *modeswap.Color = modeswap.New(DarkBlue, gowid.ColorWhite) StreamServerBg *modeswap.Color = modeswap.New(LightBlue, gowid.ColorBlue) RegularPalette gowid.Palette = gowid.Palette{ "default": gowid.MakePaletteEntry(gowid.ColorBlack, gowid.ColorWhite), "title": gowid.MakeForeground(gowid.ColorDarkRed), "pkt-list-row-focus": gowid.MakePaletteEntry(gowid.ColorWhite, PktListRowFocusBgReg), "pkt-list-cell-focus": gowid.MakePaletteEntry(gowid.ColorWhite, gowid.ColorPurple), "pkt-list-row-selected": gowid.MakePaletteEntry(gowid.ColorWhite, PktListRowSelectedBgReg), "pkt-list-cell-selected": gowid.MakePaletteEntry(PktListCellSelectedFgReg, PktListCellSelectedBgReg), "pkt-struct-focus": gowid.MakePaletteEntry(gowid.ColorWhite, PktStructFocusBgReg), "pkt-struct-selected": gowid.MakePaletteEntry(gowid.ColorWhite, PktStructSelectedBgReg), "filter-menu-focus": gowid.MakeStyledPaletteEntry(gowid.ColorBlack, gowid.ColorWhite, gowid.StyleBold), "filter-valid": gowid.MakePaletteEntry(gowid.ColorBlack, FilterValidBgReg), "filter-invalid": gowid.MakePaletteEntry(gowid.ColorBlack, gowid.ColorRed), "filter-intermediate": gowid.MakePaletteEntry(gowid.ColorBlack, gowid.ColorOrange), "dialog": gowid.MakePaletteEntry(gowid.ColorBlack, gowid.ColorYellow), "dialog-buttons": gowid.MakePaletteEntry(gowid.ColorYellow, gowid.ColorBlack), "button": gowid.MakePaletteEntry(gowid.ColorDarkBlue, ButtonBg), "button-focus": gowid.MakePaletteEntry(gowid.ColorWhite, gowid.ColorDarkBlue), "progress-default": gowid.MakeStyledPaletteEntry(gowid.ColorWhite, gowid.ColorBlack, gowid.StyleBold), "progress-complete": gowid.MakeStyleMod(gowid.MakePaletteRef("progress-default"), gowid.MakeBackground(gowid.ColorMagenta)), "progress-spinner": gowid.MakePaletteEntry(gowid.ColorYellow, gowid.ColorBlack), "hex-cur-selected": gowid.MakePaletteEntry(gowid.ColorWhite, gowid.ColorMagenta), "hex-cur-unselected": gowid.MakePaletteEntry(HexCurUnselectedFgReg, HexCurUnselectedBgReg), "hex-top-selected": gowid.MakePaletteEntry(HexTopSelectedFgReg, HexTopSelectedBgReg), "hex-top-unselected": gowid.MakePaletteEntry(HexTopUnselectedFgReg, HexTopUnselectedBgReg), "hex-bottom-selected": gowid.MakePaletteEntry(HexBottomSelectedFgReg, HexBottomSelectedBgReg), "hex-bottom-unselected": gowid.MakePaletteEntry(HexBottomUnselectedFgReg, HexBottomUnselectedBgReg), "hexln-selected": gowid.MakePaletteEntry(HexLineFgReg, HexLineBgReg), "hexln-unselected": gowid.MakePaletteEntry(HexLineFgReg, HexLineBgReg), "copy-mode-indicator": gowid.MakePaletteEntry(gowid.ColorWhite, gowid.ColorDarkRed), "copy-mode": gowid.MakePaletteEntry(gowid.ColorBlack, gowid.ColorYellow), "stream-client": gowid.MakePaletteEntry(StreamClientFg, StreamClientBg), "stream-server": gowid.MakePaletteEntry(StreamServerFg, StreamServerBg), "stream-match": gowid.MakePaletteEntry(gowid.ColorBlack, gowid.ColorYellow), "stream-search": gowid.MakePaletteEntry(gowid.ColorWhite, gowid.ColorBlack), } //====================================================================== // Dark mode // // 256 color < 256 color ButtonBg *modeswap.Color = modeswap.New(LightGray, gowid.ColorWhite) PktListRowSelectedFgDark *modeswap.Color = modeswap.New(gowid.ColorWhite, gowid.ColorBlack) PktListRowSelectedBgDark *modeswap.Color = modeswap.New(DarkGray, gowid.ColorWhite) PktListRowFocusBgDark *modeswap.Color = modeswap.New(BrightBlue, gowid.ColorBlue) PktListCellSelectedFgDark *modeswap.Color = modeswap.New(gowid.ColorWhite, gowid.ColorBlack) PktListCellSelectedBgDark *modeswap.Color = modeswap.New(MediumGray, gowid.ColorWhite) PktStructSelectedFgDark *modeswap.Color = modeswap.New(gowid.ColorWhite, gowid.ColorBlack) PktStructSelectedBgDark *modeswap.Color = modeswap.New(DarkGray, gowid.ColorWhite) PktStructFocusBgDark *modeswap.Color = modeswap.New(BrightBlue, gowid.ColorBlue) HexTopUnselectedFgDark *modeswap.Color = modeswap.New(gowid.ColorWhite, gowid.ColorBlue) HexTopUnselectedBgDark *modeswap.Color = modeswap.New(MediumGray, gowid.ColorWhite) HexTopSelectedFgDark *modeswap.Color = modeswap.New(gowid.ColorWhite, gowid.ColorWhite) HexTopSelectedBgDark *modeswap.Color = modeswap.New(BrightBlue, gowid.ColorBlue) HexBottomUnselectedFgDark *modeswap.Color = modeswap.New(gowid.ColorBlack, gowid.ColorBlack) HexBottomUnselectedBgDark *modeswap.Color = modeswap.New(DarkGray, gowid.ColorWhite) HexBottomSelectedFgDark *modeswap.Color = modeswap.New(gowid.ColorBlack, gowid.ColorBlack) HexBottomSelectedBgDark *modeswap.Color = modeswap.New(DarkGray, gowid.ColorWhite) HexCurUnselectedFgDark *modeswap.Color = modeswap.New(gowid.ColorWhite, gowid.ColorMagenta) HexCurUnselectedBgDark *modeswap.Color = modeswap.New(gowid.ColorBlack, gowid.ColorWhite) HexLineFgDark *modeswap.Color = modeswap.New(gowid.ColorBlack, gowid.ColorWhite) HexLineBgDark *modeswap.Color = modeswap.New(DarkGray, gowid.ColorBlack) FilterValidBgDark *modeswap.Color = modeswap.New(BrightGreen, gowid.ColorGreen) ButtonBgDark *modeswap.Color = modeswap.New(MediumGray, gowid.ColorWhite) StreamClientFgDark *modeswap.Color = modeswap.New(LightRed, gowid.ColorWhite) StreamClientBgDark *modeswap.Color = modeswap.New(DarkRed, gowid.ColorDarkRed) StreamServerFgDark *modeswap.Color = modeswap.New(LightBlue, gowid.ColorWhite) StreamServerBgDark *modeswap.Color = modeswap.New(DarkBlue, gowid.ColorBlue) DarkModePalette gowid.Palette = gowid.Palette{ "default": gowid.MakePaletteEntry(gowid.ColorWhite, gowid.ColorBlack), "title": gowid.MakeForeground(gowid.ColorRed), "current-capture": gowid.MakeForeground(gowid.ColorWhite), "pkt-list-row-focus": gowid.MakePaletteEntry(gowid.ColorWhite, PktListRowFocusBgDark), "pkt-list-cell-focus": gowid.MakePaletteEntry(gowid.ColorWhite, gowid.ColorPurple), "pkt-list-row-selected": gowid.MakePaletteEntry(PktListRowSelectedFgDark, PktListRowSelectedBgDark), "pkt-list-cell-selected": gowid.MakePaletteEntry(PktListCellSelectedFgDark, PktListCellSelectedBgDark), "pkt-struct-focus": gowid.MakePaletteEntry(gowid.ColorWhite, PktStructFocusBgDark), "pkt-struct-selected": gowid.MakePaletteEntry(PktStructSelectedFgDark, PktStructSelectedBgDark), "filter-menu-focus": gowid.MakeStyledPaletteEntry(gowid.ColorWhite, gowid.ColorBlack, gowid.StyleBold), "filter-valid": gowid.MakePaletteEntry(gowid.ColorBlack, FilterValidBgDark), "filter-invalid": gowid.MakePaletteEntry(gowid.ColorBlack, gowid.ColorRed), "filter-intermediate": gowid.MakePaletteEntry(gowid.ColorBlack, gowid.ColorOrange), "dialog": gowid.MakePaletteEntry(gowid.ColorBlack, gowid.ColorYellow), "dialog-buttons": gowid.MakePaletteEntry(gowid.ColorYellow, gowid.ColorBlack), "button": gowid.MakePaletteEntry(gowid.ColorBlack, ButtonBgDark), "button-focus": gowid.MakePaletteEntry(gowid.ColorWhite, gowid.ColorMagenta), "progress-default": gowid.MakeStyledPaletteEntry(gowid.ColorWhite, gowid.ColorBlack, gowid.StyleBold), "progress-complete": gowid.MakeStyleMod(gowid.MakePaletteRef("progress-default"), gowid.MakeBackground(gowid.ColorMagenta)), "progress-spinner": gowid.MakePaletteEntry(gowid.ColorYellow, gowid.ColorBlack), "hex-cur-selected": gowid.MakePaletteEntry(gowid.ColorWhite, gowid.ColorMagenta), "hex-cur-unselected": gowid.MakePaletteEntry(HexCurUnselectedFgDark, HexCurUnselectedBgDark), "hex-top-selected": gowid.MakePaletteEntry(HexTopSelectedFgDark, HexTopSelectedBgDark), "hex-top-unselected": gowid.MakePaletteEntry(HexTopUnselectedFgDark, HexTopUnselectedBgDark), "hex-bottom-selected": gowid.MakePaletteEntry(HexBottomSelectedFgDark, HexBottomSelectedBgDark), "hex-bottom-unselected": gowid.MakePaletteEntry(HexBottomUnselectedFgDark, HexBottomUnselectedBgDark), "hexln-selected": gowid.MakePaletteEntry(HexLineFgDark, HexLineBgDark), "hexln-unselected": gowid.MakePaletteEntry(HexLineFgDark, HexLineBgDark), "copy-mode-indicator": gowid.MakePaletteEntry(gowid.ColorWhite, gowid.ColorDarkRed), "copy-mode": gowid.MakePaletteEntry(gowid.ColorBlack, gowid.ColorYellow), "stream-client": gowid.MakePaletteEntry(StreamClientFgDark, StreamClientBgDark), "stream-server": gowid.MakePaletteEntry(StreamServerFgDark, StreamServerBgDark), "stream-match": gowid.MakePaletteEntry(gowid.ColorBlack, gowid.ColorYellow), "stream-search": gowid.MakePaletteEntry(gowid.ColorBlack, gowid.ColorWhite), } ) //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.0.3/ui/streamui.go000066400000000000000000000314271360044163000163150ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package 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 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{} // Make sure that existing stream widgets are discarded if the user loads a new pcap. func (t ManageStreamCache) OnNewSource(closeMe chan<- struct{}) { clearStreamState() close(closeMe) } //====================================================================== 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) PcapScheduler.RequestNewFilter(filter, MakePacketViewUpdater(app)) currentStreamKey = &streamKey{proto: proto, idx: streamIndex.Val()} // 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() } var swid *streamwidget.Widget swid2, ok := streamWidgets.Get(*currentStreamKey) if ok { swid = swid2.(*streamwidget.Widget) } else { swid = makeStreamWidget(previousFilterValue, filter, Loader.String(), proto) streamWidgets.Add(*currentStreamKey, swid) } if swid.Finished() { openStreamUi(swid, app) } else { // 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.SourceContext()) 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 stop chan struct{} stopped bool 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) // 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(closeMe chan<- struct{}) { close(closeMe) t.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{}) 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() { t.app.Run(gowid.RunFunction(func(app gowid.IApp) { t.drainChunks() t.drainPacketIndices() if !t.openedStreams { appViewNoKeys.SetSubWidget(streamView, app) openStreamUi(t.wid, app) t.openedStreams = true } })) } termshark.RunOnDoubleTicker(t.stop, fn, time.Duration(200)*time.Millisecond, time.Duration(200)*time.Millisecond, 10) }, Goroutinewg) termshark.TrackedGo(func() { Loop: for { select { case <-t.tick.C: t.app.Run(gowid.RunFunction(func(app gowid.IApp) { pleaseWaitSpinner.Update() })) case <-t.stop: break Loop } } }, Goroutinewg) } func (t *streamParseHandler) AfterIndexEnd(success bool, closeMe chan<- struct{}) { close(closeMe) t.wid.SetFinished(true) } func (t *streamParseHandler) AfterEnd(closeMe chan<- struct{}) { close(closeMe) t.app.Run(gowid.RunFunction(func(app gowid.IApp) { t.Lock() t.stopped = true t.Unlock() if !t.pleaseWaitClosed { t.pleaseWaitClosed = true ClosePleaseWait(t.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 && t.drainPacketIndices() == 0 { break } } if t.wid.NumChunks() == 0 { OpenMessage("No stream payloads found.", appView, app) } })) close(t.stop) } func (t *streamParseHandler) TrackPayloadPacket(packet int) { t.Lock() defer t.Unlock() if !t.stopped { t.pktIndices <- packet } } func (t *streamParseHandler) OnStreamHeader(hdr streams.FollowHeader, ch chan struct{}) { t.app.Run(gowid.RunFunction(func(app gowid.IApp) { t.wid.AddHeader(hdr) })) close(ch) } // 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, ch chan struct{}) { t.Lock() defer t.Unlock() if !t.stopped { t.chunks <- chunk } close(ch) } func (t *streamParseHandler) OnError(err error, closeMe chan<- struct{}) { close(closeMe) log.Error(err) if !Running { fmt.Fprintf(os.Stderr, "%v\n", err) QuitRequestedChan <- struct{}{} } 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) } t.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) OpenMessage(fmt.Sprintf("Selected packet %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, 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) PcapScheduler.RequestNewFilter(newFilter, MakePacketViewUpdater(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.0.3/ui/ui.go000066400000000000000000002422021360044163000150740ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package ui contains user-interface functions and helpers for termshark. package ui import ( "encoding/xml" "fmt" "os" "reflect" "runtime" "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/psmltable" "github.com/gcla/termshark/v2/system" "github.com/gcla/termshark/v2/ui/menuutil" "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/resizable" "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 *appkeys.KeyWidget 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 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 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 *rowFocusTableWidget var Loadingw gowid.IWidget // "loading..." var MissingMsgw gowid.IWidget // centered, holding singlePacketViewMsgHolder var EmptyStructViewTimer *time.Ticker var EmptyHexViewTimer *time.Ticker 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 Loader *pcap.Loader var PcapScheduler *pcap.Scheduler 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 Running bool // true if gowid/tcell is controlling the terminal //====================================================================== 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) } // Runs in app goroutine func UpdateProgressBarForInterface(c *pcap.Loader, app gowid.IApp) { SetProgressIndeterminate(app) switch Loader.State() { case 0: ClearProgressWidget(app) default: loadSpinner.Update() setProgressWidget(app) } } // Runs in app goroutine func UpdateProgressBarForFile(c *pcap.Loader, prevRatio float64, app gowid.IApp) float64 { SetProgressDeterminate(app) psmlProg := Prog{100, 100} pdmlPacketProg := Prog{0, 100} pdmlIdxProg := Prog{0, 100} pcapPacketProg := Prog{0, 100} pcapIdxProg := Prog{0, 100} curRowProg := Prog{100, 100} var err error var c2 int64 var m int64 var x int // This shows where we are in the packet list. We want progress to be active only // as long as our view has missing widgets. So this can help predict when our little // view into the list of packets will be populated. currentRow := -1 var currentRowMod int64 = -1 var currentRowDiv int = -1 if packetListView != nil { if fxy, err := packetListView.FocusXY(); err == nil { foo, ok := packetListView.Model().RowIdentifier(fxy.Row) if ok { pktsPerLoad := c.PacketsPerLoad() currentRow = int(foo) currentRowMod = int64(currentRow % pktsPerLoad) currentRowDiv = (currentRow / pktsPerLoad) * pktsPerLoad c.Lock() curRowProg.cur, curRowProg.max = int64(currentRow), int64(len(c.PacketPsmlData)) c.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.State()&pcap.LoadingPdml != 0 { 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.Lock() c2, m, err = system.ProcessProgress(termshark.SafePid(c.PdmlCmd), c.PcapPdml) c.Unlock() if err == nil { pdmlIdxProg.cur, pdmlIdxProg.max = c2, m if currentRow != -1 { // Only need to look this far into the psml file before my view is populated m = m * (curRowProg.cur / curRowProg.max) } } // Progress determined by how many of the (up to) 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.Lock() c2, m, err = system.ProcessProgress(termshark.SafePid(c.PcapCmd), c.PcapPcap) c.Unlock() if err == nil { pcapIdxProg.cur, pcapIdxProg.max = c2, m if currentRow != -1 { // Only need to look this far into the psml file before my view is populated m = m * (curRowProg.cur / curRowProg.max) } } } } if psml, ok := c.PcapPsml.(string); ok && c.State()&pcap.LoadingPsml != 0 { c.Lock() c2, m, err = system.ProcessProgress(termshark.SafePid(c.PsmlCmd), psml) c.Unlock() if err == nil { psmlProg.cur, psmlProg.max = c2, m } } var prog Prog // state is guaranteed not to include pcap.Loadingiface if we showing a determinate progress bar switch c.State() { case pcap.LoadingPsml: prog = psmlProg select { case <-c.StartStage2Chan: default: prog.cur = prog.cur / 2 // temporarily divide in 2. Leave original for case above - so that the 50% } case pcap.LoadingPdml: prog = progMin( progMax(pcapPacketProg, pcapIdxProg), // max because the fastest will win and cancel the other progMax(pdmlPacketProg, pdmlIdxProg), ) case pcap.LoadingPsml | pcap.LoadingPdml: select { case <-c.StartStage2Chan: prog = progMin( // min because all of these have to complete, so the slowest determines progress psmlProg, progMin( progMax(pcapPacketProg, pcapIdxProg), // max because the fastest will win and cancel the other progMax(pdmlPacketProg, pdmlIdxProg), ), ) default: prog = psmlProg prog.cur = prog.cur / 2 // temporarily divide in 2. Leave original for case above - so that the 50% } } curRatio := float64(prog.cur) / float64(prog.max) if prog.Complete() { if prevRatio < 1.0 { ClearProgressWidget(app) } } else { if prevRatio < curRatio { loadProgress.SetTarget(app, int(prog.max)) loadProgress.SetProgress(app, int(prog.cur)) setProgressWidget(app) } } return curRatio } //====================================================================== type RenderWeightUpTo struct { gowid.RenderWithWeight max int } func (s RenderWeightUpTo) MaxUnits() int { return s.max } func weightupto(w int, max int) RenderWeightUpTo { return RenderWeightUpTo{gowid.RenderWithWeight{W: w}, max} } func units(n int) gowid.RenderWithUnits { return gowid.RenderWithUnits{U: n} } func weight(n int) gowid.RenderWithWeight { return gowid.RenderWithWeight{W: n} } func ratio(r float64) gowid.RenderWithRatio { return gowid.RenderWithRatio{R: r} } //====================================================================== // 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("pkt-struct-selected")), styled.New(res, gowid.MakePaletteRef("pkt-struct-focus")), ), ) return res } // The widget representing the data at this level in the tree. Simply use what we extract from // the PDML. func makeStructNodeWidget(pos tree.IPos, tr tree.IModel) gowid.IWidget { return text.New(tr.Leaf()) } //====================================================================== // I want to have prefered position work on this, but you have to choose a subwidget // to navigate to. We have three. I know that my use of them is very similar, so I'll // just pick the first type selectedComposite struct { *isselected.Widget } var _ gowid.IComposite = (*selectedComposite)(nil) func (w *selectedComposite) SubWidget() gowid.IWidget { return w.Not } //====================================================================== // rowFocusTableWidget provides a table that highlights the selected row or // focused row. type rowFocusTableWidget struct { // 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 *table.BoundedWidget colors []pcap.PacketColors } var _ gowid.IWidget = (*rowFocusTableWidget)(nil) var _ gowid.IComposite = (*rowFocusTableWidget)(nil) func (t *rowFocusTableWidget) SubWidget() gowid.IWidget { return t.BoundedWidget } func (t *rowFocusTableWidget) InvertedModel() table.IInvertible { return t.Model().(table.IInvertible) } func (t *rowFocusTableWidget) Rows() int { return t.Widget.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.Widget.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.Widget.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.Widget.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.Widget.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.Widget.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("pkt-list-row-selected")), styled.New(w, gowid.MakePaletteRef("pkt-list-row-focus")), ), } if pos >= 0 && PacketColors { res = styled.New(res, gowid.MakePaletteEntry(t.colors[pos].FG, t.colors[pos].BG), ) } 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) } //====================================================================== 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) { // the same, for now OpenMessage(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 openCopyChoices(copyLen int, app gowid.IApp) { var cc *dialog.Widget maximizer := &dialog.Maximizer{} clips := app.Clips() cws := make([]gowid.IWidget, 0, len(clips)) copyCmd := termshark.ConfStringSlice( "main.copy-command", system.CopyToClipboard, ) if len(copyCmd) == 0 { OpenError("Config file has an invalid copy-command entry! Please remove it.", app) return } for _, clip := range clips { c2 := clip lbl := text.New(clip.ClipName() + ":") 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) view1 = appkeys.New( view1, func(ev *tcell.EventKey, app gowid.IApp) bool { if ev.Rune() == 'z' { // maximize/unmaximize if maximizer.Maxed { maximizer.Unmaximize(cc, app) } else { maximizer.Maximize(cc, app) } return true } return false }, ) cc = dialog.New(view1, dialog.Options{ Buttons: dialog.CloseOnly, NoShadow: true, BackgroundStyle: gowid.MakePaletteRef("dialog"), BorderStyle: gowid.MakePaletteRef("dialog"), ButtonStyle: gowid.MakePaletteRef("dialog-buttons"), }, ) cc.OnOpenClose(gowid.MakeWidgetCallback("cb", gowid.WidgetChangedFunction(func(app gowid.IApp, w gowid.IWidget) { if !cc.IsOpen() { app.InCopyMode(false) } }))) dialog.OpenExt(cc, 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) { QuitRequestedChan <- struct{}{} }, }, dialog.Cancel, }, NoShadow: true, BackgroundStyle: gowid.MakePaletteRef("dialog"), BorderStyle: gowid.MakePaletteRef("dialog"), ButtonStyle: gowid.MakePaletteRef("dialog-buttons"), }, ) YesNo.Open(appView, units(len(msgt)+20), 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.Lock() defer Loader.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 } //====================================================================== type NoHandlers struct{} //====================================================================== type updateCurrentCaptureInTitle struct { Ld *pcap.Scheduler App gowid.IApp } var _ pcap.INewSource = updateCurrentCaptureInTitle{} var _ pcap.IClear = updateCurrentCaptureInTitle{} func MakeUpdateCurrentCaptureInTitle(app gowid.IApp) updateCurrentCaptureInTitle { return updateCurrentCaptureInTitle{ Ld: PcapScheduler, App: app, } } func (t updateCurrentCaptureInTitle) OnNewSource(closeMe chan<- struct{}) { close(closeMe) t.App.Run(gowid.RunFunction(func(app gowid.IApp) { currentCapture.SetText(t.Ld.String(), app) currentCaptureWidgetHolder.SetSubWidget(currentCaptureWidget, app) })) } func (t updateCurrentCaptureInTitle) OnClear(closeMe chan<- struct{}) { close(closeMe) t.App.Run(gowid.RunFunction(func(app gowid.IApp) { currentCaptureWidgetHolder.SetSubWidget(nullw, app) })) } //====================================================================== type updatePacketViews struct { Ld *pcap.Scheduler App gowid.IApp } var _ pcap.IOnError = updatePacketViews{} var _ pcap.IClear = updatePacketViews{} var _ pcap.IBeforeBegin = updatePacketViews{} var _ pcap.IAfterEnd = updatePacketViews{} func MakePacketViewUpdater(app gowid.IApp) updatePacketViews { res := updatePacketViews{} res.App = app res.Ld = PcapScheduler return res } func (t updatePacketViews) EnableOperations() { t.Ld.Enable() } func (t updatePacketViews) OnClear(closeMe chan<- struct{}) { close(closeMe) t.App.Run(gowid.RunFunction(func(app gowid.IApp) { clearPacketViews(app) })) } func (t updatePacketViews) BeforeBegin(ch chan<- struct{}) { ch2 := Loader.PsmlFinishedChan t.App.Run(gowid.RunFunction(func(app gowid.IApp) { clearPacketViews(app) t.Ld.Lock() defer t.Ld.Unlock() setPacketListWidgets(t.Ld, app) setProgressWidget(app) // Start this after widgets have been cleared, to get focus change termshark.TrackedGo(func() { fn2 := func() { app.Run(gowid.RunFunction(func(app gowid.IApp) { updatePacketListWithData(Loader, app) })) } termshark.RunOnDoubleTicker(ch2, fn2, time.Duration(100)*time.Millisecond, time.Duration(2000)*time.Millisecond, 10) }, Goroutinewg) close(ch) })) } func (t updatePacketViews) AfterEnd(closeMe chan<- struct{}) { close(closeMe) t.App.Run(gowid.RunFunction(func(app gowid.IApp) { updatePacketListWithData(t.Ld, app) StopEmptyStructViewTimer() StopEmptyHexViewTimer() })) } func (t updatePacketViews) OnError(err error, closeMe chan<- struct{}) { close(closeMe) log.Error(err) if !Running { fmt.Fprintf(os.Stderr, "%v\n", err) QuitRequestedChan <- struct{}{} } 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) } t.App.Run(gowid.RunFunction(func(app gowid.IApp) { OpenError(errstr, app) StopEmptyStructViewTimer() StopEmptyHexViewTimer() })) } } //====================================================================== 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) PcapScheduler.RequestClearPcap( pcap.HandlerList{ MakePacketViewUpdater(app), MakeUpdateCurrentCaptureInTitle(app), }, ) }, }, dialog.Cancel, }, NoShadow: true, BackgroundStyle: gowid.MakePaletteRef("dialog"), BorderStyle: gowid.MakePaletteRef("dialog"), ButtonStyle: gowid.MakePaletteRef("dialog-buttons"), }, ) 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: openCopyChoices(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) } // Keys for the main view - packet list, structure, etc func mainKeyPress(evk *tcell.EventKey, app gowid.IApp) bool { handled := true if evk.Key() == tcell.KeyCtrlC && Loader.State()&pcap.LoadingPsml != 0 { PcapScheduler.RequestStopLoadStage1(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 } 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(isTab, 1, 2).(int) } else if deep.Equal(v1p, mainviewPaths[1]) == nil { newidx = gwutil.If(isTab, 2, 0).(int) } else { newidx = gwutil.If(isTab, 0, 1).(int) } } else if mainViewNoKeys.SubWidget() == altview1 { v2p := gowid.FocusPath(altview1) if deep.Equal(v2p, altview1Paths[0]) == nil { newidx = gwutil.If(isTab, 1, 2).(int) } else if deep.Equal(v2p, altview1Paths[1]) == nil { newidx = gwutil.If(isTab, 2, 0).(int) } else { newidx = gwutil.If(isTab, 0, 1).(int) } } else if mainViewNoKeys.SubWidget() == altview2 { v3p := gowid.FocusPath(altview2) if deep.Equal(v3p, altview2Paths[0]) == nil { newidx = gwutil.If(isTab, 1, 2).(int) } else if deep.Equal(v3p, altview2Paths[1]) == nil { newidx = gwutil.If(isTab, 2, 0).(int) } else { newidx = gwutil.If(isTab, 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) } } } else if 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 evk.Rune() == '\\' { w := mainViewNoKeys.SubWidget() fp := gowid.FocusPath(w) if w == viewOnlyPacketList || w == viewOnlyPacketStructure || w == viewOnlyPacketHex { 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 { mainViewNoKeys.SetSubWidget(viewOnlyPacketList, app) if deep.Equal(fp, maxViewPath) == nil { gowid.SetFocusPath(viewOnlyPacketList, maxViewPath, app) } } } else if evk.Rune() == '/' { setFocusOnDisplayFilter(app) } else if evk.Key() == tcell.KeyCtrlW { reallyClear(app) } else { handled = false } return handled } // Keys for the whole app, applicable whichever view is frontmost func appKeyPress(evk *tcell.EventKey, app gowid.IApp) bool { handled := true if evk.Key() == tcell.KeyCtrlC { reallyQuit(app) } else if evk.Key() == tcell.KeyCtrlL { app.Sync() } else if evk.Rune() == 'q' || evk.Rune() == 'Q' { reallyQuit(app) } else if evk.Key() == tcell.KeyEscape { 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) generalMenu.Open(openMenuSite, app) } else if evk.Rune() == '?' { OpenTemplatedDialog(appView, "UIHelp", app) } else { handled = false } return handled } type LoadResult struct { packetTree []*pdmltree.Model headers []string packetList [][]string } func IsProgressIndeterminate() bool { return progressHolder.SubWidget() == loadSpinner } func SetProgressDeterminate(app gowid.IApp) { progressHolder.SetSubWidget(loadProgress, app) } func SetProgressIndeterminate(app gowid.IApp) { progressHolder.SetSubWidget(loadSpinner, app) } func ClearProgressWidget(app gowid.IApp) { ds := filterCols.Dimensions() sw := filterCols.SubWidgets() sw[progWidgetIdx] = nullw ds[progWidgetIdx] = fixed filterCols.SetSubWidgets(sw, app) filterCols.SetDimensions(ds, app) } func setProgressWidget(app gowid.IApp) { stop := button.New(text.New("Stop")) stop2 := styled.NewExt(stop, gowid.MakePaletteRef("button"), gowid.MakePaletteRef("button-focus")) stop.OnClick(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, w gowid.IWidget) { PcapScheduler.RequestStopLoadStage1(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(hex) } str := getStructWidgetToDisplay(row, app) if str != nil { sw2 = enableselected.New(str) } } } if sw1 != nil { packetHexViewHolder.SetSubWidget(sw1, app) StopEmptyHexViewTimer() } else { if EmptyHexViewTimer == nil { startEmptyHexViewTimer() } } if sw2 != nil { packetStructureViewHolder.SetSubWidget(sw2, app) StopEmptyStructViewTimer() } else { if EmptyStructViewTimer == nil { startEmptyStructViewTimer() } } } func makePacketListModel(psml psmlInfo, app gowid.IApp) *psmltable.Model { packetPsmlTableModel := table.NewSimpleModel( psml.PsmlHeaders(), psml.PsmlData(), table.SimpleOptions{ Style: table.StyleOptions{ VerticalSeparator: fill.New(' '), HeaderStyleProvided: true, HeaderStyleFocus: gowid.MakePaletteRef("pkt-list-cell-focus"), CellStyleProvided: true, CellStyleSelected: gowid.MakePaletteRef("pkt-list-cell-selected"), CellStyleFocus: gowid.MakePaletteRef("pkt-list-cell-focus"), }, Layout: table.LayoutOptions{ Widths: []gowid.IWidgetDimension{ weightupto(6, 10), weightupto(8, 24), weightupto(14, 32), weightupto(14, 32), weightupto(12, 32), weightupto(8, 8), weight(40), }, }, }, ) expandingModel := psmltable.New( packetPsmlTableModel, gowid.MakePaletteRef("pkt-list-row-focus"), ) if len(expandingModel.Comparators) > 0 { expandingModel.Comparators[0] = table.IntCompare{} expandingModel.Comparators[5] = table.IntCompare{} } return expandingModel } func updatePacketListWithData(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 } } 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 = &rowFocusTableWidget{ BoundedWidget: packetListTable, colors: psml.PsmlColors(), } packetListView.Lower().IWidget = list.NewBounded(packetListView) packetListView.OnFocusChanged(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, w gowid.IWidget) { fxy, err := packetListView.FocusXY() if err != nil { return } if !newPacketsArrived { // this focus change must've been user-initiated, so stop auto-scrolling with new packets. // This mimics Wireshark's behavior. 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, Cancel: true, }) if rowm > pktsPerLoad/2 { CacheRequests = append(CacheRequests, pcap.LoadPcapSlice{ Row: ((row / pktsPerLoad) + 1) * pktsPerLoad, }) } else { row2 := ((row / pktsPerLoad) - 1) * pktsPerLoad if row2 < 0 { row2 = 0 } CacheRequests = append(CacheRequests, pcap.LoadPcapSlice{ Row: row2, }) } CacheRequestsChan <- struct{}{} setLowerWidgets(app) } })) withScrollbar := withscrollbar.New(packetListView, withscrollbar.Options{ HideIfContentFits: true, }) packetListViewHolder.SetSubWidget(enableselected.New(withScrollbar), 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-cur-unselected", CursorSelected: "hex-cur-selected", LineNumUnselected: "hexln-unselected", LineNumSelected: "hexln-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, }, ) } //====================================================================== type SaveRecents struct { Pcap string Filter string App gowid.IApp } var _ pcap.IAfterEnd = SaveRecents{} func MakeSaveRecents(pcap string, filter string, app gowid.IApp) SaveRecents { return SaveRecents{ Pcap: pcap, Filter: filter, App: app, } } func (t SaveRecents) AfterEnd(closeMe chan<- struct{}) { close(closeMe) // Run on main goroutine to avoid problems flagged by -race t.App.Run(gowid.RunFunction(func(gowid.IApp) { if t.Pcap != "" { termshark.AddToRecentFiles(t.Pcap) } if t.Filter != "" { // Run on main goroutine to avoid problems flagged by -race termshark.AddToRecentFilters(t.Filter) } })) } //====================================================================== // Call from app goroutine context func RequestLoadPcapWithCheck(pcapf string, displayFilter string, app gowid.IApp) { if _, err := os.Stat(pcapf); os.IsNotExist(err) { OpenError(fmt.Sprintf("File %s not found.", pcapf), app) } else { PcapScheduler.RequestLoadPcap(pcapf, displayFilter, pcap.HandlerList{ MakeSaveRecents(pcapf, displayFilter, app), MakePacketViewUpdater(app), MakeUpdateCurrentCaptureInTitle(app), ManageStreamCache{}, }, ) } } //====================================================================== // Prog hold a progress model - a current value on the way up to the max value type Prog struct { cur int64 max int64 } func (p Prog) Complete() bool { return p.cur >= p.max } func (p Prog) String() string { return fmt.Sprintf("cur=%d max=%d", p.cur, p.max) } func progMin(x, y Prog) Prog { if float64(x.cur)/float64(x.max) < float64(y.cur)/float64(y.max) { return x } else { return y } } func progMax(x, y Prog) Prog { if float64(x.cur)/float64(x.max) > float64(y.cur)/float64(y.max) { return x } else { return y } } //====================================================================== 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(), 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) } //====================================================================== type SetStructWidgets struct { Ld *pcap.Loader App gowid.IApp } var _ pcap.IOnError = SetStructWidgets{} var _ pcap.IClear = SetStructWidgets{} var _ pcap.IBeforeBegin = SetStructWidgets{} var _ pcap.IAfterEnd = SetStructWidgets{} func (s SetStructWidgets) OnClear(closeMe chan<- struct{}) { close(closeMe) } func (s SetStructWidgets) BeforeBegin(ch chan<- struct{}) { s2ch := Loader.Stage2FinishedChan termshark.TrackedGo(func() { fn2 := func() { s.App.Run(gowid.RunFunction(func(app gowid.IApp) { setLowerWidgets(app) })) } termshark.RunOnDoubleTicker(s2ch, fn2, time.Duration(100)*time.Millisecond, time.Duration(2000)*time.Millisecond, 10) }, Goroutinewg) close(ch) } // Close the channel before the callback. When the global loader state is idle, // app.Quit() will stop accepting app callbacks, so the goroutine that waits // for ch to be closed will never terminate. func (s SetStructWidgets) AfterEnd(ch chan<- struct{}) { close(ch) s.App.Run(gowid.RunFunction(func(app gowid.IApp) { setLowerWidgets(app) singlePacketViewMsgHolder.SetSubWidget(nullw, app) })) } func (s SetStructWidgets) OnError(err error, closeMe chan<- struct{}) { close(closeMe) log.Error(err) s.App.Run(gowid.RunFunction(func(app gowid.IApp) { OpenError(fmt.Sprintf("%v", err), app) })) } //====================================================================== func startEmptyStructViewTimer() { EmptyStructViewTimer = time.NewTicker(time.Duration(1000) * time.Millisecond) } func startEmptyHexViewTimer() { EmptyHexViewTimer = time.NewTicker(time.Duration(1000) * time.Millisecond) } func StopEmptyStructViewTimer() { if EmptyStructViewTimer != nil { EmptyStructViewTimer.Stop() EmptyStructViewTimer = nil } } func StopEmptyHexViewTimer() { if EmptyHexViewTimer != nil { EmptyHexViewTimer.Stop() EmptyHexViewTimer = nil } } //====================================================================== type SetNewPdmlRequests struct { *pcap.Scheduler } var _ pcap.ICacheUpdater = SetNewPdmlRequests{} func (u SetNewPdmlRequests) WhenLoadingPdml() { u.When(func() bool { return u.State()&pcap.LoadingPdml == pcap.LoadingPdml }, func() { CacheRequestsChan <- struct{}{} }) } func (u SetNewPdmlRequests) WhenNotLoadingPdml() { u.When(func() bool { return u.State()&pcap.LoadingPdml == 0 }, func() { CacheRequestsChan <- struct{}{} }) } func SetStructViewMissing(app gowid.IApp) { singlePacketViewMsgHolder.SetSubWidget(Loadingw, app) packetStructureViewHolder.SetSubWidget(MissingMsgw, app) } func SetHexViewMissing(app gowid.IApp) { singlePacketViewMsgHolder.SetSubWidget(Loadingw, app) packetHexViewHolder.SetSubWidget(MissingMsgw, app) } //====================================================================== func assignTo(wp interface{}, w gowid.IWidget) gowid.IWidget { reflect.ValueOf(wp).Elem().Set(reflect.ValueOf(w)) return w } //====================================================================== 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-buttons"), }, ) 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-indicator"), ) //====================================================================== 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 := []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) }, }, 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: "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: "Reassemble stream", Key: gowid.MakeKey('f'), CB: func(app gowid.IApp, w gowid.IWidget) { analysisMenu.Close(app) startStreamReassembly(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) { PcapScheduler.RequestNewFilter(FilterWidget.Value(), pcap.HandlerList{ MakeSaveRecents("", FilterWidget.Value(), app), MakePacketViewUpdater(app), ManageStreamCache{}, }, ) }) // 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( packetListViewHolder, 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: packetListViewHolder, D: weight(1), }, &gowid.ContainerWidget{ IWidget: divider.NewUnicode(), D: flow, }, &gowid.ContainerWidget{ IWidget: packetStructureViewHolder, 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: packetHexViewHolder, 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: packetStructureViewHolder, D: weight(1), }, &gowid.ContainerWidget{ IWidget: fillVBar, D: units(1), }, &gowid.ContainerWidget{ IWidget: packetHexViewHolder, 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: packetListViewHolder, 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() mainView = appkeys.New(mainViewNoKeys, mainKeyPress) //====================================================================== palette := PaletteSwitcher{ P1: &DarkModePalette, P2: &RegularPalette, ChooseOne: &DarkMode, } appView = appkeys.New( assignTo(&appViewNoKeys, holder.New(mainView)), appKeyPress, ) // Create app, etc, but don't init screen which sets ICANON, etc app, err = gowid.NewApp(gowid.AppArgs{ View: appView, Palette: palette, DontActivate: true, Log: log.StandardLogger(), }) if err != nil { return nil, err } for _, m := range FilterWidget.Menus() { app.RegisterMenu(m) } app.RegisterMenu(savedMenu) app.RegisterMenu(analysisMenu) app.RegisterMenu(generalMenu) app.RegisterMenu(conversationMenu) 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.0.3/utils.go000066400000000000000000000524031360044163000152040ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. package termshark import ( "bufio" "bytes" "compress/gzip" "encoding/binary" "encoding/gob" "encoding/json" "encoding/xml" "fmt" "io" "io/ioutil" "net" "os" "os/exec" "path" "path/filepath" "regexp" "runtime" "strconv" "strings" "sync" "syscall" "text/template" "time" "github.com/blang/semver" "github.com/gcla/gowid" "github.com/gcla/gowid/gwutil" "github.com/gcla/termshark/v2/system" "github.com/gcla/termshark/v2/widgets/resizable" "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" ) //====================================================================== 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 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() delete(viper.Get("main").(map[string]interface{}), 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, "-r", CacheFile("empty.pcap"), "-T", "psml", "-w", os.DevNull, "--color") 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) (int, error) { var err error exitCode := -1 // default bad cmd := exec.Command(prog, args...) err = cmd.Run() if err != nil { if exerr, ok := err.(*exec.ExitError); ok { ws := exerr.Sys().(syscall.WaitStatus) exitCode = ws.ExitStatus() } } else { ws := cmd.ProcessState.Sys().(syscall.WaitStatus) exitCode = ws.ExitStatus() } return exitCode, err } func 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 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 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 err } else { return nil } } 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 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(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()) } //====================================================================== 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[string]int, 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[string]int, error) { re := regexp.MustCompile("^(?P[0-9]+)\\.\\s+(?P[^\\s]+)(\\s*\\((?P[^)]+)\\))?") res := make(map[string]int) 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 && name != "" { 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}) } res[result["name1"]] = int(i) if name2, ok := result["name2"]; ok { res[name2] = int(i) } } 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 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 } //====================================================================== // Local Variables: // mode: Go // fill-column: 78 // End: termshark-2.0.3/utils_test.go000066400000000000000000000072451360044163000162470ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. package termshark import ( "bytes" "testing" "github.com/blang/semver" "github.com/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, 12, len(interfaces)) assert.Equal(t, interfaces[`\Device\NPF_{78032B7E-4968-42D3-9F37-287EA86C0AAA}`], 2) assert.Equal(t, interfaces[`Local Area Connection* 10`], 2) } 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.0.3/version.go000066400000000000000000000005561360044163000155330ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. package termshark var Version string = "v2.0.3" //====================================================================== // Local Variables: // indent-tabs-mode: nil // tab-width: 4 // fill-column: 78 // End: termshark-2.0.3/widgets/000077500000000000000000000000001360044163000151575ustar00rootroot00000000000000termshark-2.0.3/widgets/appkeys/000077500000000000000000000000001360044163000166335ustar00rootroot00000000000000termshark-2.0.3/widgets/appkeys/appkeys.go000066400000000000000000000101231360044163000206330ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package appkeys provides a widget which responds to keyboard input. package appkeys import ( "fmt" "github.com/gcla/gowid" "github.com/gdamore/tcell" ) //====================================================================== type IWidget interface { gowid.ICompositeWidget } type IAppInput interface { gowid.IComposite ApplyBefore() bool } type IAppKeys interface { KeyInput(ev *tcell.EventKey, app gowid.IApp) bool } type IAppMouse interface { MouseInput(ev *tcell.EventMouse, app gowid.IApp) bool } type KeyInputFn func(ev *tcell.EventKey, app gowid.IApp) bool type MouseInputFn func(ev *tcell.EventMouse, app gowid.IApp) bool type Options struct { ApplyBefore bool } type Widget struct { gowid.IWidget opt Options } type KeyWidget struct { *Widget fn KeyInputFn } type MouseWidget struct { *Widget fn MouseInputFn } func New(inner gowid.IWidget, fn KeyInputFn, opts ...Options) *KeyWidget { var opt Options if len(opts) > 0 { opt = opts[0] } res := &KeyWidget{ Widget: &Widget{ IWidget: inner, opt: opt, }, fn: fn, } return res } var _ gowid.ICompositeWidget = (*KeyWidget)(nil) var _ IWidget = (*KeyWidget)(nil) var _ IAppKeys = (*KeyWidget)(nil) func NewMouse(inner gowid.IWidget, fn MouseInputFn, opts ...Options) *MouseWidget { var opt Options if len(opts) > 0 { opt = opts[0] } res := &MouseWidget{ Widget: &Widget{ IWidget: inner, opt: opt, }, fn: fn, } return res } var _ gowid.ICompositeWidget = (*MouseWidget)(nil) var _ IWidget = (*MouseWidget)(nil) var _ IAppMouse = (*MouseWidget)(nil) func (w *Widget) String() string { return fmt.Sprintf("appkeys[%v]", w.SubWidget()) } func (w *Widget) ApplyBefore() bool { return w.opt.ApplyBefore } func (w *KeyWidget) KeyInput(k *tcell.EventKey, app gowid.IApp) bool { return w.fn(k, app) } func (w *MouseWidget) MouseInput(k *tcell.EventMouse, app gowid.IApp) bool { return w.fn(k, app) } func (w *Widget) SubWidget() gowid.IWidget { return w.IWidget } func (w *Widget) SetSubWidget(wi gowid.IWidget, app gowid.IApp) { w.IWidget = wi } func (w *Widget) SubWidgetSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderSize { return SubWidgetSize(w, size, focus, app) } func (w *KeyWidget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { return UserInput(w, ev, size, focus, app) } func (w *MouseWidget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { return UserInput(w, ev, size, focus, app) } //====================================================================== func SubWidgetSize(w gowid.ICompositeWidget, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderSize { return size } func RenderSize(w IWidget, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { return gowid.RenderSize(w.SubWidget(), size, focus, app) } func UserInput(w IAppInput, ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { var res bool if w.ApplyBefore() { switch ev := ev.(type) { case *tcell.EventKey: if wk, ok := w.(IAppKeys); ok { res = wk.KeyInput(ev, app) } case *tcell.EventMouse: if wm, ok := w.(IAppMouse); ok { res = wm.MouseInput(ev, app) } } if !res { res = 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.0.3/widgets/copymodetable/000077500000000000000000000000001360044163000200065ustar00rootroot00000000000000termshark-2.0.3/widgets/copymodetable/copymodetable.go000066400000000000000000000102351360044163000231650ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package 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/table" ) //====================================================================== type IRowCopier interface { CopyRow(id table.RowId) []gowid.ICopyResult } type ITableCopier interface { CopyTable() []gowid.ICopyResult } type Widget struct { table.BoundedWidget // Wrap a regular bounded table 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 table.BoundedWidget, rowClip IRowCopier, allClip ITableCopier, name string, clip gowid.IClipboardSelected) *Widget { return &Widget{ BoundedWidget: 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.Widget.Render(size, focus, app) w.SetModel(origModel, app) return res } else { return w.Widget.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.Widget } 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.0.3/widgets/copymodetree/000077500000000000000000000000001360044163000176565ustar00rootroot00000000000000termshark-2.0.3/widgets/copymodetree/copymodetree.go000066400000000000000000000074741360044163000227200ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package copymodetree provides a wrapper around a tree that supports copy mode. // It assumes the underlying tree is a termshark PDML tree and allows copying // the PDML substructure or a serialized representation of the substructure. package copymodetree import ( "bytes" "fmt" "strings" "github.com/gcla/gowid" "github.com/gcla/gowid/widgets/list" "github.com/gcla/gowid/widgets/tree" "github.com/gcla/termshark/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.0.3/widgets/enableselected/000077500000000000000000000000001360044163000201165ustar00rootroot00000000000000termshark-2.0.3/widgets/enableselected/enableselected.go000066400000000000000000000032261360044163000234070ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package enableselected provides a widget that turns on focus.Selected. // It can be used to wrap container widgets (pile, columns) which may // change their look according to the selected state. One use for this is // highlighting selected rows or columns when the widget itself is not in // focus. package enableselected import ( "github.com/gcla/gowid" ) //====================================================================== // Widget turns on the selected field in the Widget when operations are done on this widget. Then // children widgets that respond to the selected state will be activated. type Widget struct { gowid.IWidget } var _ gowid.IWidget = (*Widget)(nil) var _ gowid.IComposite = (*Widget)(nil) func New(w gowid.IWidget) *Widget { return &Widget{w} } func (w *Widget) SubWidget() gowid.IWidget { return w.IWidget } func (w *Widget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { focus.Selected = true return gowid.RenderSize(w.IWidget, size, focus, app) } func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { focus.Selected = true return 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.0.3/widgets/expander/000077500000000000000000000000001360044163000167655ustar00rootroot00000000000000termshark-2.0.3/widgets/expander/expander.go000066400000000000000000000035371360044163000211320ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package expander provides a widget that renders in one line when not in focus // but that may render using more than one line when in focus. This is useful for // showing an item in full when needed, but otherwise saving screen real-estate. package expander import ( "github.com/gcla/gowid" "github.com/gcla/gowid/widgets/boxadapter" ) //====================================================================== // Widget will render in one row when not selected, and then using // however many rows required when selected. type Widget struct { orig gowid.IWidget w *boxadapter.Widget } var _ gowid.IWidget = (*Widget)(nil) var _ gowid.IComposite = (*Widget)(nil) func New(w gowid.IWidget) *Widget { b := boxadapter.New(w, 1) return &Widget{w, b} } func (w *Widget) SubWidget() gowid.IWidget { return w.orig } func (w *Widget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { if focus.Selected { return gowid.RenderSize(w.orig, size, focus, app) } else { return gowid.RenderSize(w.w, size, focus, app) } } func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { if focus.Selected { return 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.0.3/widgets/filter/000077500000000000000000000000001360044163000164445ustar00rootroot00000000000000termshark-2.0.3/widgets/filter/filter.go000066400000000000000000000427051360044163000202700ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package filter prpvides a termshark-specific edit widget which changes // color according to the validity of its input, and which activates a // drop-down menu of possible completions for the term at point. package filter import ( "context" "fmt" "io" "os/exec" "sync" "syscall" "time" "unicode" "github.com/gcla/gowid" "github.com/gcla/gowid/gwutil" "github.com/gcla/gowid/widgets/button" "github.com/gcla/gowid/widgets/cellmod" "github.com/gcla/gowid/widgets/columns" "github.com/gcla/gowid/widgets/edit" "github.com/gcla/gowid/widgets/framed" "github.com/gcla/gowid/widgets/holder" "github.com/gcla/gowid/widgets/hpadding" "github.com/gcla/gowid/widgets/list" "github.com/gcla/gowid/widgets/menu" "github.com/gcla/gowid/widgets/styled" "github.com/gcla/gowid/widgets/text" "github.com/gcla/termshark/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 *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 { ed := edit.New() 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-focus"), ) drop := menu.New("filter", menuListBox2, gowid.RenderWithUnits{U: opt.MaxCompletions + 2}, menu.Options{ IgnoreKeysProvided: true, IgnoreKeys: []gowid.IKey{ gowid.MakeKeyExt(tcell.KeyUp), gowid.MakeKeyExt(tcell.KeyDown), }, CloseKeysProvided: true, CloseKeys: []gowid.IKey{}, }, ) site := menu.NewSite(menu.SiteOptions{ YOffset: 1, }) cb := gowid.NewCallbacks() temporarilyDisabled := false onelineEd := appkeys.New(ed, handleEnter(cb, &temporarilyDisabled), 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: &temporarilyDisabled, 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) })) }, } invalidcb := &ValidateCB{ Fn: func(app gowid.IApp) { app.Run(gowid.RunFunction(func(app gowid.IApp) { res.validitySite.SetSubWidget(res.invalid, app) gowid.RunWidgetCallbacks(res.Callbacks, InvalidCB{}, app, res) })) }, } killedcb := &ValidateCB{ Fn: func(app gowid.IApp) { app.Run(gowid.RunFunction(func(app gowid.IApp) { res.validitySite.SetSubWidget(res.intermediate, app) gowid.RunWidgetCallbacks(res.Callbacks, IntermediateCB{}, app, res) })) }, } validator := Validator{ Valid: validcb, Invalid: invalidcb, KilledCB: killedcb, } // Save up filter changes, send latest over when process is ready, discard ones in between termshark.TrackedGo(func() { send := false var latest *filtStruct CL2: for { if send && latest != nil { res.runthisfilterchan <- latest latest = nil send = false } select { // tshark process ready case <-res.quitchan: break CL2 case <-res.readytorunchan: send = true // Sent by tshark process goroutine case fs := <-res.filterchangedchan: latest = fs // We're ready to run a new one, so kill any process that is in progress. Take care // because it might not have actually started yet! validator.Kill() } } }, Goroutinewg) // Every time it gets an event, it means run the process. Another goroutine takes care of consolidating // events. Stops when channel is closed termshark.TrackedGo(func() { CL: for { // Tell other goroutine we are ready for more - each time round the loop. This makes sure // we don't run more than one tshark process - it will get killed if a new filter should take // priority. 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) } } } } // 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, temporarilyDisabled *bool) appkeys.KeyInputFn { return func(evk *tcell.EventKey, app gowid.IApp) bool { handled := false switch evk.Key() { case tcell.KeyEnter: var dummy gowid.IWidget gowid.RunWidgetCallbacks(cb, SubmitCB{}, app, dummy) *temporarilyDisabled = true 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-focus")) 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) } // 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) } 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 || evk.Key() == tcell.KeyDown { 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 ev.Key() == tcell.KeyDown { 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 ev.Key() == tcell.KeyUp { 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.0.3/widgets/framefocus/000077500000000000000000000000001360044163000173115ustar00rootroot00000000000000termshark-2.0.3/widgets/framefocus/framefocus.go000066400000000000000000000035371360044163000220020ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package 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.0.3/widgets/hexdumper/000077500000000000000000000000001360044163000171605ustar00rootroot00000000000000termshark-2.0.3/widgets/hexdumper/hexdumper.go000066400000000000000000000347241360044163000215220ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package hexdumper provides a widget which displays selectable hexdump-like // output. Because it's built for termshark, it also allows styling to be // applied to ranges of data intended to correspond to packet structure selected // in another termshark view. package hexdumper import ( "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.0.3/widgets/hexdumper/hexdumper_test.go000066400000000000000000000021171360044163000225500ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. package hexdumper import ( "testing" "github.com/gcla/gowid" "github.com/gcla/gowid/gwtest" log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" ) //====================================================================== func TestDump1(t *testing.T) { widget1 := New([]byte("abcdefghijklmnopqrstuvwxyz0123456789 abcdefghijklmnopqrstuvwxyz0123456789")) //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.0.3/widgets/hexdumper2/000077500000000000000000000000001360044163000172425ustar00rootroot00000000000000termshark-2.0.3/widgets/hexdumper2/hexdumper2.go000066400000000000000000000347251360044163000216670ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package 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/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 } type Widget struct { data []byte layers []LayerStyler position int 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(), } 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 rows int cols := 1 + 4 + 3 + ((8 * 3) - 1) + 2 + ((8 * 3) - 1) + 3 + 8 + 1 + 8 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 c := gowid.NewCanvasOfSize(cols, canvasRows) rows := gwutil.Min(canvasRows, (len(w.data)+15)/16) 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 := 0; row < rows; 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, } for k := 0; k < rows; k++ { 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, k, lineNumStyle) } } } 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 ev.Key() { case tcell.KeyRight, tcell.KeyCtrlF: //res = Scroll(w, 1, w.Wrap(), app) pos := w.Position() if pos < len(w.data) { w.SetPosition(pos+1, app) res = true } case tcell.KeyLeft, tcell.KeyCtrlB: pos := w.Position() if pos > 0 { w.SetPosition(pos-1, app) res = true } case tcell.KeyDown, tcell.KeyCtrlN: scrollDown = true case tcell.KeyUp, tcell.KeyCtrlP: 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 } } } } if scrollDown { pos := w.Position() if pos+16 < len(w.data) { w.SetPosition(pos+16, app) res = true } } else if scrollUp { pos := w.Position() if pos-16 >= 0 { w.SetPosition(pos-16, app) res = true } } return res } //====================================================================== // 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.0.3/widgets/ifwidget/000077500000000000000000000000001360044163000167615ustar00rootroot00000000000000termshark-2.0.3/widgets/ifwidget/ifwidget.go000066400000000000000000000042161360044163000211150ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package ifwidget provides a simple widget that behaves differently depending on the condition // supplied. package ifwidget import ( "fmt" "github.com/gcla/gowid" ) //====================================================================== type Widget struct { wtrue gowid.IWidget wfalse gowid.IWidget pred Predicate } var _ gowid.IWidget = (*Widget)(nil) var _ gowid.ICompositeWidget = (*Widget)(nil) type Predicate func() bool func New(wtrue gowid.IWidget, wfalse gowid.IWidget, pred Predicate) *Widget { res := &Widget{ wtrue: wtrue, wfalse: wfalse, pred: pred, } return res } func (w *Widget) String() string { return fmt.Sprintf("ifwidget[%v]", w.SubWidget()) } func (w *Widget) SubWidget() gowid.IWidget { if w.pred() { return w.wtrue } else { return w.wfalse } } func (w *Widget) SetSubWidget(wi gowid.IWidget, app gowid.IApp) { if w.pred() { w.wtrue = wi } else { w.wfalse = wi } } func (w *Widget) SubWidgetSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderSize { return size } func (w *Widget) Selectable() bool { if w.pred() { return w.wtrue.Selectable() } else { return w.wfalse.Selectable() } } func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { if w.pred() { return 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.0.3/widgets/regexstyle/000077500000000000000000000000001360044163000173525ustar00rootroot00000000000000termshark-2.0.3/widgets/regexstyle/regexstyle.go000066400000000000000000000055431360044163000221030ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package 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.0.3/widgets/renderfocused/000077500000000000000000000000001360044163000200075ustar00rootroot00000000000000termshark-2.0.3/widgets/renderfocused/renderfocused.go000066400000000000000000000030161360044163000231660ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package renderfocused will render a widget with focus true package renderfocused import ( "github.com/gcla/gowid" ) //====================================================================== type Widget struct { gowid.IWidget } var _ gowid.IWidget = (*Widget)(nil) var _ gowid.ICompositeWidget = (*Widget)(nil) func New(w gowid.IWidget) *Widget { return &Widget{ IWidget: w, } } func (w *Widget) SubWidget() gowid.IWidget { return w.IWidget } func (w *Widget) SubWidgetSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderSize { return w.SubWidget().RenderSize(size, focus, app) } func (w *Widget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { return gowid.RenderSize(w.IWidget, size, gowid.Focused, app) } func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { return 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.0.3/widgets/resizable/000077500000000000000000000000001360044163000171375ustar00rootroot00000000000000termshark-2.0.3/widgets/resizable/resizable.go000066400000000000000000000154031360044163000214510ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package resizable provides columns and piles that can be adjusted. package resizable import ( "github.com/gcla/gowid" "github.com/gcla/gowid/widgets/columns" "github.com/gcla/gowid/widgets/pile" ) //====================================================================== type Offset struct { Col1 int `json:"col1"` Col2 int `json:"col2"` Adjust int `json:"adjust"` } type IOffsets interface { GetOffsets() []Offset SetOffsets([]Offset, gowid.IApp) } type OffsetsCB struct{} type ColumnsWidget struct { *columns.Widget Offsets []Offset Callbacks *gowid.Callbacks } var _ IOffsets = (*ColumnsWidget)(nil) func NewColumns(widgets []gowid.IContainerWidget) *ColumnsWidget { res := &ColumnsWidget{ Widget: columns.New(widgets), Offsets: make([]Offset, 0, 2), } return res } func (w *ColumnsWidget) GetOffsets() []Offset { return w.Offsets } func (w *ColumnsWidget) SetOffsets(offs []Offset, app gowid.IApp) { w.Offsets = offs } func (w *ColumnsWidget) OnOffsetsSet(cb gowid.IWidgetChangedCallback) { 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 } 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.0.3/widgets/resizable/resizable_test.go000066400000000000000000000017431360044163000225120ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. package resizable import ( "encoding/json" "testing" "github.com/stretchr/testify/assert" ) //====================================================================== func TestOffset1(t *testing.T) { off1 := Offset{2, 4, 7} off1m, err := json.Marshal(off1) assert.NoError(t, err) assert.Equal(t, "{\"col1\":2,\"col2\":4,\"adjust\":7}", string(off1m)) off2 := Offset{3, 1, 15} offs := []Offset{off1, off2} offsm, err := json.Marshal(offs) assert.NoError(t, err) assert.Equal(t, "[{\"col1\":2,\"col2\":4,\"adjust\":7},{\"col1\":3,\"col2\":1,\"adjust\":15}]", string(offsm)) offs2 := make([]Offset, 0) err = json.Unmarshal(offsm, &offs2) assert.NoError(t, err) assert.Equal(t, offs, offs2) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.0.3/widgets/streamwidget/000077500000000000000000000000001360044163000176565ustar00rootroot00000000000000termshark-2.0.3/widgets/streamwidget/streamwidget.go000066400000000000000000001073231360044163000227120ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package 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/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/regexstyle" "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 // gcla later todo - custom widget, don't make callbacks, who cares 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 searchState // track the current highlighted search term } func New(displayFilter string, captureDevice string, proto streams.Protocol, convMenu *menu.Widget, convMenuHolder *holder.Widget, 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, } 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) // gcla later todo 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 makeTable(data chunkList) (gowid.IWidget, *copymodetable.Widget) { cmtbl := copymodetable.New( table.BoundedWidget{Widget: table.New(data)}, data, data, "streamtable", copyModePalette{}, ) sc := &keepSelected{ sub: withscrollbar.New(scrollableTableWidget{ Widget: cmtbl, }, withscrollbar.Options{ HideIfContentFits: true, }), } 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, // gcla later todo 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( //gowid.RenderWithWeight{1}, fixed, hpad, hpadding.New( w.turnTxt, gowid.HAlignLeft{}, fixed, ), hpad, vline, hpad, &gowid.ContainerWidget{ IWidget: searchBoxStyled, D: gowid.RenderWithWeight{W: 1}, }, hpad, hpadding.New( //styled.NewInvertedFocus(nextBtn, gowid.MakePaletteRef("default")), //styled.NewExt(nextBtn, gowid.MakePaletteRef("default"), gowid. 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] = makeTable(w.data.vdata[i].hexChunks) 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 } // gcla later todo - duplicate of below, fix 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 } //====================================================================== // To implement withscrollbar.IScrollValues type scrollableTableWidget struct { *copymodetable.Widget } func (s scrollableTableWidget) ScrollLength() int { return s.Model().(table.IBoundedModel).Rows() } func (s scrollableTableWidget) ScrollPosition() int { return s.CurrentRow() } //====================================================================== // 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 } //====================================================================== // 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 keepSelected struct { sub gowid.IWidget } func (w *keepSelected) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { return w.sub.Render(size, focus.SelectIf(true), app) } func (w *keepSelected) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { return w.sub.RenderSize(size, focus.SelectIf(true), app) } func (w *keepSelected) 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 *keepSelected) Selectable() bool { return w.sub.Selectable() } func (w *keepSelected) SubWidget() gowid.IWidget { return w.sub } func (w *keepSelected) SetSubWidget(wi gowid.IWidget, app gowid.IApp) { w.sub = wi } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.0.3/widgets/trackfocus/000077500000000000000000000000001360044163000173235ustar00rootroot00000000000000termshark-2.0.3/widgets/trackfocus/trackfocus.go000066400000000000000000000040631360044163000220210ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package 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.0.3/widgets/trackfocus/trackfocus_test.go000066400000000000000000000022621360044163000230570ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. package 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.0.3/widgets/utils.go000066400000000000000000000016021360044163000166450ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. package 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 } return res } //====================================================================== // Local Variables: // mode: Go // fill-column: 78 // End: termshark-2.0.3/widgets/withscrollbar/000077500000000000000000000000001360044163000200365ustar00rootroot00000000000000termshark-2.0.3/widgets/withscrollbar/withscrollbar.go000066400000000000000000000117241360044163000232510ustar00rootroot00000000000000// Copyright 2019 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package withscrollbar provides a widget that renders with a scrollbar on the right package withscrollbar import ( "github.com/gcla/gowid" "github.com/gcla/gowid/widgets/columns" "github.com/gcla/gowid/widgets/selectable" "github.com/gcla/gowid/widgets/vscroll" ) //====================================================================== 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 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 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, goUpDown: 0, pgUpDown: 0, opt: opt, } sb.OnClickAbove(gowid.MakeWidgetCallback("cb", res.clickUp)) sb.OnClickBelow(gowid.MakeWidgetCallback("cb", res.clickDown)) sb.OnClickUpArrow(gowid.MakeWidgetCallback("cb", res.clickUpArrow)) sb.OnClickDownArrow(gowid.MakeWidgetCallback("cb", res.clickDownArrow)) return res } func (e *Widget) clickUp(app gowid.IApp, w gowid.IWidget) { e.pgUpDown -= 1 } func (e *Widget) clickDown(app gowid.IApp, w gowid.IWidget) { e.pgUpDown += 1 } func (e *Widget) clickUpArrow(app gowid.IApp, w gowid.IWidget) { e.goUpDown -= 1 } func (e *Widget) clickDownArrow(app gowid.IApp, w gowid.IWidget) { e.goUpDown += 1 } // 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 res := w.always.UserInput(ev, size, focus, app) if res { w.always.SetFocus(app, 0) } return res } 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) } box, ok := size.(gowid.IRenderBox) if !ok { panic(gowid.WidgetSizeError{Widget: w, Size: size, Required: "gowid.IRenderBox"}) } ecols := box.BoxColumns() - 1 var x int var y int var z int if ecols >= 1 { ebox := gowid.MakeRenderBox(ecols, box.BoxRows()) if w.goUpDown != 0 || w.pgUpDown != 0 { if w.goUpDown > 0 { w.w.Down(w.goUpDown, ebox, app) } else if w.goUpDown < 0 { w.w.Up(-w.goUpDown, ebox, app) } if w.pgUpDown > 0 { w.w.DownPage(w.pgUpDown, ebox, app) } else if w.pgUpDown < 0 { w.w.UpPage(-w.pgUpDown, ebox, app) } } w.goUpDown = 0 w.pgUpDown = 0 x, y, z = 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.0.3/widgets/withscrollbar/withscrollbar_test.go000066400000000000000000000034541360044163000243110ustar00rootroot00000000000000package 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()) }