pax_global_header00006660000000000000000000000064142631200450014507gustar00rootroot0000000000000052 comment=e436160ef4f7bac030a9bddd5674d3159ebdd792 termshark-2.4.0/000077500000000000000000000000001426312004500135125ustar00rootroot00000000000000termshark-2.4.0/.all-contributorsrc000066400000000000000000000315311426312004500173460ustar00rootroot00000000000000{ "projectName": "termshark", "projectOwner": "gcla", "repoType": "github", "repoHost": "https://github.com", "files": [ "README.md" ], "imageSize": 100, "commit": false, "contributorsPerLine": 7, "commitConvention": "none", "contributors": [ { "login": "pocc", "name": "Ross Jacobs", "avatar_url": "https://avatars0.githubusercontent.com/u/10995145?v=4", "profile": "https://swit.sh", "contributions": [ "code", "bug", "userTesting" ] }, { "login": "Hongarc", "name": "Hongarc", "avatar_url": "https://avatars1.githubusercontent.com/u/19208123?v=4", "profile": "https://github.com/Hongarc", "contributions": [ "doc" ] }, { "login": "zi0r", "name": "Ryan Steinmetz", "avatar_url": "https://avatars0.githubusercontent.com/u/1676702?v=4", "profile": "https://github.com/zi0r", "contributions": [ "platform" ] }, { "login": "NicolaiSoeborg", "name": "Nicolai Søborg", "avatar_url": "https://avatars2.githubusercontent.com/u/8722223?v=4", "profile": "https://søb.org/", "contributions": [ "platform" ] }, { "login": "QuLogic", "name": "Elliott Sales de Andrade", "avatar_url": "https://avatars2.githubusercontent.com/u/302469?v=4", "profile": "https://qulogic.gitlab.io/", "contributions": [ "code" ] }, { "login": "rski", "name": "Romanos", "avatar_url": "https://avatars2.githubusercontent.com/u/2960312?v=4", "profile": "http://rski.github.io", "contributions": [ "code" ] }, { "login": "denyspozniak", "name": "Denys", "avatar_url": "https://avatars0.githubusercontent.com/u/22612345?v=4", "profile": "https://github.com/denyspozniak", "contributions": [ "bug" ] }, { "login": "jerry73204", "name": "jerry73204", "avatar_url": "https://avatars1.githubusercontent.com/u/7629150?v=4", "profile": "https://github.com/jerry73204", "contributions": [ "platform" ] }, { "login": "Thann", "name": "Jon Knapp", "avatar_url": "https://avatars1.githubusercontent.com/u/578515?v=4", "profile": "http://thann.github.com", "contributions": [ "platform" ] }, { "login": "mharjac", "name": "Mario Harjac", "avatar_url": "https://avatars2.githubusercontent.com/u/2997453?v=4", "profile": "https://github.com/mharjac", "contributions": [ "platform" ] }, { "login": "abenson", "name": "Andrew Benson", "avatar_url": "https://avatars1.githubusercontent.com/u/227317?v=4", "profile": "https://github.com/abenson", "contributions": [ "bug" ] }, { "login": "sagis-tikal", "name": "sagis-tikal", "avatar_url": "https://avatars2.githubusercontent.com/u/46102019?v=4", "profile": "https://github.com/sagis-tikal", "contributions": [ "bug" ] }, { "login": "punkymaniac", "name": "punkymaniac", "avatar_url": "https://avatars2.githubusercontent.com/u/9916797?v=4", "profile": "https://github.com/punkymaniac", "contributions": [ "bug" ] }, { "login": "msenturk", "name": "msenturk", "avatar_url": "https://avatars3.githubusercontent.com/u/9482568?v=4", "profile": "https://github.com/msenturk", "contributions": [ "bug" ] }, { "login": "szuecs", "name": "Sandor Szücs", "avatar_url": "https://avatars3.githubusercontent.com/u/50872?v=4", "profile": "https://github.com/szuecs", "contributions": [ "bug" ] }, { "login": "dawidd6", "name": "Dawid Dziurla", "avatar_url": "https://avatars1.githubusercontent.com/u/9713907?v=4", "profile": "https://github.com/dawidd6", "contributions": [ "bug", "platform" ] }, { "login": "jJit0", "name": "jJit0", "avatar_url": "https://avatars1.githubusercontent.com/u/23521148?v=4", "profile": "https://github.com/jJit0", "contributions": [ "bug" ] }, { "login": "inzel", "name": "inzel", "avatar_url": "https://avatars3.githubusercontent.com/u/20195547?v=4", "profile": "http://colinrogers001.com", "contributions": [ "bug" ] }, { "login": "thejerrod", "name": "thejerrod", "avatar_url": "https://avatars1.githubusercontent.com/u/25254103?v=4", "profile": "https://github.com/thejerrod", "contributions": [ "ideas" ] }, { "login": "gdluca", "name": "gdluca", "avatar_url": "https://avatars3.githubusercontent.com/u/12004506?v=4", "profile": "https://github.com/gdluca", "contributions": [ "bug" ] }, { "login": "winpat", "name": "Patrick Winter", "avatar_url": "https://avatars2.githubusercontent.com/u/6016963?v=4", "profile": "https://github.com/winpat", "contributions": [ "platform" ] }, { "login": "RobertLarsen", "name": "Robert Larsen", "avatar_url": "https://avatars0.githubusercontent.com/u/795303?v=4", "profile": "https://github.com/RobertLarsen", "contributions": [ "ideas", "userTesting" ] }, { "login": "mingrammer", "name": "MinJae Kwon", "avatar_url": "https://avatars0.githubusercontent.com/u/6178510?v=4", "profile": "https://mingrammer.com", "contributions": [ "bug" ] }, { "login": "the-c0d3r", "name": "the-c0d3r", "avatar_url": "https://avatars2.githubusercontent.com/u/4526565?v=4", "profile": "https://github.com/the-c0d3r", "contributions": [ "ideas" ] }, { "login": "gvanem", "name": "Gisle Vanem", "avatar_url": "https://avatars0.githubusercontent.com/u/945271?v=4", "profile": "https://github.com/gvanem", "contributions": [ "bug" ] }, { "login": "hook-s3c", "name": "hook", "avatar_url": "https://avatars1.githubusercontent.com/u/31825993?v=4", "profile": "https://github.com/hook-s3c", "contributions": [ "bug" ] }, { "login": "lennartkoopmann", "name": "Lennart Koopmann", "avatar_url": "https://avatars0.githubusercontent.com/u/35022?v=4", "profile": "https://twitter.com/_lennart", "contributions": [ "ideas" ] }, { "login": "ReK2Fernandez", "name": "Fernandez, ReK2", "avatar_url": "https://avatars1.githubusercontent.com/u/5316229?v=4", "profile": "https://keybase.io/cfernandez", "contributions": [ "bug" ] }, { "login": "mazball", "name": "mazball", "avatar_url": "https://avatars2.githubusercontent.com/u/22456251?v=4", "profile": "https://github.com/mazball", "contributions": [ "ideas" ] }, { "login": "wfailla", "name": "wfailla", "avatar_url": "https://avatars1.githubusercontent.com/u/5494665?v=4", "profile": "https://github.com/wfailla", "contributions": [ "ideas" ] }, { "login": "rongyi", "name": "荣怡", "avatar_url": "https://avatars3.githubusercontent.com/u/1034762?v=4", "profile": "https://github.com/rongyi", "contributions": [ "ideas" ] }, { "login": "thebyrdman-git", "name": "thebyrdman-git", "avatar_url": "https://avatars1.githubusercontent.com/u/55452713?v=4", "profile": "https://github.com/thebyrdman-git", "contributions": [ "bug" ] }, { "login": "cmosig", "name": "Clemens Mosig", "avatar_url": "https://avatars2.githubusercontent.com/u/32590522?v=4", "profile": "http://www.mi.fu-berlin.de/en/inf/groups/ilab/members/mosig.html", "contributions": [ "bug" ] }, { "login": "mrash", "name": "Michael Rash", "avatar_url": "https://avatars3.githubusercontent.com/u/380228?v=4", "profile": "http://www.cipherdyne.org/", "contributions": [ "userTesting" ] }, { "login": "joelparker", "name": "joelparker", "avatar_url": "https://avatars3.githubusercontent.com/u/136451?v=4", "profile": "https://github.com/joelparker", "contributions": [ "userTesting" ] }, { "login": "dragosmaftei", "name": "Dragos Maftei", "avatar_url": "https://avatars1.githubusercontent.com/u/15351028?v=4", "profile": "https://github.com/dragosmaftei", "contributions": [ "ideas" ] }, { "login": "IAXES", "name": "Matthew Giassa", "avatar_url": "https://avatars1.githubusercontent.com/u/8325672?v=4", "profile": "http://www.giassa.net", "contributions": [ "ideas" ] }, { "login": "sean-abbott", "name": "Sean Abbott", "avatar_url": "https://avatars0.githubusercontent.com/u/1402071?v=4", "profile": "https://github.com/sean-abbott", "contributions": [ "platform" ] }, { "login": "linsong", "name": "Vincent Wang", "avatar_url": "https://avatars1.githubusercontent.com/u/36017?v=4", "profile": "http://www.linsong.org", "contributions": [ "ideas" ] }, { "login": "Piping", "name": "piping", "avatar_url": "https://avatars3.githubusercontent.com/u/12042284?v=4", "profile": "https://github.com/Piping", "contributions": [ "ideas" ] }, { "login": "kevinhwang91", "name": "kevinhwang91", "avatar_url": "https://avatars0.githubusercontent.com/u/17562139?v=4", "profile": "https://github.com/kevinhwang91", "contributions": [ "ideas", "bug" ] }, { "login": "jboverfelt", "name": "Justin Overfelt", "avatar_url": "https://avatars0.githubusercontent.com/u/936126?v=4", "profile": "https://jbo.io", "contributions": [ "ideas" ] }, { "login": "loudsong", "name": "Anthony", "avatar_url": "https://avatars3.githubusercontent.com/u/1447613?v=4", "profile": "https://github.com/loudsong", "contributions": [ "ideas" ] }, { "login": "basondole", "name": "basondole", "avatar_url": "https://avatars2.githubusercontent.com/u/50369643?v=4", "profile": "https://github.com/basondole", "contributions": [ "bug" ] }, { "login": "zoulja", "name": "zoulja", "avatar_url": "https://avatars1.githubusercontent.com/u/10187203?v=4", "profile": "https://github.com/zoulja", "contributions": [ "bug" ] }, { "login": "freddii", "name": "freddii", "avatar_url": "https://avatars.githubusercontent.com/u/7213207?v=4", "profile": "https://github.com/freddii", "contributions": [ "bug" ] }, { "login": "thordy", "name": "Thord Setsaas", "avatar_url": "https://avatars.githubusercontent.com/u/1622278?v=4", "profile": "https://github.com/thordy", "contributions": [ "doc" ] }, { "login": "deliciouslytyped", "name": "deliciouslytyped", "avatar_url": "https://avatars.githubusercontent.com/u/47436522?v=4", "profile": "https://github.com/deliciouslytyped", "contributions": [ "bug" ] }, { "login": "factorion", "name": "factorion", "avatar_url": "https://avatars.githubusercontent.com/u/40322086?v=4", "profile": "https://github.com/factorion", "contributions": [ "platform" ] }, { "login": "herbygillot", "name": "Herby Gillot", "avatar_url": "https://avatars.githubusercontent.com/u/618376?v=4", "profile": "https://github.com/herbygillot", "contributions": [ "platform" ] }, { "login": "nmeum", "name": "nmeum", "avatar_url": "https://avatars.githubusercontent.com/u/2326560?v=4", "profile": "https://github.com/nmeum", "contributions": [ "ideas" ] }, { "login": "qbit", "name": "Aaron Bieber", "avatar_url": "https://avatars.githubusercontent.com/u/68368?v=4", "profile": "https://deftly.net", "contributions": [ "ideas" ] }, { "login": "elig0n", "name": "elig0n", "avatar_url": "https://avatars.githubusercontent.com/u/31196036?v=4", "profile": "https://github.com/elig0n", "contributions": [ "ideas" ] } ] } termshark-2.4.0/.github/000077500000000000000000000000001426312004500150525ustar00rootroot00000000000000termshark-2.4.0/.github/ISSUE_TEMPLATE/000077500000000000000000000000001426312004500172355ustar00rootroot00000000000000termshark-2.4.0/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000022171426312004500217310ustar00rootroot00000000000000--- 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.4.0/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000011251426312004500227610ustar00rootroot00000000000000--- 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.4.0/.github/workflows/000077500000000000000000000000001426312004500171075ustar00rootroot00000000000000termshark-2.4.0/.github/workflows/go.yml000066400000000000000000000011611426312004500202360ustar00rootroot00000000000000name: Go on: [push] jobs: build: name: Build runs-on: ubuntu-latest steps: - name: Set up Go 1.18 uses: actions/setup-go@v3 with: go-version: '>=1.18.0' id: go - name: Check out code into the Go module directory uses: actions/checkout@v3 - name: Get dependencies run: | go get -v -t -d ./... - 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.4.0/.gitignore000066400000000000000000000001301426312004500154740ustar00rootroot00000000000000Makefile dist/ .vscode/ *~ /cmd/termshark/termshark /typescript /go.work /go.work.sum termshark-2.4.0/.goreleaser.yml000066400000000000000000000021641426312004500164460ustar00rootroot00000000000000# This is an example goreleaser.yaml file with some sane defaults. # Make sure to check the documentation at http://goreleaser.com before: hooks: builds: - env: - CGO_ENABLED=0 - GO111MODULE=on main: ./cmd/termshark/termshark.go goos: - freebsd - netbsd - openbsd - windows - linux - darwin goarch: - arm - arm64 - amd64 ignore: - goos: darwin goarch: arm - goos: freebsd goarch: arm - goos: netbsd goarch: arm - goos: openbsd goarch: arm - goos: windows goarch: arm - goos: freebsd goarch: arm64 - goos: netbsd goarch: arm64 - goos: openbsd goarch: arm64 - goos: windows goarch: arm64 archives: - replacements: darwin: macOS linux: linux windows: windows amd64: x64 wrap_in_directory: true format_overrides: - goos: windows format: zip files: - none* signs: - artifacts: checksum checksum: name_template: 'checksums.txt' snapshot: name_template: "{{ .Env.TERMSHARK_GIT_DESCRIBE }}" changelog: sort: asc filters: exclude: - '^docs:' - '^test:' termshark-2.4.0/.travis.yml000066400000000000000000000061041426312004500156240ustar00rootroot00000000000000language: 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.18.x - 1.17.x - 1.16.x - 1.15.x - 1.14.x notifications: email: true before_install: - openssl aes-256-cbc -K $encrypted_1286cb654632_key -iv $encrypted_1286cb654632_iv -in configs/termshark-dd01307f2423.json.enc -out /tmp/google.json -d - sudo apt-get install -y tshark socat coreutils addons: apt: update: true script: - go test -v ./... - bash scripts/simple-tests.sh deploy: - provider: script skip_cleanup: true script: bash scripts/do-release.sh on: all_branches: true condition: "$TRAVIS_OS_NAME = linux && $TRAVIS_GO_VERSION = 1.18.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.18.x" termshark-2.4.0/CHANGELOG.md000066400000000000000000000220361426312004500153260ustar00rootroot00000000000000# Changelog ## [2.4.0] - 2022-07-11 ### Added - You can now search for information in packets, in similar fashion to Wireshark's packet search. Hit `ctrl-f` to open the search bar. - Termshark now supports Wireshark-like profiles. Access via the new minibuffer profile commands: create to make a new profile; use to switch profile. A profile can be "linked" to a Wireshark profile to make use of Wireshark color profiles in termshark. ### Changed - Now you can build and install termshark with one command: `go install github.com/gcla/termshark/v2/cmd/termshark` - Fixed a bug that resulted in stream reassembly sporadically failing and displaying a blank screen. - Termshark will now, by default, suppress errors from tshark. You can change this via the minibuffer `set suppress-tshark-errors` command. - Added a summary of standard error to the error dialogs displayed when a tshark process fails to run correctly (has a non-zero exit code). - Fixed a race condition that caused extcap captures (e.g. randpkt) to sporadically fail. - Dark-mode is now the default in the absence of a specific user-setting. - Fixed a bug that caused mouse-clicks within the hex view to not function correctly if the viewport was not at the top of the data to be displayed. - When focus is in the packet hex view, the mouse wheel will no longer move the cursor - instead it will move the scroll position. - If the display filter is empty, it is now displayed in cyan to indicate it is not yet either valid or invalid. This can be changed via the `filter-empty` theme element. - In the conversations view, a column of IP addresses is now sorted numerically rather than lexicographically. - Various text input widgets now support "bracketed-paste" meaning they understand when content is pasted into the terminal. The result is a smoother interface with fewer updates. - Fixed a bug that caused the "client pkts" and "server pkts" counts in the stream reassembly view not to be updated. ## [2.3.0] - 2021-09-04 ### Added - Termshark's columns can now be changed via the minibuffer `columns` command. Columns can be added, removed or hidden from view. If your Wireshark config is available, termshark can import your Wireshark column configuration. Custom columns can be chosen via a display filter expression. - The packet structure view now provides a contextual menu with options to - apply the structure filter as a custom column - prepare or apply the same filter as a display filter - A new console-command, "wormhole", allows you to send termshark's current pcap with magic wormhole. Pair with the tmux plugin tmux-wormhole to open the pcap quickly in Wireshark. - Added a -w flag - if supplied for a live capture, termshark will write the packets to this capture file. - Added a config option, main.disk-cache-size-mb, that can be set to have termshark limit the size of its pcap cache directory. When the directory size exceeds its limit, termshark deletes oldest pcap files first. - Added a workflow that helps a user to upgrade from a low-color TERM setting if termshark detects that there is a 256-color version available in the terminfo database. - Added 8-color light and dark themes for TERMs such as xterm and screen. - Termshark is now available for M1 on Mac. ### Changed - Fixed a bug that caused "And" and "Or" conversation filters to be incorrect if the current display filter is empty. - Fixed a bug that caused multi-token capture filters to fail. - Fixed a bug that slowed down the user's interaction with the display filter widget. ## [2.2.0] - 2021-01-03 ### Added - Termshark is now available for linux/arm64, NetBSD and OpenBSD. - Vim keys h, j, k and l can now be used in widgets that accept left, down, up and right user input. - Termshark's tables support vim-style navigation - use G to go to the bottom, gg to go to the top, or add a numeric prefix. - Other vim-style navigation keypresses are now implemented :r/:e to load a pcap, :q! to quit, ZZ to quit, C-w C-w to cycle through views and C-w = to reset spacing. - You can now set packet marks with the m key (e.g. ma, mb). Jump to packet marks with the ' key (e.g. 'a, 'b). Set cross-file packet marks with capital letters (e.g. mA, mB). Jump to last location with ''. - Display termshark's log file via the new menu option "Show Log" - Termshark now provides last-line mode/a minibuffer for issuing commands. Access it with the ":" key. - Termshark provides the following minibuffer commands: - `recents` - pick a pcap from recently loaded files. - `filter` - pick a display filter from the recently used list. - `set` - set various config properties. - `marks` - display currently set local and cross-file packet marks. - Map keys to other key sequences using a vim-style map command e.g. `map ZZ`. Use vim-syntax to express keystrokes - alphanumeric characters, and angle brackets for compound keys (``, ``, ``, ``, ``) - Added support for themes. See this [example](https://raw.githubusercontent.com/gcla/termshark/master/assets/themes/dracula-256.toml). Themes are loaded from `~/.config/termshark/themes/` or from a small cache built-in to termshark. A new minibuffer command `theme` can be used to change theme; `no-theme` turns off theming. ### Changed - Fixed a race condition that allowed an invalid Wireshark display filter to be applied. - Fixed race conditions that resulted in spurious warnings about a failure to kill tshark processes - If auto-scroll is enabled, and you navigate to a different packet in the packet list view during a live capture, auto-scroll is resumed if you hit 'G' or the `end` key. - Fixed a problem preventing the correct operation of piped input to termshark on freebsd. - The Escape key no longer opens the main menu. Instead it puts focus on the menu button. Hit Enter to open. This is more intuitive with the presence of ":" to open the minibuffer. ## [2.1.1] - 2020-02-02 ### Added - Termshark now provides a conversations view for the most common conversation types. - Termshark now supports multiple live captures/interfaces on the command-line e.g. `termshark -i eth0 -i eth1` - Termshark's packet hex view displays a scrollbar if the data doesn't fit in the space available. - Termshark can show a capture file's properties using the capinfos binary (bundled with tshark). - Termshark now supports [extcap interfaces](https://tshark.dev/capture/sources/extcap_interfaces/) by default. ## [2.0.3] - 2019-12-23 ### Added - Termshark now colorizes its packet list view by default, using the current Wireshark `colorfilter` rules. - Termshark now supports tshark's `-t` option to specify the timestamp format in the packet list view. ### Changed - Fixed a potential deadlock when reassembling very long streams. ## [2.0.2] - 2019-11-11 ### Changed - Internal Go API name changes that I didn't understand when I released termshark V2. ## [2.0.1] - 2019-11-10 ### Changed - Fix a mistake that caused a build break on homebrew. ## [2.0.0] - 2019-11-10 ### Added - Termshark supports TCP and UDP stream reassembly. See termshark's "Analysis" menu. - By popular demand, termshark now has a dark mode! To turn on, run termshark and open the menu. - Termshark can be configured to "auto-scroll" when reading live data (interface, fifo or stdin). - Termshark uses less CPU, is less laggy under mouse input, and will use less than half as much RAM on larger pcaps. - Termshark now supports piped input e.g. ``` $ tshark -i eth0 -w - | termshark ``` - Termshark now supports input from a fifo e.g. ``` 1$ mkfifo myfifo 1$ tshark -i eth0 -w myfifo 2$ termshark -r myfifo ``` - Termshark supports running its UI on a different tty (make sure the tty doesn't have another process competing for reads and writes). This is useful if you are feeding termshark with data from a process that writes to stderr, or if you want to see information displayed in the terminal that would be covered up by termshark's UI e.g. ``` termshark -i eth0 --tty=/dev/pts/5 ``` - Like Wireshark, termshark will now preserve the opened and closed structure of a packet as you move from one packet to the next. This lets the user see differences between packets more easily. - Termshark can now be installed for MacOS from [Homebrew](docs/FAQ.md#homebrew). - Termshark now respects job control signals sent via the shell i.e. SIGTSTP and SIGCONT. - Termshark on Windows no longer depends on the Cywgin tail command (and thus a Cygwin installation). - The current packet capture source (file, interface, pipe, etc) is displayed in the termshark title bar. - Termshark can be configured to eagerly load all pcap PDML data, rather than 1000 packets at a time. ### Changed - You can now simply hit enter in the display filter widget to make its value take effect. ## [1.0.0] - 2019-04-17 - Initial release. [Unreleased]: https://github.com/gcla/termshark/commpare/v2.1.1...HEAD [1.0.0]: https://github.com/gcla/termshark/releases/tag/v1.0.0 [2.0.0]: https://github.com/gcla/termshark/releases/tag/v2.0.0 [2.0.3]: https://github.com/gcla/termshark/releases/tag/v2.0.3 [2.1.1]: https://github.com/gcla/termshark/releases/tag/v2.1.1 termshark-2.4.0/LICENSE000066400000000000000000000020701426312004500145160ustar00rootroot00000000000000The 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.4.0/README.md000066400000000000000000000464051426312004500150020ustar00rootroot00000000000000[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.4 is out now with packet search and profiles for colors and columns! See the [ChangeLog](CHANGELOG.md#changelog).** ![demo24](/../gh-pages/images/demo4.gif?raw=true) If you're debugging on a remote machine with a large pcap and no desire to scp it back to your desktop, termshark can help! ## Features - Read pcap files or sniff live interfaces (where tshark is permitted) - Filter pcaps or live captures using Wireshark's display filters - Reassemble and inspect TCP and UDP flows - View network conversations by protocol - Copy ranges of packets to the clipboard from the terminal - Written in Golang, compiles to a single executable on each platform - downloads available for Linux, macOS, BSD variants, 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), [MacPorts](docs/Packages.md#macports), [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. Set `GO111MODULE=on` then run: ```bash go install github.com/gcla/termshark/v2/cmd/termshark ``` For versions of Go between 1.14 and 1.17, use ```bash go get github.com/gcla/termshark/v2/cmd/termshark ``` Then add ```~/go/bin/``` to your ```PATH```. For all packet analysis, termshark depends on tshark from the Wireshark project. Make sure ```tshark``` is in your ```PATH```. ## Quick Start Inspect a local pcap: ```bash termshark -r test.pcap ``` Capture ping packets on interface ```eth0```: ```bash termshark -i eth0 icmp ``` Run ```termshark -h``` for options. ## Downloads Pre-compiled executables are available via [Github releases](https://github.com/gcla/termshark/releases). Or download the latest build from the master branch - [![Build Status](https://travis-ci.com/gcla/termshark.svg?branch=master)](https://travis-ci.com/gcla/termshark). ## Documentation See the [termshark user guide](docs/UserGuide.md), and my best guess at some [FAQs](docs/FAQ.md). For a summary of updates, see the [ChangeLog](CHANGELOG.md#changelog). ## Dependencies Termshark depends on these open-source packages: - [tshark](https://www.wireshark.org/docs/man-pages/tshark.html) - command-line network protocol analyzer, part of [Wireshark](https://wireshark.org) - [tcell](https://github.com/gdamore/tcell) - a cell based terminal handling package, inspired by termbox - [gowid](https://github.com/gcla/gowid) - compositional terminal UI widgets, inspired by [urwid](http://urwid.org), built on [tcell](https://github.com/gdamore/tcell) Note that tshark is a run-time dependency, and must be in your ```PATH``` for termshark to function. Version 1.10.2 or higher is required (approx 2013). ## Contributors Thanks to everyone that's contributed ports, patches and effort!

Ross Jacobs

💻 🐛 📓

Hongarc

📖

Ryan Steinmetz

📦

Nicolai Søborg

📦

Elliott Sales de Andrade

💻

Romanos

💻

Denys

🐛

jerry73204

📦

Jon Knapp

📦

Mario Harjac

📦

Andrew Benson

🐛

sagis-tikal

🐛

punkymaniac

🐛

msenturk

🐛

Sandor Szücs

🐛

Dawid Dziurla

🐛 📦

jJit0

🐛

inzel

🐛

thejerrod

🤔

gdluca

🐛

Patrick Winter

📦

Robert Larsen

🤔 📓

MinJae Kwon

🐛

the-c0d3r

🤔

Gisle Vanem

🐛

hook

🐛

Lennart Koopmann

🤔

Fernandez, ReK2

🐛

mazball

🤔

wfailla

🤔

荣怡

🤔

thebyrdman-git

🐛

Clemens Mosig

🐛

Michael Rash

📓

joelparker

📓

Dragos Maftei

🤔

Matthew Giassa

🤔

Sean Abbott

📦

Vincent Wang

🤔

piping

🤔

kevinhwang91

🤔 🐛

Justin Overfelt

🤔

Anthony

🤔

basondole

🐛

zoulja

🐛

freddii

🐛

Thord Setsaas

📖

deliciouslytyped

🐛

factorion

📦

Herby Gillot

📦

nmeum

🤔

Aaron Bieber

🤔

elig0n

🤔
## 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.4.0/assets/000077500000000000000000000000001426312004500150145ustar00rootroot00000000000000termshark-2.4.0/assets/gen.go000066400000000000000000000000571426312004500161160ustar00rootroot00000000000000//go:generate statik -src=. -f package assets termshark-2.4.0/assets/statik/000077500000000000000000000000001426312004500163135ustar00rootroot00000000000000termshark-2.4.0/assets/statik/statik.go000066400000000000000000000352101426312004500201420ustar00rootroot00000000000000// Code generated by statik. DO NOT EDIT. package statik import ( "github.com/rakyll/statik/fs" ) func init() { data := "PK\x03\x04\x14\x00\x08\x00\x08\x00}\xac\x8aT\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00 \x00gen.goUT\x05\x00\x01\xbfMSb\xd2\xd7O\xcf\xb7JO\xcdK-J,IU(.I,\xc9\xccV\xd0-.J\xb6\xd5S\xd0M\xe3\xe2*HL\xceNLOUH,.N-)\xe6\x02\x04\x00\x00\xff\xffPK\x07\x080\xf3\x8fG5\x00\x00\x00/\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x08\x00\x00\xbe\xbeT\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x16\x00 \x00themes/base16-256.tomlUT\x05\x00\x01\xb0W\x95b\xd4\x96\xcbr\xa3:\x10\x86\xf7~\n\x17g\x1bR!\xd7\x93\xc5\xac\xe61RY\x08h\xdb\xaa4\x88\x92Z\xf1\xf8\xed\xa7$lI\x80\x05r\xaa<3Y\xba\xdd\x1f\x7fw\xeb\xf6\xaft\xab\x15\xd4\xeb\x1f\xeb\xec\xbf\x97W(\n\x96\xadVo%SP<\xbf\xaf\xd6\xeb\x12Y\xf5a\xfe\xfd)P\xc8\xbb\xbb\xcc\xc4\x98\x82\xbb\xc2\x05\x8b{\x17\xbc\xf7\xc1\x07\x17|\xf0\xf8\xff.\xf8\xe83\x1f]\xf0\xc9g\xbe\xb8\xe0\xb3\xcf|2\xc1\xfd\x8e\x13\xf8\xc4\x8d\x89\xc9\xbe\x83>\xf2j\"B\xb2v\x0bk\xcf\xda\xca\x0f\x80(\xf6>\xb54\xc1\xad\x04h}\x8c\x99Xu`A\x08l1\xa8\x03Y[t\xa7e\x87A\xd0\xd6WJ\xb1\xf7lQ\x98y\xd6L~\xd8ij\"a\xfe|\xcb\xfa\x11\xdf\xda\xf9f7\xee\xa7\x9dM\xe6s\xf3\x8d\xa8\xb4\n \xdb\xbf'\xfa\x1aBB\x01BEv\"1(\x90\xa9\x9a\x1ay\x0b35\xf5C\x0b\x93\xf3i\x1f\xc7\xa4\x9b\xe17\x06\x8c\x905\xc8D\x1d\xd1\x1d\xf2F\xd4\xa9e\x9d\xd2s\x86\x94X\x95C\x90\x95\x803\xb3\x92P\xf7\x84\x96\x12Z\xca+\xd6\x91\x96p\x8e\xa0\x1d4\xa0n\xfb#e\xa1\x9a3\x14\xdb\xb4.\xfa\xdc\x8bf[\xc3\x86\xe9a\xcb\xe3\x95v\xb9\x1b\x8e\x042\x87\xa6\xa3\xc3LAf\xe7\x87\xf9\xbc%\x90\x0d\xd4\x9c\xd1\xdcj\xf4\xe7m\x08~2\xe4\xf5\x0cs\x9a\xec\x11h\xa0\xd5\x97t\xb2\xf4y{\xae-\xb0\x83_yy H9\x1a\xc1yr\x98n\x13@_\x9f\xe16\x1c\xb0>\xab\x17\x9b\xb7\xa7\xce\xcb\x8d\xb8^\xfd\x04\xdaU\xfad\x98\xd2ai/\xe5)\x9a\xd6\xa5\xbd\xe6\x1d\x8c\xec\x00\xf2\x0b\xa2=w\xa9b\xc7\xaa\x0f\xa0\x1c\xb9\xa2\xbc\x02\xc4K.\xc7 \x9b*\xfd0\xc1\xa5\xd8/*\xbbe\x1d\x83\xa9\xb2\x83\x8e\x15I]\xd1\xc5\xa2G,a7\x85\x9dJ\xb1\x95\xa0T^\x89\xa6C\xa0\xb37\xdd\x99\xf9\x9e\xb0K\xee$\x07\xa9\x8e\xb7\xed\xf0u\x18\xdfz\x81VB\xb6\xd7P$\x815y\x85\x1cZJ\xb8\x8d\x8e\xf9\x0d\xa3j\x97vq\x1f \x05L\xce\"\xfe\xc8:B~\xce>\x89\xc6sX\x808\xe1`!L\xb57\x99{iVo\xc8\xb7;zO\xf1\x18\xbe\x8c\x98\xc5\x18\x01i\x16#\xeed\xbe\x8f\xc5\x18m\xd4\xafX\x0c?\xdd\x19\x8b\x11\xd9z3\x16\xe3D\xfcUo\x11\xddH\xdf\xce[,u\xb2\xf4\xf9k{\x8b\xd9\xc7\xfe\xea\xde\xa2\x8c8\x84\x84\x16ch\xba\xee\x93\x83\xafa.\xe2\x8aW7\x17\xf1\x0b\xf2\xaa\xe6b\xb1\xe3?c.\x8e\x92\xff\xa8\xb9\x08\xec\xc2r\xf2\xe4\x1d\x9fz\x8bQQK\xde\"\xfe\xeeD\xbdE\xb4\xed\xa8\xb7\x98\x10\x89\xde\xe2w\x00\x00\x00\xff\xffPK\x07\x08\x19\xcc\xe2\xfa\xb1\x02\x00\x00#\x12\x00\x00PK\x03\x04\x14\x00\x08\x00\x08\x00\x00\xbe\xbeT\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x16\x00 \x00themes/default-16.tomlUT\x05\x00\x01\xb0W\x95b\xd4\x97\xcfn\xdb0\x0c\xc6\xefy\n\xc3\xe7\xaa\xe8\xda\x0e\xd8\x0e;\xed1\x82\x1e\x14\x99I\x84\xd0\x96!S\xc9\xfc\xf6\x83%\xffQl\xd9V\n$[\x8fe\xf4\xf3'\x92\x12\xf5uc\nSA\x96\xfcJ\xd2\xdf\n\x95~yI7\x9bm\x06{n\x90>6I\xb2C.NW?'\xc9A\xf3\xfa\xdb\x10\xfb\xd1\xc5^\x03\xb1\xb7@\xec}\x14\xbb\x1c%\xc1\x10\xdb71\xedo\xeag\x13\xa9\x01Q]\x86\xe0\xce}\x0e\xa0\x18b\xbc\x89\x89\x9a\x17\xc9\x10\x83\xd4\xa6a<\x85\xf7&T\x1a]\xa2\x17\xfcn3\xe7\xfad\xd36D\xaa\xf9\xf06m\x8b\xf1l+\x91>\xf5\x7f\xdb*\xa4\xc3b\xb6W\xc2TW\x88M\xccC\x9c\xa4\xcfT\x80 \xc8\xe6:\x8f9\xe5\x86\x12y\x86\xb2\x80\xc5}\xb9:\xf9\xcbY \x9bv\xd5\x82\x08\xdb)\x9d\x81\x8e\xd6Re\xcdr\x95\xc5o\xae\x03\x18G\x8a\xde[\x0f!\xdf\x01.\x96MC\xe6\x18\xa35\x14\xc4\x04/\xc9h\x082t\x84\x1c\xaagw\x1b,\x95I\x8e\xea\x10\x9b\x8c[}c\xa1\xdbHd\xef\xf7\x12 4\x83\xbc\xa4zq[\xcd\xf9\xf7 Y\x10\xe8\x1c2\xc9)\xba9=z\xe6(\xb3E\xaa+s\x8b\xe4P\x98\xdb2Z\x97\xb0\xb7\xdc\"G\xf8\xc3v5A\xf8\xe2\x8c9\xef\xbe\xf5\xa0)n\xbcs\x0d\xb9\x97\x80Y\x9cf_\xfd\x81\x9b\x91\x1c\x93n\x0b\x1dj\xbbv\xe6\x187\"\xec\xf8\x9d\xb2\x91\xc9\x0e\xa3\xac\xa1\x91\xd7\xa0?#\xeb\xc0\x9b5K.N@\x0ceEL\x00b`\x90.4vBG\xab\xbfNx\xad.\x01q\xb76\xd0\xe11\x1a\xad|\x95wE\xda\x08\x8aHz\xac\xdb\x82Qg\xcb\xcbW\xab\x83\x86\xaabB\xe5%\x02\x85\xe7a\xa8\xd0\x1d\xd8\xfe\x16y}z\xac*eQ\x8c\xde\x93\xc9p\xf4\xf4\xa2\xd6\x0f:\x15i\xe09\x13(\xa1\xa0\xa8\x81\xd5\x129'q\x8c\x9d\x8b-S\x01\xd7+\xd0p\x9b{F\x9fG\xe9\xd8\x8e\xfa\xc9\x18G\x90$\xbc\xeeK\xb3\xe7\xa7\xb4\x7f\x9e6[\x94\x87\xa33h\xd3''t\xe4\xde\xd2\x8f;8\x95\xd01sJ_\xce\xa9\x8c+\xf09\xa72t}\xc9\xa9\xcc\x9d\xc8%\xa7\xd21\xff\xde\xa2\xcc\x9f\xf4\xafjQV3Z\x97x\x80EY\xf6\x0b\x0f\xb1(36#\xfa\xe1\x8b\xb7(!\xe5\xf7\x9e\xbe\x8fE\x99\xd7|\x84E\x99\x9f\xa5\xeb\x16e\xcd*,[\x94\xd5\xbc\x1ffQZ\xd5\xff\xda\xa2x\x96#f\xf9\xc4\x07\x04\x1c\xca\xdc\x7f\xae\xb3\x0ee\xe1\xb1\x9aw(\xf3\x05\xb8\x83C\xf9\x1b\x00\x00\xff\xffPK\x07\x08D\x93\xed^\x8c\x02\x00\x00Y\x12\x00\x00PK\x03\x04\x14\x00\x08\x00\x08\x00\x00\xbe\xbeT\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x17\x00 \x00themes/default-256.tomlUT\x05\x00\x01\xb0W\x95b\xd4\x96\xcfr\xa30\x0c\xc6\xefy\x8a\x0c{-\x1d\xda\xe6\xefa\x9f\xa4\xd3\x831\"\xf1T\x18\xc6\x88vy\xfb\x1dlcH\x02\x8e\xd3\x99f\xb7\xb9E\xe8\xc7'\xc9\xd8\xfe\x16\x8dlj\xc8\x96\xbf\x97\xd1\xaf\xed\x1e\x9e\x9eX\xb4X\xbcf\x90\xb3\x06\xe9m\xb1\\\xa6\xc8\xf8\xbb~\x9c\xe8_\xb4X.\x0f\x8a\xb5\xc9\xb3\x0e\xae6\xab\xed\xfa\xd9\x05_tp\xbdYo7O.\xb8\xd2\xc1\xfdf\xbf\xd9\x0f\x99k\x1d\xe4\xddo\xdb\x05?\x8f\x82@\xc7r\xfd\xebb\xca\x16\x96\xe7\xbdr\xa9\x98<\xf4il\xf5\xcc\xbb`\x0b\x88\xe5\xa7c\xfb\x1a\x01\xa4\xa9\xc6\xc5x\xcb\xa4me\xb75\x12)6\xe0\xba\xb3!%\x0eG\x1a=\xd8\xad\xf2\xb4{P5\xaaB\x13\xdc%I\xb2\xd3\xef,\xd8\x01$1\x13M\x93d\x97\xea\x012\xf5\xae\xa7\xd7\x10\x95\x9d\xe6kdg\xfa\xa8\x07\x1a=\xb8\xfffD\xd1\x90\x1d\xe7%o\xea\x13F\x0fg\xc4X\xd11T\x03\x02'=\xb0yn\xa4\xc5\x8b\x0c\x85\x04oif\xb0\xe3\xf4x\xa2!\x9b\xf5p\xf6\x9a\x13\xaaT\x19\xa8`\xad\xb2j\xe3\xa2\xcc\xc2\x8b\xeb\x81\x98!\x05\xd7\xe6 d)\xa0wn\n2\xc34J\x81\xa4\x98\xb3\x8a\x1a\x05\x93\x0c\x1d\xa1\x80\xfa\xd1\xec+Me\x82ay\x08m\xc6d\xdf8h\x1b\xf161d\xe7\x02 T\x0cEE\xad\xb7\xacn\xc3\x8c ! T\x01\x99`\xe4_\x1c\xb3OO\xd1\x0f\x86\"\xf3R\xfd\x98-R\x80ln\xeb\xe8\xba\x84>\x164r\x84?q\xda\x12\x84\xed\x1c\xb3\xf7O\xc1F\x06\xa1C\x95\x1d\x99\x0b\xc0lZsv\xfa\x037#yN\x9a\x12zT\xaf\xda\x07\xc3\xb0N\xcd9~ \x07vk\xee\x06\x87#kA}I\xd8\x90\xb7\xabV\x8c\xbf\x03\xc5(j\x8a9 \x06\x1c\xa7\xa3\xc5\xbd\xa0\xc3\xe5_.^\xa0\xca\xcf\x00\xf5\xe1\xbe\x99|Ax\x01'\xfd\xd7\xa4\x1aN_\x96\xb7x\xd0\xd76\xee^\x95\x07\x05u\x1d\xf3\xb2\xa8\x10h\xfa\x88\x9c\x9a{\x0f\xdag\x81;\xcaau%\xa4<\xbbb.\xce\xcb\x91^P\xfe\xa0S\x93\x02V\xc4\x1c\x05H\n:\xc3,Q0\xe2\xc7\xd0\xa3\xdf250u\x05\x1a6\xb8c\xd4\xc7Y;\x97S\xb3KL\x82\xf0ta\xba\xa2\x1f\"we-^\xb1\xfb&\xde\xc2\x0c\xccPK\xb0\x7f\x19-\x84\xcf\xbex\xac\xd2\x8f\xb3/\xe7#\xf8\x9a}\x19f\xed\xb3/s\xdf\xa4\xcf\xbe\xf4\xcc\xbf\xf7-\xf3\xdf\xd7O\xf5-W;\xba.q\x07\xdf\xe27\x11w\xf1-s\xde#\xa8\xd5\x1b\x8d\xcb\xa4\xf6\xda\xe1\xdfd\\<\xaa\xf70.\x9e\x13\xf5>\xc6\xe5z\xff\xf76.V\xfc\xbf6.##\x12\x92~\xe1\x0e&|\xcbyiW}\x8b\xe7\x02\x9b\xf7-\xf3\x03\xf8\x0e\xdf\xf27\x00\x00\xff\xffPK\x07\x08\xc2O\xb4\x9e\xc4\x02\x00\x00\xcd\x12\x00\x00PK\x03\x04\x14\x00\x08\x00\x08\x00\x00\xbe\xbeT\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x15\x00 \x00themes/default-8.tomlUT\x05\x00\x01\xb0W\x95b\xec\x96K\x8e\xa30\x10\x86\xf79\x05b\xddn\xf5\xbcW\xb3\x9ac\xb4f\xe1\x98Jb\xa5\xb0Q\xb9\x9c\x0c\xb7\x1fa\x08\x90\x84\x87A\x19u\xb74\xcbT\xea\xe3\xaf\x07\xd8\xff\xc6\x1b\xef K~&\xe9/\x8b\x96^^\xd2\xcd\xe65\x83\x9d\xf4\xc8\xbf7I\xb2E\xa9\x8eW\x7f'\xc9\xf9\xa0\x19\xba\xd8\x8f*F\xfd\x87|\xaa\"% \xdas\x17\xfcR\x05\xf7\x04`\xba\xd8\xe7*\xa6Ji\x92.\xf6=\x0d\xb2\xbe\xa7\xf0\xb5\n\x15\x9e\n\xec\x05\xbf\x85J%\x1dC\x99\x9e\xd9V\x0f~M\x9b\xe2\x9fC\x95\xe9S\xfb;t\x92v\xc9bg\x95w\x93H-\xd9g\x1c (\x0e\xbd\xc6(\xa9O\xf6\x1c!\xden\xf8\x16]\xa7\xec\x98\xbc\xe2\x15\xba\x0d\xb8t\xdad\xf7\x04\xce e\xf3\x02\x81\x87\x0f\xc4\xa1A_\xc0\xe6\xbf\xd8./\x98+\xb417\x17\xca\xdd\xe9\xd8\xd3\x8b\xca\xeft\x1c\x13\xc8\\(\xd4`8\xea\xc4j\x88\\\xb2:\xc4\x1e\x8c\x0d\xe3@\xd2\x0c\xd4M\xbce\xe84s\x9fV6% \xac\x19\xaf\x17S\x15\xfd\x94\xb6\x17\xd4\xe6\x15\xf5\xfeP[\xaa\xfbKg\xbc\x96G{\x95q\xa5\x0f\xe7Un'\xb0\xce\xab\xf4\x060\xe1U\xc6^\xc9)\xafra\xde\xde\xa4\x8c/\xfd\xa3\x9a\x94\xd9\x8e\xe6%\xde\xc0\xa4\\\xdf\xbe\xef\xce\xa3D\xb2\xab\x94'-\x90\xce\xca\xe4\xa6B\xbe\xfe\xb5\xde\xfc\x97\x1cp\xbb\x85\xcdj\xf5\xca50\xa3\xe0m\xb5^\x1f5\xd4[\xfb\xf3.\xde%Q\xb8i\xb5\xd0jQ\x1c%\xf1\xf6\xac\xbdX-\x8d\xd38\xed\xd6\xed\xac\xc6\x9a\xbf\xa4\xd1R\x05\xec\xddj\xe1>\x84\x97\xd8i\x06\xad\x14\x87I\x08\xbbFb5\xe4V\xda\xa7x\x10\xdc=\x0d\xd1i\xd1\xff\x02\x92\xb4\xd128bN`U!\x92\x03\xb3\xcf+4\xe4Gl\xc5t\x1f\xb3F,\x8d.\x95\x13S~x\x11\x87F\xd4\xadu!\xa2(\x8a\x1a\xe5\xf3$\xa9E\xf7b/\xac\x8f\x1a\x95*>\x9d\xb8\x15\xb0gvF\xa0\xdf\x9b\x01\xa5\x86\xa8h\xfaz\xdd\xb4c{\xb6\x1e7O\xdd\xffv6\x9b~q \nf\xaaE\xa456\x84*T\xc8\xc86\xdcs\xb6\xdd\xc9R,\xe3J\xe6\xb8X\xc5\x19\x1b.\x0f&\xec\xb4\xab\x9eF\x8f\xb9\xa0\n\xcdQ{\xd7*\xca:\xc8\n\xee\xdf\xdc\x19\x08@\x91wo\x1d\xa4 E\xb586\x8d\xdc1Fk\xcc)`P\x92\xd18\xc9\xd0 3\xac\x9e\xdd\x87c).A\x15G_3n\xf5\x9d\x83\xe6(\xc0\x8c\xbc\x8fM\xf4\xab\x85T\x84:\xc0\xac\xa4z\xb1\xad\xe63\x1b\x122'\xd4\x19r \xb4\xbc9\xee\x13\xbbD?@I\xbeH\x9d\xc7\xdc\"\x19\xe6\xe6>G\xb7K\xd8S\xc2\"'\xfc\x1d\xa45\xa1\xdf\x87\xe3\x8e\x87K\xd0\xe4^h\xdfeC\n\x89\x8aO\xd7\x9c\x9d~\xcf\xcd\x94\x1c\x93\xae\x853jw\xed\x03\x94\x9fS{z_\xb3\x9ef\xed}\xd0\xd1\nj\xd4_)\xeb\xc0\xbbk\x96\xc0\xde\x91\x02%+\n\x18*5q\x92.l\xec\x15\xed]=\xbc\xe2u\xf1\xe9Q\xbc\xdb\xe11\xea]\xf9\xc2wE\xda0\xf2\xb8>\xc6u[\xd0\xeb\xdd\x1a\xf8\xd5\xc5QcU\x05\xac\xc8J\x854}\x1eN\x0d\xfa\x0c\xdewluXU\xca<\x1f\xdd'W\x87\xe3\xa0\x9e\xd7\xfa\xbeNE\x1a!\x0b\x98\x92\x98\x93\xd7\x81\xd5\x12\x19\x10;\xf9\x9e\xf3-S!\xe8\x1bP\xff5w\x8c\xfe\x18\xd9\xb9\x9e\x9aq\x08IR\x97\x1b\xd34\xfd\xb4\xe9\xee\xa7\xd5\xab\x92\xc7\x13\xbd\xf9e\x95\xbe\x17\xef\xac2\xd8\x88\xa5\xa82\x9f\x8a~\\T\x19o\xc6\xd7\xa2J?\xea\xa5\xa82\xf7J.E\x953\xf3\xf73\xca\xfc\xeb\xf5S3\xcaMG\xb7K< \xa3,\x07\x86\x87d\x94\x99\x9c\xe1\xe5\xf4\xbe\x8c2Uy\xd7\xd1\xdf\x93Q\xe6k>\"\xa3\xcc\x9f\xa5\xdf\x9dQn\xfa~XFi\xab\xfe\xd3\x19e\x909|\x96_\x05\x81\x89\x882n\xedfDY\xb8\xac\xe6#\xca\xfc\x00\xbe#\xa2\xfc \x00\x00\xff\xffPK\x07\x08\xd0\xde\xd9|\xce\x02\x00\x00\x86\x12\x00\x00PK\x03\x04\x14\x00\x08\x00\x08\x00\x00\xbe\xbeT\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x19\x00 \x00themes/solarized-256.tomlUT\x05\x00\x01\xb0W\x95b\xdc\x96\xcb\x8e\x9b0\x14\x86\xf7y\x8a(\xdd\x0eU\x80\x84\xc0\xa2\x9b\xbeF4\x8b\x83}H\xac1\x17\x193S\xfa\xf4\x156\xb7\x84\xc4\x98\xa8\x13u\xba\xfd\xc5\xe7s3\xc7\xff\xaa\xca\xaa\x12\xe9\xfa\xc7z\xf3\xed\x10\xa1\xeb\xc2f\xb5:\x969\x07\xc1~#}]\xad\xd7'\x01\xb5\xab>\xd8\x87\x01\x1e\xf6\x9bV\xf3\x94\x16\xec\x0fq\xe8w\x9a\xaf\xb4\xd0\x8fvQ\xd0i;\xa5E>\xb8\xe06Z\xcc\x81\xbc)m\xbb\xf5b?\xd0Z\x85J\xf2\x820\xa6^#\x91\x1a2-\x01\xb8Q\xa8OC\xd4Z\xb8\x8f\xa2\xed\xb6\xd1R8a&A\xa9\xd4\xf7\x83P\xc1\xb9\x80\xec\xa4O$\xf1.vU\x90\xa2\x12\x05\xd7b@\x0e.\xd95\xa2h\x8b\xa7\xc4\xf7\xbc\xa4Q>\xceL\xea\xaf\x12\x9a\x04\xa8j\xab\x91\xf3\xfcC\x89?\xf7\xa1\n\xbd:R\x10oM\x83\xe2J\xca\xbc\xc9\xeb\xb8\xe9\x1b\xf7]U\xb9y\x19)\xaa?\x9b\x01p\x92\x9cT\xe5\x15\xa6\x82_`m\x81c\xb0D\x8eD\xaa\xc4\xcd\xec\x10\x92\xa4\x94\xb3\x0cg\x93\xd4\x85\x8e\x11\xe7fy\xedw/\x93\xc3.\xc8\\P\x14\x8bb\xe6E\xed\xa49]\x96h\x079\xc0\xe5\xa2<{\x90C\x8c|\xb6\x9d\x02\xa9\xe6*!0\x93\x0e\x81BV\xe2:\xd7\x8e\xd3\x7f\x96\x02(\x03\x9e\x9f\x96\xd4\xa4\x89\x07zO1\x81j\xd2\x86i-\x03\x910.Q8\x98\x16\xb2\x9eM\xb1\xf9+\xc7\x14\xcb$\x8a\x14)\x039?3\xfdW^\xe2\xef\xc0\xd9\xf5E\x9e\x92]\xe7[,\xc5\xacZ^\xa1](\xb5d\x14v\xc6_N\\K\xb4\xff\xdf\xf4\x86\xb9\x84\xab\xcc\x1a\x1f2n\xe8\x84!\xa7\xf7b\x1b&3\xb0wCOi\x9dL\x87\xab\xa9\xbe\x03\xb7\xaf\\=\x08S~A\xf1\xea\x99\xe9O\xe0P\xa3x4\xbc\x86\x1f\x8a]\x00yC\xe9pVJ\x87 \xe7\x96Kz4\xf8\xc9 \x8b\xb2\xf0&g\x88\xfc\xc32\x89\xfe\x06\\\xe3\x8b2\xb8\xe8C)EE\xe4\xcd\xf8\x86\x1bx [\xdf\xc1Q\xfd\"? ,K\x87\xe4i\xc1q\xb2Z\xcc\x03\xe8\xe0\xe5\xab\xb0G\xcb\x82e\xd9\xe4\xe9\xba\xb1xGq\xad\x99!^)\x05B\xea\x10\xce0\x9b\xcf\xb3[\x82-\x95\x82$\xe7\xd9\xa6\x8e\xde\x94\x96+\x11\x84\x058l\x84\x9e\x13\xef\x93\xf2nu\xb3\xd2\x98d\x92_\x0f\xae)a\xf42\xae\x8e\x9c\x9d\xce\xf2\xd5\xdeD\x0dY-2Q\xa31\x99=\x94\xc9\xb6}q\x0f5\xed\xca\xe3\x1ej\x18\x83\xd9C\xdd\x7f\xc9\xcd\x1e\xaa\xe3\xfe%\x0fe\xba\x8c\xff\x87\x87\xb2\xa8\xd0.\xd4\xd3<\xd4\x9c\x91y\xaa\x87\xba\xe3\x81\xac+_\xea\xa1ng\xb0\xebO\xf8L\x0fe\x8a\xfd<\x0fe\xda\xd6\xcf\xf1P\x16}x\xba\x87j\xa3\x7f\x19\x0f5\xf2C\xb6\xc8\xc4\x9c\xfc=\x0be|&M\x16\xca\xd4\x94O\xb4P\x7f\x02\x00\x00\xff\xffPK\x07\x08>6\xdf\xd3\xd2\x02\x00\x00\xc2\x13\x00\x00PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00}\xac\x8aT0\xf3\x8fG5\x00\x00\x00/\x00\x00\x00\x06\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xb4\x81\x00\x00\x00\x00gen.goUT\x05\x00\x01\xbfMSbPK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00\x00\xbe\xbeT\x19\xcc\xe2\xfa\xb1\x02\x00\x00#\x12\x00\x00\x16\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xb4\x81r\x00\x00\x00themes/base16-256.tomlUT\x05\x00\x01\xb0W\x95bPK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00\x00\xbe\xbeTD\x93\xed^\x8c\x02\x00\x00Y\x12\x00\x00\x16\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xb4\x81p\x03\x00\x00themes/default-16.tomlUT\x05\x00\x01\xb0W\x95bPK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00\x00\xbe\xbeT\xc2O\xb4\x9e\xc4\x02\x00\x00\xcd\x12\x00\x00\x17\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xb4\x81I\x06\x00\x00themes/default-256.tomlUT\x05\x00\x01\xb0W\x95bPK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00\x00\xbe\xbeT\xce\xd7VmH\x02\x00\x00\x0c\x12\x00\x00\x15\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xb4\x81[ \x00\x00themes/default-8.tomlUT\x05\x00\x01\xb0W\x95bPK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00\x00\xbe\xbeT\xd0\xde\xd9|\xce\x02\x00\x00\x86\x12\x00\x00\x17\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xb4\x81\xef\x0b\x00\x00themes/dracula-256.tomlUT\x05\x00\x01\xb0W\x95bPK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00\x00\xbe\xbeT>6\xdf\xd3\xd2\x02\x00\x00\xc2\x13\x00\x00\x19\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xb4\x81\x0b\x0f\x00\x00themes/solarized-256.tomlUT\x05\x00\x01\xb0W\x95bPK\x05\x06\x00\x00\x00\x00\x07\x00\x07\x00\x0f\x02\x00\x00-\x12\x00\x00\x00\x00" fs.Register(data) } termshark-2.4.0/assets/themes/000077500000000000000000000000001426312004500163015ustar00rootroot00000000000000termshark-2.4.0/assets/themes/base16-256.toml000066400000000000000000000110431426312004500205700ustar00rootroot00000000000000 unused = "#79e11a" [base16] black = "Color00" base01 = "Color12" base02 = "Color13" base03 = "Color08" base04 = "Color14" base05 = "Color07" base06 = "Color15" white = "Color0f" red = "Color09" orange = "Color10" yellow = "Color0b" green = "Color0a" cyan = "Color0e" blue = "Color04" purple = "Color05" brown = "Color11" [dark] button = ["base16.black","base16.base04"] button-focus = ["base16.white","base16.purple"] button-selected = ["base16.white","base16.base04"] cmdline = ["base16.black","base16.yellow"] cmdline-button = ["base16.yellow","base16.black"] cmdline-border = ["base16.black","base16.yellow"] copy-mode = ["base16.black","base16.yellow"] copy-mode-alt = ["base16.yellow","base16.black"] copy-mode-label = ["base16.white","base16.red"] current-capture = ["base16.white","themes.unused"] dialog = ["base16.black","base16.yellow"] dialog-button = ["base16.yellow","base16.black"] default = ["base16.white","base16.black"] filter-empty = ["base16.black","base16.cyan"] filter-intermediate = ["base16.black","base16.orange"] filter-invalid = ["base16.black","base16.red"] filter-menu = ["base16.white","base16.black"] filter-valid = ["base16.black","base16.green"] hex-byte-selected = ["base16.white","base16.purple"] hex-byte-unselected = ["base16.white","base16.black"] hex-field-selected = ["base16.black","base16.cyan"] hex-field-unselected = ["base16.black","base16.white"] hex-interval-selected = ["base16.white","base16.base03"] hex-interval-unselected = ["base16.white","base16.base02"] hex-layer-selected = ["base16.white","base16.base03"] hex-layer-unselected = ["base16.white","base16.base02"] packet-list-cell-focus = ["base16.white","base16.purple"] packet-list-cell-selected = ["base16.white","base16.base03"] packet-list-row-focus = ["base16.white","base16.cyan"] packet-list-row-selected = ["base16.white","base16.base02"] packet-struct-focus = ["base16.white","base16.cyan"] packet-struct-selected = ["base16.black","base16.base03"] progress-complete = ["base16.white","base16.purple"] progress-default = ["base16.white","base16.black"] progress-spinner = ["base16.yellow","base16.purple"] spinner = ["base16.yellow","base16.black"] stream-client = ["base16.black","base16.red"] stream-match = ["base16.black","base16.yellow"] stream-search = ["base16.black","base16.white"] stream-server = ["base16.black","base16.blue"] title = ["base16.red","unused"] [light] button = ["base16.black","base16.white"] button-focus = ["base16.black","base16.purple"] button-selected = ["base16.black","base16.base04"] cmdline = ["base16.black","base16.yellow"] cmdline-button = ["base16.yellow","base16.black"] cmdline-border = ["base16.black","base16.yellow"] copy-mode = ["base16.white","base16.yellow"] copy-mode-alt = ["base16.yellow","base16.white"] copy-mode-label = ["base16.black","base16.red"] current-capture = ["base16.black","unused"] dialog = ["base16.black","base16.yellow"] dialog-button = ["base16.yellow","base16.black"] default = ["base16.black","base16.white"] filter-empty = ["base16.black","base16.cyan"] filter-intermediate = ["base16.black","base16.orange"] filter-invalid = ["base16.black","base16.red"] filter-menu = ["base16.black","base16.white"] filter-valid = ["base16.black","base16.green"] hex-byte-selected = ["base16.white","base16.purple"] hex-byte-unselected = ["base16.black","base16.white"] hex-field-selected = ["base16.black","base16.cyan"] hex-field-unselected = ["base16.black","base16.base03"] hex-interval-selected = ["base16.white","base16.base03"] hex-interval-unselected = ["base16.black","base16.base05"] hex-layer-selected = ["base16.white","base16.base03"] hex-layer-unselected = ["base16.black","base16.base05"] packet-list-cell-focus = ["base16.white","base16.purple"] packet-list-cell-selected = ["base16.black","base16.base04"] packet-list-row-focus = ["base16.white","base16.cyan"] packet-list-row-selected = ["base16.black","base16.base05"] packet-struct-focus = ["base16.white","base16.cyan"] packet-struct-selected = ["base16.black","base16.base05"] progress-complete = ["base16.white","base16.purple"] progress-default = ["base16.white","base16.black"] progress-spinner = ["base16.yellow","base16.black"] spinner = ["base16.yellow","base16.white"] stream-client = ["base16.white","base16.red"] stream-match = ["base16.white","base16.yellow"] stream-search = ["base16.white","base16.black"] stream-server = ["base16.white","base16.blue"] title = ["base16.red","unused"] termshark-2.4.0/assets/themes/default-16.toml000066400000000000000000000111311426312004500210430ustar00rootroot00000000000000 unused = "Color00" [default] black = "Color00" gray1 = "Color08" gray2 = "Color08" gray3 = "Color08" gray4 = "Color08" white = "Color0f" red = "Color09" yellow = "Color0b" green = "Color0a" cyan = "Color0e" blue = "Color04" purple = "Color05" [dark] button = ["default.black","default.gray1"] button-focus = ["default.white","default.purple"] button-selected = ["default.white","default.black"] cmdline = ["default.black","default.yellow"] cmdline-button = ["default.yellow","default.black"] cmdline-border = ["default.black","default.yellow"] copy-mode = ["default.black","default.yellow"] copy-mode-alt = ["default.yellow","default.black"] copy-mode-label = ["default.white","default.red"] current-capture = ["default.white","themes.unused"] dialog = ["default.black","default.yellow"] dialog-button = ["default.yellow","default.black"] default = ["default.white","default.black"] filter-empty = ["default.black","default.cyan"] filter-intermediate = ["default.black","default.yellow"] filter-invalid = ["default.black","default.red"] filter-menu = ["default.white","default.black"] filter-valid = ["default.black","default.green"] hex-byte-selected = ["default.black","default.purple"] hex-byte-unselected = ["default.white","default.black"] hex-field-selected = ["default.black","default.cyan"] hex-field-unselected = ["default.black","default.white"] hex-interval-selected = ["default.white","default.gray2"] hex-interval-unselected = ["default.white","default.gray1"] hex-layer-selected = ["default.white","default.gray2"] hex-layer-unselected = ["default.white","default.gray1"] packet-list-cell-focus = ["default.black","default.purple"] packet-list-cell-selected = ["default.white","default.gray2"] packet-list-row-focus = ["default.gray2","default.cyan"] packet-list-row-selected = ["default.white","default.gray1"] packet-struct-focus = ["default.black","default.cyan"] packet-struct-selected = ["default.black","default.gray2"] progress-complete = ["default.white","default.purple"] progress-default = ["default.white","default.black"] progress-spinner = ["default.yellow","default.purple"] spinner = ["default.yellow","default.black"] stream-client = ["default.black","default.red"] stream-match = ["default.black","default.yellow"] stream-search = ["default.black","default.white"] stream-server = ["default.cyan","default.blue"] title = ["default.red","unused"] [light] button = ["default.white","default.gray3"] button-focus = ["default.white","default.purple"] button-selected = ["default.black","default.gray3"] cmdline = ["default.black","default.yellow"] cmdline-button = ["default.yellow","default.black"] cmdline-border = ["default.black","default.yellow"] copy-mode = ["default.white","default.yellow"] copy-mode-alt = ["default.yellow","default.white"] copy-mode-label = ["default.black","default.red"] current-capture = ["default.black","unused"] dialog = ["default.black","default.yellow"] dialog-button = ["default.yellow","default.black"] default = ["default.black","default.white"] filter-empty = ["default.black","default.cyan"] filter-intermediate = ["default.black","default.yellow"] filter-invalid = ["default.black","default.red"] filter-menu = ["default.black","default.white"] filter-valid = ["default.black","default.green"] hex-byte-selected = ["default.black","default.purple"] hex-byte-unselected = ["default.black","default.white"] hex-field-selected = ["default.black","default.cyan"] hex-field-unselected = ["default.black","default.gray2"] hex-interval-selected = ["default.white","default.gray2"] hex-interval-unselected = ["default.black","default.gray4"] hex-layer-selected = ["default.white","default.gray2"] hex-layer-unselected = ["default.black","default.gray4"] packet-list-cell-focus = ["default.black","default.purple"] packet-list-cell-selected = ["default.black","default.gray3"] packet-list-row-focus = ["default.black","default.cyan"] packet-list-row-selected = ["default.black","default.gray4"] packet-struct-focus = ["default.black","default.cyan"] packet-struct-selected = ["default.black","default.gray4"] progress-complete = ["default.white","default.purple"] progress-default = ["default.white","default.black"] progress-spinner = ["default.yellow","default.black"] spinner = ["default.yellow","default.white"] stream-client = ["default.white","default.red"] stream-match = ["default.white","default.yellow"] stream-search = ["default.white","default.black"] stream-server = ["default.cyan","default.blue"] title = ["default.red","unused"] termshark-2.4.0/assets/themes/default-256.toml000066400000000000000000000113151426312004500211350ustar00rootroot00000000000000 unused = "#79e11a" [default] black = "#000000" gray02 = "#464752" gray03 = "#565761" gray04 = "#969692" gray05 = "#ccccc7" white = "#ffffff" red = "#ff0000" orange = "#ffa42c" yellow = "#ffff00" green = "#5fff00" cyan = "#0087ff" blue = "#0000ff" brightblue = "#0084fb" purple = "#800080" magenta = "#8b008b" [dark] button = ["default.black","default.gray04"] button-focus = ["default.white","default.magenta"] button-selected = ["default.white","default.gray04"] cmdline = ["default.black","default.yellow"] cmdline-button = ["default.yellow","default.black"] cmdline-border = ["default.black","default.yellow"] copy-mode = ["default.black","default.yellow"] copy-mode-alt = ["default.yellow","default.black"] copy-mode-label = ["default.white","default.red"] current-capture = ["default.white","themes.unused"] dialog = ["default.black","default.yellow"] dialog-button = ["default.yellow","default.black"] default = ["default.white","default.black"] filter-empty = ["default.black","default.cyan"] filter-intermediate = ["default.black","default.orange"] filter-invalid = ["default.black","default.red"] filter-menu = ["default.white","default.black"] filter-valid = ["default.black","default.green"] hex-byte-selected = ["default.white","default.purple"] hex-byte-unselected = ["default.white","default.black"] hex-field-selected = ["default.black","default.cyan"] hex-field-unselected = ["default.black","default.white"] hex-interval-selected = ["default.white","default.gray03"] hex-interval-unselected = ["default.white","default.gray02"] hex-layer-selected = ["default.white","default.gray03"] hex-layer-unselected = ["default.white","default.gray02"] packet-list-cell-focus = ["default.white","default.purple"] packet-list-cell-selected = ["default.white","default.gray03"] packet-list-row-focus = ["default.white","default.brightblue"] packet-list-row-selected = ["default.white","default.gray02"] packet-struct-focus = ["default.white","default.brightblue"] packet-struct-selected = ["default.black","default.gray03"] progress-complete = ["default.white","default.purple"] progress-default = ["default.white","default.black"] progress-spinner = ["default.yellow","default.purple"] spinner = ["default.yellow","default.black"] stream-client = ["default.black","default.red"] stream-match = ["default.black","default.yellow"] stream-search = ["default.black","default.white"] stream-server = ["default.white","default.blue"] title = ["default.red","unused"] [light] button = ["default.black","default.white"] button-focus = ["default.white","default.purple"] button-selected = ["default.black","default.gray04"] cmdline = ["default.black","default.yellow"] cmdline-button = ["default.yellow","default.black"] cmdline-border = ["default.black","default.yellow"] copy-mode = ["default.white","default.yellow"] copy-mode-alt = ["default.yellow","default.white"] copy-mode-label = ["default.black","default.red"] current-capture = ["default.black","unused"] dialog = ["default.black","default.yellow"] dialog-button = ["default.yellow","default.black"] default = ["default.black","default.white"] filter-empty = ["default.black","default.cyan"] filter-intermediate = ["default.black","default.orange"] filter-invalid = ["default.black","default.red"] filter-menu = ["default.black","default.white"] filter-valid = ["default.black","default.green"] hex-byte-selected = ["default.white","default.purple"] hex-byte-unselected = ["default.black","default.white"] hex-field-selected = ["default.black","default.cyan"] hex-field-unselected = ["default.black","default.gray03"] hex-interval-selected = ["default.white","default.gray03"] hex-interval-unselected = ["default.black","default.gray05"] hex-layer-selected = ["default.white","default.gray03"] hex-layer-unselected = ["default.black","default.gray05"] packet-list-cell-focus = ["default.white","default.purple"] packet-list-cell-selected = ["default.black","default.gray04"] packet-list-row-focus = ["default.white","default.brightblue"] packet-list-row-selected = ["default.black","default.gray05"] packet-struct-focus = ["default.white","default.brightblue"] packet-struct-selected = ["default.black","default.gray05"] progress-complete = ["default.white","default.purple"] progress-default = ["default.white","default.black"] progress-spinner = ["default.yellow","default.black"] spinner = ["default.yellow","default.white"] stream-client = ["default.white","default.red"] stream-match = ["default.white","default.yellow"] stream-search = ["default.white","default.black"] stream-server = ["default.white","default.blue"] title = ["default.red","unused"] termshark-2.4.0/assets/themes/default-8.toml000066400000000000000000000110141426312004500207640ustar00rootroot00000000000000 unused = "Color00" [default] black = "Color00" white = "Color07" red = "Color01" yellow = "Color03" green = "Color02" cyan = "Color06" blue = "Color04" purple = "Color05" [dark] button = ["default.white","default.black"] button-focus = ["default.white","default.purple"] button-selected = ["default.white","default.black"] cmdline = ["default.black","default.yellow"] cmdline-button = ["default.yellow","default.black"] cmdline-border = ["default.black","default.yellow"] copy-mode = ["default.black","default.yellow"] copy-mode-alt = ["default.yellow","default.black"] copy-mode-label = ["default.white","default.red"] current-capture = ["default.white","themes.unused"] dialog = ["default.black","default.yellow"] dialog-button = ["default.yellow","default.black"] default = ["default.white","default.black"] filter-empty = ["default.black","default.cyan"] filter-intermediate = ["default.black","default.yellow"] filter-invalid = ["default.black","default.red"] filter-menu = ["default.white","default.black"] filter-valid = ["default.black","default.green"] hex-byte-selected = ["default.black","default.purple"] hex-byte-unselected = ["default.black","default.purple"] hex-field-selected = ["default.black","default.cyan"] hex-field-unselected = ["default.white","default.black"] hex-interval-selected = ["default.black","default.white"] hex-interval-unselected = ["default.black","default.white"] hex-layer-selected = ["default.black","default.white"] hex-layer-unselected = ["default.black","default.white"] packet-list-cell-focus = ["default.black","default.purple"] packet-list-cell-selected = ["default.white","default.black"] packet-list-row-focus = ["default.black","default.cyan"] packet-list-row-selected = ["default.white","default.black"] packet-struct-focus = ["default.black","default.cyan"] packet-struct-selected = ["default.black","default.white"] progress-complete = ["default.white","default.purple"] progress-default = ["default.white","default.black"] progress-spinner = ["default.yellow","default.purple"] spinner = ["default.yellow","default.black"] stream-client = ["default.black","default.red"] stream-match = ["default.black","default.yellow"] stream-search = ["default.black","default.white"] stream-server = ["default.black","default.blue"] title = ["default.red","unused"] [light] button = ["default.black","default.white"] button-focus = ["default.white","default.purple"] button-selected = ["default.black","default.white"] cmdline = ["default.black","default.yellow"] cmdline-button = ["default.yellow","default.black"] cmdline-border = ["default.black","default.yellow"] copy-mode = ["default.white","default.yellow"] copy-mode-alt = ["default.yellow","default.white"] copy-mode-label = ["default.black","default.red"] current-capture = ["default.black","unused"] dialog = ["default.black","default.yellow"] dialog-button = ["default.yellow","default.black"] default = ["default.black","default.white"] filter-empty = ["default.black","default.cyan"] filter-intermediate = ["default.black","default.yellow"] filter-invalid = ["default.black","default.red"] filter-menu = ["default.black","default.white"] filter-valid = ["default.black","default.green"] hex-byte-selected = ["default.black","default.purple"] hex-byte-unselected = ["default.black","default.white"] hex-field-selected = ["default.black","default.cyan"] hex-field-unselected = ["default.white","default.black"] hex-interval-selected = ["default.white","default.black"] hex-interval-unselected = ["default.white","default.black"] hex-layer-selected = ["default.white","default.black"] hex-layer-unselected = ["default.white","default.black"] packet-list-cell-focus = ["default.black","default.purple"] packet-list-cell-selected = ["default.white","default.black"] packet-list-row-focus = ["default.black","default.cyan"] packet-list-row-selected = ["default.white","default.black"] packet-struct-focus = ["default.black","default.cyan"] packet-struct-selected = ["default.white","default.black"] progress-complete = ["default.white","default.purple"] progress-default = ["default.white","default.black"] progress-spinner = ["default.yellow","default.black"] spinner = ["default.yellow","default.white"] stream-client = ["default.black","default.red"] stream-match = ["default.white","default.yellow"] stream-search = ["default.white","default.black"] stream-server = ["default.black","default.blue"] title = ["default.red","unused"] termshark-2.4.0/assets/themes/dracula-256.toml000066400000000000000000000112061426312004500211230ustar00rootroot00000000000000 unused = "#79e11a" [dracula] gray1 = "#464752" gray2 = "#565761" gray3 = "#b6b6b2" gray4 = "#ccccc7" black = "#282a36" blue = "#6272a4" cyan = "#8be9fd" green = "#50fa7b" magenta = "#ff79c6" orange = "#ffb86c" purple = "#bd93f9" red = "#ff5555" white = "#f8f8f2" yellow = "#f1fa8c" [dark] button = ["dracula.black","dracula.gray3"] button-focus = ["dracula.black","dracula.magenta"] button-selected = ["dracula.white","dracula.gray3"] cmdline = ["dracula.black","dracula.yellow"] cmdline-button = ["dracula.yellow","dracula.black"] cmdline-border = ["dracula.black","dracula.yellow"] copy-mode = ["dracula.black","dracula.yellow"] copy-mode-alt = ["dracula.yellow","dracula.black"] copy-mode-label = ["dracula.white","dracula.red"] current-capture = ["dracula.white","themes.unused"] dialog = ["dracula.black","dracula.yellow"] dialog-button = ["dracula.yellow","dracula.black"] default = ["dracula.white","dracula.black"] filter-empty = ["dracula.black","dracula.cyan"] filter-intermediate = ["dracula.black","dracula.orange"] filter-invalid = ["dracula.black","dracula.red"] filter-menu = ["dracula.white","dracula.black"] filter-valid = ["dracula.black","dracula.green"] hex-byte-selected = ["dracula.white","dracula.purple"] hex-byte-unselected = ["dracula.white","dracula.black"] hex-field-selected = ["dracula.black","dracula.cyan"] hex-field-unselected = ["dracula.black","dracula.white"] hex-interval-selected = ["dracula.white","dracula.gray2"] hex-interval-unselected = ["dracula.white","dracula.gray1"] hex-layer-selected = ["dracula.white","dracula.gray2"] hex-layer-unselected = ["dracula.white","dracula.gray1"] packet-list-cell-focus = ["dracula.white","dracula.purple"] packet-list-cell-selected = ["dracula.white","dracula.gray2"] packet-list-row-focus = ["dracula.white","dracula.cyan"] packet-list-row-selected = ["dracula.white","dracula.gray1"] packet-struct-focus = ["dracula.black","dracula.cyan"] packet-struct-selected = ["dracula.black","dracula.gray2"] progress-complete = ["dracula.white","dracula.purple"] progress-default = ["dracula.white","dracula.black"] progress-spinner = ["dracula.yellow","dracula.purple"] spinner = ["dracula.yellow","dracula.black"] stream-client = ["dracula.black","dracula.red"] stream-match = ["dracula.black","dracula.yellow"] stream-search = ["dracula.black","dracula.white"] stream-server = ["dracula.white","dracula.blue"] title = ["dracula.red","unused"] [light] button = ["dracula.black","dracula.white"] button-focus = ["dracula.black","dracula.purple"] button-selected = ["dracula.black","dracula.gray3"] cmdline = ["dracula.black","dracula.yellow"] cmdline-button = ["dracula.yellow","dracula.black"] cmdline-border = ["dracula.black","dracula.yellow"] copy-mode = ["dracula.white","dracula.yellow"] copy-mode-alt = ["dracula.yellow","dracula.white"] copy-mode-label = ["dracula.black","dracula.red"] current-capture = ["dracula.black","unused"] dialog = ["dracula.black","dracula.yellow"] dialog-button = ["dracula.yellow","dracula.black"] default = ["dracula.black","dracula.white"] filter-empty = ["dracula.black","dracula.cyan"] filter-intermediate = ["dracula.black","dracula.orange"] filter-invalid = ["dracula.black","dracula.red"] filter-menu = ["dracula.black","dracula.white"] filter-valid = ["dracula.black","dracula.green"] hex-byte-selected = ["dracula.white","dracula.purple"] hex-byte-unselected = ["dracula.black","dracula.white"] hex-field-selected = ["dracula.black","dracula.cyan"] hex-field-unselected = ["dracula.black","dracula.gray2"] hex-interval-selected = ["dracula.white","dracula.gray2"] hex-interval-unselected = ["dracula.black","dracula.gray4"] hex-layer-selected = ["dracula.white","dracula.gray2"] hex-layer-unselected = ["dracula.black","dracula.gray4"] packet-list-cell-focus = ["dracula.white","dracula.purple"] packet-list-cell-selected = ["dracula.black","dracula.gray3"] packet-list-row-focus = ["dracula.white","dracula.cyan"] packet-list-row-selected = ["dracula.black","dracula.gray4"] packet-struct-focus = ["dracula.black","dracula.cyan"] packet-struct-selected = ["dracula.black","dracula.gray4"] progress-complete = ["dracula.white","dracula.purple"] progress-default = ["dracula.white","dracula.black"] progress-spinner = ["dracula.yellow","dracula.black"] spinner = ["dracula.yellow","dracula.white"] stream-client = ["dracula.white","dracula.red"] stream-match = ["dracula.white","dracula.yellow"] stream-search = ["dracula.white","dracula.black"] stream-server = ["dracula.white","dracula.blue"] title = ["dracula.red","unused"] termshark-2.4.0/assets/themes/solarized-256.toml000066400000000000000000000117021426312004500215050ustar00rootroot00000000000000 unused = "#79e11a" [solarized] gray1 = "#586e75" gray2 = "#657b83" gray3 = "#839496" gray4 = "#93a1a1" black = "#002b36" blue = "#268bd2" cyan = "#2aa198" green = "#859900" magenta = "#d33682" orange = "#cb4b16" purple = "#6c71c4" red = "#dc322f" white = "#fdf6e3" yellow = "#B58900" [dark] button = ["solarized.black","solarized.gray3"] button-focus = ["solarized.white","solarized.magenta"] button-selected = ["solarized.white","solarized.gray3"] cmdline = ["solarized.black","solarized.yellow"] cmdline-button = ["solarized.yellow","solarized.black"] cmdline-border = ["solarized.black","solarized.yellow"] copy-mode = ["solarized.black","solarized.yellow"] copy-mode-alt = ["solarized.yellow","solarized.black"] copy-mode-label = ["solarized.white","solarized.red"] current-capture = ["solarized.white","unused"] dialog = ["solarized.black","solarized.yellow"] dialog-button = ["solarized.yellow","solarized.black"] default = ["solarized.white","solarized.black"] filter-empty = ["solarized.black","solarized.cyan"] filter-intermediate = ["solarized.black","solarized.orange"] filter-invalid = ["solarized.black","solarized.red"] filter-menu = ["solarized.white","solarized.black"] filter-valid = ["solarized.black","solarized.green"] hex-byte-selected = ["solarized.white","solarized.purple"] hex-byte-unselected = ["solarized.white","solarized.black"] hex-field-selected = ["solarized.black","solarized.cyan"] hex-field-unselected = ["solarized.black","solarized.white"] hex-interval-selected = ["solarized.white","solarized.gray2"] hex-interval-unselected = ["solarized.white","solarized.gray1"] hex-layer-selected = ["solarized.white","solarized.gray2"] hex-layer-unselected = ["solarized.white","solarized.gray1"] packet-list-cell-focus = ["solarized.white","solarized.purple"] packet-list-cell-selected = ["solarized.white","solarized.gray2"] packet-list-row-focus = ["solarized.white","solarized.cyan"] packet-list-row-selected = ["solarized.white","solarized.gray1"] packet-struct-focus = ["solarized.black","solarized.cyan"] packet-struct-selected = ["solarized.black","solarized.gray2"] progress-complete = ["solarized.white","solarized.purple"] progress-default = ["solarized.white","solarized.black"] progress-spinner = ["solarized.yellow","solarized.purple"] spinner = ["solarized.yellow","solarized.black"] stream-client = ["solarized.white","solarized.red"] stream-match = ["solarized.black","solarized.yellow"] stream-search = ["solarized.black","solarized.white"] stream-server = ["solarized.white","solarized.blue"] title = ["solarized.red","unused"] [light] button = ["solarized.black","solarized.white"] button-focus = ["solarized.white","solarized.purple"] button-selected = ["solarized.black","solarized.gray3"] cmdline = ["solarized.black","solarized.yellow"] cmdline-button = ["solarized.yellow","solarized.black"] cmdline-border = ["solarized.black","solarized.yellow"] copy-mode = ["solarized.white","solarized.yellow"] copy-mode-alt = ["solarized.yellow","solarized.white"] copy-mode-label = ["solarized.black","solarized.red"] current-capture = ["solarized.black","unused"] dialog = ["solarized.black","solarized.yellow"] dialog-button = ["solarized.yellow","solarized.black"] default = ["solarized.black","solarized.white"] filter-empty = ["solarized.black","solarized.cyan"] filter-intermediate = ["solarized.black","solarized.orange"] filter-invalid = ["solarized.black","solarized.red"] filter-menu = ["solarized.black","solarized.white"] filter-valid = ["solarized.black","solarized.green"] hex-byte-selected = ["solarized.white","solarized.purple"] hex-byte-unselected = ["solarized.black","solarized.white"] hex-field-selected = ["solarized.black","solarized.cyan"] hex-field-unselected = ["solarized.black","solarized.gray2"] hex-interval-selected = ["solarized.white","solarized.gray2"] hex-interval-unselected = ["solarized.black","solarized.gray4"] hex-layer-selected = ["solarized.white","solarized.gray2"] hex-layer-unselected = ["solarized.black","solarized.gray4"] packet-list-cell-focus = ["solarized.white","solarized.purple"] packet-list-cell-selected = ["solarized.black","solarized.gray3"] packet-list-row-focus = ["solarized.white","solarized.cyan"] packet-list-row-selected = ["solarized.black","solarized.gray4"] packet-struct-focus = ["solarized.black","solarized.cyan"] packet-struct-selected = ["solarized.black","solarized.gray4"] progress-complete = ["solarized.white","solarized.purple"] progress-default = ["solarized.white","solarized.black"] progress-spinner = ["solarized.yellow","solarized.black"] spinner = ["solarized.yellow","solarized.white"] stream-client = ["solarized.white","solarized.red"] stream-match = ["solarized.white","solarized.yellow"] stream-search = ["solarized.white","solarized.black"] stream-server = ["solarized.white","solarized.blue"] title = ["solarized.red","unused"] termshark-2.4.0/cmd/000077500000000000000000000000001426312004500142555ustar00rootroot00000000000000termshark-2.4.0/cmd/termshark/000077500000000000000000000000001426312004500162555ustar00rootroot00000000000000termshark-2.4.0/cmd/termshark/termshark.go000066400000000000000000001356401426312004500206150ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. package main import ( "bytes" "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/configs/profiles" "github.com/gcla/termshark/v2/pkg/capinfo" "github.com/gcla/termshark/v2/pkg/cli" "github.com/gcla/termshark/v2/pkg/confwatcher" "github.com/gcla/termshark/v2/pkg/convs" "github.com/gcla/termshark/v2/pkg/fields" "github.com/gcla/termshark/v2/pkg/pcap" "github.com/gcla/termshark/v2/pkg/shark" "github.com/gcla/termshark/v2/pkg/streams" "github.com/gcla/termshark/v2/pkg/summary" "github.com/gcla/termshark/v2/pkg/system" "github.com/gcla/termshark/v2/pkg/tailfile" "github.com/gcla/termshark/v2/pkg/tty" "github.com/gcla/termshark/v2/ui" "github.com/gcla/termshark/v2/widgets/filter" "github.com/gcla/termshark/v2/widgets/wormhole" "github.com/gdamore/tcell/v2" flags "github.com/jessevdk/go-flags" "github.com/mattn/go-isatty" "github.com/shibukawa/configdir" log "github.com/sirupsen/logrus" "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 pcap.Goroutinewg = &ensureGoroutinesStopWG streams.Goroutinewg = &ensureGoroutinesStopWG capinfo.Goroutinewg = &ensureGoroutinesStopWG convs.Goroutinewg = &ensureGoroutinesStopWG ui.Goroutinewg = &ensureGoroutinesStopWG wormhole.Goroutinewg = &ensureGoroutinesStopWG summary.Goroutinewg = &ensureGoroutinesStopWG confwatcher.Goroutinewg = &ensureGoroutinesStopWG res := cmain() ensureGoroutinesStopWG.Wait() if !termshark.ShouldSwitchTerminal && !termshark.ShouldSwitchBack { os.Exit(res) } os.Clearenv() for _, e := range termshark.OriginalEnv { ks := strings.SplitN(e, "=", 2) if len(ks) == 2 { os.Setenv(ks[0], ks[1]) } } exe, err := os.Executable() if err != nil { log.Warnf("Unexpected error determining termshark executable: %v", err) os.Exit(1) } switch { case termshark.ShouldSwitchTerminal: os.Setenv("TERMSHARK_ORIGINAL_TERM", os.Getenv("TERM")) os.Setenv("TERM", fmt.Sprintf("%s-256color", os.Getenv("TERM"))) case termshark.ShouldSwitchBack: os.Setenv("TERM", os.Getenv("TERMSHARK_ORIGINAL_TERM")) os.Setenv("TERMSHARK_ORIGINAL_TERM", "") } // Need exec because we really need to re-initialize everything, including have // all init() functions be called again err = syscall.Exec(exe, os.Args, os.Environ()) if err != nil { log.Warnf("Unexpected error exec-ing termshark %s: %v", exe, err) res = 1 } 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 // Preserve in case we need to re-exec e.g. if the user switches TERM termshark.OriginalEnv = os.Environ() 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) stdConf := configdir.New("", "termshark") dirs := stdConf.QueryFolders(configdir.Cache) if err := dirs[0].CreateParentDir("dummy"); err != nil { fmt.Fprintf(os.Stderr, "Warning: could not create cache dir: %v\n", err) } dirs = stdConf.QueryFolders(configdir.Global) if err := dirs[0].CreateParentDir("dummy"); err != nil { fmt.Fprintf(os.Stderr, "Warning: could not create config dir: %v\n", err) } else { if err = os.MkdirAll(filepath.Join(dirs[0].Path, "profiles"), 0755); err != nil { fmt.Fprintf(os.Stderr, "Warning: could not create profiles dir: %v\n", err) } } err := profiles.ReadDefaultConfig(dirs[0].Path) if err != nil { fmt.Fprintf(os.Stderr, fmt.Sprintf("%s\n", err.Error())) } // 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 } } // On Windows, termshark itself is used to tail the pcap generated by dumpcap, and the output // is fed into tshark -T psml ... if tsopts.TailFileValue() != "" { err = tailfile.Tail(tsopts.TailFileValue()) if err != nil { fmt.Fprintf(os.Stderr, "Error: %v", err) return 1 } else { return 0 } } // From here, the current profile is referenced. So load it up prior to first use. If the user // provides a non-existent profile name, it should be an error, just as for Wireshark. if tsopts.Profile != "" { if err = profiles.Use(tsopts.Profile); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) return 1 } } // If this variable is set, it's set by termshark internally, and termshark is guaranteed to // construct a valid command-line invocation. So it doesn't matter if I do this after the CLI // parsing logic because there's no risk of an error causing a short-circuit and this command // not being run. The reason to do it after the CLI parsing logic is so that I have the correct // config profile loaded, needed for the tshark command. if os.Getenv("TERMSHARK_CAPTURE_MODE") == "1" { err = system.DumpcapExt(termshark.DumpcapBin(), termshark.TSharkBin(), os.Args[1:]...) if err != nil { return 1 } else { return 0 } } // 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" } // 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 := profiles.ConfString("main.term", "") if termVar != "" { fmt.Fprintf(os.Stderr, "Configuration file overrides TERM setting, using TERM=%s\n", termVar) os.Setenv("TERM", termVar) } var psrcs []pcap.IPacketSource defer func() { for _, psrc := range psrcs { if psrc != nil { if remover, ok := psrc.(pcap.ISourceRemover); ok { remover.Remove() } } } }() pcapf := string(opts.Pcap) // If no interface specified, and no pcap specified via -r, then we assume the first // argument is a pcap file e.g. termshark foo.pcap if pcapf == "" && len(opts.Ifaces) == 0 { pcapf = string(opts.Args.FilterOrPcap) // `termshark` => `termshark -i 1` (livecapture on default interface if no args) if pcapf == "" { if termshark.IsTerminal(os.Stdin.Fd()) { pfile, err := system.PickFile() switch err { case nil: // We're on termux/android, and we were given a file. Note that termux // makes a copy, so we ought to clean that up when termshark terminates. psrcs = append(psrcs, pcap.TemporaryFileSource{pcap.FileSource{Filename: pfile}}) case system.NoPicker: // We're not on termux/android. Treat like this: // $ termshark // # use network interface 1 - maps to // # termshark -i 1 psrcs = append(psrcs, pcap.InterfaceSource{Iface: "1"}) default: // We're on termux/android, but got an unexpected error. //if err != termshark.NoPicker { // !NoPicker means we could be on android/termux, but something else went wrong if err = system.PickFileError(err.Error()); err != nil { // Termux's toast ran into an error...! Maybe not installed? fmt.Fprintf(os.Stderr, err.Error()) } return 1 } } else { // $ cat foo.pcap | termshark // # use stdin - maps to // $ cat foo.pcap | termshark -r - psrcs = append(psrcs, pcap.FileSource{Filename: "-"}) } } } else { // Add it to filter args. Figure out later if they're capture or display. filterArgs = append(filterArgs, opts.Args.FilterOrPcap) } if pcapf != "" && len(opts.Ifaces) > 0 { fmt.Fprintf(os.Stderr, "Please supply either a pcap or one or more live captures.\n") return 1 } // Invariant: pcap != "" XOR len(opts.Ifaces) > 0 if len(psrcs) == 0 { switch { case pcapf != "": psrcs = append(psrcs, pcap.FileSource{Filename: pcapf}) case len(opts.Ifaces) > 0: for _, iface := range opts.Ifaces { psrcs = append(psrcs, pcap.InterfaceSource{Iface: iface}) } } } // Here we check for // (a) sources named '-' - these need rewritten to /dev/fd/N and stdin needs to be moved // (b) fifo sources - these are switched from -r to -i because that's what tshark needs haveStdin := false for pi, psrc := range psrcs { switch { case psrc.Name() == "-": if haveStdin { fmt.Fprintf(os.Stderr, "Requested live capture %v (\"stdin\") cannot be supplied more than once.\n", psrc.Name()) return 1 } if termshark.IsTerminal(os.Stdin.Fd()) { fmt.Fprintf(os.Stderr, "Requested live capture is %v (\"stdin\") but stdin is a tty.\n", psrc.Name()) fmt.Fprintf(os.Stderr, "Perhaps you intended to pipe packet input to termshark?\n") return 1 } if runtime.GOOS != "windows" { psrcs[pi] = pcap.PipeSource{Descriptor: "/dev/fd/0", Fd: int(os.Stdin.Fd())} haveStdin = true } else { fmt.Fprintf(os.Stderr, "Sorry, termshark does not yet support piped input on Windows.\n") return 1 } default: stat, err := os.Stat(psrc.Name()) if err != nil { if psrc.IsFile() || psrc.IsFifo() { // Means this was supplied with -r - since any file sources means there's (a) 1 and (b) // no other sources. So it must stat. Note if we started with -i fifo, this check // isn't done... but it still ought to exist. fmt.Fprintf(os.Stderr, "Error reading file %s: %v.\n", psrc.Name(), err) return 1 } continue } if stat.Mode()&os.ModeNamedPipe != 0 { // If termshark was invoked with -r myfifo, switch to -i myfifo, which tshark uses. This // also puts termshark in "interface" mode where it assumes the source is unbounded // (e.g. a different spinner) psrcs[pi] = pcap.FifoSource{Filename: psrc.Name()} } else { if pcapffile, err := os.Open(psrc.Name()); err != nil { // Do this up front before the UI starts to catch simple errors quickly - like // the file not being readable. It's possible that tshark would be able to read // it and the termshark user not, but unlikely. fmt.Fprintf(os.Stderr, "Error reading file %s: %v.\n", psrc.Name(), err) return 1 } else { pcapffile.Close() } } } } // Means files fileSrcs := pcap.FileSystemSources(psrcs) if len(fileSrcs) == 1 { if len(psrcs) > 1 { fmt.Fprintf(os.Stderr, "You can't specify both a pcap and a live capture.\n") return 1 } } else if len(fileSrcs) > 1 { fmt.Fprintf(os.Stderr, "You can't specify more than one pcap.\n") return 1 } // Invariant: len(psrcs) > 0 // Invariant: len(fileSrcs) == 1 => len(psrcs) == 1 // go-flags returns [""] when no extra args are provided, so I can't just // test the length of this slice termshark.ReverseStringSlice(filterArgs) argsFilter := strings.Join(filterArgs, " ") // Work out capture filter afterwards because we need to determine first // whether any potential first argument is intended as a pcap file instead of // a capture filter. captureFilter := opts.CaptureFilter // Meaning there are only live captures if len(fileSrcs) == 0 && argsFilter != "" { if opts.CaptureFilter != "" { fmt.Fprintf(os.Stderr, "Two capture filters provided - '%s' and '%s' - please supply one only.\n", opts.CaptureFilter, argsFilter) return 1 } captureFilter = argsFilter } // -w something if opts.WriteTo != "" { if len(fileSrcs) > 0 { fmt.Fprintf(os.Stderr, "The -w flag is incompatible with regular capture sources %v\n", fileSrcs) return 1 } if opts.WriteTo == "-" { fmt.Fprintf(os.Stderr, "Cannot set -w to stdout. Target file must be regular or a symlink.\n") return 1 } // If the file does not exist, then proceed. If it does exist, check it is something "normal". if _, err = os.Stat(string(opts.WriteTo)); err == nil || !os.IsNotExist(err) { if !system.FileRegularOrLink(string(opts.WriteTo)) { fmt.Fprintf(os.Stderr, "Cannot set -w to %s. Target file must be regular or a symlink.\n", opts.WriteTo) return 1 } } } displayFilter := opts.DisplayFilter // Validate supplied filters e.g. no capture filter when reading from file if len(fileSrcs) > 0 { if captureFilter != "" { fmt.Fprintf(os.Stderr, "Cannot use a capture filter when reading from a pcap file - '%s' and '%s'.\n", captureFilter, pcapf) return 1 } if argsFilter != "" { if opts.DisplayFilter != "" { fmt.Fprintf(os.Stderr, "Two display filters provided - '%s' and '%s' - please supply one only.\n", opts.DisplayFilter, argsFilter) return 1 } displayFilter = argsFilter } } // Here we now have an accurate view of all psrcs - either file, fifo, pipe or interface // Helpful to use logging when enumerating interfaces below, so do it first if !opts.LogTty { logfile := termshark.CacheFile("termshark.log") logfd, err := os.OpenFile(logfile, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0666) if err != nil { fmt.Fprintf(os.Stderr, "Could not create log file %s: %v\n", logfile, err) return 1 } // Don't close it - just let the descriptor be closed at exit. logrus is used // in many places, some outside of this main function, and closing results in // an error often on freebsd. //defer logfd.Close() log.SetOutput(logfd) } debug := false if (opts.Debug.Set && opts.Debug.Val == true) || (!opts.Debug.Set && profiles.ConfBool("main.debug", false)) { debug = true } if debug { for _, addr := range termshark.LocalIPs() { log.Infof("Starting debug web server at http://%s:6060/debug/pprof/", addr) } go func() { log.Println(http.ListenAndServe("0.0.0.0:6060", nil)) }() } for _, dir := range []string{termshark.CacheDir(), termshark.DefaultPcapDir(), 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 := profiles.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) profiles.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 != profiles.ConfString("main.last-used-tshark", "") { fields.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. profiles.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 := profiles.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) profiles.SetConf("main.color-tsharks", colorTsharks) } } // If any of opts.Ifaces is provided as a number, it's meant as the index of the interfaces as // per the order returned by the OS. useIface will always be the name of the interface. var systemInterfaces map[int][]string // See if the interface argument is an integer for pi, psrc := range psrcs { checkInterfaceName := false ifaceIdx := -1 if psrc.IsInterface() { if i, err := strconv.Atoi(psrc.Name()); err == nil { ifaceIdx = i } // If it's a fifo, then always treat is as a fifo and not a reference to something in tshark -D if ifaceIdx != -1 { // if the argument is an integer, then confirm it in the output of tshark -D checkInterfaceName = true } else if runtime.GOOS == "windows" { // If we're on windows, then all interfaces - indices and names - // will be in tshark -D, so confirm it there checkInterfaceName = true } } if checkInterfaceName { if systemInterfaces == nil { systemInterfaces, err = termshark.Interfaces() if err != nil { fmt.Fprintf(os.Stderr, "Could not enumerate network interfaces: %v\n", err) return 1 } } gotit := false var canonicalName string iLoop: for n, i := range systemInterfaces { // (7, ["NDIS_...", "Local Area..."]) if n == ifaceIdx { gotit = true canonicalName = i[0] break } else { for _, iname := range i { if iname == psrc.Name() { gotit = true canonicalName = i[0] break iLoop } } } } if gotit { // Guaranteed that psrc.IsInterface() is true // Use the canonical name e.g. "NDIS_...". Then the temporary filename will // have a more meaningful name. psrcs[pi] = pcap.InterfaceSource{Iface: canonicalName} } else { fmt.Fprintf(os.Stderr, "Could not find network interface %s\n", psrc.Name()) return 1 } } } watcher, err := confwatcher.New() if err != nil { fmt.Fprintf(os.Stderr, "Problem constructing config file watcher: %v", err) return 1 } defer watcher.Close() //====================================================================== // If != "", then the name of the file to which packets are saved when read from an // interface source. We can't just use the loader because the user might clear then load // a recent pcap on top of the originally loaded packets. ifacePcapFilename := "" defer func() { // if useIface != "" then we run dumpcap with the -i option - which // means the packet source is either an interface, a pipe, or a // fifo. In all cases, we save the packets to a file so that if a // filter is applied, we can restart - and so that we preserve the // capture at the end of running termshark. if len(pcap.FileSystemSources(psrcs)) == 0 && startedSuccessfully && !ui.WriteToSelected && !ui.WriteToDeleted { fmt.Fprintf(os.Stderr, "Packets read from %s have been saved in %s\n", pcap.SourcesString(psrcs), ifacePcapFilename) } }() //====================================================================== ifaceExitCode := 0 stderr := &bytes.Buffer{} var ifaceErr error // This is deferred until after the app is Closed - otherwise messages written to stdout/stderr are // swallowed by tcell. defer func() { if ifaceExitCode != 0 { fmt.Fprintf(os.Stderr, "Cannot capture on device %s", pcap.SourcesString(psrcs)) if ifaceErr != nil { fmt.Fprintf(os.Stderr, ": %v", ifaceErr) } fmt.Fprintf(os.Stderr, " (exit code %d)\n", ifaceExitCode) if stderr.Len() != 0 { // The default capture bin is termshark itself, with a special environment // variable set that causes it to try dumpcap, then tshark, in that order (for // efficiency of capture, but falling back to tshark for extcap interfaces). // But telling the user the capture process is "termshark" is misleading. cbin, err1 := filepath.Abs(filepath.FromSlash(termshark.CaptureBin())) def, err2 := filepath.Abs("termshark") if err1 == nil && err2 == nil && cbin == def { cbin = "the capture process" } fmt.Fprintf(os.Stderr, "Standard error stream from %s:\n", cbin) fmt.Fprintf(os.Stderr, "------\n%s\n------\n", stderr.String()) } if runtime.GOOS == "linux" && os.Geteuid() != 0 { fmt.Fprintf(os.Stderr, "You might need: sudo setcap cap_net_raw,cap_net_admin+eip %s\n", termshark.PrivilegedBin()) fmt.Fprintf(os.Stderr, "Or try running with sudo or as root.\n") } fmt.Fprintf(os.Stderr, "See https://termshark.io/no-root for more info.\n") } }() // Initialize application state for dark mode and auto-scroll ui.DarkMode = profiles.ConfBool("main.dark-mode", true) ui.AutoScroll = profiles.ConfBool("main.auto-scroll", true) ui.PacketColors = profiles.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 := profiles.ConfStringSlice("main.pdml-args", []string{}) psmlArgs := profiles.ConfStringSlice("main.psml-args", []string{}) if opts.TimestampFormat != "" { psmlArgs = append(psmlArgs, "-t", opts.TimestampFormat) } tsharkArgs := profiles.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 := profiles.ConfInt("main.pcap-cache-size", 64) bundleSize := profiles.ConfInt("main.pcap-bundle-size", 1000) if bundleSize <= 0 { maxBundleSize := 100000 log.Infof("Config specifies pcap-bundle-size as %d - setting to max (%d)", bundleSize, maxBundleSize) bundleSize = maxBundleSize } var ifaceTmpFile string var waitingForPackets bool // no file sources - so interface or fifo if len(pcap.FileSystemSources(psrcs)) == 0 { if opts.WriteTo != "" { ifaceTmpFile = string(opts.WriteTo) ui.WriteToSelected = true } else { srcNames := make([]string, 0, len(psrcs)) for _, psrc := range psrcs { srcNames = append(srcNames, psrc.Name()) } ifaceTmpFile = pcap.TempPcapFile(srcNames...) } waitingForPackets = true } else { // Start UI right away, reading from a file close(ui.StartUIChan) } // Need to figure out possible changes to COLORTERM before creating the // tcell screen. Note that even though apprunner.Start() below will create // a new screen, it will use a terminfo that it constructed the first time // we call NewApp(), because tcell stores these in a global map. So if the // first terminfo is created in an environment with COLORTERM=truecolor, // the terminfo Go struct is extended with codes that emit truecolor-compatible // ansi codes for colors. Then if I later create a new screen without COLORTERM, // tcell will still use the extended terminfo struct and emit truecolor-codes // anyway. // // If you are using base16-shell, the lowest colors 0-21 in the 256 color space // will be remapped to whatever colors the terminal base16 theme sets up. If you // are using a termshark theme that expresses colors in RGB style (#7799AA), and // termshark is running in a 256-color terminal, then termshark will find the closest // match for the RGB color in the 256 color-space. But termshark assumes that colors // 0-21 are set up normally, and not remapped. If the closest match is one of those // colors, then the theme won't look as expected. A workaround is to tell // gowid not to use colors 0-21 when finding the closest match. if profiles.ConfKeyExists("main.ignore-base16-colors") { gowid.IgnoreBase16 = profiles.ConfBool("main.ignore-base16-colors", false) } else { // Try to auto-detect whether or not base16-shell is installed and in-use gowid.IgnoreBase16 = (os.Getenv("BASE16_SHELL") != "") } if gowid.IgnoreBase16 { log.Infof("Will not consider colors 0-21 from the terminal 256-color-space when interpolating theme colors") // If main.respect-colorterm=true then termshark will leave COLORTERM set and use // 24-bit color if possible. The problem with this, in the presence of base16, is that // some terminal-emulators - e.g. gnome-terminal - still seems to map RGB ANSI codes // colors that are set at values 0-21 in the 256-color space. I'm not sure if this is // just an implementation snafu, or if something else is going on... In any case, // termshark will fall back to 256-colors if base16 is detected because I can // programmatically avoid choosing colors 0-21 for anything termshark needs. if os.Getenv("COLORTERM") != "" && !profiles.ConfBool("main.respect-colorterm", false) { log.Infof("Pessimistically disabling 24-bit color to avoid conflicts with base16") os.Unsetenv("COLORTERM") } } // the app variable is created here so I can bind it in the defer below var app *gowid.App // 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() } if ui.SearchWidget != nil { ui.SearchWidget.Close(app) } if ui.CurrentWormholeWidget != nil { ui.CurrentWormholeWidget.Close() } if ui.CurrentColsWidget != nil { ui.CurrentColsWidget.Close() } }() if app, err = ui.Build(usetty); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) // Tcell returns ExitError now because if its internal terminfo DB does not have // a matching entry, it tries to build one with infocmp. if _, ok := termshark.RootCause(err).(*exec.ExitError); ok { fmt.Fprintf(os.Stderr, "Termshark could not recognize your terminal. Try changing $TERM.\n") } return 1 } appRunner := app.Runner() pcap.PcapCmds = pcap.MakeCommands(opts.DecodeAs, tsharkArgs, pdmlArgs, psmlArgs, ui.PacketColors) pcap.PcapOpts = pcap.Options{ CacheSize: cacheSize, PacketsPerLoad: bundleSize, } // This is a global. The type supports swapping out the real loader by embedding it via // pointer, but I assume this only happens in the main goroutine. ui.Loader = &pcap.PacketLoader{ParentLoader: pcap.NewPcapLoader(pcap.PcapCmds, &pcap.Runner{app}, pcap.PcapOpts)} // Populate the filter widget initially - runs asynchronously go ui.FilterWidget.UpdateCompletions(app) ui.Running = false validator := filter.DisplayFilterValidator{ Invalid: &filter.ValidateCB{ App: app, Fn: func(app gowid.IApp) { if !ui.Running { fmt.Fprintf(os.Stderr, "Invalid filter: %s\n", displayFilter) ui.RequestQuit() } else { app.Run(gowid.RunFunction(func(app gowid.IApp) { ui.OpenError(fmt.Sprintf("Invalid filter: %s", displayFilter), app) })) } }, }, } // Do this before the load starts, so that the PSML process has a guaranteed safe // PSML column format to use when it begins. The call to RequestLoadPcapWithCheck, // for example, will access the setting for preferred PSML columns. // // Init a global variable with the list of all valid tshark columns and // their formats. This might start a tshark process if the data isn't // cached. If so, print a message to console - "initializing". I'm not doing // anything smarter or async - it's not worth it, this should take a fraction // of a second. err = shark.InitValidColumns() // If this message is needed, we want it to appear after the init message for the packet // columns - after InitValidColumns if waitingForPackets { fmt.Fprintf(os.Stderr, fmt.Sprintf("(The termshark UI will start when packets are detected on %s...)\n", strings.Join(pcap.SourcesNames(psrcs), " or "))) } // Refresh fileSrcs = pcap.FileSystemSources(psrcs) if len(fileSrcs) > 0 { psrc := fileSrcs[0] absfile, err := filepath.Abs(psrc.Name()) if err != nil { fmt.Fprintf(os.Stderr, "Could not determine working directory: %v\n", err) return 1 } doit := func(app gowid.IApp) { app.Run(gowid.RunFunction(func(app gowid.IApp) { ui.FilterWidget.SetValue(displayFilter, app) })) ui.RequestLoadPcap(absfile, displayFilter, ui.NoGlobalJump, app) } validator.Valid = &filter.ValidateCB{Fn: doit, App: app} validator.EmptyCB = &filter.ValidateCB{Fn: doit, App: app} validator.Validate(displayFilter) } else { // Verifies whether or not we will be able to read from the interface (hopefully) ifaceExitCode = 0 for _, psrc := range psrcs { if psrc.IsInterface() { if ifaceExitCode, ifaceErr = termshark.RunForStderr( termshark.CaptureBin(), []string{"-i", psrc.Name(), "-a", "duration:1"}, append(os.Environ(), "TERMSHARK_CAPTURE_MODE=1"), stderr, ); ifaceExitCode != 0 { return 1 } } else { // We only test one - the assumption is that if dumpcap can read from eth0, it can also read from eth1, ... And // this lets termshark start up more quickly. break } } doLoad := func(app gowid.IApp) { ifacePcapFilename = ifaceTmpFile ui.RequestLoadInterfaces(psrcs, captureFilter, displayFilter, ifaceTmpFile, app) } ifValid := func(app gowid.IApp) { app.Run(gowid.RunFunction(func(app gowid.IApp) { ui.FilterWidget.SetValue(displayFilter, app) })) doLoad(app) } validator.Valid = &filter.ValidateCB{Fn: ifValid, App: app} validator.EmptyCB = &filter.ValidateCB{Fn: doLoad, App: app} validator.Validate(displayFilter) } quitIssuedToApp := false wasLoadingPdmlLastTime := ui.Loader.PdmlLoader.IsLoading() wasLoadingAnythingLastTime := ui.Loader.LoadingAnything() // Keep track of this across runs of the main loop so we don't go backwards (because // that looks wrong to the user) var prevProgPercentage float64 progTicker := time.NewTicker(time.Duration(200) * time.Millisecond) ctrlzLineDisc := tty.TerminalSignals{} // This is used to stop iface load and any stream reassembly. Make sure to // avoid any stream reassembly errors, since this is a controlled shutdown // but the tshark processes reading data for stream reassembly may still // complain about interruptions stopLoaders := func() { if ui.StreamLoader != nil { ui.StreamLoader.SuppressErrors = true } ui.Loader.CloseMain() } inactiveDuration := 60 * time.Second inactivityTimer := time.NewTimer(inactiveDuration) currentlyInactive := false // True if the timer has fired and termshark is in "inactive" state var progCancelTimer *time.Timer checkedPcapCache := false checkPcapCacheDuration := 5 * time.Second checkPcapCacheTimer := time.NewTimer(checkPcapCacheDuration) Loop: for { var finChan <-chan time.Time var tickChan <-chan time.Time var inactivityChan <-chan time.Time var checkPcapCacheChan <-chan time.Time var tcellEvents <-chan tcell.Event var opsChan <-chan gowid.RunFunction var afterRenderEvents <-chan gowid.IAfterRenderEvent // For setting struct views empty. This isn't done as soon as a load is initiated because // in the case we are loading from an interface and following new packets, we get an ugly // blinking effect where the loading message is displayed, shortly followed by the struct or // hex view which comes back from the pdml process (because the pdml process can only read // up to the end of the currently seen packets, each time it has to start afresh from the // beginning to get new packets). Waiting 500ms to display loading gives enough time, in // practice, // On change of state - check for new pdml requests if ui.Loader.PdmlLoader.IsLoading() != wasLoadingPdmlLastTime { ui.CacheRequestsChan <- struct{}{} } // This should really be moved to a handler... if !ui.Loader.LoadingAnything() { if wasLoadingAnythingLastTime { // If the state has just switched to 0, it means no interface-reading process is // running. That means we will no longer be reading from an interface or a fifo, so // we point the loader at the file we wrote to the cache, and redirect all // loads/filters to that now. ui.Loader.TurnOffPipe() app.Run(gowid.RunFunction(func(app gowid.IApp) { ui.ClearProgressWidgetFor(app, ui.LoaderOwns) ui.SetProgressDeterminateFor(app, ui.LoaderOwns) // always switch back - for pdml (partial) loads of later data. })) // When the progress bar is enabled, track the previous percentage reached. This is // so that I don't go "backwards" if I generate a progress value less than the last // one, using the current algorithm (because it would be confusing to see it go // backwards) prevProgPercentage = 0.0 } // EnableOpsVar will be enabled when all the handlers have run, which happen in the main goroutine. // I need them to run because the loader channel is closed in one, and the ticker goroutines // don't terminate until these goroutines stop if ui.QuitRequested { if ui.Running { if !quitIssuedToApp { app.Quit() quitIssuedToApp = true // Avoid closing app twice - doubly-closed channel } } else { // No UI so exit loop immediately break Loop } } } // Only display the progress bar if PSML is loading or if PDML is loading that is needed // by the UI. If the PDML is an optimistic load out of the display, then no need for // progress. doprog := false if ui.Loader.PsmlLoader.IsLoading() || (ui.Loader.PdmlLoader.IsLoading() && ui.Loader.PdmlLoader.LoadIsVisible()) { if prevProgPercentage >= 1.0 { if progCancelTimer != nil { progCancelTimer.Reset(time.Duration(500) * time.Millisecond) progCancelTimer = nil } } else { ui.SetProgressWidget(app) if progCancelTimer == nil { progCancelTimer = time.AfterFunc(time.Duration(100000)*time.Hour, func() { app.Run(gowid.RunFunction(func(app gowid.IApp) { ui.ClearProgressWidgetFor(app, ui.LoaderOwns) progCancelTimer = nil })) }) } } tickChan = progTicker.C // progress is only enabled when a pcap may be loading // Rule: // - prefer progress if we can apply it to psml only (not pdml) // - otherwise use a spinner if interface load or fifo load in operation // - otherwise use progress for pdml if system.HaveFdinfo { // Prefer progress, if the OS supports it. doprog = true if ui.Loader.ReadingFromFifo() { // But if we are have an interface load (or a pipe load), then we can't // predict when the data will run out, so use a spinner. That's because we // feed the data to tshark -T psml with a tail command which reads from // the tmp file being created by the pipe/interface source. doprog = false if !ui.Loader.InterfaceLoader.IsLoading() && !ui.Loader.PsmlLoader.IsLoading() { // Unless those loads are finished, and the only loading activity is now // PDML/pcap, which is loaded on demand in blocks of 1000. Then we can // use the progress bar. doprog = true } } } } if ui.Loader.InterfaceLoader.IsLoading() && !currentlyInactive { inactivityChan = inactivityTimer.C } if !checkedPcapCache { checkPcapCacheChan = checkPcapCacheTimer.C } // Only process tcell and gowid events if the UI is running. if ui.Running { tcellEvents = app.TCellEvents } if ui.Fin != nil && ui.Fin.Active() { finChan = ui.Fin.C() } // For operations like ClearPcap - need previous loads to be fully finished first. The operations // channel is enabled until an operation starts, then disabled until the operation re-enables it // via a handler. // // Make sure state doesn't change until all handlers have been run if !ui.Loader.PdmlLoader.IsLoading() && !ui.Loader.PsmlLoader.IsLoading() { opsChan = pcap.OpsChan } afterRenderEvents = app.AfterRenderEvents wasLoadingPdmlLastTime = ui.Loader.PdmlLoader.IsLoading() wasLoadingAnythingLastTime = ui.Loader.LoadingAnything() select { case <-checkPcapCacheChan: // Only check the cache dir if we own it; don't want to delete pcap files // that might be shared with wireshark if profiles.ConfBool("main.use-tshark-temp-for-pcap-cache", false) { log.Infof("Termshark does not own the pcap temp dir %s; skipping size check", termshark.PcapDir()) } else { termshark.PrunePcapCache() } checkedPcapCache = true case <-inactivityChan: if ui.Fin != nil { ui.Fin.Activate() } currentlyInactive = true case <-finChan: ui.Fin.Advance() app.Redraw() case <-ui.StartUIChan: log.Infof("Launching termshark UI") // Go to termshark UI view if err = app.ActivateScreen(); err != nil { fmt.Fprintf(os.Stderr, "Error starting UI: %v\n", err) return 1 } // Need to do that here because the app won't know how many colors the screen // has (and therefore which variant of the theme to load) until the screen is // activated. ui.ApplyCurrentTheme(app) // This needs to run after the toml config file is loaded. ui.SetupColors() // Start tcell/gowid events for keys, etc appRunner.Start() // Reinstate our terminal overrides that allow ctrl-z if err := ctrlzLineDisc.Set(usetty); err != nil { ui.OpenError(fmt.Sprintf("Unexpected error setting Ctrl-z handler: %v\n", err), app) } ui.Running = true startedSuccessfully = true ui.StartUIChan = nil // make sure it's not triggered again if runtime.GOOS != "windows" { if app.GetColorMode() == gowid.Mode8Colors { // If exists is true, it means we already tried and then reverted back, so // just load up termshark normally with no further interruption. if _, exists := os.LookupEnv("TERMSHARK_ORIGINAL_TERM"); !exists { if !profiles.ConfBool("main.disable-term-helper", false) { err = termshark.Does256ColorTermExist() if err != nil { log.Infof("Must use 8-color mode because 256-color version of TERM=%s unavailable - %v.", os.Getenv("TERM"), err) } else { time.AfterFunc(time.Duration(3)*time.Second, func() { app.Run(gowid.RunFunction(func(app gowid.IApp) { ui.SuggestSwitchingTerm(app) })) }) } } } } else if os.Getenv("TERMSHARK_ORIGINAL_TERM") != "" { time.AfterFunc(time.Duration(3)*time.Second, func() { app.Run(gowid.RunFunction(func(app gowid.IApp) { ui.IsTerminalLegible(app) })) }) } } defer func() { // Do this to make sure the program quits quickly if quit is invoked // mid-load. It's safe to call this if a pcap isn't being loaded. // // The regular stopLoadPcap will send a signal to pcapChan. But if app.quit // is called, the main select{} loop will be broken, and nothing will listen // to that channel. As a result, nothing stops a pcap load. This calls the // context cancellation function right away stopLoaders() appRunner.Stop() app.Close() ui.Running = false }() case fn := <-opsChan: app.Run(fn) case <-ui.QuitRequestedChan: ui.QuitRequested = true // Without this, a quit during a pcap load won't happen until the load is finished if ui.Loader.LoadingAnything() { // We know we're not idle, so stop any load so the quit op happens quickly for the user. Quit // will happen next time round because the quitRequested flag is checked. stopLoaders() } case sig := <-sigChan: if system.IsSigTSTP(sig) { if ui.Running { // Remove our terminal overrides that allow ctrl-z ctrlzLineDisc.Restore() // Stop tcell/gowid events for keys, etc appRunner.Stop() // Go back to terminal view app.DeactivateScreen() ui.Running = false uiSuspended = true } else { log.Infof("UI not active - no terminal changes required.") } // This is not synchronous, but some time after calling this, we'll be suspended. if err := system.StopMyself(); err != nil { fmt.Fprintf(os.Stderr, "Unexpected error issuing SIGSTOP: %v\n", err) return 1 } } else if system.IsSigCont(sig) { if uiSuspended { // Go to termshark UI view if err = app.ActivateScreen(); err != nil { fmt.Fprintf(os.Stderr, "Error starting UI: %v\n", err) return 1 } // Start tcell/gowid events for keys, etc appRunner.Start() // Reinstate our terminal overrides that allow ctrl-z if err := ctrlzLineDisc.Set(usetty); err != nil { ui.OpenError(fmt.Sprintf("Unexpected error setting Ctrl-z handler: %v\n", err), app) } ui.Running = true uiSuspended = false } } else if system.IsSigUSR1(sig) { if debug { termshark.ProfileCPUFor(20) } else { log.Infof("SIGUSR1 ignored by termshark - see the --debug flag") } } else if system.IsSigUSR2(sig) { if debug { termshark.ProfileHeap() } else { log.Infof("SIGUSR2 ignored by termshark - see the --debug flag") } } else { log.Infof("Starting termination via signal %v", sig) ui.RequestQuit() } case <-ui.CacheRequestsChan: ui.CacheRequests = pcap.ProcessPdmlRequests(ui.CacheRequests, ui.Loader.ParentLoader, ui.Loader.PdmlLoader, ui.SetStructWidgets{ui.Loader}, app) case <-tickChan: // We already know that we are LoadingPdml|LoadingPsml if doprog { app.Run(gowid.RunFunction(func(app gowid.IApp) { prevProgPercentage = ui.UpdateProgressBarForFile(ui.Loader, prevProgPercentage, app) })) } else { app.Run(gowid.RunFunction(func(app gowid.IApp) { ui.UpdateProgressBarForInterface(ui.Loader.InterfaceLoader, app) })) } case ev := <-tcellEvents: app.HandleTCellEvent(ev, gowid.IgnoreUnhandledInput) inactivityTimer.Reset(inactiveDuration) currentlyInactive = false checkPcapCacheTimer.Reset(checkPcapCacheDuration) 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 } ev.RunThenRenderEvent(app) if ui.Running { app.RedrawTerminal() } case <-watcher.ConfigChanged(): // Re-read so changes that can take effect immediately do so if err := profiles.ReadDefaultConfig(dirs[0].Path); err != nil { log.Warnf("Unexpected error re-reading toml config: %v", err) } ui.UpdateRecentMenu(app) } } return 0 } //====================================================================== // Local Variables: // mode: Go // fill-column: 78 // End: termshark-2.4.0/configs/000077500000000000000000000000001426312004500151425ustar00rootroot00000000000000termshark-2.4.0/configs/profiles/000077500000000000000000000000001426312004500167655ustar00rootroot00000000000000termshark-2.4.0/configs/profiles/profiles.go000066400000000000000000000163471426312004500211520ustar00rootroot00000000000000// Copyright 2019-2022 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 profiles import ( "fmt" "io/ioutil" "os" "path/filepath" "sync" "github.com/shibukawa/configdir" "github.com/spf13/viper" ) //====================================================================== // 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 // If this is non-nil, then the user has a profile loaded var currentName string var vProfile *viper.Viper var vDefault *viper.Viper //====================================================================== func init() { vDefault = viper.New() } //====================================================================== // First is error, second is warning func ReadDefaultConfig(dir string) error { return readConfig(vDefault, dir, "termshark", true) } func readConfig(v *viper.Viper, dir string, base string, createIfNecessary bool) error { confMutex.Lock() defer confMutex.Unlock() var err error v.SetConfigName(base) // no need to include file extension - looks for file called termshark.ini for example v.AddConfigPath(dir) fp := filepath.Join(dir, fmt.Sprintf("%s.toml", base)) if createIfNecessary { var f *os.File if f, err = os.OpenFile(fp, os.O_RDONLY|os.O_CREATE, 0666); err == nil { f.Close() } } // We managed anyway - so don't alarm the user if v.ReadInConfig() == nil { err = nil } else if err != nil { err = fmt.Errorf("Profile %s not found. (%w)", fp, err) } else { err = fmt.Errorf("Profile %s not found.", fp) } return err } func Default() *viper.Viper { return vDefault } func Current() *viper.Viper { if vProfile != nil { return vProfile } return Default() } func ConfKeyExists(name string) bool { return ConfKeyExistsIn(Current(), name) || ConfKeyExistsIn(Default(), name) } func ConfKeyExistsIn(v *viper.Viper, name string) bool { return v.Get(name) != nil } func ConfString(name string, def string) string { return ConfStringFrom(Current(), Default(), name, def) } func ConfStringFrom(v *viper.Viper, vd *viper.Viper, name string, def string) string { confMutex.Lock() defer confMutex.Unlock() // Use GetString because viper will not allow deletion of keys; so I always // use the assumption that "" is the same as unset for a string key; then // I can fallback to the default map if the requested key's value is either "" // or missing if v != nil && v.GetString(name) != "" { return v.GetString(name) } else if vd.GetString(name) != "" { return vd.GetString(name) } else { return def } } func SetConf(name string, val interface{}) { SetConfIn(Current(), name, val) } func SetConfIn(v *viper.Viper, name string, val interface{}) { confMutex.Lock() defer confMutex.Unlock() v.Set(name, val) v.WriteConfig() } func ConfStrings(name string) []string { return confStrings(Current(), Default(), name) } func confStrings(v *viper.Viper, vd *viper.Viper, name string) []string { confMutex.Lock() defer confMutex.Unlock() if v != nil && ConfKeyExistsIn(v, name) { return v.GetStringSlice(name) } else { return vd.GetStringSlice(name) } } func DeleteConf(name string) { deleteConf(Current(), name) } func deleteConf(v *viper.Viper, name string) { confMutex.Lock() defer confMutex.Unlock() v.Set(name, "") v.WriteConfig() } func ConfInt(name string, def int) int { return confInt(Current(), Default(), name, def) } func confInt(v *viper.Viper, vd *viper.Viper, name string, def int) int { confMutex.Lock() defer confMutex.Unlock() if v != nil && v.Get(name) != nil { return v.GetInt(name) } else if vd != nil && vd.Get(name) != nil { return vd.GetInt(name) } else { return def } } func ConfBool(name string, def ...bool) bool { return confBool(vProfile, vDefault, name, def...) } func confBool(v *viper.Viper, vd *viper.Viper, name string, def ...bool) bool { confMutex.Lock() defer confMutex.Unlock() if v != nil && v.Get(name) != nil { return v.GetBool(name) } else if vd != nil && vd.Get(name) != nil { return vd.GetBool(name) } else { if len(def) > 0 { return def[0] } else { return false } } } func ConfStringSlice(name string, def []string) []string { return ConfStringSliceFrom(vProfile, vDefault, name, def) } func ConfStringSliceFrom(v *viper.Viper, vd *viper.Viper, name string, def []string) []string { confMutex.Lock() defer confMutex.Unlock() var res []string if v != nil { res = v.GetStringSlice(name) } if res == nil && vd != nil { res = vd.GetStringSlice(name) } if res == nil { res = def } return res } func WriteConfigAs(name string) error { return writeConfigAs(Current(), name) } func writeConfigAs(v *viper.Viper, name string) error { return v.WriteConfigAs(name) } func profilesDir() (string, error) { stdConf := configdir.New("", "termshark") conf := stdConf.QueryFolderContainsFile("profiles") if conf == nil { return "", fmt.Errorf("Could not find profiles dir.") } dirs := stdConf.QueryFolders(configdir.Global) return filepath.Join(dirs[0].Path, "profiles"), nil } func CopyToAndUse(name string) error { if Default() == Current() { vProfile = viper.New() } dir, err := profilesDir() if err != nil { return err } dir = filepath.Join(dir, name) if _, err := os.Stat(dir); os.IsNotExist(err) { err = os.Mkdir(dir, 0777) if err != nil { return fmt.Errorf("Unexpected error making dir %s: %v", dir, err) } } vProfile.SetConfigFile(filepath.Join(dir, "termshark.toml")) vProfile.WriteConfig() return Use(name) } func CurrentName() string { if currentName == "" { return "default" } return currentName } func AllNames() []string { res := AllNonDefaultNames() return append(res, "default") } func AllNonDefaultNames() []string { matches := make([]string, 0) profPath, err := profilesDir() if err != nil { return matches } files, err := ioutil.ReadDir(profPath) if err == nil { for _, file := range files { if file.Name() != "default" { if _, err := os.Stat(filepath.Join(profPath, file.Name(), "termshark.toml")); err == nil { matches = append(matches, file.Name()) } } } } return matches } func Delete(name string) error { dir, err := profilesDir() if err != nil { return err } dir = filepath.Join(dir, name) err = os.RemoveAll(dir) if err != nil { return fmt.Errorf("Unexpected error deleting profile dir %s: %v", dir, err) } return nil } func Use(name string) error { // Go back to default - so no overriding profile if name == "" || name == "default" { confMutex.Lock() defer confMutex.Unlock() vProfile = nil currentName = "default" return nil } vNew := viper.New() dir, err := profilesDir() if err != nil { return err } dir = filepath.Join(dir, name) if _, err := os.Stat(dir); os.IsNotExist(err) { err = os.Mkdir(dir, 0777) if err != nil { return fmt.Errorf("Unexpected error making dir %s: %v", dir, err) } } if err := readConfig(vNew, dir, "termshark", false); err != nil { return err } vProfile = vNew currentName = name return nil } //====================================================================== // Local Variables: // mode: Go // fill-column: 78 // End: termshark-2.4.0/configs/termshark-dd01307f2423.json.enc000066400000000000000000000044001426312004500222400ustar00rootroot00000000000000c*Ƅ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). As of termshark v2.4, dark-mode is the default. ## Termshark's colors are wrong! See [this answer](#what-settings-affect-termsharks-colors). If termshark's background is a strange color like dark blue or orange, maybe a tool like base16-shell has remapped some of the colors in the 256-color-space, but termshark is unaware of this. Try setting this in `termshark.toml`: ```toml [main] ignore-base16-colors = true ``` ## What settings affect termshark's colors? Unfortunately there are several :-/ First of all, your terminal emulator's `TERM` variable determines the range of colors available to termshark e.g. `xterm-16color`, `xterm-256color`. If you also have `COLORTERM=truecolor` set, and the terminal emulator has support, 24-bit color will be available. Termshark will emit these 24-bit ANSI color codes and color should be faithfully reproduced. You can override the value of `TERM` with termshark's `main.term` setting in `termshark.toml` e.g. ```toml [main] term = "screen-256color" ``` When termshark runs, it will load your selected theme if it's available in the terminal's color mode. If not, it will choose the built-in `default` theme which is available in every mode. If you run in truecolor mode, and your chosen theme is only defined for 256-colors, termshark will load the 256-color theme. Termshark will load its theme from `~/.config/termshark/themes/` if it can find it, otherwise it will look in its built-in database. Termshark has themes called `default`, `dracula`, `solarized` and `base16` built-in. If you are using [base16-shell](https://github.com/chriskempson/base16-shell), then you might have colors 0-21 of your terminal's 256-color-space remapped. If you are running in 256-color mode, and your theme specifies RGB colors, termshark will choose the closest match among those in the 256-color-space. Termshark will ignore colors 0-21 as match candidates if `BASE16_SHELL` is set in the environment. It will also ignore these colors if you set `main.ignore-base16-colors` in `termshark.toml`. Otherwise, termshark will assume colors 0-21 are displayed "normally", and may pick these remapped colors as the closest match to a theme's color - resulting in incorrect colors. ## How do I rebuild termshark? If you don't have the source, clone it like this: ```bash $ git clone https://github.com/gcla/termshark ``` You'll get best results with the latest version of Golang - 1.15 as I write this - but anything >= 1.12 will work. Set your environment: ```bash $ export GO111MODULE=on ``` Change to the termshark dir and type ```bash $ go generate ./... $ go install ./... ``` The generate step is only necessary if you have changed any files under `termshark/assets/themes/`. If not, just run ```bash $ go install ./... ``` Termshark will be installed as `~/go/bin/termshark`. ## Where are the config and log files? You can find the config file, `termshark.toml`, in: - `${XDG_CONFIG_HOME}/termshark/` `(${HOME}/.config/termshark/)` on Linux - `${HOME}/Library/Application Support/termshark/` on macOS - `%APPDATA%\termshark\` `(C:\Users\\AppData\Roaming\termshark\)` on Windows You can find the log file, `termshark.log`, in: - `${XDG_CACHE_HOME}/termshark/` `(${HOME}/.cache//termshark/)` on Linux - `${HOME}/Library/Caches/termshark/` on macOS - `%LOCALAPPDATA%\termshark\` `(C:\Users\\AppData\Local\termshark\)` on Windows ## I'm capturing with tcpdump. Can termshark treat that pcap like a live capture? Yes, at least on Unix systems. Try this: ```bash shell1$ tcpdump -i eth0 -w foo.pcap shell2$ tail -f -c +0 foo.pcap | termshark ``` ## The console is too narrow on Windows Unfortunately, the standard console window won't let you increase its size beyond its initial bounds using the mouse. To work around this, after termshark starts, right-click on the window title and select "Properties". Click "Layout" and then adjust the "Window Size" settings. When you quit termshark, your console window will be restored to its original size. ![winconsole](/../gh-pages/images/winconsole.png?raw=true) ## Can I pass extra arguments to tshark? Yes, via `~/.config/termshark/termshark.toml`. Here is an example I use: ```toml [main] tshark-args = ["-d","udp.port==2075,cflow","-d","udp.port==9191,cflow","-d","udp.port==2055,cflow","-d","udp.port==2095,cflow"] ``` ## How does termshark use tshark? Termshark uses tshark to provide all the data it displays, and to validate display filter expressions. When you give termshark a pcap file, it will run ````bash tshark -T psml -r my.pcap -Y '' -o gui.column.format:"..."``` ```` The data provided to the `gui.column.format` tshark argument is stored in termshark's config file under the key `main.column-format`. Let's say the user is focused on packet number 1234. Then termshark will load packet structure and hex/byte data using commands like: ```bash tshark -T pdml -r my.pcap -Y ' and frame.number >= 1000 and frame.number < 2000' tshark -F pcap -r my.pcap -Y ' and frame.number >= 1000 and frame.number < 2000' -w - ``` If the user is reading from an interface, some extra processes are needed. To capture the data, termshark runs ```bash dumpcap -P -i eth0 -f -w ``` This process runs until the user hits `ctrl-c` or clicks the "Stop" button in the UI. The path to `tmpfile` is printed out to the user when termshark exits. Then to feed data continually to termshark, another process is started: ```bash tail -f -c +0 tmpfile ``` The stdout of the `tail` command is connected to the stdin of the PSML reading command, which is adjusted to: ````bash tshark -T psml -i - -l -Y '' -o gui.column.format:"..."``` ```` The `-l` switch might push the data to the UI more quickly... The PDML and byte/hex generating commands read directly from `tmpfile`, since they don't need to provide continual updates (they load data in batches as the user moves around). When the user types in termshark's display filter widget, termshark issues the following command for each change: ```bash tshark -Y '' -r empty.pcap ``` and checks the return code of the process. If it's zero, termshark assumes the filter expression is valid, and turns the widget green. If the return code is non-zero, termshark assumes the expression is invalid and turns the widget red. The file `empty.pcap` is generated once on startup and cached in `$XDG_CACHE_HOME/termshark/empty.pcap` (on Linux, `~/.cache/termshark/empty.pcap`) On slower systems like the Raspberry Pi, you might see this widget go orange for a couple of seconds while termshark waits for tshark to finish. If the user selects the "Analysis -> Reassemble stream" menu option, termshark starts two more tshark processes to gather the data to display. First, tshark is invoked with the '-z' option to generate the reassembled stream information. Termshark knows the protocol and stream index to supply to tshark because it saves this information when processing the PDML to populate the packet structure view: ```bash tshark -r my.pcap -q -z follow,tcp,raw,15 ``` This means "follow TCP stream number 15". The output will look something like: ```console =================================================================== Follow: tcp,raw Filter: tcp.stream eq 15 Node 0: 192.168.0.114:1137 Node 1: 192.168.0.193:21 3232302043687269732053616e6465727320465450205365727665720d0a 55534552206373616e646572730d0a 3333312050617373776f726420726571756972656420666f72206373616e646572732e0d0a ... ``` A second tshark process is started concurrently: ```bash tshark -T pdml -r my.pcap -Y "tcp.stream eq 15" ``` The output of that is parsed to build an array mapping the index of each "chunk" of the stream (e.g. above, 0 is "3232", 1 is "5553", 2 is "3333") to the index of the corresponding packet. This is not always x->x because the stream payloads for some packets are of zero length and are not represented in the output from the first tshark '-z' process. The mapping is used when the user clicks on a chunk of the reassembled stream in the UI - termshark will then change focus behind the scenes to the corresponding packet in the packet list view. If you exit the stream reassembly UI, you can see the newly selected packet. When termshark starts these stream reassembly processes, it also sets a display filter in the main UI e.g. "tcp.stream eq 15". This causes termshark to invoke the PSML and PDML processes again - in addition to the two stream-reassembly-specific processes that I've just described. If the user selects "Analysis -> Conversations" from the menu, termshark starts a tshark process to gather this information. If the configured conversation types are `eth`, `ip`, `tcp`, then the invocation will look like: ```bash tshark -r my.pcap -q -z conv,eth -z conv,ip -z conv,tcp ``` The information is displayed in a table by conversation type. If the user has a display filter active - e.g. `http` - and hits the "Limit to filter" checkbox, then tshark will be invoked like this: ```bash tshark -r my.pcap -q -z conv,eth,http -z conv,ip,http -z conv,tcp,http ``` If the user runs a packet search and chooses the Display Filter option, termshark will launch a tshark process to find packets that match the filter. ````bash tshark -T psml -r my.pcap -Y '' -o gui.column.format:"No.","%m"``` ```` If the user has an active display filter via the UI, it is combined with the search expression: ````bash tshark -T psml -r my.pcap -Y '() && ()' -o gui.column.format:"No.","%m"``` ```` Termshark also uses tshark to generate the possible completions for prefixes of display filter terms. If you type `tcp.` in the filter widget, termshark will show a drop-down menu of possible completions. This is generated once at startup by running ```bash tshark -G fields ``` then parsing the output into a nested collection of Go maps, and serializing it to `$XDG_CACHE_HOME/termshark/tsharkfieldsv3.gob.gz`. If the user creates a new termshark profile, an option is provided to link to a Wireshark profile. To gather a list of these profiles, termshark runs ```bash tshark -G folders ``` and parses out the global and personal configuration directories. Finally, termshark runs tshark to generate the list of all valid columns and their names. These are used to populate a dropdown menu showing valid column choices when the user configures their column set. Termshark runs ```bash tshark -G column-formats ``` and serializes this list to `$XDG_CACHE_HOME/termshark/tsharkcolumnsv2.gob.gz`. Termshark also uses the `capinfos` binary to compute the information displayed via the menu "Analysis -> Capture file properties". `capinfos` is typically distributed with tshark. ## How can I make termshark run without root? Termshark depends on tshark, and termshark will run without root if tshark/dumpcap will. On Linux, these are the most common ways to allow tshark to run as a non-root user - For Ubuntu/Debian systems, you can add your user to the `wireshark` group. These instructions are taken [from this answer](https://osqa-ask.wireshark.org/questions/7976/wireshark-setup-linux-for-nonroot-user/51058) on [wireshark.org](https://ask.wireshark.org/questions/): ```bash sudo apt-get install wireshark sudo dpkg-reconfigure wireshark-common sudo usermod -a -G wireshark $USER newgrp wireshark ``` If you logout and login again after `usermod`, you can omit the `newgrp` command. - You might need to set the capabilities of `dumpcap` using a command like this: ```bash sudo setcap cap_net_raw,cap_net_admin+eip /usr/sbin/dumpcap ``` You can find more detail at https://wiki.wireshark.org/CaptureSetup/CapturePrivileges. ## Why is termshark generating traffic on port 5037? See [this issue](https://github.com/gcla/termshark/issues/98). TL;DR - try deleting `/usr/lib/wireshark/extcap/androiddump`. ## How can termshark capture from extcap interfaces with dumpcap? Termshark doesn't always capture using dumpcap. It will try to use dumpcap if possible, because testing (from @pocc) indicated that it is less likely to drop packets - presumably because dumpcap's job is limited to generating a pcap with little interpretation of data. However, dumpcap doesn't support extcap interfaces like `randpkt`. If termshark detects that the live capture device is an extcap interface, it will use tshark as the capture binary instead. It does this automatically by using `termshark` itself as the default `capture-command`, and to make this work, termshark now runs the capture command with the environment variable `TERMSHARK_CAPTURE_MODE` set. dumpcap and tshark will ignore that, but termshark will detect it at startup and switch immediately to capture mode. It then runs this, in pseudo-code form`: ```go cmd := exec.Command(dumpcap, args...) if cmd.Run() != nil { syscall.Exec(tshark, append([]string{tshark}, args...), os.Environ()) } ``` This trick is only implemented for Unix OSes. On Windows, termshark will use dumpcap. If you need to read extcap interfaces on Windows, you can set `capture-command` to `tshark` in the toml config file. ## Termshark is laggy or using a lot of RAM I hope this is much-improved with v2. If you still experience problems, try running termshark with the `--debug` flag e.g. ```bash termshark --debug -r foo.pcap ``` You can then generate a CPU profile with ```bash pkill -SIGUSR1 termshark ``` or a heap/memory profile with ```bash pkill -SIGUSR2 termshark ``` The profiles are stored under `$XDG_CACHE_HOME/termshark` (e.g. ~/.cache/termshark/). You can investigate with `go tool pprof` like this: ```bash go tool pprof -http=:6061 $(which termshark) ~/.cache/termshark/mem-20190929122218.prof ``` and then navigate to http://127.0.0.1:6061/ui/ (or remote IP) - or open a termshark issue and upload the profile for us to check :-) There will also be a debug web server running at http://127.0.0.1:6060/debug/pprof (or rmote IP) from where you can see running goroutines and other information. ## Termshark is using lots of disk space By default, termshark saves live captures in `${XDG_CACHE_HOME}/termshark/pcaps`. Over time, this directory can grow very large. If you do not need these captures, you can safely delete the directory. Termshark v2.3 and above provides a config option to control the growth of this directory e.g. ```toml [main] disk-cache-size-mb = 100 ``` If this setting is active and not -1, shortly after startup, termshark will check the directory and if it is too large, delete files, oldest first, to bring its size within the configured limit. Termshark v2.3 and above will also let you choose, prior to exiting, whether or not you want to keep the pcap of the current live capture. You can also invoke termshark with the `-w` flag to choose where the live capture pcap is written. ## 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? As I write this, I'm about to release termshark v2.4. Here's what might come in v2.5 and beyond: - Built-in support for editing packet color profiles - Expose many more of tshark's `-z` options - HTTP statistics and Wireshark's I/O graph - Allow the user to start reading from available interfaces once the UI has started - Anything you raise on Github issues - let me know what features you'd like! termshark-2.4.0/docs/Maintainer.md000066400000000000000000000056261426312004500170640ustar00rootroot00000000000000# How to Package Termshark for Release ## Termux (Android) I've been building use the termux docker builder. ```bash docker pull termux/package-builder ``` Clone the `termux-packages` and `termux-root-packages` repos: ```bash cd source/ git clone https://github.com/termux/termux-packages cd termux-packages git clone https://github.com/termux/termux-root-packages ``` Open `termux-packages/termux-root-packages/packages/termshark/build.sh` in an editor. Change ```bash cd $TERMUX_PKG_BUILDDIR go get -d -v github.com/gcla/termshark/v2/cmd/termshark@e185fa59d87c06fe1bafb83ce6dc15591434ccc8 go install github.com/gcla/termshark/v2/cmd/termshark ``` to use the correct uuid - I am using the uuid for v2.0.3 ```bash cd $TERMUX_PKG_BUILDDIR go get -d -v github.com/gcla/termshark/v2/cmd/termshark@73dfd1f6cb8c553eb524ebc27d991f637c1ac5ea go install github.com/gcla/termshark/v2/cmd/termshark ``` Change `TERMUX_PKG_VERSION` too. Save. Start docker and build (from `termux-packages` dir): ```bash gcla@elgin:~/source/termux-packages$ ./scripts/run-docker.sh Running container 'termux-package-builder' from image 'termux/package-builder'... builder@201c39983bf8:~/termux-packages$ rm /data/data/.built-packages/termshark builder@201c39983bf8:~/termux-packages$ ./clean.sh # to rebuild everything! builder@201c39983bf8:~/termux-packages$ ./build-package.sh termux-root-packages/packages/termshark/ ... ``` This will take several minutes. You'll probably see an error like this: ``` Wrong checksum for https://termshark.io: Expected: 36e45dfeb97f89379bda5be6bfe69c46e5c4211674120977e7b0033f5d90321a Actual: c05a64f1e502d406cc149c6e8b92720ad6310aecd1dd206e05713fd8a2247a84 ``` Open `termux-packages/termux-root-packages/packages/termshark/build.sh` again and change `TERMUX_PKG_SHA256`. Rebuild. Submit a PR to `termux-root-packages`. To edit files in use by a docker container, you can use tramp + emacs with a path like this: `/docker:builder@201c39983bf8:/home/builder/termux-packages/termux-root-packages/packages/termshark/build.sh` ## Snapcraft Fork Mario's termshark-snap repository: https://github.com/mharjac/termshark-snap (@mharjac) and clone it to a recentish Linux. Edit `snapcraft.yaml`. Change `version:` and edit this section to use the correct hash - this one corresponds to v2.0.3: ``` go get github.com/gcla/termshark/v2/cmd/termshark@73dfd1f6cb8c553eb524ebc27d991f637c1ac5ea ``` From a shell, type ``` snapcraft ``` If you have prior snapcraft builds of termshark, you might need ``` snapcraft clean ``` first. On my 19.10 machine, I ran into snapcraft failures that resolved when I simply ran `snapcraft` again... When this succeeds, the working directory should have a file `termshark_2.0.3_amd64.snap`. To install this - to test it out - try this: ``` snap install --dangerous ./termshark_2.0.3_amd64.snap ``` then to run it: ``` /snap/bin/termshark -h ``` Check your changes in and submit a PR to Mario (@mharjac). termshark-2.4.0/docs/Packages.md000066400000000000000000000050311426312004500165010ustar00rootroot00000000000000# Install Packages Here's how to install termshark on various OSes and with various package managers. ## Arch Linux - [termshark](https://archlinux.org/packages/community/x86_64/termshark/): The official package. - [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 ``` ## MacPorts ```bash sudo port selfupdate sudo port install termshark ``` ## Kali Linux ```bash apt update apt install termshark ``` ## NixOS Thanks to [Patrick Winter](https://github.com/winpat) ```bash nix-channel --add https://nixos.org/channels/nixpkgs-unstable nix-channel --update nix-env -iA nixpkgs.termshark ``` ## SnapCraft Thanks to [mharjac](https://github.com/mharjac) Termshark can be easily installed on almost all major distros just by issuing: ```bash snap install termshark ``` Note there is a big caveat with Snap and the architecture of Wireshark that prevents termshark being able to read network interfaces. If installed via Snap, termshark will only be able to work with pcap files. See [this explanation](https://forum.snapcraft.io/t/wireshark-and-setcap/9629/6). ## Termux (Android) ```bash pkg install root-repo pkg install termshark ``` Note that termshark does not require a rooted phone to inspect a pcap, but it does depend on tshark which is itself in Termux's root-repo for programs that do work best on a rooted phone. If you would like to use termshark's copy-mode to copy sections of packets to your Android clipboard, you will also need [Termux:API](https://play.google.com/store/apps/details?id=com.termux.api&hl=en_US). Install from the Play Store, then from termux, type: ```bash pkg install termux-api ``` ![device art](/../gh-pages/images/device art.png?raw=true) ## Ubuntu If you are running Ubuntu 19.10 (eoan) or higher, termshark can be installed like this: ```bash sudo apt install termshark ``` For Ubuntu < 19.10, you can use the PPA _nicolais/termshark_ to install termshark: ```bash sudo add-apt-repository --update ppa:nicolais/termshark sudo apt install termshark ``` Thanks to [Nicolai Søberg](https://github.com/NicolaiSoeborg) termshark-2.4.0/docs/UserGuide.md000066400000000000000000001320361426312004500166650ustar00rootroot00000000000000# Termshark User Guide Termshark provides a terminal-based user interface for analyzing packet captures. ## Table of Contents - [Table of Contents](#table-of-contents) - [Basic Usage](#basic-usage) - [Choose a Source](#choose-a-source) - [Reading from an Interface](#reading-from-an-interface) - [Read a pcap File](#read-a-pcap-file) - [Changing Files](#changing-files) - [Reading from a fifo or stdin](#reading-from-a-fifo-or-stdin) - [Using the TUI](#using-the-tui) - [Filtering](#filtering) - [Changing Views](#changing-views) - [Packet List View](#packet-list-view) - [Packet Structure View](#packet-structure-view) - [Packet Hex View](#packet-hex-view) - [Marking Packets](#marking-packets) - [Searching Packets](#searching-packets) - [Copy Mode](#copy-mode) - [Packet Capture Information](#packet-capture-information) - [Stream Reassembly](#stream-reassembly) - [Conversations](#conversations) - [Columns](#columns) - [Command-Line](#command-line) - [Macros](#macros) - [Transfer a pcap File](#transfer-a-pcap-file) - [Configuration](#configuration) - [Profiles](#profiles) - [Dark Mode](#dark-mode) - [Packet Colors](#packet-colors) - [Themes](#themes) - [Config File](#config-file) - [Troubleshooting](#troubleshooting) ## Basic Usage Termshark is inspired by Wireshark, and depends on tshark for all its intelligence. Termshark is run from the command-line. You can see its options with ```console $ termshark -h termshark v2.4.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 [FilterOrPcap] Application Options: -i= Interface(s) to read. -r= Pcap file/fifo to read. Use - for stdin. -w= Write raw packet data to outfile. -d===, Specify dissection of layer type. -D Print a list of the interfaces on which termshark can capture. -Y= Apply display filter. -f= Apply capture filter. -t=[a|ad|adoy|d|dd|e|r|u|ud|udoy] Set the format of the packet timestamp printed in summary lines. --tty= Display the UI on this terminal. -C, --profile= Start with this configuration profile. --pass-thru=[auto|true|false] Run tshark instead (auto => if stdout is not a tty). (default: auto) --log-tty Log to the terminal. -h, --help Show this help message. -v, --version Show version information. Arguments: FilterOrPcap: Filter (capture for iface, display for pcap), or pcap to read. If --pass-thru is true (or auto, and stdout is not a tty), tshark will be executed with the supplied command-line flags. You can provide tshark-specific flags and they will be passed through to tshark (-n, -d, -T, etc). For example: $ termshark -r file.pcap -T psml -n | less ``` By default, termshark will launch an ncurses-like application in your terminal window, but if your standard output is not a tty, termshark will simply defer to tshark and pass its options through: ```console $ termshark -r test.pcap | cat 1 0.000000 192.168.44.123 → 192.168.44.213 TFTP 77 Read Request, File: C:\IBMTCPIP\lccm.1, Transfer type: octet 2 0.000000 192.168.44.123 → 192.168.44.213 TFTP 77 Read Request, File: C:\IBMTCPIP\lccm.1, Transfer type: octet ``` ## Choose a Source ### Reading from an Interface Launch termshark like this to read from an interface: ```bash termshark -i eth0 ``` By default, termshark will save the packets - e.g. to `~/.cache/termshark/pcaps/` on Linux. If you use the `-w` flag, you can save them to your own file: ```bash termshark -i eth0 -w save.pcap ``` 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. The UI will show the capture filter in parentheses at the top, after the name of the packet source. Termshark supports reading from more than one interface at a time: ```bash termshark -i eth0 -i eth1 ``` Once packets are detected, termshark's UI will launch and the packet views will update as packets are read: ![readiface](/../gh-pages/images/readiface.png?raw=true) You can apply a display filter while the packet capture process is ongoing - termshark will dynamically apply the filter without restarting the capture. Press `ctrl-c` to stop the capture process. When you exit termshark, it will print a message with the location of the pcap file that was captured: ```console $ termshark -i eth0 Packets read from interface eth0 have been saved in /home/gcla/.cache/termshark/pcaps/eth0--2021-09-03--11-20-58.pcap ``` ### Read a pcap File Launch termshark like this to inspect a file: ```bash termshark -r test.pcap ``` You can also apply a display filter directly from the command-line: ```bash termshark -r test.pcap icmp ``` Note that when reading a file, the filter will be interpreted as a [display filter](https://wiki.wireshark.org/DisplayFilters). When reading from an interface, the filter is interpreted as a [capture filter](https://wiki.wireshark.org/CaptureFilters). This follows tshark's behavior. Termshark will launch in your terminal. From here, you can press `?` for help: ![tshelp](/../gh-pages/images/tshelp.png?raw=true) #### Changing Files Termshark provides a "Recent" button which will open a menu with your most recently-loaded pcap files. Each invocation of termshark with the `-r` flag will add a pcap to the start of this list: ![recent](/../gh-pages/images/recent.png?raw=true) ### Reading from a fifo or stdin Termshark supports reading packets from a Unix fifo or from standard input - for example ```bash tcpdump -i eth0 -w - icmp | termshark ``` On some machines, packet capture commands might require sudo or root access. To facilitate this, termshark's UI will not launch until it detects that it has received some packet data on its input. This makes it easier for the user to type in his or her root password on the tty before termshark takes over: ```console $ sudo tcpdump -i eth0 -w - icmp | termshark (The termshark UI will start when packets are detected...) [sudo] password for gcla: ``` If the termshark UI is active in the terminal but you want to see something displayed there before termshark started, you can now issue a SIGTSTP signal (on Unix) and termshark will suspend itself and give up control of the terminal. In bash, this operation is usually bound to `ctrl-z`. ```console $ termshark -r foo.pcap [1]+ Stopped termshark -r foo.pcap $ ``` Type `fg` to resume termshark. Another option is to launch termshark in its own tty. You could do this using a split screen in tmux. In one pane, type ```bash tty && sleep infinity ``` If the output is e.g. `/dev/pts/10`, then you can launch termshark in the other tmux pane like this: ```bash termshark -r foo.pcap --tty=/dev/pts/10 ``` Issue a sleep in the pane for `/dev/pts/10` so that no other process reads from the terminal while it is dedicated to termshark. ## Using the TUI ### Filtering Press `/` to focus on the display filter. Now you can type in a Wireshark display filter expression. The UI will update in real-time to display the validity of the current expression. If the expression is invalid, the filter widget will change color to red. As you type, termshark presents a drop-down menu with possible completions for the current term: ![filterbad](/../gh-pages/images/filterbad.png?raw=true) When the filter widget is green, you can hit the "Apply" button to make its value take effect. Termshark will then reload the packets with the new display filter applied. ![filterbad](/../gh-pages/images/filterbad.png?raw=true) ### Changing Views Press `tab` or `ctrl-w ctrl-w` to move between the three packet views. You can also use the mouse to move views by clicking with the left mouse button. When focus is in any of these three views, hit the `\` key to maximize that view: ![max](/../gh-pages/images/max.png?raw=true) Press `\` to restore the original layout. Press `|` to move the hex view to the right-hand side: ![altview](/../gh-pages/images/altview.png?raw=true) You can also press `<`,`>`,`+` and `-` to change the relative size of each view. To reset termshark to use its original relative sizes, hit `ctrl-w =`. All termshark views support vim-style navigation with `h`, `j`, `k` and `l` along with regular cursor keys. ### Packet List View Termshark's top-most view is a list of packets read from the capture (or interface). Termshark generates the data by running `tshark` on the input with the `-T psml` options, and parsing the resulting XML. Currently the columns displayed cannot be configured, and are the same as Wireshark's defaults. When the source is a pcap file, the list can be sorted by column by clicking the button next to each column header: ![sortcol](/../gh-pages/images/sortcol.png?raw=true) You can hit `home` or `gg` to jump to the top of the list and `end` or `G` to jump to the bottom. You can jump to a specific packet by entering its number - as a prefix - before hitting `gg` or `G` Sometimes, especially if running on a small terminal, the values in a column will be truncated (e.g. long IPv6 addresses). To see the full value, move the purple cursor over the value: ![ipv6](/../gh-pages/images/ipv6.png?raw=true) ### Packet Structure View Termshark's middle view shows the structure of the packet selected in the list view. You can expand and contract the structure using the `[+]` and `[-]` buttons, the 'enter' key, or the right and left cursor keys: ![structure](/../gh-pages/images/structure.png?raw=true) As you navigate the packet structure, different sections of the bottom view - a hex representation of the packet - will be highlighted. The currently selected line in this view will display a small button at the right hand-side. This button opens a contextual menu from which you can add a custom column or apply a filter based on the current level of the packet structure. ![pdmlmenu](/../gh-pages/images/termshark-pdml-menu.png?raw=true) ### Packet Hex View Termshark's bottom view shows the bytes that the packet comprises. Like Wireshark, they are displayed in a hexdump-like format. As you move around the bytes, the middle (structure) view will update to show you where you are in the packet's structure. ### Marking Packets To make it easier to compare packets, you can mark a packet in the packet list view and then jump back to it later. Termshark's marks are modeled on vim's. Set a mark by navigating to the packet and then hit `m` followed by a letter - `a` through `z`. ![marks1](/../gh-pages/images/marks1.png?raw=true) To jump back to that mark, hit `'` followed by the letter you selected. To jump back to the packet that was selected prior to your jump, hit `''`. When you exit termshark or load a new pcap, these marks are deleted; but termshark also supports cross-pcap marks which are saved in termshark's config file. To make a cross-pcap mark, hit `m` followed by a capital letter - `A` through `Z`. If you jump to a cross-pcap mark made in another pcap, termshark will load that pcap back up. To display your current marks, use the [command-line](#command-line) `marks` command: ![marks2](/../gh-pages/images/marks2.png?raw=true) ### Searching Packets To search within packets, hit `ctrl-f` to open termshark's search bar. The options provided closely mirror those available with Wireshark. The first button displays a menu that lets you choose the type of data searched: - Packet List - the info shown in the packet list view (by default the top data pane) - Packet Struct - the info shown in the packet struct view (by default the middle data pane) - Packet Bytes - the info shown in the packet hex view (by default the bottom data pane) The second button lets you choose what to search for: - String (with or without case sensitivity) - Regex (with or without case sensitivity) - Hex - Display Filter The Hex syntax follows Wireshark and requires a sequence of 2 hex-digits, concatenated. For example, entering "AF054c" would mean to search for the following 3 bytes, consecutively - 175, 5, 76. Display Filter search is a special case and does not search the packet data directly. Instead, termshark launches a tshark process on the current pcap source with flags to apply the user's search filter. Termshark parses the output and every packet that appears in the PSML data is a match for the search. If a match is found, termshark will navigate to the match location in the UI. For a Packet List search, the matching row and column are selected. For a Packet Struct search, the matching element in the packet structure view is expanded and the UI centered around it. For a Packet Bytes search, the cursor is moved to the start of the match in the packet hex view. To terminate the search early, hit `ctrl-c`. To set focus on the search bar's input, hit `ctrl-f` again. To close the search bar, hit `ctrl-f` one more time. ![search1](/../gh-pages/images/search1.png?raw=true) ### Copy Mode Both the structure and hex view support "copy mode" a feature which lets you copy ranges of data from the currently selected packet. First, move focus to the part of the packet you wish to copy. Now hit the `c` key - a section of the packet will be highlighted in yellow: ![copymode1](/../gh-pages/images/copymode1.png?raw=true) You can hit the `left` and `right` arrow keys to expand or contract the selected region. Now hit `ctrl-c` to copy. Termshark will display a dialog showing you the format in which you can copy the data: ![copymode2](/../gh-pages/images/copymode2.png?raw=true) Select the format you want and hit `enter` (or click). Copy mode is available in the packet structure and packet hex views. This feature comes with a caveat! If you are connected to a remote machine e.g. via ssh, then you should use the `-X` flag to forward X11. On Linux, the default copy command is `xsel`. If you forward X11 with ssh, then the packet data will be copied to your desktop machine's clipboard. You can customize the copy command using termshark's [config file](UserGuide.md#config-file) e.g. ```toml [main] copy-command = ["xsel", "-i", "-p"] ``` to instead set the primary selection. If forwarding X11 is not an option, you could instead upload the data (received via stdin) to a service like pastebin, and print the URL on stdout - termshark will display the copy command's output in a dialog when the command completes. See the [FAQ](FAQ.md). If you are running on OSX, termux (Android) or Windows, termshark assumes you are running locally and uses a platform-specific copy command. ### Packet Capture Information To show a summary of the information represented in the current pcap file, go to the "Analysis" menu and choose "Capture file properties". Termshark generates this information using the `capinfos` binary which is distributed with `tshark`. ![capinfos1](/../gh-pages/images/capinfos1.png?raw=true) ### Stream Reassembly Termshark is able to present reassembled TCP and UDP streams in a similar manner to Wireshark. In the packet list view, select a TCP or UDP packet then go to the "Analysis" menu and choose "Reassemble stream": ![streams1](/../gh-pages/images/streams1.png?raw=true) Termshark shows you: - A list of each client and server payload, in order, colored accordingly. - The number of client and server packets, and times the conversation switched sides. - A search box. - A button to display the entire conversation, only the client side, or only the server side. You can type a string in the search box and hit enter - or the Next button - to move through the matches. ![streams2](/../gh-pages/images/streams2.png?raw=true) Select Regex to instead have termshark interpret your search string as a regular expression. Because termshark is written in Golang, the regular expression uses Golang's regex dialect. [regex101](https://regex101.com/) provides a nice online way to experiment with matches. A quick tip - if you want your match to [cross line endings](https://stackoverflow.com/a/58318036/784226), prefix your search with `(?s)`. You can choose how to view the reassembled data by using the buttons at the bottom of the screen - ASCII, hex or Wireshark's raw format. Termshark will remember your preferred format. ![streams3](/../gh-pages/images/streams3.png?raw=true) Like Wireshark, you can filter the displayed data to show only the client-side or only the server-side of the conversation: ![streams4](/../gh-pages/images/streams4.png?raw=true) You can use Copy Mode in stream reassembly too. Hit the `c` key to enter Copy Mode. The currently selected "chunk" will be highlighted. Hit `ctrl-c` to copy that data. By default, termshark will copy the data to your clipboard. Hit the left arrow key to widen the data copied to the entire conversation (or filtered by client or server if that is selected). ![streams5](/../gh-pages/images/streams5.png?raw=true) Finally, clicking on a reassembled piece of the stream (enter or left mouse click) will cause termshark to select the underlying packet that contributed that payload. If you hit `q` to exit stream reassembly, termshark will set focus on the selected packet. ### Conversations To display a table of conversations represented in the current pcap, go to the "Analysis" menu and choose "Conversations". Termshark uses `tshark` to generate a list of conversations by protocol. Currently, termshark supports displaying Ethernet, IPv4, IPv6, UDP and TCP. ![convs1](/../gh-pages/images/convs1.png?raw=true) You can make termshark filter the packets displayed according to the current conversation selected. The "Prepare..." button will set termshark's display filter field, but *not* apply it, letting you futher edit it first. The "Apply..." button will set the display filter and apply it immediately. Navigate to the interesting conversation, then click either "Prepare..." or "Apply..." ![convs2](/../gh-pages/images/convs2.png?raw=true) In the first pop-up menu, you can choose how to extend the current display filter, if there is one. In the second pop-up menu, you can choose whether to filter by the conversation bidirectionally, unidirectionally, or just using the source or destination. These menus mirror those used in Wireshark. When you hit enter, the filter will be adjusted. Hit 'q' to quit the conversations screen. ![convs3](/../gh-pages/images/convs3.png?raw=true) ### Columns Like Wireshark, you can configure the columns that termshark displays. To do this, choose "Edit Columns" from the main menu, or type `columns` from the command-line. ![configcolumns](/../gh-pages/images/custom-columns.png?raw=true) From this dialog, you can: - Rearrange your current column set - Hide columns or make them visible - Delete and add columns - Give each of your columns a custom name If termshark can find your Wireshark config, it also offers the option of importing your Wireshark column set. Use the drop-down menus to choose the column type. If you select a custom column, termshark will require you to provide a valid display filter expression. Like Wireshark, the syntax of valid column expressions is a subset of those for display filters - essentially the disjunction (or) of filter fields. Note that termshark follows Wireshark and currently allows you to enter *any* valid display filter expression. Like Wireshark, the packet structure view allows you to quickly create custom columns. Navigate to the end of your chosen line of packet structure and select the `[=]` hamburger button. Based on the display filter expression that maps to this structure, you can create a custom column, or either prepare or directly set termshark's display filter. ![colfromstruct](/../gh-pages/images/vrrpcol1.png?raw=true) ### Command-Line For fast navigation around the UI, termshark offers a vim-style command-line. To activate the command-line, hit the ':' key: ![cmdline1](/../gh-pages/images/cmdline1.png?raw=true) Many of termshark's operations can be initiated from the command-line. After opening the command-line, hit tab to show all the commands available: - **capinfo** - Show the current capture file properties (using the `capinfos` command) - **clear-filter** - Clear the current display filter - **clear-packets** - Clear the current pcap - **columns** - Configure termshark's columns - **config** - Show termshark's config file (Unix-only) - **convs** - Open the conversations view - **filter** - Choose a display filter from those recently-used - **help** - Show one of several help dialogs - **load** - Load a pcap from the filesystem - **logs** - Show termshark's log file (Unix-only) - **map** - Map a keypress to a key sequence (see `help map`) - **marks** - Show file-local and global packet marks - **menu** - Open the UI menubar - **no-theme** - Clear theme for the current terminal color mode - **profile** - Profile actions - create, use, delete, etc - **quit** - Quit termshark - **recents** - Load a pcap from those recently-used - **set** - Set various config properties (see `help set`) - **streams** - Open the stream reassemably view - **theme** - Set a new termshark theme - **unmap** - Remove a keypress mapping made with the `map` command - **wormhole** - Transfer the current pcap using magic wormhole Some commands require a parameter or more. Candidate completions will be shown when possible; you can then scroll up or down through them and hit tab or enter to complete the candidate. Candidates are filtered as you type. Hit enter to run a valid command or hit `ctrl-c` to close the command-line. ### Vim Navigation Termshark lets you navigate the UI using familiar Vim key bindings and tries to apply other Vim concepts where it makes sense. All tabular views support Vim's `hjkl` navigation keys. Here is a list of other Vim-style bindings: - **gg** - Go to the top of the current table - **G** - Go to the bottom of the current table - **5gg** - Go to the 5th row of the table - **C-w C-w** - Switch panes (same as tab) - **C-w =** - Equalize pane spacing - **ma** - Mark current packet (use a through z) - **'a** - Jump to packet marked 'a' - **mA** - Mark current packet + pcap (use A through Z) - **'A** - Jump to packet + pcap marked 'A' - **''** - After a jump; jump back to prior packet - **ZZ** - Quit without confirmation The command-line supports some Vim shortcuts too e.g. `:q!` to quit immediately. ### Macros To support navigational shortcuts that are not directly built-in to the termshark UI, you can now create simple keyboard macros. These are modeled on vim's key mappings. To create a macro, open the [command-line](#command-line) and use the `map` command. The first argument is the key to map and the second argument is a sequence of keypresses that your first key should now map to. Termshark uses vim-syntax for keys. To express a keypress for a printable character, simply use the printable character. Here is the syntax for the other keys that termshark understands: - `` - `` - `` - `-` - modifiers - ``, `` - ``, ``, ``, `` - ``, `` - ``, `` Here are some example macros: - `map /` - hit ctrl-s to activate the display filter widget - `map :quit` - hit f1 to quit termshark without asking for confirmation - `map ZZ` - another way to quit quickly! - `map d` - toggle dark-mode A termshark user requested the ability to move up and down the packet list but to keep focus on the packet structure view. This can be accomplished by setting these macros: - `map ` - `map ` Then with focus on the packet structure view, hit `f5` to go down a packet and `f6` to go up a packet. Macros are saved in the termshark config file. To display the current list of macros, simply type `map` from the command-line with no arguments. ![macros](/../gh-pages/images/macros.png?raw=true) ### Transfer a pcap File Termshark can be convenient, but sometimes you need to get your current capture into Wireshark! Termshark integrates [wormhole-william](https://github.com/psanford/wormhole-william) to help you quickly transfer your current capture to your Wireshark machine using [magic wormhole](https://github.com/magic-wormhole/magic-wormhole). To start this process, choose "Send Pcap" from the "Misc" menu, or run "wormhole" from the termshark command-line: ![wormhole1](/../gh-pages/images/wormhole1.png?raw=true) Termshark will display the magic-wormhole code. On your Wireshark machine, use any magic-wormhole client to download using the code. For example: ``` $ wormhole receive 9-mosquito-athens Receiving file (2.8 MB) into: vrrp.pcap ok? (y/N): y Receiving (->tcp:10.6.14.67:45483).. 100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 2.83M/2.83M [00:00<00:00, 161MB/s] Received file written to vrrp.pcap ``` If you use tmux on your Wireshark machine and run termshark - locally or over ssh - from that tmux session, then you can download and open the pcap with a single keypress using [tmux-wormhole](https://github.com/gcla/tmux-wormhole), a tmux tpm plugin. Here's a demo: https://user-images.githubusercontent.com/45680/122692277-0de7e180-d202-11eb-964c-fbc4a2534255.mp4 ## Configuration ### Profiles Termshark v2.4 introduces support for Wireshark-like profiles. A simple mental model for a profile called "work" is a separate toml file, stored as `$XDG_CONFIG_HOME/termshark/profiles/work/termshark.toml`, whose settings take precedence over the default `termshark.toml` file. When a non-default profile is active, termshark will display its name at the top of the user-interface. Configuration changes are saved to the currently-active profile. These include typical termshark settings like dark-mode, columns and theme. To create a profile, open the command line and type `profile create`. Termshark will open a dialog that lets you name your profile and, optionally, "link" it to a Wireshark-profile. ![profilecreate](/../gh-pages/images/profilecreate.png?raw=true) The effect of linking to a Wireshark profile "wshark" is that termshark will run tshark with the flags `-C wshark`. One useful effect of this is that tshark will generate packet colors according to those configured for the "wshark" Wireshark profile. This allows Wireshark packet coloring rules to be used flexibly in termshark; create a new termshark profile and link to the Wireshark profile with the colors configured the way you want. Once a profile is created, you can unlink and re-link to a different Wireshark profile using the `profile unlink` and `profile link` commands respectively. Once you have profiles configured, you can switch between them using the `profile use` command. Termshark may reload your current packet source if it determines it needs to regenerate parts of the UI e.g. if packet colors or columns may be different. Note that if you create a new profile "new" while currently using profile "old", the settings for "new" are initialized from "old" (the toml file is copied); but then further changes apply to profile "new" only. Note that some configuration settings are read only from the default profile. These include - `main.term` - termshark will launch on this tty (Unix-only) ### Dark Mode If termshark is too bright for your taste, try dark-mode. To enable, hit Esc to open the main menu and select "Toggle Dark Mode". ![darkmode](/../gh-pages/images/darkmode.png?raw=true) Your choice is stored in the termshark [config file](UserGuide.md#config-file) for the currently active profile. Dark-mode is supported throughout the termshark user-interface. ### Packet Colors By default, termshark will now display packets in the packet list view colored according to Wireshark's default color rules. With recent installations of Wireshark, you can find this file at `$XDG_CONFIG_HOME/wireshark/colorfilters`. Termshark doesn't provide a way to edit the colors - the colors are provided by `tshark`. You can read about Wireshark's support [here](https://www.wireshark.org/docs/wsug_html_chunked/ChCustColorizationSection.html). If you don't like the way this looks in termshark, you can turn it off using termshark's main menu. ### Themes Termshark can be themed to better line up with other terminal applications that you use. Most of termshark's UI elements have names and you can tie colors to these names. Here is an example theme: ```toml [dracula] gray1 = "#464752" ... orange = "#ffb86c" purple = "#bd93f9" red = "#ff5555" white = "#f8f8f2" yellow = "#f1fa8c" [dark] button = ["dracula.black","dracula.gray3"] button-focus = ["dracula.white","dracula.magenta"] button-selected = ["dracula.white","dracula.gray3"] ... [light] button = ["dracula.black","dracula.white"] button-focus = ["dracula.black","dracula.purple"] button-selected = ["dracula.black","dracula.gray3"] ... ``` Termshark finds themes in two ways - from: - `$XDG_CONFIG_HOME/termshark/themes/*.toml` (e.g. `~/.config/termshark/themes/dracula.toml`) - from a small database compiled-in to the termshark binary. The termshark command-line provides two commands to interact with themes: - `theme` - choose a new theme from those provided and apply it. - `no-theme` - use no theme. Termshark saves your selected theme against the terminal color mode, which can be one of - 16-color - 256-color - truecolor i.e. 24-bit color The theme is saved in the current profile's `termshark.toml` under, respectively, the keys: - `main.theme-16` - `main.theme-256` - `main.theme-truecolor` This means that if you run termshark on the same machine but with a different terminal emulator, you might need to re-apply the theme if the color mode has changed (e.g. `xterm` v `gnome terminal`) If you are running in truecolor/24-bit color, termshark will make the 256-color themes available too. Terminal emulators that support 24-bit color will support 256-color mode as well. If you have enabled termshark's packet colors - shown in the packet list view - then these colors will be reproduced faithfully according to Wireshark's rules. These colors don't adhere to termshark's themes. #### Built-in Themes and Base16 Termshark has four themes built-in: - `default` - termshark's original color scheme (16-color, 256-color, truecolor) - `dracula` - colors based on [Dracula theme](https://draculatheme.com/) project (256-color, truecolor) - `solarized` - based on [Ethan Schoonover's](https://ethanschoonover.com/solarized/) work (256-color, truecolor) - `base16` - (256-color, truecolor) If you make another, please submit it! :-) [Base16](https://github.com/chriskempson/base16) is a set of guidelines for building themes using a limited range of 8 colors and 8 grays. The [base16-shell](https://github.com/chriskempson/base16-shell) project is a set of scripts that remap colors 0-21 in the 256-color space of a terminal emulator. If you're in 256-color mode, this lets you have consistent coloring of your work in a terminal emulator, whether it's typing at the shell, or running a TUI. If you use base16-shell, choose termshark's `base16` theme to make use of your shell theme's colors. #### Make a Theme The layout of a theme is: - the color definitions - `[mytheme]` - set the foreground and background color of UI elements for - dark mode - `[dark]` - regular/light mode - `[light]` Here's an [example theme](https://raw.githubusercontent.com/gcla/termshark/master/assets/themes/dracula-256.toml) to follow. This [tcell source file](https://github.com/gdamore/tcell/blob/fcaa20f283682d6bbe19ceae067b37df3dc699d7/color.go#L821) shows some sample color names you can use. The UI elements are listed in the `[dark]` and `[light]` sections. Each element is assigned a pair of colors - foreground and background. The colors can be: - a reference to another field in the theme toml e.g. `dracula.black` - a color that termshark understands natively e.g. `#ffcc43`, `dark green`, `g50` (medium gray). Save your theme toml file under `~/.config/termshark/themes/` with a suffix indicating the color-mode e.g. `mytheme-256.toml`. If your theme is a truecolor theme (suffix `-truecolor.toml`), then RGB colors will be reproduced precisely by termshark and so by the terminal emulator. If your theme is a 256-color theme (suffix `-256.toml`), you can still use RGB colors in your toml, and termshark will then try to pick the closest matching color in the 256-color space. If termshark detects you are using base16-shell, then it will ignore colors 0-21 when choosing the closest match, since these will likely be remapped by the base16-shell theme. Hopefully the meaning of the UI-element names is guessable, but one detail to know is the difference between focus, selected and unselected. In a gowid application, one widget at a time will have "focus". For example, if you are navigating the packet's tree structure (the middle pane), one level of that protocol structure will be shown in blue, and will be the focus widget. If you hit tab to move to the hex view of the packet's bytes (the lower pane), then focus will move to the hex byte under the cursor; but the previously blue protocol structure in the middle pane will still be obvious, shown in grey. That protocol level is now "selected", but not in "focus". So selected is a way to highlight a widget in a container of widgets that will have focus when control returns to the container. Unselected means neither focus nor selected. ### Config File Termshark reads options from a TOML configuration file saved in `$XDG_CONFIG_HOME/termshark/termshark.toml` (e.g. `~/.config/termshark/termshark.toml` on Linux) if you are using the default profile. If you are using a profile called "work", the settings are saved in `$XDG_CONFIG_HOME/termshark/profiles/work/termshark.toml`. All options are saved under the `[main]` section. The available options are: - `always-keep-pcap` (bool) - if true, and if termshark is run on a live packet source (`-i`), when termshark is asked to exit, it will not prompt the user to choose whether to keep or delete the capture. - `auto-scroll` (bool) - if true, termshark will automatically scroll down when packets are read in a live-capture mode (e.g. `-i eth0`) - `browse-command` (string list) - termshark will run this command with a URL e.g. when the user selects "FAQ" from the main menu. Any argument in the list that equals `$1` will be replaced by the URL prior to the command being run e.g. ```toml [main] browse-command = ["firefox", "$1"] ``` - `capinfos` (string) - make termshark use this specific `capinfos` binary (for pcap properties). - `capture-command` (string) - use this binary to capture packets, passing `-i`, `-w` and `-f` flags. - `color-tsharks` (string list) - a list of the paths of tshark binaries that termshark has confirmed support the `--color` flag. If you run termshark and the selected tshark binary is not in this list, termshark will check to see if it supports the `--color` flag. - `colors` (bool) - if true, and tshark supports the feature, termshark will colorize packets in its list view. - `column-format` (string list) - a list of columns, each a group of three strings: field name, display name, and visibility. - `column-format-bak` (string list) - the value of `column-format` prior to its last change; for restoring previous settings. - `conv-absolute-time` (bool) - if true, have tshark provide conversation data with a relative start time field. - `conv-resolve-names` (bool) - if true, have tshark provide conversation data with ethernet names resolved. - `conv-use-filter` (bool) - if true, have tshark provide conversation data limited to match the active display filter. - `conv-types` (string list) - a list of the conversation types termshark will query for and display in the conversations view. Currently limited to `eth`, `ip`, `ipv6`, `udp`, `tcp`. - `copy-command` (string) - the command termshark executes when the user hits `ctrl-c` in copy-mode. The default commands on each platform will copy the selected area to the clipboard. ```toml [main] copy-command = ["xsel", "-i", "-b"] ``` - `copy-command-timeout` (int) - how long termshark will wait (in seconds) for the copy command to complete before reporting an error. - `dark-mode` (bool) - if true, termshark will run in dark-mode. - `debug` (bool) - if true, run a debug web-server on http://localhost:6060. Shows termshark/golang internals - in case of a problem. - `disable-shark-fin` (bool) - if true then turn off the shark-fin screen-saver permanently. - `disable-term-helper` (bool) - if true then don't try to nudge the user towards a 256-color TERM; run as-is. - `disk-cache-size-mb` (int) - how large termshark will allow `$XDG_CACHE_HOME/termshark/pcaps/` to grow; if the limit is exceeded, termshark will delete pcaps, oldest first. Set to -1 to disable (grow indefinitely). - `dumpcap` (string) - make termshark use this specific `dumpcap` (used when reading from an interface). - `ignore-base16-colors` (bool) - if true, when running in a terminal with 256-colors, ignore colors 0-21 in the 256-color-space when choosing the best match for a theme's RGB (24-bit) color. This avoids choosing colors that are remapped using e.g. [base16-shell](https://github.com/chriskempson/base16-shell). - `key-mappings` (string list) - a list of macros, where each string contains a vim-style keypress, a space, and then a sequence of keypresses. - `marks` (string json) - a serialized json structure representing the cross-pcap marks - for each, the keypress (`A` through `Z`); the pcap filename; the packet number; and a short summary of the packet. - `packet-colors` (bool) - if true (or missing), termshark will colorize packets according to Wireshark's rules. - `pager` (string) - the pager program to use when displaying termshark's log file - run like this: `sh -c " termshark.log"` - `pcap-bundle-size` - (int) - load tshark PDML this many packets at a time. Termshark will lazily load PDML because it's a slow process and uses a lot of RAM. For example, if `pcap-bundle-size`=1000, then on first loading a pcap, termshark will load PDML for packets 1-1000. If you scroll past packet 500, termshark will optimistically load PDML for packets 1001-2000. A higher value will make termshark load more packets at a time; a value of 0 means load the entire pcap's worth of PDML. Termshark stores the data compressed in RAM, but expect approximately 10MB per 1000 packets loaded. If you have the memory, can wait a minute or two for the entire pcap to load, and e.g. plan to use the packet list header to sort the packets in various ways, setting `pcap-bundle-size` to 0 will provide the best experience. - `pcap-cache-dir` - (string) - if `use-tshark-temp-for-pcap-cache` is false, when termshark is run on a live packet source (`-i`), the captured packets will be saved here. - `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. - `respect-colorterm` (bool) - if termshark detects you are using base16-shell, it won't map any theme RGB color names (like #90FF32) to 0-21 in the 256-color space to avoid clashes with the active base16 theme. This shouldn't affect color reproduction if the terminal is 24-bit capable, but some terminal emulators (e.g. gnome-terminal) seem to use the 256-color space anyway. Termshark works around this by falling back to 256-color mode, interpolating RGB colors into the 256-color space and avoiding 0-21. If you really want termshark to run in 24-bit color mode anyway, set this to true. - `search-type` - (string) - how to interpret the user's packet search term; one of `filter`, `hex`, `string` or `regex`. - `search-target` - (string) - the type of packet data to search (unless `search-type` is `filter`); one of `list`, `details` or `bytes`. - `search-case-sensitive` - (bool) - true if the user's packet search should be sensitive to the case of the search term. - `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". - `suppress-tshark-errors` (bool) - if `true`, hide from the UI any errors generated during parsing of tshark-generated XML. - `tail-command` (string) - make termshark use this specific `tail` command. This is used when reading from an interface in order to feed `dumpcap`-saved data to `tshark`. The default is `tail -f -c +0 `. If you are running on Windows, the default is to use `termshark` itself with a special hidden `--tail` flag. But probably better to use Wireshark on Windows :-) - `term` (string) - termshark will use this as a replacement for the TERM environment variable. ```toml [main] term = "screen-256color" ``` - `theme-8` (string) - the theme applied when termshark runs in a 8-color terminal. If absent, no theme is used. - `theme-16` (string) - the theme applied when termshark runs in a 16-color terminal. If absent, no theme is used. - `theme-256` (string) - the theme applied when termshark runs in a 256-color terminal. If absent, no theme is used. - `theme-truecolor` (string) - the theme applied when termshark runs in a terminal that supports 24-bit color. If absent, no theme is used. - `tshark` (string) - make termshark use this specific `tshark`. - `tshark-args` (string list) - these are added to each invocation of `tshark` made by termshark e.g. ```toml [main] tshark-args = ["-d","udp.port==2075,cflow]" ``` - `ui-cache-size` - (int) - termshark will remember the state of widgets representing packets e.g. which parts are expanded in the structure view, and which byte is in focus in the hex view. This setting allows the user to override the number of widgets that are cached. The default is 1000. - `use-tshark-temp-for-pcap-cache` - (bool) - if true, when termshark is run on a live packet source (`-i`), the captured packets will be saved in tshark's `Temp` folder (`tshark -G folders`). - `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). - `wormhole-length` - (int) - the number of words in the magic-wormhole code. - `wormhole-rendezvous-url` - (string) - the magic-wormhole rendezvous server to use. "The server performs store-and-forward delivery for small key-exchange and control messages." (https://github.com/magic-wormhole/magic-wormhole-mailbox-server). Omit to use the default. - `wormhole-transit-relay` - (string) - the magic-wormhole transit relay to use. "helps clients establish bulk-data transit connections even when both are behind NAT boxes" (https://github.com/magic-wormhole/magic-wormhole-transit-relay). Omit to use the default. ## Troubleshooting If termshark is running slowly or otherwise misbehaving, you might be able to narrow the issue down by using the `--debug` flag. When you start termshark with `--debug`, three things happen: 1. A web server runs with content available at [http://127.0.0.1:6060/debug/pprof](http://127.0.0.1:6060/debug/pprof) (or the remote IP). This is a Golang feature and provides a view of some low-level internals of the process such as running goroutines. 2. On receipt of SIGUSR1, termshark will start a Golang CPU profile that runs for 20 seconds. 3. On receipt of SIGUSR2, termshark will create a Golang memory/heap profile. Profiles are stored under `$XDG_CACHE_HOME/termshark` (e.g. `~/.cache/termshark/`). If you open a termshark issue on github, these profiles will be useful for debugging. For commonly asked questions, check out the [FAQ](/docs/FAQ.md). termshark-2.4.0/go.mod000066400000000000000000000025711426312004500146250ustar00rootroot00000000000000module github.com/gcla/termshark/v2 go 1.13 require ( github.com/adam-hanna/arrayOperations v0.2.6 github.com/antchfx/xmlquery v1.3.3 github.com/antchfx/xpath v1.1.11 // indirect github.com/blang/semver v3.5.1+incompatible github.com/flytam/filenamify v1.1.0 github.com/gcla/deep v1.0.2 github.com/gcla/gowid v1.4.0 github.com/gcla/tail v1.0.1-0.20190505190527-650e90873359 github.com/gcla/term v0.0.0-20220601234708-3e6af2ebff27 github.com/gdamore/tcell/v2 v2.5.0 github.com/gin-gonic/gin v1.7.0 // indirect github.com/go-test/deep v1.0.2 // indirect github.com/hashicorp/golang-lru v0.5.4 github.com/jessevdk/go-flags v1.4.0 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/mattn/go-isatty v0.0.14 github.com/mitchellh/go-homedir v1.1.0 github.com/mreiferson/go-snappystream v0.2.3 github.com/pkg/errors v0.9.1 github.com/psanford/wormhole-william v1.0.6-0.20210402190004-049df45b8d5a github.com/rakyll/statik v0.1.7 github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0 github.com/sirupsen/logrus v1.7.0 github.com/spf13/viper v1.12.0 github.com/stretchr/testify v1.7.1 github.com/tevino/abool v1.2.0 gitlab.com/jonas.jasas/condchan v0.0.0-20190210165812-36637ad2b5bc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a gopkg.in/fsnotify/fsnotify.v1 v1.4.7 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect ) termshark-2.4.0/go.sum000066400000000000000000003152411426312004500146530ustar00rootroot00000000000000cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA= github.com/adam-hanna/arrayOperations v0.2.6 h1:QZC99xC8MgUawXnav7bFMejs/dm7YySnDpMx3oZzz2Y= github.com/adam-hanna/arrayOperations v0.2.6/go.mod h1:iIzkSjP91FnE66cUFNAjjUJVmjyAwCH0SXnWsx2nbdk= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/antchfx/xmlquery v1.3.3 h1:HYmadPG0uz8CySdL68rB4DCLKXz2PurCjS3mnkVF4CQ= github.com/antchfx/xmlquery v1.3.3/go.mod h1:64w0Xesg2sTaawIdNqMB+7qaW/bSqkQm+ssPaCMWNnc= github.com/antchfx/xpath v1.1.10/go.mod h1:Yee4kTMuNiPYJ7nSNorELQMr1J33uOpXDMByNYhvtNk= github.com/antchfx/xpath v1.1.11 h1:WOFtK8TVAjLm3lbgqeP0arlHpvCEeTANeWZ/csPpJkQ= github.com/antchfx/xpath v1.1.11/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/araddon/dateparse v0.0.0-20210207001429-0eec95c9db7e h1:OjdSMCht0ZVX7IH0nTdf00xEustvbtUGRgMh3gbdmOg= github.com/araddon/dateparse v0.0.0-20210207001429-0eec95c9db7e/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= 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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cheggaaa/pb/v3 v3.0.1/go.mod h1:SqqeMF/pMOIu3xgGoxtPYhMNQP258xE4x/XRTYua+KU= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.15 h1:cKRCLMj3Ddm54bKSpemfQ8AtYFBhAI2MPmdys22fBdc= github.com/creack/pty v1.1.15/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 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/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/flytam/filenamify v1.1.0 h1:iEOcC/1UgxJf4lp2E2CWtYO7TMyYmgb2RPSTou89FLs= github.com/flytam/filenamify v1.1.0/go.mod h1:Dzf9kVycwcsBlr2ATg6uxjqiFgKGH+5SKFuhdeP5zu8= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= 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.3.1-0.20220603022106-4d04cba8013a h1:8kh1ORv/P5ruPPoZBMrXAxw406+o9Uvg4fPblsMfcUo= github.com/gcla/gowid v1.3.1-0.20220603022106-4d04cba8013a/go.mod h1:7T4Xzfznq31XvQyAOX+SZzQjvF7RX16mjXDXGB7R/44= github.com/gcla/gowid v1.3.1-0.20220626203558-a594805196f2 h1:lTVa5VHxePP+1aMijXK0+McQdHL6RFFciSFp6VZ+oEk= github.com/gcla/gowid v1.3.1-0.20220626203558-a594805196f2/go.mod h1:7T4Xzfznq31XvQyAOX+SZzQjvF7RX16mjXDXGB7R/44= github.com/gcla/gowid v1.3.1-0.20220626210608-5f3e0e82f1ad h1:3ST4tAhRrpAEU424Yr4uAKbMZ88JotTTuDViOLLVh/M= github.com/gcla/gowid v1.3.1-0.20220626210608-5f3e0e82f1ad/go.mod h1:7T4Xzfznq31XvQyAOX+SZzQjvF7RX16mjXDXGB7R/44= github.com/gcla/gowid v1.3.1-0.20220703181750-06abc4f10c26 h1:a1aC03ABo8O36DEmwPUHIcuIo1EFU8RDe+Rw0+M2fOc= github.com/gcla/gowid v1.3.1-0.20220703181750-06abc4f10c26/go.mod h1:7T4Xzfznq31XvQyAOX+SZzQjvF7RX16mjXDXGB7R/44= github.com/gcla/gowid v1.4.0 h1:sZRBh2gqO9EQAXQXrg//2iL4Di8CjFX5NoM66Ldlmig= github.com/gcla/gowid v1.4.0/go.mod h1:7T4Xzfznq31XvQyAOX+SZzQjvF7RX16mjXDXGB7R/44= 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/term v0.0.0-20220601234708-3e6af2ebff27 h1:OmzD2vZl1UeSblMmUo4mKjnrtsn+VLxhkkqnDOPHrXY= github.com/gcla/term v0.0.0-20220601234708-3e6af2ebff27/go.mod h1:clvpNvF78b5j4SoZwUykoKNbZwjlDmFeHGbKeixkScI= github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= github.com/gdamore/tcell/v2 v2.5.0 h1:/LA5f/wqTP5mWT79czngibKVVx5wOgdFTIXPQ68fMO8= github.com/gdamore/tcell/v2 v2.5.0/go.mod h1:wSkrPaXoiIWZqW/g7Px4xc79di6FTcpB8tvaKJ6uGBo= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= github.com/gin-gonic/gin v1.7.0 h1:jGB9xAJQ12AIGNB4HguylppmDK1Am9ppF7XnGXXJuoU= github.com/gin-gonic/gin v1.7.0/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 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/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/guptarohit/asciigraph v0.4.1/go.mod h1:9fYEfE5IGJGxlP1B+w8wHFy7sNZMhPtn59f0RLtpRFM= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= github.com/hashicorp/consul/api v1.12.0/go.mod h1:6pVBMo0ebnYdt2S3H87XhekM/HHrUoTD2XXb/VrZVy0= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4/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/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= github.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 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/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.11.13 h1:eSvu8Tmq6j2psUJqJrLcWH6K3w5Dwc+qipbaA6eVEN4= github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= 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/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 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/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU= github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= 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/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/psanford/wormhole-william v1.0.6-0.20210402190004-049df45b8d5a h1:IfxVpoAvmF0UXdBEQsa+5KZ7YlrjetPCT9biVvkYfKo= github.com/psanford/wormhole-william v1.0.6-0.20210402190004-049df45b8d5a/go.mod h1:Cz4DnYGGu/7Y8DZxmL0HsZ78EkyTYeKFwZJzF7FKCxw= github.com/rakyll/statik v0.1.6/go.mod h1:OEi9wJV/fMUAGx1eNjq75DKDsJVuEv1U0oYdX6GX8Zs= github.com/rakyll/statik v0.1.7 h1:OF3QCZUuyPxuGEP7B4ypUa7sB/iHtqOTDYZXGM8KOdQ= github.com/rakyll/statik v0.1.7/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Unghqrcc= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sagikazarmark/crypt v0.6.0/go.mod h1:U8+INwJo3nBv1m6A/8OBXAq7Jnpspk5AxSgDyEQcea8= github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= 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/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo= github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ= github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI= 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/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.3.0 h1:mjC+YW8QpAdXibNi+vNWgzmgBH4+5l5dCXv8cNysBLI= github.com/subosito/gotenv v1.3.0/go.mod h1:YzJjq/33h7nrwdY+iHMhEOEEbW0ovIz0tB6t6PwAXzs= github.com/tevino/abool v1.2.0 h1:heAkClL8H6w+mK5md9dzsuohKeXHUpY7Vw0ZCKW+huA= github.com/tevino/abool v1.2.0/go.mod h1:qc66Pna1RiIsPa7O4Egxxs9OqkuxDX55zznh9K07Tzg= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= gitlab.com/jonas.jasas/condchan v0.0.0-20190210165812-36637ad2b5bc h1:zCsu+odZEHb2f8U8WWhDgY5N5w3JCLHxuCIqVqCsLcQ= gitlab.com/jonas.jasas/condchan v0.0.0-20190210165812-36637ad2b5bc/go.mod h1:4JS8TdA7HSdK+x43waOdTGodqY/VKsj4w+8pWDL0E88= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v2 v2.305.4/go.mod h1:Ud+VUwIi9/uQHOMA+4ekToJ12lTxlv0zB/+DHwTGEbU= go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/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-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 h1:kUhD7nTDoI3fVd9G4ORWrbV5NY0liEs/Jg2pv5f+bBA= golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 h1:NWy5+hlRbC7HK+PmcXVUmW1IMyFce7to56IUvhUFm7Y= golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/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-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220318055525-2edf467146b5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 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/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU= google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= google.golang.org/api v0.81.0/go.mod h1:FA6Mb/bZxj706H2j+j2d6mHEEaHBmbbWnkfvmorOCko= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 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/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4= gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 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.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= nhooyr.io/websocket v1.8.6 h1:s+C3xAMLwGmlI31Nyn/eAehUlZPwfYZu2JXM621Q5/k= nhooyr.io/websocket v1.8.6/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= salsa.debian.org/vasudev/gospake2 v0.0.0-20180813171123-adcc69dd31d5 h1:j+F9fFxAFNdrO85XnERJSYS5QGfPUWUy9IP4s9BkV6A= salsa.debian.org/vasudev/gospake2 v0.0.0-20180813171123-adcc69dd31d5/go.mod h1:soKzqXBAtqHTODjyA0VzH2iERtpzN1w65eZUfetn2cQ= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= termshark-2.4.0/pkg/000077500000000000000000000000001426312004500142735ustar00rootroot00000000000000termshark-2.4.0/pkg/capinfo/000077500000000000000000000000001426312004500157125ustar00rootroot00000000000000termshark-2.4.0/pkg/capinfo/loader.go000066400000000000000000000102071426312004500175070ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. package capinfo import ( "bytes" "context" "fmt" "os/exec" "sync" "github.com/gcla/gowid" "github.com/gcla/termshark/v2" "github.com/gcla/termshark/v2/pkg/pcap" log "github.com/sirupsen/logrus" ) //====================================================================== var Goroutinewg *sync.WaitGroup //====================================================================== type ILoaderCmds interface { Capinfo(pcap string) pcap.IPcapCommand } type commands struct{} func MakeCommands() commands { return commands{} } var _ ILoaderCmds = commands{} func (c commands) Capinfo(pcapfile string) pcap.IPcapCommand { args := []string{pcapfile} return &pcap.Command{ Cmd: exec.Command(termshark.CapinfosBin(), args...), } } //====================================================================== type Loader struct { cmds ILoaderCmds SuppressErrors bool // if true, don't report process errors e.g. at shutdown mainCtx context.Context // cancelling this cancels the dependent contexts mainCancelFn context.CancelFunc capinfoCtx context.Context capinfoCancelFn context.CancelFunc capinfoCmd pcap.IPcapCommand } func NewLoader(cmds ILoaderCmds, ctx context.Context) *Loader { res := &Loader{ cmds: cmds, } res.mainCtx, res.mainCancelFn = context.WithCancel(ctx) return res } func (c *Loader) StopLoad() { if c.capinfoCancelFn != nil { c.capinfoCancelFn() } } //====================================================================== type ICapinfoCallbacks interface { OnCapinfoData(data string) AfterCapinfoEnd(success bool) } func (c *Loader) StartLoad(pcap string, app gowid.IApp, cb ICapinfoCallbacks) { termshark.TrackedGo(func() { c.loadCapinfoAsync(pcap, app, cb) }, Goroutinewg) } func (c *Loader) loadCapinfoAsync(pcapf string, app gowid.IApp, cb ICapinfoCallbacks) { c.capinfoCtx, c.capinfoCancelFn = context.WithCancel(c.mainCtx) procChan := make(chan int) pid := 0 defer func() { if pid == 0 { close(procChan) } }() c.capinfoCmd = c.cmds.Capinfo(pcapf) termChan := make(chan error) termshark.TrackedGo(func() { var err error cmd := c.capinfoCmd cancelledChan := c.capinfoCtx.Done() procChan := procChan state := pcap.NotStarted kill := func() { err := termshark.KillIfPossible(cmd) if err != nil { log.Infof("Did not kill tshark capinfos process: %v", err) } } loop: for { select { case err = <-termChan: state = pcap.Terminated if !c.SuppressErrors && err != nil { if _, ok := err.(*exec.ExitError); ok { pcap.HandleError(pcap.CapinfoCode, app, pcap.MakeUsefulError(c.capinfoCmd, err), cb) } } case pid := <-procChan: procChan = nil if pid != 0 { state = pcap.Started if cancelledChan == nil { kill() } } case <-cancelledChan: cancelledChan = nil if state == pcap.Started { kill() } } if state == pcap.Terminated || (procChan == nil && state == pcap.NotStarted) { break loop } } }, Goroutinewg) capinfoOut, err := c.capinfoCmd.StdoutReader() if err != nil { pcap.HandleError(pcap.CapinfoCode, app, err, cb) return } defer func() { cb.AfterCapinfoEnd(true) }() app.Run(gowid.RunFunction(func(app gowid.IApp) { pcap.HandleBegin(pcap.CapinfoCode, app, cb) })) defer func() { app.Run(gowid.RunFunction(func(app gowid.IApp) { pcap.HandleEnd(pcap.CapinfoCode, app, cb) })) }() err = c.capinfoCmd.Start() if err != nil { err = fmt.Errorf("Error starting capinfo %v: %v", c.capinfoCmd, err) pcap.HandleError(pcap.CapinfoCode, app, err, cb) return } log.Infof("Started capinfo command %v with pid %d", c.capinfoCmd, c.capinfoCmd.Pid()) termshark.TrackedGo(func() { termChan <- c.capinfoCmd.Wait() }, Goroutinewg) pid = c.capinfoCmd.Pid() procChan <- pid buf := new(bytes.Buffer) buf.ReadFrom(capinfoOut) cb.OnCapinfoData(buf.String()) c.capinfoCancelFn() } //====================================================================== // Local Variables: // mode: Go // fill-column: 78 // End: termshark-2.4.0/pkg/cli/000077500000000000000000000000001426312004500150425ustar00rootroot00000000000000termshark-2.4.0/pkg/cli/all.go000066400000000000000000000071031426312004500161420ustar00rootroot00000000000000// Copyright 2019-2022 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)."` Profile string `long:"profile" short:"C" description:"Start with this configuration profile." value-name:""` PrintIfaces bool `short:"D" optional:"true" optional-value:"true" description:"Print a list of the interfaces on which termshark can capture."` TailSwitch } // Termshark's own command line arguments. Used if we don't pass through to tshark. type Termshark struct { Ifaces []string `value-name:"" short:"i" description:"Interface(s) to read."` Pcap flags.Filename `value-name:"" short:"r" description:"Pcap file/fifo to read. Use - for stdin."` WriteTo flags.Filename `value-name:"" short:"w" description:"Write raw packet data to outfile."` DecodeAs []string `short:"d" description:"Specify dissection of layer type." value-name:"==,"` PrintIfaces bool `short:"D" optional:"true" optional-value:"true" description:"Print a list of the interfaces on which termshark can capture."` DisplayFilter string `short:"Y" description:"Apply display filter." value-name:""` 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 Profile string `long:"profile" short:"C" description:"Start with this configuration profile." value-name:""` PassThru string `long:"pass-thru" default:"auto" optional:"true" optional-value:"true" choice:"auto" choice:"true" choice:"false" description:"Run tshark instead (auto => if stdout is not a tty)."` LogTty bool `long:"log-tty" optional:"true" optional-value:"true" choice:"true" choice:"false" description:"Log to the terminal."` Debug TriState `long:"debug" default:"unset" hidden:"true" optional:"true" optional-value:"true" description:"Enable termshark debugging. See https://termshark.io/userguide."` Help bool `long:"help" short:"h" optional:"true" optional-value:"true" description:"Show this help message."` Version []bool `long:"version" short:"v" optional:"true" optional-value:"true" description:"Show version information."` Args struct { FilterOrPcap string `value-name:"" description:"Filter (capture for iface, display for pcap), or pcap to read."` } `positional-args:"yes"` } // If args are passed through to tshark (e.g. stdout not a tty), then // strip these out so tshark doesn't fail. var TermsharkOnly = []string{"--pass-thru", "--profile", "--log-tty", "--debug", "--tail"} func FlagIsTrue(val string) bool { return val == "true" || val == "yes" } //====================================================================== // Local Variables: // mode: Go // fill-column: 78 // End: termshark-2.4.0/pkg/cli/flags.go000066400000000000000000000014231426312004500164650ustar00rootroot00000000000000// Copyright 2019-2022 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.4.0/pkg/cli/flags_windows.go000066400000000000000000000014321426312004500202370ustar00rootroot00000000000000// Copyright 2019-2022 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.4.0/pkg/cli/tristate.go000066400000000000000000000015701426312004500172330ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // package cli //====================================================================== type TriState struct { Set bool Val bool } func (b *TriState) UnmarshalFlag(value string) error { switch value { case "true", "TRUE", "t", "T", "1", "y", "Y", "yes", "Yes", "YES": b.Set = true b.Val = true case "false", "FALSE", "f", "F", "0", "n", "N", "no", "No", "NO": b.Set = true b.Val = false default: b.Set = false } return nil } func (b TriState) MarshalFlag() string { if b.Set { if b.Val { return "true" } else { return "false" } } else { return "unset" } } //====================================================================== // Local Variables: // mode: Go // fill-column: 78 // End: termshark-2.4.0/pkg/confwatcher/000077500000000000000000000000001426312004500165765ustar00rootroot00000000000000termshark-2.4.0/pkg/confwatcher/confwatcher.go000066400000000000000000000044371426312004500214400ustar00rootroot00000000000000// Copyright 2019-2022 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 confwatcher import ( "os" "sync" "github.com/gcla/termshark/v2" log "github.com/sirupsen/logrus" fsnotify "gopkg.in/fsnotify/fsnotify.v1" ) //====================================================================== type ConfigWatcher struct { watcher *fsnotify.Watcher change chan struct{} closech chan struct{} closeWait sync.WaitGroup } func New() (*ConfigWatcher, error) { watcher, err := fsnotify.NewWatcher() if err != nil { panic(err) } change := make(chan struct{}) closech := make(chan struct{}) res := &ConfigWatcher{ change: change, closech: closech, } res.closeWait.Add(1) termshark.TrackedGo(func() { defer func() { res.watcher.Close() close(change) res.closeWait.Done() }() Loop: for { select { case <-watcher.Events: res.change <- struct{}{} case err := <-watcher.Errors: log.Debugf("Error from config watcher: %v", err) case <-closech: break Loop } } }, Goroutinewg) if err := watcher.Add(termshark.ConfFile("termshark.toml")); err != nil && !os.IsNotExist(err) { return nil, err } res.watcher = watcher return res, nil } func (c *ConfigWatcher) Close() { // drain the change channel to ensure the goroutine above can process the close. This // is safe because I know, at this point, there are no other readers because termshark // has exited its select loop. termshark.TrackedGo(func() { // This might block because the goroutine above might not be blocked sending // to c.change. But then that means the goroutine's for loop above will terminate, // c.change will be closed, and then this goroutine will end. If the above // goroutine is blocked sending to c.change, then this will drain that value, // and again the goroutine above will end. <-c.change }, Goroutinewg) c.closech <- struct{}{} c.closeWait.Wait() } func (c *ConfigWatcher) ConfigChanged() <-chan struct{} { return c.change } //====================================================================== var Goroutinewg *sync.WaitGroup //====================================================================== // Local Variables: // mode: Go // fill-column: 78 // End: termshark-2.4.0/pkg/convs/000077500000000000000000000000001426312004500154235ustar00rootroot00000000000000termshark-2.4.0/pkg/convs/loader.go000066400000000000000000000111141426312004500172160ustar00rootroot00000000000000// Copyright 2019-2022 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 convs import ( "bytes" "context" "fmt" "os/exec" "sync" "github.com/gcla/gowid" "github.com/gcla/termshark/v2" "github.com/gcla/termshark/v2/pkg/pcap" log "github.com/sirupsen/logrus" ) //====================================================================== var Goroutinewg *sync.WaitGroup //====================================================================== type ILoaderCmds interface { Convs(pcapfile string, convs []string, filter string, abs bool, resolve bool) pcap.IPcapCommand } type commands struct{} func MakeCommands() commands { return commands{} } var _ ILoaderCmds = commands{} func (c commands) Convs(pcapfile string, convs []string, filter string, abs bool, resolve bool) pcap.IPcapCommand { args := []string{"-q", "-r", pcapfile} if abs { args = append(args, "-t", "a") } if !resolve { args = append(args, "-n") } for _, conv := range convs { args = append(args, "-z", fmt.Sprintf("conv,%s", conv)) if filter != "" { args[len(args)-1] = fmt.Sprintf("%s,%s", args[len(args)-1], filter) } } 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 convsCtx context.Context convsCancelFn context.CancelFunc convsCmd 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.convsCancelFn != nil { c.convsCancelFn() } } //====================================================================== type IConvsCallbacks interface { OnData(data string) AfterDataEnd(success bool) } func (c *Loader) StartLoad(pcap string, convs []string, filter string, abs bool, resolve bool, app gowid.IApp, cb IConvsCallbacks) { termshark.TrackedGo(func() { c.loadConvAsync(pcap, convs, filter, abs, resolve, app, cb) }, Goroutinewg) } func (c *Loader) loadConvAsync(pcapf string, convs []string, filter string, abs bool, resolve bool, app gowid.IApp, cb IConvsCallbacks) { c.convsCtx, c.convsCancelFn = context.WithCancel(c.mainCtx) procChan := make(chan int) pid := 0 defer func() { if pid == 0 { close(procChan) } }() c.convsCmd = c.cmds.Convs(pcapf, convs, filter, abs, resolve) termChan := make(chan error) termshark.TrackedGo(func() { var err error cmd := c.convsCmd cancelledChan := c.convsCtx.Done() procChan := procChan state := pcap.NotStarted kill := func() { err := termshark.KillIfPossible(cmd) if err != nil { log.Infof("Did not kill tshark conv process: %v", err) } } loop: for { select { case err = <-termChan: state = pcap.Terminated if !c.SuppressErrors && err != nil { if _, ok := err.(*exec.ExitError); ok { pcap.HandleError(pcap.ConvCode, app, pcap.MakeUsefulError(c.convsCmd, err), cb) } } case pid := <-procChan: procChan = nil if pid != 0 { state = pcap.Started if cancelledChan == nil { kill() } } case <-cancelledChan: cancelledChan = nil if state == pcap.Started { kill() } } if state == pcap.Terminated || (procChan == nil && state == pcap.NotStarted) { break loop } } }, Goroutinewg) convsOut, err := c.convsCmd.StdoutReader() if err != nil { pcap.HandleError(pcap.ConvCode, app, err, cb) return } defer func() { cb.AfterDataEnd(true) }() app.Run(gowid.RunFunction(func(app gowid.IApp) { pcap.HandleBegin(pcap.ConvCode, app, cb) })) defer func() { app.Run(gowid.RunFunction(func(app gowid.IApp) { pcap.HandleEnd(pcap.ConvCode, app, cb) })) }() err = c.convsCmd.Start() if err != nil { err = fmt.Errorf("Error starting %v: %v", c.convsCmd, err) pcap.HandleError(pcap.ConvCode, app, err, cb) return } log.Infof("Started command %v with pid %d", c.convsCmd, c.convsCmd.Pid()) termshark.TrackedGo(func() { termChan <- c.convsCmd.Wait() }, Goroutinewg) pid = c.convsCmd.Pid() procChan <- pid buf := new(bytes.Buffer) buf.ReadFrom(convsOut) cb.OnData(buf.String()) c.convsCancelFn() } //====================================================================== // Local Variables: // mode: Go // fill-column: 78 // End: termshark-2.4.0/pkg/convs/types.go000066400000000000000000000072411426312004500171220ustar00rootroot00000000000000// Copyright 2019-2022 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 convs import "fmt" type Ethernet struct{} type IPv4 struct{} type IPv6 struct{} type UDP struct{} type TCP struct{} var OfficialNameToType = map[string]string{ Ethernet{}.String(): Ethernet{}.Short(), IPv4{}.String(): IPv4{}.Short(), IPv6{}.String(): IPv6{}.Short(), UDP{}.String(): UDP{}.Short(), TCP{}.String(): TCP{}.Short(), } //====================================================================== func (t Ethernet) String() string { return "Ethernet" } func (t Ethernet) Short() string { return "eth" } func (t Ethernet) FilterTo(vals ...string) string { return fmt.Sprintf("eth.dst == %s", vals[0]) } func (t Ethernet) FilterFrom(vals ...string) string { return fmt.Sprintf("eth.src == %s", vals[0]) } func (t Ethernet) FilterAny(vals ...string) string { return fmt.Sprintf("eth.addr == %s", vals[0]) } func (t Ethernet) AIndex() []int { return []int{0} } func (t Ethernet) BIndex() []int { return []int{1} } //====================================================================== func (t IPv4) String() string { return "IPv4" } func (t IPv4) Short() string { return "ip" } func (t IPv4) FilterTo(vals ...string) string { return fmt.Sprintf("ip.dst == %s", vals[0]) } func (t IPv4) FilterFrom(vals ...string) string { return fmt.Sprintf("ip.src == %s", vals[0]) } func (t IPv4) FilterAny(vals ...string) string { return fmt.Sprintf("ip.addr == %s", vals[0]) } func (t IPv4) AIndex() []int { return []int{0} } func (t IPv4) BIndex() []int { return []int{1} } //====================================================================== func (t IPv6) String() string { return "IPv6" } func (t IPv6) Short() string { return "ipv6" } func (t IPv6) FilterTo(vals ...string) string { return fmt.Sprintf("ipv6.dst == %s", vals[0]) } func (t IPv6) FilterFrom(vals ...string) string { return fmt.Sprintf("ipv6.src == %s", vals[0]) } func (t IPv6) FilterAny(vals ...string) string { return fmt.Sprintf("ipv6.addr == %s", vals[0]) } func (t IPv6) AIndex() []int { return []int{0} } func (t IPv6) BIndex() []int { return []int{1} } //====================================================================== func (t UDP) String() string { return "UDP" } func (t UDP) Short() string { return "udp" } func (t UDP) FilterTo(vals ...string) string { return fmt.Sprintf("%s && udp.dstport == %s", IPv4{}.FilterTo(vals[0]), vals[1]) } func (t UDP) FilterFrom(vals ...string) string { return fmt.Sprintf("%s && udp.srcport == %s", IPv4{}.FilterFrom(vals[0]), vals[1]) } func (t UDP) FilterAny(vals ...string) string { return fmt.Sprintf("%s && udp.port == %s", IPv4{}.FilterAny(vals[0]), vals[1]) } func (t UDP) AIndex() []int { return []int{0, 1} } func (t UDP) BIndex() []int { return []int{2, 3} } //====================================================================== func (t TCP) String() string { return "TCP" } func (t TCP) Short() string { return "tcp" } func (t TCP) FilterTo(vals ...string) string { return fmt.Sprintf("%s && tcp.dstport == %s", IPv4{}.FilterTo(vals[0]), vals[1]) } func (t TCP) FilterFrom(vals ...string) string { return fmt.Sprintf("%s && tcp.srcport == %s", IPv4{}.FilterFrom(vals[0]), vals[1]) } func (t TCP) FilterAny(vals ...string) string { return fmt.Sprintf("%s && tcp.port == %s", IPv4{}.FilterAny(vals[0]), vals[1]) } func (t TCP) AIndex() []int { return []int{0, 1} } func (t TCP) BIndex() []int { return []int{2, 3} } //====================================================================== // Local Variables: // mode: Go // fill-column: 78 // End: termshark-2.4.0/pkg/fields/000077500000000000000000000000001426312004500155415ustar00rootroot00000000000000termshark-2.4.0/pkg/fields/fields.go000066400000000000000000000236431426312004500173460ustar00rootroot00000000000000// Copyright 2019-2022 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 fields import ( "bufio" "encoding/gob" "os" "os/exec" "sort" "strings" "sync" "github.com/gcla/termshark/v2" log "github.com/sirupsen/logrus" ) //====================================================================== type FieldType uint // // from epan/ftypes/ftypes.h // // enum ftenum { const ( FT_NONE FieldType = iota /* used for text labels with no value */ FT_PROTOCOL FieldType = iota FT_BOOLEAN FieldType = iota /* TRUE and FALSE come from */ FT_CHAR FieldType = iota /* 1-octet character as 0-255 */ FT_UINT8 FieldType = iota FT_UINT16 FieldType = iota FT_UINT24 FieldType = iota /* really a UINT32, but displayed as 6 hex-digits if FD_HEX*/ FT_UINT32 FieldType = iota FT_UINT40 FieldType = iota /* really a UINT64, but displayed as 10 hex-digits if FD_HEX*/ FT_UINT48 FieldType = iota /* really a UINT64, but displayed as 12 hex-digits if FD_HEX*/ FT_UINT56 FieldType = iota /* really a UINT64, but displayed as 14 hex-digits if FD_HEX*/ FT_UINT64 FieldType = iota FT_INT8 FieldType = iota FT_INT16 FieldType = iota FT_INT24 FieldType = iota /* same as for UINT24 */ FT_INT32 FieldType = iota FT_INT40 FieldType = iota /* same as for UINT40 */ FT_INT48 FieldType = iota /* same as for UINT48 */ FT_INT56 FieldType = iota /* same as for UINT56 */ FT_INT64 FieldType = iota FT_IEEE_11073_SFLOAT FieldType = iota FT_IEEE_11073_FLOAT FieldType = iota FT_FLOAT FieldType = iota FT_DOUBLE FieldType = iota FT_ABSOLUTE_TIME FieldType = iota FT_RELATIVE_TIME FieldType = iota FT_STRING FieldType = iota FT_STRINGZ FieldType = iota /* for use with proto_tree_add_item() */ FT_UINT_STRING FieldType = iota /* for use with proto_tree_add_item() */ FT_ETHER FieldType = iota FT_BYTES FieldType = iota FT_UINT_BYTES FieldType = iota FT_IPv4 FieldType = iota FT_IPv6 FieldType = iota FT_IPXNET FieldType = iota FT_FRAMENUM FieldType = iota /* a UINT32, but if selected lets you go to frame with that number */ FT_PCRE FieldType = iota /* a compiled Perl-Compatible Regular Expression object */ FT_GUID FieldType = iota /* GUID, UUID */ FT_OID FieldType = iota /* OBJECT IDENTIFIER */ FT_EUI64 FieldType = iota FT_AX25 FieldType = iota FT_VINES FieldType = iota FT_REL_OID FieldType = iota /* RELATIVE-OID */ FT_SYSTEM_ID FieldType = iota FT_STRINGZPAD FieldType = iota /* for use with proto_tree_add_item() */ FT_FCWWN FieldType = iota FT_NUM_TYPES FieldType = iota /* last item number plus one */ ) var FieldTypeMap = map[string]FieldType{ "FT_NONE": FT_NONE, "FT_PROTOCOL": FT_PROTOCOL, "FT_BOOLEAN": FT_BOOLEAN, "FT_CHAR": FT_CHAR, "FT_UINT8": FT_UINT8, "FT_UINT16": FT_UINT16, "FT_UINT24": FT_UINT24, "FT_UINT32": FT_UINT32, "FT_UINT40": FT_UINT40, "FT_UINT48": FT_UINT48, "FT_UINT56": FT_UINT56, "FT_UINT64": FT_UINT64, "FT_INT8": FT_INT8, "FT_INT16": FT_INT16, "FT_INT24": FT_INT24, "FT_INT32": FT_INT32, "FT_INT40": FT_INT40, "FT_INT48": FT_INT48, "FT_INT56": FT_INT56, "FT_INT64": FT_INT64, "FT_IEEE_11073_SFLOAT": FT_IEEE_11073_SFLOAT, "FT_IEEE_11073_FLOAT": FT_IEEE_11073_FLOAT, "FT_FLOAT": FT_FLOAT, "FT_DOUBLE": FT_DOUBLE, "FT_ABSOLUTE_TIME": FT_ABSOLUTE_TIME, "FT_RELATIVE_TIME": FT_RELATIVE_TIME, "FT_STRING": FT_STRING, "FT_STRINGZ": FT_STRINGZ, "FT_UINT_STRING": FT_UINT_STRING, "FT_ETHER": FT_ETHER, "FT_BYTES": FT_BYTES, "FT_UINT_BYTES": FT_UINT_BYTES, "FT_IPv4": FT_IPv4, "FT_IPv6": FT_IPv6, "FT_IPXNET": FT_IPXNET, "FT_FRAMENUM": FT_FRAMENUM, "FT_PCRE": FT_PCRE, "FT_GUID": FT_GUID, "FT_OID": FT_OID, "FT_EUI64": FT_EUI64, "FT_AX25": FT_AX25, "FT_VINES": FT_VINES, "FT_REL_OID": FT_REL_OID, "FT_SYSTEM_ID": FT_SYSTEM_ID, "FT_STRINGZPAD": FT_STRINGZPAD, "FT_FCWWN": FT_FCWWN, "FT_NUM_TYPES": FT_NUM_TYPES, } func ParseFieldType(s string) (res FieldType, ok bool) { res, ok = FieldTypeMap[s] return } type Protocol string type Field struct { Name string Type FieldType } type FieldsAndProtos struct { Fields interface{} // protocol or field or map[string]interface{} Protocols map[string]struct{} } func init() { gob.Register(make(map[string]interface{})) gob.Register(Protocol("")) gob.Register(Field{}) } type TSharkFields struct { once sync.Once ser *FieldsAndProtos } type IPrefixCompleterCallback interface { Call([]string) } type IPrefixCompleter interface { Completions(prefix string, cb IPrefixCompleterCallback) } func New() *TSharkFields { return &TSharkFields{} } func DeleteCachedFields() error { return os.Remove(termshark.CacheFile("tsharkfieldsv3.gob.gz")) } // Can be run asynchronously. // This ought to use interfaces to make it testable. func (w *TSharkFields) Init() error { newer, err := termshark.FileNewerThan(termshark.CacheFile("tsharkfieldsv3.gob.gz"), termshark.DirOfPathCommandUnsafe(termshark.TSharkBin())) if err == nil && newer { f := &FieldsAndProtos{ Fields: make(map[string]interface{}), Protocols: make(map[string]struct{}), } err = termshark.ReadGob(termshark.CacheFile("tsharkfieldsv3.gob.gz"), f) if err == nil { w.ser = 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 = termshark.WriteGob(termshark.CacheFile("tsharkfieldsv3.gob.gz"), w.ser) if err != nil { return err } return nil } func (w *TSharkFields) InitNoCache() error { cmd := exec.Command(termshark.TSharkBin(), []string{"-G", "fields"}...) out, err := cmd.StdoutPipe() if err != nil { return err } cmd.Start() fieldsMap := make(map[string]interface{}) protMap := make(map[string]struct{}) scanner := bufio.NewScanner(out) for scanner.Scan() { line := scanner.Text() if strings.HasPrefix(line, "F") { // Wireshark field fields := strings.Split(line, "\t") protos := strings.SplitN(fields[2], ".", 2) if len(protos) > 1 { cur := fieldsMap for i := 0; i < len(protos)-1; i++ { if val, ok := cur[protos[i]]; ok { cur = val.(map[string]interface{}) } else { next := make(map[string]interface{}) cur[protos[i]] = next cur = next } } // Get none value if it's not found - so use that ty, _ := ParseFieldType(fields[3]) cur[protos[len(protos)-1]] = Field{ Name: fields[2], Type: ty, } } } else if strings.HasPrefix(line, "P") { // Wireshark protocol fields := strings.Split(line, "\t") protMap[fields[2]] = struct{}{} } } cmd.Wait() w.ser = &FieldsAndProtos{ Fields: fieldsMap, Protocols: protMap, } return nil } func dedup(s []string) []string { if len(s) == 0 { return s } i := 0 for j := 1; j < len(s); j++ { if s[i] == s[j] { continue } i++ s[i] = s[j] } return s[0 : i+1] } func (t *TSharkFields) LookupField(name string) (bool, Field) { fields := strings.Split(name, ".") cur := t.ser.Fields.(map[string]interface{}) for i := 0; i < len(fields); i++ { if val, ok := cur[fields[i]]; ok { if i == len(fields)-1 { switch val := val.(type) { // means there's another level of indirection, so our input is too long case Field: return true, val default: return false, Field{} } } else { switch val := val.(type) { case map[string]interface{}: cur = val default: return false, Field{} } } } else { return false, Field{} } } return false, Field{} } 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.Warnf("Field completion error: %v", err) } // might be nil if I am still loading from tshark -G if t.ser == 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.ser.Fields.(map[string]interface{}) failed := false loop: for i := 0; i < len(fields); i++ { if val, ok := cur[fields[i]]; ok { if i == len(fields)-1 { switch val.(type) { // means there's another level of indirection, so our input is too long case map[string]interface{}: failed = true } } else { switch val := val.(type) { case map[string]interface{}: prefs = append(prefs, fields[i]) cur = val default: failed = true break loop } } } } if !failed { for k, _ := range cur { if strings.HasPrefix(k, fields[len(fields)-1]) { res = append(res, strings.Join(append(prefs, k), ".")) } } } for k, _ := range t.ser.Protocols { if strings.HasPrefix(k, field) { res = append(res, k) } } sort.Strings(res) res = dedup(res) cb.Call(res) } //====================================================================== // Local Variables: // mode: Go // fill-column: 78 // End: termshark-2.4.0/pkg/fields/fields_test.go000066400000000000000000000014251426312004500203770ustar00rootroot00000000000000// Copyright 2019-2022 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 fields import ( "testing" "github.com/stretchr/testify/assert" ) //====================================================================== func TestFields1(t *testing.T) { fields := New() err := fields.InitNoCache() assert.NoError(t, err) m1, ok := fields.ser.Fields.(map[string]interface{})["tcp"] assert.Equal(t, true, ok) m2, ok := m1.(map[string]interface{})["port"] assert.Equal(t, true, ok) assert.IsType(t, Field{}, m2) assert.Equal(t, m2.(Field).Type, FT_UINT16) } //====================================================================== // Local Variables: // mode: Go // fill-column: 78 // End: termshark-2.4.0/pkg/format/000077500000000000000000000000001426312004500155635ustar00rootroot00000000000000termshark-2.4.0/pkg/format/hexdump.go000066400000000000000000000025361426312004500175720ustar00rootroot00000000000000// Copyright 2019-2022 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.4.0/pkg/format/hexdump_test.go000066400000000000000000000017371426312004500206330ustar00rootroot00000000000000// Copyright 2019-2022 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.4.0/pkg/format/printable.go000066400000000000000000000036651426312004500201040ustar00rootroot00000000000000// Copyright 2019-2022 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.4.0/pkg/noroot/000077500000000000000000000000001426312004500156135ustar00rootroot00000000000000termshark-2.4.0/pkg/noroot/noroot.go000066400000000000000000000017011426312004500174610ustar00rootroot00000000000000// Copyright 2019-2022 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 noroot import ( "github.com/gcla/gowid/widgets/list" "github.com/gcla/gowid/widgets/tree" ) //====================================================================== type Walker struct { *tree.TreeWalker } func NewWalker(w *tree.TreeWalker) *Walker { return &Walker{ TreeWalker: w, } } // for omitting top level node func (f *Walker) Next(pos list.IWalkerPosition) list.IWalkerPosition { return tree.WalkerNext(f, pos) } func (f *Walker) 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.4.0/pkg/pcap/000077500000000000000000000000001426312004500152165ustar00rootroot00000000000000termshark-2.4.0/pkg/pcap/cmds.go000066400000000000000000000147221426312004500165010ustar00rootroot00000000000000// Copyright 2019-2022 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" "strings" "sync" "github.com/gcla/termshark/v2" "github.com/gcla/termshark/v2/configs/profiles" "github.com/gcla/termshark/v2/pkg/summary" "github.com/gcla/termshark/v2/pkg/shark" "github.com/kballard/go-shellquote" ) //====================================================================== type ProcessNotStarted struct { Command *exec.Cmd } var _ error = ProcessNotStarted{} func (e ProcessNotStarted) Error() string { return fmt.Sprintf("Process %v [%v] not started yet", e.Command.Path, shellquote.Join(e.Command.Args...)) } //====================================================================== type Command struct { sync.Mutex *exec.Cmd summaryReader *summary.Reader summaryWriter io.Closer } func (c *Command) String() string { c.Lock() defer c.Unlock() return fmt.Sprintf("%v %v", c.Cmd.Path, shellquote.Join(c.Cmd.Args...)) } func (c *Command) Start() error { c.Lock() defer c.Unlock() pr, pw := io.Pipe() c.summaryWriter = pw c.summaryReader = summary.New(pr) c.Cmd.Stderr = io.MultiWriter(pw, termshark.ErrLogger("cmd", c.Path)) c.PutInNewGroupOnUnix() res := c.Cmd.Start() return res } func (c *Command) Wait() error { err := c.Cmd.Wait() c.Lock() c.summaryWriter.Close() c.Unlock() return err } func (c *Command) StdoutReader() (io.ReadCloser, error) { c.Lock() defer c.Unlock() return c.Cmd.StdoutPipe() } func (c *Command) StderrSummary() []string { c.Lock() defer c.Unlock() return c.summaryReader.Summary() } func (c *Command) SetStdout(w io.Writer) { c.Lock() defer c.Unlock() c.Cmd.Stdout = w } // If stdout supports Close(), call it. If stdout is a pipe, for example, // this can be used to have EOF appear on the reading side (e.g. tshark -T psml) func (c *Command) Close() error { c.Lock() defer c.Unlock() if cl, ok := c.Cmd.Stdout.(io.Closer); ok { return cl.Close() } return nil } func (c *Command) Pid() int { c.Lock() defer c.Unlock() if c.Cmd.Process == nil { return -1 } return c.Cmd.Process.Pid } //====================================================================== type Commands struct { DecodeAs []string Args []string PdmlArgs []string PsmlArgs []string Color bool } func MakeCommands(decodeAs []string, args []string, pdml []string, psml []string, color bool) Commands { return Commands{ DecodeAs: decodeAs, Args: args, PdmlArgs: pdml, PsmlArgs: psml, Color: color, } } var _ ILoaderCmds = Commands{} func (c Commands) Iface(ifaces []string, captureFilter string, tmpfile string) IBasicCommand { args := make([]string, 0) for _, iface := range ifaces { args = append(args, "-i", iface) } args = append(args, "-w", tmpfile) if captureFilter != "" { args = append(args, "-f", captureFilter) } prof := profiles.ConfString("main.wireshark-profile", "") if prof != "" { args = append(args, "-C", prof) } res := &Command{ Cmd: exec.Command(termshark.CaptureBin(), args...), } // This tells termshark to start in a special capture mode. It allows termshark // to run itself like this: // // termshark -i eth0 -w foo.pcap // // which will then run dumpcap and if that fails, tshark. The idea // is to use the most specialized/efficient capture method if that // works, but fall back to tshark if needed e.g. for randpkt, sshcapture, etc // (extcap interfaces). res.Cmd.Env = append(os.Environ(), "TERMSHARK_CAPTURE_MODE=1") res.Cmd.Stdin = os.Stdin res.Cmd.Stderr = os.Stderr res.Cmd.Stdout = os.Stdout return res } func (c Commands) Tail(tmpfile string) ITailCommand { args := termshark.TailCommand() args = append(args, tmpfile) return &Command{Cmd: exec.Command(args[0], args[1:]...)} } func (c Commands) Psml(pcap interface{}, displayFilter string) IPcapCommand { fifo := true switch pcap.(type) { case string: fifo = false } cols := shark.GetPsmlColumnFormat() specs := make([]string, 0, len(cols)) for _, w := range cols { if !w.Hidden { specs = append(specs, fmt.Sprintf("\"%s\",\"%s\"", w.Name, w.Field)) } } args := []string{ // "-i", // "0", // "-o", // "0", //"-f", "-o", fmt.Sprintf("/tmp/foo-%d", delme), "-s", "256", "-tt", //termshark.TSharkBin(), "-T", "psml", // Deliberately add in a No. column as the first, no matter what the user's config says. This is // not added to the UI, but the loader needs it to track packet numbers when a filter is in // effect - so a table row (int) can be mapped to a packet number. "-o", fmt.Sprintf("gui.column.format:\"No.\",\"%%m\",%s", strings.Join(specs, ",")), } if !fifo { // read from cmdline file args = append(args, "-r", pcap.(string)) } else { args = append(args, "-r", "-") args = append(args, "-l") // provide data sooner to decoder routine in termshark } if displayFilter != "" { args = append(args, "-Y", displayFilter) } for _, arg := range c.DecodeAs { args = append(args, "-d", arg) } if c.Color { args = append(args, "--color") } args = append(args, c.PsmlArgs...) args = append(args, c.Args...) prof := profiles.ConfString("main.wireshark-profile", "") if prof != "" { args = append(args, "-C", prof) } //cmd := exec.Command("strace", args...) cmd := exec.Command(termshark.TSharkBin(), args...) //cmd := exec.Command("stdbuf", args...) if fifo { cmd.Stdin = pcap.(io.Reader) } return &Command{Cmd: cmd} } func (c Commands) Pcap(pcap string, displayFilter string) IPcapCommand { // need to use stdout and -w - otherwise, tshark writes one-line text output args := []string{"-r", pcap, "-x"} if displayFilter != "" { args = append(args, "-Y", displayFilter) } args = append(args, c.Args...) return &Command{Cmd: exec.Command(termshark.TSharkBin(), args...)} } func (c Commands) Pdml(pcap string, displayFilter string) IPcapCommand { args := []string{"-T", "pdml", "-r", pcap} if c.Color { args = append(args, "--color") } if displayFilter != "" { args = append(args, "-Y", displayFilter) } for _, arg := range c.DecodeAs { args = append(args, "-d", arg) } args = append(args, c.PdmlArgs...) args = append(args, c.Args...) prof := profiles.ConfString("main.wireshark-profile", "") if prof != "" { args = append(args, "-C", prof) } return &Command{Cmd: exec.Command(termshark.TSharkBin(), args...)} } //====================================================================== // Local Variables: // mode: Go // fill-column: 78 // End: termshark-2.4.0/pkg/pcap/cmds_unix.go000066400000000000000000000014621426312004500175410ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // +build !windows package pcap import ( "syscall" "github.com/kballard/go-shellquote" "github.com/pkg/errors" log "github.com/sirupsen/logrus" ) func (c *Command) PutInNewGroupOnUnix() { c.Cmd.SysProcAttr = &syscall.SysProcAttr{ Setpgid: true, Pgid: 0, } } func (c *Command) Kill() error { c.Lock() defer c.Unlock() if c.Cmd.Process == nil { return errors.WithStack(ProcessNotStarted{Command: c.Cmd}) } log.Infof("Sending SIGKILL to %v: %v", c.Cmd.Process.Pid, shellquote.Join(c.Cmd.Args...)) // The PSML tshark process doesn't reliably die with a SIGTERM - not sure why return syscall.Kill(-c.Cmd.Process.Pid, syscall.SIGKILL) } termshark-2.4.0/pkg/pcap/cmds_windows.go000066400000000000000000000006611426312004500202500ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. package pcap import "github.com/pkg/errors" func (c *Command) PutInNewGroupOnUnix() {} func (c *Command) Kill() error { c.Lock() defer c.Unlock() if c.Cmd.Process == nil { return errors.WithStack(ProcessNotStarted{Command: c.Cmd}) } return c.Cmd.Process.Kill() } termshark-2.4.0/pkg/pcap/handlers.go000066400000000000000000000060141426312004500173460ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. package pcap import "github.com/gcla/gowid" //====================================================================== type HandlerCode int const ( NoneCode HandlerCode = 1 << iota PdmlCode PsmlCode TailCode IfaceCode ConvCode StreamCode CapinfoCode ) type IClear interface { OnClear(code HandlerCode, app gowid.IApp) } type INewSource interface { OnNewSource(code HandlerCode, app gowid.IApp) } type IOnError interface { OnError(code HandlerCode, app gowid.IApp, err error) } type IBeforeBegin interface { BeforeBegin(code HandlerCode, app gowid.IApp) } type IAfterEnd interface { AfterEnd(code HandlerCode, app gowid.IApp) } type IPsmlHeader interface { OnPsmlHeader(code HandlerCode, app gowid.IApp) } type IUnpack interface { Unpack() []interface{} } type HandlerList []interface{} func (h HandlerList) Unpack() []interface{} { return h } type unpackedHandlerFunc func(HandlerCode, gowid.IApp, interface{}) bool func HandleUnpack(code HandlerCode, cb interface{}, handler unpackedHandlerFunc, app gowid.IApp) bool { if c, ok := cb.(IUnpack); ok { handlers := c.Unpack() for _, cb := range handlers { handler(code, app, cb) // will wait on channel if it has to, doesn't matter if not } return true } return false } func HandleBegin(code HandlerCode, app gowid.IApp, cb interface{}) bool { res := false if !HandleUnpack(code, cb, HandleBegin, app) { if c, ok := cb.(IBeforeBegin); ok { c.BeforeBegin(code, app) res = true } } return res } func HandleEnd(code HandlerCode, app gowid.IApp, cb interface{}) bool { res := false if !HandleUnpack(code, cb, HandleEnd, app) { if c, ok := cb.(IAfterEnd); ok { c.AfterEnd(code, app) res = true } } return res } func HandleError(code HandlerCode, app gowid.IApp, err error, cb interface{}) bool { res := false if !HandleUnpack(code, cb, func(code HandlerCode, app gowid.IApp, cb2 interface{}) bool { return HandleError(code, app, err, cb2) }, app) { if ec, ok := cb.(IOnError); ok { ec.OnError(code, app, err) res = true } } return res } func handlePsmlHeader(code HandlerCode, app gowid.IApp, cb interface{}) bool { res := false if !HandleUnpack(code, cb, handlePsmlHeader, app) { if c, ok := cb.(IPsmlHeader); ok { c.OnPsmlHeader(code, app) res = true } } return res } func handleClear(code HandlerCode, app gowid.IApp, cb interface{}) bool { res := false if !HandleUnpack(code, cb, handleClear, app) { if c, ok := cb.(IClear); ok { c.OnClear(code, app) res = true } } return res } func handleNewSource(code HandlerCode, app gowid.IApp, cb interface{}) bool { res := false if !HandleUnpack(code, cb, handleNewSource, app) { if c, ok := cb.(INewSource); ok { c.OnNewSource(code, app) res = true } } return res } //====================================================================== // Local Variables: // mode: Go // fill-column: 78 // End: termshark-2.4.0/pkg/pcap/loader.go000066400000000000000000002067541426312004500170310ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. package pcap import ( "bufio" "context" "encoding/xml" "fmt" "io" "os" "os/exec" "path/filepath" "regexp" "strconv" "strings" "sync" "sync/atomic" "github.com/gcla/gowid" "github.com/gcla/gowid/gwutil" "github.com/gcla/termshark/v2" "github.com/gcla/termshark/v2/pkg/format" lru "github.com/hashicorp/golang-lru" log "github.com/sirupsen/logrus" fsnotify "gopkg.in/fsnotify/fsnotify.v1" ) //====================================================================== var PcapCmds ILoaderCmds var PcapOpts Options var OpsChan chan gowid.RunFunction func init() { OpsChan = make(chan gowid.RunFunction, 100) } //====================================================================== var Goroutinewg *sync.WaitGroup type RunFn func() //====================================================================== type LoaderState bool const ( NotLoading LoaderState = false Loading LoaderState = true ) func (t LoaderState) String() string { if t { return "loading" } else { return "not-loading" } } //====================================================================== type ProcessState int const ( NotStarted ProcessState = 0 Started ProcessState = 1 Terminated ProcessState = 2 ) func (p ProcessState) String() string { switch p { case NotStarted: return "NotStarted" case Started: return "Started" case Terminated: return "Terminated" default: return "Unknown" } } //====================================================================== type IBasicCommand interface { fmt.Stringer Start() error Wait() error Pid() int Kill() error StderrSummary() []string } func MakeUsefulError(cmd IBasicCommand, err error) gowid.KeyValueError { return gowid.WithKVs(termshark.BadCommand, map[string]interface{}{ "command": cmd.String(), "error": err, "stderr": strings.Join(cmd.StderrSummary(), "\n"), }) } type ITailCommand interface { IBasicCommand SetStdout(io.Writer) // set to the write side of a fifo, for example - the command will .Write() here Close() error // closes stdout, which signals tshark -T psml } type IPcapCommand interface { IBasicCommand StdoutReader() (io.ReadCloser, error) // termshark will .Read() from result } type ILoaderCmds interface { Iface(ifaces []string, captureFilter string, tmpfile string) IBasicCommand Tail(tmpfile string) ITailCommand Psml(pcap interface{}, displayFilter string) IPcapCommand Pcap(pcap string, displayFilter string) IPcapCommand Pdml(pcap string, displayFilter string) IPcapCommand } //====================================================================== // PacketLoader supports swapping out loaders type PacketLoader struct { *ParentLoader } // Renew is called when a new pcap is loaded from an open termshark session i.e. termshark // was started with one packet source, then a new one is selected. This ensures that all // connected loaders that might still be doing work are cancelled. func (c *PacketLoader) Renew() { if c.ParentLoader != nil { c.ParentLoader.CloseMain() } c.ParentLoader = NewPcapLoader(c.ParentLoader.cmds, c.runner, c.ParentLoader.opt) } type ParentLoader struct { // Note that a nil InterfaceLoader implies this loader is not handling a "live" packet source *InterfaceLoader // these are only replaced from the main goroutine, so no lock needed *PsmlLoader *PdmlLoader cmds ILoaderCmds tailStoppedDeliberately bool // true if tail is stopped because its packet feed has run out psrcs []IPacketSource // The canonical struct for the loader's current packet source. displayFilter string captureFilter string ifaceFile string // shared between InterfaceLoader and PsmlLoader - to preserve and feed packets mainCtx context.Context // cancelling this cancels the dependent contexts - used to close whole loader. mainCancelFn context.CancelFunc loadWasCancelled bool // True if the last load (iface or file) was halted by the stop button or ctrl-c runner IMainRunner opt Options // held only to pass to the PDML and PSML loaders when renewed } type InterfaceLoader struct { state LoaderState ifaceCtx context.Context // cancels the iface reader process ifaceCancelFn context.CancelFunc ifaceCmd IBasicCommand sync.Mutex // set by the iface procedure when it has finished e.g. the pipe to the fifo has finished, the // iface process has been killed, etc. This tells the psml-reading procedure when it should stop i.e. // when this many bytes have passed through. totalFifoBytesWritten gwutil.Int64Option totalFifoBytesRead gwutil.Int64Option fifoError error } type PsmlLoader struct { state LoaderState // which pieces are currently loading PcapPsml interface{} // Pcap file source for the psml reader - fifo if iface+!stopped; tmpfile if iface+stopped; pcap otherwise psmlStoppedDeliberately_ bool // true if loader is in a transient state due to a user operation e.g. stop, reload, etc psmlCtx context.Context // cancels the psml loading process psmlCancelFn context.CancelFunc tailCtx context.Context // cancels the tail reader process (if iface in operation) tailCancelFn context.CancelFunc // Signalled when the psml is fully loaded (or already loaded) - to tell // the pdml and pcap reader goroutines to start - they can then map table // row -> frame number startStage2Chan chan struct{} PsmlFinishedChan chan struct{} // closed when entire psml load process is done tailCmd ITailCommand PsmlCmd IPcapCommand // gcla later todo - change to pid like PdmlPid sync.Mutex packetAverageLength []averageTracker // length of num columns packetMaxLength []maxTracker // length of num columns packetPsmlData [][]string packetPsmlColors []PacketColors packetPsmlHeaders []string PacketNumberMap map[int]int // map from actual packet row 12 to pos in unsorted table // This would be affected by a display filter e.g. packet 12 might be the 1st packet in the table. // I need this so that if the user jumps to a mark stored as "packet 12", I can find the right table row. PacketNumberOrder map[int]int // e.g. {12->44, 44->71, 71->72,...} - the packet numbers, in order, affected by a filter. // If I use a generic ordered map, I could avoid this separate structure PacketCache *lru.Cache // i -> [pdml(i * 1000)..pdml(i+1*1000)] - accessed from any goroutine opt Options } type PdmlLoader struct { state LoaderState // which pieces are currently loading PcapPdml string // Pcap file source for the pdml reader - tmpfile if iface; pcap otherwise PcapPcap string // Pcap file source for the pcap reader - tmpfile if iface; pcap otherwise pdmlStoppedDeliberately_ bool // true if loader is in a transient state due to a user operation e.g. stop, reload, etc stage2Ctx context.Context // cancels the pcap/pdml loading process stage2CancelFn context.CancelFunc stage2Wg sync.WaitGroup startChan chan struct{} Stage2FinishedChan chan struct{} // closed when entire pdml+pcap load process is done PdmlPid int // 0 if process not started PcapPid int // 0 if process not started sync.Mutex visible bool // true if this pdml load is needed right now by the UI rowCurrentlyLoading int // set by the pdml loading stage - main goroutine only highestCachedRow int // main goroutine only KillAfterReadingThisMany int // A shortcut - tell pcap/pdml to read one - no lock worked out yet opt Options } type PacketColors struct { FG gowid.IColor BG gowid.IColor } type Options struct { CacheSize int PacketsPerLoad int } type iLoaderEnv interface { Commands() ILoaderCmds MainRun(fn gowid.RunFunction) Context() context.Context } type iPsmlLoaderEnv interface { iLoaderEnv iTailCommand PsmlStoppedDeliberately() bool TailStoppedDeliberately() bool LoadWasCancelled() bool DisplayFilter() string InterfaceFile() string PacketSources() []IPacketSource } // IMainRunner is implemented by a type that runs a closure on termshark's main loop // (via gowid's App.Run) type IMainRunner interface { Run(fn gowid.RunFunction) } type Runner struct { gowid.IApp } var _ IMainRunner = (*Runner)(nil) func (a *Runner) Run(fn gowid.RunFunction) { a.IApp.Run(fn) } //====================================================================== func NewPcapLoader(cmds ILoaderCmds, runner IMainRunner, opts ...Options) *ParentLoader { var opt Options if len(opts) > 0 { opt = opts[0] } if opt.CacheSize == 0 { opt.CacheSize = 32 } if opt.PacketsPerLoad == 0 { opt.PacketsPerLoad = 1000 // default } else if opt.PacketsPerLoad < 100 { opt.PacketsPerLoad = 100 // minimum } res := &ParentLoader{ PsmlLoader: &PsmlLoader{}, // so default fields are set and XmlLoader is not nil PdmlLoader: &PdmlLoader{ opt: opt, }, cmds: cmds, runner: runner, opt: opt, } res.mainCtx, res.mainCancelFn = context.WithCancel(context.Background()) res.RenewPsmlLoader() res.RenewPdmlLoader() return res } func (c *ParentLoader) RenewPsmlLoader() { c.PsmlLoader = &PsmlLoader{ PcapPsml: c.PsmlLoader.PcapPsml, tailCmd: c.PsmlLoader.tailCmd, PsmlCmd: c.PsmlLoader.PsmlCmd, packetAverageLength: make([]averageTracker, 64), packetMaxLength: make([]maxTracker, 64), packetPsmlData: make([][]string, 0), packetPsmlColors: make([]PacketColors, 0), packetPsmlHeaders: make([]string, 0, 10), PacketNumberMap: make(map[int]int), PacketNumberOrder: make(map[int]int), startStage2Chan: make(chan struct{}), // do this before signalling start PsmlFinishedChan: make(chan struct{}), opt: c.opt, } packetCache, err := lru.New(c.opt.CacheSize) if err != nil { log.Fatal(err) } c.PacketCache = packetCache } func (c *ParentLoader) RenewPdmlLoader() { c.PdmlLoader = &PdmlLoader{ PcapPdml: c.PcapPdml, PcapPcap: c.PcapPcap, rowCurrentlyLoading: -1, highestCachedRow: -1, opt: c.opt, } } func (c *ParentLoader) RenewIfaceLoader() { c.InterfaceLoader = &InterfaceLoader{} } func (p *ParentLoader) LoadingAnything() bool { return p.PsmlLoader.IsLoading() || p.PdmlLoader.IsLoading() || p.InterfaceLoader.IsLoading() } func (p *ParentLoader) InterfaceFile() string { return p.ifaceFile } func (p *ParentLoader) DisplayFilter() string { return p.displayFilter } func (p *ParentLoader) CaptureFilter() string { return p.captureFilter } func (p *ParentLoader) TurnOffPipe() { // Switch over to the temp pcap file. If a new filter is applied // after stopping, we should read from the temp file and not the fifo // because nothing will be feeding the fifo. if p.PsmlLoader.PcapPsml != p.PdmlLoader.PcapPdml { log.Infof("Switching from interface/fifo mode to file mode") p.PsmlLoader.PcapPsml = p.PdmlLoader.PcapPdml } } func (p *ParentLoader) PacketSources() []IPacketSource { return p.psrcs } func (p *ParentLoader) PsmlStoppedDeliberately() bool { return p.psmlStoppedDeliberately_ } func (p *ParentLoader) TailStoppedDeliberately() bool { return p.tailStoppedDeliberately } func (p *ParentLoader) LoadWasCancelled() bool { return p.loadWasCancelled } func (p *ParentLoader) Commands() ILoaderCmds { return p.cmds } func (p *ParentLoader) Context() context.Context { return p.mainCtx } func (p *ParentLoader) MainRun(fn gowid.RunFunction) { p.runner.Run(fn) } // CloseMain shuts down the whole loader, including progress monitoring goroutines. Use this only // when about to load a new pcap (use a new loader) func (c *ParentLoader) CloseMain() { c.psmlStoppedDeliberately_ = true c.pdmlStoppedDeliberately_ = true if c.mainCancelFn != nil { c.mainCancelFn() c.mainCancelFn = nil } } func (c *ParentLoader) StopLoadPsmlAndIface(cb interface{}) { log.Infof("Requested stop psml + iface") c.psmlStoppedDeliberately_ = true c.loadWasCancelled = true c.stopTail() c.stopLoadPsml() c.stopLoadIface() } //====================================================================== func (c *PacketLoader) Reload(filter string, cb interface{}, app gowid.IApp) { c.stopTail() c.stopLoadPsml() c.stopLoadPdml() OpsChan <- gowid.RunFunction(func(app gowid.IApp) { c.RenewPsmlLoader() c.RenewPdmlLoader() // This is not ideal. I'm clearing the views, but I'm about to // restart. It's not really a new source, so called the new source // handler is an untidy way of updating the current capture in the // title bar again handleClear(NoneCode, app, cb) c.displayFilter = filter log.Infof("Applying display filter '%s'", filter) c.loadPsmlSync(c.InterfaceLoader, c, cb, app) }) } func (c *PacketLoader) LoadPcap(pcap string, displayFilter string, cb interface{}, app gowid.IApp) { log.Infof("Requested pcap file load for '%v'", pcap) curDisplayFilter := displayFilter // The channel is unbuffered, and monitored from the same goroutine, so this would block // unless we start a new goroutine if c.Pcap() == pcap && c.DisplayFilter() == curDisplayFilter { log.Infof("No operation - same pcap and filter.") HandleError(NoneCode, app, fmt.Errorf("Same pcap and filter - nothing to do."), cb) } else { c.stopTail() c.stopLoadPsml() c.stopLoadPdml() c.stopLoadIface() OpsChan <- gowid.RunFunction(func(app gowid.IApp) { c.Renew() // This will enable the operation when clear completes handleClear(NoneCode, app, cb) c.psrcs = []IPacketSource{FileSource{Filename: pcap}} c.ifaceFile = "" c.PcapPsml = pcap c.PcapPdml = pcap c.PcapPcap = pcap c.displayFilter = displayFilter // call from main goroutine - when new filename is established handleNewSource(NoneCode, app, cb) log.Infof("Starting new pcap file load '%s'", pcap) c.loadPsmlSync(nil, c.ParentLoader, cb, app) }) } } // Clears the currently loaded data. If the loader is currently reading from an // interface, the loading continues after the current data has been discarded. If // the loader is currently reading from a file, the loading *stops*. // Intended to restart iface loader - since a clear will discard all data up to here. func (c *PacketLoader) ClearPcap(cb interface{}) { startIfaceAgain := false if c.InterfaceLoader != nil { // Don't restart if the previous interface load was deliberately cancelled if !c.loadWasCancelled { startIfaceAgain = true for _, psrc := range c.psrcs { startIfaceAgain = startIfaceAgain && CanRestart(psrc) // Only try to restart if the packet source allows } } c.stopLoadIface() } // Don't close main context, it's used by interface process. // We may not have anything running, but it's ok - then the op channel // will be enabled if !startIfaceAgain { c.loadWasCancelled = true } c.stopTail() c.stopLoadPsml() c.stopLoadPdml() // When stop is done, launch the clear and restart OpsChan <- gowid.RunFunction(func(app gowid.IApp) { // Don't CloseMain - that will stop the interface process too c.loadWasCancelled = false c.RenewPsmlLoader() c.RenewPdmlLoader() handleClear(NoneCode, app, cb) if !startIfaceAgain { c.psrcs = c.psrcs[:0] c.ifaceFile = "" c.PcapPsml = "" c.PcapPdml = "" c.PcapPcap = "" c.displayFilter = "" } else { c.RenewIfaceLoader() if err := c.loadInterfaces(c.psrcs, c.CaptureFilter(), c.DisplayFilter(), c.InterfaceFile(), cb, app); err != nil { HandleError(NoneCode, app, err, cb) } } }) } // Always called from app goroutine context - so don't need to protect for race on cancelfn // Assumes gstate is ready // iface can be a number, or a fifo, or a pipe... func (c *PacketLoader) LoadInterfaces(psrcs []IPacketSource, captureFilter string, displayFilter string, tmpfile string, cb interface{}, app gowid.IApp) error { c.RenewIfaceLoader() return c.loadInterfaces(psrcs, captureFilter, displayFilter, tmpfile, cb, app) } func (c *ParentLoader) loadPsmlForInterfaces(psrcs []IPacketSource, captureFilter string, displayFilter string, tmpfile string, cb interface{}, app gowid.IApp) error { // It's a temporary unique file, and no processes are started yet, so either // (a) it doesn't exist, OR // (b) it does exist in which case this load is a result of a restart. // In ths second case, we need to discard existing packets before starting // tail in case it catches this file with existing data. err := os.Remove(tmpfile) if err != nil && !os.IsNotExist(err) { return err } c.PcapPsml = nil c.PcapPdml = tmpfile c.PcapPcap = tmpfile c.psrcs = psrcs // dpm't know if it's fifo (tfifo), pipe (/dev/fd/3) or iface (eth0). Treated same way c.ifaceFile = tmpfile c.displayFilter = displayFilter c.captureFilter = captureFilter handleNewSource(NoneCode, app, cb) log.Infof("Starting new interface/fifo load '%v'", SourcesString(psrcs)) c.PsmlLoader.loadPsmlSync(c.InterfaceLoader, c, cb, app) return nil } // intended for internal use func (c *ParentLoader) loadInterfaces(psrcs []IPacketSource, captureFilter string, displayFilter string, tmpfile string, cb interface{}, app gowid.IApp) error { if err := c.loadPsmlForInterfaces(psrcs, captureFilter, displayFilter, tmpfile, cb, app); err != nil { return err } // Deliberately use only HandleEnd handler once, in the PSML load - when it finishes, // we'll reenable ops c.InterfaceLoader.loadIfacesSync(c, cb, app) return nil } func (c *ParentLoader) String() string { names := make([]string, 0, len(c.psrcs)) for _, psrc := range c.psrcs { switch { case psrc.IsFile() || psrc.IsFifo(): names = append(names, filepath.Base(psrc.Name())) case psrc.IsPipe(): names = append(names, "") case psrc.IsInterface(): names = append(names, psrc.Name()) default: names = append(names, "(no packet source)") } } return strings.Join(names, " + ") } func (c *ParentLoader) Empty() bool { return len(c.psrcs) == 0 } func (c *ParentLoader) Pcap() string { for _, psrc := range c.psrcs { if psrc != nil && psrc.IsFile() { return psrc.Name() } } return "" } func (c *ParentLoader) Interfaces() []string { names := make([]string, 0, len(c.psrcs)) for _, psrc := range c.psrcs { if psrc != nil && !psrc.IsFile() { names = append(names, psrc.Name()) } } return names } func (c *ParentLoader) loadIsNecessary(ev LoadPcapSlice) bool { res := true if ev.Row > c.NumLoaded() { res = false } else if ce, ok := c.CacheAt((ev.Row / c.opt.PacketsPerLoad) * c.opt.PacketsPerLoad); ok && ce.Complete() { // Might be less because a cache load might've been interrupted - if it's not truncated then // we're set res = false // I can't conclude that a load based at row 0 is sufficient to ignore this one. // The previous load might've started when only 10 packets were available (via the // the PSML data), so the PDML end idx would be frame.number < 10. This load might // be for a rocus position of 20, which would map via rounding to row 0. But we // don't have the data. // Hang on - this is for a load that has finished. If it was a live load, the cache // will not be marked complete for this batch of data - so a live load that is loading // this batch, but started earlier in the load (so frame.number < X where X < row) // will not be marked complete in the cache, so the load will be redone if needed. If // we get here, the load is still underway, so let it complete. } else if c.LoadingRow() == ev.Row { res = false } return res } //====================================================================== // Holds a reference to the loader, and wraps Read() around the tail process's // Read(). Count the bytes, and when they are equal to the final total of bytes // written by the tshark -i process (spooling to a tmp file), a function is called // which stops the PSML process. type tailReadTracker struct { tailReader io.Reader loader *InterfaceLoader tail iTailCommand callback interface{} app gowid.IApp } func (r *tailReadTracker) Read(p []byte) (int, error) { n, err := r.tailReader.Read(p) r.loader.Lock() if r.loader.totalFifoBytesRead.IsNone() { r.loader.totalFifoBytesRead = gwutil.SomeInt64(int64(n)) } else { r.loader.totalFifoBytesRead = gwutil.SomeInt64(int64(n) + r.loader.totalFifoBytesRead.Val()) } // err == ErrClosed if the pipe (tailReader) that is wrapped in this tracker is closed. // This can happen because this call to Read() and the deferred closepipe() function run // at the same time. if err != nil && r.loader.fifoError == nil && err != io.EOF && !errIsAlreadyClosed(err) { r.loader.fifoError = err } r.loader.Unlock() r.loader.checkAllBytesRead(r.tail, r.callback, r.app) return n, err } func errIsAlreadyClosed(err error) bool { if err == os.ErrClosed { return true } else if err, ok := err.(*os.PathError); ok { return errIsAlreadyClosed(err.Err) } else { return false } } //====================================================================== type iPdmlLoaderEnv interface { iLoaderEnv DisplayFilter() string ReadingFromFifo() bool StartStage2ChanFn() chan struct{} PacketCacheFn() *lru.Cache // i -> [pdml(i * 1000)..pdml(i+1*1000)] updateCacheEntryWithPdml(row int, pdml []IPdmlPacket, done bool) updateCacheEntryWithPcap(row int, pcap [][]byte, done bool) LengthOfPdmlCacheEntry(row int) (int, error) LengthOfPcapCacheEntry(row int) (int, error) CacheAt(row int) (CacheEntry, bool) DoWithPsmlData(func([][]string)) } func (c *PdmlLoader) loadPcapSync(row int, visible bool, ps iPdmlLoaderEnv, cb interface{}, app gowid.IApp) { // Used to cancel the tickers below which update list widgets with the latest data and // update the progress meter. Note that if ctx is cancelled, then this context is cancelled // too. When the 2/3 data loading processes are done, a goroutine will then run uiCtxCancel() // to stop the UI updates. c.stage2Ctx, c.stage2CancelFn = context.WithCancel(ps.Context()) c.state = Loading c.rowCurrentlyLoading = row c.visible = visible // Set to true by a goroutine started within here if ctxCancel() is called i.e. the outer context var pdmlCancelled int32 var pcapCancelled int32 c.startChan = make(chan struct{}) c.Stage2FinishedChan = make(chan struct{}) // gcla later todo - suspect // Returns true if it's an error we should bring to user's attention unexpectedPdmlError := func(err error) bool { cancelled := atomic.LoadInt32(&pdmlCancelled) if cancelled == 0 { if err != io.EOF { if err, ok := err.(*xml.SyntaxError); !ok || err.Msg != "unexpected EOF" { return true } } } return false } unexpectedPcapError := func(err error) bool { cancelled := atomic.LoadInt32(&pcapCancelled) if cancelled == 0 { if err != io.EOF { if err, ok := err.(*xml.SyntaxError); !ok || err.Msg != "unexpected EOF" { return true } } } return false } setPcapCancelled := func() { atomic.CompareAndSwapInt32(&pcapCancelled, 0, 1) } setPdmlCancelled := func() { atomic.CompareAndSwapInt32(&pdmlCancelled, 0, 1) } //====================================================================== var displayFilterStr string sidx := -1 eidx := -1 // Determine this in main goroutine termshark.TrackedGo(func() { ps.MainRun(gowid.RunFunction(func(app gowid.IApp) { HandleBegin(PdmlCode, app, cb) })) // This should correctly wait for all resources, no matter where in the process of creating them // an interruption or error occurs defer func(p *PdmlLoader) { // Wait for all other goroutines to complete p.stage2Wg.Wait() // The process Wait() goroutine will always expect a stage2 cancel at some point. It can // come early, if the user interrupts the load. If not, then we send it now, to let // that goroutine terminate. p.stage2CancelFn() ps.MainRun(gowid.RunFunction(func(app gowid.IApp) { close(p.Stage2FinishedChan) HandleEnd(PdmlCode, app, cb) p.state = NotLoading p.rowCurrentlyLoading = -1 p.stage2CancelFn = nil })) }(c) // Set these before starting the pcap and pdml process goroutines so that // at the beginning, PdmlCmd and PcapCmd are definitely not nil. These // values are saved by the goroutine, and used to access the pid of these // processes, if they are started. var pdmlCmd IPcapCommand var pcapCmd IPcapCommand // // Goroutine to set mapping between table rows and frame numbers // termshark.TrackedGo(func() { select { case <-ps.StartStage2ChanFn(): break case <-c.stage2Ctx.Done(): return } // Do this - but if we're cancelled first (stage2Ctx.Done), then they // don't need to be signalled because the other selects waiting on these // channels will be cancelled too. // This has to wait until the PsmlCmd and PcapCmd are set - because next stages depend // on those defer func() { // Signal the pdml and pcap reader to start. select { case <-c.startChan: // it will be closed if the psml has loaded already, and this e.g. a cached load default: close(c.startChan) } }() // If there's no filter, psml, pdml and pcap run concurrently for speed. Therefore the pdml and pcap // don't know how large the psml will be. So we set numToRead to 1000. This might be too high, but // we only use this to determine when we can kill the reading processes early. The result will be // correct if we don't kill the processes, it just might load for longer. c.KillAfterReadingThisMany = c.opt.PacketsPerLoad var err error if ps.DisplayFilter() == "" { sidx = row + 1 // +1 for frame.number being 1-based; +1 to read past the end so that // the XML decoder doesn't stall and I can kill after abcdex eidx = row + c.opt.PacketsPerLoad + 1 + 1 } else { ps.DoWithPsmlData(func(psmlData [][]string) { if len(psmlData) > row { sidx, err = strconv.Atoi(psmlData[row][0]) if err != nil { log.Fatal(err) } if len(psmlData) > row+c.opt.PacketsPerLoad+1 { // If we have enough packets to request one more than the amount to // cache, then requesting one more will mean the XML decoder won't // block at packet 999 waiting for - so this is a hack to // let me promptly kill tshark when I've read enough. eidx, err = strconv.Atoi(psmlData[row+c.opt.PacketsPerLoad+1][0]) if err != nil { log.Fatal(err) } } else { eidx, err = strconv.Atoi(psmlData[len(psmlData)-1][0]) if err != nil { log.Fatal(err) } eidx += 1 // beyond end of last frame c.KillAfterReadingThisMany = len(psmlData) - row } } }) } if ps.DisplayFilter() != "" { displayFilterStr = fmt.Sprintf("(%s) and (frame.number >= %d) and (frame.number < %d)", ps.DisplayFilter(), sidx, eidx) } else { displayFilterStr = fmt.Sprintf("(frame.number >= %d) and (frame.number < %d)", sidx, eidx) } // These need to be set after displayFilterStr is set but before stage 2 is started pdmlCmd = ps.Commands().Pdml(c.PcapPdml, displayFilterStr) pcapCmd = ps.Commands().Pcap(c.PcapPcap, displayFilterStr) }, &c.stage2Wg, Goroutinewg) //====================================================================== pdmlPidChan := make(chan int) pcapPidChan := make(chan int) pdmlTermChan := make(chan error) pcapTermChan := make(chan error) pdmlCtx, pdmlCancelFn := context.WithCancel(c.stage2Ctx) pcapCtx, pcapCancelFn := context.WithCancel(c.stage2Ctx) // // Goroutine to track pdml and pcap process lifetimes // termshark.TrackedGo(func() { select { case <-c.startChan: case <-c.stage2Ctx.Done(): return } var err error stage2CtxChan := c.stage2Ctx.Done() pdmlPidChan := pdmlPidChan pcapPidChan := pcapPidChan pdmlCancelledChan := pdmlCtx.Done() pcapCancelledChan := pcapCtx.Done() pdmlState := NotStarted pcapState := NotStarted killPcap := func() { err := termshark.KillIfPossible(pcapCmd) if err != nil { log.Infof("Did not kill pcap process: %v", err) } } killPdml := func() { err = termshark.KillIfPossible(pdmlCmd) if err != nil { log.Infof("Did not kill pdml process: %v", err) } } loop: for { select { case err = <-pdmlTermChan: pdmlState = Terminated case err = <-pcapTermChan: pcapState = Terminated case pid := <-pdmlPidChan: // this channel can be closed on a stage2 cancel, before the // pdml process has been started, meaning we get nil for the // pid. If that's the case, don't save the cmd, so we know not // to try to kill anything later. pdmlPidChan = nil // don't select on this channel again if pid != 0 { pdmlState = Started // gcla later todo - use lock? c.PdmlPid = pid if stage2CtxChan == nil || pdmlCancelledChan == nil { // means that stage2 has been cancelled (so stop the load), and // pdmlCmd != nil => for sure a process was started. So kill it. // It won't have been cleaned up anywhere else because Wait() is // only called below, in this goroutine. killPdml() } } case pid := <-pcapPidChan: pcapPidChan = nil // don't select on this channel again if pid != 0 { pcapState = Started c.PcapPid = pid if stage2CtxChan == nil || pcapCancelledChan == nil { killPcap() } } case <-pdmlCancelledChan: pdmlCancelledChan = nil // don't select on this channel again setPdmlCancelled() if pdmlState == Started { killPdml() } case <-pcapCancelledChan: pcapCancelledChan = nil // don't select on this channel again setPcapCancelled() if pcapState == Started { // means that for sure, a process was started killPcap() } case <-stage2CtxChan: // This will automatically signal pdmlCtx.Done and pcapCtx.Done() // Once the pcap/pdml load is initiated, we guarantee we get a stage2 cancel // once all the stage2 goroutines are finished. So we don't quit the select loop // until this channel (as well as the others) has received a signal stage2CtxChan = nil } // if pdmlpidchan is nil, it means the the channel has been closed or we've received a message // a message means the proc has started // closed means it won't be started // if closed, then pdmlCmd == nil // 04/11/21: I can't take a shortcut here and condition on Terminated || (cancelledChan == nil && NotStarted) // See the pcap or pdml goroutines below. I block at the beginning, checking on the stage2 cancellation. // If I get past that point, and there are no errors in the process invocation, I am guaranteed to start both // the pdml and pcap processes. If there are errors, I am guaranteed to close the pcapPidChan with a defer. // If I take a shortcut and end this goroutine via a stage2 cancellation before waiting for the pcap pid, // then I'll block in that goroutine, trying to send to the pcapPidChan, but with nothing here to receive // the value. In the pcap process goroutine, if I get past the stage2 cancellation check, then I need to // have something to receive the pid - this goroutine. It needs to stay alive until it gets the pid, or a // zero. if (pdmlState == Terminated || (pdmlPidChan == nil && c.PdmlPid == 0)) && (pcapState == Terminated || (pcapPidChan == nil && c.PcapPid == 0)) { // nothing to select on so break break loop } } }, Goroutinewg) //====================================================================== // // Goroutine to run pdml process // termshark.TrackedGo(func() { // Wait for stage 2 to be kicked off (potentially by psml load, then mapping table row to frame num); or // quit if that happens first select { case <-c.startChan: case <-c.stage2Ctx.Done(): close(pdmlPidChan) return } // We didn't get a stage2 cancel yet. We could now, but for now we've been told to continue // now we'll guarantee either: // - we'll send the pdml pid on pdmlPidChan if it starts // - we'll close the channel if it doesn't start pid := 0 defer func() { // Guarantee that at the end of this goroutine, if we didn't start a process (pid == 0) // we will close the channel to signal the Wait() goroutine above. if pid == 0 { close(pdmlPidChan) } }() pdmlOut, err := pdmlCmd.StdoutReader() if err != nil { HandleError(PdmlCode, app, err, cb) return } err = pdmlCmd.Start() if err != nil { err = fmt.Errorf("Error starting PDML process %v: %v", pdmlCmd, err) HandleError(PdmlCode, app, err, cb) return } log.Infof("Started PDML command %v with pid %d", pdmlCmd, pdmlCmd.Pid()) pid = pdmlCmd.Pid() pdmlPidChan <- pid d := xml.NewDecoder(pdmlOut) packets := make([]IPdmlPacket, 0, c.opt.PacketsPerLoad) issuedKill := false readAllRequiredPdml := false var packet PdmlPacket var cpacket IPdmlPacket Loop: for { tok, err := d.Token() if err != nil { if !issuedKill && unexpectedPdmlError(err) { err = fmt.Errorf("Could not read PDML data: %v", err) issuedKill = true pdmlCancelFn() HandleError(PdmlCode, app, err, cb) } if err == io.EOF { readAllRequiredPdml = true } break } switch tok := tok.(type) { case xml.StartElement: switch tok.Name.Local { case "packet": err := d.DecodeElement(&packet, &tok) if err != nil { if !issuedKill && unexpectedPdmlError(err) { err = fmt.Errorf("Could not decode PDML data: %v", err) issuedKill = true pdmlCancelFn() HandleError(PdmlCode, app, err, cb) } break Loop } // Enabled for now - do something more subtle perhaps in the future if true { cpacket = SnappyPdmlPacket(packet) } else { cpacket = packet } packets = append(packets, cpacket) ps.updateCacheEntryWithPdml(row, packets, false) if len(packets) == c.KillAfterReadingThisMany { // Shortcut - we never take more than abcdex - so just kill here issuedKill = true readAllRequiredPdml = true c.pdmlStoppedDeliberately_ = true pdmlCancelFn() } } } } // The Wait has to come after the last read, which is above pdmlTermChan <- pdmlCmd.Wait() // Want to preserve invariant - for simplicity - that we only add full loads // to the cache ps.MainRun(gowid.RunFunction(func(gowid.IApp) { // never evict row 0 ps.PacketCacheFn().Get(0) if c.highestCachedRow != -1 { // try not to evict "end" ps.PacketCacheFn().Get(c.highestCachedRow) } // the cache entry is marked complete if we are not reading from a fifo, which implies // the source of packets will not grow larger. If it could grow larger, we want to ensure // that termshark doesn't think that there are only 900 packets, because that's what's // in the cache from a previous request - now there might be 950 packets. // // If the PDML routine was stopped programmatically, that implies the load was not complete // so we don't mark the cache as complete then either. markComplete := false if !ps.ReadingFromFifo() && readAllRequiredPdml { markComplete = true } ps.updateCacheEntryWithPdml(row, packets, markComplete) if row > c.highestCachedRow { c.highestCachedRow = row } })) }, &c.stage2Wg, Goroutinewg) //====================================================================== // // Goroutine to run pcap process // termshark.TrackedGo(func() { // Wait for stage 2 to be kicked off (potentially by psml load, then mapping table row to frame num); or // quit if that happens first select { case <-c.startChan: case <-c.stage2Ctx.Done(): close(pcapPidChan) return } pid := 0 defer func() { if pid == 0 { close(pcapPidChan) } }() pcapOut, err := pcapCmd.StdoutReader() if err != nil { HandleError(PdmlCode, app, err, cb) return } err = pcapCmd.Start() if err != nil { // e.g. on the pi err = fmt.Errorf("Error starting PCAP process %v: %v", pcapCmd, err) HandleError(PdmlCode, app, err, cb) return } log.Infof("Started pcap command %v with pid %d", pcapCmd, pcapCmd.Pid()) pid = pcapCmd.Pid() pcapPidChan <- pid packets := make([][]byte, 0, c.opt.PacketsPerLoad) issuedKill := false readAllRequiredPcap := false re := regexp.MustCompile(`([0-9a-f][0-9a-f] )`) rd := bufio.NewReader(pcapOut) packet := make([]byte, 0) for { line, err := rd.ReadString('\n') if err != nil { if !issuedKill && unexpectedPcapError(err) { err = fmt.Errorf("Could not read PCAP packet: %v", err) HandleError(PdmlCode, app, err, cb) } if err == io.EOF { readAllRequiredPcap = true } break } parseResults := re.FindAllStringSubmatch(string(line), -1) if len(parseResults) < 1 { packets = append(packets, packet) packet = make([]byte, 0) readEnough := (len(packets) >= c.KillAfterReadingThisMany) ps.updateCacheEntryWithPcap(row, packets, false) if readEnough && !issuedKill { // Shortcut - we never take more than abcdex - so just kill here issuedKill = true readAllRequiredPcap = true pcapCancelFn() } } else { // Ignore line number for _, parsedByte := range parseResults[1:] { b, err := strconv.ParseUint(string(parsedByte[0][0:2]), 16, 8) if err != nil { err = fmt.Errorf("Could not read PCAP packet: %v", err) if !issuedKill { HandleError(PdmlCode, app, err, cb) } break } packet = append(packet, byte(b)) } } } // The Wait has to come after the last read, which is above pcapTermChan <- pcapCmd.Wait() // I just want to ensure I read it from ram, obviously this is racey // never evict row 0 ps.PacketCacheFn().Get(0) if c.highestCachedRow != -1 { // try not to evict "end" ps.PacketCacheFn().Get(c.highestCachedRow) } markComplete := false if !ps.ReadingFromFifo() && readAllRequiredPcap { markComplete = true } ps.updateCacheEntryWithPcap(row, packets, markComplete) }, &c.stage2Wg, Goroutinewg) }, Goroutinewg) } // waitForFileData sets an inotify watch on filename, and returns when a WRITE // event is seen. There is special logic for the case where the file is // removed; then the watcher is deleted and reinstated. This is to handle a // specific loading bug in termshark due to an optimized packet capture // process. To capture packets, termshark runs itself with a special env var // set. It detects this at startup, then launches dumpcap as the first // capture method. If this fails (e.g. the source is an extcap), it launches // tshark instead. Dumpcap is more efficient, but tshark is needed for the // extcap sources. The problem is that termshark needs a heuristic for when // packets have actually been detected - this is so it can wait to launch the // UI (in case a password is needed at the terminal, I don't want to obscure // that with the UI). So termshark waits for a WRITE to the pcap generated by // the capture process, and then launches tail. BUT - if dumpcap fails, it // will delete (unlink) the capture file passed to it with the -w argument // before tshark starts. If we don't watch for WRITE, this triggers the // notifier; then tail starts; then tail fails because depending on timing, // tshark may not have started yet and so the tail target pcap does not exist. // The fix is to monitor for inotify REMOVE too, and if seen, recreate the // pcap file (empty), and restart the watcher. And importantly, don't let // the tail process start until the WRITE event is seen. func waitForFileData(ctx context.Context, filename string, errFn func(error)) { OuterLoop: for { // this set up is so that I can detect when there are actually packets to read (e.g // maybe there's no traffic on the interface). When there's something to read, the // rest of the procedure can spring into action. Why not spring into action right away? // Because the tail command needs a file to exist to watch it with -f. Can I rely on // tail -F across all supported platforms? (e.g. Windows) watcher, err := fsnotify.NewWatcher() if err != nil { err = fmt.Errorf("Could not create FS watch: %v", err) errFn(err) return } defer watcher.Close() file, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0644) if err != nil { err = fmt.Errorf("Could not touch temporary pcap file %s: %v", filename, err) errFn(err) } file.Close() if err := watcher.Add(filename); err != nil { err = fmt.Errorf("Could not set up watcher for %s: %v", filename, err) errFn(err) return } removeWatcher := func(file string) { if watcher != nil { watcher.Remove(file) watcher = nil } } // Make sure that no matter what happens from here on, the watcher is not leaked. But we'll remove // it earlier under normal operation so that setting and removing watches with new loaders do not // race. defer removeWatcher(filename) NotifyLoop: for { select { case fe := <-watcher.Events: if fe.Name == filename { switch fe.Op { case fsnotify.Remove: removeWatcher(filename) continue OuterLoop default: break NotifyLoop } } case err := <-watcher.Errors: err = fmt.Errorf("Unexpected watcher error for %s: %v", filename, err) errFn(err) return case <-ctx.Done(): return } } break OuterLoop } } // loadPsmlSync starts tshark processes, and other processes, to generate PSML // data. There is coordination with the PDML loader via a channel, // startStage2Chan. If a filter is set, then we might need to read far more // than a block of 1000 PDML packets (via frame.number <= 4000, for example), // and we don't know how many to read until the PSML is loaded. We don't want // to only load one PDML packet at a time, and reload as the user hits arrow // down through the PSML (in the case the packets selected by the filter are // very spaced out). // // The flow is as follows: // - if the source of packets is a fifo/interface then // - create a pipe // - set PcapPsml to a Reader object that tracks bytes read from the pipe // - start the PSML tshark command and get its stdout // - if the source of packets is a fifo/interface then // - use inotify to wait for the tmp pcap file to appear // - start the tail command to read the tmp file created by the interface loader // - read the PSML and add to data structures // // Goroutines are started to track the process lifetimes of both processes. // func (p *PsmlLoader) loadPsmlSync(iloader *InterfaceLoader, e iPsmlLoaderEnv, cb interface{}, app gowid.IApp) { // Used to cancel the tickers below which update list widgets with the latest data and // update the progress meter. Note that if ctx is cancelled, then this context is cancelled // too. When the 2/3 data loading processes are done, a goroutine will then run uiCtxCancel() // to stop the UI updates. p.psmlCtx, p.psmlCancelFn = context.WithCancel(e.Context()) p.tailCtx, p.tailCancelFn = context.WithCancel(e.Context()) intPsmlCtx, intPsmlCancelFn := context.WithCancel(context.Background()) p.state = Loading //====================================================================== var psmlOut io.ReadCloser // Only start this process if we are in interface mode var err error var fifoPipeReader *os.File var fifoPipeWriter *os.File //====================================================================== // Make sure we start the goroutine that monitors for shutdown early - so if/when // a shutdown happens, and we get blocked in the XML parser, this will be able to // respond psmlPidChan := make(chan int) tailPidChan := make(chan int) psmlTermChan := make(chan error) tailTermChan := make(chan error) psmlPid := 0 // 0 means not running tailPid := 0 //====================================================================== termshark.TrackedGo(func() { e.MainRun(gowid.RunFunction(func(app gowid.IApp) { HandleBegin(PsmlCode, app, cb) })) defer func(ch chan struct{}) { // This will signal goroutines using select on this channel to terminate - like // ticker routines that update the packet list UI with new data every second. close(p.PsmlFinishedChan) e.MainRun(gowid.RunFunction(func(gowid.IApp) { HandleEnd(PsmlCode, app, cb) p.state = NotLoading p.psmlCancelFn = nil })) }(p.PsmlFinishedChan) //====================================================================== // Set to true by a goroutine started within here if ctxCancel() is called i.e. the outer context if e.DisplayFilter() == "" || p.ReadingFromFifo() { // don't hold up pdml and pcap generators. If the filter is "", then the frame numbers // equal the row numbers, so we don't need the psml to map from row -> frame. // // And, if we are in interface mode, we won't reach the end of the psml anyway. // close(p.startStage2Chan) } //====================================================================== closedPipe := false closePipe := func() { if !closedPipe { fifoPipeWriter.Close() fifoPipeReader.Close() closedPipe = true } } if p.ReadingFromFifo() { // PcapPsml will be nil if here // Build a pipe - the side to be read from will be given to the PSML process // and the side to be written to is given to the tail process, which feeds in // data from the pcap source. // fifoPipeReader, fifoPipeWriter, err = os.Pipe() if err != nil { err = fmt.Errorf("Could not create pipe: %v", err) HandleError(PsmlCode, app, err, cb) intPsmlCancelFn() return } // pw is used as Stdout for the tail command, which unwinds in this // goroutine - so we can close at this point in the unwinding. pr // is used as stdin for the psml command, which also runs in this // goroutine. defer func() { 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. p.PcapPsml = &tailReadTracker{ tailReader: fifoPipeReader, loader: iloader, tail: e, callback: cb, app: app, } } // Set c.PsmlCmd before it's referenced in the goroutine below. We want to be // sure that if if psmlCmd is nil then that means the process has finished (not // has not yet started) p.PsmlCmd = e.Commands().Psml(p.PcapPsml, e.DisplayFilter()) // this channel always needs to be signalled or else the goroutine below won't terminate. // Closing it will pass a zero-value int (pid) to the goroutine which will understand that // means the psml process is NOT running, so it won't call cmd.Wait() on it. defer func() { if psmlPid == 0 { close(psmlPidChan) } }() //====================================================================== // Goroutine to track process state changes termshark.TrackedGo(func() { cancelledChan := p.psmlCtx.Done() intCancelledChan := intPsmlCtx.Done() var err error psmlCmd := p.PsmlCmd pidChan := psmlPidChan state := NotStarted kill := func() { err := termshark.KillIfPossible(psmlCmd) if err != nil { log.Infof("Did not kill tshark psml process: %v", err) } if p.ReadingFromFifo() { closePipe() } } loop: for { select { case err = <-psmlTermChan: state = Terminated if !p.psmlStoppedDeliberately_ { if err != nil { if _, ok := err.(*exec.ExitError); ok { HandleError(PsmlCode, app, MakeUsefulError(psmlCmd, err), cb) } } } case <-cancelledChan: intPsmlCancelFn() // start internal shutdown cancelledChan = nil case <-intCancelledChan: intCancelledChan = nil if state == Started { kill() } case pid := <-pidChan: pidChan = nil if pid != 0 { state = Started if intCancelledChan == nil { kill() } } } if state == Terminated || (pidChan == nil && state == NotStarted) { break loop } } }, Goroutinewg) //====================================================================== psmlOut, err = p.PsmlCmd.StdoutReader() if err != nil { err = fmt.Errorf("Could not access pipe output: %v", err) HandleError(PsmlCode, app, err, cb) intPsmlCancelFn() return } err = p.PsmlCmd.Start() if err != nil { err = fmt.Errorf("Error starting PSML command %v: %v", p.PsmlCmd, err) HandleError(PsmlCode, app, err, cb) intPsmlCancelFn() return } log.Infof("Started PSML command %v with pid %d", p.PsmlCmd, p.PsmlCmd.Pid()) // Do this here because code later can return early - e.g. the watcher fails to be // set up - and then we'll never issue a Wait waitedForPsml := false // Prefer a defer rather than a goroutine here. That's because otherwise, this goroutine // and the XML processing routine reading the process's StdoutPipe are running in parallel, // and the XML routine should not issue a Read() (which it does behind the scenes) after // Wait() has been called. waitForPsml := func() { if !waitedForPsml { psmlTermChan <- p.PsmlCmd.Wait() waitedForPsml = true } } defer waitForPsml() psmlPid = p.PsmlCmd.Pid() psmlPidChan <- psmlPid //====================================================================== // If it was cancelled, then we don't need to start the tail process because // psml will read from the tmp pcap file generated by the interface reading // process. p.tailCmd = nil // Need to run dumpcap -i eth0 -w if p.ReadingFromFifo() { p.tailCmd = e.Commands().Tail(e.InterfaceFile()) defer func() { if tailPid == 0 { close(tailPidChan) } }() //====================================================================== // process lifetime goroutine for the tail process: // tshark -i > tmp // tail -f tmp | tshark -i - -t psml // ^^^^^^^^^^^ termshark.TrackedGo(func() { cancelledChan := p.tailCtx.Done() var err error tailCmd := p.tailCmd pidChan := tailPidChan state := NotStarted kill := func() { err := termshark.KillIfPossible(tailCmd) if err != nil { log.Infof("Did not kill tshark tail process: %v", err) } } loop: for { select { case err = <-tailTermChan: state = Terminated // Don't close the pipe - the psml might not have finished reading yet // gcla later todo - is this right or wrong // Close the pipe so that the psml reader gets EOF and will also terminate; // otherwise the PSML reader will block waiting for more data from the pipe fifoPipeWriter.Close() if !p.psmlStoppedDeliberately_ && !e.TailStoppedDeliberately() { if err != nil { if _, ok := err.(*exec.ExitError); ok { HandleError(PsmlCode, app, MakeUsefulError(tailCmd, err), cb) } } } case <-cancelledChan: cancelledChan = nil if state == Started { kill() } case pid := <-pidChan: pidChan = nil if pid != 0 { state = Started if cancelledChan == nil { kill() } } } // successfully started then died/kill, OR // was never started, won't be started, and cancelled if state == Terminated || (pidChan == nil && state == NotStarted) { break loop } } }, Goroutinewg) //====================================================================== p.tailCmd.SetStdout(fifoPipeWriter) waitForFileData(intPsmlCtx, e.InterfaceFile(), func(err error) { HandleError(PsmlCode, app, err, cb) intPsmlCancelFn() p.tailCancelFn() // needed to end the goroutine, end if tailcmd has not started }, ) log.Infof("Starting Tail command: %v", p.tailCmd) err = p.tailCmd.Start() if err != nil { err = fmt.Errorf("Could not start tail command %v: %v", p.tailCmd, err) HandleError(PsmlCode, app, err, cb) intPsmlCancelFn() p.tailCancelFn() // needed to end the goroutine, end if tailcmd has not started return } termshark.TrackedGo(func() { tailTermChan <- p.tailCmd.Wait() }, Goroutinewg) tailPid = p.tailCmd.Pid() tailPidChan <- tailPid } // end of reading from fifo //====================================================================== // // Goroutine to read psml xml and update data structures // defer func(ch chan struct{}) { select { case <-ch: // already done/closed, do nothing default: close(ch) } // This will kill the tail process if there is one intPsmlCancelFn() // stop the ticker }(p.startStage2Chan) d := xml.NewDecoder(psmlOut) // //
1
//
0.000000
//
192.168.44.123
//
192.168.44.213
//
TFTP
//
77
//
Read Request, File: C:\IBMTCPIP\lccm.1, Transfer type: octet
//
var curPsml []string var curCounts []int var fg string var bg string var pidx int ppidx := 0 // the previous packet number read; 0 means no packet. I can use 0 because // the psml I read will start at packet 1 so - map[0] => 1st packet ready := false empty := true structure := false for { if intPsmlCtx.Err() != nil { break } tok, err := d.Token() if err != nil { // gcla later todo - LoadWasCancelled is checked outside of the main goroutine here if err != io.EOF && !e.LoadWasCancelled() { err = fmt.Errorf("Could not read PSML data: %v", err) HandleError(PsmlCode, app, err, cb) } break } switch tok := tok.(type) { case xml.EndElement: switch tok.Name.Local { case "structure": structure = false p.Lock() // Don't keep the first column - we add on column number to all PSML // loads whether or not the user wants it to track table number -> packet // number. This is then stripped from the columns shown to the user. p.packetPsmlHeaders = p.packetPsmlHeaders[1:] p.Unlock() case "packet": p.Lock() // Track the mapping of packet number
12
to position // in the table e.g. 5th element. This is so that I can jump to the correct // row with marks even if a filter is currently applied. pidx, err = strconv.Atoi(curPsml[0]) if err != nil { log.Fatal(err) } p.PacketNumberMap[pidx] = len(p.packetPsmlData) p.PacketNumberOrder[ppidx] = pidx ppidx = pidx p.packetPsmlData = append(p.packetPsmlData, curPsml[1:]) if len(p.packetAverageLength) > len(curPsml)-1 { p.packetAverageLength = p.packetAverageLength[0 : len(curPsml)-1] } if len(p.packetMaxLength) > len(curPsml)-1 { p.packetMaxLength = p.packetMaxLength[0 : len(curPsml)-1] } for i, ct := range curCounts[1:] { // skip the first one - that's not displayed in the UI. We always have element 0 as No. p.packetAverageLength[i].update(ct) p.packetMaxLength[i].update(ct) } p.packetPsmlColors = append(p.packetPsmlColors, PacketColors{ FG: psmlColorToIColor(fg), BG: psmlColorToIColor(bg), }) p.Unlock() case "section": ready = false // Means we got without any char data i.e. empty
if empty { curCounts = append(curCounts, 0) curPsml = append(curPsml, "") } } case xml.StartElement: switch tok.Name.Local { case "structure": structure = true case "packet": curPsml = make([]string, 0, 10) curCounts = make([]int, 0, 10) fg = "" bg = "" for _, attr := range tok.Attr { switch attr.Name.Local { case "foreground": fg = attr.Value case "background": bg = attr.Value } } case "section": ready = true empty = true } case xml.CharData: if ready { if structure { p.Lock() p.packetPsmlHeaders = append(p.packetPsmlHeaders, string(tok)) p.Unlock() e.MainRun(gowid.RunFunction(func(app gowid.IApp) { handlePsmlHeader(PsmlCode, app, cb) })) } else { curPsml = append(curPsml, string(format.TranslateHexCodes(tok))) curCounts = append(curCounts, len(curPsml[len(curPsml)-1])) empty = false } } } } }, Goroutinewg) } func (c *PsmlLoader) DoWithPsmlData(fn func([][]string)) { c.Lock() defer c.Unlock() fn(c.packetPsmlData) } func (c *PsmlLoader) ReadingFromFifo() bool { // If it's a string it means that it's a filename, so it's not a fifo. Other values // in practise are the empty interface, or the read end of a fifo _, ok := c.PcapPsml.(string) return !ok } func (c *PsmlLoader) IsLoading() bool { return c.state == Loading } func (c *PsmlLoader) StartStage2ChanFn() chan struct{} { return c.startStage2Chan } func (c *PsmlLoader) PacketCacheFn() *lru.Cache { // i -> [pdml(i * 1000)..pdml(i+1*1000)] return c.PacketCache } // Assumes this is a clean stop, not an error func (p *ParentLoader) stopTail() { p.tailStoppedDeliberately = true if p.tailCancelFn != nil { p.tailCancelFn() } } func (p *PsmlLoader) PacketsPerLoad() int { p.Lock() defer p.Unlock() return p.opt.PacketsPerLoad } func (p *PsmlLoader) stopLoadPsml() { p.psmlStoppedDeliberately_ = true if p.psmlCancelFn != nil { p.psmlCancelFn() } } func (p *PsmlLoader) PsmlData() [][]string { return p.packetPsmlData } func (p *PsmlLoader) PsmlHeaders() []string { return p.packetPsmlHeaders } func (p *PsmlLoader) PsmlColors() []PacketColors { return p.packetPsmlColors } func (p *PsmlLoader) PsmlAverageLengths() []gwutil.IntOption { res := make([]gwutil.IntOption, 0, len(p.packetAverageLength)) for _, avg := range p.packetAverageLength { res = append(res, avg.average()) } return res } func (p *PsmlLoader) PsmlMaxLengths() []int { res := make([]int, 0, len(p.packetMaxLength)) for _, maxer := range p.packetMaxLength { res = append(res, int(maxer.max())) } return res } // if done==true, then this cache entry is complete func (p *PsmlLoader) updateCacheEntryWithPdml(row int, pdml []IPdmlPacket, done bool) { var ce CacheEntry p.Lock() defer p.Unlock() if ce2, ok := p.PacketCache.Get(row); ok { ce = ce2.(CacheEntry) } ce.Pdml = pdml ce.PdmlComplete = done p.PacketCache.Add(row, ce) } func (p *PsmlLoader) updateCacheEntryWithPcap(row int, pcap [][]byte, done bool) { var ce CacheEntry p.Lock() defer p.Unlock() if ce2, ok := p.PacketCache.Get(row); ok { ce = ce2.(CacheEntry) } ce.Pcap = pcap ce.PcapComplete = done p.PacketCache.Add(row, ce) } func (p *PsmlLoader) LengthOfPdmlCacheEntry(row int) (int, error) { p.Lock() defer p.Unlock() if ce, ok := p.PacketCache.Get(row); ok { ce2 := ce.(CacheEntry) return len(ce2.Pdml), nil } return -1, fmt.Errorf("No cache entry found for row %d", row) } func (p *PsmlLoader) LengthOfPcapCacheEntry(row int) (int, error) { p.Lock() defer p.Unlock() if ce, ok := p.PacketCache.Get(row); ok { ce2 := ce.(CacheEntry) return len(ce2.Pcap), nil } return -1, fmt.Errorf("No cache entry found for row %d", row) } func (c *PsmlLoader) CacheAt(row int) (CacheEntry, bool) { if ce, ok := c.PacketCache.Get(row); ok { return ce.(CacheEntry), ok } return CacheEntry{}, false } func (c *PsmlLoader) NumLoaded() int { c.Lock() defer c.Unlock() return len(c.packetPsmlData) } //====================================================================== func (c *PdmlLoader) IsLoading() bool { return c.state == Loading } func (c *PdmlLoader) LoadIsVisible() bool { return c.visible } // Only call from main goroutine func (c *PdmlLoader) LoadingRow() int { return c.rowCurrentlyLoading } func (p *PdmlLoader) stopLoadPdml() { p.pdmlStoppedDeliberately_ = true if p.stage2CancelFn != nil { p.stage2CancelFn() } } //====================================================================== type iTailCommand interface { stopTail() } type iIfaceLoaderEnv interface { iLoaderEnv iTailCommand PsmlStoppedDeliberately() bool InterfaceFile() string PacketSources() []IPacketSource CaptureFilter() string } // dumpcap -i eth0 -w /tmp/foo.pcap // dumpcap -i /dev/fd/3 -w /tmp/foo.pcap func (i *InterfaceLoader) loadIfacesSync(e iIfaceLoaderEnv, cb interface{}, app gowid.IApp) { i.totalFifoBytesWritten = gwutil.NoneInt64() i.ifaceCtx, i.ifaceCancelFn = context.WithCancel(e.Context()) log.Infof("Starting Iface command: %v", i.ifaceCmd) pid := 0 ifacePidChan := make(chan int) defer func() { if pid == 0 { close(ifacePidChan) } }() // tshark -i eth0 -w foo.pcap i.ifaceCmd = e.Commands().Iface(SourcesNames(e.PacketSources()), e.CaptureFilter(), e.InterfaceFile()) err := i.ifaceCmd.Start() if err != nil { err = fmt.Errorf("Error starting interface reader %v: %v", i.ifaceCmd, err) HandleError(IfaceCode, app, err, cb) return } ifaceTermChan := make(chan error) i.state = Loading log.Infof("Started Iface command %v with pid %d", i.ifaceCmd, i.ifaceCmd.Pid()) // Do this in a goroutine because the function is expected to return quickly termshark.TrackedGo(func() { ifaceTermChan <- i.ifaceCmd.Wait() }, Goroutinewg) //====================================================================== // Process goroutine termshark.TrackedGo(func() { defer func() { // if psrc is a PipeSource, then we open /dev/fd/3 in termshark, and reroute descriptor // stdin to number 3 when termshark starts. So to kill the process writing in, we need // to close our side of the pipe. for _, psrc := range e.PacketSources() { if cl, ok := psrc.(io.Closer); ok { cl.Close() } } e.MainRun(gowid.RunFunction(func(gowid.IApp) { i.state = NotLoading i.ifaceCancelFn = nil })) }() cancelledChan := i.ifaceCtx.Done() state := NotStarted var err error pidChan := ifacePidChan ifaceCmd := i.ifaceCmd killIface := func() { err = termshark.KillIfPossible(i.ifaceCmd) if err != nil { log.Infof("Did not kill iface process: %v", err) } } loop: for { select { case err = <-ifaceTermChan: state = Terminated if !e.PsmlStoppedDeliberately() && err != nil { if _, ok := err.(*exec.ExitError); ok { // This could be if termshark is started like this: cat nosuchfile.pcap | termshark -i - // Then dumpcap will be started with /dev/fd/3 as its stdin, but will fail with EOF and // exit status 1. HandleError(IfaceCode, app, MakeUsefulError(ifaceCmd, err), cb) } } case pid := <-pidChan: // this channel can be closed on a stage2 cancel, before the // pdml process has been started, meaning we get nil for the // pid. If that's the case, don't save the cmd, so we know not // to try to kill anything later. pidChan = nil if pid != 0 { state = Started if cancelledChan == nil { killIface() } } case <-cancelledChan: cancelledChan = nil if state == Started { killIface() } } // if pdmlpidchan is nil, it means the the channel has been closed or we've received a message // a message means the proc has started // closed means it won't be started // if closed, then pdmlCmd == nil if state == Terminated || (pidChan == nil && state == NotStarted) { // nothing to select on so break break loop } } // Calculate the final size of the tmp file we wrote with packets read from the // interface/pipe. This runs after the dumpcap command finishes. fi, err := os.Stat(e.InterfaceFile()) i.Lock() if err != nil { log.Warn(err) // Deliberately not a fatal error - it can happen if the source of packets to tshark -i // is corrupt, resulting in a tshark error. Setting zero here will line up with the // reading end which will read zero, and so terminate the tshark -T psml procedure. if i.fifoError == nil && !os.IsNotExist(err) { // Ignore ENOENT because it means there was an error before dumpcap even wrote // anything to disk i.fifoError = err } } else { i.totalFifoBytesWritten = gwutil.SomeInt64(fi.Size()) } i.Unlock() i.checkAllBytesRead(e, cb, app) }, Goroutinewg) //====================================================================== pid = i.ifaceCmd.Pid() ifacePidChan <- pid } // checkAllBytesRead is called (a) when the tshark -i process is finished // writing to the tmp file and (b) every time the tmpfile tail process reads // bytes. totalFifoBytesWrite is set to non-nil only when the tail process // completes. totalFifoBytesRead is updated every read. If they are every // found to be equal, it means that (1) the tail process has finished, meaning // killed or has reached EOF with its packet source (e.g. stdin, fifo) and (2) // the tail process has read all those bytes - so no packets will be // missed. In that case, the tail process is killed and its stdout closed, // which will trigger the psml reading process to shut down, and termshark // will turn off its loading UI. func (i *InterfaceLoader) checkAllBytesRead(e iTailCommand, cb interface{}, app gowid.IApp) { cancel := false if !i.totalFifoBytesWritten.IsNone() && !i.totalFifoBytesRead.IsNone() { if i.totalFifoBytesRead.Val() == i.totalFifoBytesWritten.Val() { cancel = true } } if i.fifoError != nil { cancel = true } // if there was a fifo error, OR we have read all the bytes that were written, then // we need to stop the tail command if cancel { if i.fifoError != nil { err := fmt.Errorf("Fifo error: %v", i.fifoError) HandleError(IfaceCode, app, err, cb) } e.stopTail() } } func (i *InterfaceLoader) stopLoadIface() { if i != nil && i.ifaceCancelFn != nil { i.ifaceCancelFn() } } func (c *InterfaceLoader) IsLoading() bool { return c != nil && c.state == Loading } //====================================================================== type CacheEntry struct { Pdml []IPdmlPacket Pcap [][]byte PdmlComplete bool PcapComplete bool } func (c CacheEntry) Complete() bool { return c.PdmlComplete && c.PcapComplete } //====================================================================== type LoadPcapSlice struct { Row int CancelCurrent bool Jump int // 0 means no jump } func (m LoadPcapSlice) String() string { pieces := make([]string, 0, 3) pieces = append(pieces, fmt.Sprintf("loadslice: %d", m.Row)) if m.CancelCurrent { pieces = append(pieces, fmt.Sprintf("cancelcurrent: %v", m.CancelCurrent)) } if m.Jump != 0 { pieces = append(pieces, fmt.Sprintf("jumpto: %d", m.Jump)) } return fmt.Sprintf("[%s]", strings.Join(pieces, ", ")) } //====================================================================== func ProcessPdmlRequests(requests []LoadPcapSlice, mloader *ParentLoader, loader *PdmlLoader, cb interface{}, app gowid.IApp) []LoadPcapSlice { Loop: for { if len(requests) == 0 { break } else { ev := requests[0] if !mloader.loadIsNecessary(ev) { requests = requests[1:] } else { if loader.state == Loading { if ev.CancelCurrent { loader.stopLoadPdml() } } else { mloader.RenewPdmlLoader() // ops? mloader.loadPcapSync(ev.Row, ev.CancelCurrent, mloader, cb, app) requests = requests[1:] } break Loop } } } return requests } //====================================================================== func psmlColorToIColor(col string) gowid.IColor { if res, err := gowid.MakeRGBColorSafe(col); err != nil { return nil } else { return res } } // https://stackoverflow.com/a/28005931/784226 func TempPcapFile(tokens ...string) string { tokensClean := make([]string, 0, len(tokens)) for _, token := range tokens { re := regexp.MustCompile(`[^a-zA-Z0-9.-]`) tokensClean = append(tokensClean, re.ReplaceAllString(token, "_")) } tokenClean := strings.Join(tokensClean, "-") return filepath.Join(termshark.PcapDir(), fmt.Sprintf("%s--%s.pcap", tokenClean, termshark.DateStringForFilename(), )) } //====================================================================== // Local Variables: // mode: Go // fill-column: 78 // End: termshark-2.4.0/pkg/pcap/loader_tshark_test.go000066400000000000000000000366131426312004500214370ustar00rootroot00000000000000// Copyright 2019-2022 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.4.0/pkg/pcap/pdml.go000066400000000000000000000051611426312004500165040ustar00rootroot00000000000000// Copyright 2019-2022 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.4.0/pkg/pcap/source.go000066400000000000000000000074351426312004500170560ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. package pcap import ( "fmt" "os" "strings" "github.com/gcla/termshark/v2/pkg/system" ) //====================================================================== type IPacketSource interface { Name() string IsFile() bool IsInterface() bool IsFifo() bool IsPipe() bool } //====================================================================== func FileSystemSources(srcs []IPacketSource) []IPacketSource { res := make([]IPacketSource, 0) for _, src := range srcs { if src.IsFile() { res = append(res, src) } } return res } func SourcesString(srcs []IPacketSource) string { return strings.Join(SourcesNames(srcs), " + ") } func SourcesNames(srcs []IPacketSource) []string { names := make([]string, 0, len(srcs)) for _, psrc := range srcs { names = append(names, psrc.Name()) } return names } func UIName(src IPacketSource) string { if src.IsPipe() { return "" } else { return src.Name() } } func CanRestart(src IPacketSource) bool { return src.IsFile() || src.IsInterface() } //====================================================================== type FileSource struct { Filename string } var _ IPacketSource = FileSource{} func (p FileSource) Name() string { return p.Filename } func (p FileSource) IsFile() bool { return true } func (p FileSource) IsInterface() bool { return false } func (p FileSource) IsFifo() bool { return false } func (p FileSource) IsPipe() bool { return false } func (p FileSource) String() string { return fmt.Sprintf("File:%s", p.Filename) } //====================================================================== type TemporaryFileSource struct { FileSource } type ISourceRemover interface { Remove() error } func (p TemporaryFileSource) Remove() error { return os.Remove(p.Filename) } func (p TemporaryFileSource) String() string { return fmt.Sprintf("TempFile:%s", p.Filename) } //====================================================================== type InterfaceSource struct { Iface string } var _ IPacketSource = InterfaceSource{} func (p InterfaceSource) Name() string { return p.Iface } func (p InterfaceSource) IsFile() bool { return false } func (p InterfaceSource) IsInterface() bool { return true } func (p InterfaceSource) IsFifo() bool { return false } func (p InterfaceSource) IsPipe() bool { return false } func (p InterfaceSource) String() string { return fmt.Sprintf("Interface:%s", p.Iface) } //====================================================================== type FifoSource struct { Filename string } var _ IPacketSource = FifoSource{} func (p FifoSource) Name() string { return p.Filename } func (p FifoSource) IsFile() bool { return false } func (p FifoSource) IsInterface() bool { return false } func (p FifoSource) IsFifo() bool { return true } func (p FifoSource) IsPipe() bool { return false } func (p FifoSource) String() string { return fmt.Sprintf("Fifo:%s", p.Filename) } //====================================================================== type PipeSource struct { Descriptor string Fd int } var _ IPacketSource = PipeSource{} func (p PipeSource) Name() string { return p.Descriptor } func (p PipeSource) IsFile() bool { return false } func (p PipeSource) IsInterface() bool { return false } func (p PipeSource) IsFifo() bool { return false } func (p PipeSource) IsPipe() bool { return true } func (p PipeSource) Close() error { system.CloseDescriptor(p.Fd) return nil } func (p PipeSource) String() string { return fmt.Sprintf("Pipe:%s(%d)", p.Descriptor, p.Fd) } //====================================================================== // Local Variables: // mode: Go // fill-column: 78 // End: termshark-2.4.0/pkg/pcap/testdata/000077500000000000000000000000001426312004500170275ustar00rootroot00000000000000termshark-2.4.0/pkg/pcap/testdata/1.hexdump000066400000000000000000000234051426312004500205670ustar00rootroot000000000000000000 30 fd 38 d2 76 12 e8 de 27 19 de 6c 08 00 45 00 0.8.v...'..l..E. 0010 00 73 7d ef 40 00 40 06 8a 64 c0 a8 56 f6 34 14 .s}.@.@..d..V.4. 0020 e6 7e a4 c8 01 bb 3e ad 82 88 17 e6 8f 97 80 18 .~....>......... 0030 01 3f 7d 3b 00 00 01 01 08 0a 0c 5d 30 7f 3f 24 .?};.......]0.?$ 0040 4a 9b 17 03 03 00 3a 00 00 00 00 00 00 01 8c 43 J.....:........C 0050 07 f0 2f f9 b2 7b 87 1a a3 39 94 c2 2e a0 3b 79 ../..{...9....;y 0060 76 87 b5 67 c1 c0 aa d4 42 59 18 e6 9f f7 92 ca v..g....BY...... 0070 ad 7d e9 30 eb e4 fe 6d fe 1a f6 b9 4c 59 af 64 .}.0...m....LY.d 0080 64 d 0000 e8 de 27 19 de 6c 30 fd 38 d2 76 12 08 00 45 00 ..'..l0.8.v...E. 0010 00 59 23 f9 40 00 ef 06 35 74 34 14 e6 7e c0 a8 .Y#.@...5t4..~.. 0020 56 f6 01 bb a4 c8 17 e6 8f 97 3e ad 82 c7 80 18 V.........>..... 0030 00 75 71 4a 00 00 01 01 08 0a 3f 24 54 60 0c 5d .uqJ......?$T`.] 0040 30 7f 17 03 03 00 20 00 00 00 00 00 00 01 8c e3 0..... ......... 0050 85 15 a7 9d ac aa 2d 70 e8 86 63 ec 68 c4 5a 15 ......-p..c.h.Z. 0060 d8 39 73 e6 e1 a4 e8 .9s.... 0000 30 fd 38 d2 76 12 e8 de 27 19 de 6c 08 00 45 00 0.8.v...'..l..E. 0010 00 34 7d f0 40 00 40 06 8a a2 c0 a8 56 f6 34 14 .4}.@.@.....V.4. 0020 e6 7e a4 c8 01 bb 3e ad 82 c7 17 e6 8f bc 80 10 .~....>......... 0030 01 3f 63 42 00 00 01 01 08 0a 0c 5d 30 8e 3f 24 .?cB.......]0.?$ 0040 54 60 T` 0000 30 fd 38 d2 76 12 e8 de 27 19 de 6c 08 00 45 00 0.8.v...'..l..E. 0010 00 53 23 16 40 00 40 06 9e ab c0 a8 56 f6 1f 0d .S#.@.@.....V... 0020 42 38 a2 ea 01 bb 9b 73 44 01 3f cd d0 cb 80 18 B8.....sD.?..... 0030 04 86 34 9e 00 00 01 01 08 0a 7f 84 4e 30 90 2e ..4.........N0.. 0040 e5 24 17 03 03 00 1a 51 02 04 20 f7 1d 99 45 8a .$.....Q.. ...E. 0050 99 3d a0 86 74 df f5 b2 6e dc 16 a4 56 54 11 34 .=..t...n...VT.4 0060 a1 . 0000 e8 de 27 19 de 6c 30 fd 38 d2 76 12 08 00 45 00 ..'..l0.8.v...E. 0010 00 5a 07 d4 40 00 58 06 a1 e6 1f 0d 42 38 c0 a8 .Z..@.X.....B8.. 0020 56 f6 01 bb a2 ea 3f cd d0 cb 9b 73 44 20 80 18 V.....?....sD .. 0030 00 c1 5d ea 00 00 01 01 08 0a 90 2f 39 1e 7f 84 ..]......../9... 0040 4e 30 17 03 03 00 21 3a 3f 24 21 3e 3a 54 25 3c N0....!:?$!>:T%< 0050 fe af 72 f2 61 fb c4 a3 c4 19 1e e5 cc 25 96 e2 ..r.a........%.. 0060 7d 36 8b ef 94 f6 fb 95 }6...... 0000 30 fd 38 d2 76 12 e8 de 27 19 de 6c 08 00 45 00 0.8.v...'..l..E. 0010 00 34 23 17 40 00 40 06 9e c9 c0 a8 56 f6 1f 0d .4#.@.@.....V... 0020 42 38 a2 ea 01 bb 9b 73 44 20 3f cd d0 f1 80 10 B8.....sD ?..... 0030 04 86 cd 34 00 00 01 01 08 0a 7f 84 4e 54 90 2f ...4........NT./ 0040 39 1e 9. 0000 01 00 5e 7f ff fa e8 de 27 19 de 6c 08 00 45 00 ..^.....'..l..E. 0010 00 81 79 35 40 00 01 11 f8 9d c0 a8 56 f6 ef ff ..y5@.......V... 0020 ff fa c1 75 07 6c 00 6d 3b 89 4d 2d 53 45 41 52 ...u.l.m;.M-SEAR 0030 43 48 20 2a 20 48 54 54 50 2f 31 2e 31 0d 0a 48 CH * HTTP/1.1..H 0040 4f 53 54 3a 20 32 33 39 2e 32 35 35 2e 32 35 35 OST: 239.255.255 0050 2e 32 35 30 3a 31 39 30 30 0d 0a 4d 41 4e 3a 20 .250:1900..MAN: 0060 22 73 73 64 70 3a 64 69 73 63 6f 76 65 72 22 0d "ssdp:discover". 0070 0a 53 54 3a 20 75 70 6e 70 3a 72 6f 6f 74 64 65 .ST: upnp:rootde 0080 76 69 63 65 0d 0a 4d 58 3a 20 35 0d 0a 0d 0a vice..MX: 5.... 0000 01 00 5e 7f ff fa 28 b2 bd 04 5c 7c 08 00 45 00 ..^...(...\|..E. 0010 00 81 da 35 40 00 01 11 98 48 c0 a8 56 4b ef ff ...5@....H..VK.. 0020 ff fa df 09 07 6c 00 6d 1e a0 4d 2d 53 45 41 52 .....l.m..M-SEAR 0030 43 48 20 2a 20 48 54 54 50 2f 31 2e 31 0d 0a 48 CH * HTTP/1.1..H 0040 4f 53 54 3a 20 32 33 39 2e 32 35 35 2e 32 35 35 OST: 239.255.255 0050 2e 32 35 30 3a 31 39 30 30 0d 0a 4d 41 4e 3a 20 .250:1900..MAN: 0060 22 73 73 64 70 3a 64 69 73 63 6f 76 65 72 22 0d "ssdp:discover". 0070 0a 53 54 3a 20 75 70 6e 70 3a 72 6f 6f 74 64 65 .ST: upnp:rootde 0080 76 69 63 65 0d 0a 4d 58 3a 20 35 0d 0a 0d 0a vice..MX: 5.... 0000 e8 de 27 19 de 6c 30 fd 38 d2 76 12 08 00 45 00 ..'..l0.8.v...E. 0010 00 56 f4 3b 40 00 38 06 af f9 ac 68 da 65 c0 a8 .V.;@.8....h.e.. 0020 56 f6 01 bb ad d8 12 45 09 4e 49 30 76 b2 80 18 V......E.NI0v... 0030 00 13 c9 19 00 00 01 01 08 0a 8b c9 4b 15 4e a9 ............K.N. 0040 85 74 17 03 03 00 1d 3a ab 21 78 55 55 d8 97 dc .t.....:.!xUU... 0050 87 fd 4d a6 5d bf 4e cd 3b 67 dc 44 eb b5 93 a2 ..M.].N.;g.D.... 0060 cc d4 aa 80 .... 0000 30 fd 38 d2 76 12 e8 de 27 19 de 6c 08 00 45 00 0.8.v...'..l..E. 0010 00 34 09 f6 40 00 40 06 92 61 c0 a8 56 f6 ac 68 .4..@.@..a..V..h 0020 da 65 ad d8 01 bb 49 30 76 b2 12 45 09 70 80 10 .e....I0v..E.p.. 0030 01 3f 7a 10 00 00 01 01 08 0a 4e a9 ac 4d 8b c9 .?z.......N..M.. 0040 4b 15 K. 0000 e8 de 27 19 de 6c 30 fd 38 d2 76 12 08 00 45 00 ..'..l0.8.v...E. 0010 01 74 71 6b 40 00 40 11 99 c5 c0 a8 56 01 c0 a8 .tqk@.@.....V... 0020 56 f6 07 6c c1 75 01 60 c9 bf 48 54 54 50 2f 31 V..l.u.`..HTTP/1 0030 2e 31 20 32 30 30 20 4f 4b 0d 0a 43 41 43 48 45 .1 200 OK..CACHE 0040 2d 43 4f 4e 54 52 4f 4c 3a 20 6d 61 78 2d 61 67 -CONTROL: max-ag 0050 65 3d 31 32 30 0d 0a 53 54 3a 20 75 70 6e 70 3a e=120..ST: upnp: 0060 72 6f 6f 74 64 65 76 69 63 65 0d 0a 55 53 4e 3a rootdevice..USN: 0070 20 75 75 69 64 3a 36 35 39 62 35 35 64 66 2d 64 uuid:659b55df-d 0080 65 35 38 2d 34 30 64 65 2d 39 37 36 61 2d 64 36 e58-40de-976a-d6 0090 61 36 38 32 62 38 65 33 37 63 3a 3a 75 70 6e 70 a682b8e37c::upnp 00a0 3a 72 6f 6f 74 64 65 76 69 63 65 0d 0a 45 58 54 :rootdevice..EXT 00b0 3a 0d 0a 53 45 52 56 45 52 3a 20 63 68 72 6f 6d :..SERVER: chrom 00c0 69 75 6d 6f 73 2f 72 6f 6c 6c 69 6e 67 20 55 50 iumos/rolling UP 00d0 6e 50 2f 31 2e 31 20 4d 69 6e 69 55 50 6e 50 64 nP/1.1 MiniUPnPd 00e0 2f 31 2e 39 0d 0a 4c 4f 43 41 54 49 4f 4e 3a 20 /1.9..LOCATION: 00f0 68 74 74 70 3a 2f 2f 31 39 32 2e 31 36 38 2e 38 http://192.168.8 0100 36 2e 31 3a 35 30 30 30 2f 72 6f 6f 74 44 65 73 6.1:5000/rootDes 0110 63 2e 78 6d 6c 0d 0a 4f 50 54 3a 20 22 68 74 74 c.xml..OPT: "htt 0120 70 3a 2f 2f 73 63 68 65 6d 61 73 2e 75 70 6e 70 p://schemas.upnp 0130 2e 6f 72 67 2f 75 70 6e 70 2f 31 2f 30 2f 22 3b .org/upnp/1/0/"; 0140 20 6e 73 3d 30 31 0d 0a 30 31 2d 4e 4c 53 3a 20 ns=01..01-NLS: 0150 31 0d 0a 42 4f 4f 54 49 44 2e 55 50 4e 50 2e 4f 1..BOOTID.UPNP.O 0160 52 47 3a 20 31 0d 0a 43 4f 4e 46 49 47 49 44 2e RG: 1..CONFIGID. 0170 55 50 4e 50 2e 4f 52 47 3a 20 31 33 33 37 0d 0a UPNP.ORG: 1337.. 0180 0d 0a .. 0000 38 8b 59 1f a9 a3 e8 de 27 19 de 6c 08 00 45 00 8.Y.....'..l..E. 0010 00 a9 24 1b 40 00 40 06 e7 d6 c0 a8 56 f6 c0 a8 ..$.@.@.....V... 0020 56 16 94 dc 1f 49 da 81 c8 98 8d 30 da 0f 80 18 V....I.....0.... 0030 01 67 88 b9 00 00 01 01 08 0a 55 f7 3c 8a 00 69 .g........U.<..i 0040 65 57 17 03 03 00 70 00 00 00 00 00 00 03 15 56 eW....p........V 0050 f6 bc 8d 80 4b 21 58 8b 2a 50 a3 f5 ec 78 14 38 ....K!X.*P...x.8 0060 7f da fd 34 2e ff 8e 10 d3 be 0b a4 3f 9e 2d 45 ...4........?.-E 0070 fd 71 e7 dd ea 28 c0 c0 27 87 2a 36 45 e6 00 d0 .q...(..'.*6E... 0080 e8 27 6c 04 d3 3d 7a 5f ff bb 49 3a 3b 92 33 0b .'l..=z_..I:;.3. 0090 e3 e7 87 6e 7c ab a1 72 fe 19 54 49 db 24 12 58 ...n|..r..TI.$.X 00a0 ea 0d f6 4e e1 11 76 21 00 82 4d 66 51 c7 74 9b ...N..v!..MfQ.t. 00b0 34 70 b2 8f e7 21 85 4p...!. 0000 38 8b 59 1f a9 a3 e8 de 27 19 de 6c 08 00 45 00 8.Y.....'..l..E. 0010 00 34 24 1c 40 00 40 06 e8 4a c0 a8 56 f6 c0 a8 .4$.@.@..J..V... 0020 56 16 94 dc 1f 49 da 81 c9 0d 8d 30 da 84 80 10 V....I.....0.... 0030 01 67 8d 46 00 00 01 01 08 0a 55 f7 3c 9b 00 69 .g.F......U.<..i 0040 67 4c gL 0000 e8 de 27 19 de 6c 30 fd 38 d2 76 12 08 06 00 01 ..'..l0.8.v..... 0010 08 00 06 04 00 01 30 fd 38 d2 76 12 c0 a8 56 01 ......0.8.v...V. 0020 00 00 00 00 00 00 c0 a8 56 f6 ........V. 0000 30 fd 38 d2 76 12 e8 de 27 19 de 6c 08 06 00 01 0.8.v...'..l.... 0010 08 00 06 04 00 02 e8 de 27 19 de 6c c0 a8 56 f6 ........'..l..V. 0020 30 fd 38 d2 76 12 c0 a8 56 01 0.8.v...V. 0000 30 fd 38 d2 76 12 e8 de 27 19 de 6c 08 00 45 00 0.8.v...'..l..E. 0010 00 70 92 a6 40 00 40 06 15 6c c0 a8 56 f6 23 ae .p..@.@..l..V.#. 0020 57 29 d2 44 01 bb f0 9f 15 b2 61 2b 5b 37 80 18 W).D......a+[7.. 0030 01 12 dc 9d 00 00 01 01 08 0a ba b7 5d 1b 11 87 ............]... 0040 12 ad 17 03 03 00 37 00 00 00 00 00 00 00 6b 04 ......7.......k. 0050 92 2c 7d 6f 27 61 dd 20 7b 47 cc 5c fe 00 ea 9c .,}o'a. {G.\.... 0060 2f 62 0a 18 6e 66 10 92 95 57 e9 d1 2a bc fa a9 /b..nf...W..*... 0070 d9 6c 14 cc bd 58 3a c0 d3 b0 7d 46 9e e4 .l...X:...}F.. 0000 e8 de 27 19 de 6c 30 fd 38 d2 76 12 08 00 45 00 ..'..l0.8.v...E. 0010 00 6a 55 56 40 00 ed 06 a5 c1 23 ae 57 29 c0 a8 .jUV@.....#.W).. 0020 56 f6 01 bb d2 44 61 2b 5b 37 f0 9f 15 ee 80 18 V....Da+[7...... 0030 00 84 e5 59 00 00 01 01 08 0a 11 87 1c 26 ba b7 ...Y.........&.. 0040 5d 1b 17 03 03 00 31 00 00 00 00 00 00 00 6b 3d ].....1.......k= 0050 4b 5b a4 2a 4f 6c 52 5e 76 bd 9e ba 90 2b 25 59 K[.*OlR^v....+%Y 0060 0f b8 5f e7 73 2c 70 af c1 0e 26 bc 4d 1d d0 ac .._.s,p...&.M... 0070 71 5c 0b f0 60 78 d8 57 q\..`x.W 0000 30 fd 38 d2 76 12 e8 de 27 19 de 6c 08 00 45 00 0.8.v...'..l..E. 0010 00 34 92 a7 40 00 40 06 15 a7 c0 a8 56 f6 23 ae .4..@.@.....V.#. 0020 57 29 d2 44 01 bb f0 9f 15 ee 61 2b 5b 6d 80 10 W).D......a+[m.. 0030 01 12 06 4f 00 00 01 01 08 0a ba b7 5d 5b 11 87 ...O........][.. 0040 1c 26 .& termshark-2.4.0/pkg/pcap/testdata/1.pcap000066400000000000000000000044701426312004500200410ustar00rootroot00000000000000ò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.4.0/pkg/pcap/testdata/1.psml000066400000000000000000000112761426312004500200730ustar00rootroot00000000000000
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.4.0/pkg/pcap/testdata/2.hexdump-body000066400000000000000000000005531426312004500215220ustar00rootroot000000000000000000 10 40 00 20 35 01 2b 59 00 06 29 17 93 f8 aa aa .@. 5.+Y..)..... 0010 03 00 00 00 08 00 45 00 00 37 f9 39 00 00 40 11 ......E..7.9..@. 0020 a6 db c0 a8 2c 7b c0 a8 2c d5 f9 39 00 45 00 23 ....,{..,..9.E.# 0030 8d 73 00 01 43 3a 5c 49 42 4d 54 43 50 49 50 5c .s..C:\IBMTCPIP\ 0040 6c 63 63 6d 2e 31 00 6f 63 74 65 74 00 lccm.1.octet. termshark-2.4.0/pkg/pcap/testdata/2.hexdump-footer000066400000000000000000000000001426312004500220460ustar00rootroot00000000000000termshark-2.4.0/pkg/pcap/testdata/2.hexdump-header000066400000000000000000000000001426312004500220000ustar00rootroot00000000000000termshark-2.4.0/pkg/pcap/testdata/2.pcap-body000066400000000000000000000001351426312004500207670ustar00rootroot00000000000000*9RMM@ 5+Y)E79@,{,9E#sC:\IBMTCPIP\lccm.1octettermshark-2.4.0/pkg/pcap/testdata/2.pcap-footer000066400000000000000000000000001426312004500213170ustar00rootroot00000000000000termshark-2.4.0/pkg/pcap/testdata/2.pcap-header000066400000000000000000000000301426312004500212540ustar00rootroot00000000000000òtermshark-2.4.0/pkg/pcap/testdata/2.pdml-body000066400000000000000000000252631426312004500210110ustar00rootroot00000000000000 termshark-2.4.0/pkg/pcap/testdata/2.pdml-footer000066400000000000000000000000121426312004500213330ustar00rootroot00000000000000 termshark-2.4.0/pkg/pcap/testdata/2.pdml-header000066400000000000000000000005511426312004500212750ustar00rootroot00000000000000 termshark-2.4.0/pkg/pcap/testdata/2.psml-body000066400000000000000000000004061426312004500210200ustar00rootroot00000000000000
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.4.0/pkg/pcap/testdata/2.psml-footer000066400000000000000000000000111426312004500213510ustar00rootroot00000000000000 termshark-2.4.0/pkg/pcap/testdata/2.psml-header000066400000000000000000000004441426312004500213150ustar00rootroot00000000000000
No.
Time
Source
Destination
Protocol
Length
Info
termshark-2.4.0/pkg/pcap/utils.go000066400000000000000000000016061426312004500167100ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. package pcap import ( "github.com/gcla/gowid/gwutil" ) //====================================================================== type averageTracker struct { count uint64 total uint64 } func (a averageTracker) average() gwutil.IntOption { if a.count == 0 { return gwutil.NoneInt() } return gwutil.SomeInt(int(a.total / a.count)) } func (a *averageTracker) update(more int) { a.count += 1 a.total += uint64(more) } type maxTracker struct { cur int } func (a maxTracker) max() int { return a.cur } func (a *maxTracker) update(candidate int) { if candidate > a.cur { a.cur = candidate } } //====================================================================== // Local Variables: // mode: Go // fill-column: 78 // End: termshark-2.4.0/pkg/pdmltree/000077500000000000000000000000001426312004500161075ustar00rootroot00000000000000termshark-2.4.0/pkg/pdmltree/pdmltree.go000066400000000000000000000230651426312004500202600ustar00rootroot00000000000000// Copyright 2019-2022 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] } // pos points one head, so logically is -1 on init, but I use zero so the // go default init makes sense. type ExpandedIterator struct { tree *ExpandedModel pos int } var _ tree.IIterator = (*ExpandedIterator)(nil) func (p *ExpandedIterator) Next() bool { p.pos += 1 return (p.pos - 1) < len(p.tree.Children_) } func (p *ExpandedIterator) Value() tree.IModel { var res *ExpandedModel = (*ExpandedModel)(p.tree.Children_[p.pos-1]) return res } //====================================================================== // 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:"-"` Show string `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) type ExpandedModel Model var _ tree.IModel = (*ExpandedModel)(nil) // This ignores the first child, "Frame 15", because its range covers the whole packet // which results in me always including that in the layers for any position. func (n *Model) HexLayers(pos int, includeFirst bool) []hexdumper2.LayerStyler { res := make([]hexdumper2.LayerStyler, 0) sidx := 1 if includeFirst { sidx = 0 } for _, c := range n.Children_[sidx:] { if c.Pos <= pos && pos < c.Pos+c.Size { res = append(res, hexdumper2.LayerStyler{ Start: c.Pos, End: c.Pos + c.Size, ColUnselected: "hex-layer-unselected", ColSelected: "hex-layer-selected", }) for _, c2 := range c.Children_ { if c2.Pos <= pos && pos < c2.Pos+c2.Size { res = append(res, hexdumper2.LayerStyler{ Start: c2.Pos, End: c2.Pos + c2.Size, ColUnselected: "hex-field-unselected", ColSelected: "hex-field-selected", }) } } } } return res } // Implement xml.Unmarshaler. Create a Model struct by unmarshaling the // provided XML. Takes special action before deferring to DecodeElement. func (n *Model) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var err error n.Attrs = map[string]string{} for _, a := range start.Attr { n.Attrs[a.Name.Local] = a.Value switch a.Name.Local { case "pos": n.Pos, err = strconv.Atoi(a.Value) if err != nil { return errors.WithStack(err) } case "size": n.Size, err = strconv.Atoi(a.Value) if err != nil { return errors.WithStack(err) } case "showname": n.UiName = a.Value case "show": if n.UiName == "" { n.UiName = a.Value } n.Show = 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) } } } // Note that this expands every node along the way to the position cf // expandByPath which only expands the leaf node func (m *Model) ExpandByPosition(pos *tree.TreePos) { posCopy := pos.Copy().(*tree.TreePos) if len(posCopy.Pos) == 0 { return } m.Expanded = true if posCopy.Pos[0] >= len(m.Children_) { return } mChild := m.Children_[posCopy.Pos[0]] posChild := tree.NewPosExt(posCopy.Pos[1:]) mChild.ExpandByPosition(posChild) } 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 *ExpandedModel) Children() tree.IIterator { return &ExpandedIterator{ tree: p, } } func (p *Model) HasChildren() bool { return len(p.Children_) > 0 } func (p *Model) Leaf() string { return p.UiName } func (p *ExpandedModel) Leaf() string { return p.UiName } func (p *Model) String() string { return p.stringAt(1) } func (p *ExpandedModel) 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 *ExpandedModel) stringAt(level int) string { return (*Model)(p).stringAt(level) } 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.4.0/pkg/pdmltree/pdmltree_test.go000066400000000000000000000705071426312004500213220ustar00rootroot00000000000000// Copyright 2019-2022 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.4.0/pkg/psmlmodel/000077500000000000000000000000001426312004500162675ustar00rootroot00000000000000termshark-2.4.0/pkg/psmlmodel/model.go000066400000000000000000000072101426312004500177160ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. package psmlmodel import ( "sort" "github.com/gcla/gowid" "github.com/gcla/gowid/widgets/button" "github.com/gcla/gowid/widgets/columns" "github.com/gcla/gowid/widgets/holder" "github.com/gcla/gowid/widgets/isselected" "github.com/gcla/gowid/widgets/styled" "github.com/gcla/gowid/widgets/table" "github.com/gcla/gowid/widgets/text" "github.com/gcla/termshark/v2/widgets/expander" ) //====================================================================== // Model is a table model that provides a widget that will render // in one row only when not selected. type Model struct { *table.SimpleModel styler gowid.ICellStyler } func New(m *table.SimpleModel, st gowid.ICellStyler) *Model { return &Model{ SimpleModel: m, styler: st, } } // Provides the ith "cell" widget, upstream makes the "row" func (c *Model) CellWidget(i int, s string) gowid.IWidget { w := table.SimpleCellWidget(c, i, s) if w != nil { w = expander.New(w) } return w } func (c *Model) CellWidgets(row table.RowId) []gowid.IWidget { return table.SimpleCellWidgets(c, row) } // table.ITable2 func (c *Model) HeaderWidget(ws []gowid.IWidget, focus int) gowid.IWidget { hws := c.HeaderWidgets() hw := c.SimpleModel.HeaderWidget(hws, focus).(*columns.Widget) hw2 := isselected.NewExt( hw, styled.New(hw, c.styler), styled.New(hw, c.styler), ) return hw2 } func (c *Model) HeaderWidgets() []gowid.IWidget { var res []gowid.IWidget if c.Headers != nil { res = make([]gowid.IWidget, 0, len(c.Headers)) bhs := make([]*holder.Widget, len(c.Headers)) bms := make([]*button.Widget, len(c.Headers)) for i, s := range c.Headers { i2 := i var all, label gowid.IWidget label = text.New(s + " ") sorters := c.Comparators if sorters != nil { sorteri := sorters[i2] if sorteri != nil { bmid := button.NewBare(text.New("-")) bfor := button.NewBare(text.New("^")) brev := button.NewBare(text.New("v")) bh := holder.New(bmid) bhs[i] = bh bms[i] = bmid action := func(rev bool, next *button.Widget, app gowid.IApp) { sorter := &table.SimpleTableByColumn{ SimpleModel: c.SimpleModel, Column: i2, } if rev { sort.Sort(sort.Reverse(sorter)) } else { sort.Sort(sorter) } bh.SetSubWidget(next, app) for j, bhj := range bhs { if j != i2 { bhj.SetSubWidget(bms[j], app) } } } bmid.OnClick(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, widget gowid.IWidget) { action(false, bfor, app) })) bfor.OnClick(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, widget gowid.IWidget) { action(true, brev, app) })) brev.OnClick(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, widget gowid.IWidget) { action(false, bfor, app) })) all = columns.NewFixed(label, styled.NewFocus(bh, gowid.MakeStyledAs(gowid.StyleReverse))) } } var w gowid.IWidget if c.Style.HeaderStyleProvided { w = isselected.New( styled.New( all, c.GetStyle().HeaderStyleNoFocus, ), styled.New( all, c.GetStyle().HeaderStyleSelected, ), styled.New( all, c.GetStyle().HeaderStyleFocus, ), ) } else { w = styled.NewExt( all, nil, gowid.MakeStyledAs(gowid.StyleReverse), ) } res = append(res, w) } } return res } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.4.0/pkg/shark/000077500000000000000000000000001426312004500154035ustar00rootroot00000000000000termshark-2.4.0/pkg/shark/columnformat.go000066400000000000000000000413101426312004500204370ustar00rootroot00000000000000// Copyright 2019-2022 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 shark import ( "bufio" "fmt" "os/exec" "regexp" "strconv" "strings" "sync" "github.com/gcla/gowid/widgets/table" "github.com/gcla/termshark/v2" "github.com/gcla/termshark/v2/configs/profiles" "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus" ) //====================================================================== type PsmlField struct { Token string Filter string Occurrence int } func (p PsmlField) FullString() string { return fmt.Sprintf("%s:%s:%d:R", p.Token, p.Filter, p.Occurrence) } func (p PsmlField) String() string { if p.Filter == "" { return p.Token } else { return p.FullString() } } var InvalidCustomColumnError = fmt.Errorf("The custom column is invalid") func (p *PsmlField) FromString(s string) error { fields := strings.Split(s, ":") if len(fields) == 1 { if fields[0] == "%Cus" { //logrus.Warnf("Found a custom column with no definition - ignoring") return InvalidCustomColumnError } *p = PsmlField{Token: fields[0]} } else if len(fields) != 4 { return InvalidCustomColumnError //logrus.Warnf("Found an unexpected custom column '%s' - ignoring", pieces[0]) //continue } else { occ, err := strconv.ParseInt(fields[2], 10, 32) if err != nil { return InvalidCustomColumnError //logrus.Warnf("Found an unexpected occurrence in a custom column '%s' - ignoring", pieces[0]) //continue } *p = PsmlField{ Token: fields[0], Filter: fields[1], Occurrence: int(occ), } //p.Field.Token = fields[0] //p.Field.Filter = fields[1] //p.Field.Occurrence = int(occ) } return nil } type PsmlColumnSpec struct { Name string Field PsmlField Hidden bool } var DefaultPsmlColumnSpec = []PsmlColumnSpec{ PsmlColumnSpec{Field: PsmlField{Token: "%m"}, Name: "No."}, PsmlColumnSpec{Field: PsmlField{Token: "%t"}, Name: "Time"}, PsmlColumnSpec{Field: PsmlField{Token: "%s"}, Name: "Source"}, PsmlColumnSpec{Field: PsmlField{Token: "%d"}, Name: "Dest"}, PsmlColumnSpec{Field: PsmlField{Token: "%p"}, Name: "Proto"}, PsmlColumnSpec{Field: PsmlField{Token: "%L"}, Name: "Length"}, PsmlColumnSpec{Field: PsmlField{Token: "%i"}, Name: "Info"}, } type PsmlColumnInfo struct { Field string Short string Long string Comparator table.ICompare } // AllowedColumnFormats is initialized when the cached columns file is read from disk var AllowedColumnFormats map[string]PsmlColumnInfo // BuiltInColumnFormats is the list we know of, from tshark as of the end of 2020. I'll keep this // up to date over time. The canonical list is retrieved from tshark -G column-formats, then merged // with this to add useful short names and comparators. var BuiltInColumnFormats = map[string]PsmlColumnInfo{ "%q": PsmlColumnInfo{Field: "%q", Short: "VLAN", Long: "802.1Q VLAN id", Comparator: table.IntCompare{}}, /* 0) COL_8021Q_VLAN_ID */ "%Yt": PsmlColumnInfo{Field: "%Yt", Short: "Time", Long: "Absolute date, as YYYY-MM-DD, and time", Comparator: table.DateTimeCompare{}}, /* 1) COL_ABS_YMD_TIME */ "%YDOYt": PsmlColumnInfo{Field: "%YDOYt", Short: "Time", Long: "Absolute date, as YYYY/DOY, and time", Comparator: table.DateTimeCompare{}}, /* 2) COL_ABS_YDOY_TIME */ "%At": PsmlColumnInfo{Field: "%At", Short: "Time", Long: "Absolute time", Comparator: table.DateTimeCompare{}}, /* 3) COL_ABS_TIME */ "%V": PsmlColumnInfo{Field: "%V", Short: "VSAN", Long: "Cisco VSAN"}, /* 4) COL_VSAN - !! DEPRECATED !!*/ "%B": PsmlColumnInfo{Field: "%B", Short: "Cuml Bytes", Long: "Cumulative Bytes", Comparator: table.IntCompare{}}, /* 5) COL_CUMULATIVE_BYTES */ "%Cus": PsmlColumnInfo{Field: "%Cus", Short: "Custom", Long: "Custom"}, /* 6) COL_CUSTOM */ "%y": PsmlColumnInfo{Field: "%y", Short: "DCE/RPC", Long: "DCE/RPC call (cn_call_id / dg_seqnum)", Comparator: table.IntCompare{}}, /* 7) COL_DCE_CALL */ "%Tt": PsmlColumnInfo{Field: "%Tt", Short: "Time Delt", Long: "Delta time", Comparator: table.FloatCompare{}}, /* 8) COL_DELTA_TIME */ "%Gt": PsmlColumnInfo{Field: "%Gt", Short: "Time Delt", Long: "Delta time displayed", Comparator: table.FloatCompare{}}, /* 9) COL_DELTA_TIME_DIS */ "%rd": PsmlColumnInfo{Field: "%rd", Short: "Dest", Long: "Dest addr (resolved)"}, /* 10) COL_RES_DST */ "%ud": PsmlColumnInfo{Field: "%ud", Short: "Dest", Long: "Dest addr (unresolved)", Comparator: termshark.IPCompare{}}, /* 11) COL_UNRES_DST */ "%rD": PsmlColumnInfo{Field: "%rD", Short: "DPort", Long: "Dest port (resolved)"}, /* 12) COL_RES_DST_PORT */ "%uD": PsmlColumnInfo{Field: "%uD", Short: "DPort", Long: "Dest port (unresolved)", Comparator: table.IntCompare{}}, /* 13) COL_UNRES_DST_PORT */ "%d": PsmlColumnInfo{Field: "%d", Short: "Dest", Long: "Destination address"}, /* 14) COL_DEF_DST */ "%D": PsmlColumnInfo{Field: "%D", Short: "DPort", Long: "Destination port", Comparator: table.IntCompare{}}, /* 15) COL_DEF_DST_PORT */ "%a": PsmlColumnInfo{Field: "%a", Short: "Expert", Long: "Expert Info Severity"}, /* 16) COL_EXPERT */ "%I": PsmlColumnInfo{Field: "%I", Short: "FW-1", Long: "FW-1 monitor if/direction"}, /* 17) COL_IF_DIR */ "%F": PsmlColumnInfo{Field: "%F", Short: "Freq/Chan", Long: "Frequency/Channel", Comparator: table.IntCompare{}}, /* 18) COL_FREQ_CHAN */ "%hd": PsmlColumnInfo{Field: "%hd", Short: "DMAC", Long: "Hardware dest addr"}, /* 19) COL_DEF_DL_DST */ "%hs": PsmlColumnInfo{Field: "%hs", Short: "SMAC", Long: "Hardware src addr"}, /* 20) COL_DEF_DL_SRC */ "%rhd": PsmlColumnInfo{Field: "%rhd", Short: "DMAC", Long: "Hw dest addr (resolved)"}, /* 21) COL_RES_DL_DST */ "%uhd": PsmlColumnInfo{Field: "%uhd", Short: "DMAC", Long: "Hw dest addr (unresolved)"}, /* 22) COL_UNRES_DL_DST */ "%rhs": PsmlColumnInfo{Field: "%rhs", Short: "SMAC", Long: "Hw src addr (resolved)"}, /* 23) COL_RES_DL_SRC*/ "%uhs": PsmlColumnInfo{Field: "%uhs", Short: "SMAC", Long: "Hw src addr (unresolved)"}, /* 24) COL_UNRES_DL_SRC */ "%e": PsmlColumnInfo{Field: "%e", Short: "RSSI", Long: "IEEE 802.11 RSSI", Comparator: table.FloatCompare{}}, /* 25) COL_RSSI */ "%x": PsmlColumnInfo{Field: "%x", Short: "TX Rate", Long: "IEEE 802.11 TX rate", Comparator: table.FloatCompare{}}, /* 26) COL_TX_RATE */ "%f": PsmlColumnInfo{Field: "%f", Short: "DSCP", Long: "IP DSCP Value"}, /* 27) COL_DSCP_VALUE */ "%i": PsmlColumnInfo{Field: "%i", Short: "Info", Long: "Information"}, /* 28) COL_INFO */ "%rnd": PsmlColumnInfo{Field: "%rnd", Short: "Dest", Long: "Net dest addr (resolved)"}, /* 29) COL_RES_NET_DST */ "%und": PsmlColumnInfo{Field: "%und", Short: "Dest", Long: "Net dest addr (unresolved)", Comparator: termshark.IPCompare{}}, /* 30) COL_UNRES_NET_DST */ "%rns": PsmlColumnInfo{Field: "%rns", Short: "Source", Long: "Net src addr (resolved)"}, /* 31) COL_RES_NET_SRC */ "%uns": PsmlColumnInfo{Field: "%uns", Short: "Source", Long: "Net src addr (unresolved)", Comparator: termshark.IPCompare{}}, /* 32) COL_UNRES_NET_SRC */ "%nd": PsmlColumnInfo{Field: "%nd", Short: "Dest", Long: "Network dest addr"}, /* 33) COL_DEF_NET_DST */ "%ns": PsmlColumnInfo{Field: "%ns", Short: "Dest", Long: "Network src addr"}, /* 34) COL_DEF_NET_SRC */ "%m": PsmlColumnInfo{Field: "%m", Short: "No.", Long: "Number", Comparator: table.IntCompare{}}, /* 35) COL_NUMBER */ "%L": PsmlColumnInfo{Field: "%L", Short: "Length", Long: "Packet length (bytes)", Comparator: table.IntCompare{}}, /* 36) COL_PACKET_LENGTH */ "%p": PsmlColumnInfo{Field: "%p", Short: "Proto", Long: "Protocol"}, /* 37) COL_PROTOCOL */ // IGMPv3, NBNS, TLSv1.3 "%Rt": PsmlColumnInfo{Field: "%Rt", Short: "Time", Long: "Relative time", Comparator: table.FloatCompare{}}, /* 38) COL_REL_TIME */ // 5.961798653 "%s": PsmlColumnInfo{Field: "%s", Short: "Source", Long: "Source address", Comparator: termshark.IPCompare{}}, /* 39) COL_DEF_SRC */ "%S": PsmlColumnInfo{Field: "%S", Short: "SPort", Long: "Source port", Comparator: table.IntCompare{}}, /* 40) COL_DEF_SRC_PORT */ "%rs": PsmlColumnInfo{Field: "%rs", Short: "Source", Long: "Src addr (resolved)"}, /* 41) COL_RES_SRC */ "%us": PsmlColumnInfo{Field: "%us", Short: "Source", Long: "Src addr (unresolved)", Comparator: termshark.IPCompare{}}, /* 42) COL_UNRES_SRC */ "%rS": PsmlColumnInfo{Field: "%rS", Short: "SPort", Long: "Src port (resolved)"}, /* 43) COL_RES_SRC_PORT */ "%uS": PsmlColumnInfo{Field: "%uS", Short: "SPort", Long: "Src port (unresolved)", Comparator: table.IntCompare{}}, /* 44) COL_UNRES_SRC_PORT */ "%E": PsmlColumnInfo{Field: "%E", Short: "TEI", Long: "TEI", Comparator: table.IntCompare{}}, /* 45) COL_TEI */ "%Yut": PsmlColumnInfo{Field: "%Yut", Short: "Time", Long: "UTC date, as YYYY-MM-DD, and time", Comparator: table.DateTimeCompare{}}, /* 46) COL_UTC_YMD_TIME */ "%YDOYut": PsmlColumnInfo{Field: "%YDOYut", Short: "Time", Long: "UTC date, as YYYY/DOY, and time", Comparator: table.DateTimeCompare{}}, /* 47) COL_UTC_YDOY_TIME */ "%Aut": PsmlColumnInfo{Field: "%Aut", Short: "Time", Long: "UTC time", Comparator: table.DateTimeCompare{}}, /* 48) COL_UTC_TIME */ "%t": PsmlColumnInfo{Field: "%t", Short: "Time", Long: "Time (format as specified)", Comparator: table.DateTimeCompare{}}, /* 49) COL_CLS_TIME */ // 6916.185051 } var cachedPsmlColumnFormat []PsmlColumnSpec var cachedPsmlColumnFormatMutex sync.Mutex // The fields field is serialized using gob. type ColumnsFromTshark struct { once sync.Once fields []PsmlColumnSpec } // Singleton var validColumns *ColumnsFromTshark var TsharkColumnsCacheOldError = fmt.Errorf("The cached tshark columns database is out of date") var ColumnsFormatError = fmt.Errorf("The supplied list of columns and names is invalid") func init() { // This will be computed from tshark -G column-formats. We then merge in some short // column names from all the fields we know. I'll have to keep that list up to date, // over time. AllowedColumnFormats = make(map[string]PsmlColumnInfo) } //====================================================================== func (p PsmlColumnInfo) WithLongName(name string) PsmlColumnInfo { p.Long = name return p } // InitValidColumns will run tshark, if necessary, to compute the columns that tshark understands. This // is the set of columns the user is allowed to configure. This will block - if it's noticeable I'll make // it async. This is called from termshark's main and makes specific assumptions i.e. that it can write // to stdout and the user will see it. func InitValidColumns() error { validColumns = &ColumnsFromTshark{} err := validColumns.InitFromCache() if err != nil { fmt.Printf("Termshark is initializing - please wait...\n") log.Infof("Did not read cached tshark column formats (%v) - regenerating...", err) // This will block for a second err = validColumns.InitNoCache() if err != nil { log.Warnf("Did not generate tshark column formats (%v)", err) } else { err = termshark.WriteGob(termshark.CacheFile("tsharkcolumnsv2.gob.gz"), validColumns.fields) if err != nil { log.Warnf("Could not serialize tshark column formats (%v)", err) } } } for _, f := range validColumns.fields { // Use short names that we understand if cached, ok := BuiltInColumnFormats[f.Field.Token]; ok { AllowedColumnFormats[f.Field.Token] = cached.WithLongName(f.Name) } else { // We don't have a short name from tshark... :( AllowedColumnFormats[f.Field.Token] = PsmlColumnInfo{Short: f.Name, Long: f.Name} } } return err } func (w *ColumnsFromTshark) InitFromCache() error { newer, err := termshark.FileNewerThan(termshark.CacheFile("tsharkcolumnsv2.gob.gz"), termshark.DirOfPathCommandUnsafe(termshark.TSharkBin())) if err != nil { return err } if !newer { return TsharkColumnsCacheOldError } f := []PsmlColumnSpec{} err = termshark.ReadGob(termshark.CacheFile("tsharkcolumnsv2.gob.gz"), &f) if err != nil { return err } w.fields = f log.Infof("Read cached tshark column formats.") return nil } func (w *ColumnsFromTshark) InitNoCache() error { re := regexp.MustCompile("\\s+") cmd := exec.Command(termshark.TSharkBin(), []string{"-G", "column-formats"}...) out, err := cmd.StdoutPipe() if err != nil { return err } cmd.Start() w.fields = make([]PsmlColumnSpec, 0, 128) scanner := bufio.NewScanner(out) for scanner.Scan() { fields := re.Split(scanner.Text(), 2) if len(fields) == 2 && strings.HasPrefix(fields[0], "%") { w.fields = append(w.fields, PsmlColumnSpec{ Field: PsmlField{Token: fields[0]}, Name: fields[1], }) } } cmd.Wait() return nil } //====================================================================== func GetPsmlColumnFormatCached() []PsmlColumnSpec { cachedPsmlColumnFormatMutex.Lock() defer cachedPsmlColumnFormatMutex.Unlock() if cachedPsmlColumnFormat == nil { cachedPsmlColumnFormat = getPsmlColumnFormatWithoutLock("main.column-format") } return cachedPsmlColumnFormat } func GetPsmlColumnFormat() []PsmlColumnSpec { cachedPsmlColumnFormatMutex.Lock() defer cachedPsmlColumnFormatMutex.Unlock() cachedPsmlColumnFormat = getPsmlColumnFormatWithoutLock("main.column-format") return cachedPsmlColumnFormat } func GetPsmlColumnFormatFrom(colKey string) []PsmlColumnSpec { return getPsmlColumnFormatWithoutLock(colKey) } func getPsmlColumnFormatWithoutLock(colKey string) []PsmlColumnSpec { res := make([]PsmlColumnSpec, 0) widths := profiles.ConfStringSlice(colKey, []string{}) if len(widths) == 0 || (len(widths)/3)*3 != len(widths) { logrus.Warnf("Unexpected %s structure - using defaults", colKey) res = DefaultPsmlColumnSpec } else { // Cross references with those column specs that we know about from having // queried tshark with tshark -G column-formats. Any that are not known // are discarded. If none are left, use our safe defaults pieces := [3]string{} for i := 0; i < len(widths); i += 3 { pieces[0] = widths[i] pieces[1] = widths[i+1] pieces[2] = widths[i+2] var spec PsmlColumnSpec err := spec.Field.FromString(pieces[0]) if err != nil { logrus.Warnf(err.Error()) continue } if _, ok := AllowedColumnFormats[spec.Field.Token]; !ok { logrus.Warnf("Do not understand PSML column format token '%s' - skipping its use", pieces[0]) continue } if pieces[1] != "" { spec.Name = pieces[1] } else { // Already confirmed it's in map spec.Name = AllowedColumnFormats[pieces[0]].Short } visible, err := strconv.ParseBool(pieces[2]) if err != nil { logrus.Warnf("Do not understand PSML column format hidden token '%s' - skipping its use", pieces[2]) continue } spec.Hidden = !visible res = append(res, spec) } if len(res) == 0 { logrus.Warnf("No configured PSML column formats were understood. Using safe default") res = DefaultPsmlColumnSpec } } return res } //====================================================================== // Local Variables: // mode: Go // fill-column: 78 // End: termshark-2.4.0/pkg/shark/columnformat_test.go000066400000000000000000000014051426312004500214770ustar00rootroot00000000000000// Copyright 2019-2022 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 shark import ( "fmt" "testing" "github.com/stretchr/testify/assert" ) //====================================================================== func TestCF1(t *testing.T) { fields := &ColumnsFromTshark{} err := fields.InitNoCache() assert.NoError(t, err) cfmap := make(map[string]PsmlColumnSpec) for _, f := range fields.fields { fmt.Printf("GCLA: adding %v\n", f) cfmap[f.Field.Token] = f } m1, ok := cfmap["%At"] assert.Equal(t, true, ok) assert.Equal(t, "Absolute time", m1.Name) m2, ok := cfmap["%rs"] assert.Equal(t, true, ok) assert.Equal(t, "Src addr (resolved)", m2.Name) } termshark-2.4.0/pkg/shark/wiresharkcfg/000077500000000000000000000000001426312004500200625ustar00rootroot00000000000000termshark-2.4.0/pkg/shark/wiresharkcfg/cfg.go000066400000000000000000000044241426312004500211540ustar00rootroot00000000000000// Copyright 2019-2022 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 wiresharkcfg import ( "fmt" "os" "path" "strings" homedir "github.com/mitchellh/go-homedir" "github.com/shibukawa/configdir" ) //====================================================================== var NotFoundError = fmt.Errorf("Could not find wireshark preferences") var NotParsedError = fmt.Errorf("Could not parse wireshark preferences") type Config struct { Strings map[string]string Lists map[string][]string } func NewDefault() (*Config, error) { // See https://www.wireshark.org/docs/wsug_html_chunked/ChAppFilesConfigurationSection.html // Wireshark had a ~/.wireshark directory before adopting XDG tryXDG := true cpath, err := homedir.Expand("~/.wireshark/preferences") if err == nil { _, err = os.Stat(cpath) if err == nil { tryXDG = false } } if tryXDG { stdConf := configdir.New("", "wireshark") dirs := stdConf.QueryFolders(configdir.All) cpath = path.Join(dirs[0].Path, "preferences") _, err = os.Stat(cpath) if os.IsNotExist(err) { return nil, err } } res := &Config{} err = res.PopulateFrom(cpath) return res, err } func (c *Config) PopulateFrom(filename string) error { file, err := os.Open(filename) if err != nil { return err } parsed, err := ParseReader("", file) if err != nil { return err } *c = *(parsed.(*Config)) return nil } func (c *Config) GetList(key string) []string { if c == nil { return nil } if val, ok := c.Lists[key]; ok { return val } return nil } func (c *Config) ColumnFormat() []string { return c.GetList("gui.column.format") } func (c *Config) merge(other *Config) { for k, v := range other.Strings { c.Strings[k] = v } for k, v := range other.Lists { c.Lists[k] = v } } func (c *Config) String() string { res := make([]string, 0, len(c.Strings)+len(c.Lists)) for k, v := range c.Strings { res = append(res, fmt.Sprintf("%s: %s", k, v)) } for k, v := range c.Lists { v2 := strings.Join(v, ", ") res = append(res, fmt.Sprintf("%s: %s", k, v2)) } return strings.Join(res, "\n") } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.4.0/pkg/shark/wiresharkcfg/parser.go000066400000000000000000001265521426312004500217200ustar00rootroot00000000000000// Code generated by pigeon; DO NOT EDIT. package wiresharkcfg 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: 60, col: 1, offset: 1262}, expr: &actionExpr{ pos: position{line: 60, col: 10, offset: 1271}, run: (*parser).callonInput1, expr: &seqExpr{ pos: position{line: 60, col: 10, offset: 1271}, exprs: []interface{}{ &labeledExpr{ pos: position{line: 60, col: 10, offset: 1271}, label: "e", expr: &ruleRefExpr{ pos: position{line: 60, col: 12, offset: 1273}, name: "OneEntry", }, }, &labeledExpr{ pos: position{line: 60, col: 21, offset: 1282}, label: "es", expr: &zeroOrMoreExpr{ pos: position{line: 60, col: 24, offset: 1285}, expr: &ruleRefExpr{ pos: position{line: 60, col: 24, offset: 1285}, name: "OneEntry", }, }, }, }, }, }, }, { name: "OneEntry", pos: position{line: 69, col: 1, offset: 1442}, expr: &actionExpr{ pos: position{line: 69, col: 13, offset: 1454}, run: (*parser).callonOneEntry1, expr: &labeledExpr{ pos: position{line: 69, col: 13, offset: 1454}, label: "sv", expr: &choiceExpr{ pos: position{line: 69, col: 17, offset: 1458}, alternatives: []interface{}{ &ruleRefExpr{ pos: position{line: 69, col: 17, offset: 1458}, name: "ListKeyValue", }, &ruleRefExpr{ pos: position{line: 69, col: 32, offset: 1473}, name: "StringKeyValue", }, }, }, }, }, }, { name: "Comment", pos: position{line: 73, col: 1, offset: 1522}, expr: &choiceExpr{ pos: position{line: 73, col: 12, offset: 1533}, alternatives: []interface{}{ &oneOrMoreExpr{ pos: position{line: 73, col: 12, offset: 1533}, expr: &ruleRefExpr{ pos: position{line: 73, col: 12, offset: 1533}, name: "_nl", }, }, &seqExpr{ pos: position{line: 73, col: 19, offset: 1540}, exprs: []interface{}{ &seqExpr{ pos: position{line: 73, col: 20, offset: 1541}, exprs: []interface{}{ &zeroOrMoreExpr{ pos: position{line: 73, col: 20, offset: 1541}, expr: &ruleRefExpr{ pos: position{line: 73, col: 20, offset: 1541}, name: "_nl", }, }, &litMatcher{ pos: position{line: 73, col: 25, offset: 1546}, val: "#", ignoreCase: false, }, &zeroOrMoreExpr{ pos: position{line: 73, col: 29, offset: 1550}, expr: &charClassMatcher{ pos: position{line: 73, col: 29, offset: 1550}, val: "[^\\r\\n]", chars: []rune{'\r', '\n'}, ignoreCase: false, inverted: true, }, }, &ruleRefExpr{ pos: position{line: 73, col: 38, offset: 1559}, name: "_nl", }, }, }, &stateCodeExpr{ pos: position{line: 73, col: 43, offset: 1564}, run: (*parser).callonComment12, }, }, }, }, }, }, { name: "StringKeyValue", pos: position{line: 77, col: 1, offset: 1591}, expr: &actionExpr{ pos: position{line: 77, col: 19, offset: 1609}, run: (*parser).callonStringKeyValue1, expr: &seqExpr{ pos: position{line: 77, col: 19, offset: 1609}, exprs: []interface{}{ &zeroOrMoreExpr{ pos: position{line: 77, col: 19, offset: 1609}, expr: &ruleRefExpr{ pos: position{line: 77, col: 19, offset: 1609}, name: "Comment", }, }, &labeledExpr{ pos: position{line: 77, col: 28, offset: 1618}, label: "k", expr: &ruleRefExpr{ pos: position{line: 77, col: 30, offset: 1620}, name: "StringKey", }, }, &litMatcher{ pos: position{line: 77, col: 40, offset: 1630}, val: ":", ignoreCase: false, }, &labeledExpr{ pos: position{line: 77, col: 44, offset: 1634}, label: "v", expr: &ruleRefExpr{ pos: position{line: 77, col: 46, offset: 1636}, name: "StringValue", }, }, &ruleRefExpr{ pos: position{line: 77, col: 58, offset: 1648}, name: "_nl", }, &ruleRefExpr{ pos: position{line: 77, col: 62, offset: 1652}, name: "_nl", }, }, }, }, }, { name: "StringKey", pos: position{line: 87, col: 1, offset: 1831}, expr: &actionExpr{ pos: position{line: 87, col: 14, offset: 1844}, run: (*parser).callonStringKey1, expr: &oneOrMoreExpr{ pos: position{line: 87, col: 14, offset: 1844}, expr: &charClassMatcher{ pos: position{line: 87, col: 14, offset: 1844}, val: "[a-zA-z0-9._]", chars: []rune{'.', '_'}, ranges: []rune{'a', 'z', 'A', 'z', '0', '9'}, ignoreCase: false, inverted: false, }, }, }, }, { name: "StringValue", pos: position{line: 91, col: 1, offset: 1893}, expr: &actionExpr{ pos: position{line: 91, col: 16, offset: 1908}, run: (*parser).callonStringValue1, expr: &oneOrMoreExpr{ pos: position{line: 91, col: 16, offset: 1908}, expr: &charClassMatcher{ pos: position{line: 91, col: 16, offset: 1908}, val: "[^\\n\\t\\r]", chars: []rune{'\n', '\t', '\r'}, ignoreCase: false, inverted: true, }, }, }, }, { name: "ListKeyValue", pos: position{line: 95, col: 1, offset: 1954}, expr: &actionExpr{ pos: position{line: 95, col: 17, offset: 1970}, run: (*parser).callonListKeyValue1, expr: &seqExpr{ pos: position{line: 95, col: 17, offset: 1970}, exprs: []interface{}{ &zeroOrMoreExpr{ pos: position{line: 95, col: 17, offset: 1970}, expr: &ruleRefExpr{ pos: position{line: 95, col: 17, offset: 1970}, name: "Comment", }, }, &labeledExpr{ pos: position{line: 95, col: 26, offset: 1979}, label: "k", expr: &ruleRefExpr{ pos: position{line: 95, col: 28, offset: 1981}, name: "ListKey", }, }, &litMatcher{ pos: position{line: 95, col: 36, offset: 1989}, val: ":", ignoreCase: false, }, &ruleRefExpr{ pos: position{line: 95, col: 40, offset: 1993}, name: "_", }, &labeledExpr{ pos: position{line: 95, col: 42, offset: 1995}, label: "lv", expr: &ruleRefExpr{ pos: position{line: 95, col: 45, offset: 1998}, name: "ListValue", }, }, }, }, }, }, { name: "ListKey", pos: position{line: 105, col: 1, offset: 2198}, expr: &choiceExpr{ pos: position{line: 105, col: 12, offset: 2209}, alternatives: []interface{}{ &litMatcher{ pos: position{line: 105, col: 12, offset: 2209}, val: "gui.column.format", ignoreCase: false, }, &actionExpr{ pos: position{line: 105, col: 34, offset: 2231}, run: (*parser).callonListKey3, expr: &litMatcher{ pos: position{line: 105, col: 34, offset: 2231}, val: "gui.column.hidden", ignoreCase: false, }, }, }, }, }, { name: "ListItem", pos: position{line: 109, col: 1, offset: 2278}, expr: &actionExpr{ pos: position{line: 109, col: 13, offset: 2290}, run: (*parser).callonListItem1, expr: &oneOrMoreExpr{ pos: position{line: 109, col: 13, offset: 2290}, expr: &charClassMatcher{ pos: position{line: 109, col: 13, offset: 2290}, val: "[^\\n\\t\\r,]", chars: []rune{'\n', '\t', '\r', ','}, ignoreCase: false, inverted: true, }, }, }, }, { name: "ListValue", pos: position{line: 113, col: 1, offset: 2337}, expr: &actionExpr{ pos: position{line: 113, col: 14, offset: 2350}, run: (*parser).callonListValue1, expr: &seqExpr{ pos: position{line: 113, col: 14, offset: 2350}, exprs: []interface{}{ &labeledExpr{ pos: position{line: 113, col: 14, offset: 2350}, label: "li", expr: &ruleRefExpr{ pos: position{line: 113, col: 17, offset: 2353}, name: "ListItem", }, }, &ruleRefExpr{ pos: position{line: 113, col: 26, offset: 2362}, name: "_", }, &labeledExpr{ pos: position{line: 113, col: 28, offset: 2364}, label: "lv", expr: &zeroOrMoreExpr{ pos: position{line: 113, col: 31, offset: 2367}, expr: &seqExpr{ pos: position{line: 113, col: 33, offset: 2369}, exprs: []interface{}{ &litMatcher{ pos: position{line: 113, col: 33, offset: 2369}, val: ",", ignoreCase: false, }, &ruleRefExpr{ pos: position{line: 113, col: 37, offset: 2373}, name: "_", }, &ruleRefExpr{ pos: position{line: 113, col: 39, offset: 2375}, name: "ListValue", }, }, }, }, }, }, }, }, }, { name: "_nl", displayName: "\"newline\"", pos: position{line: 128, col: 1, offset: 2712}, expr: &seqExpr{ pos: position{line: 128, col: 18, offset: 2729}, exprs: []interface{}{ &zeroOrOneExpr{ pos: position{line: 128, col: 18, offset: 2729}, expr: &charClassMatcher{ pos: position{line: 128, col: 18, offset: 2729}, val: "[\\r]", chars: []rune{'\r'}, ignoreCase: false, inverted: false, }, }, &charClassMatcher{ pos: position{line: 128, col: 23, offset: 2734}, val: "[\\n]", chars: []rune{'\n'}, ignoreCase: false, inverted: false, }, }, }, }, { name: "_", displayName: "\"whitespace\"", pos: position{line: 130, col: 1, offset: 2740}, expr: &zeroOrMoreExpr{ pos: position{line: 130, col: 19, offset: 2758}, expr: &charClassMatcher{ pos: position{line: 130, col: 19, offset: 2758}, val: "[ \\n\\t\\r]", chars: []rune{' ', '\n', '\t', '\r'}, ignoreCase: false, inverted: false, }, }, }, { name: "__", displayName: "\"mandatory whitespace\"", pos: position{line: 132, col: 1, offset: 2770}, expr: &oneOrMoreExpr{ pos: position{line: 132, col: 30, offset: 2799}, expr: &charClassMatcher{ pos: position{line: 132, col: 30, offset: 2799}, val: "[ \\n\\t\\r]", chars: []rune{' ', '\n', '\t', '\r'}, ignoreCase: false, inverted: false, }, }, }, { name: "EOF", pos: position{line: 134, col: 1, offset: 2811}, expr: ¬Expr{ pos: position{line: 134, col: 8, offset: 2818}, expr: &anyMatcher{ line: 134, col: 9, offset: 2819, }, }, }, }, } func (c *current) onInput1(e, es interface{}) (interface{}, error) { res := e.(*Config) for _, e2 := range es.([]interface{}) { e2.(*Config).merge(res) res = e2.(*Config) } return res, nil } func (p *parser) callonInput1() (interface{}, error) { stack := p.vstack[len(p.vstack)-1] _ = stack return p.cur.onInput1(stack["e"], stack["es"]) } func (c *current) onOneEntry1(sv interface{}) (interface{}, error) { return sv.(*Config), nil } func (p *parser) callonOneEntry1() (interface{}, error) { stack := p.vstack[len(p.vstack)-1] _ = stack return p.cur.onOneEntry1(stack["sv"]) } func (c *current) onComment12() error { return nil } func (p *parser) callonComment12() error { stack := p.vstack[len(p.vstack)-1] _ = stack return p.cur.onComment12() } func (c *current) onStringKeyValue1(k, v interface{}) (interface{}, error) { strs := make(map[string]string) strs[k.(string)] = v.(string) res := &Config{ Lists: make(map[string][]string), Strings: strs, } return res, nil } func (p *parser) callonStringKeyValue1() (interface{}, error) { stack := p.vstack[len(p.vstack)-1] _ = stack return p.cur.onStringKeyValue1(stack["k"], stack["v"]) } func (c *current) onStringKey1() (interface{}, error) { return string(c.text), nil } func (p *parser) callonStringKey1() (interface{}, error) { stack := p.vstack[len(p.vstack)-1] _ = stack return p.cur.onStringKey1() } func (c *current) onStringValue1() (interface{}, error) { return string(c.text), nil } func (p *parser) callonStringValue1() (interface{}, error) { stack := p.vstack[len(p.vstack)-1] _ = stack return p.cur.onStringValue1() } func (c *current) onListKeyValue1(k, lv interface{}) (interface{}, error) { lists := make(map[string][]string) lists[string(k.([]uint8))] = lv.([]string) res := &Config{ Lists: lists, Strings: make(map[string]string), } return res, nil } func (p *parser) callonListKeyValue1() (interface{}, error) { stack := p.vstack[len(p.vstack)-1] _ = stack return p.cur.onListKeyValue1(stack["k"], stack["lv"]) } func (c *current) onListKey3() (interface{}, error) { return c.text, nil } func (p *parser) callonListKey3() (interface{}, error) { stack := p.vstack[len(p.vstack)-1] _ = stack return p.cur.onListKey3() } func (c *current) onListItem1() (interface{}, error) { return string(c.text), nil } func (p *parser) callonListItem1() (interface{}, error) { stack := p.vstack[len(p.vstack)-1] _ = stack return p.cur.onListItem1() } func (c *current) onListValue1(li, lv interface{}) (interface{}, error) { res := make([]string, 0) res = append(res, li.(string)) noneOrSome := lv.([]interface{}) if len(noneOrSome) > 0 { some := noneOrSome[0].([]interface{}) // [[44] [] [[...]]] if len(some) == 3 { vals := some[2].([]string) res = append(res, vals...) } } return res, nil } func (p *parser) callonListValue1() (interface{}, error) { stack := p.vstack[len(p.vstack)-1] _ = stack return p.cur.onListValue1(stack["li"], stack["lv"]) } 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.4.0/pkg/shark/wiresharkcfg/parser.peg000066400000000000000000000054051426312004500220570ustar00rootroot00000000000000// Copyright 2019-2022 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/share/wiresharkcfg/ // pigeon parser.peg > parser.go // { package wiresharkcfg import ( "io" "unicode" "strings" "os" "fmt" "strconv" "errors" "io/ioutil" "bytes" "unicode/utf8" log "github.com/sirupsen/logrus" ) } // // Parse input that looks roughly like: // // # Packet list hidden columns // # List all columns to hide in the packet list. // gui.column.hidden: // // # Packet list column format // # Each pair of strings consists of a column title and its format // gui.column.format: // "No.", "%m", // "gcla3", "%Yt", // "Time", "%t", // "Source", "%s", // "Destination", "%d", // "Protocol", "%p", // "Length", "%L", // "Info", "%i", // "gcla", "%V", // "gcla2", "%B", // "utc", "%Aut" // // ####### User Interface: Font ######## // // # Font name for packet list, protocol tree, and hex dump panes. (Qt) // # A string // gui.qt.font_name: Liberation Mono,11,-1,5,50,0,0,0,0,0 // ... Input <- e:OneEntry es:OneEntry* { res := e.(*Config) for _, e2 := range es.([]interface{}) { e2.(*Config).merge(res) res = e2.(*Config) } return res, nil } OneEntry <- sv:(ListKeyValue / StringKeyValue) { return sv.(*Config), nil } Comment <- _nl+ / (_nl* "#" [^\r\n]* _nl) #{ return nil } StringKeyValue <- Comment* k:StringKey ":" v:StringValue _nl _nl { strs := make(map[string]string) strs[k.(string)] = v.(string) res := &Config{ Lists: make(map[string][]string), Strings: strs, } return res, nil } StringKey <- [a-zA-z0-9._]+ { return string(c.text), nil } StringValue <- [^\n\t\r]+ { return string(c.text), nil } ListKeyValue <- Comment* k:ListKey ':' _ lv:ListValue { lists := make(map[string][]string) lists[string(k.([]uint8))] = lv.([]string) res := &Config{ Lists: lists, Strings: make(map[string]string), } return res, nil } ListKey <- "gui.column.format" / "gui.column.hidden" { return c.text, nil } ListItem <- [^\n\t\r,]+ { return string(c.text), nil } ListValue <- li:ListItem _ lv:( ',' _ ListValue )* { res := make([]string, 0) res = append(res, li.(string)) noneOrSome := lv.([]interface{}) if len(noneOrSome) > 0 { some := noneOrSome[0].([]interface{}) // [[44] [] [[...]]] if len(some) == 3 { vals := some[2].([]string) res = append(res, vals...) } } return res, nil } _nl "newline" <- [\r]?[\n] _ "whitespace" <- [ \n\t\r]* __ "mandatory whitespace" <- [ \n\t\r]+ EOF <- !. termshark-2.4.0/pkg/shark/wiresharkcfg/parser_test.go000066400000000000000000006117411426312004500227560ustar00rootroot00000000000000// Copyright 2019-2022 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 wiresharkcfg import ( "fmt" "log" "strings" "testing" "github.com/stretchr/testify/assert" ) func TestArgConv(t *testing.T) { inp1 := ` # Configuration file for Wireshark 3.2.3. # # This file is regenerated each time preferences are saved within # Wireshark. Making manual changes should be safe, however. # Preferences that have been commented out have not been # changed from their default value. ####### User Interface ######## # Open a console window (Windows only) # One of: NEVER, AUTOMATIC, ALWAYS # (case-insensitive). #gui.console_open: NEVER # Restore current display filter after following a stream? # TRUE or FALSE (case-insensitive) #gui.restore_filter_after_following_stream: FALSE # Where to start the File Open dialog box # One of: LAST_OPENED, SPECIFIED # (case-insensitive). #gui.fileopen.style: LAST_OPENED # The max. number of items in the open recent files list # A decimal number #gui.recent_files_count.max: 10 # The max. number of entries in the display filter list # A decimal number #gui.recent_display_filter_entries.max: 10 # Directory to start in when opening File Open dialog. # A path to a directory #gui.fileopen.dir: # The preview timeout in the File Open dialog # A decimal number #gui.fileopen.preview: 3 # Ask to save unsaved capture files? # TRUE or FALSE (case-insensitive) #gui.ask_unsaved: TRUE # Display an autocomplete suggestion for display and capture filter controls # TRUE or FALSE (case-insensitive) #gui.autocomplete_filter: TRUE # Wrap to beginning/end of file during search? # TRUE or FALSE (case-insensitive) #gui.find_wrap: TRUE # Save window position at exit? # TRUE or FALSE (case-insensitive) #gui.geometry.save.position: TRUE # Save window size at exit? # TRUE or FALSE (case-insensitive) #gui.geometry.save.size: TRUE # Save window maximized state at exit? # TRUE or FALSE (case-insensitive) #gui.geometry.save.maximized: TRUE # Main Toolbar style # One of: ICONS, TEXT, BOTH # (case-insensitive). #gui.toolbar_main_style: ICONS # Check for updates (Windows and macOS only) # TRUE or FALSE (case-insensitive) #gui.update.enabled: TRUE # The type of update to fetch. You should probably leave this set to STABLE. # One of: DEVELOPMENT, STABLE # (case-insensitive). #gui.update.channel: STABLE # How often to check for software updates in seconds # A decimal number #gui.update.interval: 86400 # Custom window title to be appended to the existing title # %F = file path of the capture file # %P = profile name # %S = a conditional separator (" - ") that only shows when surrounded by variables with values or static text # %V = version info # A string #gui.window_title: # Custom window title to be prepended to the existing title # %F = file path of the capture file # %P = profile name # %S = a conditional separator (" - ") that only shows when surrounded by variables with values or static text # %V = version info # A string #gui.prepend_window_title: # Custom start page title # A string #gui.start_title: The World's Most Popular Network Protocol Analyzer # Show version in the start page and/or main screen's title bar # One of: WELCOME, TITLE, BOTH, NEITHER # (case-insensitive). #gui.version_placement: BOTH # The maximum number of objects that can be exported # A decimal number #gui.max_export_objects: 1000 # Enable Packet Editor (Experimental) # TRUE or FALSE (case-insensitive) #gui.packet_editor.enabled: FALSE # The position of "..." in packet list text. # One of: LEFT, RIGHT, MIDDLE, NONE # (case-insensitive). #gui.packet_list_elide_mode: RIGHT # Show all interfaces, including interfaces marked as hidden # TRUE or FALSE (case-insensitive) #gui.interfaces_show_hidden: FALSE # Show remote interfaces in the interface selection # TRUE or FALSE (case-insensitive) #gui.interfaces_remote_display: TRUE # Hide the given interface types in the startup list. # A commma-separated string of interface type values (e.g. 5,9). # 0 = Wired, # 1 = AirPCAP, # 2 = Pipe, # 3 = STDIN, # 4 = Bluetooth, # 5 = Wireless, # 6 = Dial-Up, # 7 = USB, # 8 = External Capture, # 9 = Virtual # A string #gui.interfaces_hidden_types: ####### User Interface: Colors ######## # Foregound color for an active selected item # A six-digit hexadecimal RGB color triplet (e.g. fce94f) #gui.active_frame.fg: 000000 # Backgound color for an active selected item # A six-digit hexadecimal RGB color triplet (e.g. fce94f) #gui.active_frame.bg: cbe8ff # Color style for an active selected item # One of: DEFAULT, FLAT, GRADIENT # (case-insensitive). #gui.active_frame.style: DEFAULT # Foregound color for an inactive selected item # A six-digit hexadecimal RGB color triplet (e.g. fce94f) #gui.inactive_frame.fg: 000000 # Backgound color for an inactive selected item # A six-digit hexadecimal RGB color triplet (e.g. fce94f) #gui.inactive_frame.bg: efefef # Color style for an inactive selected item # One of: DEFAULT, FLAT, GRADIENT # (case-insensitive). #gui.inactive_frame.style: DEFAULT # Color preferences for a marked frame # A six-digit hexadecimal RGB color triplet (e.g. fce94f) #gui.marked_frame.fg: ffffff # Color preferences for a marked frame # A six-digit hexadecimal RGB color triplet (e.g. fce94f) #gui.marked_frame.bg: 00202a # Color preferences for a ignored frame # A six-digit hexadecimal RGB color triplet (e.g. fce94f) #gui.ignored_frame.fg: 7f7f7f # Color preferences for a ignored frame # A six-digit hexadecimal RGB color triplet (e.g. fce94f) #gui.ignored_frame.bg: ffffff # TCP stream window color preference # A six-digit hexadecimal RGB color triplet (e.g. fce94f) #gui.stream.client.fg: 7f0000 # TCP stream window color preference # A six-digit hexadecimal RGB color triplet (e.g. fce94f) #gui.stream.client.bg: fbeded # TCP stream window color preference # A six-digit hexadecimal RGB color triplet (e.g. fce94f) #gui.stream.server.fg: 00007f # TCP stream window color preference # A six-digit hexadecimal RGB color triplet (e.g. fce94f) #gui.stream.server.bg: ededfb # Valid color filter background # A six-digit hexadecimal RGB color triplet (e.g. fce94f) #gui.color_filter_bg.valid: afffaf # Invalid color filter background # A six-digit hexadecimal RGB color triplet (e.g. fce94f) #gui.color_filter_bg.invalid: ffafaf # Deprecated color filter background # A six-digit hexadecimal RGB color triplet (e.g. fce94f) #gui.color_filter_bg.deprecated: ffffaf ####### User Interface: Columns ######## # Packet list hidden columns # List all columns to hide in the packet list. gui.column.hidden: %Yut,%Cus:ip.flags:0:R # Packet list column format # Each pair of strings consists of a column title and its format gui.column.format: "No.", "%m", "Time", "%t", "Source", "%s", "Destination", "%d", "Protocol", "%p", "Length", "%L", "Info", "%i", "gcla1", "%Yut", "Flags", "%Cus:ip.flags:0:R", "Authority RRs", "%Cus:nbns.count.auth_rr:0:R" ####### User Interface: Font ######## # Font name for packet list, protocol tree, and hex dump panes. (Qt) # A string gui.qt.font_name: Liberation Mono,11,-1,5,50,0,0,0,0,0 ####### User Interface: Layout ######## # Layout type (1-6) # A decimal number #gui.layout_type: 1 # Layout content of the pane 1 # One of: NONE, PLIST, PDETAILS, PBYTES # (case-insensitive). #gui.layout_content_1: PLIST # Layout content of the pane 2 # One of: NONE, PLIST, PDETAILS, PBYTES # (case-insensitive). #gui.layout_content_2: PDETAILS # Layout content of the pane 3 # One of: NONE, PLIST, PDETAILS, PBYTES # (case-insensitive). #gui.layout_content_3: PBYTES # Enable Packet List Separator # TRUE or FALSE (case-insensitive) #gui.packet_list_separator.enabled: FALSE # Show column definition in packet list header # TRUE or FALSE (case-insensitive) #gui.packet_header_column_definition.enabled: TRUE # Show selected packet in the Status Bar # TRUE or FALSE (case-insensitive) #gui.show_selected_packet.enabled: FALSE # Show file load time in the Status Bar # TRUE or FALSE (case-insensitive) #gui.show_file_load_time.enabled: FALSE # Show related packet indicators in the first column # TRUE or FALSE (case-insensitive) #gui.packet_list_show_related: TRUE # Show the intelligent scroll bar (a minimap of packet list colors in the scrollbar) # TRUE or FALSE (case-insensitive) #gui.packet_list_show_minimap: TRUE ####### Capture ######## # Default capture device # A string #capture.device: # Interface link-layer header types (Ex: en0(1),en1(143),...) # A string #capture.devices_linktypes: # Interface descriptions (Ex: eth0(eth0 descr),eth1(eth1 descr),...) # A string #capture.devices_descr: # Hide interface? (Ex: eth0,eth3,...) # A string #capture.devices_hide: # By default, capture in monitor mode on interface? (Ex: eth0,eth3,...) # A string #capture.devices_monitor_mode: # Interface buffer size (Ex: en0(1),en1(143),...) # A string #capture.devices_buffersize: # Interface snap length (Ex: en0(65535),en1(1430),...) # A string #capture.devices_snaplen: # Interface promiscuous mode (Ex: en0(0),en1(1),...) # A string #capture.devices_pmode: # Capture in promiscuous mode? # TRUE or FALSE (case-insensitive) #capture.prom_mode: TRUE # Interface capture filter (Ex: en0(tcp),en1(udp),...) # A string #capture.devices_filter: # Capture in pcapng format? # TRUE or FALSE (case-insensitive) #capture.pcap_ng: TRUE # Update packet list in real time during capture? # TRUE or FALSE (case-insensitive) #capture.real_time_update: TRUE # Don't automatically load capture interfaces on startup # TRUE or FALSE (case-insensitive) #capture.no_interface_load: FALSE # Disable external capture modules (extcap) # TRUE or FALSE (case-insensitive) #capture.no_extcap: FALSE # Scroll packet list during capture? # TRUE or FALSE (case-insensitive) #capture.auto_scroll: TRUE # Show capture information dialog while capturing? # TRUE or FALSE (case-insensitive) #capture.show_info: FALSE # Column list # List of columns to be displayed in the capture options dialog. # Possible values: INTERFACE, LINK, PMODE, SNAPLEN, MONITOR, BUFFER, FILTER # #capture.columns: # "INTERFACE", "LINK", # "PMODE", "SNAPLEN", # "MONITOR", "BUFFER", # "FILTER" ####### Console ######## # Log level # Console log level (for debugging) # A bitmask of log levels: # ERROR = 4 # CRITICAL = 8 # WARNING = 16 # MESSAGE = 32 # INFO = 64 # DEBUG = 128 #console.log.level: 28 # Look for dissectors that left some bytes undecoded (debug) # TRUE or FALSE (case-insensitive) #console.incomplete_dissectors_check_debug: FALSE ####### Extcap Utilities ######## # Save arguments on start of capture # TRUE or FALSE (case-insensitive) #extcap.gui_save_on_start: TRUE # Remote SSH server address # A string #extcap.sshdump.remotehost: # Remote SSH server port # A string #extcap.sshdump.remoteport: # Remote SSH server username # A string #extcap.sshdump.remoteusername: # Path to SSH private key # A string #extcap.sshdump.sshkey: # ProxyCommand # A string #extcap.sshdump.proxycommand: # Remote interface # A string #extcap.sshdump.remoteinterface: # Remote capture command # A string #extcap.sshdump.remotecapturecommand: # Use sudo on the remote machine # A string #extcap.sshdump.remotesudo: # No promiscuous mode # A string #extcap.sshdump.remotenoprom: # Remote capture filter # A string #extcap.sshdump.remotefilter: not ((host fe80::159e:3034:7984:2edc or host fe80::222d:65ae:e2c3:1120 or host fe80::42:6bff:fe9e:b8df or host 172.16.0.1 or host 10.6.14.98 or host 192.168.86.28 or host 10.225.13.1 or host 172.18.0.1 or host 172.17.0.1 or host 172.19.0.1) and port 22) # Packets to capture # A string #extcap.sshdump.remotecount: 0 # Run in debug mode # A string #extcap.sshdump.debug: false # Use a file for debug # A string #extcap.sshdump.debugfile: # Remote SSH server address # A string #extcap.ciscodump.remotehost: # Remote SSH server port # A string #extcap.ciscodump.remoteport: 22 # Remote SSH server username # A string #extcap.ciscodump.remoteusername: gcla # Path to SSH private key # A string #extcap.ciscodump.sshkey: # ProxyCommand # A string #extcap.ciscodump.proxycommand: # Remote interface # A string #extcap.ciscodump.remoteinterface: # Remote capture filter # A string #extcap.ciscodump.remotefilter: deny tcp host fe80::159e:3034:7984:2edc any eq 0, deny tcp any eq 0 host fe80::159e:3034:7984:2edc, deny tcp host fe80::222d:65ae:e2c3:1120 any eq 0, deny tcp any eq 0 host fe80::222d:65ae:e2c3:1120, deny tcp host fe80::42:6bff:fe9e:b8df any eq 0, deny tcp any eq 0 host fe80::42:6bff:fe9e:b8df, deny tcp host 172.16.0.1 any eq 0, deny tcp any eq 0 host 172.16.0.1, deny tcp host 10.6.14.98 any eq 0, deny tcp any eq 0 host 10.6.14.98, deny tcp host 192.168.86.28 any eq 0, deny tcp any eq 0 host 192.168.86.28, deny tcp host 10.225.13.1 any eq 0, deny tcp any eq 0 host 10.225.13.1, deny tcp host 172.18.0.1 any eq 0, deny tcp any eq 0 host 172.18.0.1, deny tcp host 172.17.0.1 any eq 0, deny tcp any eq 0 host 172.17.0.1, deny tcp host 172.19.0.1 any eq 0, deny tcp any eq 0 host 172.19.0.1, permit ip any any # Packets to capture # A string #extcap.ciscodump.remotecount: # Run in debug mode # A string #extcap.ciscodump.debug: false # Use a file for debug # A string #extcap.ciscodump.debugfile: # Interface index # A string #extcap.dpauxmon.interface_id: 0 # Run in debug mode # A string #extcap.dpauxmon.debug: false # Use a file for debug # A string #extcap.dpauxmon.debugfile: # Max bytes in a packet # A string #extcap.randpkt.maxbytes: 5000 # Number of packets # A string #extcap.randpkt.count: 1000 # Packet delay (ms) # A string #extcap.randpkt.delay: 0 # Random type # A string #extcap.randpkt.randomtype: false # All random packets # A string #extcap.randpkt.allrandom: false # Type of packet # A string #extcap.randpkt.type: # Run in debug mode # A string #extcap.randpkt.debug: false # Use a file for debug # A string #extcap.randpkt.debugfile: # Listen port # A string #extcap.udpdump.port: 5555 # Payload type # A string #extcap.udpdump.payload: data # Run in debug mode # A string #extcap.udpdump.debug: false # Use a file for debug # A string #extcap.udpdump.debugfile: # Starting position # A string #extcap.sdjournal.startfrom: # Run in debug mode # A string #extcap.sdjournal.debug: false # Use a file for debug # A string #extcap.sdjournal.debugfile: ####### Name Resolution ######## # Resolve Ethernet MAC addresses to host names from the preferences or system's Ethers file, or to a manufacturer based name. # TRUE or FALSE (case-insensitive) #nameres.mac_name: TRUE # Resolve TCP/UDP ports into service names # TRUE or FALSE (case-insensitive) #nameres.transport_name: FALSE # Resolve IPv4, IPv6, and IPX addresses into host names. The next set of check boxes determines how name resolution should be performed. If no other options are checked name resolution is made from Wireshark's host file and capture file name resolution blocks. # TRUE or FALSE (case-insensitive) #nameres.network_name: FALSE # Whether address/name pairs found in captured DNS packets should be used by Wireshark for name resolution. # TRUE or FALSE (case-insensitive) #nameres.dns_pkt_addr_resolution: TRUE # Use your system's configured name resolver (usually DNS) to resolve network names. Only applies when network name resolution is enabled. # TRUE or FALSE (case-insensitive) #nameres.use_external_name_resolver: TRUE # Uses DNS Servers list to resolve network names if TRUE. If FALSE, default information is used # TRUE or FALSE (case-insensitive) #nameres.use_custom_dns_servers: FALSE # The maximum number of DNS requests that may be active at any time. A large value (many thousands) might overload the network or make your DNS server behave badly. # A decimal number #nameres.name_resolve_concurrency: 500 # By default "hosts" files will be loaded from multiple sources. Checking this box only loads the "hosts" in the current profile. # TRUE or FALSE (case-insensitive) #nameres.hosts_file_handling: FALSE # Resolve VLAN IDs to network names from the preferences "vlans" file. Format of the file is: "IDName". One line per VLAN, e.g.: 1 Management # TRUE or FALSE (case-insensitive) #nameres.vlan_name: FALSE # Resolve SS7 Point Codes to node names from the profiles "ss7pcs" file. Format of the file is: "Network_IndicatorPC_DecimalName". One line per Point Code, e.g.: 2-1234 MyPointCode1 # TRUE or FALSE (case-insensitive) #nameres.ss7_pc_name: FALSE # Resolve Object IDs to object names from the MIB and PIB modules defined below. You must restart Wireshark for this change to take effect # TRUE or FALSE (case-insensitive) #nameres.load_smi_modules: FALSE # While loading MIB or PIB modules errors may be detected, which are reported. Some errors can be ignored. If unsure, set to false. # TRUE or FALSE (case-insensitive) #nameres.suppress_smi_errors: FALSE ####### Protocols ######## # Display all hidden protocol items in the packet list. # TRUE or FALSE (case-insensitive) #protocols.display_hidden_proto_items: FALSE # Display all byte fields with a space character between each byte in the packet list. # TRUE or FALSE (case-insensitive) #protocols.display_byte_fields_with_spaces: FALSE # Look for dissectors that left some bytes undecoded. # TRUE or FALSE (case-insensitive) #protocols.enable_incomplete_dissectors_check: FALSE # Protocols may use things like VLAN ID or interface ID to narrow the potential for duplicate conversations.Currently only ICMP and ICMPv6 use this preference to add VLAN ID to conversation tracking # TRUE or FALSE (case-insensitive) #protocols.strict_conversation_tracking_heuristics: FALSE # Use a registered heuristic sub-dissector to decode the data payload # TRUE or FALSE (case-insensitive) #lbmc.use_heuristic_subdissectors: TRUE # Reassemble data message fragments # TRUE or FALSE (case-insensitive) #lbmc.reassemble_fragments: FALSE # Recognize and dissect payloads containing LBMPDM messages (requires reassembly to be enabled) # TRUE or FALSE (case-insensitive) #lbmc.dissect_lbmpdm: FALSE # Set the low end of the TCP port range # A decimal number #lbmpdm_tcp.port_low: 14371 # Set the high end of the port range # A decimal number #lbmpdm_tcp.port_high: 14390 # Use table of LBMPDM-TCP tags to decode the packet instead of above values # TRUE or FALSE (case-insensitive) #lbmpdm_tcp.use_lbmpdm_tcp_domain: FALSE # Set the UDP port for incoming multicast topic resolution (context resolver_multicast_incoming_port) # A decimal number #lbmr.mc_incoming_port: 12965 # Set the multicast address for incoming multicast topic resolution (context resolver_multicast_incoming_address) # A string #lbmr.mc_incoming_address: 224.9.10.11 # Set the UDP port for outgoing multicast topic resolution (context resolver_multicast_outgoing_port) # A decimal number #lbmr.mc_outgoing_port: 12965 # Set the multicast address for outgoing multicast topic resolution (context resolver_multicast_outgoing_address) # A string #lbmr.mc_outgoing_address: 224.9.10.11 # Set the low UDP port for unicast topic resolution (context resolver_unicast_port_low) # A decimal number #lbmr.uc_port_low: 14402 # Set the high UDP port for unicast topic resolution (context resolver_unicast_port_high) # A decimal number #lbmr.uc_port_high: 14406 # Set the destination port for unicast topic resolution (context resolver_unicast_destination_port) # A decimal number #lbmr.uc_dest_port: 15380 # Set the address of the unicast resolver daemon (context resolver_unicast_address) # A string #lbmr.uc_address: 0.0.0.0 # Use table of LBMR tags to decode the packet instead of above values # TRUE or FALSE (case-insensitive) #lbmr.use_lbmr_domain: FALSE # Set the low end of the LBT-RM multicast address range (context transport_lbtrm_multicast_address_low) # A string #lbtrm.mc_address_low: 224.10.10.10 # Set the high end of the LBT-RM multicast address range (context transport_lbtrm_multicast_address_high) # A string #lbtrm.mc_address_high: 224.10.10.14 # Set the low end of the LBT-RM UDP destination port range (source transport_lbtrm_destination_port) # A decimal number #lbtrm.dport_low: 14400 # Set the high end of the LBT-RM UDP destination port range (source transport_lbtrm_destination_port) # A decimal number #lbtrm.dport_high: 14400 # Set the low end of the LBT-RM UDP source port range (context transport_lbtrm_source_port_low) # A decimal number #lbtrm.sport_low: 14390 # Set the high end of the LBT-RM UDP source port range (context transport_lbtrm_source_port_high) # A decimal number #lbtrm.sport_high: 14399 # Set the incoming MIM multicast address (context mim_incoming_address) # A string #lbtrm.mim_incoming_address: 224.10.10.21 # Set the outgoing MIM multicast address (context mim_outgoing_address) # A string #lbtrm.mim_outgoing_address: 224.10.10.21 # Set the incoming MIM UDP port (context mim_incoming_destination_port) # A decimal number #lbtrm.mim_incoming_dport: 14401 # Set the outgoing MIM UDP port (context mim_outgoing_destination_port) # A decimal number #lbtrm.mim_outgoing_dport: 14401 # Separate multiple NAKs from a single packet into distinct Expert Info entries # TRUE or FALSE (case-insensitive) #lbtrm.expert_separate_naks: FALSE # Separate multiple NCFs from a single packet into distinct Expert Info entries # TRUE or FALSE (case-insensitive) #lbtrm.expert_separate_ncfs: FALSE # Perform analysis on LBT-RM sequence numbers to determine out-of-order, gaps, loss, etc # TRUE or FALSE (case-insensitive) #lbtrm.sequence_analysis: FALSE # Use table of LBT-RM tags to decode the packet instead of above values # TRUE or FALSE (case-insensitive) #lbtrm.use_lbtrm_domain: FALSE # Set the low end of the LBT-RU source UDP port range (context transport_lbtru_port_low) # A decimal number #lbtru.source_port_low: 14380 # Set the high end of the LBT-RU source UDP port range (context transport_lbtru_port_high) # A decimal number #lbtru.source_port_high: 14389 # Set the low end of the LBT-RU receiver UDP port range (receiver transport_lbtru_port_low) # A decimal number #lbtru.receiver_port_low: 14360 # Set the high end of the LBT-RU receiver UDP port range (receiver transport_lbtru_port_high) # A decimal number #lbtru.receiver_port_high: 14379 # Separate multiple NAKs from a single packet into distinct Expert Info entries # TRUE or FALSE (case-insensitive) #lbtru.expert_separate_naks: FALSE # Separate multiple NCFs from a single packet into distinct Expert Info entries # TRUE or FALSE (case-insensitive) #lbtru.expert_separate_ncfs: FALSE # Perform analysis on LBT-RU sequence numbers to determine out-of-order, gaps, loss, etc # TRUE or FALSE (case-insensitive) #lbtru.sequence_analysis: FALSE # Use table of LBT-RU tags to decode the packet instead of above values # TRUE or FALSE (case-insensitive) #lbtru.use_lbtru_domain: FALSE # Set the low end of the LBT-TCP source TCP port range (context transport_tcp_port_low) # A decimal number #lbttcp.source_port_low: 14371 # Set the high end of the LBT-TCP source TCP port range (context transport_tcp_port_high) # A decimal number #lbttcp.source_port_high: 14390 # Set the low end of the LBT-TCP request TCP port range (context request_tcp_port_low) # A decimal number #lbttcp.request_port_low: 14391 # Set the high end of the LBT-TCP request TCP port range (context request_tcp_port_high) # A decimal number #lbttcp.request_port_high: 14395 # Set the low end of the LBT-TCP UME Store TCP port range # A decimal number #lbttcp.store_port_low: 0 # Set the high end of the LBT-TCP UME Store TCP port range # A decimal number #lbttcp.store_port_high: 0 # Use table of LBT-TCP tags to decode the packet instead of above values # TRUE or FALSE (case-insensitive) #lbttcp.use_lbttcp_domain: FALSE # Enable this option to recognise all traffic on RTP dynamic payload type 96 (0x60) as FEC data corresponding to Pro-MPEG Code of Practice #3 release 2 # TRUE or FALSE (case-insensitive) #2dparityfec.enable: FALSE # Derive IID from a short 16-bit address according to RFC 4944 (using the PAN ID). # TRUE or FALSE (case-insensitive) #6lowpan.rfc4944_short_address_format: FALSE # Linux kernels before version 4.12 does toggle the Universal/Local bit. # TRUE or FALSE (case-insensitive) #6lowpan.iid_has_universal_local_bit: FALSE # Whether the IPv6 summary line should be shown in the protocol tree # TRUE or FALSE (case-insensitive) #6lowpan.summary_in_tree: TRUE # IPv6 prefix to use for stateful address decompression. # A string #6lowpan.context0: # IPv6 prefix to use for stateful address decompression. # A string #6lowpan.context1: # IPv6 prefix to use for stateful address decompression. # A string #6lowpan.context2: # IPv6 prefix to use for stateful address decompression. # A string #6lowpan.context3: # IPv6 prefix to use for stateful address decompression. # A string #6lowpan.context4: # IPv6 prefix to use for stateful address decompression. # A string #6lowpan.context5: # IPv6 prefix to use for stateful address decompression. # A string #6lowpan.context6: # IPv6 prefix to use for stateful address decompression. # A string #6lowpan.context7: # IPv6 prefix to use for stateful address decompression. # A string #6lowpan.context8: # IPv6 prefix to use for stateful address decompression. # A string #6lowpan.context9: # IPv6 prefix to use for stateful address decompression. # A string #6lowpan.context10: # IPv6 prefix to use for stateful address decompression. # A string #6lowpan.context11: # IPv6 prefix to use for stateful address decompression. # A string #6lowpan.context12: # IPv6 prefix to use for stateful address decompression. # A string #6lowpan.context13: # IPv6 prefix to use for stateful address decompression. # A string #6lowpan.context14: # IPv6 prefix to use for stateful address decompression. # A string #6lowpan.context15: # Some generators incorrectly indicate long preamble when the preamble was actuallyshort. Always assume short preamble when calculating duration. # TRUE or FALSE (case-insensitive) #wlan_radio.always_short_preamble: FALSE # Some generators timestamp the end of the PPDU rather than the start of the (A)MPDU. # TRUE or FALSE (case-insensitive) #wlan_radio.tsf_at_end: TRUE # Enables an additional panel for navigating through packets # TRUE or FALSE (case-insensitive) #wlan_radio.timeline: FALSE # Radiotap has a bit to indicate whether the FCS is still on the frame or not. Some generators (e.g. AirPcap) use a non-standard radiotap flag 14 to put the FCS into the header. # TRUE or FALSE (case-insensitive) #radiotap.bit14_fcs_in_header: FALSE # Some generators use rates with bit 7 set to indicate an MCS, e.g. BSD. others (Linux, AirPcap) do not. # TRUE or FALSE (case-insensitive) #radiotap.interpret_high_rates_as_mcs: FALSE # Whether to use the FCS bit, assume the FCS is always present, or assume the FCS is never present. # One of: Use the FCS bit, Assume all packets have an FCS at the end, Assume all packets don't have an FCS at the end # (case-insensitive). #radiotap.fcs_handling: Use the FCS bit # Use ipaccess nanoBTS specific definitions for OML # One of: ETSI/3GPP TS 12.21, Siemens, ip.access, Ericsson OM2000 # (case-insensitive). #gsm_abis_oml.oml_dialect: ETSI/3GPP TS 12.21 # Enable Streaming DMX extension dissector (ANSI BSR E1.31) # TRUE or FALSE (case-insensitive) #acn.dmx_enable: FALSE # Display format # One of: Hex , Decimal, Percent # (case-insensitive). #acn.dmx_display_view: Hex # Display zeros instead of dots # TRUE or FALSE (case-insensitive) #acn.dmx_display_zeros: FALSE # Display leading zeros on levels # TRUE or FALSE (case-insensitive) #acn.dmx_display_leading_zeros: FALSE # Display line format # One of: 20 per line, 16 per line # (case-insensitive). #acn.dmx_display_line_format: 20 per line # Server Port # A decimal number #adb_cs.server_port: 5037 # Dissect more detail for framebuffer service # TRUE or FALSE (case-insensitive) #adb_service.framebuffer_more_details: FALSE # Specify if the Data sections of packets should be dissected or not # TRUE or FALSE (case-insensitive) #adwin.dissect_data: TRUE # Include next/previous frame for channel, stream, and term, and other transport sequence analysis. # TRUE or FALSE (case-insensitive) #aeron.sequence_analysis: FALSE # Include stream analysis, tracking publisher and subscriber positions. Requires "Analyze transport sequencing". # TRUE or FALSE (case-insensitive) #aeron.stream_analysis: FALSE # Reassemble fragmented data messages. Requires "Analyze transport sequencing" and "Analyze stream sequencing". # TRUE or FALSE (case-insensitive) #aeron.reassemble_fragments: FALSE # Use a registered heuristic sub-dissector to decode the payload data. Requires "Analyze transport sequencing", "Analyze stream sequencing", and "Reassemble fragmented data". # TRUE or FALSE (case-insensitive) #aeron.use_heuristic_subdissectors: FALSE # Whether the AIM dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #aim.desegment: TRUE # Whether the LCT header Codepoint field should be considered the FEC Encoding ID of carried object # TRUE or FALSE (case-insensitive) #alc.lct.codepoint_as_fec_id: TRUE # How to decode LCT header extension 192 # One of: Don't decode, Decode as FLUTE extension (EXT_FDT) # (case-insensitive). #alc.lct.ext.192: Decode as FLUTE extension (EXT_FDT) # How to decode LCT header extension 193 # One of: Don't decode, Decode as FLUTE extension (EXT_CENC) # (case-insensitive). #alc.lct.ext.193: Decode as FLUTE extension (EXT_CENC) # Whether persistent call leg information is to be kept # TRUE or FALSE (case-insensitive) #alcap.leg_info: TRUE # Set the TCP port for AMQP over SSL/TLS(if other than the default of 5671) # A decimal number #amqp.tls.port: 5671 # The dynamic payload types which will be interpreted as AMR(default 0) # A string denoting an positive integer range (e.g., "1-20,30-40") #amr.dynamic.payload.type: # The dynamic payload types which will be interpreted as AMR-WB(default 0) # A string denoting an positive integer range (e.g., "1-20,30-40") #amr.wb.dynamic.payload.type: # Type of AMR encoding of the payload # One of: RFC 3267 octet aligned, RFC 3267 BW-efficient, AMR IF1, AMR IF2 # (case-insensitive). #amr.encoding.version: RFC 3267 octet aligned # The AMR mode # One of: Narrowband AMR, Wideband AMR # (case-insensitive). #amr.mode: Narrowband AMR # (if other than the default of IOS 4.0.1) # One of: IS-634 rev. 0, TSB-80, IS-634-A, IOS 2.x, IOS 3.x, IOS 4.0.1, IOS 5.0.1 # (case-insensitive). #ansi_a_bsmap.global_variant: IOS 4.0.1 # Whether the mobile ID and service options are displayed in the INFO column # TRUE or FALSE (case-insensitive) #ansi_a_bsmap.top_display_mid_so: TRUE # ANSI MAP SSNs to decode as ANSI MAP # A string denoting an positive integer range (e.g., "1-20,30-40") #ansi_map.map.ssn: 5-14 # Type of matching invoke/response, risk of mismatch if loose matching chosen # One of: Transaction ID only, Transaction ID and Source, Transaction ID Source and Destination # (case-insensitive). #ansi_map.transaction.matchtype: Transaction ID and Source # Type of matching invoke/response, risk of mismatch if loose matching chosen # One of: Transaction ID only, Transaction ID and Source, Transaction ID Source and Destination # (case-insensitive). #ansi_tcap.transaction.matchtype: Transaction ID only # Whether the AOL dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #aol.desegment: TRUE # Attempt to display common APRS protocol violations correctly # TRUE or FALSE (case-insensitive) #aprs.showaprslax: FALSE # Attempt to detect excessive rate of ARP requests # TRUE or FALSE (case-insensitive) #arp.detect_request_storms: FALSE # Number of requests needed within period to indicate a storm # A decimal number #arp.detect_storm_number_of_packets: 30 # Period in milliseconds during which a packet storm may be detected # A decimal number #arp.detect_storm_period: 100 # Attempt to detect duplicate use of IP addresses # TRUE or FALSE (case-insensitive) #arp.detect_duplicate_ips: TRUE # Try to resolve physical addresses to host names from ARP requests/responses # TRUE or FALSE (case-insensitive) #arp.register_network_address_binding: TRUE # Select the CAT001 version # One of: Version 1.2 # (case-insensitive). #asterix.i001_version: Version 1.2 # Select the CAT002 version # One of: Version 1.0 # (case-insensitive). #asterix.i002_version: Version 1.0 # Select the CAT004 version # One of: Version 1.7 # (case-insensitive). #asterix.i004_version: Version 1.7 # Select the CAT008 version # One of: Version 1.1 # (case-insensitive). #asterix.i008_version: Version 1.1 # Select the CAT009 version # One of: Version 2.0 # (case-insensitive). #asterix.i009_version: Version 2.0 # Select the CAT019 version # One of: Version 1.3 # (case-insensitive). #asterix.i019_version: Version 1.3 # Select the CAT020 version # One of: Version 1.9 # (case-insensitive). #asterix.i020_version: Version 1.9 # Select the CAT021 version # One of: Version 2.3, Version 2.1, Version 0.26, Version 0.23 # (case-insensitive). #asterix.i021_version: Version 2.3 # Select the CAT023 version # One of: Version 1.2 # (case-insensitive). #asterix.i023_version: Version 1.2 # Select the CAT025 version # One of: Version 1.1 # (case-insensitive). #asterix.i025_version: Version 1.1 # Select the CAT032 version # One of: Version 1.0 # (case-insensitive). #asterix.i032_version: Version 1.0 # Select the CAT034 version # One of: Version 1.27 # (case-insensitive). #asterix.i034_version: Version 1.27 # Select the CAT048 version # One of: Version 1.23, Version 1.21, Version 1.17 # (case-insensitive). #asterix.i048_version: Version 1.23 # Select the CAT062 version # One of: Version 1.18, Version 1.17, Version 1.16, Version 0.17 # (case-insensitive). #asterix.i062_version: Version 1.18 # Select the CAT063 version # One of: Version 1.4 # (case-insensitive). #asterix.i063_version: Version 1.4 # Select the CAT065 version # One of: Version 1.4, Version 1.3 # (case-insensitive). #asterix.i065_version: Version 1.4 # Force treat packets as DTE (PC) or DCE (Modem) role # One of: Off, Sent is DTE, Rcvd is DCE, Sent is DCE, Rcvd is DTE # (case-insensitive). #at.role: Off # Autodection between LANE and SSCOP is hard. As default LANE is preferred # TRUE or FALSE (case-insensitive) #atm.dissect_lane_as_sscop: FALSE # Whether the ATP dissector should reassemble messages spanning multiple DDP packets # TRUE or FALSE (case-insensitive) #atp.desegment: TRUE # In the standard the Source Node Identifier is the first byte and the Control Bit Vector is the second byte. Using this parameter they can be swapped # TRUE or FALSE (case-insensitive) #autosar-nm.swap_ctrl_and_src: TRUE # Revision 4.3.1 of the specification doesn't have 'NM Coordinator Id' in Control Bit Vector. Using this parameter one may switch to a mode compatible with revision 3.2 of the specification. # TRUE or FALSE (case-insensitive) #autosar-nm.interpret_coord_id: FALSE # Identifier that is used to filter packets that should be dissected. Set bit 31 when defining an extended id. (works with the mask defined below) # A hexadecimal number #autosar-nm.can_id: 0 # Mask applied to CAN identifiers when decoding whether a packet should dissected. Use 0xFFFFFFFF mask to require exact match. # A hexadecimal number #autosar-nm.can_id_mask: 0 # Enable checksum calculation. # TRUE or FALSE (case-insensitive) #ax25_kiss.showcksum: FALSE # Enable decoding of the payload as APRS. # TRUE or FALSE (case-insensitive) #ax25_nol3.showaprs: FALSE # Enable decoding of the payload as DX cluster info. # TRUE or FALSE (case-insensitive) #ax25_nol3.showcluster: FALSE # Ethertype used to indicate B.A.T.M.A.N. packet. # A hexadecimal number #batadv.batmanadv.ethertype: 0x4305 # Whether the Bazaar dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #bzr.desegment: TRUE # Specifies that BEEP requires CRLF as a terminator, and not just CR or LF # TRUE or FALSE (case-insensitive) #beep.strict_header_terminator: TRUE # Whether the dissector should also display internal ASN.1 BER details such as Identifier and Length fields # TRUE or FALSE (case-insensitive) #ber.show_internals: FALSE # Whether the dissector should decode unexpected tags as ASN.1 BER encoded data # TRUE or FALSE (case-insensitive) #ber.decode_unexpected: FALSE # Whether the dissector should try decoding OCTET STRINGs as constructed ASN.1 BER encoded data # TRUE or FALSE (case-insensitive) #ber.decode_octetstring: FALSE # Whether the dissector should try decoding unknown primitive as constructed ASN.1 BER encoded data # TRUE or FALSE (case-insensitive) #ber.decode_primitive: FALSE # Whether the dissector should warn if excessive leading zero (0) bits # TRUE or FALSE (case-insensitive) #ber.warn_too_many_bytes: FALSE # Whether the BGP dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #bgp.desegment: TRUE # BGP dissector detect the length of the AS number in AS_PATH attributes automatically or manually (NOTE: Automatic detection is not 100% accurate) # One of: Auto-detect, 2 octet, 4 octet # (case-insensitive). #bgp.asn_len: Auto-detect # Whether the Bitcoin dissector should desegment all messages spanning multiple TCP segments # TRUE or FALSE (case-insensitive) #bitcoin.desegment: TRUE # Whether the BitTorrent dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #bittorrent.desegment: TRUE # Enabling this will tell which BitTorrent client that produced the handshake message # TRUE or FALSE (case-insensitive) #bittorrent.decode_client: FALSE # Force decoding stream as A2DP with Content Protection SCMS-T # TRUE or FALSE (case-insensitive) #bta2dp.a2dp.content_protection.scms_t: FALSE # Force decoding stream as A2DP with specified codec # One of: Default, SBC, MPEG12 AUDIO, MPEG24 AAC, aptX, aptX HD, LDAC # (case-insensitive). #bta2dp.a2dp.codec: Default # Dissecting the top protocols # TRUE or FALSE (case-insensitive) #btbnep.bnep.top_dissect: TRUE # If "yes" localhost will be treat as Client, "no" as Server # One of: Default, Yes, No # (case-insensitive). #bthcrp.hcrp.force_client: Default # L2CAP PSM for Control # A decimal number #bthcrp.hcrp.control.psm: 0 # L2CAP PSM for Data # A decimal number #bthcrp.hcrp.data.psm: 0 # L2CAP PSM for Notification # A decimal number #bthcrp.hcrp.notification.psm: 0 # Force treat packets as AG or HS role # One of: Off, Sent is AG, Rcvd is HS, Sent is HS, Rcvd is AG # (case-insensitive). #bthfp.hfp.hfp_role: Off # Show what is deprecated in HID 1.1 # TRUE or FALSE (case-insensitive) #bthid.hid.deprecated: FALSE # Force treat packets as AG or HS role # One of: Off, Sent is AG, Rcvd is HS, Sent is HS, Rcvd is AG # (case-insensitive). #bthsp.hsp.hsp_role: Off # Detect retransmission based on SN (Sequence Number) # TRUE or FALSE (case-insensitive) #btle.detect_retransmit: TRUE # Turn on/off decode by next rules # TRUE or FALSE (case-insensitive) #btrfcomm.rfcomm.decode_by.enabled: FALSE # Dissecting the top protocols # One of: off, Put higher dissectors under this one, On top # (case-insensitive). #btsap.sap.top_dissect: Put higher dissectors under this one # Force decoding stream as VDP with Content Protection SCMS-T # TRUE or FALSE (case-insensitive) #btvdp.vdp.content_protection.scms_t: FALSE # Force decoding stream as VDP with specified codec # One of: H263, MPEG4 VSP # (case-insensitive). #btvdp.vdp.codec: H263 # Whether the ACL dissector should reassemble fragmented PDUs # TRUE or FALSE (case-insensitive) #bthci_acl.hci_acl_reassembly: TRUE # Whether the BMP dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #bmp.desegment: TRUE # For the sake of sub-dissectors registering to accept data from the BSSAP/BSAP dissector, this defines whether it is identified as BSSAP or BSAP. # One of: BSSAP, BSAP # (case-insensitive). #bssap.bsap_or_bssap: BSSAP # GSM-A is the interface between the BSC and the MSC. Lb is the interface between the BSC and the SMLC. # One of: GSM A, Lb # (case-insensitive). #bssap.gsm_or_lb_interface: GSM A # Set Subsystem number used for BSSAP+ # A decimal number #bssap_plus.ssn: 98 # Decode NRI (for use with SGSN in Pool) # TRUE or FALSE (case-insensitive) #bssgp.decode_nri: FALSE # NRI length, in bits # A decimal number #bssgp.nri_length: 4 # Dissect next layer # TRUE or FALSE (case-insensitive) #btsnoop.dissect_next_layer: FALSE # Whether the C12.22 dissector should reassemble all messages spanning multiple TCP segments # TRUE or FALSE (case-insensitive) #c1222.desegment: TRUE # Base object identifier for use in resolving relative object identifiers # A string #c1222.baseoid: # Whether the C12.22 dissector should verify the crypto for all relevant messages # TRUE or FALSE (case-insensitive) #c1222.decrypt: TRUE # Whether the C12.22 dissector should interpret procedure numbers as big-endian # TRUE or FALSE (case-insensitive) #c1222.big_endian: FALSE # The date format: (DD/MM) or (MM/DD) # One of: DD/MM/YYYY, MM/DD/YYYY # (case-insensitive). #camel.date.format: DD/MM/YYYY # TCAP Subsystem numbers used for Camel # A string denoting an positive integer range (e.g., "1-20,30-40") #camel.tcap.ssn: 146 # Enable response time analysis # TRUE or FALSE (case-insensitive) #camel.srt: FALSE # Statistics for Response Time # TRUE or FALSE (case-insensitive) #camel.persistentsrt: FALSE # Whether the CAN ID/flags field should be byte-swapped # TRUE or FALSE (case-insensitive) #can.byte_swap: FALSE # Try to decode a packet using an heuristic sub-dissector before using a sub-dissector registered to "decode as" # TRUE or FALSE (case-insensitive) #can.try_heuristic_first: FALSE # Try to decode a packet using an heuristic sub-dissector before using a sub-dissector registered to "decode as" # TRUE or FALSE (case-insensitive) #acf-can.try_heuristic_first: FALSE # Enable support of Cisco Wireless Controller (based on old 8 draft revision). # TRUE or FALSE (case-insensitive) #capwap.draft_8_cisco: FALSE # Reassemble fragmented CAPWAP packets. # TRUE or FALSE (case-insensitive) #capwap.reassemble: TRUE # Swap frame control bytes (needed for some APs). # TRUE or FALSE (case-insensitive) #capwap.swap_fc: TRUE # Whether the CAST dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #cast.reassembly: TRUE # Whether the checksum of all messages should be validated or not # TRUE or FALSE (case-insensitive) #cattp.checksum: TRUE # Specify how the dissector should handle the CCSDS checkword # One of: Use header flag, Override header flag to be false, Override header flag to be true # (case-insensitive). #ccsds.global_pref_checkword: Use header flag # Whether or not the RTP header is present in the CES payload. # TRUE or FALSE (case-insensitive) #cesoeth.rtp_header: FALSE # Heuristically determine if an RTP header is present in the CES payload. # TRUE or FALSE (case-insensitive) #cesoeth.rtp_header_heuristic: TRUE # Set the port(s) for NetFlow messages (default: 2055,9996) # A string denoting an positive integer range (e.g., "1-20,30-40") #cflow.netflow.ports: 2055,9996 # Set the port(s) for IPFIX messages (default: 4739) # A string denoting an positive integer range (e.g., "1-20,30-40") #cflow.ipfix.ports: 4739 # Set the number of fields allowed in a template. Use 0 (zero) for unlimited. (default: 60) # A decimal number #cflow.max_template_fields: 60 # Whether the Netflow/Ipfix dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #cflow.desegment: TRUE # Whether to validate the Frame Check Sequence # TRUE or FALSE (case-insensitive) #cfp.check_fcs: FALSE # The type of CHDLC frame checksum (none, 16-bit, 32-bit) # One of: None, 16-Bit, 32-Bit # (case-insensitive). #chdlc.fcs_type: None # The version of CIGI with which to dissect packets # One of: From Packet, CIGI 2, CIGI 3 # (case-insensitive). #cigi.version: From Packet # The byte order with which to dissect CIGI packets (CIGI3) # One of: From Packet, Big-Endian, Little-Endian # (case-insensitive). #cigi.byte_order: From Packet # IPv4 address or hostname of the host # A string #cigi.host: # IPv4 address or hostname of the image generator # A string #cigi.ig: # Whether the CIP dissector should display enhanced/verbose data in the Info column for CIP explicit messages # TRUE or FALSE (case-insensitive) #cip.enhanced_info_column: TRUE # NSAP selector for Transport Protocol (last byte in hex) # A hexadecimal number #clnp.tp_nsap_selector: 0x21 # Always try to decode NSDU as transport PDUs # TRUE or FALSE (case-insensitive) #clnp.always_decode_transport: FALSE # Whether segmented CLNP datagrams should be reassembled # TRUE or FALSE (case-insensitive) #clnp.reassemble: TRUE # Whether ATN security label should be decoded # TRUE or FALSE (case-insensitive) #clnp.decode_atn_options: FALSE # Whether the CMP-over-TCP dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #cmp.desegment: TRUE # Decode this TCP port's traffic as CMP-over-HTTP. Set to "0" to disable. Use this if the Content-Type is not set correctly. # A decimal number #cmp.http_alternate_port: 0 # Decode this TCP port's traffic as TCP-transport-style CMP-over-HTTP. Set to "0" to disable. Use this if the Content-Type is not set correctly. # A decimal number #cmp.tcp_style_http_alternate_port: 0 # Whether the COPS dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #cops.desegment: TRUE # Decode the COPS messages using PacketCable clients. (Select port 2126) # TRUE or FALSE (case-insensitive) #cops.packetcable: TRUE # Semicolon-separated list of keys for decryption(e.g. key1;key2;... # A string #corosync_totemnet.private_keys: # Whether segmented COTP datagrams should be reassembled. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #cotp.reassemble: TRUE # How TSAPs should be displayed # One of: As strings if printable, As strings, As bytes # (case-insensitive). #cotp.tsap_display: As strings if printable # Whether to decode OSI TPDUs with ATN (Aereonautical Telecommunications Network) extensions. To use this option, you must also enable "Always try to decode NSDU as transport PDUs" in the CLNP protocol settings. # TRUE or FALSE (case-insensitive) #cotp.decode_atn: FALSE # Whether the memcache dissector should reassemble PDUs spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #couchbase.desegment_pdus: TRUE # The port used for communicating with the data service via SSL/TLS # A decimal number #couchbase.tls.port: 11207 # Whether the SEL Protocol dissector should automatically pre-process Telnet data to remove IAC bytes # TRUE or FALSE (case-insensitive) #cp2179.telnetclean: TRUE # Set the port for InstanceToInstance messages (if other than the default of 5001) # A decimal number #cpfi.udp.port2: 5001 # Control the way the '-->' is displayed. When enabled, keeps the 'lowest valued' endpoint of the src-dest pair on the left, and the arrow moves to distinguish source from dest. When disabled, keeps the arrow pointing right so the source of the frame is always on the left. # TRUE or FALSE (case-insensitive) #cpfi.arrow_ctl: TRUE # Show not dissected data on new Packet Bytes pane # TRUE or FALSE (case-insensitive) #data.datapref.newpane: FALSE # Try to uncompress zlib compressed data and show as uncompressed if successful # TRUE or FALSE (case-insensitive) #data.uncompress_data: FALSE # Show data as text in the Packet Details pane # TRUE or FALSE (case-insensitive) #data.show_as_text: FALSE # Whether or not MD5 hashes should be generated and shown for each payload. # TRUE or FALSE (case-insensitive) #data.md5_hash: FALSE # Whether the LAN sync dissector should reassemble PDUs spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #db-lsp.desegment_pdus: TRUE # Try to decode the payload using an heuristic sub-dissector # TRUE or FALSE (case-insensitive) #db-lsp.try_heuristic: TRUE # Whether the DCCP summary line should be shown in the protocol tree # TRUE or FALSE (case-insensitive) #dccp.summary_in_tree: TRUE # Try to decode a packet using an heuristic sub-dissector before using a sub-dissector registered to a specific port # TRUE or FALSE (case-insensitive) #dccp.try_heuristic_first: FALSE # Whether to check the validity of the DCCP checksum # TRUE or FALSE (case-insensitive) #dccp.check_checksum: TRUE # Whether the DCE/RPC dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #dcerpc.desegment_dcerpc: TRUE # Whether the DCE/RPC dissector should reassemble fragmented DCE/RPC PDUs # TRUE or FALSE (case-insensitive) #dcerpc.reassemble_dcerpc: TRUE # Display some DCOM unmarshalled fields usually hidden # TRUE or FALSE (case-insensitive) #dcom.display_unmarshalling_details: FALSE # If a payload looks like it's embedded in an IP primitive message, and there is a Wireshark dissector matching the DCT2000 protocol name, try parsing the payload using that dissector # TRUE or FALSE (case-insensitive) #dct2000.ipprim_heuristic: TRUE # If a payload looks like it's embedded in an SCTP primitive message, and there is a Wireshark dissector matching the DCT2000 protocol name, try parsing the payload using that dissector # TRUE or FALSE (case-insensitive) #dct2000.sctpprim_heuristic: TRUE # When set, attempt to decode LTE RRC frames. Note that this won't affect other protocols that also call the LTE RRC dissector # TRUE or FALSE (case-insensitive) #dct2000.decode_lte_rrc: TRUE # When set, look for formatted messages indicating specific events. This may be quite slow, so should be disabled if LTE MAC is not being analysed # TRUE or FALSE (case-insensitive) #dct2000.decode_mac_lte_oob_messages: TRUE # When set, look for some older protocol names so thatthey may be matched with wireshark dissectors. # TRUE or FALSE (case-insensitive) #dct2000.convert_old_protocol_names: FALSE # Novell Servers option 85 can be configured as a string instead of address # TRUE or FALSE (case-insensitive) #dhcp.novellserverstring: FALSE # The PacketCable CCC protocol version # One of: PKT-SP-PROV-I05-021127, IETF Draft 5, RFC 3495 # (case-insensitive). #dhcp.pkt.ccc.protocol_version: RFC 3495 # Option Number for PacketCable CableLabs Client Configuration # A decimal number #dhcp.pkt.ccc.option: 122 # Endianness applied to UUID fields # One of: Little Endian, Big Endian # (case-insensitive). #dhcp.uuid.endian: Little Endian # Whether the DHCP failover dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #dhcpfo.desegment: TRUE # Whether Option 18 is dissected as CableLab or RFC 3315 # TRUE or FALSE (case-insensitive) #dhcpv6.cablelabs_interface_id: FALSE # Whether the Bulk Leasequery dissector should desegment all messages spanning multiple TCP segments # TRUE or FALSE (case-insensitive) #dhcpv6.bulk_leasequery.desegment: TRUE # SCTP ports to be decoded as Diameter (default: 3868) # A string denoting an positive integer range (e.g., "1-20,30-40") #diameter.sctp.ports: 3868 # Whether the Diameter dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #diameter.desegment: TRUE # Create DICOM File Meta Header according to PS 3.10 on export for PDUs. If the captured PDV does not contain a SOP Class UID and SOP Instance UID (e.g. for command PDVs), wireshark specific ones will be created. # TRUE or FALSE (case-insensitive) #dicom.export_header: TRUE # Do not show items below this size in the export list. Set it to 0, to see DICOM commands and responses in the list. Set it higher, to just export DICOM IODs (i.e. CT Images, RT Structures). # A decimal number #dicom.export_minsize: 4096 # Create a node for sequences and items, and show children in a hierarchy. De-select this option, if you prefer a flat display or e.g. when using TShark to create a text output. # TRUE or FALSE (case-insensitive) #dicom.seq_tree: TRUE # Create a node for a tag and show tag details as single elements. This can be useful to debug a tag and to allow display filters on these attributes. When using TShark to create a text output, it's better to have it disabled. # TRUE or FALSE (case-insensitive) #dicom.tag_tree: FALSE # Show message ID and number of completed, remaining, warned or failed operations in header and info column. # TRUE or FALSE (case-insensitive) #dicom.cmd_details: TRUE # Decode all DICOM tags in the last PDV. This will ensure the proper reassembly. De-select, to troubleshoot PDU length issues, or to understand PDV fragmentation. When not set, the decoding may fail and the exports may become corrupt. # TRUE or FALSE (case-insensitive) #dicom.pdv_reassemble: TRUE # Whether the DISTCC dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #distcc.desegment_distcc_over_tcp: TRUE # Whether DJIUAV should reassemble messages spanning multiple TCP segments (required to get useful results) # TRUE or FALSE (case-insensitive) #djiuav.desegment: TRUE # Set the SCTP port for Distributed Lock Manager # A decimal number #dlm3.sctp.port: 21064 # Select the type of decoding for nationally-defined values # One of: None (raw data), As for regular, Thales XOmail # (case-insensitive). #dmp.national_decode: As for regular # Select the nation of sending server. This is used when presenting security classification values in messages with security policy set to National (nation of local server) # One of: None, Albania, Armenia, Austria, Azerbaijan, Belarus, Belgium, Bosnia and Hercegowina, Bulgaria, Canada, Croatia, Czech Republic, Denmark, Estonia, Euro-Atlantic Partnership Council (EAPC), European Union (EU), Finland, Former Yugoslav Republic of Macedonia, France, Georgia, Germany, Greece, Hungary, Iceland, International Security Assistance Force (ISAF), Ireland, Italy, Kazakhstan, Kyrgyztan, Latvia, Lithuania, Luxembourg, Malta, Moldova, Montenegro, Netherlands, Norway, Partnership for Peace (PfP), Poland, Portugal, Romania, Russian Federation, Serbia, Slovakia, Slovenia, Spain, Sweden, Switzerland, Tajikistan, Turkey, Turkmenistan, United Kingdom, United States, Ukraine, Uzbekistan, Western European Union (WEU) # (case-insensitive). #dmp.local_nation: None # Calculate sequence/acknowledgement analysis # TRUE or FALSE (case-insensitive) #dmp.seq_ack_analysis: TRUE # Align identifiers in info list (does not align when retransmission or duplicate acknowledgement indication) # TRUE or FALSE (case-insensitive) #dmp.align_ids: FALSE # The way DMX values are displayed # One of: Percent, Hexadecimal, Decimal # (case-insensitive). #dmx_chan.dmx_disp_chan_val_type: Percent # The way DMX channel numbers are displayed # One of: Hexadecimal, Decimal # (case-insensitive). #dmx_chan.dmx_disp_chan_nr_type: Hexadecimal # The number of columns for the DMX display # One of: 6, 10, 12, 16, 24 # (case-insensitive). #dmx_chan.dmx_disp_col_count: 16 # Whether the DNP3 dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #dnp3.desegment: TRUE # Whether the DNS dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #dns.desegment_dns_messages: TRUE # Number of seconds allowed between DNS requests with the same transaction ID to consider it a retransmission. Otherwise its considered a new request. # A decimal number #dns.retransmission_timer: 5 # Whether or not to validate the Header Check Sequence # TRUE or FALSE (case-insensitive) #docsis.check_fcs: TRUE # Whether or not to attempt to dissect encrypted DOCSIS payload # TRUE or FALSE (case-insensitive) #docsis.dissect_encrypted_frames: FALSE # Specifies that decryption should be attempted on all packets, even if the session initialization wasn't captured. # TRUE or FALSE (case-insensitive) #dof.custom_dof_decrypt_all: FALSE # Specifies that operations should be tracked across multiple packets, providing summary lists. This takes time and memory. # TRUE or FALSE (case-insensitive) #dof.custom_dof_track_operations: FALSE # Limits the number of operations shown before and after the current operations # A decimal number #dof.custom_dof_track_operations_window: 5 # Whether the DRDA dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #drda.desegment: TRUE # Whether the DSI dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #dsi.desegment: TRUE # redirect dtls debug to file name; leave empty to disable debug, use "-" to redirect output to stderr # # A path to a file #dtls.debug_file: # Pre-Shared-Key as HEX string. Should be 0 to 16 bytes. # A string #dtls.psk: # SAC Encryption Key (16 hex bytes) # A string #dvb-ci.sek: # SAC Init Vector (16 hex bytes) # A string #dvb-ci.siv: # Dissect the content of messages transmitted on the Low-Speed Communication resource. This requires a dissector for the protocol and target port contained in the connection descriptor. # TRUE or FALSE (case-insensitive) #dvb-ci.dissect_lsc_msg: FALSE # Check this to enable full protocol dissection of data above GSE Layer # TRUE or FALSE (case-insensitive) #dvb-s2_modeadapt.full_decode: FALSE # Allow only packets with Major=0x03//Minor=0xFF as DVMRP V3 packets # TRUE or FALSE (case-insensitive) #dvmrp.strict_v3: FALSE # Decode the Message Types according to eCPRI Specification V1.2 # TRUE or FALSE (case-insensitive) #ecpri.ecpripref.msg.decoding: TRUE # Whether the eDonkey dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #edonkey.desegment: TRUE # Whether the EtherNet/IP dissector should desegment all messages spanning multiple TCP segments # TRUE or FALSE (case-insensitive) #enip.desegment: TRUE # Determines whether all I/O connections will assume a 32-bit header in the O->T direction # TRUE or FALSE (case-insensitive) #enip.o2t_run_idle: TRUE # Determines whether all I/O connections will assume a 32-bit header in the T->O direction # TRUE or FALSE (case-insensitive) #enip.t2o_run_idle: FALSE # The way DMX values are displayed # One of: Percent, Hexadecimal, Decimal # (case-insensitive). #enttec.dmx_disp_chan_val_type: Percent # The way DMX channel numbers are displayed # One of: Hexadecimal, Decimal # (case-insensitive). #enttec.dmx_disp_chan_nr_type: Hexadecimal # The number of columns for the DMX display # One of: 6, 10, 12, 16, 24 # (case-insensitive). #enttec.dmx_disp_col_count: 16 # If you are capturing in networks with multiplexed or slow nodes, this can be useful # TRUE or FALSE (case-insensitive) #epl.show_soc_flags: FALSE # For analysis purposes one might want to show the command layer even if the dissectore assumes a duplicated frame # TRUE or FALSE (case-insensitive) #epl.show_duplicated_command_layer: FALSE # For analysis purposes one might want to see how long the current mapping has been active for and what OD write caused it # TRUE or FALSE (case-insensitive) #epl.show_pdo_meta_info: FALSE # Partition PDOs according to ObjectMappings sent via SDO # TRUE or FALSE (case-insensitive) #epl.use_sdo_mappings: TRUE # If you want to parse the defaultValue (XDD) and actualValue (XDC) attributes for ObjectMappings in order to detect default PDO mappings, which may not be sent over SDO # TRUE or FALSE (case-insensitive) #epl.use_xdc_mappings: TRUE # If a data field has untyped data under 8 byte long, interpret it as unsigned little endian integer and show decimal and hexadecimal representation thereof. Otherwise use stock data dissector # TRUE or FALSE (case-insensitive) #epl.interpret_untyped_as_le: TRUE # If you have a capture without IdentResponse and many nodes, it's easier to set a default profile here than to add entries for all MAC address or Node IDs # A path to a file #epl.default_profile: # Protocol encapsulated in HDLC records # One of: Cisco HDLC, PPP serial, Frame Relay, SS7 MTP2, Attempt to guess # (case-insensitive). #erf.hdlc_type: Attempt to guess # Whether raw ATM cells should be treated as the first cell of an AAL5 PDU # TRUE or FALSE (case-insensitive) #erf.rawcell_first: FALSE # Protocol encapsulated in ATM AAL5 packets # One of: Attempt to guess, LLC multiplexed, Unspecified # (case-insensitive). #erf.aal5_type: Attempt to guess # The packets contain the optional Incremental Redundancy (IR) fields # TRUE or FALSE (case-insensitive) #gsm_abis_pgsl.ir: FALSE # This is done only if the Decoding is not SET or the packet does not belong to a SA. Assumes a 12 byte auth (HMAC-SHA1-96/HMAC-MD5-96/AES-XCBC-MAC-96) and attempts decode based on the ethertype 13 bytes from packet end # TRUE or FALSE (case-insensitive) #esp.enable_null_encryption_decode_heuristic: FALSE # Check that successive frames increase sequence number by 1 within an SPI. This should work OK when only one host is sending frames on an SPI # TRUE or FALSE (case-insensitive) #esp.do_esp_sequence_analysis: TRUE # Attempt to decode based on the SAD described hereafter. # TRUE or FALSE (case-insensitive) #esp.enable_encryption_decode: FALSE # Attempt to Check ESP Authentication based on the SAD described hereafter. # TRUE or FALSE (case-insensitive) #esp.enable_authentication_check: FALSE # Whether the E-Tag summary line should be shown in the protocol tree # TRUE or FALSE (case-insensitive) #etag.summary_in_tree: TRUE # Place the hash/symbol files (generated by the Apache Etch compiler) ending with .ewh here # A path to a directory #etch.file: # Some devices add trailing data to frames. When this setting is checked the Ethernet dissector will assume there has been added padding to the frame before the trailer was added. Uncheck if a device added a trailer before the frame was padded. # TRUE or FALSE (case-insensitive) #eth.assume_padding: TRUE # Some TAPs add a fixed length ethernet trailer at the end of the frame, but before the (optional) FCS. Make sure it gets interpreted correctly. # A decimal number #eth.trailer_length: 0 # Some Ethernet adapters and drivers include the FCS at the end of a packet, others do not. The Ethernet dissector attempts to guess whether a captured packet has an FCS, but it cannot always guess correctly. # TRUE or FALSE (case-insensitive) #eth.assume_fcs: FALSE # Whether to validate the Frame Check Sequence # TRUE or FALSE (case-insensitive) #eth.check_fcs: FALSE # Whether packets should be interpreted as coming from CheckPoint FireWall-1 monitor file if they look as if they do # TRUE or FALSE (case-insensitive) #eth.interpret_as_fw1_monitor: FALSE # When capturing on a Cisco FEX some frames start with an extra destination mac # TRUE or FALSE (case-insensitive) #eth.deduplicate_dmac: FALSE # Set the condition that must be true for the CCSDS dissector to be called # TRUE or FALSE (case-insensitive) #eth.ccsds_heuristic_length: FALSE # Set the condition that must be true for the CCSDS dissector to be called # TRUE or FALSE (case-insensitive) #eth.ccsds_heuristic_version: FALSE # Set the condition that must be true for the CCSDS dissector to be called # TRUE or FALSE (case-insensitive) #eth.ccsds_heuristic_header: FALSE # Set the condition that must be true for the CCSDS dissector to be called # TRUE or FALSE (case-insensitive) #eth.ccsds_heuristic_bit: FALSE # Whether the EVRC dissector should process payload type 60 as legacy EVRC packets # TRUE or FALSE (case-insensitive) #evrc.legacy_pt_60: FALSE # The dynamic payload type which will be interpreted as EVS; The value must be greater than 95 # A decimal number #evs.dynamic.payload.type: 0 # Controls the display of the session's username in the info column. This is only displayed if the packet containing it was seen during this capture session. # TRUE or FALSE (case-insensitive) #exec.info_show_username: TRUE # Controls the display of the command being run on the server by this session in the info column. This is only displayed if the packet containing it was seen during this capture session. # TRUE or FALSE (case-insensitive) #exec.info_show_command: FALSE # Disable this if you do not want this dissector to populate well-known fields in other dissectors (i.e. ip.addr, ipv6.addr, tcp.port and udp.port). Enabling this will allow filters that reference those fields to also find data in the trailers but will reduce performance. After disabling, you should restart Wireshark to get performance back. # TRUE or FALSE (case-insensitive) #f5ethtrailer.pop_other_fields: FALSE # Enabling this will perform analysis of the trailer data. It will enable taps on other protocols and slow down Wireshark. # TRUE or FALSE (case-insensitive) #f5ethtrailer.perform_analysis: TRUE # In/out only removes slot/tmm information. Brief shortens the string to >S/T (for in) or <" ('>' for in and '<' for out). If this is not set or is less than two characters, the default is used. If it is longer than two characters, the extra characters are ignored. # A string #f5ethtrailer.brief_inout_chars: # If the platform in the F5 FILEINFO packet matches the provided regex, slot information will be displayed in the info column; otherwise, it will not. A reasonable value is "^(A.*|Z101)$". If the regex is empty or there is no platform information in the capture, slot information is always displayed. # A string #f5ethtrailer.slots_regex: # If present, include the RST cause text from the trailer in the "info" column of the packet list pane. # TRUE or FALSE (case-insensitive) #f5ethtrailer.rstcause_in_info: TRUE # If enabled, KEYLOG entires will be added to the TLS decode in the f5ethtrailer protocol tree. It will populate the f5ethtrailer.tls.keylog field. # TRUE or FALSE (case-insensitive) #f5ethtrailer.generate_keylog: TRUE # If enabled, reassembly of multi-frame sequences is done # TRUE or FALSE (case-insensitive) #fc.reassemble: TRUE # This is the size of non-last frames in a multi-frame sequence # A decimal number #fc.max_frame_size: 1024 # Whether the FDDI dissector should add 3-byte padding to all captured FDDI packets (useful with e.g. Tru64 UNIX tcpdump) # TRUE or FALSE (case-insensitive) #fddi.padding: FALSE # Whether the FCIP dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #fcip.desegment: TRUE # Port number used for FCIP # A decimal number #fcip.target_port: 3225 # Dissect next layer # TRUE or FALSE (case-insensitive) #file-pcap.dissect_next_layer: FALSE # Dissect next layer # TRUE or FALSE (case-insensitive) #file-pcapng.dissect_next_layer: FALSE # Whether the FIX dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #fix.desegment: TRUE # Try to decode a packet using an heuristic sub-dissector before using a sub-dissector registered to "decode as" # TRUE or FALSE (case-insensitive) #flexray.try_heuristic_first: FALSE # With this option display filters for fmp fhandle a RPC call, even if the actual fhandle is only present in one of the packets # TRUE or FALSE (case-insensitive) #fmp.fhandle_find_both_reqrep: FALSE # Decode packets on this sctp port as ForCES # A decimal number #forces.sctp_high_prio_port: 0 # Decode packets on this sctp port as ForCES # A decimal number #forces.sctp_med_prio_port: 0 # Decode packets on this sctp port as ForCES # A decimal number #forces.sctp_low_prio_port: 0 # Show reported release info # TRUE or FALSE (case-insensitive) #fp.show_release_info: TRUE # Call MAC dissector for payloads # TRUE or FALSE (case-insensitive) #fp.call_mac: TRUE # Validate FP payload checksums # TRUE or FALSE (case-insensitive) #fp.payload_checksum: TRUE # Validate FP header checksums # TRUE or FALSE (case-insensitive) #fp.header_checksum: TRUE # For each PCH data frame, Try to show the paging indications bitmap found in the previous frame # TRUE or FALSE (case-insensitive) #fp.track_paging_indications: TRUE # Whether the UID value should be appended in the protocol tree # TRUE or FALSE (case-insensitive) #fp_mux.uid_in_tree: TRUE # Whether to try heuristic FP dissectors for the muxed payloads # TRUE or FALSE (case-insensitive) #fp_mux.call_heur_fp: TRUE # Encapsulation # One of: FRF 3.2/Cisco HDLC, GPRS Network Service, Raw Ethernet, LAPB (T1.617a-1994 Annex G) # (case-insensitive). #fr.encap: FRF 3.2/Cisco HDLC # Show offset of frame in capture file # TRUE or FALSE (case-insensitive) #frame.show_file_off: FALSE # Treat all frames as DOCSIS Frames # TRUE or FALSE (case-insensitive) #frame.force_docsis_encap: FALSE # Whether or not MD5 hashes should be generated for each frame, useful for finding duplicate frames. # TRUE or FALSE (case-insensitive) #frame.generate_md5_hash: FALSE # Whether or not an Epoch time entry should be generated for each frame. # TRUE or FALSE (case-insensitive) #frame.generate_epoch_time: TRUE # Whether or not the number of bits in the frame should be shown. # TRUE or FALSE (case-insensitive) #frame.generate_bits_field: TRUE # Whether or not 'packet size limited during capture' message in shown in Info column. # TRUE or FALSE (case-insensitive) #frame.disable_packet_size_limited_in_summary: FALSE # Whether the FireWall-1 summary line should be shown in the protocol tree # TRUE or FALSE (case-insensitive) #fw1.summary_in_tree: TRUE # Whether the Firewall-1 monitor file includes UUID information # TRUE or FALSE (case-insensitive) #fw1.with_uuid: FALSE # Whether the interface list includes the chain position # TRUE or FALSE (case-insensitive) #fw1.iflist_with_chain: FALSE # Whether the Gadu-Gadu dissector should reassemble messages spanning multiple TCP segments.To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #gadu-gadu.desegment: TRUE # Whether the Gearman dissector should desegment all messages spanning multiple TCP segments # TRUE or FALSE (case-insensitive) #gearman.desegment: TRUE # Whether the GED125 dissector should desegment all messages spanning multiple TCP segments # TRUE or FALSE (case-insensitive) #ged125.desegment_body: TRUE # Whether the GIOP dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #giop.desegment_giop_messages: TRUE # Whether fragmented GIOP messages should be reassembled # TRUE or FALSE (case-insensitive) #giop.reassemble: TRUE # Maximum allowed message size in bytes (default=10485760) # A decimal number #giop.max_message_size: 10485760 # File containing stringified IORs, one per line. # A path to a file #giop.ior_txt: IOR.txt # Whether the GIT dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #git.desegment: TRUE # Whether the Gigamon header summary line should be shown in the protocol tree # TRUE or FALSE (case-insensitive) #gmhdr.summary_in_tree: TRUE # Whether the Gigamon Trailer summary line should be shown in the protocol tree # TRUE or FALSE (case-insensitive) #gmtrailer.summary_in_tree: TRUE # Whether the Gigamon trailer containing HW timestamp, source id and original CRC should be decoded # TRUE or FALSE (case-insensitive) #gmtrailer.decode_trailer_timestamp: TRUE # Make the GeoNetworking dissector analyze GeoNetworking sequence numbers to find and flag duplicate packet (Annex A) # TRUE or FALSE (case-insensitive) #gnw.analyze_sequence_numbers: TRUE # Whether to autodetect the cipher bit (because it might be set on unciphered data) # TRUE or FALSE (case-insensitive) #llcgprs.autodetect_cipher_bit: FALSE # Help for debug... # TRUE or FALSE (case-insensitive) #gquic.debug.quic: FALSE # Normally application/grpc message is protobuf, but sometime the true message is json. If this option in on, we always check whether the message is JSON (body starts with '{' and ends with '}') regardless of grpc_message_type_subdissector_table settings (which dissect grpc message according to content-type). # TRUE or FALSE (case-insensitive) #grpc.detect_json_automaticlly: TRUE # If turned on, http2 will reassemble gRPC message as soon as possible. Or else the gRPC message will be reassembled at the end of each HTTP2 STREAM. If your .proto files contains streaming RPCs (declaring RPC operation input/output message type with 'stream' label), you need to keep this option on. # TRUE or FALSE (case-insensitive) #grpc.streaming_reassembly_mode: TRUE # Embed gRPC messages under HTTP2 protocol tree items. # TRUE or FALSE (case-insensitive) #grpc.embeded_under_http2: FALSE # Whether the Gryphon dissector should desegment all messages spanning multiple TCP segments # TRUE or FALSE (case-insensitive) #gryphon.desegment: TRUE # No description # TRUE or FALSE (case-insensitive) #gsm_ipa.hsl_debug_in_root_tree: FALSE # No description # TRUE or FALSE (case-insensitive) #gsm_ipa.hsl_debug_in_info: FALSE # Whether the dissector should reassemble SMS spanning multiple packets # TRUE or FALSE (case-insensitive) #gsm_sms.reassemble: TRUE # Whether the dissector should take into account info coming from lower layers (like GSM-MAP) to perform SMS reassembly # TRUE or FALSE (case-insensitive) #gsm_sms.reassemble_with_lower_layers_info: TRUE # Always decode a GSM Short Message as Connectionless WSP if a Port Number Information Element is present in the SMS User Data Header. # TRUE or FALSE (case-insensitive) #gsm_sms_ud.port_number_udh_means_wsp: FALSE # Always try subdissection of the 1st fragment of a fragmented GSM Short Message. If reassembly is possible, the Short Message may be dissected twice (once as a short frame, once in its entirety). # TRUE or FALSE (case-insensitive) #gsm_sms_ud.try_dissect_1st_fragment: FALSE # Prevent sub-dissectors from replacing column data with their own. Eg. Prevent WSP dissector overwriting SMPP information. # TRUE or FALSE (case-insensitive) #gsm_sms_ud.prevent_dissectors_chg_cols: FALSE # Treat ARFCN 512-810 as DCS 1800 rather than PCS 1900 # TRUE or FALSE (case-insensitive) #gsm_um.dcs1800: TRUE # Dissect Q.931 User-To-User information # TRUE or FALSE (case-insensitive) #gsm-r-uus1.dissect_q931_u2u: FALSE # Dissect GSM-A User-To-User information # TRUE or FALSE (case-insensitive) #gsm-r-uus1.dissect_gsm_a_u2u: TRUE # TCAP Subsystem numbers used for GSM MAP # A string denoting an positive integer range (e.g., "1-20,30-40") #gsm_map.tcap.ssn: 6-9,145,148-150 # How to treat Application context # One of: Use application context from the trace, Treat as AC 1, Treat as AC 2, Treat as AC 3 # (case-insensitive). #gsm_map.application.context.version: Use application context from the trace # When enabled, dissector will use the non 3GPP standard extensions from Ericsson (that can override the standard ones) # TRUE or FALSE (case-insensitive) #gsm_map.ericsson.proprietary.extensions: FALSE # Whether or not to try reassembling GSSAPI blobs spanning multiple (SMB/SessionSetup) PDUs # TRUE or FALSE (case-insensitive) #gss-api.gssapi_reassembly: TRUE # Show GSUP Source/Destination names as text in the Packet Details pane # TRUE or FALSE (case-insensitive) #gsup.show_name_as_text: TRUE # GTPv0 and GTP' port (default 3386) # A decimal number #gtp.v0_port: 3386 # GTPv1 and GTPv2 control plane port (default 2123) # A decimal number #gtp.v1c_port: 2123 # GTPv1 user plane port (default 2152) # A decimal number #gtp.v1u_port: 2152 # Dissect T-PDU as # One of: None, TPDU Heuristic, PDCP-LTE, PDCP-NR, SYNC # (case-insensitive). #gtp.dissect_tpdu_as: TPDU Heuristic # Request/reply pair matches only if their timestamps are closer than that value, in ms (default 0, i.e. don't use timestamps) # A decimal number #gtp.pair_max_interval: 0 # GTP ETSI order # TRUE or FALSE (case-insensitive) #gtp.check_etsi: FALSE # Dissect GTP over TCP # TRUE or FALSE (case-insensitive) #gtp.dissect_gtp_over_tcp: TRUE # Track GTP session # TRUE or FALSE (case-insensitive) #gtp.track_gtp_session: FALSE # Use this setting to decode the Transparent Containers in the SRVCC PS-to-CS messages. # This is needed until there's a reliable way to determine the contents of the transparent containers. # One of: Don't decode, Assume UTRAN target # (case-insensitive). #gtpv2.decode_srvcc_p2c_trans_cont_target: Don't decode # Request/reply pair matches only if their timestamps are closer than that value, in ms (default 0, i.e. don't use timestamps) # A decimal number #gtpv2.pair_max_interval: 0 # H.225 Server TLS Port # A decimal number #h225.tls.port: 1300 # Whether the H.225 dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #h225.reassembly: TRUE # ON - display tunnelled H.245 inside H.225.0 tree, OFF - display tunnelled H.245 in root tree after H.225.0 # TRUE or FALSE (case-insensitive) #h225.h245_in_tree: TRUE # ON - display tunnelled protocols inside H.225.0 tree, OFF - display tunnelled protocols in root tree after H.225.0 # TRUE or FALSE (case-insensitive) #h225.tp_in_tree: TRUE # Whether the H.245 dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #h245.reassembly: TRUE # Whether the dissector should show short names or the long names from the standard # TRUE or FALSE (case-insensitive) #h245.shorttypes: FALSE # Whether the dissector should print items of h245 Info column in reversed order # TRUE or FALSE (case-insensitive) #h245.prepand: FALSE # Desegment H.501 messages that span more TCP segments # TRUE or FALSE (case-insensitive) #h501.desegment: TRUE # Maintain relationships between transactions and contexts and display an extra tree showing context data # TRUE or FALSE (case-insensitive) #h248.ctx_info: FALSE # Desegment H.248 messages that span more TCP segments # TRUE or FALSE (case-insensitive) #h248.desegment: TRUE # The dynamic payload type which will be interpreted as H264; The value must be greater than 95 # A decimal number #h263p.dynamic.payload.type: 0 # Dynamic payload types which will be interpreted as H264; Values must be in the range 96 - 127 # A string denoting an positive integer range (e.g., "1-20,30-40") #h264.dynamic.payload.type: # Dynamic payload types which will be interpreted as H265; Values must be in the range 96 - 127 # A string denoting an positive integer range (e.g., "1-20,30-40") #h265.dynamic.payload.type: # Whether the HART-IP dissector should desegment all messages spanning multiple TCP segments # TRUE or FALSE (case-insensitive) #hart_ip.desegment: TRUE # Whether the hazel dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #hzlcst.desegment: TRUE # The ethernet type used for L2 communications # A decimal number #hcrt.dissector_ethertype: 61522 # Specifies that the raw text of the HL7 message should be displayed in addition to the dissection tree # TRUE or FALSE (case-insensitive) #hl7.display_raw: FALSE # Specifies that the LLP session information should be displayed (Start/End Of Block) in addition to the dissection tree # TRUE or FALSE (case-insensitive) #hl7.display_llp: FALSE # Set the port for HNBAP messages (Default of 29169) # A decimal number #hnbap.port: 29169 # Whether the HPFEEDS dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #hpfeeds.desegment_hpfeeds_messages: TRUE # Try to decode the payload using an heuristic sub-dissector # TRUE or FALSE (case-insensitive) #hpfeeds.try_heuristic: TRUE # Whether the HTTP dissector should reassemble headers of a request spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #http.desegment_headers: TRUE # Whether the HTTP dissector should use the "Content-length:" value, if present, to reassemble the body of a request spanning multiple TCP segments, and reassemble chunked data spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #http.desegment_body: TRUE # Whether to reassemble bodies of entities that are transferred using the "Transfer-Encoding: chunked" method # TRUE or FALSE (case-insensitive) #http.dechunk_body: TRUE # Whether to uncompress entity bodies that are compressed using "Content-Encoding: " # TRUE or FALSE (case-insensitive) #http.decompress_body: TRUE # SCTP Ports range # A string denoting an positive integer range (e.g., "1-20,30-40") #http.sctp.port: 80 # SSL/TLS Ports range # A string denoting an positive integer range (e.g., "1-20,30-40") #http.tls.port: 443 # The UDP port for RROCE messages (default 4791) # A decimal number #infiniband.rroce.port: 4791 # Try to decode a packet using an heuristic sub-dissector before using Decode As # TRUE or FALSE (case-insensitive) #infiniband.try_heuristic_first: TRUE # Maximum number of batch requests allowed # A decimal number #icep.max_batch_requests: 64 # Maximum length allowed of an ICEP string # A decimal number #icep.max_ice_string_len: 512 # Maximum number of context pairs allowed # A decimal number #icep.max_ice_context_pairs: 64 # Whether the 128th and following bytes of the ICMP payload should be decoded as MPLS extensions or as a portion of the original packet # TRUE or FALSE (case-insensitive) #icmp.favor_icmp_mpls: FALSE # Length of the Cause of Transmission Field, configurable in '101 and fixed at 2 octets with '104 # One of: 1 octet, 2 octet # (case-insensitive). #iec60870_101.cot_len: 1 octet # Length of the Common ASDU Address Field, configurable in '101 and fixed at 2 octets with '104 # One of: 1 octet, 2 octet # (case-insensitive). #iec60870_101.asdu_addr_len: 1 octet # Length of the Information Object Address Field, configurable in '101 and fixed at 3 octets with '104 # One of: 2 octet, 3 octet # (case-insensitive). #iec60870_101.asdu_ioa_len: 2 octet # Whether fragmented 802.11 datagrams should be reassembled # TRUE or FALSE (case-insensitive) #wlan.defragment: TRUE # Don't dissect 802.11n draft HT elements (which might contain duplicate information). # TRUE or FALSE (case-insensitive) #wlan.ignore_draft_ht: FALSE # Whether retransmitted 802.11 frames should be subdissected # TRUE or FALSE (case-insensitive) #wlan.retransmitted: TRUE # Some 802.11 cards include the FCS at the end of a packet, others do not. # TRUE or FALSE (case-insensitive) #wlan.check_fcs: FALSE # Whether to validate the FCS checksum or not. # TRUE or FALSE (case-insensitive) #wlan.check_checksum: FALSE # Some 802.11 cards leave the Protection bit set even though the packet is decrypted, and some also leave the IV (initialization vector). # One of: No, Yes - without IV, Yes - with IV # (case-insensitive). #wlan.ignore_wep: No # Whether to enable MIC Length override or not. # TRUE or FALSE (case-insensitive) #wlan.wpa_key_mic_len_enable: FALSE # Some Key MIC lengths are greater than 16 bytes, so set the length you require # A decimal number #wlan.wpa_key_mic_len: 0 # Enable WEP and WPA/WPA2 decryption # TRUE or FALSE (case-insensitive) #wlan.enable_decryption: TRUE # (Hexadecimal) Ethertype used to indicate IEEE 802.15.4 frame. # A hexadecimal number #wpan.802154_ethertype: 0x809a # The FCS format in the captured payload # One of: TI CC24xx metadata, ITU-T CRC-16, ITU-T CRC-32 # (case-insensitive). #wpan.fcs_format: ITU-T CRC-16 # Dissect payload only if FCS is valid. # TRUE or FALSE (case-insensitive) #wpan.802154_fcs_ok: TRUE # Match frames with ACK request to ACK packets # TRUE or FALSE (case-insensitive) #wpan.802154_ack_tracking: FALSE # Parse assuming 802.15.4e quirks for compatibility # TRUE or FALSE (case-insensitive) #wpan.802154e_compatibility: FALSE # Specifies the security suite to use for 802.15.4-2003 secured frames (only supported suites are listed). Option ignored for 802.15.4-2006 and unsecured frames. # One of: AES-128 Encryption, 128-bit Integrity Protection, AES-128 Encryption, 64-bit Integrity Protection, AES-128 Encryption, 32-bit Integrity Protection # (case-insensitive). #wpan.802154_sec_suite: AES-128 Encryption, 64-bit Integrity Protection # Set if the manufacturer extends the authentication data with the security header. Option ignored for 802.15.4-2006 and unsecured frames. # TRUE or FALSE (case-insensitive) #wpan.802154_extend_auth: TRUE # (Hexadecimal) Ethertype used to indicate IEEE 802.1ah tag. # A hexadecimal number #ieee8021ah.8021ah_ethertype: 0x88e7 # Whether the iFCP dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #ifcp.desegment: TRUE # Whether the ILP dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #ilp.desegment_ilp_messages: TRUE # Whether to use heuristics for post-STARTTLS detection of encrypted IMAP conversations # TRUE or FALSE (case-insensitive) #imap.ssl_heuristic: TRUE # TCAP Subsystem numbers used for INAP # A string denoting an positive integer range (e.g., "1-20,30-40") #inap.ssn: 106,241 # Whether the IPDC dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #ipdc.desegment_ipdc_messages: TRUE # Range of session IDs to be decoded as SAMIS-TYPE-1 records # A string denoting an positive integer range (e.g., "1-20,30-40") #ipdr.sessions.samis_type_1: # Dissect IPMB commands # TRUE or FALSE (case-insensitive) #ipmi.dissect_bus_commands: FALSE # FRU Language Code is English; strings are ASCII+LATIN1 (vs. Unicode) # TRUE or FALSE (case-insensitive) #ipmi.fru_langcode_is_english: TRUE # Do not search for responses coming after this timeout (milliseconds) # A decimal number #ipmi.response_after_req: 5000 # Allow for responses before requests (milliseconds) # A decimal number #ipmi.response_before_req: 0 # Format of messages embedded into Send/Get/Forward Message # One of: None, IPMB, Session-based (LAN, ...), Use heuristics # (case-insensitive). #ipmi.msgfmt: Use heuristics # Selects which OEM format is used for commands that IPMI does not define # One of: None, Pigeon Point Systems # (case-insensitive). #ipmi.selected_oem: None # Whether the IPv4 type-of-service field should be decoded as a Differentiated Services field (see RFC2474/RFC2475) # TRUE or FALSE (case-insensitive) #ip.decode_tos_as_diffserv: TRUE # Whether fragmented IPv4 datagrams should be reassembled # TRUE or FALSE (case-insensitive) #ip.defragment: TRUE # Whether the IPv4 summary line should be shown in the protocol tree # TRUE or FALSE (case-insensitive) #ip.summary_in_tree: TRUE # Whether to validate the IPv4 checksum # TRUE or FALSE (case-insensitive) #ip.check_checksum: FALSE # Whether to correct for TSO-enabled (TCP segmentation offload) hardware captures, such as spoofing the IP packet length # TRUE or FALSE (case-insensitive) #ip.tso_support: TRUE # Whether to look up IP addresses in each MaxMind database we have loaded # TRUE or FALSE (case-insensitive) #ip.use_geoip: TRUE # Whether to interpret the originally reserved flag as security flag # TRUE or FALSE (case-insensitive) #ip.security_flag: FALSE # Try to decode a packet using an heuristic sub-dissector before using a sub-dissector registered to a specific port # TRUE or FALSE (case-insensitive) #ip.try_heuristic_first: FALSE # Whether fragmented IPv6 datagrams should be reassembled # TRUE or FALSE (case-insensitive) #ipv6.defragment: TRUE # Whether the IPv6 summary line should be shown in the protocol tree # TRUE or FALSE (case-insensitive) #ipv6.summary_in_tree: TRUE # Whether to look up IPv6 addresses in each MaxMind database we have loaded # TRUE or FALSE (case-insensitive) #ipv6.use_geoip: TRUE # Check that all RPL Source Routed packets conform to RFC 6554 and do not visit a node more than once # TRUE or FALSE (case-insensitive) #ipv6.perform_strict_rpl_srh_rfc_checking: FALSE # Try to decode a packet using an heuristic sub-dissector before using a sub-dissector registered to a specific port # TRUE or FALSE (case-insensitive) #ipv6.try_heuristic_first: FALSE # Whether to display IPv6 extension headers as a separate protocol or a sub-protocol of the IPv6 packet # TRUE or FALSE (case-insensitive) #ipv6.exthdr_under_root_protocol_tree: FALSE # If enabled the Length field in octets will be hidden # TRUE or FALSE (case-insensitive) #ipv6.exthdr_hide_len_oct_field: FALSE # Whether to correct for TSO-enabled (TCP segmentation offload) hardware captures, such as spoofing the IPv6 packet length # TRUE or FALSE (case-insensitive) #ipv6.tso_support: FALSE # The iSCSI protocol version # One of: Draft 08, Draft 09, Draft 11, Draft 12, Draft 13 # (case-insensitive). #iscsi.protocol_version: Draft 13 # Whether the iSCSI dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #iscsi.desegment_iscsi_messages: TRUE # When enabled, packets that appear bogus are ignored # TRUE or FALSE (case-insensitive) #iscsi.bogus_pdu_filter: TRUE # Ignore packets that haven't set the F bit when they should have # TRUE or FALSE (case-insensitive) #iscsi.demand_good_f_bit: FALSE # Treat packets whose data segment length is greater than this value as bogus # A decimal number #iscsi.bogus_pdu_max_data_len: 262144 # Range of iSCSI target ports(default 3260) # A string denoting an positive integer range (e.g., "1-20,30-40") #iscsi.target_ports: 3260 # System port number of iSCSI target # A decimal number #iscsi.target_system_port: 860 # The protocol running on the D channel # One of: LAPD, DPNSS # (case-insensitive). #isdn.dchannel_protocol: LAPD # Range of iSER target ports(default 3260) # A string denoting an positive integer range (e.g., "1-20,30-40") #iser.target_ports: 3260 # The dynamic payload type which will be interpreted as ISMACryp # A decimal number #ismacryp.dynamic.payload.type: 0 # ISMACryp version # One of: ISMACryp v1.1, ISMACryp v2.0 # (case-insensitive). #ismacryp.version: ISMACryp v1.1 # Indicates whether or not the ISMACryp version deduced from RTP payload type, if present, is used or whether the version above is used # TRUE or FALSE (case-insensitive) #ismacryp.override_rtp_pt: FALSE # Set the length of the IV in the ISMACryp AU Header in bytes # A decimal number #ismacryp.iv_length: 4 # Set the length of the Delta IV in the ISMACryp AU Header in bytes # A decimal number #ismacryp.delta_iv_length: 0 # Set the length of the Key Indicator in the ISMACryp AU Header in bytes # A decimal number #ismacryp.key_indicator_length: 0 # Indicates whether or not the Key Indicator is present in all AU Headers (T/F) # TRUE or FALSE (case-insensitive) #ismacryp.key_indicator_per_au_flag: FALSE # Indicates whether or not selective encryption is enabled (T/F) # TRUE or FALSE (case-insensitive) #ismacryp.selective_encryption: TRUE # Indicates whether or not slice start / end is present (T/F) # TRUE or FALSE (case-insensitive) #ismacryp.slice_indication: FALSE # Indicates whether or not padding information is present (T/F) # TRUE or FALSE (case-insensitive) #ismacryp.padding_indication: FALSE # RFC3640 mode # One of: aac-hbr, mpeg4-video, avc-video # (case-insensitive). #ismacryp.rfc3640_mode: avc-video # Indicates use of user mode instead of RFC3640 modes (T/F) # TRUE or FALSE (case-insensitive) #ismacryp.user_mode: FALSE # Set the length of the AU size in the AU Header in bits # A decimal number #ismacryp.au_size_length: 0 # Set the length of the AU index in the AU Header in bits # A decimal number #ismacryp.au_index_length: 0 # Set the length of the AU delta index in the AU Header in bits # A decimal number #ismacryp.au_index_delta_length: 0 # Set the length of the CTS delta field in the AU Header in bits # A decimal number #ismacryp.cts_delta_length: 0 # Set the length of the DTS delta field in the AU Header in bits # A decimal number #ismacryp.dts_delta_length: 0 # Indicates whether or not the RAP field is present in the AU Header (T/F) # TRUE or FALSE (case-insensitive) #ismacryp.random_access_indication: FALSE # Indicates the number of bits on which the stream state field is encoded in the AU Header (bits) # A decimal number #ismacryp.stream_state_indication: 0 # Whether the iSNS dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #isns.desegment: TRUE # Addressing of ISO 15765. Normal or Extended # One of: Normal addressing, Extended addressing # (case-insensitive). #iso15765.addressing: Normal addressing # Window of ISO 15765 fragments # A decimal number #iso15765.window: 8 # Endian of the length field. Big endian or Little endian # One of: Big endian, Little endian # (case-insensitive). #iso8583.len_endian: Little endian # charset for numbers # One of: Digits represented as ASCII Characters, Digits represented in nibbles # (case-insensitive). #iso8583.charset: Digits represented as ASCII Characters # binary data representation # One of: Bin data represented as Hex Ascii characters, Bin data not encoded # (case-insensitive). #iso8583.binencode: Bin data represented as Hex Ascii characters # File containing a translation from object ID to string # A path to a file #isobus.vt.object_ids: # Note national variants may not be fully supported # One of: ITU Standard, French National Standard, Israeli National Standard, Russian National Standard, Japan National Standard, Japan National Standard (TTC) # (case-insensitive). #isup.variant: ITU Standard # Show the CIC value (in addition to the message type) in the Info column # TRUE or FALSE (case-insensitive) #isup.show_cic_in_info: TRUE # Whether APM messages datagrams should be reassembled # TRUE or FALSE (case-insensitive) #isup.defragment_apm: TRUE # The MPLS label (aka Flow Bundle ID) used by ITDM traffic. # A hexadecimal number #itdm.mpls_label: 0x99887 # Flow Number used by I-TDM Control Protocol traffic. # A decimal number #itdm.ctl_flowno: 0 # Support Implementers Guide (version 01) # TRUE or FALSE (case-insensitive) #iua.support_ig: FALSE # Use SAPI values as specified in TS 48 056 # TRUE or FALSE (case-insensitive) #iua.use_gsm_sapi_values: TRUE # Whether IuUP Payload bits should be dissected # TRUE or FALSE (case-insensitive) #iuup.dissect_payload: FALSE # The payload contains a two byte pseudoheader indicating direction and circuit_id # TRUE or FALSE (case-insensitive) #iuup.two_byte_pseudoheader: FALSE # The dynamic payload type which will be interpreted as IuUP # A decimal number #iuup.dynamic.payload.type: 0 # Whether the trailer summary line should be shown in the protocol tree # TRUE or FALSE (case-insensitive) #ixiatrailer.summary_in_tree: TRUE # Display JSON like in browsers devtool # TRUE or FALSE (case-insensitive) #json.compact_form: FALSE # Enable to have correctly typed MIME media dissected as JXTA Messages. # TRUE or FALSE (case-insensitive) #jxta.msg.mediatype: TRUE # Whether the JXTA dissector should reassemble messages spanning multiple UDP/TCP/SCTP segments. To use this option you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings and enable "Reassemble fragmented IP datagrams" in the IP protocol settings. # TRUE or FALSE (case-insensitive) #jxta.desegment: TRUE # No description # TRUE or FALSE (case-insensitive) #kafka.show_string_bytes_lengths: FALSE # Set the SCTP port for kNet messages # A decimal number #knet.sctp.port: 2345 # Keyring.XML file (exported from ETS) # A path to a file #kip.key_file: # Keyring password # A string #kip.key_file_pwd: # Output file (- for stdout) for keys extracted from key file # A path to a file #kip.key_info_file: # KNX decryption key (format: 16 bytes as hex; example: A0 A1 A2 A3 A4 A5 A6 A7 A8 A9 AA AB AC AD AE AF) # A string #kip.key_1: # KNX decryption key (format: 16 bytes as hex; example: A0 A1 A2 A3 A4 A5 A6 A7 A8 A9 AA AB AC AD AE AF) # A string #kip.key_2: # KNX decryption key (format: 16 bytes as hex; example: A0 A1 A2 A3 A4 A5 A6 A7 A8 A9 AA AB AC AD AE AF) # A string #kip.key_3: # KNX decryption key (format: 16 bytes as hex; example: A0 A1 A2 A3 A4 A5 A6 A7 A8 A9 AA AB AC AD AE AF) # A string #kip.key_4: # KNX decryption key (format: 16 bytes as hex; example: A0 A1 A2 A3 A4 A5 A6 A7 A8 A9 AA AB AC AD AE AF) # A string #kip.key_5: # KNX decryption key (format: 16 bytes as hex; example: A0 A1 A2 A3 A4 A5 A6 A7 A8 A9 AA AB AC AD AE AF) # A string #kip.key_6: # KNX decryption key (format: 16 bytes as hex; example: A0 A1 A2 A3 A4 A5 A6 A7 A8 A9 AA AB AC AD AE AF) # A string #kip.key_7: # KNX decryption key (format: 16 bytes as hex; example: A0 A1 A2 A3 A4 A5 A6 A7 A8 A9 AA AB AC AD AE AF) # A string #kip.key_8: # KNX decryption key (format: 16 bytes as hex; example: A0 A1 A2 A3 A4 A5 A6 A7 A8 A9 AA AB AC AD AE AF) # A string #kip.key_9: # KNX decryption key (format: 16 bytes as hex; example: A0 A1 A2 A3 A4 A5 A6 A7 A8 A9 AA AB AC AD AE AF) # A string #kip.key_10: # Whether the Kpasswd dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #kpasswd.desegment: TRUE # Whether the Kerberos dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #kerberos.desegment: TRUE # Whether the dissector should try to decrypt encrypted Kerberos blobs. This requires that the proper keytab file is installed as well. # TRUE or FALSE (case-insensitive) #kerberos.decrypt: FALSE # The keytab file containing all the secrets # A path to a file #kerberos.file: # KT allows binary values in keys and values. Attempt to show an ASCII representation anyway (which might be prematurely terminated by a NULL! # TRUE or FALSE (case-insensitive) #kt.present_key_val_as_ascii: FALSE # Whether the L&G 8979 dissector should desegment all messages spanning multiple TCP segments # TRUE or FALSE (case-insensitive) #lg8979.desegment: TRUE # L2TPv3 Cookie Size # One of: Detect, None, 4 Byte Cookie, 8 Byte Cookie # (case-insensitive). #l2tp.cookie_size: Detect # L2TPv3 L2-Specific Sublayer # One of: Detect, None, Default L2-Specific, ATM-Specific, LAPD-Specific, DOCSIS DMPT-Specific # (case-insensitive). #l2tp.l2_specific: Detect # Shared secret used for control message digest authentication # A string #l2tp.shared_secret: # Use SAPI values as specified in TS 48 056 # TRUE or FALSE (case-insensitive) #lapd.use_gsm_sapi_values: FALSE # RTP payload type for embedded LAPD. It must be one of the dynamic types from 96 to 127. Set it to 0 to disable. # A decimal number #lapd.rtp_payload_type: 0 # SCTP Payload Protocol Identifier for LAPD. It is a 32 bits value from 0 to 4294967295. Set it to 0 to disable. # A decimal number #lapd.sctp_payload_protocol_identifier: 0 # Whether the dissector should defragment LAPDm messages spanning multiple packets. # TRUE or FALSE (case-insensitive) #lapdm.reassemble: TRUE # Whether the Laplink dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #laplink.desegment_laplink_over_tcp: TRUE # Set the SCTP port for LCSAP messages # A decimal number #lcsap.sctp.port: 9082 # Whether the LDAP dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #ldap.desegment_ldap_messages: TRUE # Set the port for LDAP operations over TLS # A decimal number #ldap.tls.port: 636 # Whether the LDP dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #ldp.desegment_ldp_messages: TRUE # Which Information will be showed at Column Information is decided by the selection # One of: Default Column Info, PROFINET Special Column Info # (case-insensitive). #lldp.column_info_selection: Default Column Info # Dissect this ethertype as LLT traffic in addition to the default, 0xCAFE. # A hexadecimal number #llt.alternate_ethertype: 0 # Whether LMP contains a checksum which can be checked # TRUE or FALSE (case-insensitive) #lmp.checksum: FALSE # There might be plugins corresponding to different version of the specification If they are present they should be listed here. # One of: FD1, Rel8 dec 2008 # (case-insensitive). #log3gpp.rrc_release_version: Rel8 dec 2008 # There might be plugins corresponding to different version of the specification If they are present they should be listed here. # One of: FD1, Rel8 dec 2008 # (case-insensitive). #log3gpp.nas_eps_release_version: Rel8 dec 2008 # Use oneline info column by replace all new line characters by spaces # TRUE or FALSE (case-insensitive) #logcat.oneline_info_column: TRUE # Swap frame control bytes (needed for some APs # TRUE or FALSE (case-insensitive) #lwapp.swap_fc: FALSE # Whether to validate the LWL4 crc when crc bit is not set # TRUE or FALSE (case-insensitive) #lwl4.check_crc: TRUE # 128-bit decryption key in hexadecimal format # A string #lwm.lwmes_key: # Version used by Wireshark # One of: Internet Draft version 2, Internet Draft version 8, RFC 4165 # (case-insensitive). #m2pa.version: RFC 4165 # Set the port for M2PA messages (default: 3565) # A decimal number #m2pa.port: 3565 # The value of the parameter tag for protocol data 1 # One of: 0x000e (Draft 7), 0x0300 (RFC3331) # (case-insensitive). #m2ua.protocol_data_1_tag: 0x0300 (RFC3331) # Version used by Wireshark # One of: Internet Draft version 5, Internet Draft version 6, Internet Draft version 7, RFC 4666 # (case-insensitive). #m3ua.version: RFC 4666 # TSN size in bits, either 6 or 14 bit # One of: 6 bits, 14 bits # (case-insensitive). #mac.tsn_size: 6 bits # Number of Re-Transmits before expert warning triggered # A decimal number #mac-lte.retx_count_warn: 3 # Attempt to decode BCH, PCH and CCCH data using LTE RRC dissector # TRUE or FALSE (case-insensitive) #mac-lte.attempt_rrc_decode: TRUE # Attempt to dissect frames that have failed CRC check # TRUE or FALSE (case-insensitive) #mac-lte.attempt_to_dissect_crc_failures: FALSE # Will call LTE RLC dissector with standard settings as per RRC spec # TRUE or FALSE (case-insensitive) #mac-lte.attempt_to_dissect_srb_sdus: TRUE # Will call LTE RLC dissector for MCH LCID 0 # TRUE or FALSE (case-insensitive) #mac-lte.attempt_to_dissect_mcch: FALSE # Call RLC dissector MTCH LCIDs # TRUE or FALSE (case-insensitive) #mac-lte.call_rlc_for_mtch: FALSE # Set whether LCID -> drb Table is taken from static table (below) or from info learned from control protocol (e.g. RRC) # One of: From static table, From configuration protocol # (case-insensitive). #mac-lte.lcid_to_drb_mapping_source: From static table # If any BSR report is >= this number, an expert warning will be added # A decimal number #mac-lte.bsr_warn_threshold: 50 # Track status of SRs, providing links between requests, failure indications and grants # TRUE or FALSE (case-insensitive) #mac-lte.track_sr: TRUE # Can show PHY, MAC or RLC layer info in Info column # One of: PHY Info, MAC Info, RLC Info # (case-insensitive). #mac-lte.layer_to_show: RLC Info # Attempt to decode 6 bytes of Contention Resolution body as an UL CCCH PDU # TRUE or FALSE (case-insensitive) #mac-lte.decode_cr_body: FALSE # Apply DRX config and show DRX state within each UE # TRUE or FALSE (case-insensitive) #mac-lte.show_drx: FALSE # Add as a generated field the middle of the range indicated by the BSR index # TRUE or FALSE (case-insensitive) #mac-lte.show_bsr_median: FALSE # Attempt to decode BCCH, PCCH and CCCH data using NR RRC dissector # TRUE or FALSE (case-insensitive) #mac-nr.attempt_rrc_decode: TRUE # Will call NR RLC dissector with standard settings as per RRC spec # TRUE or FALSE (case-insensitive) #mac-nr.attempt_to_dissect_srb_sdus: TRUE # Set whether LCID -> drb Table is taken from static table (below) or from info learned from control protocol (i.e. RRC) # One of: From static table, From configuration protocol # (case-insensitive). #mac-nr.lcid_to_drb_mapping_source: From static table # The name of the file containing the mate module's configuration # A path to a file #mate.config: # Decode control data received on "usb.control" with an unknown interface class as MBIM # TRUE or FALSE (case-insensitive) #mbim.control_decode_unknown_itf: FALSE # Format used for SMS PDU decoding # One of: Automatic, 3GPP, 3GPP2 # (case-insensitive). #mbim.sms_pdu_format: Automatic # Set the UDP port for the MCPE Server # A decimal number #mcpe.udp.port: 19132 # A frame is considered for decoding as MDSHDR if either ethertype is 0xFCFC or zero. Turn this flag off if you don't want ethertype zero to be decoded as MDSHDR. This might be useful to avoid problems with test frames. # TRUE or FALSE (case-insensitive) #mdshdr.decode_if_etype_zero: FALSE # Set the SCTP port for MEGACO text messages # A decimal number #megaco.sctp.txt_port: 2944 # Specifies that the raw text of the MEGACO message should be displayed instead of (or in addition to) the dissection tree # TRUE or FALSE (case-insensitive) #megaco.display_raw_text: TRUE # Specifies that the dissection tree of the MEGACO message should be displayed instead of (or in addition to) the raw text # TRUE or FALSE (case-insensitive) #megaco.display_dissect_tree: TRUE # Maintain relationships between transactions and contexts and display an extra tree showing context data # TRUE or FALSE (case-insensitive) #megaco.ctx_info: FALSE # Whether the MEMCACHE dissector should reassemble headers of a request spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #memcache.desegment_headers: TRUE # Whether the memcache dissector should reassemble PDUs spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #memcache.desegment_pdus: TRUE # Set the UDP port for gateway messages (if other than the default of 2427) # A decimal number #mgcp.tcp.gateway_port: 2427 # Set the TCP port for gateway messages (if other than the default of 2427) # A decimal number #mgcp.udp.gateway_port: 2427 # Set the TCP port for callagent messages (if other than the default of 2727) # A decimal number #mgcp.tcp.callagent_port: 2727 # Set the UDP port for callagent messages (if other than the default of 2727) # A decimal number #mgcp.udp.callagent_port: 2727 # Specifies that the raw text of the MGCP message should be displayed instead of (or in addition to) the dissection tree # TRUE or FALSE (case-insensitive) #mgcp.display_raw_text: FALSE # Display the number of MGCP messages found in a packet in the protocol column. # TRUE or FALSE (case-insensitive) #mgcp.display_mgcp_message_count: FALSE # Display multipart bodies with no media type dissector as raw text (may cause problems with binary data). # TRUE or FALSE (case-insensitive) #mime_multipart.display_unknown_body_as_text: FALSE # Remove any base64 content-transfer encoding from bodies. This supports export of the body and its further dissection. # TRUE or FALSE (case-insensitive) #mime_multipart.remove_base64_encoding: FALSE # Dissect payload only if MIC is valid. # TRUE or FALSE (case-insensitive) #mle.meshlink_mic_ok: FALSE # Register Format # One of: UINT16 , INT16 , UINT32 , INT32 , IEEE FLT , MODICON FLT # (case-insensitive). #modbus.mbus_register_format: UINT16 # Whether the Modbus RTU dissector should desegment all messages spanning multiple TCP segments # TRUE or FALSE (case-insensitive) #mbrtu.desegment: TRUE # Whether to validate the CRC # TRUE or FALSE (case-insensitive) #mbrtu.crc_verification: FALSE # Whether the Modbus RTU dissector should desegment all messages spanning multiple TCP segments # TRUE or FALSE (case-insensitive) #mbtcp.desegment: TRUE # The dynamic payload type which will be interpreted as MP4V-ES # A decimal number #mp4v-es.dynamic.payload.type: 0 # Whether the section dissector should verify the CRC or checksum # TRUE or FALSE (case-insensitive) #mpeg_dsmcc.verify_crc: FALSE # Whether the section dissector should verify the CRC # TRUE or FALSE (case-insensitive) #mpeg_sect.verify_crc: FALSE # Lowest label is used to segregate flows inside a pseudowire # TRUE or FALSE (case-insensitive) #mpls.flowlabel_in_mpls_header: FALSE # Enable to allow non-zero Length in Control Word. This may be needed to correctly decode traffic from some legacy devices which generate non-zero Length even if there is no padding in the packet. Note that Length should have proper value (dissector checks this anyway). # # Disable to blame all packets with CW.Length <> 0. This conforms to RFC4717. # TRUE or FALSE (case-insensitive) #mplspwatmaal5sdu.allow_cw_length_nonzero_aal5: FALSE # Enable to use reserved bits (8..9) of Control Word as an extension of CW.Length. This may be needed to correctly decode traffic from some legacy devices which uses reserved bits as extension of Length # # Disable to blame all packets with CW.Reserved <> 0. This conforms to RFC4717. # TRUE or FALSE (case-insensitive) #mplspwatmaal5sdu.extend_cw_length_with_rsvd_aal5: FALSE # Enable to allow non-zero Length in Control Word. This may be needed to correctly decode traffic from some legacy devices which generate non-zero Length even if there is no padding in the packet. Note that Length should have proper value (dissector checks this anyway). # # Disable to blame all packets with CW.Length <> 0. This conforms to RFC4717. # TRUE or FALSE (case-insensitive) #mplspwatmn1cw.allow_cw_length_nonzero: FALSE # Enable to use reserved bits (8..9) of Control Word as an extension of CW.Length. This may be needed to correctly decode traffic from some legacy devices which uses reserved bits as extension of Length # # Disable to blame all packets with CW.Reserved <> 0. This conforms to RFC4717. # TRUE or FALSE (case-insensitive) #mplspwatmn1cw.extend_cw_length_with_rsvd: FALSE # To use this option you must also enable "Analyze TCP sequence numbers". # TRUE or FALSE (case-insensitive) #mptcp.analyze_mptcp: TRUE # In case you don't capture the key, it will use the first DSN seen # TRUE or FALSE (case-insensitive) #mptcp.relative_sequence_numbers: TRUE # Scales logarithmically with the number of packetsYou need to capture the handshake for this to work."Map TCP subflows to their respective MPTCP connections" # TRUE or FALSE (case-insensitive) #mptcp.analyze_mappings: FALSE # (Greedy algorithm: Scales linearly with number of subflows and logarithmic scaling with number of packets)You need to enable DSS mapping analysis for this option to work # TRUE or FALSE (case-insensitive) #mptcp.intersubflows_retransmission: FALSE # Whether the MQ dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #mq.desegment: TRUE # Whether the MQ dissector should reassemble MQ messages spanning multiple TSH segments # TRUE or FALSE (case-insensitive) #mq.reassembly: TRUE # When dissecting PCF there can be a lot of parameters. You can limit the number of parameter decoded, before it continue with the next PCF. # A decimal number #mqpcf.maxprm: 999 # When dissecting a parameter of a PCFm, if it is a StringList, IntegerList or Integer64 List, You can limit the number of elements displayed, before it continues with the next Parameter. # A decimal number #mqpcf.maxlst: 20000 # Show Publish Message as text # TRUE or FALSE (case-insensitive) #mqtt.show_msg_as_text: FALSE # Specifies that the raw text of the MSRP message should be displayed in addition to the dissection tree # TRUE or FALSE (case-insensitive) #msrp.display_raw_text: TRUE # Where available, show which protocol and frame caused this MSRP stream to be created # TRUE or FALSE (case-insensitive) #msrp.show_setup_info: TRUE # Whether the MTP2 dissector should use extended sequence numbers as described in Q.703, Annex A as a default. # TRUE or FALSE (case-insensitive) #mtp2.use_extended_sequence_numbers: FALSE # Some SS7 capture hardware includes the FCS at the end of the packet, others do not. # TRUE or FALSE (case-insensitive) #mtp2.capture_contains_frame_check_sequence: FALSE # Reverse the bit order inside bytes specified in Q.703. # TRUE or FALSE (case-insensitive) #mtp2.reverse_bit_order_mtp2: FALSE # RTP payload types for embedded packets in RTP stream. Must be of the dynamic types from 96 to 127. # A string denoting an positive integer range (e.g., "1-20,30-40") #mtp2.rtp_payload_type: # This only works for SCCP traffic for now # TRUE or FALSE (case-insensitive) #mtp3.heuristic_standard: FALSE # The SS7 standard used in MTP3 packets # One of: ITU, ANSI, Chinese ITU, Japan # (case-insensitive). #mtp3.standard: ITU # The structure of the pointcodes in ITU networks # One of: Unstructured, 3-8-3, 4-3-4-3 # (case-insensitive). #mtp3.itu_pc_structure: Unstructured # The structure of the pointcodes in Japan networks # One of: Unstructured, 7-4-5, 3-4-4-5 # (case-insensitive). #mtp3.japan_pc_structure: Unstructured # Use 5-bit (instead of 8-bit) SLS in ANSI MTP3 packets # TRUE or FALSE (case-insensitive) #mtp3.ansi_5_bit_sls: FALSE # Use 5-bit (instead of 4-bit) SLS in Japan MTP3 packets # TRUE or FALSE (case-insensitive) #mtp3.japan_5_bit_sls: FALSE # Format for point code in the address columns # One of: Decimal, Hexadecimal, NI-Decimal, NI-Hexadecimal, Dashed # (case-insensitive). #mtp3.addr_format: Dashed # Decode the spare bits of the SIO as the MSU priority (a national option in ITU) # TRUE or FALSE (case-insensitive) #mtp3.itu_priority: FALSE # Whether the MySQL dissector should reassemble MySQL buffers spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #mysql.desegment_buffers: TRUE # Whether the MySQL dissector should display the SQL query string in the INFO column. # TRUE or FALSE (case-insensitive) #mysql.show_sql_query: FALSE # This should work when the NAS ciphering algorithm is NULL (5G-EEA0) # TRUE or FALSE (case-insensitive) #nas-5gs.null_decipher: FALSE # Always dissect NAS EPS messages as plain # TRUE or FALSE (case-insensitive) #nas-eps.dissect_plain: FALSE # This should work when the NAS ciphering algorithm is NULL (128-EEA0) # TRUE or FALSE (case-insensitive) #nas-eps.null_decipher: TRUE # No description # TRUE or FALSE (case-insensitive) #nas-eps.user_data_container_as_ip: TRUE # Whether the Nasdaq ITCH dissector should decode Chi X extensions. # TRUE or FALSE (case-insensitive) #nasdaq_itch.chi_x: TRUE # Whether the Nasdaq-SoupTCP dissector should reassemble messages spanning multiple TCP segments. # TRUE or FALSE (case-insensitive) #nasdaq_soup.desegment: TRUE # foo # One of: MAC_CONTENT_UNKNOWN, MAC_CONTENT_DCCH, MAC_CONTENT_PS_DTCH, MAC_CONTENT_CS_DTCH, MAC_CONTENT_CCCH # (case-insensitive). #nbap.lch1_content: MAC_CONTENT_DCCH # foo # One of: MAC_CONTENT_UNKNOWN, MAC_CONTENT_DCCH, MAC_CONTENT_PS_DTCH, MAC_CONTENT_CS_DTCH, MAC_CONTENT_CCCH # (case-insensitive). #nbap.lch2_content: MAC_CONTENT_DCCH # foo # One of: MAC_CONTENT_UNKNOWN, MAC_CONTENT_DCCH, MAC_CONTENT_PS_DTCH, MAC_CONTENT_CS_DTCH, MAC_CONTENT_CCCH # (case-insensitive). #nbap.lch3_content: MAC_CONTENT_DCCH # foo # One of: MAC_CONTENT_UNKNOWN, MAC_CONTENT_DCCH, MAC_CONTENT_PS_DTCH, MAC_CONTENT_CS_DTCH, MAC_CONTENT_CCCH # (case-insensitive). #nbap.lch4_content: MAC_CONTENT_DCCH # foo # One of: MAC_CONTENT_UNKNOWN, MAC_CONTENT_DCCH, MAC_CONTENT_PS_DTCH, MAC_CONTENT_CS_DTCH, MAC_CONTENT_CCCH # (case-insensitive). #nbap.lch5_content: MAC_CONTENT_CS_DTCH # foo # One of: MAC_CONTENT_UNKNOWN, MAC_CONTENT_DCCH, MAC_CONTENT_PS_DTCH, MAC_CONTENT_CS_DTCH, MAC_CONTENT_CCCH # (case-insensitive). #nbap.lch6_content: MAC_CONTENT_CS_DTCH # foo # One of: MAC_CONTENT_UNKNOWN, MAC_CONTENT_DCCH, MAC_CONTENT_PS_DTCH, MAC_CONTENT_CS_DTCH, MAC_CONTENT_CCCH # (case-insensitive). #nbap.lch7_content: MAC_CONTENT_CS_DTCH # foo # One of: MAC_CONTENT_UNKNOWN, MAC_CONTENT_DCCH, MAC_CONTENT_PS_DTCH, MAC_CONTENT_CS_DTCH, MAC_CONTENT_CCCH # (case-insensitive). #nbap.lch8_content: MAC_CONTENT_DCCH # foo # One of: MAC_CONTENT_UNKNOWN, MAC_CONTENT_DCCH, MAC_CONTENT_PS_DTCH, MAC_CONTENT_CS_DTCH, MAC_CONTENT_CCCH # (case-insensitive). #nbap.lch9_content: MAC_CONTENT_PS_DTCH # foo # One of: MAC_CONTENT_UNKNOWN, MAC_CONTENT_DCCH, MAC_CONTENT_PS_DTCH, MAC_CONTENT_CS_DTCH, MAC_CONTENT_CCCH # (case-insensitive). #nbap.lch10_content: MAC_CONTENT_UNKNOWN # foo # One of: MAC_CONTENT_UNKNOWN, MAC_CONTENT_DCCH, MAC_CONTENT_PS_DTCH, MAC_CONTENT_CS_DTCH, MAC_CONTENT_CCCH # (case-insensitive). #nbap.lch11_content: MAC_CONTENT_PS_DTCH # foo # One of: MAC_CONTENT_UNKNOWN, MAC_CONTENT_DCCH, MAC_CONTENT_PS_DTCH, MAC_CONTENT_CS_DTCH, MAC_CONTENT_CCCH # (case-insensitive). #nbap.lch12_content: MAC_CONTENT_PS_DTCH # foo # One of: MAC_CONTENT_UNKNOWN, MAC_CONTENT_DCCH, MAC_CONTENT_PS_DTCH, MAC_CONTENT_CS_DTCH, MAC_CONTENT_CCCH # (case-insensitive). #nbap.lch13_content: MAC_CONTENT_CS_DTCH # foo # One of: MAC_CONTENT_UNKNOWN, MAC_CONTENT_DCCH, MAC_CONTENT_PS_DTCH, MAC_CONTENT_CS_DTCH, MAC_CONTENT_CCCH # (case-insensitive). #nbap.lch14_content: MAC_CONTENT_PS_DTCH # foo # One of: MAC_CONTENT_UNKNOWN, MAC_CONTENT_DCCH, MAC_CONTENT_PS_DTCH, MAC_CONTENT_CS_DTCH, MAC_CONTENT_CCCH # (case-insensitive). #nbap.lch15_content: MAC_CONTENT_CCCH # foo # One of: MAC_CONTENT_UNKNOWN, MAC_CONTENT_DCCH, MAC_CONTENT_PS_DTCH, MAC_CONTENT_CS_DTCH, MAC_CONTENT_CCCH # (case-insensitive). #nbap.lch16_content: MAC_CONTENT_DCCH # Encoding used for the IB-SG-DATA element carrying segments of information blocks # One of: Encoding Variant 1 (TS 25.433 Annex D.2), Encoding Variant 2 (TS 25.433 Annex D.3) # (case-insensitive). #nbap.ib_sg_data_encoding: Encoding Variant 1 (TS 25.433 Annex D.2) # Whether the NBD dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings # TRUE or FALSE (case-insensitive) #nbd.desegment_nbd_messages: TRUE # Whether the NBSS dissector should reassemble packets spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #nbss.desegment_nbss_commands: TRUE # Whether the NCP dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #ncp.desegment: TRUE # Whether the NCP dissector should defragment NDS messages spanning multiple reply packets. # TRUE or FALSE (case-insensitive) #ncp.defragment_nds: TRUE # Dissect the NetWare Information Structure as NetWare 5.x or higher or as older NetWare 3.x. # TRUE or FALSE (case-insensitive) #ncp.newstyle: TRUE # Whether the NCP dissector should echo the NDS Entry ID to name resolves to the expert table. # TRUE or FALSE (case-insensitive) #ncp.eid_2_expert: TRUE # Whether the NCP dissector should echo NCP connection information to the expert table. # TRUE or FALSE (case-insensitive) #ncp.connection_2_expert: FALSE # Whether the NCP dissector should echo protocol errors to the expert table. # TRUE or FALSE (case-insensitive) #ncp.error_2_expert: TRUE # Whether the NCP dissector should echo server information to the expert table. # TRUE or FALSE (case-insensitive) #ncp.server_2_expert: TRUE # Whether the NCP dissector should echo file open/close/oplock information to the expert table. # TRUE or FALSE (case-insensitive) #ncp.file_2_expert: FALSE # Version of the NDMP protocol to assume if the version can not be automatically detected from the capture # One of: Version 2, Version 3, Version 4, Version 5 # (case-insensitive). #ndmp.default_protocol_version: Version 4 # Whether the NDMP dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #ndmp.desegment: TRUE # Whether the dissector should defragment NDMP messages spanning multiple packets. # TRUE or FALSE (case-insensitive) #ndmp.defragment: TRUE # Whether the NDPS dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #ndps.desegment_tcp: TRUE # Whether the NDPS dissector should reassemble fragmented NDPS messages spanning multiple SPX packets # TRUE or FALSE (case-insensitive) #ndps.desegment_spx: TRUE # Whether or not the NDPS dissector should show object id's and other details # TRUE or FALSE (case-insensitive) #ndps.show_oid: FALSE # Whether the NetBIOS dissector should defragment messages spanning multiple frames # TRUE or FALSE (case-insensitive) #netbios.defragment: TRUE # Whether the Netsync dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #netsync.desegment_netsync_messages: TRUE # Whether the dissector should snoop the FH to filename mappings by looking inside certain packets # TRUE or FALSE (case-insensitive) #nfs.file_name_snooping: FALSE # Whether the dissector should snoop the full pathname for files for matching FH's # TRUE or FALSE (case-insensitive) #nfs.file_full_name_snooping: FALSE # With this option display filters for nfs fhandles (nfs.fh.{name|full_name|hash}) will find both the request and response packets for a RPC call, even if the actual fhandle is only present in one of the packets # TRUE or FALSE (case-insensitive) #nfs.fhandle_find_both_reqrep: FALSE # When enabled, this option will print the NFSv4 tag (if one exists) in the Info column in the Summary pane # TRUE or FALSE (case-insensitive) #nfs.display_nfsv4_tag: TRUE # When enabled, shows only the significant NFSv4 Operations in the info column. Others (like GETFH, PUTFH, etc) are not displayed # TRUE or FALSE (case-insensitive) #nfs.display_major_nfsv4_ops: TRUE # Set the SCTP port for NGAP messages # A decimal number #ngap.sctp.port: 38412 # Dissect TransparentContainers that are opaque to NGAP # TRUE or FALSE (case-insensitive) #ngap.dissect_container: TRUE # Select whether target NG-RAN container should be decoded automatically (based on NG Setup procedure) or manually # One of: automatic, gNB, ng-eNB # (case-insensitive). #ngap.dissect_target_ng_ran_container_as: automatic # Whether the Authentication Extension data contains the source address. Some Cisco IOS implementations forgo this part of RFC2332. # TRUE or FALSE (case-insensitive) #nhrp.auth_ext_has_addr: TRUE # Whether the dissector will track and match MSG and RES calls for asynchronous NLM # TRUE or FALSE (case-insensitive) #nlm.msg_res_matching: FALSE # NT Password (used to decrypt payloads) # A string #ntlmssp.nt_password: # Range of NVMe Subsystem ports(default 4420) # A string denoting an positive integer range (e.g., "1-20,30-40") #nvme-rdma.subsystem_ports: 4420 # Range of NVMe Subsystem ports(default 4420) # A string denoting an positive integer range (e.g., "1-20,30-40") #nvme-tcp.subsystem_ports: 4420 # Whether to validate the PDU header digest or not. # TRUE or FALSE (case-insensitive) #nvme-tcp.check_hdgst: FALSE # Whether to validate the PDU data digest or not. # TRUE or FALSE (case-insensitive) #nvme-tcp.check_ddgst: FALSE # Whether the dissector should put the internal OER data in the tree or if it should hide it # TRUE or FALSE (case-insensitive) #oer.display_internal_oer_fields: FALSE # Dissect custom olsr.org message types (compatible with rfc routing agents) # TRUE or FALSE (case-insensitive) #olsr.ff_olsrorg: TRUE # Dissect custom nrlolsr tc message (incompatible with rfc routing agents) # TRUE or FALSE (case-insensitive) #olsr.nrlolsr: TRUE # SSL/TLS Ports range # A string denoting an positive integer range (e.g., "1-20,30-40") #opa.fe.tls.port: 3249-3252 # Attempt to parse mad payload even when MAD.Status is non-zero # TRUE or FALSE (case-insensitive) #opa.mad.parse_mad_error: FALSE # Attempt to reassemble the mad payload of RMPP segments # TRUE or FALSE (case-insensitive) #opa.mad.reassemble_rmpp: TRUE # Whether the OpenFlow dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #openflow.desegment: TRUE # To be able to fully dissect SSDO and SPDO packages, a valid UDID for the SCM has to be provided # A string #opensafety.scm_udid: 00:00:00:00:00:00 # Automatically assign a detected SCM UDID (by reading SNMT->SNTM_assign_UDID_SCM) and set it for the file # TRUE or FALSE (case-insensitive) #opensafety.scm_udid_autoset: TRUE # A comma-separated list of nodes to be filtered during dissection # A string #opensafety.filter_nodes: # If set to true, only nodes in the list will be shown, otherwise they will be hidden # TRUE or FALSE (case-insensitive) #opensafety.filter_show_nodes_in_filterlist: TRUE # Port used by any UDP demo implementation to transport data # A decimal number #opensafety.network_udp_port: 9877 # UDP port used by SercosIII to transport data # A decimal number #opensafety.network_udp_port_sercosiii: 8755 # In an SercosIII/UDP transport stream, openSAFETY frame 2 will be expected before frame 1 # TRUE or FALSE (case-insensitive) #opensafety.network_udp_frame_first_sercosiii: FALSE # In the transport stream, openSAFETY frame 2 will be expected before frame 1 # TRUE or FALSE (case-insensitive) #opensafety.network_udp_frame_first: FALSE # Modbus/TCP words can be transcoded either big- or little endian. Default will be little endian # TRUE or FALSE (case-insensitive) #opensafety.mbtcp_big_endian: FALSE # Enables additional information in the dissection for better debugging an openSAFETY trace # TRUE or FALSE (case-insensitive) #opensafety.debug_verbose: FALSE # Enable heuristic dissection for openSAFETY over UDP encoded traffic # TRUE or FALSE (case-insensitive) #opensafety.enable_udp: TRUE # Enable heuristic dissection for Modbus/TCP # TRUE or FALSE (case-insensitive) #opensafety.enable_mbtcp: TRUE # Display the data between openSAFETY packets # TRUE or FALSE (case-insensitive) #opensafety.display_intergap_data: FALSE # SPDOs may only be found in cyclic data, SSDOs/SNMTS only in acyclic data # TRUE or FALSE (case-insensitive) #opensafety.classify_transport: TRUE # Port used by the openSAFETY over UDP data transport # A decimal number #opensafety_udp.network_udp_port: 9877 # If tls-auth detection fails, you can choose to override detection and set tls-auth yourself # TRUE or FALSE (case-insensitive) #openvpn.tls_auth_detection_override: FALSE # If the parameter --tls-auth is used, the following preferences must also be defined. # TRUE or FALSE (case-insensitive) #openvpn.tls_auth: FALSE # If the parameter --tls-auth is used, a HMAC header is being inserted. # The default HMAC algorithm is SHA-1 which generates a 160 bit HMAC, therefore 20 bytes should be ok. # The value must be between 20 (160 bits) and 64 (512 bits). # A decimal number #openvpn.tls_auth_hmac_size: 20 # If the parameter --tls-auth is used, an additional packet-id for replay protection is inserted after the HMAC signature. This field can either be 4 bytes or 8 bytes including an optional time_t timestamp long. # This option is only evaluated if tls_auth_hmac_size > 0. # The default value is TRUE. # TRUE or FALSE (case-insensitive) #openvpn.long_format: TRUE # Whether the Openwire dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #openwire.desegment: TRUE # Whether verbose type and length information are displayed in the protocol tree # TRUE or FALSE (case-insensitive) #openwire.verbose_type: FALSE # Whether the OPSI dissector should desegment all messages spanning multiple TCP segments # TRUE or FALSE (case-insensitive) #opsi.desegment_opsi_messages: TRUE # Whether segmented TPKT datagrams should be reassembled # TRUE or FALSE (case-insensitive) #osi.tpkt_reassemble: FALSE # Whether segmented RTSE datagrams should be reassembled. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #rtse.reassemble: TRUE # Whether the IDMP dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #idmp.desegment_idmp_messages: TRUE # Whether segmented IDMP datagrams should be reassembled. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #idmp.reassemble: TRUE # Reassemble fragmented P_Mul packets # TRUE or FALSE (case-insensitive) #p_mul.reassemble: TRUE # Make the P_Mul dissector use relative message id number instead of absolute ones # TRUE or FALSE (case-insensitive) #p_mul.relative_msgid: TRUE # Calculate sequence/acknowledgement analysis # TRUE or FALSE (case-insensitive) #p_mul.seq_ack_analysis: TRUE # Type of content in Data_PDU # One of: No decoding, BER encoded ASN.1, Compressed Data Type # (case-insensitive). #p_mul.decode: No decoding # Attempt to decode parts of the message that aren't fully understood yet # TRUE or FALSE (case-insensitive) #papi.experimental_decode: FALSE # SCCP (and SUA) SSNs to decode as PCAP # A string denoting an positive integer range (e.g., "1-20,30-40") #pcap.ssn: # Whether the PCLI summary line should be shown in the protocol tree # TRUE or FALSE (case-insensitive) #pcli.summary_in_tree: TRUE # Show uncompressed User-Plane data as IP # TRUE or FALSE (case-insensitive) #pdcp-lte.show_user_plane_as_ip: TRUE # Show unciphered Signalling-Plane data as RRC # TRUE or FALSE (case-insensitive) #pdcp-lte.show_signalling_plane_as_rrc: TRUE # Do sequence number analysis # One of: No-Analysis, Only-RLC-frames, Only-PDCP-frames # (case-insensitive). #pdcp-lte.check_sequence_numbers: Only-RLC-frames # Attempt to decode ROHC data # TRUE or FALSE (case-insensitive) #pdcp-lte.dissect_rohc: FALSE # Can show RLC, PDCP or Traffic layer info in Info column # One of: RLC Info, PDCP Info, Traffic Info # (case-insensitive). #pdcp-lte.layer_to_show: RLC Info # If RRC Security Info not seen, e.g. in Handover # One of: EEA0 (NULL), EEA1 (SNOW3G), EEA2 (AES), EEA3 (ZUC) # (case-insensitive). #pdcp-lte.default_ciphering_algorithm: EEA0 (NULL) # If RRC Security Info not seen, e.g. in Handover # One of: EIA0 (NULL), EIA1 (SNOW3G), EIA2 (AES), EIA3 (ZUC) # (case-insensitive). #pdcp-lte.default_integrity_algorithm: EIA0 (NULL) # N.B. only possible if build with algorithm support, and have key available and configured # TRUE or FALSE (case-insensitive) #pdcp-lte.decipher_signalling: TRUE # N.B. only possible if build with algorithm support, and have key available and configured # TRUE or FALSE (case-insensitive) #pdcp-lte.decipher_userplane: FALSE # N.B. only possible if build with algorithm support, and have key available and configured # TRUE or FALSE (case-insensitive) #pdcp-lte.verify_integrity: TRUE # Ignore the LTE RRC security algorithm configuration, to be used when PDCP is already deciphered in the capture # TRUE or FALSE (case-insensitive) #pdcp-lte.ignore_rrc_sec_params: FALSE # Show uncompressed User-Plane data as IP # TRUE or FALSE (case-insensitive) #pdcp-nr.show_user_plane_as_ip: TRUE # Show unciphered Signalling-Plane data as RRC # TRUE or FALSE (case-insensitive) #pdcp-nr.show_signalling_plane_as_rrc: TRUE # Do sequence number analysis # One of: No-Analysis, Only-RLC-frames, Only-PDCP-frames # (case-insensitive). #pdcp-nr.check_sequence_numbers: Only-RLC-frames # Attempt to decode ROHC data # TRUE or FALSE (case-insensitive) #pdcp-nr.dissect_rohc: FALSE # Can show RLC, PDCP or Traffic layer info in Info column # One of: RLC Info, PDCP Info, Traffic Info # (case-insensitive). #pdcp-nr.layer_to_show: RLC Info # Whether the dissector should put the internal PER data in the tree or if it should hide it # TRUE or FALSE (case-insensitive) #per.display_internal_per_fields: FALSE # PFCP port (default 8805) # A decimal number #pfcp.port_pfcp: 8805 # Track PFCP session # TRUE or FALSE (case-insensitive) #pfcp.track_pfcp_session: FALSE # Whether or not UID and PID fields are dissected in big or little endian # TRUE or FALSE (case-insensitive) #pflog.uid_endian: TRUE # Whether to check the validity of the PGM checksum # TRUE or FALSE (case-insensitive) #pgm.check_checksum: TRUE # Whether the PIM payload is shown off of the main tree or encapsulated within the PIM options # TRUE or FALSE (case-insensitive) #pim.payload_tree: TRUE # The password to used to decrypt the encrypted elements within the PKCS#12 file # A string #pkcs12.password: # Whether to try and decrypt the encrypted data within the PKCS#12 with a NULL password # TRUE or FALSE (case-insensitive) #pkcs12.try_null_password: FALSE # Whether the PN-RT summary line should be shown in the protocol tree # TRUE or FALSE (case-insensitive) #pn_rt.summary_in_tree: TRUE # Reassemble PNIO Fragments and get them decoded # TRUE or FALSE (case-insensitive) #pn_rt.desegment: TRUE # Protocol payload type # One of: Data, Sony FeliCa, NXP MiFare, ISO 7816 # (case-insensitive). #pn532.prtype532: Data # Whether the PNIO dissector is allowed to use detailed PROFIsafe dissection of cyclic data frames # TRUE or FALSE (case-insensitive) #pn_io.pnio_ps_selection: TRUE # Select your Networkpath to your GSD-Files. # A path to a directory #pn_io.pnio_ps_networkpath: # Whether the POP dissector should reassemble RETR and TOP responses and spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #pop.desegment_data: TRUE # Whether fragmented 802.11 aggregated MPDUs should be reassembled # TRUE or FALSE (case-insensitive) #ppi.reassemble: TRUE # The type of PPP frame checksum (none, 16-bit, 32-bit) # One of: None, 16-Bit, 32-Bit # (case-insensitive). #ppp.fcs_type: None # Default Protocol ID to be used for PPPMuxCP # A hexadecimal number #ppp.default_proto_id: 0 # Whether PPP Multilink frames use 12-bit sequence numbers # TRUE or FALSE (case-insensitive) #mp.short_seqno: FALSE # Maximum number of PPP Multilink fragments to try to reassemble into one frame # A decimal number #mp.max_fragments: 6 # Age off unreassmbled fragments after this many packets # A decimal number #mp.fragment_aging: 4000 # Show values of tags and lengths of data fields # TRUE or FALSE (case-insensitive) #pppoed.show_tags_and_lengths: FALSE # Show the names of message, field, enum and enum_value. Show the wire type and field number format of field. Show value nodes of field and enum_value. # TRUE or FALSE (case-insensitive) #protobuf.show_details: FALSE # Show all fields of bytes type as string. For example ETCD string # TRUE or FALSE (case-insensitive) #protobuf.bytes_as_string: FALSE # Try to dissect all undefined length-delimited fields as string. # TRUE or FALSE (case-insensitive) #protobuf.try_dissect_as_string: FALSE # Try to show all possible field types for each undefined field according to wire type. # TRUE or FALSE (case-insensitive) #protobuf.show_all_types: FALSE # Properly translates vendor specific opcodes # One of: Unknown vendor, Eastman Kodak, Canon, Nikon, Casio EX-F1, Microsoft / MTP, Olympus E series # (case-insensitive). #ptpip.vendor: Unknown vendor # Whether the PVFS dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #pvfs.desegment: TRUE # Whether the Q.931 dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #q931.desegment_h323_messages: TRUE # Reassemble segmented Q.931 messages (Q.931 - Annex H) # TRUE or FALSE (case-insensitive) #q931.reassembly: TRUE # Decode ISO/IEC cause coding standard as ITU-T # TRUE or FALSE (case-insensitive) #q931.iso_iec_cause_coding: FALSE # Type of Facility encoding # One of: Dissect facility as QSIG, Dissect facility as ETSI # (case-insensitive). #q932.facility_encoding: Dissect facility as QSIG # Set the UDP base port for the Quake III Arena Server # A decimal number #quake3.udp.arena_port: 27960 # Set the UDP base port for the Quake III Arena Master Server # A decimal number #quake3.udp.master_port: 27950 # Shared secret used to decode User Passwords and validate Response Authenticators # A string #radius.shared_secret: # Whether to check or not if Response Authenticator is correct. You need to define shared secret for this to work. # TRUE or FALSE (case-insensitive) #radius.validate_authenticator: FALSE # Whether to add or not to the tree the AVP's payload length # TRUE or FALSE (case-insensitive) #radius.show_length: FALSE # Whether to interpret 241-246 as extended attributes according to RFC 6929 # TRUE or FALSE (case-insensitive) #radius.disable_extended_attributes: FALSE # The SCCP SubSystem Number for RANAP (default 142) # A decimal number #ranap.sccp_ssn: 142 # Attempt to dissect RRC message embedded in RRC-Container IE # TRUE or FALSE (case-insensitive) #ranap.dissect_rrc_container: FALSE # Where available, show which protocol and frame caused this RDT stream to be created # TRUE or FALSE (case-insensitive) #rdt.show_setup_info: TRUE # Whether fragmented RELOAD datagrams should be reassembled # TRUE or FALSE (case-insensitive) #reload.defragment: TRUE # Length of the NodeId as defined in the overlay. # A decimal number #reload.nodeid_length: 16 # topology plugin defined in the overlay # A string #reload.topology_plugin: CHORD-RELOAD # Display the third and forth bytes of the RIPv2 header as the Routing Domain field (introduced in RFC 1388 [January 1993] and obsolete as of RFC 1723 [November 1994]) # TRUE or FALSE (case-insensitive) #rip.display_routing_domain: FALSE # When enabled, try to reassemble SDUs from the various PDUs received # TRUE or FALSE (case-insensitive) #rlc.perform_reassembly: TRUE # When enabled, if data is not present, don't report as an error, but instead add expert info to indicate that headers were omitted # TRUE or FALSE (case-insensitive) #rlc.header_only_mode: FALSE # When enabled, RLC will ignore sequence numbers reported in 'Security Mode Command'/'Security Mode Complete' (RRC) messages when checking if frames are ciphered # TRUE or FALSE (case-insensitive) #rlc.ignore_rrc_cipher_indication: FALSE # When enabled, RLC will assume all payloads in RLC frames are ciphered # TRUE or FALSE (case-insensitive) #rlc.ciphered_data: FALSE # LI size in bits, either 7 or 15 bit # One of: 7 bits, 15 bits, Let upper layers decide # (case-insensitive). #rlc.li_size: Let upper layers decide # Attempt to keep track of PDUs for AM channels, and point out problems # One of: No-Analysis, Only-MAC-frames, Only-RLC-frames # (case-insensitive). #rlc-lte.do_sequence_analysis_am: Only-MAC-frames # Attempt to keep track of PDUs for UM channels, and point out problems # One of: No-Analysis, Only-MAC-frames, Only-RLC-frames # (case-insensitive). #rlc-lte.do_sequence_analysis: Only-MAC-frames # Call PDCP dissector for signalling PDUs. Note that without reassembly, it canonly be called for complete PDUs (i.e. not segmented over RLC) # TRUE or FALSE (case-insensitive) #rlc-lte.call_pdcp_for_srb: TRUE # Call PDCP dissector for user-plane PDUs. Note that without reassembly, it canonly be called for complete PDUs (i.e. not segmented over RLC) # One of: Off, 7-bit SN, 12-bit SN, 15-bit SN, 18-bit SN, Use signalled value # (case-insensitive). #rlc-lte.call_pdcp_for_drb: Use signalled value # Call RRC dissector for CCCH PDUs # TRUE or FALSE (case-insensitive) #rlc-lte.call_rrc_for_ccch: TRUE # Call RRC dissector for MCCH PDUs Note that without reassembly, it canonly be called for complete PDUs (i.e. not segmented over RLC) # TRUE or FALSE (case-insensitive) #rlc-lte.call_rrc_for_mcch: FALSE # Call ip dissector for MTCH PDUs Note that without reassembly, it canonly be called for complete PDUs (i.e. not segmented over RLC) # TRUE or FALSE (case-insensitive) #rlc-lte.call_ip_for_mtch: FALSE # When enabled, if data is not present, don't report as an error, but instead add expert info to indicate that headers were omitted # TRUE or FALSE (case-insensitive) #rlc-lte.header_only_mode: FALSE # When enabled, attempts to re-assemble upper-layer SDUs that are split over more than one RLC PDU. Note: does not currently support out-of-order or re-segmentation. N.B. sequence analysis must also be turned on in order for reassembly to work # TRUE or FALSE (case-insensitive) #rlc-lte.reassembly: TRUE # Call PDCP dissector for signalling PDUs. Note that without reassembly, it canonly be called for complete PDUs (i.e. not segmented over RLC) # TRUE or FALSE (case-insensitive) #rlc-nr.call_pdcp_for_srb: TRUE # Call PDCP dissector for UL user-plane PDUs. Note that without reassembly, it canonly be called for complete PDUs (i.e. not segmented over RLC) # One of: Off, 12-bit SN, 18-bit SN, Use signalled value # (case-insensitive). #rlc-nr.call_pdcp_for_ul_drb: Off # Call PDCP dissector for DL user-plane PDUs. Note that without reassembly, it canonly be called for complete PDUs (i.e. not segmented over RLC) # One of: Off, 12-bit SN, 18-bit SN, Use signalled value # (case-insensitive). #rlc-nr.call_pdcp_for_dl_drb: Off # Call RRC dissector for CCCH PDUs # TRUE or FALSE (case-insensitive) #rlc-nr.call_rrc_for_ccch: TRUE # When enabled, if data is not present, don't report as an error, but instead add expert info to indicate that headers were omitted # TRUE or FALSE (case-insensitive) #rlc-nr.header_only_mode: FALSE # N.B. This should be considered experimental/incomplete, in that it doesn't try to discard reassembled state when reestablishmenment happens, or in certain packet-loss cases # TRUE or FALSE (case-insensitive) #rlc-nr.reassemble_um_frames: FALSE # Whether the RPC dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #rpc.desegment_rpc_over_tcp: TRUE # Whether the RPC dissector should defragment RPC-over-TCP messages. # TRUE or FALSE (case-insensitive) #rpc.defragment_rpc_over_tcp: TRUE # Set the maximum size of RPCoverTCP PDUs. If the size field of the record marker is larger than this value it will not be considered a valid RPC PDU. # A decimal number #rpc.max_tcp_pdu_size: 4194304 # Whether the RPC dissector should attempt to dissect RPC PDUs containing programs that are not known to Wireshark. This will make the heuristics significantly weaker and elevate the risk for falsely identifying and misdissecting packets significantly. # TRUE or FALSE (case-insensitive) #rpc.dissect_unknown_programs: FALSE # Whether the RPC dissector should attempt to locate RPC PDU boundaries when initial fragment alignment is not known. This may cause false positives, or slow operation. # TRUE or FALSE (case-insensitive) #rpc.find_fragment_start: FALSE # Whether the RPCAP dissector should reassemble PDUs spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #rpcap.desegment_pdus: TRUE # Whether the packets should be decoded according to the link-layer type. # TRUE or FALSE (case-insensitive) #rpcap.decode_content: TRUE # Default link-layer type to use if an Open Reply packet has not been received. # A decimal number #rpcap.linktype: 4294967295 # RPKI-Router Protocol TCP TLS port if other than the default # A decimal number #rpkirtr.tcp.rpkirtr_tls.port: 324 # Controls the display of the session's client username in the info column. This is only displayed if the packet containing it was seen during this capture session. # TRUE or FALSE (case-insensitive) #rsh.info_show_client_username: FALSE # Controls the display of the session's server username in the info column. This is only displayed if the packet containing it was seen during this capture session. # TRUE or FALSE (case-insensitive) #rsh.info_show_server_username: TRUE # Controls the display of the command being run on the server by this session in the info column. This is only displayed if the packet containing it was seen during this capture session. # TRUE or FALSE (case-insensitive) #rsh.info_show_command: FALSE # Use ipaccess nanoBTS specific definitions for RSL # TRUE or FALSE (case-insensitive) #gsm_abis_rsl.use_ipaccess_rsl: FALSE # The Physical Context Information field is not specified This information should be not be analysed by BSC, but merely forwarded from one TRX/channel to another. # TRUE or FALSE (case-insensitive) #gsm_abis_rsl.dissect_phy_ctx_inf: TRUE # Specifies whether Wireshark should decode and display sub-messages within BUNDLE messages # TRUE or FALSE (case-insensitive) #rsvp.process_bundle: TRUE # Specifies how Wireshark should dissect generalized labels # One of: data (no interpretation), SONET/SDH ("S, U, K, L, M" scheme), Wavelength Label (fixed or flexi grid), ODUk Label # (case-insensitive). #rsvp.generalized_label_options: data (no interpretation) # Set the TCP port for RSYNC messages # A decimal number #rsync.tcp_port: 873 # Whether the RSYNC dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #rsync.desegment: TRUE # RTCDC SCTP PPID if other than the default # A decimal number #rtcdc.sctp.ppi: 50 # Where available, show which protocol and frame caused this RTCP stream to be created # TRUE or FALSE (case-insensitive) #rtcp.show_setup_info: TRUE # Try to work out network delay by comparing time between packets as captured and delays as seen by endpoint # TRUE or FALSE (case-insensitive) #rtcp.show_roundtrip_calculation: FALSE # Minimum (absolute) calculated roundtrip delay time in milliseconds that should be reported # A decimal number #rtcp.roundtrip_min_threshhold: 10 # Whether the RTMPT dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #rtmpt.desegment: TRUE # The largest acceptable packet size for reassembly # A decimal number #rtmpt.max_packet_size: 32768 # Where available, show which protocol and frame caused this RTP stream to be created # TRUE or FALSE (case-insensitive) #rtp.show_setup_info: TRUE # Whether subdissector can request RTP streams to be reassembled # TRUE or FALSE (case-insensitive) #rtp.desegment_rtp_streams: TRUE # If an RTP version 0 packet is encountered, it can be treated as an invalid or ZRTP packet, a CLASSIC-STUN packet, or a T.38 packet # One of: Invalid or ZRTP packets, STUN packets, CLASSIC-STUN packets, T.38 packets, SPRT packets # (case-insensitive). #rtp.version0_type: Invalid or ZRTP packets # Payload Type for RFC2198 Redundant Audio Data # A decimal number #rtp.rfc2198_payload_type: 99 # This is the value of the Payload Type field that specifies RTP Events # A decimal number #rtpevent.event_payload_type_value: 101 # This is the value of the Payload Type field that specifies Cisco Named Signaling Events # A decimal number #rtpevent.cisco_nse_payload_type_value: 100 # This is the value of the Payload Type field that specifies RTP-MIDI # A decimal number #rtpmidi.midi_payload_type_value: 0 # Specifies that RTP/RTCP/T.38/MSRP/etc streams are decoded based upon port numbers found in RTPproxy answers # TRUE or FALSE (case-insensitive) #rtpproxy.establish_conversation: TRUE # Maximum timeout value in waiting for reply from RTPProxy (in milliseconds). # A decimal number #rtpproxy.reply.timeout: 1000 # Specifies the maximum number of samples dissected in a DATA_BATCH submessage. Increasing this value may affect performances if the trace has a lot of big batched samples. # A decimal number #rtps.max_batch_samples_dissected: 16 # Shows the Topic Name and Type Name of the samples. # TRUE or FALSE (case-insensitive) #rtps.enable_topic_info: FALSE # Enables the reassembly of DATA_FRAG submessages. # TRUE or FALSE (case-insensitive) #rtps.enable_rtps_reassembly: FALSE # Whether the RTSP dissector should reassemble headers of a request spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #rtsp.desegment_headers: TRUE # Whether the RTSP dissector should use the "Content-length:" value to desegment the body of a request spanning multiple TCP segments # TRUE or FALSE (case-insensitive) #rtsp.desegment_body: TRUE # Set the port for RUA messages (Default of 29169) # A decimal number #rua.port: 29169 # S101 TCP port if other than the default # A decimal number #s101.tcp.port: 9000 # Set the SCTP port for S1AP messages # A decimal number #s1ap.sctp.port: 36412 # Dissect TransparentContainers that are opaque to S1AP # TRUE or FALSE (case-insensitive) #s1ap.dissect_container: TRUE # Select whether LTE TransparentContainer should be dissected as NB-IOT or legacy LTE # One of: Automatic, Legacy LTE, NB-IoT # (case-insensitive). #s1ap.dissect_lte_container_as: Automatic # Show length of text field # TRUE or FALSE (case-insensitive) #sametime.show_length: FALSE # reassemble packets # TRUE or FALSE (case-insensitive) #sametime.reassemble: TRUE # Whether the SASP dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #sasp.desegment_sasp_messages: TRUE # The source point code (usually MSC) (to determine whether message is uplink or downlink) # A hexadecimal number #sccp.source_pc: 0 # Show parameter length in the protocol tree # TRUE or FALSE (case-insensitive) #sccp.show_length: FALSE # Whether SCCP messages should be reassembled # TRUE or FALSE (case-insensitive) #sccp.defragment_xudt: TRUE # Whether to keep information about messages and their associations # TRUE or FALSE (case-insensitive) #sccp.trace_sccp: FALSE # Show SLR, DLR, and CAUSE Parameters in the Information Column of the Summary # TRUE or FALSE (case-insensitive) #sccp.show_more_info: FALSE # Set the source and destination addresses to the GT digits (if present). This may affect TCAP's ability to recognize which messages belong to which TCAP session. # TRUE or FALSE (case-insensitive) #sccp.set_addresses: FALSE # The protocol which should be used to dissect the payload if nothing else has claimed it # A string #sccp.default_payload: # When Target Cannot Be Identified, Decode SCSI Messages As # One of: Block Device, Sequential Device, Object Based Storage Device, Medium Changer Device, Multimedia Device # (case-insensitive). #scsi.decode_scsi_messages_as: Block Device # Whether fragmented SCSI DATA IN/OUT transfers should be reassembled # TRUE or FALSE (case-insensitive) #scsi.defragment: FALSE # Show source and destination port numbers in the protocol tree # TRUE or FALSE (case-insensitive) #sctp.show_port_numbers_in_tree: TRUE # The type of checksum used in SCTP packets # One of: None, Adler 32, CRC 32c, Automatic # (case-insensitive). #sctp.checksum: None # Show always SCTP control chunks in the Info column # TRUE or FALSE (case-insensitive) #sctp.show_always_control_chunks: TRUE # Try to decode a packet using an heuristic sub-dissector before using a sub-dissector registered to a specific port or PPI # TRUE or FALSE (case-insensitive) #sctp.try_heuristic_first: FALSE # Whether fragmented SCTP user messages should be reassembled # TRUE or FALSE (case-insensitive) #sctp.reassembly: TRUE # Match TSNs and their SACKs # TRUE or FALSE (case-insensitive) #sctp.tsn_analysis: TRUE # Match verification tags(CPU intense) # TRUE or FALSE (case-insensitive) #sctp.association_index: FALSE # Dissect upper layer protocols # TRUE or FALSE (case-insensitive) #sctp.ulp_dissection: TRUE # Data rate # One of: Attempt to guess, OC-3, OC-12, OC-24, OC-48 # (case-insensitive). #sdh.data.rate: OC-3 # Specifies that RTP/RTCP/T.38/MSRP/etc streams are decoded based upon port numbers found in SDP payload # TRUE or FALSE (case-insensitive) #sdp.establish_conversation: TRUE # Whether the SEL Protocol dissector should desegment all messages spanning multiple TCP segments # TRUE or FALSE (case-insensitive) #selfm.desegment: TRUE # Whether the SEL Protocol dissector should automatically pre-process Telnet data to remove duplicate 0xFF IAC bytes # TRUE or FALSE (case-insensitive) #selfm.telnetclean: TRUE # Perform CRC16 validation on Fast Messages # TRUE or FALSE (case-insensitive) #selfm.crc_verification: FALSE # List of word bits contained in SER equations (Comma-separated, no Quotes or Checksums) # A string #selfm.ser_list: # Whether the session dissector should reassemble messages spanning multiple SES segments # TRUE or FALSE (case-insensitive) #ses.desegment: TRUE # Enabling dissection makes it easy to view protocol details in each of the sampled headers. Disabling dissection may reduce noise caused when display filters match the contents of any sampled header(s). # TRUE or FALSE (case-insensitive) #sflow.enable_dissection: TRUE # This option only makes sense if dissection of sampled headers is enabled and probably not even then. # TRUE or FALSE (case-insensitive) #sflow.enable_analysis: FALSE # Port numbers used for SGsAP traffic (default 29118) # A string denoting an positive integer range (e.g., "1-20,30-40") #sgsap.sctp_ports: 29118 # Preference whether to Dissect the UDVM code or not # TRUE or FALSE (case-insensitive) #sigcomp.display.udvm.code: FALSE # preference whether to display the bytecode in UDVM operands or not # TRUE or FALSE (case-insensitive) #sigcomp.display.bytecode: FALSE # preference whether to decompress message or not # TRUE or FALSE (case-insensitive) #sigcomp.decomp.msg: FALSE # preference whether to display the decompressed message as raw text or not # TRUE or FALSE (case-insensitive) #sigcomp.display.decomp.msg.as.txt: FALSE # 'No-Printout' = UDVM executes silently, then increasing detail about execution of UDVM instructions; Warning! CPU intense at high detail # One of: No-Printout, Low-detail, Medium-detail, High-detail # (case-insensitive). #sigcomp.show.udvm.execution: No-Printout # Set the CA_system_ID used to decode ECM datagram as MIKEY # A hexadecimal number #simulcrypt.ca_system_id_mikey: 0x9999 # SIP Server TLS Port # A decimal number #sip.tls.port: 5061 # Specifies that the raw text of the SIP message should be displayed in addition to the dissection tree # TRUE or FALSE (case-insensitive) #sip.display_raw_text: FALSE # If the raw text of the SIP message is displayed, the trailing carriage return and line feed are not shown # TRUE or FALSE (case-insensitive) #sip.display_raw_text_without_crlf: FALSE # If enabled, only SIP/2.0 traffic will be dissected as SIP. Disable it to allow SIP traffic with a different version to be dissected as SIP. # TRUE or FALSE (case-insensitive) #sip.strict_sip_version: TRUE # Whether the SIP dissector should reassemble headers of a request spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #sip.desegment_headers: TRUE # Whether the SIP dissector should use the "Content-length:" value, if present, to reassemble the body of a request spanning multiple TCP segments, and reassemble chunked data spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #sip.desegment_body: TRUE # Whether retransmissions are detected coming from the same source port only. # TRUE or FALSE (case-insensitive) #sip.retrans_the_same_sport: TRUE # Whether SIP should delay tracking the media (e.g., RTP/RTCP) until an SDP offer is answered. If enabled, mid-dialog changes to SDP and media state only take effect if and when an SDP offer is successfully answered; however enabling this prevents tracking media in early-media call scenarios # TRUE or FALSE (case-insensitive) #sip.delay_sdp_changes: FALSE # Whether the generated call id should be hiddden(not displayed) in the tree or not. # TRUE or FALSE (case-insensitive) #sip.hide_generatd_call_id: FALSE # Validate SIP authorizations with known credentials # TRUE or FALSE (case-insensitive) #sip.validate_authorization: FALSE # Whether the SKINNY dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #skinny.desegment: TRUE # Whether the dissector should reassemble the payload of SMB Transaction commands spanning multiple SMB PDUs # TRUE or FALSE (case-insensitive) #smb.trans_reassembly: TRUE # Whether the dissector should reassemble DCERPC over SMB commands # TRUE or FALSE (case-insensitive) #smb.dcerpc_reassembly: TRUE # Whether the dissector should snoop SMB and related CIFS protocols to discover and display Names associated with SIDs # TRUE or FALSE (case-insensitive) #smb.sid_name_snooping: FALSE # Whether the dissector should display SIDs and RIDs in hexadecimal rather than decimal # TRUE or FALSE (case-insensitive) #smb.sid_display_hex: FALSE # Whether the export object functionality will take the full path file name as file identifier # TRUE or FALSE (case-insensitive) #smb.eosmb_take_name_as_fid: FALSE # Whether the export object functionality will take the full path file name as file identifier # TRUE or FALSE (case-insensitive) #smb2.eosmb2_take_name_as_fid: FALSE # Whether the dissector should reassemble Named Pipes over SMB2 commands # TRUE or FALSE (case-insensitive) #smb2.pipe_reassembly: TRUE # Whether the SMB Direct dissector should reassemble fragmented payloads # TRUE or FALSE (case-insensitive) #smb_direct.reassemble_smb_direct: TRUE # Enable reassembling (default is enabled) # TRUE or FALSE (case-insensitive) #sml.reassemble: TRUE # Enable crc (default is disabled) # TRUE or FALSE (case-insensitive) #sml.crc: FALSE # Whether the SMP dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #smp.desegment: TRUE # Whether the SMPP dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #smpp.reassemble_smpp_over_tcp: TRUE # Whether to decode the SMS contents when DCS is equal to 0 (zero). # One of: None, ASCII, IA5, ISO-8859-1, ISO-8859-5, ISO-8859-8, UCS2 # (case-insensitive). #smpp.decode_sms_over_smpp: None # Whether the SMTP dissector should reassemble command and response lines spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #smtp.desegment_lines: TRUE # Whether the SMTP dissector should reassemble DATA command and lines spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #smtp.desegment_data: TRUE # Whether the SMTP dissector should decode Base64 encoded AUTH parameters # TRUE or FALSE (case-insensitive) #smtp.decryption: FALSE # Whether fragmented BIUs should be reassembled # TRUE or FALSE (case-insensitive) #sna.defragment: TRUE # Whether the SNMP OID should be shown in the info column # TRUE or FALSE (case-insensitive) #snmp.display_oid: TRUE # Whether the SNMP dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #snmp.desegment: TRUE # ON - display dissected variables inside SNMP tree, OFF - display dissected variables in root tree after SNMP # TRUE or FALSE (case-insensitive) #snmp.var_in_tree: TRUE # Set whether dissector should run Snort itself or use user packet comments # One of: Not looking for Snort alerts, From running Snort, From user comments # (case-insensitive). #snort.alerts_source: Not looking for Snort alerts # The name of the snort binary file to run # A path to a file #snort.binary: /usr/sbin/snort # The name of the file containing the snort IDS configuration. Typically snort.conf # A path to a file #snort.config: /etc/snort/snort.conf # Whether or not information about the rule set and detected alerts should be shown in the tree of every snort PDU tree # TRUE or FALSE (case-insensitive) #snort.show_rule_set_stats: FALSE # Whether or not expert info should be used to highlight fired alerts # TRUE or FALSE (case-insensitive) #snort.show_alert_expert_info: FALSE # Attempt to show alert in reassembled frame where possible # TRUE or FALSE (case-insensitive) #snort.show_alert_in_reassembled_frame: FALSE # Show unidentified fields ("padding") in packet dissections # TRUE or FALSE (case-insensitive) #solaredge.unknown: TRUE # Inverter system encryption key # A string #solaredge.system_encryption_key: # SOME/IP Port Ranges UDP. # A string denoting an positive integer range (e.g., "1-20,30-40") #someip.ports.udp: # SOME/IP Port Ranges TCP. # A string denoting an positive integer range (e.g., "1-20,30-40") #someip.ports.tcp: # Reassemble SOME/IP-TP segments # TRUE or FALSE (case-insensitive) #someip.reassemble_tp: TRUE # Should the SOME/IP Dissector use the payload dissector? # TRUE or FALSE (case-insensitive) #someip.payload_dissector_activated: FALSE # SOME/IP Ignore Port Ranges UDP. These ports are not automatically added by the SOME/IP-SD. # A string denoting an positive integer range (e.g., "1-20,30-40") #someipsd.ports.udp.ignore: # SOME/IP Ignore Port Ranges TCP. These ports are not automatically added by the SOME/IP-SD. # A string denoting an positive integer range (e.g., "1-20,30-40") #someipsd.ports.tcp.ignore: # Whether the SoulSeek dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #slsk.desegment: TRUE # Whether the SoulSeek dissector should decompress all zlib compressed packets inside messages # TRUE or FALSE (case-insensitive) #slsk.decompress: TRUE # Whether the SoupBinTCP dissector should reassemble messages spanning multiple TCP segments. # TRUE or FALSE (case-insensitive) #soupbintcp.desegment: TRUE # Whether the SPDY dissector should reassemble multiple data frames into an entity body. # TRUE or FALSE (case-insensitive) #spdy.assemble_data_frames: TRUE # Whether to uncompress SPDY headers. # TRUE or FALSE (case-insensitive) #spdy.decompress_headers: TRUE # Whether to uncompress entity bodies that are compressed using "Content-Encoding: " # TRUE or FALSE (case-insensitive) #spdy.decompress_body: TRUE # Where available, show which protocol and frame caused this SPRT stream to be created # TRUE or FALSE (case-insensitive) #sprt.show_setup_info: TRUE # Show the DLCI field in I_OCTET messages as well as the frame that enabled/disabled the DLCI # TRUE or FALSE (case-insensitive) #sprt.show_dlci_info: TRUE # Whether the SRVLOC dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #srvloc.desegment_tcp: TRUE # SSCOP payload (dissector to call on SSCOP payload) # One of: Data (no further dissection), Q.2931, SSCF-NNI (MTP3-b), ALCAP, NBAP # (case-insensitive). #sscop.payload: Q.2931 # Whether the SSH dissector should reassemble SSH buffers spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #ssh.desegment_buffers: TRUE # Whether the STANAG 5066 DTS Layer dissector should reassemble DPDUs spanning multiple TCP segments # TRUE or FALSE (case-insensitive) #s5066dts.proto_desegment: TRUE # Whether the S5066 SIS dissector should reassemble PDUs spanning multiple TCP segments. The default is to use reassembly. # TRUE or FALSE (case-insensitive) #s5066sis.desegment_pdus: TRUE # Whether the S5066 SIS dissector should dissect this edition of the STANAG. This edition was never formally approved and is very rare. The common edition is edition 1.2. # TRUE or FALSE (case-insensitive) #s5066sis.edition_one: FALSE # Whether the StarTeam dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #starteam.desegment: TRUE # Steam IHS Discovery UDP port if other than the default # A decimal number #steam_ihs_discovery.udp.port: 27036 # Whether the BPDU dissector should use 802.1t System ID Extensions when dissecting the Bridge Identifier # TRUE or FALSE (case-insensitive) #stp.use_system_id_extension: TRUE # Reassembles greater than MTU sized STT packets broken into segments on transmit # TRUE or FALSE (case-insensitive) #stt.reassemble: TRUE # Whether to validate the STT checksum or not. # TRUE or FALSE (case-insensitive) #stt.check_checksum: FALSE # Version used by Wireshark # One of: Internet Draft version 08, RFC 3868 # (case-insensitive). #sua.version: RFC 3868 # Set the source and destination addresses to the PC or GT digits, depending on the routing indicator. This may affect TCAP's ability to recognize which messages belong to which TCAP session. # TRUE or FALSE (case-insensitive) #sua.set_addresses: FALSE # No description # TRUE or FALSE (case-insensitive) #sv.decode_data_as_phsmeas: FALSE # Whether the T.38 dissector should decode using the Pre-Corrigendum T.38 ASN.1 specification (1998). # TRUE or FALSE (case-insensitive) #t38.use_pre_corrigendum_asn1_specification: TRUE # Whether a UDP packet that looks like RTP version 2 packet will be dissected as RTP packet or T.38 packet. If enabled there is a risk that T.38 UDPTL packets with sequence number higher than 32767 may be dissected as RTP. # TRUE or FALSE (case-insensitive) #t38.dissect_possible_rtpv2_packets_as_rtp: FALSE # Whether the dissector should reassemble T.38 PDUs spanning multiple TCP segments when TPKT is used over TCP. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #t38.reassembly: TRUE # Whether T.38 is used with TPKT for TCP # One of: Never, Always, Maybe # (case-insensitive). #t38.tpkt_usage: Maybe # Where available, show which protocol and frame caused this T.38 stream to be created # TRUE or FALSE (case-insensitive) #t38.show_setup_info: TRUE # Whether the TACACS+ dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #tacplus.desegment: TRUE # TACACS+ Encryption Key # A string #tacplus.key: # Whether the TALI dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #tali.reassemble: TRUE # SCCP (and SUA) SSNs to decode as TCAP # A string denoting an positive integer range (e.g., "1-20,30-40") #tcap.ssn: # Activate the analyse for Response Time # TRUE or FALSE (case-insensitive) #tcap.srt: FALSE # Statistics for Response Time # TRUE or FALSE (case-insensitive) #tcap.persistentsrt: FALSE # Maximal delay for message repetition # A decimal number #tcap.repetitiontimeout: 10 # Maximal delay for message lost # A decimal number #tcap.losttimeout: 30 # Whether the TCP summary line should be shown in the protocol tree # TRUE or FALSE (case-insensitive) #tcp.summary_in_tree: TRUE # Whether to validate the TCP checksum or not. (Invalid checksums will cause reassembly, if enabled, to fail.) # TRUE or FALSE (case-insensitive) #tcp.check_checksum: FALSE # Whether subdissector can request TCP streams to be reassembled # TRUE or FALSE (case-insensitive) #tcp.desegment_tcp_streams: TRUE # Whether out-of-order segments should be buffered and reordered before passing it to a subdissector. To use this option you must also enable "Allow subdissector to reassemble TCP streams". # TRUE or FALSE (case-insensitive) #tcp.reassemble_out_of_order: FALSE # Make the TCP dissector analyze TCP sequence numbers to find and flag segment retransmissions, missing segments and RTT # TRUE or FALSE (case-insensitive) #tcp.analyze_sequence_numbers: TRUE # Make the TCP dissector use relative sequence numbers instead of absolute ones. To use this option you must also enable "Analyze TCP sequence numbers". # TRUE or FALSE (case-insensitive) #tcp.relative_sequence_numbers: TRUE # Make the TCP dissector use this scaling factor for streams where the signalled scaling factor is not visible in the capture # One of: Not known, 0 (no scaling), 1 (multiply by 2), 2 (multiply by 4), 3 (multiply by 8), 4 (multiply by 16), 5 (multiply by 32), 6 (multiply by 64), 7 (multiply by 128), 8 (multiply by 256), 9 (multiply by 512), 10 (multiply by 1024), 11 (multiply by 2048), 12 (multiply by 4096), 13 (multiply by 8192), 14 (multiply by 16384) # (case-insensitive). #tcp.default_window_scaling: Not known # Make the TCP dissector track the number on un-ACKed bytes of data are in flight per packet. To use this option you must also enable "Analyze TCP sequence numbers". This takes a lot of memory but allows you to track how much data are in flight at a time and graphing it in io-graphs # TRUE or FALSE (case-insensitive) #tcp.track_bytes_in_flight: TRUE # Calculate timestamps relative to the first frame and the previous frame in the tcp conversation # TRUE or FALSE (case-insensitive) #tcp.calculate_timestamps: TRUE # Try to decode a packet using an heuristic sub-dissector before using a sub-dissector registered to a specific port # TRUE or FALSE (case-insensitive) #tcp.try_heuristic_first: FALSE # Do not place the TCP Timestamps in the summary line # TRUE or FALSE (case-insensitive) #tcp.ignore_tcp_timestamps: FALSE # Do not call any subdissectors for Retransmitted or OutOfOrder segments # TRUE or FALSE (case-insensitive) #tcp.no_subdissector_on_error: TRUE # Assume TCP Experimental Options (253, 254) have a Magic Number and use it for dissection # TRUE or FALSE (case-insensitive) #tcp.dissect_experimental_options_with_magic: TRUE # Collect and store process information retrieved from IPFIX dissector # TRUE or FALSE (case-insensitive) #tcp.display_process_info_from_ipfix: FALSE # Whether the TCPROS dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #tcpros.desegment_tcpros_messages: TRUE # The TDMoE channel that contains the D-Channel. # A decimal number #tdmoe.d_channel: 24 # The TDMoD channel that contains the D-Channel. # A decimal number #tdmop.d_channel: 16 # The bitmask of channels in uncompressed TDMoP frame # A hexadecimal number #tdmop.ts_mask: 0xffffffff # The ethertype assigned to TDMoP (without IP/UDP) stream # A hexadecimal number #tdmop.ethertype: 0 # Whether the TDS dissector should reassemble TDS buffers spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #tds.desegment_buffers: TRUE # Whether the TDS dissector should defragment messages spanning multiple Netlib buffers # TRUE or FALSE (case-insensitive) #tds.defragment: TRUE # Hint as to version of TDS protocol being decoded # One of: Not Specified, TDS 4.x, TDS 5.0, TDS 7.0, TDS 7.1, TDS 7.2, TDS 7.3, TDS 7.3A, TDS 7.3B, TDS 7.4 # (case-insensitive). #tds.protocol_type: Not Specified # Hint as to whether to decode TDS protocol as little-endian or big-endian. (TDS7/8 always decoded as little-endian) # One of: Little Endian, Big Endian # (case-insensitive). #tds.endian_type: Little Endian # Whether the captured data include carrier number # TRUE or FALSE (case-insensitive) #tetra.include_carrier_number: TRUE # 32-bit sequence counter for hash # A string #thread.thr_seq_ctr: # Set if the PAN ID should be used as the first two octets of the master key (PAN ID LSB), (PAN ID MSB), Key[2]... # TRUE or FALSE (case-insensitive) #thread.thr_use_pan_id_in_key: FALSE # Set if the Thread sequence counter should be automatically acquired from Key ID mode 2 MLE messages. # TRUE or FALSE (case-insensitive) #thread.thr_auto_acq_thr_seq_ctr: TRUE # Thrift TLS Port # A decimal number #thrift.tls.port: 0 # Try the default RSA key in use by nearly all Open Tibia servers # TRUE or FALSE (case-insensitive) #tibia.try_otserv_key: TRUE # Shows active character for every packet # TRUE or FALSE (case-insensitive) #tibia.show_char_name: TRUE # Shows account name/password or session key for every packet # TRUE or FALSE (case-insensitive) #tibia.show_acc_info: TRUE # Shows which XTEA key was applied for a packet # TRUE or FALSE (case-insensitive) #tibia.show_xtea_key: FALSE # Only decrypt packets and dissect login packets. Pass game commands to the data dissector # TRUE or FALSE (case-insensitive) #tibia.dissect_game_commands: FALSE # Whether the Tibia dissector should reassemble packets spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #tibia.reassemble_tcp_segments: TRUE # Time display type # One of: UTC, Local # (case-insensitive). #time.display_time_type: Local # Whether TIPCv1 SEGMENTATION_MANAGER datagrams should be reassembled # TRUE or FALSE (case-insensitive) #tipc.defragment: TRUE # Whether to try to dissect TIPC data or not # TRUE or FALSE (case-insensitive) #tipc.dissect_tipc_data: TRUE # Try to decode a TIPCv2 packet using an heuristic sub-dissector before using a registered sub-dissector # TRUE or FALSE (case-insensitive) #tipc.try_heuristic_first: FALSE # TIPC 1.7 removes/adds fields (not) available in TIPC 1.5/1.6 while keeping the version number 2 in the packages. "ALL" shows all fields that were ever used in both versions. # One of: ALL, TIPC 1.5/1.6, TIPC 1.7 # (case-insensitive). #tipc.handle_v2_as: ALL # Whether the TIPC-over-TCP dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #tipc.desegment: TRUE # Redirect TLS debug to the file specified. Leave empty to disable debugging or use "-" to redirect output to stderr. # A path to a file #tls.debug_file: # Whether the TLS dissector should reassemble TLS records spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #tls.desegment_ssl_records: TRUE # Whether the TLS dissector should reassemble TLS Application Data spanning multiple TLS records. # TRUE or FALSE (case-insensitive) #tls.desegment_ssl_application_data: TRUE # For troubleshooting ignore the mac check result and decrypt also if the Message Authentication Code (MAC) fails. # TRUE or FALSE (case-insensitive) #tls.ignore_ssl_mac_failed: FALSE # Pre-Shared-Key as HEX string. Should be 0 to 16 bytes. # A string #tls.psk: # The name of a file which contains a list of # (pre-)master secrets in one of the following formats: # # RSA # RSA Session-ID: Master-Key: # CLIENT_RANDOM # PMS_CLIENT_RANDOM # # Where: # = First 8 bytes of the Encrypted PMS # = The Pre-Master-Secret (PMS) used to derive the MS # = The SSL Session ID # = The Master-Secret (MS) # = The Client's random number from the ClientHello message # # (All fields are in hex notation) # A path to a file #tls.keylog_file: # Whether the TNS dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #tns.desegment_tns_messages: TRUE # Whether Linux mangling of the link-layer header should be checked for and worked around # TRUE or FALSE (case-insensitive) #tr.fix_linux_botches: FALSE # Whether the TPKT dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #tpkt.desegment: TRUE # TCP ports to be decoded as TPKT (default: 102) # A string denoting an positive integer range (e.g., "1-20,30-40") #tpkt.tcp.ports: 102 # Whether to load the Database or not; not loading the DB disables the protocol; Wireshark has to be restarted for the setting to take effect. # TRUE or FALSE (case-insensitive) #tpncp.load_db: FALSE # No description # A decimal number #tpncp.tcp.trunkpack_port: 2424 # No description # A decimal number #tpncp.udp.trunkpack_port: 2424 # Position of the capture unit that produced this trace. This setting affects the way TRANSUM handles TCP Retransmissions. See the manual for details. # One of: Client, Intermediate, Service # (case-insensitive). #transum.capture_position: Client # Set this to match to the TCP subdissector reassembly setting # TRUE or FALSE (case-insensitive) #transum.reassembly: TRUE # Add and remove ports numbers separated by commas # Ranges are supported e.g. 25,80,2000-3000,5432 # A string denoting an positive integer range (e.g., "1-20,30-40") #transum.tcp_port_ranges: 25,80,443,1433 # Add and remove ports numbers separated by commas # Ranges are supported e.g. 123,137-139,520-521,2049 # A string denoting an positive integer range (e.g., "1-20,30-40") #transum.udp_port_ranges: 137-139 # Set this to discard any packet in the direction client to service, # with a 1-byte payload of 0x00 and the ACK flag set # TRUE or FALSE (case-insensitive) #transum.orphan_ka_discard: FALSE # RTE data will be added to the first request packet # TRUE or FALSE (case-insensitive) #transum.rte_on_first_req: FALSE # RTE data will be added to the last request packet # TRUE or FALSE (case-insensitive) #transum.rte_on_last_req: TRUE # RTE data will be added to the first response packet # TRUE or FALSE (case-insensitive) #transum.rte_on_first_rsp: FALSE # RTE data will be added to the last response packet # TRUE or FALSE (case-insensitive) #transum.rte_on_last_rsp: FALSE # Set this only to troubleshoot problems # TRUE or FALSE (case-insensitive) #transum.debug_enabled: FALSE # Critical Traffic Mask (base hex) # A hexadecimal number #tte.ct_mask_value: 0 # Critical Traffic Marker (base hex) # A hexadecimal number #tte.ct_marker_value: 0xffffffff # Setup RTP/RTCP conversations when parsing Start/Record RTP messages # TRUE or FALSE (case-insensitive) #ua3g.setup_conversations: TRUE # NOE SIP Protocol # TRUE or FALSE (case-insensitive) #uasip.noesip: FALSE # IPv4 address of the proxy (Invalid values will be ignored) # A string #uasip.proxy_ipaddr: # IPv4 (or IPv6) address of the call server. (Used only in case of identical source and destination ports) # A string #uaudp.system_ip: # Whether the UCP dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #ucp.desegment_ucp_messages: TRUE # Whether the UDP summary line should be shown in the protocol tree # TRUE or FALSE (case-insensitive) #udp.summary_in_tree: TRUE # Try to decode a packet using an heuristic sub-dissector before using a sub-dissector registered to a specific port # TRUE or FALSE (case-insensitive) #udp.try_heuristic_first: FALSE # Whether to validate the UDP checksum # TRUE or FALSE (case-insensitive) #udp.check_checksum: FALSE # Collect process flow information from IPFIX # TRUE or FALSE (case-insensitive) #udp.process_info: FALSE # Calculate timestamps relative to the first frame and the previous frame in the udp conversation # TRUE or FALSE (case-insensitive) #udp.calculate_timestamps: TRUE # Ignore an invalid checksum coverage field and continue dissection # TRUE or FALSE (case-insensitive) #udplite.ignore_checksum_coverage: TRUE # Whether to validate the UDP-Lite checksum # TRUE or FALSE (case-insensitive) #udplite.check_checksum: FALSE # Calculate timestamps relative to the first frame and the previous frame in the udp-lite conversation # TRUE or FALSE (case-insensitive) #udplite.calculate_timestamps: TRUE # Whether the ULP dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #ulp.desegment_ulp_messages: TRUE # Whether the UMA dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #uma.desegment_ucp_messages: TRUE # Try to decode a packet using a heuristic sub-dissector before attempting to dissect the packet using the "usb.bulk", "usb.interrupt" or "usb.control" dissector tables. # TRUE or FALSE (case-insensitive) #usb.try_heuristics: TRUE # Activate workaround for weird Ettus UHD header offset on data packets # TRUE or FALSE (case-insensitive) #vrt.ettus_uhd_header_format: FALSE # Whether the vlan summary line should be shown in the protocol tree # TRUE or FALSE (case-insensitive) #vlan.summary_in_tree: TRUE # The (hexadecimal) Ethertype used to indicate 802.1QinQ VLAN in VLAN tunneling. # A hexadecimal number #vlan.qinq_ethertype: 0x9100 # IEEE 802.1Q specification version used (802.1Q-1998 uses 802.1D-2004 for PRI values) # One of: IEEE 802.1Q-1998, IEEE 802.1Q-2005, IEEE 802.1Q-2011 # (case-insensitive). #vlan.version: IEEE 802.1Q-2011 # Number of priorities supported, and number of those drop eligible (not used for 802.1Q-1998) # One of: 8 Priorities, 0 Drop Eligible, 7 Priorities, 1 Drop Eligible, 6 Priorities, 2 Drop Eligible, 5 Priorities, 3 Drop Eligible # (case-insensitive). #vlan.priority_drop: 8 Priorities, 0 Drop Eligible # Whether the VNC dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #vnc.desegment: TRUE # Dynamic payload types which will be interpreted as vp8; Values must be in the range 96 - 127 # A string denoting an positive integer range (e.g., "1-20,30-40") #vp8.dynamic.payload.type: # There is some ambiguity on how to calculate V3 checksumsAs in V3 will use a pseudo header(which may only be implemented for IPv6 by some manufacturers) # TRUE or FALSE (case-insensitive) #vrrp.v3_checksum_as_in_v2: FALSE # Enable this preference if you want to view the WBXML tokens without the representation in a media type (e.g., WML). Tokens will show up as Tag_0x12, attrStart_0x08 or attrValue_0x0B for example. # TRUE or FALSE (case-insensitive) #wbxml.skip_wbxml_token_mapping: FALSE # Enable this preference if you want to skip the parsing of the WBXML tokens that constitute the body of the WBXML document. Only the WBXML header will be dissected (and visualized) then. # TRUE or FALSE (case-insensitive) #wbxml.disable_wbxml_token_parsing: FALSE # Select dissector for websocket text # One of: No subdissection, Line based text, As json, As SIP # (case-insensitive). #websocket.text_type: No subdissection # No description # TRUE or FALSE (case-insensitive) #websocket.decompress: TRUE # Set the maximum Basic CID used in the Wimax decoder (if other than the default of 320). Note: The maximum Primary CID is double the maximum Basic CID. # A decimal number #wmx.basic_cid_max: 320 # Set to TRUE to use the Corrigendum 2 version of Wimax message decoding. Set to FALSE to use the 802.16e-2005 version. # TRUE or FALSE (case-insensitive) #wmx.corrigendum_2_version: FALSE # Show transaction ID direction bit separately from the rest of the transaction ID field. # TRUE or FALSE (case-insensitive) #wimaxasncp.show_transaction_id_d_bit: FALSE # Print debug output to the console. # TRUE or FALSE (case-insensitive) #wimaxasncp.debug_enabled: FALSE # Version of the NWG that the R6 protocol complies with # One of: R1.0 v1.0.0, R1.0 v1.2.0, R1.0 v1.2.1 # (case-insensitive). #wimaxasncp.nwg_version: R1.0 v1.2.1 # Whether the WINS-Replication dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #winsrepl.reassemble: TRUE # Whether the IP dissector should dissect decrypted transport data. # TRUE or FALSE (case-insensitive) #wg.dissect_packet: TRUE # The path to the file which contains a list of secrets in the following format: # " = " (without quotes, leading spaces and spaces around '=' are ignored). # is one of: LOCAL_STATIC_PRIVATE_KEY, REMOTE_STATIC_PUBLIC_KEY, LOCAL_EPHEMERAL_PRIVATE_KEY or PRESHARED_KEY. # A path to a file #wg.keylog_file: # Whether the wow dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #wow.desegment: TRUE # If CALL REQUEST not seen or didn't specify protocol, dissect as QLLC/SNA # TRUE or FALSE (case-insensitive) #x25.payload_is_qllc_sna: FALSE # If CALL REQUEST has no data, assume the protocol handled is COTP # TRUE or FALSE (case-insensitive) #x25.call_request_nodata_is_cotp: FALSE # If CALL REQUEST not seen or didn't specify protocol, check user data before checking heuristic dissectors # TRUE or FALSE (case-insensitive) #x25.payload_check_data: FALSE # Reassemble fragmented X.25 packets # TRUE or FALSE (case-insensitive) #x25.reassemble: TRUE # Whether the X11 dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #x11.desegment: TRUE # Set the SCTP port for X2AP messages # A decimal number #x2ap.sctp.port: 36422 # Select whether RRC Context should be dissected as legacy LTE or NB-IOT # One of: LTE, NB-IoT # (case-insensitive). #x2ap.dissect_rrc_context_as: LTE # Try to recognize XML encoded in Unicode (UCS-2BE) # TRUE or FALSE (case-insensitive) #xml.heuristic_unicode: FALSE # Whether the XMPP dissector should reassemble messages. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings # TRUE or FALSE (case-insensitive) #xmpp.desegment: TRUE # Set the SCTP port for XnAP messages # A decimal number #xnap.sctp.port: 38422 # Select whether target NG-RAN container should be decoded automatically (based on Xn Setup procedure) or manually # One of: automatic, gNB, ng-eNB # (case-insensitive). #xnap.dissect_target_ng_ran_container_as: automatic # Whether the X.25-over-TCP dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings # TRUE or FALSE (case-insensitive) #xot.desegment: TRUE # Whether the X.25-over-TCP dissector should reassemble all X.25 packets before calling the X25 dissector. If the TCP packets arrive out-of-order, the X.25 reassembly can otherwise fail. To use this option, you should also enable "Reassemble X.25-over-TCP messages spanning multiple TCP segments", "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings and "Reassemble fragmented X.25 packets" in the X.25 protocol settings. # TRUE or FALSE (case-insensitive) #xot.x25_desegment: FALSE # Whether the YAMI dissector should reassemble messages spanning multiple TCP segments.To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #yami.desegment: TRUE # Whether the YMSG dissector should reassemble messages spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #ymsg.desegment: TRUE # Whether the Z39.50 dissector should reassemble TDS buffers spanning multiple TCP segments. To use this option, you must also enable "Allow subdissectors to reassemble TCP streams" in the TCP protocol settings. # TRUE or FALSE (case-insensitive) #z3950.desegment_buffers: TRUE # Specifies the security level to use in the # decryption process. This value is ignored # for ZigBee 2004 and unsecured networks. # One of: No Security, No Encryption, 32-bit Integrity Protection, No Encryption, 64-bit Integrity Protection, No Encryption, 128-bit Integrity Protection, AES-128 Encryption, No Integrity Protection, AES-128 Encryption, 32-bit Integrity Protection, AES-128 Encryption, 64-bit Integrity Protection, AES-128 Encryption, 128-bit Integrity Protection # (case-insensitive). #zbee_nwk.seclevel: AES-128 Encryption, 32-bit Integrity Protection # Specifies the ZigBee Smart Energy version used when dissecting ZigBee APS messages within the Smart Energy Profile # One of: SE 1.1b, SE 1.2, SE 1.2a, SE 1.2b, SE 1.4 # (case-insensitive). #zbee_aps.zbeeseversion: SE 1.4 ####### Statistics ######## # Determines time between tap updates # A decimal number #statistics.update_interval: 3000 # If enabled burst rates will be calcuted for statistics that use the stats_tree system. Burst rates are calculated over a much shorter time interval than the rate column. # TRUE or FALSE (case-insensitive) #statistics.st_enable_burstinfo: TRUE # If selected the stats_tree statistics nodes will show the count of events within the burst window instead of a burst rate. Burst rate is calculated as number of events within burst window divided by the burst windown length. # TRUE or FALSE (case-insensitive) #statistics.st_burst_showcount: FALSE # Sets the duration of the time interval into which events are grouped when calculating the burst rate. Higher resolution (smaller number) increases processing overhead. # A decimal number #statistics.st_burst_resolution: 5 # Sets the duration of the sliding window during which the burst rate is measured. Longer window relative to burst rate resolution increases processing overhead. Will be truncated to a multiple of burst resolution. # A decimal number #statistics.st_burst_windowlen: 100 # Sets the default column by which stats based on the stats_tree system is sorted. # One of: Node name (topic/item), Item count, Average value of the node, Minimum value of the node, Maximum value of the node, Burst rate of the node # (case-insensitive). #statistics.st_sort_defcolflag: Item count # When selected, statistics based on the stats_tree system will by default be sorted in descending order. # TRUE or FALSE (case-insensitive) #statistics.st_sort_defdescending: TRUE # When selected, the item/node names of statistics based on the stats_tree system will be sorted taking case into account. Else the case of the name will be ignored. # TRUE or FALSE (case-insensitive) #statistics.st_sort_casesensitve: TRUE # When selected, the stats_tree nodes representing a range of values (0-49, 50-100, etc.) will always be sorted by name (the range of the node). Else range nodes are sorted by the same column as the rest of the tree. # TRUE or FALSE (case-insensitive) #statistics.st_sort_rng_nameonly: TRUE # When selected, the stats_tree nodes representing a range of values (0-49, 50-100, etc.) will always be sorted ascending; else it follows the sort direction of the tree. Only effective if "Always sort 'range' nodes by name" is also selected. # TRUE or FALSE (case-insensitive) #statistics.st_sort_rng_fixorder: TRUE # When selected, the full name (including menu path) of the stats_tree plug-in is show in windows. If cleared the plug-in name is shown without menu path (only the part of the name after last '/' character.) # TRUE or FALSE (case-insensitive) #statistics.st_sort_showfullname: FALSE ` for _, win := range []bool{false, true} { var td string if win { td = strings.ReplaceAll(inp1, "\n", "\r\n") } else { td = inp1 } parsed, err := ParseReader("", strings.NewReader(td)) if err != nil { log.Fatal(err) } fmt.Printf("Result is %v of type %T\n", parsed, parsed) cfg := parsed.(*Config) assert.Contains(t, cfg.Lists, "gui.column.format") assert.Contains(t, cfg.Lists, "gui.column.hidden") assert.Contains(t, cfg.Strings, "gui.qt.font_name") assert.Equal(t, 20, len(cfg.Lists["gui.column.format"])) assert.Equal(t, "\"gcla1\"", cfg.Lists["gui.column.format"][14]) assert.Equal(t, "\"%Yut\"", cfg.Lists["gui.column.format"][15]) assert.Equal(t, 2, len(cfg.Lists["gui.column.hidden"])) assert.Equal(t, "%Yut", cfg.Lists["gui.column.hidden"][0]) } } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.4.0/pkg/streams/000077500000000000000000000000001426312004500157515ustar00rootroot00000000000000termshark-2.4.0/pkg/streams/follow.go000066400000000000000000001562161426312004500176150ustar00rootroot00000000000000// Code generated by pigeon; DO NOT EDIT. package streams import ( "bytes" "errors" "fmt" "io" "io/ioutil" "math" "os" "sort" "strconv" "strings" "sync" "unicode" "unicode/utf8" ) var g = &grammar{ rules: []*rule{ { name: "Input", pos: position{line: 45, col: 1, offset: 1065}, expr: &actionExpr{ pos: position{line: 45, col: 10, offset: 1074}, run: (*parser).callonInput1, expr: &seqExpr{ pos: position{line: 45, col: 10, offset: 1074}, exprs: []interface{}{ &ruleRefExpr{ pos: position{line: 45, col: 10, offset: 1074}, name: "_", }, &ruleRefExpr{ pos: position{line: 45, col: 12, offset: 1076}, name: "Separator", }, &labeledExpr{ pos: position{line: 45, col: 22, offset: 1086}, label: "follow", expr: &ruleRefExpr{ pos: position{line: 45, col: 29, offset: 1093}, name: "Follow", }, }, &ruleRefExpr{ pos: position{line: 45, col: 36, offset: 1100}, name: "Separator", }, &ruleRefExpr{ pos: position{line: 45, col: 46, offset: 1110}, name: "EOF", }, }, }, }, }, { name: "Separator", pos: position{line: 49, col: 1, offset: 1141}, expr: &seqExpr{ pos: position{line: 49, col: 14, offset: 1154}, exprs: []interface{}{ &oneOrMoreExpr{ pos: position{line: 49, col: 14, offset: 1154}, expr: &litMatcher{ pos: position{line: 49, col: 14, offset: 1154}, val: "=", ignoreCase: false, }, }, &ruleRefExpr{ pos: position{line: 49, col: 19, offset: 1159}, name: "__", }, }, }, }, { name: "DataLines", pos: position{line: 55, col: 1, offset: 1342}, expr: &actionExpr{ pos: position{line: 55, col: 14, offset: 1355}, run: (*parser).callonDataLines1, expr: &labeledExpr{ pos: position{line: 55, col: 14, offset: 1355}, label: "dl", expr: &choiceExpr{ pos: position{line: 55, col: 19, offset: 1360}, alternatives: []interface{}{ &ruleRefExpr{ pos: position{line: 55, col: 19, offset: 1360}, name: "DataLineBackComplete", }, &ruleRefExpr{ pos: position{line: 55, col: 42, offset: 1383}, name: "DataLineForwardComplete", }, }, }, }, }, }, { name: "ThrowUndefLabel", pos: position{line: 68, col: 1, offset: 1618}, expr: &throwExpr{ pos: position{line: 68, col: 19, offset: 1636}, label: "undeflabel", }, }, { name: "DataLineBackComplete", pos: position{line: 70, col: 1, offset: 1651}, expr: &actionExpr{ pos: position{line: 70, col: 25, offset: 1675}, run: (*parser).callonDataLineBackComplete1, expr: &labeledExpr{ pos: position{line: 70, col: 25, offset: 1675}, label: "dl", expr: &ruleRefExpr{ pos: position{line: 70, col: 28, offset: 1678}, name: "DataLineBack", }, }, }, }, { name: "DataLineForwardComplete", pos: position{line: 81, col: 1, offset: 1872}, expr: &actionExpr{ pos: position{line: 81, col: 28, offset: 1899}, run: (*parser).callonDataLineForwardComplete1, expr: &labeledExpr{ pos: position{line: 81, col: 28, offset: 1899}, label: "dl", expr: &ruleRefExpr{ pos: position{line: 81, col: 31, offset: 1902}, name: "DataLineForward", }, }, }, }, { name: "DataLineBack", pos: position{line: 95, col: 1, offset: 2177}, expr: &actionExpr{ pos: position{line: 95, col: 17, offset: 2193}, run: (*parser).callonDataLineBack1, expr: &seqExpr{ pos: position{line: 95, col: 17, offset: 2193}, exprs: []interface{}{ &charClassMatcher{ pos: position{line: 95, col: 17, offset: 2193}, val: "[\\t]", chars: []rune{'\t'}, ignoreCase: false, inverted: false, }, &labeledExpr{ pos: position{line: 95, col: 22, offset: 2198}, label: "data", expr: &ruleRefExpr{ pos: position{line: 95, col: 27, offset: 2203}, name: "Data", }, }, }, }, }, }, { name: "DataLineForward", pos: position{line: 102, col: 1, offset: 2309}, expr: &actionExpr{ pos: position{line: 102, col: 20, offset: 2328}, run: (*parser).callonDataLineForward1, expr: &labeledExpr{ pos: position{line: 102, col: 20, offset: 2328}, label: "data", expr: &ruleRefExpr{ pos: position{line: 102, col: 25, offset: 2333}, name: "Data", }, }, }, }, { name: "Data", pos: position{line: 106, col: 1, offset: 2400}, expr: &actionExpr{ pos: position{line: 106, col: 9, offset: 2408}, run: (*parser).callonData1, expr: &seqExpr{ pos: position{line: 106, col: 9, offset: 2408}, exprs: []interface{}{ &labeledExpr{ pos: position{line: 106, col: 9, offset: 2408}, label: "ds", expr: &oneOrMoreExpr{ pos: position{line: 106, col: 13, offset: 2412}, expr: &ruleRefExpr{ pos: position{line: 106, col: 13, offset: 2412}, name: "Datum", }, }, }, &zeroOrOneExpr{ pos: position{line: 106, col: 21, offset: 2420}, expr: &charClassMatcher{ pos: position{line: 106, col: 21, offset: 2420}, val: "[\\r]", chars: []rune{'\r'}, ignoreCase: false, inverted: false, }, }, &charClassMatcher{ pos: position{line: 106, col: 27, offset: 2426}, val: "[\\n]", chars: []rune{'\n'}, ignoreCase: false, inverted: false, }, }, }, }, }, { name: "Datum", pos: position{line: 114, col: 1, offset: 2591}, expr: &actionExpr{ pos: position{line: 114, col: 10, offset: 2600}, run: (*parser).callonDatum1, expr: &seqExpr{ pos: position{line: 114, col: 10, offset: 2600}, exprs: []interface{}{ &labeledExpr{ pos: position{line: 114, col: 10, offset: 2600}, label: "b1", expr: &charClassMatcher{ pos: position{line: 114, col: 13, offset: 2603}, val: "[0-9a-fA-F]", ranges: []rune{'0', '9', 'a', 'f', 'A', 'F'}, ignoreCase: false, inverted: false, }, }, &labeledExpr{ pos: position{line: 114, col: 25, offset: 2615}, label: "b2", expr: &charClassMatcher{ pos: position{line: 114, col: 28, offset: 2618}, val: "[0-9a-fA-F]", ranges: []rune{'0', '9', 'a', 'f', 'A', 'F'}, ignoreCase: false, inverted: false, }, }, }, }, }, }, { name: "DataIndex", pos: position{line: 121, col: 1, offset: 2821}, expr: &actionExpr{ pos: position{line: 121, col: 14, offset: 2834}, run: (*parser).callonDataIndex1, expr: &seqExpr{ pos: position{line: 121, col: 14, offset: 2834}, exprs: []interface{}{ &labeledExpr{ pos: position{line: 121, col: 14, offset: 2834}, label: "b0", expr: &charClassMatcher{ pos: position{line: 121, col: 17, offset: 2837}, val: "[0-9a-fA-F]", ranges: []rune{'0', '9', 'a', 'f', 'A', 'F'}, ignoreCase: false, inverted: false, }, }, &labeledExpr{ pos: position{line: 121, col: 29, offset: 2849}, label: "b1", expr: &charClassMatcher{ pos: position{line: 121, col: 32, offset: 2852}, val: "[0-9a-fA-F]", ranges: []rune{'0', '9', 'a', 'f', 'A', 'F'}, ignoreCase: false, inverted: false, }, }, &labeledExpr{ pos: position{line: 121, col: 44, offset: 2864}, label: "b2", expr: &charClassMatcher{ pos: position{line: 121, col: 47, offset: 2867}, val: "[0-9a-fA-F]", ranges: []rune{'0', '9', 'a', 'f', 'A', 'F'}, ignoreCase: false, inverted: false, }, }, &labeledExpr{ pos: position{line: 121, col: 59, offset: 2879}, label: "b3", expr: &charClassMatcher{ pos: position{line: 121, col: 62, offset: 2882}, val: "[0-9a-fA-F]", ranges: []rune{'0', '9', 'a', 'f', 'A', 'F'}, ignoreCase: false, inverted: false, }, }, &labeledExpr{ pos: position{line: 121, col: 74, offset: 2894}, label: "b4", expr: &charClassMatcher{ pos: position{line: 121, col: 77, offset: 2897}, val: "[0-9a-fA-F]", ranges: []rune{'0', '9', 'a', 'f', 'A', 'F'}, ignoreCase: false, inverted: false, }, }, &labeledExpr{ pos: position{line: 121, col: 89, offset: 2909}, label: "b5", expr: &charClassMatcher{ pos: position{line: 121, col: 92, offset: 2912}, val: "[0-9a-fA-F]", ranges: []rune{'0', '9', 'a', 'f', 'A', 'F'}, ignoreCase: false, inverted: false, }, }, &labeledExpr{ pos: position{line: 121, col: 104, offset: 2924}, label: "b6", expr: &charClassMatcher{ pos: position{line: 121, col: 107, offset: 2927}, val: "[0-9a-fA-F]", ranges: []rune{'0', '9', 'a', 'f', 'A', 'F'}, ignoreCase: false, inverted: false, }, }, &labeledExpr{ pos: position{line: 121, col: 119, offset: 2939}, label: "b7", expr: &charClassMatcher{ pos: position{line: 121, col: 122, offset: 2942}, val: "[0-9a-fA-F]", ranges: []rune{'0', '9', 'a', 'f', 'A', 'F'}, ignoreCase: false, inverted: false, }, }, }, }, }, }, { name: "DataSegment", pos: position{line: 134, col: 1, offset: 3618}, expr: &actionExpr{ pos: position{line: 134, col: 16, offset: 3633}, run: (*parser).callonDataSegment1, expr: &labeledExpr{ pos: position{line: 134, col: 16, offset: 3633}, label: "ds", expr: &oneOrMoreExpr{ pos: position{line: 134, col: 19, offset: 3636}, expr: &ruleRefExpr{ pos: position{line: 134, col: 21, offset: 3638}, name: "Datum", }, }, }, }, }, { name: "Node0Clause", pos: position{line: 143, col: 1, offset: 3821}, expr: &actionExpr{ pos: position{line: 143, col: 16, offset: 3836}, run: (*parser).callonNode0Clause1, expr: &seqExpr{ pos: position{line: 143, col: 16, offset: 3836}, exprs: []interface{}{ &litMatcher{ pos: position{line: 143, col: 16, offset: 3836}, val: "Node 0:", ignoreCase: false, }, &ruleRefExpr{ pos: position{line: 143, col: 26, offset: 3846}, name: "_", }, &labeledExpr{ pos: position{line: 143, col: 28, offset: 3848}, label: "node", expr: &ruleRefExpr{ pos: position{line: 143, col: 33, offset: 3853}, name: "NodeExpr", }, }, &ruleRefExpr{ pos: position{line: 143, col: 42, offset: 3862}, name: "__nl", }, }, }, }, }, { name: "Node1Clause", pos: position{line: 148, col: 1, offset: 3935}, expr: &actionExpr{ pos: position{line: 148, col: 16, offset: 3950}, run: (*parser).callonNode1Clause1, expr: &seqExpr{ pos: position{line: 148, col: 16, offset: 3950}, exprs: []interface{}{ &litMatcher{ pos: position{line: 148, col: 16, offset: 3950}, val: "Node 1:", ignoreCase: false, }, &ruleRefExpr{ pos: position{line: 148, col: 26, offset: 3960}, name: "_", }, &labeledExpr{ pos: position{line: 148, col: 28, offset: 3962}, label: "node", expr: &ruleRefExpr{ pos: position{line: 148, col: 33, offset: 3967}, name: "NodeExpr", }, }, &ruleRefExpr{ pos: position{line: 148, col: 42, offset: 3976}, name: "__nl", }, }, }, }, }, { name: "NodeExpr", pos: position{line: 153, col: 1, offset: 4024}, expr: &actionExpr{ pos: position{line: 153, col: 13, offset: 4036}, run: (*parser).callonNodeExpr1, expr: &oneOrMoreExpr{ pos: position{line: 153, col: 13, offset: 4036}, expr: &charClassMatcher{ pos: position{line: 153, col: 13, offset: 4036}, val: "[^\\n]", chars: []rune{'\n'}, ignoreCase: false, inverted: true, }, }, }, }, { name: "FollowClause", pos: position{line: 157, col: 1, offset: 4078}, expr: &actionExpr{ pos: position{line: 157, col: 17, offset: 4094}, run: (*parser).callonFollowClause1, expr: &seqExpr{ pos: position{line: 157, col: 17, offset: 4094}, exprs: []interface{}{ &litMatcher{ pos: position{line: 157, col: 17, offset: 4094}, val: "Follow:", ignoreCase: false, }, &ruleRefExpr{ pos: position{line: 157, col: 27, offset: 4104}, name: "_", }, &labeledExpr{ pos: position{line: 157, col: 29, offset: 4106}, label: "fexpr", expr: &ruleRefExpr{ pos: position{line: 157, col: 35, offset: 4112}, name: "FollowExpr", }, }, &ruleRefExpr{ pos: position{line: 157, col: 46, offset: 4123}, name: "__nl", }, }, }, }, }, { name: "FollowExpr", pos: position{line: 161, col: 1, offset: 4154}, expr: &actionExpr{ pos: position{line: 161, col: 15, offset: 4168}, run: (*parser).callonFollowExpr1, expr: &oneOrMoreExpr{ pos: position{line: 161, col: 15, offset: 4168}, expr: &charClassMatcher{ pos: position{line: 161, col: 15, offset: 4168}, val: "[a-zA-Z,]", chars: []rune{','}, ranges: []rune{'a', 'z', 'A', 'Z'}, ignoreCase: false, inverted: false, }, }, }, }, { name: "FilterClause", pos: position{line: 165, col: 1, offset: 4214}, expr: &actionExpr{ pos: position{line: 165, col: 17, offset: 4230}, run: (*parser).callonFilterClause1, expr: &seqExpr{ pos: position{line: 165, col: 17, offset: 4230}, exprs: []interface{}{ &litMatcher{ pos: position{line: 165, col: 17, offset: 4230}, val: "Filter:", ignoreCase: false, }, &ruleRefExpr{ pos: position{line: 165, col: 27, offset: 4240}, name: "_", }, &labeledExpr{ pos: position{line: 165, col: 29, offset: 4242}, label: "fexpr", expr: &ruleRefExpr{ pos: position{line: 165, col: 35, offset: 4248}, name: "FilterExpr", }, }, &ruleRefExpr{ pos: position{line: 165, col: 46, offset: 4259}, name: "__nl", }, }, }, }, }, { name: "FilterExpr", pos: position{line: 169, col: 1, offset: 4290}, expr: &actionExpr{ pos: position{line: 169, col: 15, offset: 4304}, run: (*parser).callonFilterExpr1, expr: &oneOrMoreExpr{ pos: position{line: 169, col: 15, offset: 4304}, expr: &charClassMatcher{ pos: position{line: 169, col: 15, offset: 4304}, val: "[^\\n]", chars: []rune{'\n'}, ignoreCase: false, inverted: true, }, }, }, }, { name: "Header", pos: position{line: 178, col: 1, offset: 4451}, expr: &actionExpr{ pos: position{line: 178, col: 11, offset: 4461}, run: (*parser).callonHeader1, expr: &seqExpr{ pos: position{line: 178, col: 11, offset: 4461}, exprs: []interface{}{ &labeledExpr{ pos: position{line: 178, col: 11, offset: 4461}, label: "fc", expr: &ruleRefExpr{ pos: position{line: 178, col: 14, offset: 4464}, name: "FollowClause", }, }, &labeledExpr{ pos: position{line: 178, col: 27, offset: 4477}, label: "fic", expr: &ruleRefExpr{ pos: position{line: 178, col: 31, offset: 4481}, name: "FilterClause", }, }, &labeledExpr{ pos: position{line: 178, col: 44, offset: 4494}, label: "node0", expr: &ruleRefExpr{ pos: position{line: 178, col: 50, offset: 4500}, name: "Node0Clause", }, }, &labeledExpr{ pos: position{line: 178, col: 62, offset: 4512}, label: "node1", expr: &ruleRefExpr{ pos: position{line: 178, col: 68, offset: 4518}, name: "Node1Clause", }, }, }, }, }, }, { name: "Follow", pos: position{line: 208, col: 1, offset: 5268}, expr: &actionExpr{ pos: position{line: 208, col: 11, offset: 5278}, run: (*parser).callonFollow1, expr: &seqExpr{ pos: position{line: 208, col: 11, offset: 5278}, exprs: []interface{}{ &labeledExpr{ pos: position{line: 208, col: 11, offset: 5278}, label: "hdr", expr: &ruleRefExpr{ pos: position{line: 208, col: 15, offset: 5282}, name: "Header", }, }, &labeledExpr{ pos: position{line: 208, col: 22, offset: 5289}, label: "data", expr: &zeroOrMoreExpr{ pos: position{line: 208, col: 27, offset: 5294}, expr: &ruleRefExpr{ pos: position{line: 208, col: 27, offset: 5294}, name: "DataLines", }, }, }, }, }, }, }, { name: "_", displayName: "\"whitespace\"", pos: position{line: 219, col: 1, offset: 5544}, expr: &zeroOrMoreExpr{ pos: position{line: 219, col: 19, offset: 5562}, expr: &charClassMatcher{ pos: position{line: 219, col: 19, offset: 5562}, val: "[ \\n\\t\\r]", chars: []rune{' ', '\n', '\t', '\r'}, ignoreCase: false, inverted: false, }, }, }, { name: "__", displayName: "\"mandatory whitespace\"", pos: position{line: 221, col: 1, offset: 5574}, expr: &oneOrMoreExpr{ pos: position{line: 221, col: 30, offset: 5603}, expr: &charClassMatcher{ pos: position{line: 221, col: 30, offset: 5603}, val: "[ \\n\\t\\r]", chars: []rune{' ', '\n', '\t', '\r'}, ignoreCase: false, inverted: false, }, }, }, { name: "__nl", displayName: "\"whitespace and newline\"", pos: position{line: 223, col: 1, offset: 5615}, expr: &seqExpr{ pos: position{line: 223, col: 34, offset: 5648}, exprs: []interface{}{ &zeroOrMoreExpr{ pos: position{line: 223, col: 34, offset: 5648}, expr: &charClassMatcher{ pos: position{line: 223, col: 34, offset: 5648}, val: "[ \\t\\r]", chars: []rune{' ', '\t', '\r'}, ignoreCase: false, inverted: false, }, }, &charClassMatcher{ pos: position{line: 223, col: 43, offset: 5657}, val: "[\\n]", chars: []rune{'\n'}, ignoreCase: false, inverted: false, }, }, }, }, { name: "EOF", pos: position{line: 225, col: 1, offset: 5663}, expr: ¬Expr{ pos: position{line: 225, col: 8, offset: 5670}, expr: &anyMatcher{ line: 225, col: 9, offset: 5671, }, }, }, }, } func (c *current) onInput1(follow interface{}) (interface{}, error) { return follow, nil } func (p *parser) callonInput1() (interface{}, error) { stack := p.vstack[len(p.vstack)-1] _ = stack return p.cur.onInput1(stack["follow"]) } func (c *current) onDataLines1(dl interface{}) (interface{}, error) { if cb, ok := c.globalStore["callbacks"]; ok { if cb, ok := cb.(IOnStreamChunk); ok { cb.OnStreamChunk(dl.(Bytes)) } } return dl, nil } func (p *parser) callonDataLines1() (interface{}, error) { stack := p.vstack[len(p.vstack)-1] _ = stack return p.cur.onDataLines1(stack["dl"]) } func (c *current) onDataLineBackComplete1(dl interface{}) (interface{}, error) { if ctx, ok := c.globalStore["context"]; ok { ctx := ctx.(parseContext) if ctx.Err() != nil { panic(StreamParseError{}) } } return dl, nil } func (p *parser) callonDataLineBackComplete1() (interface{}, error) { stack := p.vstack[len(p.vstack)-1] _ = stack return p.cur.onDataLineBackComplete1(stack["dl"]) } func (c *current) onDataLineForwardComplete1(dl interface{}) (interface{}, error) { if ctx, ok := c.globalStore["context"]; ok { ctx := ctx.(parseContext) if ctx.Err() != nil { panic(StreamParseError{}) } } return dl, nil } func (p *parser) callonDataLineForwardComplete1() (interface{}, error) { stack := p.vstack[len(p.vstack)-1] _ = stack return p.cur.onDataLineForwardComplete1(stack["dl"]) } func (c *current) onDataLineBack1(data interface{}) (interface{}, error) { return Bytes{Dirn: Server, Data: data.([]byte)}, nil } func (p *parser) callonDataLineBack1() (interface{}, error) { stack := p.vstack[len(p.vstack)-1] _ = stack return p.cur.onDataLineBack1(stack["data"]) } func (c *current) onDataLineForward1(data interface{}) (interface{}, error) { return Bytes{Dirn: Client, Data: data.([]byte)}, nil } func (p *parser) callonDataLineForward1() (interface{}, error) { stack := p.vstack[len(p.vstack)-1] _ = stack return p.cur.onDataLineForward1(stack["data"]) } func (c *current) onData1(ds interface{}) (interface{}, error) { data := make([]byte, 0, len(ds.([]interface{}))) for _, l := range ds.([]interface{}) { data = append(data, l.(byte)) } return data, nil } func (p *parser) callonData1() (interface{}, error) { stack := p.vstack[len(p.vstack)-1] _ = stack return p.cur.onData1(stack["ds"]) } func (c *current) onDatum1(b1, b2 interface{}) (interface{}, error) { byte1, _ := strconv.ParseUint(string(b1.([]byte)[0]), 16, 4) byte2, _ := strconv.ParseUint(string(b2.([]byte)[0]), 16, 4) return byte(byte1<<4 | byte2), nil } func (p *parser) callonDatum1() (interface{}, error) { stack := p.vstack[len(p.vstack)-1] _ = stack return p.cur.onDatum1(stack["b1"], stack["b2"]) } func (c *current) onDataIndex1(b0, b1, b2, b3, b4, b5, b6, b7 interface{}) (interface{}, error) { byte0, _ := strconv.ParseUint(string(b0.([]byte)[0]), 16, 4) byte1, _ := strconv.ParseUint(string(b1.([]byte)[0]), 16, 4) byte2, _ := strconv.ParseUint(string(b2.([]byte)[0]), 16, 4) byte3, _ := strconv.ParseUint(string(b3.([]byte)[0]), 16, 4) byte4, _ := strconv.ParseUint(string(b4.([]byte)[0]), 16, 4) byte5, _ := strconv.ParseUint(string(b5.([]byte)[0]), 16, 4) byte6, _ := strconv.ParseUint(string(b6.([]byte)[0]), 16, 4) byte7, _ := strconv.ParseUint(string(b7.([]byte)[0]), 16, 4) return uint32(byte0<<28 | byte1<<24 | byte2<<20 | byte3<<16 | byte4<<12 | byte5<<8 | byte6<<4 | byte7), nil } func (p *parser) callonDataIndex1() (interface{}, error) { stack := p.vstack[len(p.vstack)-1] _ = stack return p.cur.onDataIndex1(stack["b0"], stack["b1"], stack["b2"], stack["b3"], stack["b4"], stack["b5"], stack["b6"], stack["b7"]) } func (c *current) onDataSegment1(ds interface{}) (interface{}, error) { res := make([]byte, 0) for _, d := range ds.([]interface{}) { res = append(res, d.(byte)) } return res, nil } func (p *parser) callonDataSegment1() (interface{}, error) { stack := p.vstack[len(p.vstack)-1] _ = stack return p.cur.onDataSegment1(stack["ds"]) } func (c *current) onNode0Clause1(node interface{}) (interface{}, error) { return node, nil } func (p *parser) callonNode0Clause1() (interface{}, error) { stack := p.vstack[len(p.vstack)-1] _ = stack return p.cur.onNode0Clause1(stack["node"]) } func (c *current) onNode1Clause1(node interface{}) (interface{}, error) { return node, nil } func (p *parser) callonNode1Clause1() (interface{}, error) { stack := p.vstack[len(p.vstack)-1] _ = stack return p.cur.onNode1Clause1(stack["node"]) } func (c *current) onNodeExpr1() (interface{}, error) { return string(c.text), nil } func (p *parser) callonNodeExpr1() (interface{}, error) { stack := p.vstack[len(p.vstack)-1] _ = stack return p.cur.onNodeExpr1() } func (c *current) onFollowClause1(fexpr interface{}) (interface{}, error) { return fexpr, nil } func (p *parser) callonFollowClause1() (interface{}, error) { stack := p.vstack[len(p.vstack)-1] _ = stack return p.cur.onFollowClause1(stack["fexpr"]) } func (c *current) onFollowExpr1() (interface{}, error) { return string(c.text), nil } func (p *parser) callonFollowExpr1() (interface{}, error) { stack := p.vstack[len(p.vstack)-1] _ = stack return p.cur.onFollowExpr1() } func (c *current) onFilterClause1(fexpr interface{}) (interface{}, error) { return fexpr, nil } func (p *parser) callonFilterClause1() (interface{}, error) { stack := p.vstack[len(p.vstack)-1] _ = stack return p.cur.onFilterClause1(stack["fexpr"]) } func (c *current) onFilterExpr1() (interface{}, error) { return string(c.text), nil } func (p *parser) callonFilterExpr1() (interface{}, error) { stack := p.vstack[len(p.vstack)-1] _ = stack return p.cur.onFilterExpr1() } func (c *current) onHeader1(fc, fic, node0, node1 interface{}) (interface{}, error) { fh := FollowHeader{ Follow: fc.(string), Filter: fic.(string), Node0: node0.(string), Node1: node1.(string), } if cb, ok := c.globalStore["callbacks"]; ok { if cb, ok := cb.(IOnStreamHeader); ok { cb.OnStreamHeader(fh) } } return fh, nil } func (p *parser) callonHeader1() (interface{}, error) { stack := p.vstack[len(p.vstack)-1] _ = stack return p.cur.onHeader1(stack["fc"], stack["fic"], stack["node0"], stack["node1"]) } func (c *current) onFollow1(hdr, data interface{}) (interface{}, error) { bytes := make([]Bytes, 0, len(data.([]interface{}))) for _, dl := range data.([]interface{}) { bytes = append(bytes, dl.(Bytes)) } return &FollowStream{ FollowHeader: hdr.(FollowHeader), Bytes: bytes, }, nil } func (p *parser) callonFollow1() (interface{}, error) { stack := p.vstack[len(p.vstack)-1] _ = stack return p.cur.onFollow1(stack["hdr"], stack["data"]) } var ( // errNoRule is returned when the grammar to parse has no rule. errNoRule = errors.New("grammar has no rule") // errInvalidEntrypoint is returned when the specified entrypoint rule // does not exit. errInvalidEntrypoint = errors.New("invalid entrypoint") // errInvalidEncoding is returned when the source is not properly // utf8-encoded. errInvalidEncoding = errors.New("invalid encoding") // errMaxExprCnt is used to signal that the maximum number of // expressions have been parsed. errMaxExprCnt = errors.New("max number of expressions 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.4.0/pkg/streams/follow.peg000066400000000000000000000130561426312004500177550ustar00rootroot00000000000000// Copyright 2019-2022 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.4.0/pkg/streams/follow_test.go000066400000000000000000000125131426312004500206430ustar00rootroot00000000000000// Copyright 2019-2022 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.4.0/pkg/streams/loader.go000066400000000000000000000222441426312004500175520ustar00rootroot00000000000000// Copyright 2019-2022 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/pkg/pcap" log "github.com/sirupsen/logrus" ) //====================================================================== var Goroutinewg *sync.WaitGroup //====================================================================== type ILoaderCmds interface { Stream(pcap string, proto string, idx int) pcap.IPcapCommand Indexer(pcap string, proto string, idx int) pcap.IPcapCommand } type commands struct{} func MakeCommands() commands { return commands{} } var _ ILoaderCmds = commands{} func (c commands) Stream(pcapfile string, proto string, idx int) pcap.IPcapCommand { args := []string{"-r", pcapfile, "-q", "-z", fmt.Sprintf("follow,%s,raw,%d", proto, idx)} return &pcap.Command{Cmd: exec.Command(termshark.TSharkBin(), args...)} } // startAt is zero-indexed func (c commands) Indexer(pcapfile string, proto string, idx int) pcap.IPcapCommand { args := []string{"-T", "pdml", "-r", pcapfile, "-Y", fmt.Sprintf("%s.stream eq %d", proto, idx)} return &pcap.Command{Cmd: exec.Command(termshark.TSharkBin(), args...)} } //====================================================================== type Loader struct { cmds ILoaderCmds SuppressErrors bool // if true, don't report process errors e.g. at shutdown mainCtx context.Context // cancelling this cancels the dependent contexts mainCancelFn context.CancelFunc streamCtx context.Context // cancels the stream chunk reader process streamCancelFn context.CancelFunc indexerCtx context.Context // cancels the stream indexer (pdml) process indexerCancelFn context.CancelFunc streamCmd pcap.IPcapCommand indexerCmd pcap.IPcapCommand } func NewLoader(cmds ILoaderCmds, ctx context.Context) *Loader { res := &Loader{ cmds: cmds, } res.mainCtx, res.mainCancelFn = context.WithCancel(ctx) return res } // Called by user to cancel a stream reassembly op. Stops both processes straight away. // Note that typically, the indexer will be further behind. func (c *Loader) StopLoad() { c.SuppressErrors = true if c.streamCancelFn != nil { c.streamCancelFn() } if c.indexerCancelFn != nil { c.indexerCancelFn() } } //====================================================================== type ITrackPayload interface { TrackPayloadPacket(packet int) } type IIndexerCallbacks interface { IOnStreamChunk ITrackPayload AfterIndexEnd(success bool) } func (c *Loader) StartLoad(pcap string, proto string, idx int, app gowid.IApp, cb IIndexerCallbacks) { c.SuppressErrors = false termshark.TrackedGo(func() { c.loadStreamReassemblyAsync(pcap, proto, idx, app, cb) }, Goroutinewg) termshark.TrackedGo(func() { c.startStreamIndexerAsync(pcap, proto, idx, app, cb) }, Goroutinewg) } type ISavedData interface { NumChunks() int Chunk(i int) IChunk } func (c *Loader) loadStreamReassemblyAsync(pcapf string, proto string, idx int, app gowid.IApp, cb interface{}) { c.streamCtx, c.streamCancelFn = context.WithCancel(c.mainCtx) procChan := make(chan int) pid := 0 defer func() { if pid == 0 { close(procChan) } }() c.streamCmd = c.cmds.Stream(pcapf, proto, idx) termChan := make(chan error) termshark.TrackedGo(func() { var err error origCmd := c.streamCmd cancelled := c.streamCtx.Done() procChan := procChan state := pcap.NotStarted kill := func() { err := termshark.KillIfPossible(origCmd) if err != nil { log.Infof("Did not kill tshark stream process: %v", err) } } loop: for { select { case err = <-termChan: state = pcap.Terminated if !c.SuppressErrors && err != nil { if _, ok := err.(*exec.ExitError); ok { pcap.HandleError(pcap.StreamCode, app, pcap.MakeUsefulError(c.streamCmd, err), cb) } } case pid := <-procChan: procChan = nil if pid != 0 { state = pcap.Started if cancelled == nil { kill() } } case <-cancelled: cancelled = nil if state == pcap.Started { kill() } } if state == pcap.Terminated || (procChan == nil && state == pcap.NotStarted) { break loop } } }, Goroutinewg) streamOut, err := c.streamCmd.StdoutReader() if err != nil { pcap.HandleError(pcap.StreamCode, app, err, cb) return } app.Run(gowid.RunFunction(func(app gowid.IApp) { pcap.HandleBegin(pcap.StreamCode, app, cb) })) defer func() { app.Run(gowid.RunFunction(func(app gowid.IApp) { pcap.HandleEnd(pcap.StreamCode, app, cb) })) }() err = c.streamCmd.Start() if err != nil { err = fmt.Errorf("Error starting stream reassembly %v: %v", c.streamCmd, err) pcap.HandleError(pcap.StreamCode, app, err, cb) return } log.Infof("Started stream reassembly command %v with pid %d", c.streamCmd, c.streamCmd.Pid()) defer func() { termChan <- c.streamCmd.Wait() }() pid = c.streamCmd.Pid() procChan <- pid var ops []Option ops = append(ops, GlobalStore("app", app)) ops = append(ops, GlobalStore("context", c.streamCtx)) ops = append(ops, GlobalStore("callbacks", cb)) func() { _, err := ParseReader("", streamOut, ops...) if err != nil { log.Warnf("Stream parser reported error: %v", err) } }() c.streamCancelFn() } func (c *Loader) startStreamIndexerAsync(pcapf string, proto string, idx int, app gowid.IApp, cb IIndexerCallbacks) { res := false procChan := make(chan int) pid := 0 defer func() { if pid == 0 { close(procChan) } }() c.indexerCtx, c.indexerCancelFn = context.WithCancel(c.mainCtx) c.indexerCmd = c.cmds.Indexer(pcapf, proto, idx) streamOut, err := c.indexerCmd.StdoutReader() if err != nil { pcap.HandleError(pcap.StreamCode, app, err, cb) return } procWaitChan := make(chan error, 1) termshark.TrackedGo(func() { var err error cancelledChan := c.indexerCtx.Done() procChan := procChan state := pcap.NotStarted kill := func() { err = termshark.KillIfPossible(c.indexerCmd) if err != nil { log.Infof("Did not kill indexer process: %v", err) } } loop: for { select { case err = <-procWaitChan: state = pcap.Terminated if !c.SuppressErrors && err != nil { if _, ok := err.(*exec.ExitError); ok { pcap.HandleError(pcap.StreamCode, app, pcap.MakeUsefulError(c.indexerCmd, err), cb) } } streamOut.Close() case pid := <-procChan: procChan = nil if pid != 0 { state = pcap.Started if cancelledChan == nil { kill() } } case <-cancelledChan: cancelledChan = nil if state == pcap.Started { kill() } } if state == pcap.Terminated || (procChan == nil && state == pcap.NotStarted) { break loop } } }, Goroutinewg) defer func() { cb.AfterIndexEnd(res) }() err = c.indexerCmd.Start() if err != nil { err = fmt.Errorf("Error starting stream indexer %v: %v", c.indexerCmd, err) pcap.HandleError(pcap.StreamCode, app, err, cb) return } log.Infof("Started stream indexer command %v with pid %d", c.indexerCmd, c.indexerCmd.Pid()) defer func() { procWaitChan <- c.indexerCmd.Wait() }() pid = c.indexerCmd.Pid() procChan <- pid res = decodeStreamXml(streamOut, proto, c.indexerCtx, cb) } func decodeStreamXml(streamOut io.Reader, proto string, ctx context.Context, cb ITrackPayload) bool { inTCP := false inUDP := false curPkt := 0 curDataLen := 0 res := false d := xml.NewDecoder(streamOut) for { if ctx.Err() != nil { break } t, tokenErr := d.Token() if tokenErr != nil { if tokenErr == io.EOF { res = true break } } switch t := t.(type) { case xml.EndElement: switch t.Name.Local { case "packet": if curDataLen > 0 { cb.TrackPayloadPacket(curPkt) } curPkt++ curDataLen = 0 inTCP = false inUDP = false } case xml.StartElement: switch t.Name.Local { case "proto": for _, attr := range t.Attr { if attr.Name.Local == "name" { switch attr.Value { case "tcp": inTCP = true case "udp": inUDP = true } break } } case "field": aloop: for _, attr := range t.Attr { if attr.Name.Local == "name" { switch attr.Value { case "tcp.len": if proto == "tcp" && inTCP { for _, attr2 := range t.Attr { if attr2.Name.Local == "show" { if val, err := strconv.Atoi(attr2.Value); err == nil { // add val to end of list for tcp:curTCP curDataLen = val } break aloop } } } case "udp.length": if proto == "udp" && inUDP { for _, attr2 := range t.Attr { if attr2.Name.Local == "show" { if val, err := strconv.Atoi(attr2.Value); err == nil { // add val to end of list for udp:curUDP curDataLen = val } break aloop } } } } } } } } } return res } //====================================================================== //====================================================================== // Local Variables: // mode: Go // fill-column: 78 // End: termshark-2.4.0/pkg/streams/loader_test.go000066400000000000000000004210251426312004500206110ustar00rootroot00000000000000// Copyright 2019-2022 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.4.0/pkg/streams/parse.go000066400000000000000000000052471426312004500174220ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. package streams import ( "encoding/hex" "fmt" "strings" ) //====================================================================== type IChunk interface { Direction() Direction StreamData() []byte } type IOnStreamChunk interface { OnStreamChunk(chunk IChunk) } type IOnStreamHeader interface { OnStreamHeader(header FollowHeader) } //====================================================================== type parseContext interface { Err() error } type StreamParseError struct{} func (e StreamParseError) Error() string { return "Stream reassembly parse error" } var _ error = StreamParseError{} //====================================================================== type Protocol int const ( Unspecified Protocol = 0 TCP Protocol = iota UDP Protocol = iota ) var _ fmt.Stringer = Protocol(0) func (p Protocol) String() string { switch p { case Unspecified: return "Unspecified" case TCP: return "TCP" case UDP: return "UDP" default: panic(nil) } } //====================================================================== type Direction int const ( Client Direction = 0 Server Direction = iota ) func (d Direction) String() string { switch d { case Client: return "Client" case Server: return "Server" default: return "Unknown!" } } //====================================================================== type Bytes struct { Dirn Direction Data []byte } var _ fmt.Stringer = Bytes{} var _ IChunk = Bytes{} func (b Bytes) Direction() Direction { return b.Dirn } func (b Bytes) StreamData() []byte { return b.Data } func (b Bytes) String() string { return fmt.Sprintf("Direction: %v\n%s", b.Dirn, hex.Dump(b.Data)) } //====================================================================== type FollowHeader struct { Follow string Filter string Node0 string Node1 string } func (h FollowHeader) String() string { return fmt.Sprintf("[client:%s server:%s follow:%s filter:%s]", h.Node0, h.Node1, h.Follow, h.Filter) } type FollowStream struct { FollowHeader Bytes []Bytes } var _ fmt.Stringer = FollowStream{} func (f FollowStream) String() string { datastrs := make([]string, 0, len(f.Bytes)) for _, b := range f.Bytes { datastrs = append(datastrs, b.String()) } data := strings.Join(datastrs, "\n") return fmt.Sprintf("Follow: %s\nFilter: %s\nNode0: %s\nNode1: %s\nData:\n%s", f.Follow, f.Filter, f.Node0, f.Node1, data) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.4.0/pkg/summary/000077500000000000000000000000001426312004500157705ustar00rootroot00000000000000termshark-2.4.0/pkg/summary/summary.go000066400000000000000000000036541426312004500200240ustar00rootroot00000000000000// Copyright 2019-2022 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 summary import ( "bufio" "io" "sync" "github.com/gcla/termshark/v2" ) //====================================================================== // Reader maintains the first two and last two lines read from a source io.Reader. // At any point, the Summary() function can be called to extract a summary of // what's been read so far. I'm using this to create a summary of the stderr of // a termshark command. type Reader struct { source io.Reader first *string second *string penultimate *string last *string num int lock sync.Mutex } func New(source io.Reader) *Reader { res := &Reader{ source: source, } termshark.TrackedGo(func() { res.start() }, Goroutinewg) return res } func (h *Reader) Summary() []string { h.lock.Lock() defer h.lock.Unlock() res := make([]string, 0, 5) if h.num >= 1 { res = append(res, *h.first) } if h.num >= 2 { res = append(res, *h.second) } if h.num >= 5 { res = append(res, "...") } if h.num >= 4 { res = append(res, *h.penultimate) } if h.num >= 3 { res = append(res, *h.last) } return res } func (h *Reader) start() { scanner := bufio.NewScanner(h.source) for scanner.Scan() { line := scanner.Text() h.lock.Lock() h.num += 1 if h.first == nil { h.first = &line } else if h.second == nil { h.second = &line } h.penultimate = h.last h.last = &line h.lock.Unlock() } } //====================================================================== // 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 //====================================================================== // Local Variables: // mode: Go // fill-column: 78 // End: termshark-2.4.0/pkg/system/000077500000000000000000000000001426312004500156175ustar00rootroot00000000000000termshark-2.4.0/pkg/system/dumpcapext.go000066400000000000000000000050551426312004500203250ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // +build !windows // +build !darwin package system import ( "fmt" "os" "os/exec" "regexp" "strconv" "syscall" ) //====================================================================== var fdre *regexp.Regexp = regexp.MustCompile(`/dev/fd/([[:digit:]]+)`) // DumpcapExt will run dumpcap first, but if it fails, run tshark. Intended as // a special case to allow termshark -i to use dumpcap if possible, // but if it fails (e.g. iface==randpkt), fall back to tshark. dumpcap is more // efficient than tshark at just capturing, and will drop fewer packets, but // tshark supports extcap interfaces. func DumpcapExt(dumpcapBin string, tsharkBin string, args ...string) error { var err error // If the first argument is /dev/fd/X, it means the process should have // descriptor X open and will expect packet data to be readable on it. // This /dev/fd feature does not work on tshark when run on freebsd, meaning // tshark will fail if you do something like // // cat foo.pcap | tshark -r /dev/fd/0 // // The fix here is to replace /dev/fd/X with the arg "-", which tshark will // interpret as stdin, then dup descriptor X to 0 before starting dumpcap/tshark // if len(args) >= 2 { if os.Getenv("TERMSHARK_REPLACE_DEVFD") != "0" { fdnum := fdre.FindStringSubmatch(args[1]) if len(fdnum) == 2 { fd, err := strconv.Atoi(fdnum[1]) if err != nil { fmt.Fprintf(os.Stderr, "Unexpected error parsing %s: %v\n", args[1], err) } else { err = Dup2(fd, 0) if err != nil { fmt.Fprintf(os.Stderr, "Problem duplicating fd %d to 0: %v\n", fd, err) fmt.Fprintf(os.Stderr, "Will not try to replace argument %s to tshark\n", args[1]) } else { fmt.Fprintf(os.Stderr, "Replacing argument %s with - for tshark compatibility\n", args[1]) args[1] = "-" } } } } } fmt.Fprintf(os.Stderr, "Starting termshark's custom live capture procedure.\n") dumpcapCmd := exec.Command(dumpcapBin, args...) fmt.Fprintf(os.Stderr, "Trying dumpcap command %v\n", dumpcapCmd) dumpcapCmd.Stdin = os.Stdin dumpcapCmd.Stdout = os.Stdout dumpcapCmd.Stderr = os.Stderr if dumpcapCmd.Run() != nil { var tshark string tshark, err = exec.LookPath(tsharkBin) if err == nil { fmt.Fprintf(os.Stderr, "Retrying with capture command %v\n", append([]string{tshark}, args...)) err = syscall.Exec(tshark, append([]string{tshark}, args...), os.Environ()) } } return err } termshark-2.4.0/pkg/system/dumpcapext_arm64.go000066400000000000000000000025341426312004500213350ustar00rootroot00000000000000// Copyright 2019-2022 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 // +build !linux package system import ( "fmt" "os" "os/exec" "syscall" ) //====================================================================== // DumpcapExt will run dumpcap first, but if it fails, run tshark. Intended as // a special case to allow termshark -i to use dumpcap if possible, // but if it fails (e.g. iface==randpkt), fall back to tshark. dumpcap is more // efficient than tshark at just capturing, and will drop fewer packets, but // tshark supports extcap interfaces. func DumpcapExt(dumpcapBin string, tsharkBin string, args ...string) error { var err error dumpcapCmd := exec.Command(dumpcapBin, args...) fmt.Fprintf(os.Stderr, "Starting termshark's custom live capture procedure.\n") fmt.Fprintf(os.Stderr, "Trying dumpcap command %v\n", dumpcapCmd) dumpcapCmd.Stdin = os.Stdin dumpcapCmd.Stdout = os.Stdout dumpcapCmd.Stderr = os.Stderr if dumpcapCmd.Run() != nil { var tshark string tshark, err = exec.LookPath(tsharkBin) if err == nil { fmt.Fprintf(os.Stderr, "Retrying with dumpcap command %v\n", append([]string{tshark}, args...)) err = syscall.Exec(tshark, append([]string{tshark}, args...), os.Environ()) } } return err } termshark-2.4.0/pkg/system/dumpcapext_darwin.go000066400000000000000000000024701426312004500216670ustar00rootroot00000000000000// Copyright 2019-2022 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" "syscall" ) //====================================================================== // DumpcapExt will run dumpcap first, but if it fails, run tshark. Intended as // a special case to allow termshark -i to use dumpcap if possible, // but if it fails (e.g. iface==randpkt), fall back to tshark. dumpcap is more // efficient than tshark at just capturing, and will drop fewer packets, but // tshark supports extcap interfaces. func DumpcapExt(dumpcapBin string, tsharkBin string, args ...string) error { var err error dumpcapCmd := exec.Command(dumpcapBin, args...) fmt.Fprintf(os.Stderr, "Starting termshark's custom live capture procedure.\n") fmt.Fprintf(os.Stderr, "Trying dumpcap command %v\n", dumpcapCmd) dumpcapCmd.Stdin = os.Stdin dumpcapCmd.Stdout = os.Stdout dumpcapCmd.Stderr = os.Stderr if dumpcapCmd.Run() != nil { var tshark string tshark, err = exec.LookPath(tsharkBin) if err == nil { fmt.Fprintf(os.Stderr, "Retrying with dumpcap command %v\n", append([]string{tshark}, args...)) err = syscall.Exec(tshark, append([]string{tshark}, args...), os.Environ()) } } return err } termshark-2.4.0/pkg/system/dumpcapext_windows.go000066400000000000000000000024011426312004500220670ustar00rootroot00000000000000// Copyright 2019-2022 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" ) //====================================================================== // DumpcapExt will run dumpcap first, but if it fails, run tshark. Intended as // a special case to allow termshark -i to use dumpcap if possible, // but if it fails (e.g. iface==randpkt), fall back to tshark. dumpcap is more // efficient than tshark at just capturing, and will drop fewer packets, but // tshark supports extcap interfaces. func DumpcapExt(dumpcapBin string, tsharkBin string, args ...string) error { dumpcapCmd := exec.Command(dumpcapBin, args...) fmt.Fprintf(os.Stderr, "Starting termshark's custom live capture procedure.\n") fmt.Fprintf(os.Stderr, "Trying dumpcap command %v\n", dumpcapCmd) dumpcapCmd.Stdin = os.Stdin dumpcapCmd.Stdout = os.Stdout dumpcapCmd.Stderr = os.Stderr if dumpcapCmd.Run() == nil { return nil } tsharkCmd := exec.Command(tsharkBin, args...) fmt.Fprintf(os.Stderr, "Retrying with dumpcap command %v\n", tsharkCmd) tsharkCmd.Stdin = os.Stdin tsharkCmd.Stdout = os.Stdout tsharkCmd.Stderr = os.Stderr return tsharkCmd.Run() } termshark-2.4.0/pkg/system/dup.go000066400000000000000000000005131426312004500167350ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // +build !windows // +build !linux !arm64 // +build !linux !riscv64 package system import "syscall" func Dup2(fd int, fd2 int) error { return syscall.Dup2(fd, fd2) } termshark-2.4.0/pkg/system/dup_linux_arm64.go000066400000000000000000000004101426312004500211610ustar00rootroot00000000000000// Copyright 2019-2022 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 "syscall" func Dup2(fd int, fd2 int) error { return syscall.Dup3(fd, fd2, 0) } termshark-2.4.0/pkg/system/dup_linux_riscv64.go000066400000000000000000000004101426312004500215300ustar00rootroot00000000000000// Copyright 2019-2022 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 "syscall" func Dup2(fd int, fd2 int) error { return syscall.Dup3(fd, fd2, 0) } termshark-2.4.0/pkg/system/errors.go000066400000000000000000000011051426312004500174570ustar00rootroot00000000000000// Copyright 2019-2022 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.4.0/pkg/system/extcmds.go000066400000000000000000000004541426312004500176200ustar00rootroot00000000000000// Copyright 2019-2022 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.4.0/pkg/system/extcmds_android.go000066400000000000000000000004701426312004500213160ustar00rootroot00000000000000// Copyright 2019-2022 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.4.0/pkg/system/extcmds_darwin.go000066400000000000000000000003711426312004500211620ustar00rootroot00000000000000// Copyright 2019-2022 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.4.0/pkg/system/extcmds_windows.go000066400000000000000000000003731426312004500213720ustar00rootroot00000000000000// Copyright 2019-2022 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.4.0/pkg/system/fd.go000066400000000000000000000012311426312004500165340ustar00rootroot00000000000000// Copyright 2019-2022 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" ) //====================================================================== func CloseDescriptor(fd int) { syscall.Close(fd) } func FileRegularOrLink(filename string) bool { fi, err := os.Stat(filename) if err != nil { return false } return fi.Mode().IsRegular() || (fi.Mode()&os.ModeSymlink != 0) } //====================================================================== // Local Variables: // mode: Go // fill-column: 78 // End: termshark-2.4.0/pkg/system/fd_windows.go000066400000000000000000000006501426312004500203120ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // +build windows package system func CloseDescriptor(fd int) { } func FileRegularOrLink(filename string) bool { return true } //====================================================================== // Local Variables: // mode: Go // fill-column: 78 // End: termshark-2.4.0/pkg/system/fdinfo.go000066400000000000000000000032631426312004500174170ustar00rootroot00000000000000// Copyright 2019-2022 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.4.0/pkg/system/have_fdinfo.go000066400000000000000000000005521426312004500204200ustar00rootroot00000000000000// Copyright 2019-2022 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.4.0/pkg/system/have_fdinfo_linux.go000066400000000000000000000005161426312004500216370ustar00rootroot00000000000000// Copyright 2019-2022 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.4.0/pkg/system/picker.go000066400000000000000000000006211426312004500174220ustar00rootroot00000000000000// Copyright 2019-2022 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.4.0/pkg/system/picker_android.go000066400000000000000000000041161426312004500211250ustar00rootroot00000000000000// Copyright 2019-2022 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\n") 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.4.0/pkg/system/signals.go000066400000000000000000000022431426312004500176070ustar00rootroot00000000000000// Copyright 2019-2022 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.4.0/pkg/system/signals_windows.go000066400000000000000000000015231426312004500213610ustar00rootroot00000000000000// Copyright 2019-2022 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.4.0/pkg/tailfile/000077500000000000000000000000001426312004500160645ustar00rootroot00000000000000termshark-2.4.0/pkg/tailfile/tailfile.go000066400000000000000000000010451426312004500202040ustar00rootroot00000000000000// Copyright 2019-2022 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 tailfile import ( "os" "os/exec" ) //====================================================================== func Tail(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.4.0/pkg/tailfile/tailfile_windows.go000066400000000000000000000013031426312004500217530ustar00rootroot00000000000000// Copyright 2019-2022 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 tailfile import ( "os" "github.com/gcla/tail" ) //====================================================================== func Tail(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.4.0/pkg/theme/000077500000000000000000000000001426312004500153755ustar00rootroot00000000000000termshark-2.4.0/pkg/theme/modeswap/000077500000000000000000000000001426312004500172145ustar00rootroot00000000000000termshark-2.4.0/pkg/theme/modeswap/modeswap.go000066400000000000000000000020321426312004500213570ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // package modeswap provides an IColor-conforming type Color that renders differently // if in low-color mode package modeswap import ( "github.com/gcla/gowid" ) //====================================================================== type Color struct { modeHi gowid.IColor mode256 gowid.IColor mode16 gowid.IColor } var _ gowid.IColor = (*Color)(nil) func New(hi, mid, lo gowid.IColor) *Color { return &Color{ modeHi: hi, mode256: mid, mode16: lo, } } func (c *Color) ToTCellColor(mode gowid.ColorMode) (gowid.TCellColor, bool) { var col gowid.IColor = c.mode16 switch mode { case gowid.Mode24BitColors: col = c.modeHi case gowid.Mode256Colors: col = c.mode256 default: col = c.mode16 } return col.ToTCellColor(mode) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.4.0/pkg/theme/utils.go000066400000000000000000000073651426312004500170770ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // package theme provides utilities for customizing the styling of termshark. package theme import ( "fmt" "io" "os" "path" "path/filepath" "github.com/gcla/gowid" "github.com/rakyll/statik/fs" "github.com/shibukawa/configdir" log "github.com/sirupsen/logrus" "github.com/spf13/viper" _ "github.com/gcla/termshark/v2/assets/statik" ) //====================================================================== type Layer int const ( Foreground Layer = 0 Background Layer = iota ) var theme *viper.Viper // MakeColorSafe extends gowid's MakeColorSafe function, preferring to interpret // its string argument as a toml file config key lookup first; if this fails, then // fall back to gowid.MakeColorSafe, which will then read colors as urwid color names, // #-prefixed hex digits, grayscales, etc. func MakeColorSafe(s string, l Layer) (gowid.Color, error) { loops := 10 cur := s if theme != nil { for { next := theme.GetString(cur) if next != "" { cur = next } else { next := theme.GetStringSlice(cur) if next == nil || len(next) != 2 { break } else { cur = next[l] } } loops -= 1 if loops == 0 { break } } } col, err := gowid.MakeColorSafe(cur) if err == nil { return gowid.Color{IColor: col, Id: s}, nil } return gowid.MakeColorSafe(s) } type Mode gowid.ColorMode func (m Mode) String() string { switch gowid.ColorMode(m) { case gowid.Mode256Colors: return "256" case gowid.Mode88Colors: return "88" case gowid.Mode16Colors: return "16" case gowid.Mode8Colors: return "8" case gowid.ModeMonochrome: return "mono" case gowid.Mode24BitColors: return "truecolor" default: return "unknown" } } // Load will set the package-level theme object to a viper object representing the // toml file either (a) read from disk, or failing that (b) built-in to termshark. // Disk themes are preferred and take precedence. func Load(name string, app gowid.IApp) error { var err error theme = viper.New() defer func() { if err != nil { theme = nil } }() theme.SetConfigType("toml") stdConf := configdir.New("", "termshark") dirs := stdConf.QueryFolders(configdir.Global) mode := Mode(app.GetColorMode()).String() log.Infof("Loading theme %s in terminal mode %v", name, app.GetColorMode()) // If there's not a truecolor theme, we assume the user wants the best alternative to be loaded, // and if a terminal has truecolor support, it'll surely have 256-color support. modes := []string{mode} if mode == "truecolor" { modes = append(modes, Mode(gowid.Mode256Colors).String()) } for _, m := range modes { // Prefer to load from disk themeFileName := filepath.Join(dirs[0].Path, "themes", fmt.Sprintf("%s-%s.toml", name, m)) log.Infof("Trying to load user theme %s", themeFileName) var file io.ReadCloser file, err = os.Open(themeFileName) if err == nil { defer file.Close() log.Infof("Loaded user theme %s", themeFileName) return theme.ReadConfig(file) } } // Fall back to built-in themes statikFS, err := fs.New() if err != nil { return fmt.Errorf("in mode %v: %v", app.GetColorMode(), err) } for _, m := range modes { themeFileName := path.Join("/themes", fmt.Sprintf("%s-%s.toml", name, m)) log.Infof("Trying to load built-in theme %s", themeFileName) var file io.ReadCloser file, err = statikFS.Open(themeFileName) if err == nil { defer file.Close() log.Infof("Loaded built-in theme %s", themeFileName) return theme.ReadConfig(file) } } return err } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.4.0/pkg/tty/000077500000000000000000000000001426312004500151135ustar00rootroot00000000000000termshark-2.4.0/pkg/tty/tty.go000066400000000000000000000031221426312004500162600ustar00rootroot00000000000000// Copyright 2019-2022 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/gcla/term/termios" "golang.org/x/sys/unix" ) //====================================================================== type TerminalSignals struct { tiosp *unix.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(outtty string) error { var e error var newtios unix.Termios var fd uintptr if t.out, e = os.OpenFile(outtty, os.O_WRONLY, 0); e != nil { goto failed } fd = uintptr(t.out.Fd()) if t.tiosp, e = termios.Tcgetattr(fd); 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.4.0/pkg/tty/tty_windows.go000066400000000000000000000011471426312004500200370ustar00rootroot00000000000000// Copyright 2019-2022 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(tty string) error { t.set = true return nil } //====================================================================== // Local Variables: // mode: Go // fill-column: 78 // End: termshark-2.4.0/scripts/000077500000000000000000000000001426312004500152015ustar00rootroot00000000000000termshark-2.4.0/scripts/do-release.sh000077500000000000000000000005261426312004500175630ustar00rootroot00000000000000#!/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.4.0/scripts/pcaps/000077500000000000000000000000001426312004500163075ustar00rootroot00000000000000termshark-2.4.0/scripts/pcaps/telnet-cooked.pcap000066400000000000000000000220341426312004500217120ustar00rootroot00000000000000ò@8JJ;E}x '$%,@8]];EOF>@@sŠc>}xng '$%, !"'#@83EE;E7=@c>šCg %,'$%@84BB;E4F?@@s!šcA}x '3%,@84EE;E7F@@@sšcA}xȢ '3%,%@8 :BB;E4_@cAš C'@ %,'3@8@[[;EM:0@cAš C %,'3 !"" @8A;EtFA@@rš cZ}x '3%,P "bb B  "@8EBB;E4_@jcZšKC'' %,'3@8 "${PCAP}" d4c3b2a102000400 0000000000000000 0000040006000000 f32a395200000000 4d0000004d000000 1040002035012b59 0006291793f8aaaa 0300000008004500 0037f93900004011 a6dbc0a82c7bc0a8 2cd5f93900450023 8d730001433a5c49 424d54435049505c 6c63636d2e31006f 6374657400f32a39 52000000004d0000 004d000000104000 2035012b59000629 1793f8aaaa030000 00080045000037f9 3900004011a6dbc0 a82c7bc0a82cd5f9 39004500238d7300 01433a5c49424d54 435049505c6c6363 6d2e31006f637465 7400 EOF echo Running termshark cli tests. # if timeout is invoked because termshark is stuck, the exit code will be non-zero export TS="$GOPATH/bin/termshark" # stdout is not a tty, so falls back to tshark $TS -r "${PCAP}" | grep '192.168.44.213 TFTP 77' # prove that options provided are passed through to tshark [[ $($TS -r "${PCAP}" -T psml -n | grep '' | wc -l) == 2 ]] # Must choose either a file or an interface ! $TS -r "${PCAP}" -i eth0 # only display the second line via tshark [[ $($TS -r "${PCAP}" 'frame.number == 2' | wc -l) == 1 ]] # test fifos mkfifo "${FIFO}" cat "${PCAP}" > "${FIFO}" & $TS -r "${FIFO}" | grep '192.168.44.213 TFTP 77' wait rm "${FIFO}" # Check pass-thru option works. Make termshark run in a tty to ensure it's taking effect [[ $(script -q -e -c "$TS -r "${PCAP}" --pass-thru" | wc -l) == 2 ]] [[ $(script -q -e -c "$TS -r "${PCAP}" --pass-thru=true" | wc -l) == 2 ]] # run in script so termshark thinks it's in a tty cat version.go | grep -o -E "v[0-9]+\.[0-9]+(\.[0-9]+)?" | \ xargs -i bash -c "script -q -e -c \"$TS -v\" | grep {}" echo Running termshark UI tests. in_tty() { ARGS=$@ # make into one token socat - EXEC:"bash -c \\\"stty rows 50 cols 80 && TERM=xterm && $ARGS\\\"",pty,setsid,ctty } wait_for_load() { rm ~/.cache/termshark/termshark.log > /dev/null 2>&1 tail -F ~/.cache/termshark/termshark.log 2> /dev/null | while [ 1 ] ; do read ; echo Log: $REPLY 1>&2 ; grep "Load operation complete" <<<$REPLY && break ; done } echo UI test 1 # Load a pcap, quit { wait_for_load ; sleep 0.5s ; echo q ; sleep 0.5s ; echo ; } | in_tty $TS -r "${PCAP}" > /dev/null echo Tests disabled for now until I understand whats going on with Travis... exit 0 echo UI test 2 # Run with stdout not a tty, but disable the pass-thru to tshark { wait_for_load ; sleep 0.5s ; echo q ; sleep 0.5s ; echo ; } | in_tty "$TS -r "${PCAP}" --pass-thru=false | cat" > /dev/null echo UI test 3 # Load a pcap, very rudimentary scrape for an IP, quit { wait_for_load ; sleep 0.5s ; echo q ; sleep 0.5s ; echo ; } | in_tty "$TS -r "${PCAP}"" | grep -a 192.168.44.123 > /dev/null # Ensure -r flag isn't needed { wait_for_load ; sleep 0.5s ; echo q ; sleep 0.5s ; echo ; } | in_tty "$TS "${PCAP}"" | grep -a 192.168.44.123 > /dev/null echo UI test 4 # Load a pcap from stdin { wait_for_load ; sleep 0.5s ; echo q ; sleep 0.5s ; echo ; } | in_tty "cat "${PCAP}" | TERM=xterm $TS -i -" > /dev/null { wait_for_load ; sleep 0.5s ; echo q ; sleep 0.5s ; echo ; } | in_tty "cat "${PCAP}" | TERM=xterm $TS -r -" > /dev/null { wait_for_load ; sleep 0.5s ; echo q ; sleep 0.5s ; echo ; } | in_tty "cat "${PCAP}" | TERM=xterm $TS" > /dev/null echo UI test 5 # Display filter at end of command line { wait_for_load ; sleep 0.5s ; echo q ; sleep 0.5s ; echo ; } | in_tty "$TS -r scripts/pcaps/telnet-cooked.pcap \'frame.number == 2\'" | grep -a "Frame 2: 74 bytes" > /dev/null echo UI test 6 mkfifo "${FIFO}" cat "${PCAP}" > "${FIFO}" & { wait_for_load ; sleep 0.5s ; echo q ; sleep 0.5s ; echo ; } | in_tty "$TS -r "${FIFO}"" > /dev/null wait cat "${PCAP}" > "${FIFO}" & { wait_for_load ; sleep 0.5s ; echo q ; sleep 0.5s ; echo ; } | in_tty "$TS -i "${FIFO}"" > /dev/null wait cat "${PCAP}" > "${FIFO}" & { wait_for_load ; sleep 0.5s ; echo q ; sleep 0.5s ; echo ; } | in_tty "$TS "${FIFO}"" > /dev/null #{ sleep 5s ; echo q ; echo ; } | in_tty "$TS "${FIFO}" \'frame.number == 2\'" | grep -a "Frame 2: 74 bytes" > /dev/null wait echo Tests were successful. termshark-2.4.0/ui/000077500000000000000000000000001426312004500141275ustar00rootroot00000000000000termshark-2.4.0/ui/capinfoui.go000066400000000000000000000065641426312004500164460ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package ui contains user-interface functions and helpers for termshark. package ui import ( "os" "strings" "time" "github.com/gcla/gowid" "github.com/gcla/termshark/v2" "github.com/gcla/termshark/v2/pkg/capinfo" "github.com/gcla/termshark/v2/pkg/pcap" log "github.com/sirupsen/logrus" ) var CapinfoLoader *capinfo.Loader var CapinfoData string var CapinfoTime time.Time //====================================================================== func startCapinfo(app gowid.IApp) { if Loader.PcapPdml == "" { OpenError("No pcap loaded.", app) return } fi, err := os.Stat(Loader.PcapPdml) if err != nil || CapinfoTime.Before(fi.ModTime()) { CapinfoLoader = capinfo.NewLoader(capinfo.MakeCommands(), Loader.Context()) handler := capinfoParseHandler{} CapinfoLoader.StartLoad( Loader.PcapPdml, app, &handler, ) } else { OpenMessageForCopy(CapinfoData, appView, app) } } //====================================================================== type capinfoParseHandler struct { tick *time.Ticker // for updating the spinner stop chan struct{} pleaseWaitClosed bool } var _ capinfo.ICapinfoCallbacks = (*capinfoParseHandler)(nil) var _ pcap.IBeforeBegin = (*capinfoParseHandler)(nil) var _ pcap.IAfterEnd = (*capinfoParseHandler)(nil) func (t *capinfoParseHandler) OnCapinfoData(data string) { CapinfoData = strings.Replace(data, "\r\n", "\n", -1) // For windows... fi, err := os.Stat(Loader.PcapPdml) if err != nil { log.Warnf("Could not read mtime from pcap %s: %v", Loader.PcapPdml, err) } else { CapinfoTime = fi.ModTime() } } func (t *capinfoParseHandler) AfterCapinfoEnd(success bool) { } func (t *capinfoParseHandler) BeforeBegin(code pcap.HandlerCode, app gowid.IApp) { if code&pcap.CapinfoCode == 0 { return } app.Run(gowid.RunFunction(func(app gowid.IApp) { OpenPleaseWait(appView, app) })) t.tick = time.NewTicker(time.Duration(200) * time.Millisecond) t.stop = make(chan struct{}) termshark.TrackedGo(func() { Loop: for { select { case <-t.tick.C: app.Run(gowid.RunFunction(func(app gowid.IApp) { pleaseWaitSpinner.Update() })) case <-t.stop: break Loop } } }, Goroutinewg) } func (t *capinfoParseHandler) AfterEnd(code pcap.HandlerCode, app gowid.IApp) { if code&pcap.CapinfoCode == 0 { return } app.Run(gowid.RunFunction(func(app gowid.IApp) { if !t.pleaseWaitClosed { t.pleaseWaitClosed = true ClosePleaseWait(app) } OpenMessageForCopy(CapinfoData, appView, app) })) close(t.stop) } //====================================================================== func clearCapinfoState() { CapinfoTime = time.Time{} } //====================================================================== type ManageCapinfoCache struct{} var _ pcap.INewSource = ManageCapinfoCache{} var _ pcap.IClear = ManageCapinfoCache{} // Make sure that existing stream widgets are discarded if the user loads a new pcap. func (t ManageCapinfoCache) OnNewSource(pcap.HandlerCode, gowid.IApp) { clearCapinfoState() } func (t ManageCapinfoCache) OnClear(pcap.HandlerCode, gowid.IApp) { clearCapinfoState() } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.4.0/ui/convscallbacks.go000066400000000000000000000042551426312004500174540ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package ui contains user-interface functions and helpers for termshark. package ui import ( "strings" "time" "github.com/gcla/gowid" "github.com/gcla/termshark/v2" "github.com/gcla/termshark/v2/pkg/pcap" ) //====================================================================== type IOnDataSync interface { OnData(data string, app gowid.IApp) OnCancel(gowid.IApp) } type convsParseHandler struct { app gowid.IApp tick *time.Ticker // for updating the spinner stop chan struct{} ondata IOnDataSync pleaseWaitClosed bool } var _ pcap.IBeforeBegin = (*convsParseHandler)(nil) var _ pcap.IAfterEnd = (*convsParseHandler)(nil) func (t *convsParseHandler) OnData(data string) { data = strings.Replace(data, "\r\n", "\n", -1) // For windows... if t.ondata != nil { t.app.Run(gowid.RunFunction(func(app gowid.IApp) { t.ondata.OnData(data, app) })) } } func (t *convsParseHandler) AfterDataEnd(success bool) { if t.ondata != nil && !success { t.app.Run(gowid.RunFunction(func(app gowid.IApp) { t.ondata.OnCancel(app) })) } } func (t *convsParseHandler) BeforeBegin(code pcap.HandlerCode, app gowid.IApp) { if code&pcap.ConvCode == 0 { return } app.Run(gowid.RunFunction(func(app gowid.IApp) { OpenPleaseWait(appView, t.app) })) t.tick = time.NewTicker(time.Duration(200) * time.Millisecond) t.stop = make(chan struct{}) termshark.TrackedGo(func() { Loop: for { select { case <-t.tick.C: app.Run(gowid.RunFunction(func(app gowid.IApp) { pleaseWaitSpinner.Update() })) case <-t.stop: break Loop } } }, Goroutinewg) } func (t *convsParseHandler) AfterEnd(code pcap.HandlerCode, app gowid.IApp) { if code&pcap.ConvCode == 0 { return } t.app.Run(gowid.RunFunction(func(app gowid.IApp) { if !t.pleaseWaitClosed { t.pleaseWaitClosed = true ClosePleaseWait(t.app) } })) close(t.stop) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.4.0/ui/convsui.go000066400000000000000000000643541426312004500161600ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package ui contains user-interface functions and helpers for termshark. package ui import ( "bufio" "context" "fmt" "runtime" "strings" "github.com/gcla/gowid" "github.com/gcla/gowid/widgets/button" "github.com/gcla/gowid/widgets/checkbox" "github.com/gcla/gowid/widgets/columns" "github.com/gcla/gowid/widgets/divider" "github.com/gcla/gowid/widgets/framed" "github.com/gcla/gowid/widgets/holder" "github.com/gcla/gowid/widgets/hpadding" "github.com/gcla/gowid/widgets/isselected" "github.com/gcla/gowid/widgets/list" "github.com/gcla/gowid/widgets/menu" "github.com/gcla/gowid/widgets/null" "github.com/gcla/gowid/widgets/overlay" "github.com/gcla/gowid/widgets/pile" "github.com/gcla/gowid/widgets/styled" "github.com/gcla/gowid/widgets/table" "github.com/gcla/gowid/widgets/text" "github.com/gcla/gowid/widgets/vpadding" "github.com/gcla/termshark/v2" "github.com/gcla/termshark/v2/configs/profiles" "github.com/gcla/termshark/v2/pkg/convs" "github.com/gcla/termshark/v2/pkg/pcap" "github.com/gcla/termshark/v2/pkg/psmlmodel" "github.com/gcla/termshark/v2/ui/tableutil" "github.com/gcla/termshark/v2/widgets/appkeys" "github.com/gcla/termshark/v2/widgets/copymodetable" "github.com/gcla/termshark/v2/widgets/enableselected" "github.com/gcla/termshark/v2/widgets/keepselected" "github.com/gcla/termshark/v2/widgets/scrollabletable" "github.com/gcla/termshark/v2/widgets/withscrollbar" "github.com/gdamore/tcell/v2" ) var convsView *holder.Widget var convsUi *ConvsUiWidget var convCancel context.CancelFunc var convsPcapSize int64 // track size of source, if changes then recalculation conversations var vdiv string var frameRunes framed.FrameRunes type Direction int const ( Any Direction = 0 To Direction = iota From Direction = iota ) type ConvAddr int const ( IPv4Addr ConvAddr = 0 IPv6Addr ConvAddr = iota MacAddr ConvAddr = iota ) type FilterMask int const ( AtfB FilterMask = 0 AtB FilterMask = iota BtA FilterMask = iota AtfAny FilterMask = iota AtAny FilterMask = iota AnytA FilterMask = iota AnytfB FilterMask = iota AnytB FilterMask = iota BtAny FilterMask = iota ) type FilterCombinator int const ( Selected FilterCombinator = 0 NotSelected FilterCombinator = iota AndSelected FilterCombinator = iota OrSelected FilterCombinator = iota AndNotSelected FilterCombinator = iota OrNotSelected FilterCombinator = iota ) // Use to construct a string like "ip.addr == 1.2.3.4 && tcp.port == 12345" type IFilterBuilder interface { fmt.Stringer FilterFrom(vals ...string) string FilterTo(vals ...string) string FilterAny(vals ...string) string AIndex() []int BIndex() []int } var convTypes = map[string]IFilterBuilder{} func init() { convTypes[convs.Ethernet{}.Short()] = convs.Ethernet{} convTypes[convs.IPv4{}.Short()] = convs.IPv4{} convTypes[convs.IPv6{}.Short()] = convs.IPv6{} convTypes[convs.UDP{}.Short()] = convs.UDP{} convTypes[convs.TCP{}.Short()] = convs.TCP{} if runtime.GOOS == "windows" { vdiv = "│" frameRunes = framed.FrameRunes{'┌', '┐', '└', '┘', 0, '─', '│', '│'} } else { vdiv = "┃" frameRunes = framed.FrameRunes{'┏', '┓', '┗', '┛', 0, '━', '┃', '┃'} } } //====================================================================== type ManageConvsCache struct{} var _ pcap.INewSource = ManageConvsCache{} // Make sure that existing data is discarded if the user loads a new pcap. func (t ManageConvsCache) OnNewSource(pcap.HandlerCode, gowid.IApp) { convsView = nil // which then deletes all refs to loaded data convsPcapSize = 0 } //====================================================================== type ConvsModel struct { *psmlmodel.Model proto IFilterBuilder } func (m ConvsModel) GetAFilter(row int, dir Direction) string { line := m.Data[row] parms := []string{} for _, idx := range m.proto.AIndex() { parms = append(parms, line[idx]) } switch dir { case To: return m.proto.FilterTo(parms...) case From: return m.proto.FilterFrom(parms...) default: return m.proto.FilterAny(parms...) } } func (m ConvsModel) GetBFilter(row int, dir Direction) string { line := m.Data[row] parms := []string{} for _, idx := range m.proto.BIndex() { parms = append(parms, line[idx]) } switch dir { case To: return m.proto.FilterTo(parms...) case From: return m.proto.FilterFrom(parms...) default: return m.proto.FilterAny(parms...) } } //====================================================================== func convsKeyPress(sections *pile.Widget, evk *tcell.EventKey, app gowid.IApp) bool { handled := false switch { case evk.Rune() == 'q' || evk.Rune() == 'Q' || evk.Key() == tcell.KeyEscape: closeConvsUi(app) convCancel() handled = true case evk.Key() == tcell.KeyTAB: if next, ok := sections.FindNextSelectable(gowid.Forwards, true); ok { sections.SetFocus(app, next) handled = true } case evk.Key() == tcell.KeyBacktab: if next, ok := sections.FindNextSelectable(gowid.Backwards, true); ok { sections.SetFocus(app, next) handled = true } } return handled } //====================================================================== type pleaseWait struct{} func (p pleaseWait) OpenPleaseWait(app gowid.IApp) { OpenPleaseWait(appView, app) } func (p pleaseWait) ClosePleaseWait(app gowid.IApp) { ClosePleaseWait(app) } // Dynamically load conv. If the convs window was last opened with a different filter, and the "limit to // filter" checkbox is checked, then the data needs to be reloaded. func openConvsUi(app gowid.IApp) { var convCtx context.Context convCtx, convCancel = context.WithCancel(Loader.Context()) newSize, reset := termshark.FileSizeDifferentTo(Loader.PcapPdml, convsPcapSize) if reset { convsView = nil } // This is nil if a new pcap is loaded (or the old one cleared) if convsView == nil { convsPcapSize = newSize // gcla later todo - PcapPdml - hack? convsUi = NewConvsUi( Loader.String(), Loader.DisplayFilter(), Loader.PcapPdml, pleaseWait{}, ConvsUiOptions{ CopyModeWidget: CopyModeWidget, }, ) convsView = holder.New(convsUi) } else if convsUi.FilterValue() != Loader.DisplayFilter() && convsUi.UseFilter() { convsUi.ReloadNeeded() } convsUi.ctx = convCtx convsUi.focusOnFilter = false convsUi.displayFilter = Loader.DisplayFilter() copyModeConvsView := appkeys.New( appkeys.New( convsView, copyModeExitKeys20, appkeys.Options{ ApplyBefore: true, }, ), copyModeEnterKeys, appkeys.Options{ ApplyBefore: true, }, ) appViewNoKeys.SetSubWidget(copyModeConvsView, app) } func closeConvsUi(app gowid.IApp) { appViewNoKeys.SetSubWidget(mainView, app) if convsUi.focusOnFilter { setFocusOnDisplayFilter(app) } else { // Do this if the user starts conversations from the menu - better UX setFocusOnPacketList(app) } } //====================================================================== func NewConvsUi(captureDevice string, displayFilter string, pcapf string, pw IPleaseWait, opts ...ConvsUiOptions) *ConvsUiWidget { var opt ConvsUiOptions if len(opts) > 0 { opt = opts[0] } res := &ConvsUiWidget{ opt: opt, displayFilter: displayFilter, captureDevice: captureDevice, pcapf: pcapf, pleaseWait: pw, tabIndex: make(map[string]int), buttonLabels: make(map[string]*text.Widget), } res.construct() return res } type IPleaseWait interface { OpenPleaseWait(app gowid.IApp) ClosePleaseWait(app gowid.IApp) } type ConvsUiOptions struct { CopyModeWidget gowid.IWidget // What to display when copy-mode is started. } type ConvsUiWidget struct { gowid.IWidget opt ConvsUiOptions captureDevice string // "eth0" displayFilter string // "tcp.stream eq 1" pcapf string // "eth0-ddddd.pcap" ctx context.Context pleaseWait IPleaseWait convHolder *holder.Widget convs []*oneConvWidget // the widgets displayed in each tab prepFiltBtn *button.Widget // "Prepare filter" -> click to prep filter applyFiltBtn *button.Widget // "Apply filter" -> click to prep filter filterPrep bool // if true prepare filter, don't apply; otherwise apply immediately filterSelectedIndex FilterCombinator // which filter combination is active e.g. A -> B focusOnFilter bool // Whether to set focus on display filter on closing widget buttonLabels map[string]*text.Widget // map "eth" to button, so I can update with a count of conversations shortNames []string // ["eth", "ip", ...] - from config file tabIndex map[string]int // {"eth": 0, "ipv6": 2, ...} -> mapping to tabs in UI started bool // false if stream load needs to be done, true if under way or done } func (w *ConvsUiWidget) AbsoluteTime() bool { return profiles.ConfBool("main.conv-absolute-time", false) } func (w *ConvsUiWidget) SetAbsoluteTime(val bool) { profiles.SetConf("main.conv-absolute-time", val) } func (w *ConvsUiWidget) ResolveNames() bool { return profiles.ConfBool("main.conv-resolve-names", false) } func (w *ConvsUiWidget) SetResolveNames(val bool) { profiles.SetConf("main.conv-resolve-names", val) } func (w *ConvsUiWidget) Context() context.Context { return w.ctx } func (w *ConvsUiWidget) FilterValue() string { return w.displayFilter } func (w *ConvsUiWidget) UseFilter() bool { return profiles.ConfBool("main.conv-use-filter", false) } func (w *ConvsUiWidget) SetUseFilter(val bool) { profiles.SetConf("main.conv-use-filter", val) } func (w *ConvsUiWidget) construct() { convs := make([]*oneConvWidget, 0) header := w.makeHeaderConvsUiWidget() convsHeader := columns.NewWithDim( gowid.RenderWithWeight{1}, header, ) colws := make([]interface{}, 0) colws = append(colws, text.New(vdiv), ) w.shortNames = termshark.ConvTypes() // Just in case there are none w.convHolder = holder.New(null.New()) for i, p := range w.shortNames { p := p i := i w.tabIndex[p] = i newconv := newOneConv(p) convs = append(convs, newconv) if i == 0 { w.convHolder = holder.New(newconv) } w.buttonLabels[p] = text.New(fmt.Sprintf(" %s ", convTypes[p])) b := button.NewBare(w.buttonLabels[p]) b.OnClick(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, w2 gowid.IWidget) { w.convHolder.SetSubWidget(newconv, app) })) bs := isselected.NewExt( b, styled.New(b, gowid.MakePaletteRef("button-selected")), styled.New(b, gowid.MakePaletteRef("button-focus")), ) colws = append(colws, bs, text.New(vdiv)) } panel := framed.New(w.convHolder, framed.Options{ Frame: frameRunes, }) cols := keepselected.New(columns.NewFixed(colws...)) nameCheck := checkbox.New(w.ResolveNames()) nameCheck.OnClick(gowid.WidgetCallback{"cb", func(app gowid.IApp, w2 gowid.IWidget) { w.SetResolveNames(nameCheck.IsChecked()) w.ReloadNeeded() }}) nameLabel := text.New(" Name res.") nameW := hpadding.New( columns.NewFixed(nameCheck, nameLabel), gowid.HAlignMiddle{}, gowid.RenderFixed{}, ) filterCheck := checkbox.New(w.UseFilter()) filterCheck.OnClick(gowid.WidgetCallback{"cb", func(app gowid.IApp, w2 gowid.IWidget) { w.SetUseFilter(filterCheck.IsChecked()) w.ReloadNeeded() }}) filterLabel := text.New(" Limit to filter") filterW := hpadding.New( columns.NewFixed(filterCheck, filterLabel), gowid.HAlignMiddle{}, gowid.RenderFixed{}, ) absTimeCheck := checkbox.New(w.AbsoluteTime()) absTimeCheck.OnClick(gowid.WidgetCallback{"cb", func(app gowid.IApp, w2 gowid.IWidget) { w.SetAbsoluteTime(absTimeCheck.IsChecked()) w.ReloadNeeded() }}) absTimeLabel := text.New(" Abs. time") absTimeW := hpadding.New( columns.NewFixed(absTimeCheck, absTimeLabel), gowid.HAlignMiddle{}, gowid.RenderFixed{}, ) //==================== prepFiltBtnSite := menu.NewSite(menu.SiteOptions{YOffset: -8}) w.prepFiltBtn = button.New(text.New("Prep Filter")) w.prepFiltBtn.OnClick(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, w2 gowid.IWidget) { w.filterPrep = true filterConvsMenu1.Open(prepFiltBtnSite, app) })) styledPrepFiltBtn := styled.NewExt( w.prepFiltBtn, gowid.MakePaletteRef("button"), gowid.MakePaletteRef("button-focus"), ) prepFiltCols := columns.NewFixed(prepFiltBtnSite, styledPrepFiltBtn) prepFiltColsW := hpadding.New( prepFiltCols, gowid.HAlignMiddle{}, gowid.RenderFixed{}, ) //==================== applyFiltBtnSite := menu.NewSite(menu.SiteOptions{YOffset: -8}) w.applyFiltBtn = button.New(text.New("Apply Filter")) w.applyFiltBtn.OnClick(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, w2 gowid.IWidget) { w.filterPrep = false filterConvsMenu1.Open(applyFiltBtnSite, app) })) styledApplyFiltBtn := styled.NewExt( w.applyFiltBtn, gowid.MakePaletteRef("button"), gowid.MakePaletteRef("button-focus"), ) applyFiltCols := columns.NewFixed(applyFiltBtnSite, styledApplyFiltBtn) applyFiltColsW := hpadding.New( applyFiltCols, gowid.HAlignMiddle{}, gowid.RenderFixed{}, ) //==================== bcols := columns.NewWithDim(gowid.RenderWithWeight{W: 1}, prepFiltColsW, applyFiltColsW, nameW, filterW, absTimeW, ) main := pile.New([]gowid.IContainerWidget{ &gowid.ContainerWidget{ IWidget: convsHeader, D: gowid.RenderWithUnits{U: 2}, }, &gowid.ContainerWidget{ IWidget: cols, D: gowid.RenderWithUnits{U: 1}, }, &gowid.ContainerWidget{ IWidget: panel, D: gowid.RenderWithWeight{W: 1}, }, &gowid.ContainerWidget{ IWidget: bcols, D: gowid.RenderWithUnits{U: 1}, }, }) w.IWidget = appkeys.New( main, func(ev *tcell.EventKey, app gowid.IApp) bool { return convsKeyPress(main, ev, app) }, appkeys.Options{ ApplyBefore: true, }, ) w.convs = convs } func (w *ConvsUiWidget) ReloadNeeded() { w.started = false } func (w *ConvsUiWidget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { if !w.started { w.started = true ld := convs.NewLoader(convs.MakeCommands(), w.Context()) handler := convsParseHandler{ app: app, ondata: w, } filter := "" if w.UseFilter() { filter = w.FilterValue() } ld.StartLoad( w.pcapf, w.shortNames, //w.ctype, filter, w.AbsoluteTime(), w.ResolveNames(), app, &handler, ) } return w.IWidget.Render(size, focus, app) } // The widget displayed in the first line of the stream reassembly UI. func (w *ConvsUiWidget) makeHeaderConvsUiWidget() gowid.IWidget { var headerText string var headerText1 string var headerText2 string var headerText3 string headerText1 = "Conversations" if w.displayFilter != "" { headerText2 = fmt.Sprintf("(%s)", w.displayFilter) } if w.captureDevice != "" { headerText3 = fmt.Sprintf("- %s", w.captureDevice) } headerText = strings.Join([]string{headerText1, headerText2, headerText3}, " ") headerView := overlay.New( hpadding.New(w.opt.CopyModeWidget, gowid.HAlignMiddle{}, fixed), hpadding.New( text.New(headerText), gowid.HAlignMiddle{}, fixed, ), gowid.VAlignTop{}, gowid.RenderWithRatio{R: 1}, gowid.HAlignMiddle{}, gowid.RenderWithRatio{R: 1}, overlay.Options{ BottomGetsFocus: true, TopGetsNoFocus: true, BottomGetsCursor: true, }, ) return headerView } // convsModelWithRow is able to provide an A and a B for a conversation A <-> B. It looks // up the model at a specific row to find the conversation. type convsModelWithRow struct { model *ConvsModel row int } var _ IFilterModel = (*convsModelWithRow)(nil) func (c *convsModelWithRow) GetAFilter(dir Direction) string { return c.model.GetAFilter(c.row, dir) } func (c *convsModelWithRow) GetBFilter(dir Direction) string { return c.model.GetBFilter(c.row, dir) } func (w *ConvsUiWidget) doFilterMenuOp(dirOp FilterMask, app gowid.IApp) { conv1 := w.convHolder.SubWidget() if conv1 != nil { if conv1, ok := conv1.(*oneConvWidget); ok { if conv1.tbl.Length() == 0 { OpenError("No conversation selected.", app) return } pos := conv1.tbl.Pos() cmodel := &convsModelWithRow{ model: conv1.model, row: pos, } filter := ComputeConvFilterOp(dirOp, w.filterSelectedIndex, cmodel, FilterWidget.Value()) FilterWidget.SetValue(filter, app) if w.filterPrep { // Don't run the filter, just add to the displayfilter widget. Leave focus there w.focusOnFilter = true OpenMessage("Display filter prepared.", appView, app) } else { RequestNewFilter(filter, app) w.displayFilter = filter OpenMessage("Display filter applied.", appView, app) w.ReloadNeeded() } } } } type IFilterModel interface { GetAFilter(Direction) string GetBFilter(Direction) string } func ComputeConvFilterOp(dirOp FilterMask, comb FilterCombinator, model IFilterModel, curFilter string) string { var filter string switch dirOp { case AtfB: filter = fmt.Sprintf("%s && %s", model.GetAFilter(Any), model.GetBFilter(Any)) case AtB: filter = fmt.Sprintf("%s && %s", model.GetAFilter(From), model.GetBFilter(To)) case BtA: filter = fmt.Sprintf("%s && %s", model.GetBFilter(From), model.GetAFilter(To)) case AtfAny: filter = model.GetAFilter(Any) case AtAny: filter = model.GetAFilter(From) case AnytA: filter = model.GetAFilter(To) case AnytfB: filter = model.GetBFilter(Any) case AnytB: filter = model.GetBFilter(To) case BtAny: filter = model.GetBFilter(From) } return ComputeFilterCombOp(comb, filter, curFilter) } func ComputeFilterCombOp(comb FilterCombinator, newFilter string, curFilter string) string { switch comb { case NotSelected: newFilter = fmt.Sprintf("!(%s)", newFilter) case AndSelected: if curFilter != "" { newFilter = fmt.Sprintf("%s && (%s)", curFilter, newFilter) } else { newFilter = fmt.Sprintf("%s", newFilter) } case OrSelected: if curFilter != "" { newFilter = fmt.Sprintf("%s || (%s)", curFilter, newFilter) } else { newFilter = fmt.Sprintf("%s", newFilter) } case AndNotSelected: if curFilter != "" { newFilter = fmt.Sprintf("%s && !(%s)", curFilter, newFilter) } else { newFilter = fmt.Sprintf("!%s", newFilter) } case OrNotSelected: if curFilter != "" { newFilter = fmt.Sprintf("%s || !(%s)", curFilter, newFilter) } else { newFilter = fmt.Sprintf("!%s", newFilter) } } return newFilter } func (w *ConvsUiWidget) OnCancel(app gowid.IApp) { for _, cw := range w.convs { cw.IWidget = cw.cancelledWidget } } func (w *ConvsUiWidget) OnData(data string, app gowid.IApp) { var hdrs []string var wids []gowid.IWidgetDimension var comps []table.ICompare var cur string var next string var ports bool = false var ( addra string porta string addrb string portb string framesto string bytesto string framesfrom string bytesfrom string frames string bytes string start string durn string ) var datas [][]string saveConversation := func(cur string) { tblModel := table.NewSimpleModel(hdrs, datas, table.SimpleOptions{ Comparators: comps, Style: table.StyleOptions{ HorizontalSeparator: nil, TableSeparator: divider.NewUnicode(), VerticalSeparator: nil, CellStyleProvided: true, CellStyleSelected: gowid.MakePaletteRef("packet-list-cell-selected"), CellStyleFocus: gowid.MakePaletteRef("packet-list-cell-focus"), HeaderStyleProvided: true, HeaderStyleFocus: gowid.MakePaletteRef("packet-list-cell-focus"), }, Layout: table.LayoutOptions{ Widths: wids, }, }) ptblModel := psmlmodel.New( tblModel, gowid.MakePaletteRef("packet-list-row-focus"), ) if currentShortName, ok := convs.OfficialNameToType[cur]; ok { model := &ConvsModel{ Model: ptblModel, proto: convTypes[currentShortName], } tbl := &table.BoundedWidget{ Widget: table.New(model), } boundedTbl := NewRowFocusTableWidget( tbl, "packet-list-row-selected", "packet-list-row-focus", ) var _ list.IWalker = boundedTbl var _ gowid.IWidget = boundedTbl var _ table.IBoundedModel = tblModel w.convs[w.tabIndex[currentShortName]].IWidget = appkeys.New( enableselected.New( withscrollbar.New( scrollabletable.New( copymodetable.New( boundedTbl, CsvTableCopier{hdrs, datas}, CsvTableCopier{hdrs, datas}, "convstable", copyModePalette{}, ), ), withscrollbar.Options{ HideIfContentFits: true, }, ), ), tableutil.GotoHandler(&tableutil.GoToAdapter{ BoundedWidget: tbl, KeyState: &keyState, }), ) w.convs[w.tabIndex[currentShortName]].tbl = tbl w.convs[w.tabIndex[currentShortName]].model = model w.buttonLabels[currentShortName].SetText(fmt.Sprintf(" %s (%d) ", cur, len(datas)), app) } } scanner := bufio.NewScanner(strings.NewReader(data)) var n int var err error for scanner.Scan() { line := scanner.Text() r := strings.NewReader(line) n, err = fmt.Fscanf(r, "%s Conversations", &next) if err == nil && n == 1 { if cur != "" { saveConversation(cur) } datas = make([][]string, 0) cur = next ports = termshark.StringInSlice(cur, []string{"UDP", "TCP"}) ipv6 := (cur == "IPv6") var addrComp table.ICompare = termshark.IPCompare{} if termshark.StringInSlice(cur, []string{"Ethernet"}) { addrComp = termshark.MACCompare{} } var convComp table.ICompare = termshark.ConvPktsCompare{} if ports { hdrs = []string{ "Addr A", "Port A", "Addr B", "Port B", "Pkts", "Bytes", "Pkts A→B", "Bytes A→B", "Pkts B→A", "Bytes B→A", "Start", "Durn", } wids = []gowid.IWidgetDimension{ weightupto(400, 32), // addra weightupto(200, 7), // port weightupto(400, 32), // addrb weightupto(200, 7), // port weightupto(200, 8), // pkts weightupto(200, 10), weightupto(200, 12), // pkts a -> b weightupto(200, 12), // bytes a -> b weightupto(200, 12), // pkts a -> b weightupto(200, 12), // bytes a -> b weightupto(500, 14), // start weightupto(200, 8), // durn } comps = []table.ICompare{ addrComp, table.IntCompare{}, addrComp, table.IntCompare{}, table.IntCompare{}, convComp, table.IntCompare{}, convComp, table.IntCompare{}, convComp, table.FloatCompare{}, table.FloatCompare{}, } } else { hdrs = []string{ "Addr A", "Addr B", "Pkts", "Bytes", "Pkts A→B", "Bytes A→B", "Pkts B→A", "Bytes B→A", "Start", "Durn", } wids = []gowid.IWidgetDimension{ weightupto(400, 32), // addra weightupto(400, 32), // addrb weightupto(200, 8), // pkts weightupto(200, 10), weightupto(200, 12), // pkts a -> b weightupto(200, 12), // bytes a -> b weightupto(200, 12), // pkts a -> b weightupto(200, 12), // bytes a -> b weightupto(500, 14), // start weightupto(200, 10), // durn } if ipv6 { wids[0] = weightupto(500, 42) wids[1] = weightupto(500, 42) } comps = []table.ICompare{ addrComp, addrComp, table.IntCompare{}, convComp, table.IntCompare{}, convComp, table.IntCompare{}, convComp, table.FloatCompare{}, table.FloatCompare{}, } } continue } line = strings.Replace(line, " bytes", "", -1) line = strings.Replace(line, "bytes", "", -1) line = strings.Replace(line, " kB", "kB", -1) line = strings.Replace(line, " MB", "MB", -1) r = strings.NewReader(line) n, err = fmt.Fscanf(r, "%s <-> %s %s %s %s %s %s %s %s %s", &addra, &addrb, &framesto, &bytesto, &framesfrom, &bytesfrom, &frames, &bytes, &start, &durn, ) if err == nil && n == 10 { bytesto = strings.Replace(bytesto, "kB", " kB", -1) bytesfrom = strings.Replace(bytesfrom, "kB", " kB", -1) bytes = strings.Replace(bytes, "kB", " kB", -1) bytesto = strings.Replace(bytesto, "MB", " MB", -1) bytesfrom = strings.Replace(bytesfrom, "MB", " MB", -1) bytes = strings.Replace(bytes, "MB", " MB", -1) if ports { pa := strings.Split(addra, ":") pb := strings.Split(addrb, ":") if len(pa) == 2 && len(pb) == 2 { addra = pa[0] porta = pa[1] addrb = pb[0] portb = pb[1] datas = append(datas, []string{addra, porta, addrb, portb, framesto, bytesto, framesfrom, bytesfrom, frames, bytes, start, durn}) } } else { datas = append(datas, []string{addra, addrb, framesto, bytesto, framesfrom, bytesfrom, frames, bytes, start, durn}) } } } saveConversation(cur) } //====================================================================== type oneConvWidget struct { gowid.IWidget ctype string pleaseWaitWidget gowid.IWidget cancelledWidget gowid.IWidget model *ConvsModel tbl *table.BoundedWidget } func newOneConv(ctype string) *oneConvWidget { pleaseWaitWidget := vpadding.New( hpadding.New( text.New(fmt.Sprintf("Please wait for %s", ctype)), gowid.HAlignMiddle{}, gowid.RenderFixed{}, ), gowid.VAlignMiddle{}, gowid.RenderFlow{}, ) cancelledWidget := text.New("Conversation load was cancelled.") res := &oneConvWidget{ IWidget: pleaseWaitWidget, ctype: ctype, pleaseWaitWidget: pleaseWaitWidget, cancelledWidget: cancelledWidget, } return res } //====================================================================== type CsvTableCopier struct { hdrs []string data [][]string } func (c CsvTableCopier) CopyRow(id table.RowId) []gowid.ICopyResult { row := strings.Join(c.data[id], ",") return []gowid.ICopyResult{ gowid.CopyResult{ Name: "Copy conversation", Val: row, }, } } func (c CsvTableCopier) CopyTable() []gowid.ICopyResult { res := make([]string, 0, len(c.data)+1) res = append(res, strings.Join(c.hdrs, ",")) for _, d := range c.data { res = append(res, strings.Join(d, ",")) } prt := strings.Join(res, "\n") return []gowid.ICopyResult{ gowid.CopyResult{ Name: "Copy all", Val: prt, }, } } var _ copymodetable.IRowCopier = CsvTableCopier{} var _ copymodetable.ITableCopier = CsvTableCopier{} //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.4.0/ui/convsui_test.go000066400000000000000000000022351426312004500172050ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package ui contains user-interface functions and helpers for termshark. package ui import ( "fmt" "strings" "testing" "github.com/stretchr/testify/assert" ) func TestScan1(t *testing.T) { line := `127.0.0.1:47416 <-> 127.0.0.1:9191 0 0 43549 9951808 43549 9951808 4.160565000 9.4522` var ( addra string addrb string framesto int bytesto int framesfrom int bytesfrom int frames int bytes int start string durn string ) r := strings.NewReader(line) n, err := fmt.Fscanf(r, "%s <-> %s %d %d %d %d %d %d %s %s", &addra, &addrb, &framesto, &bytesto, &framesfrom, &bytesfrom, &frames, &bytes, &start, &durn, ) assert.NoError(t, err) assert.Equal(t, 10, n) assert.Equal(t, "4.160565000", start) assert.Equal(t, "127.0.0.1:9191", addrb) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.4.0/ui/darkmode.go000066400000000000000000000016251426312004500162500ustar00rootroot00000000000000// Copyright 2019-2022 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.4.0/ui/dialog.go000066400000000000000000000124441426312004500157220ustar00rootroot00000000000000// Copyright 2019-2022 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/paragraph" "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" "github.com/gcla/termshark/v2/widgets/appkeys" "github.com/gcla/termshark/v2/widgets/framefocus" "github.com/gcla/termshark/v2/widgets/minibuffer" "github.com/gcla/termshark/v2/widgets/scrollabletext" "github.com/gcla/termshark/v2/widgets/withscrollbar" ) //====================================================================== var ( fixed gowid.RenderFixed flow gowid.RenderFlow hmiddle gowid.HAlignMiddle hleft gowid.HAlignLeft vmiddle gowid.VAlignMiddle YesNo *dialog.Widget MiniBuffer *minibuffer.Widget PleaseWait *dialog.Widget ) type textID string func (t textID) ID() interface{} { return string(t) } // So that I can capture ctrl-c etc before the dialog type copyable struct { *dialog.Widget wrapper gowid.IWidget } func (w *copyable) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { return w.wrapper.UserInput(ev, size, focus, app) } func OpenMessage(msgt string, openOver gowid.ISettableComposite, app gowid.IApp) *dialog.Widget { return openMessage(msgt, openOver, false, false, app) } func OpenLongMessage(msgt string, openOver gowid.ISettableComposite, app gowid.IApp) *dialog.Widget { return openMessage(msgt, openOver, false, true, app) } func OpenMessageForCopy(msgt string, openOver gowid.ISettableComposite, app gowid.IApp) *dialog.Widget { return openMessage(msgt, openOver, true, false, app) } func openMessage(msgt string, openOver gowid.ISettableComposite, selectableWidget bool, doFlow bool, app gowid.IApp) *dialog.Widget { var dh gowid.IWidgetDimension = fixed var dw gowid.IWidgetDimension = fixed if doFlow { dh = flow dw = ratio(0.7) } var al gowid.IHAlignment = hmiddle if strings.Count(msgt, "\n") > 0 || doFlow { al = hleft } var view gowid.IWidget = text.NewCopyable(msgt, textID(msgt), styled.UsePaletteIfSelectedForCopy{Entry: "copy-mode-alt"}, text.Options{ Align: al, }, ) if selectableWidget { view = selectable.New(view) } view = hpadding.New( view, hmiddle, dh, ) if selectableWidget { view = framefocus.NewSlim(view) } view = framed.NewSpace(view) YesNo = dialog.New( view, dialog.Options{ Buttons: dialog.CloseOnly, NoShadow: true, BackgroundStyle: gowid.MakePaletteRef("dialog"), BorderStyle: gowid.MakePaletteRef("dialog"), ButtonStyle: gowid.MakePaletteRef("dialog-button"), Modal: true, }, ) wrapper := appkeys.New( appkeys.New( YesNo, copyModeExitKeys20, appkeys.Options{ ApplyBefore: true, }, ), copyModeEnterKeys, appkeys.Options{ ApplyBefore: true, }, ) dialog.OpenExt( ©able{ Widget: YesNo, wrapper: wrapper, }, openOver, dw, dh, app, ) return YesNo } func OpenTemplatedDialog(container gowid.ISettableComposite, tmplName string, app gowid.IApp) *dialog.Widget { msg := termshark.TemplateToString(Templates, tmplName, TemplateData) lines := strings.Split(msg, "\n") ws := make([]interface{}, 0, len(lines)) for _, line := range lines { if line == "" { ws = append(ws, text.New(line)) } else { words := strings.Fields(line) for i := 0; i < len(words); i++ { words[i] = strings.ReplaceAll(words[i], "_", " ") } ws = append(ws, paragraph.NewWithWords(words...)) } } body := pile.NewFlow(ws...) YesNo = dialog.New( framed.NewSpace(body), dialog.Options{ Buttons: dialog.CloseOnly, NoShadow: true, Modal: true, BackgroundStyle: gowid.MakePaletteRef("dialog"), BorderStyle: gowid.MakePaletteRef("dialog"), ButtonStyle: gowid.MakePaletteRef("dialog-button"), }, ) YesNo.Open(container, ratio(0.5), app) return YesNo } func OpenTemplatedDialogExt(container gowid.ISettableComposite, tmplName string, width gowid.IWidgetDimension, height gowid.IWidgetDimension, app gowid.IApp) *dialog.Widget { YesNo = dialog.New(framed.NewSpace( withscrollbar.New( scrollabletext.New( termshark.TemplateToString(Templates, tmplName, TemplateData), ), withscrollbar.Options{ HideIfContentFits: true, }, ), ), dialog.Options{ Buttons: dialog.CloseOnly, NoShadow: true, BackgroundStyle: gowid.MakePaletteRef("dialog"), BorderStyle: gowid.MakePaletteRef("dialog"), ButtonStyle: gowid.MakePaletteRef("dialog-button"), }, ) dialog.OpenExt(YesNo, container, width, height, app) return YesNo } func OpenPleaseWait(container gowid.ISettableComposite, app gowid.IApp) { PleaseWait.Open(container, fixed, app) } func ClosePleaseWait(app gowid.IApp) { PleaseWait.Close(app) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.4.0/ui/filterconvs.go000066400000000000000000000071111426312004500170140ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package ui contains user-interface functions and helpers for termshark. package ui import ( "github.com/gcla/gowid" "github.com/gcla/gowid/widgets/columns" "github.com/gcla/gowid/widgets/holder" "github.com/gcla/gowid/widgets/menu" "github.com/gcla/termshark/v2/ui/menuutil" "github.com/gdamore/tcell/v2" ) //====================================================================== var filterConvsMenu1 *menu.Widget var filterConvsMenu1Site *menu.SiteWidget var filterConvsMenu2 *menu.Widget type indirect struct { *holder.Widget } type iFilterMenuActor interface { HandleFilterMenuSelection(FilterCombinator, gowid.IApp) } type convsFilterMenuActor struct{} var _ iFilterMenuActor = convsFilterMenuActor{} func (c convsFilterMenuActor) HandleFilterMenuSelection(conv FilterCombinator, app gowid.IApp) { convsUi.filterSelectedIndex = conv filterConvsMenu2.Open(filterConvsMenu1Site, app) } func buildFilterConvsMenu() { filterConvsMenu1Holder := &indirect{} filterConvsMenu2Holder := &indirect{} filterConvsMenu1 = menu.New("filterconvs1", filterConvsMenu1Holder, fixed, menu.Options{ Modal: true, CloseKeysProvided: true, CloseKeys: []gowid.IKey{ gowid.MakeKey('q'), gowid.MakeKeyExt(tcell.KeyLeft), gowid.MakeKeyExt(tcell.KeyEscape), gowid.MakeKeyExt(tcell.KeyCtrlC), }, }) filterConvsMenu2 = menu.New("filterconvs2", filterConvsMenu2Holder, fixed, menu.Options{ Modal: true, CloseKeysProvided: true, CloseKeys: []gowid.IKey{ gowid.MakeKey('q'), gowid.MakeKeyExt(tcell.KeyLeft), gowid.MakeKeyExt(tcell.KeyEscape), gowid.MakeKeyExt(tcell.KeyCtrlC), }, }) w := makeFilterCombineMenuWidget(convsFilterMenuActor{}) filterConvsMenu1Site = menu.NewSite(menu.SiteOptions{ XOffset: -3, YOffset: -3, }) cols := columns.New([]gowid.IContainerWidget{ &gowid.ContainerWidget{IWidget: w, D: fixed}, &gowid.ContainerWidget{IWidget: filterConvsMenu1Site, D: fixed}, }) filterConvsMenu1Holder.Widget = holder.New(cols) w2 := makeFilterConvs2MenuWidget() filterConvsMenu2Holder.Widget = holder.New(w2) } func makeFilterCombineMenuWidget(handler iFilterMenuActor) gowid.IWidget { menuItems := make([]menuutil.SimpleMenuItem, 0) for i, s := range []string{ "Selected", "Not Selected", "...and Selected", "...or Selected", "...and not Selected", "...or not Selected", } { i2 := i menuItems = append(menuItems, menuutil.SimpleMenuItem{ Txt: s, Key: gowid.MakeKey('1' + rune(i)), CB: func(app gowid.IApp, w2 gowid.IWidget) { handler.HandleFilterMenuSelection(FilterCombinator(i2), app) }, }, ) } lb, _ := menuutil.MakeMenuWithHotKeys(menuItems, nil) return lb } func makeFilterConvs2MenuWidget() gowid.IWidget { menuItems := make([]menuutil.SimpleMenuItem, 0) for i, s := range []string{ "A ↔ B", "A → B", "B → A", "A ↔ Any", "A → Any", "Any → A", "Any ↔ B", "Any → B", "B → Any", } { i2 := i menuItems = append(menuItems, menuutil.SimpleMenuItem{ Txt: s, Key: gowid.MakeKey('1' + rune(i)), CB: func(app gowid.IApp, w2 gowid.IWidget) { filterConvsMenu1.Close(app) filterConvsMenu2.Close(app) convsUi.doFilterMenuOp(FilterMask(i2), app) }, }, ) } convListBox, _ := menuutil.MakeMenuWithHotKeys(menuItems, nil) return convListBox } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.4.0/ui/lastline.go000066400000000000000000000503111426312004500162710ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package ui contains user-interface functions and helpers for termshark. package ui import ( "fmt" "io/ioutil" "path/filepath" "strconv" "strings" "github.com/gcla/gowid" "github.com/gcla/gowid/gwutil" "github.com/gcla/gowid/vim" "github.com/gcla/termshark/v2" "github.com/gcla/termshark/v2/configs/profiles" "github.com/gcla/termshark/v2/pkg/theme" "github.com/gcla/termshark/v2/widgets/mapkeys" "github.com/gcla/termshark/v2/widgets/minibuffer" "github.com/rakyll/statik/fs" "github.com/shibukawa/configdir" _ "github.com/gcla/termshark/v2/assets/statik" ) //====================================================================== var notEnoughArgumentsErr = fmt.Errorf("Not enough arguments provided") var invalidSetCommandErr = fmt.Errorf("Invalid set command") var invalidReadCommandErr = fmt.Errorf("Invalid read command") var invalidRecentsCommandErr = fmt.Errorf("Invalid recents command") var invalidMapCommandErr = fmt.Errorf("Invalid map command") var invalidFilterCommandErr = fmt.Errorf("Invalid filter command") var invalidThemeCommandErr = fmt.Errorf("Invalid theme command") var invalidProfileCommandErr = fmt.Errorf("Invalid profile command") type minibufferFn func(gowid.IApp, ...string) error func (m minibufferFn) Run(app gowid.IApp, args ...string) error { return m(app, args...) } func (m minibufferFn) OfferCompletion() bool { return true } func (m minibufferFn) Arguments([]string, gowid.IApp) []minibuffer.IArg { return nil } type quietMinibufferFn func(gowid.IApp, ...string) error func (m quietMinibufferFn) Run(app gowid.IApp, args ...string) error { return m(app, args...) } func (m quietMinibufferFn) OfferCompletion() bool { return false } func (m quietMinibufferFn) Arguments([]string, gowid.IApp) []minibuffer.IArg { return nil } //====================================================================== type substrArg struct { candidates []string sub string } var _ minibuffer.IArg = substrArg{} func (s substrArg) OfferCompletion() bool { return true } // return these in sorted order func (s substrArg) Completions() []string { res := make([]string, 0) for _, str := range s.candidates { if strings.Contains(str, s.sub) { res = append(res, str) } } return res } //====================================================================== func newCachedArg(sub string, comps []string) substrArg { return substrArg{ sub: sub, candidates: comps, } } func newBoolArg(sub string) substrArg { return substrArg{ sub: sub, candidates: []string{"false", "true"}, } } func newOnOffArg(sub string) substrArg { return substrArg{ sub: sub, candidates: []string{"off", "on"}, } } func newSetArg(sub string) substrArg { return substrArg{ sub: sub, candidates: []string{ "auto-scroll", "copy-command-timeout", "dark-mode", "disable-shark-fin", "packet-colors", "pager", "nopager", "suppress-tshark-errors", "term", "noterm", }, } } func newHelpArg(sub string) substrArg { return substrArg{ sub: sub, candidates: []string{ "cmdline", "map", "set", "vim", }, } } func newProfileArg(sub string) substrArg { return substrArg{ sub: sub, candidates: []string{ "create", "use", "delete", "link", "unlink", }, } } //====================================================================== type unhelpfulArg struct { arg string } var _ minibuffer.IArg = unhelpfulArg{} func (s unhelpfulArg) OfferCompletion() bool { return false } // return these in sorted order func (s unhelpfulArg) Completions() []string { return nil } //====================================================================== type fileArg struct { substr string } var _ minibuffer.IArg = fileArg{} func (s fileArg) OfferCompletion() bool { return true } func (s fileArg) Completions() []string { matches, _ := filepath.Glob(s.substr + "*") if matches == nil { return []string{} } return matches } //====================================================================== type recentsArg struct { substr string } var _ minibuffer.IArg = recentsArg{} func (s recentsArg) OfferCompletion() bool { return true } func (s recentsArg) Completions() []string { matches := make([]string, 0) cfiles := profiles.ConfStringSlice("main.recent-files", []string{}) if cfiles != nil { for _, sc := range cfiles { scopy := sc if strings.Contains(scopy, s.substr) { matches = append(matches, scopy) } } } return matches } //====================================================================== type filterArg struct { field string substr string } var _ minibuffer.IArg = filterArg{} func (s filterArg) OfferCompletion() bool { return true } func (s filterArg) Completions() []string { matches := make([]string, 0) cfiles := profiles.ConfStringSlice(s.field, []string{}) if cfiles != nil { for _, sc := range cfiles { scopy := sc if strings.Contains(scopy, s.substr) { matches = append(matches, scopy) } } } return matches } //====================================================================== type themeArg struct { substr string modes []string } var _ minibuffer.IArg = themeArg{} func (s themeArg) OfferCompletion() bool { return true } func (s themeArg) Completions() []string { matches := make([]string, 0) // First gather built-in themes statikFS, err := fs.New() if err == nil { dir, err := statikFS.Open("/themes") if err == nil { info, err := dir.Readdir(-1) if err == nil { for _, finfo := range info { for _, mode := range s.modes { suff := fmt.Sprintf("-%s.toml", mode) if strings.HasSuffix(finfo.Name(), suff) { m := strings.TrimSuffix(finfo.Name(), suff) if strings.Contains(m, s.substr) { matches = append(matches, m) } } } } } } } // Then from filesystem stdConf := configdir.New("", "termshark") conf := stdConf.QueryFolderContainsFile("themes") if conf != nil { files, err := ioutil.ReadDir(filepath.Join(conf.Path, "themes")) if err == nil { for _, file := range files { for _, mode := range s.modes { suff := fmt.Sprintf("-%s.toml", mode) if strings.HasSuffix(file.Name(), suff) { m := strings.TrimSuffix(file.Name(), suff) if !termshark.StringInSlice(m, matches) { if strings.Contains(m, s.substr) { matches = append(matches, m) } } } } } } } return matches } //====================================================================== type profileArg struct { substr string } var _ minibuffer.IArg = profileArg{} func (s profileArg) OfferCompletion() bool { return true } func (s profileArg) Completions() []string { matches := make([]string, 0) profs := profiles.AllNames() for _, prof := range profs { if strings.Contains(prof, s.substr) { matches = append(matches, prof) } } return matches } //====================================================================== func stringIn(s string, a []string) bool { for _, s2 := range a { if s == s2 { return true } } return false } func parseOnOff(str string) (bool, error) { switch str { case "on", "ON", "On": return true, nil case "off", "OFF", "Off": return false, nil } return false, strconv.ErrSyntax } type setCommand struct{} var _ minibuffer.IAction = setCommand{} func (d setCommand) Run(app gowid.IApp, args ...string) error { var err error var b bool var i uint64 switch len(args) { case 3: switch args[1] { case "auto-scroll": if b, err = parseOnOff(args[2]); err == nil { AutoScroll = b profiles.SetConf("main.auto-scroll", AutoScroll) OpenMessage(fmt.Sprintf("Packet auto-scroll is now %s", gwutil.If(b, "on", "off").(string)), appView, app) } case "copy-timeout": if i, err = strconv.ParseUint(args[2], 10, 32); err == nil { profiles.SetConf("main.copy-command-timeout", i) OpenMessage(fmt.Sprintf("Copy timeout is now %ds", i), appView, app) } case "dark-mode": if b, err = parseOnOff(args[2]); err == nil { SetDarkMode(b) } case "disable-shark-fin": if b, err = strconv.ParseBool(args[2]); err == nil { profiles.SetConf("main.disable-shark-fin", DarkMode) OpenMessage(fmt.Sprintf("Shark-saver is now %s", gwutil.If(b, "off", "on").(string)), appView, app) } case "packet-colors": if b, err = parseOnOff(args[2]); err == nil { PacketColors = b profiles.SetConf("main.packet-colors", PacketColors) OpenMessage(fmt.Sprintf("Packet colors are now %s", gwutil.If(b, "on", "off").(string)), appView, app) } case "suppress-tshark-errors": if b, err = parseOnOff(args[2]); err == nil { profiles.SetConf("main.suppress-tshark-errors", b) if b { OpenMessage("tshark errors will be suppressed.", appView, app) } else { OpenMessage("tshark errors will be displayed.", appView, app) } } case "term": if err = termshark.ValidateTerm(args[2]); err == nil { profiles.SetConf("main.term", args[2]) OpenMessage(fmt.Sprintf("Terminal type is now %s\n(Requires restart)", args[2]), appView, app) } case "pager": profiles.SetConf("main.pager", args[2]) OpenMessage(fmt.Sprintf("Pager is now %s", args[2]), appView, app) default: err = invalidSetCommandErr } case 2: switch args[1] { case "noterm": profiles.DeleteConf("main.term") OpenMessage("Terminal type is now unset\n(Requires restart)", appView, app) case "nopager": profiles.DeleteConf("main.pager") OpenMessage("Pager is now unset", appView, app) default: err = invalidSetCommandErr } } if err != nil { OpenMessage(fmt.Sprintf("Error: %s", err), appView, app) } return err } func (d setCommand) OfferCompletion() bool { return true } func (d setCommand) Arguments(toks []string, app gowid.IApp) []minibuffer.IArg { res := make([]minibuffer.IArg, 0) res = append(res, newSetArg(toks[0])) if len(toks) > 0 { onOffCmds := []string{"auto-scroll", "dark-mode", "packet-colors", "suppress-tshark-errors"} boolCmds := []string{"disable-shark-fin"} intCmds := []string{"disk-cache-size-mb", "copy-command-timeout"} pref := "" if len(toks) > 1 { pref = toks[1] } if stringIn(toks[0], boolCmds) { res = append(res, newBoolArg(pref)) } else if stringIn(toks[0], intCmds) { res = append(res, unhelpfulArg{}) } else if stringIn(toks[0], onOffCmds) { res = append(res, newOnOffArg(pref)) } } return res } //====================================================================== type readCommand struct { complete bool } var _ minibuffer.IAction = readCommand{} func (d readCommand) Run(app gowid.IApp, args ...string) error { var err error if len(args) != 2 { err = invalidReadCommandErr } else { MaybeKeepThenRequestLoadPcap(args[1], FilterWidget.Value(), NoGlobalJump, app) } if err != nil { OpenMessage(fmt.Sprintf("Error: %s", err), appView, app) } return err } func (d readCommand) OfferCompletion() bool { return d.complete } func (d readCommand) Arguments(toks []string, app gowid.IApp) []minibuffer.IArg { res := make([]minibuffer.IArg, 0) pref := "" if len(toks) > 0 { pref = toks[0] } res = append(res, fileArg{substr: pref}) return res } //====================================================================== type recentsCommand struct{} var _ minibuffer.IAction = recentsCommand{} func (d recentsCommand) Run(app gowid.IApp, args ...string) error { var err error if len(args) != 2 { err = invalidRecentsCommandErr } else { MaybeKeepThenRequestLoadPcap(args[1], FilterWidget.Value(), NoGlobalJump, app) } if err != nil { OpenMessage(fmt.Sprintf("Error: %s", err), appView, app) } return err } func (d recentsCommand) OfferCompletion() bool { return true } func (d recentsCommand) Arguments(toks []string, app gowid.IApp) []minibuffer.IArg { res := make([]minibuffer.IArg, 0) pref := "" if len(toks) > 0 { pref = toks[0] } res = append(res, recentsArg{substr: pref}) return res } //====================================================================== type filterCommand struct{} var _ minibuffer.IAction = filterCommand{} func (d filterCommand) Run(app gowid.IApp, args ...string) error { var err error if len(args) != 2 { err = invalidFilterCommandErr } else { setFocusOnDisplayFilter(app) FilterWidget.SetValue(args[1], app) } if err != nil { OpenMessage(fmt.Sprintf("Error: %s", err), appView, app) } return err } func (d filterCommand) OfferCompletion() bool { return true } func (d filterCommand) Arguments(toks []string, app gowid.IApp) []minibuffer.IArg { res := make([]minibuffer.IArg, 0) pref := "" if len(toks) > 0 { pref = toks[0] } res = append(res, filterArg{ field: "main.recent-filters", substr: pref, }) return res } //====================================================================== type themeCommand struct{} var _ minibuffer.IAction = themeCommand{} func (d themeCommand) Run(app gowid.IApp, args ...string) error { var err error if len(args) != 2 { err = invalidThemeCommandErr } else { mode := theme.Mode(app.GetColorMode()).String() // more concise profiles.SetConf(fmt.Sprintf("main.theme-%s", mode), args[1]) theme.Load(args[1], app) SetupColors() OpenMessage(fmt.Sprintf("Set %s theme for terminal mode %v.", args[1], app.GetColorMode()), appView, app) } if err != nil { OpenMessage(fmt.Sprintf("Error: %s", err), appView, app) } return err } func (d themeCommand) OfferCompletion() bool { return true } func (d themeCommand) Arguments(toks []string, app gowid.IApp) []minibuffer.IArg { res := make([]minibuffer.IArg, 0) pref := "" if len(toks) > 0 { pref = toks[0] } modes := make([]string, 0, 3) switch app.GetColorMode() { case gowid.Mode24BitColors: modes = append(modes, "truecolor", "256") case gowid.Mode256Colors: modes = append(modes, "256") default: modes = append(modes, "16") } res = append(res, themeArg{substr: pref, modes: modes}) return res } //====================================================================== type profileCommand struct { termsharkProfiles []string wiresharkProfiles []string } var _ minibuffer.IAction = profileCommand{} func newProfileCommand() *profileCommand { return &profileCommand{ termsharkProfiles: profiles.AllNonDefaultNames(), wiresharkProfiles: termshark.WiresharkProfileNames(), } } func (d profileCommand) Run(app gowid.IApp, args ...string) error { var err error switch len(args) { case 3: switch args[1] { case "use": if !termshark.StringInSlice(args[2], append(d.termsharkProfiles, "default")) { err = fmt.Errorf("%s is not a valid termshark profile", args[2]) break } cur := profiles.Current() // gcla later todo - validate arg2 is in list err = profiles.Use(args[2]) if err == nil { err = ApplyCurrentProfile(app, cur, profiles.Current()) } if err == nil { OpenMessage(fmt.Sprintf("Now using profile %s.", args[2]), appView, app) } case "link": // gcla later todo - need to validate it is in list! if !termshark.StringInSlice(args[2], d.wiresharkProfiles) { err = fmt.Errorf("%s is not a valid Wireshark profile", args[2]) break } curLink := profiles.ConfString("main.wireshark-profile", "") profiles.SetConf("main.wireshark-profile", args[2]) if curLink != args[2] { RequestReload(app) } case "delete": if !termshark.StringInSlice(args[2], d.termsharkProfiles) { err = fmt.Errorf("%s is not a valid termshark profile", args[2]) break } confirmAction( fmt.Sprintf("Really delete profile %s?", args[2]), func(app gowid.IApp) { cur := profiles.Current() curName := profiles.CurrentName() if curName == args[2] { err = profiles.Use("default") if err == nil { err = ApplyCurrentProfile(app, cur, profiles.Current()) } } if err == nil { err = profiles.Delete(args[2]) } if err == nil { msg := fmt.Sprintf("Profile %s deleted.", args[2]) if curName == args[2] { OpenMessage(fmt.Sprintf("%s Switched back to default profile.", msg), appView, app) } else { OpenMessage(msg, appView, app) } } else if err != nil { OpenMessage(fmt.Sprintf("Error: %s", err), appView, app) } }, app, ) default: err = invalidSetCommandErr } case 2: switch args[1] { case "create": openNewProfile(app) case "unlink": curLink := profiles.ConfString("main.wireshark-profile", "") profiles.SetConf("main.wireshark-profile", "") if curLink != "" { RequestReload(app) } default: err = invalidProfileCommandErr } } if err != nil { OpenMessage(fmt.Sprintf("Error: %s", err), appView, app) } return err } func (d profileCommand) OfferCompletion() bool { return true } func (d profileCommand) Arguments(toks []string, app gowid.IApp) []minibuffer.IArg { res := make([]minibuffer.IArg, 0) res = append(res, newProfileArg(toks[0])) if len(toks) > 0 { pref := "" if len(toks) > 1 { pref = toks[1] } switch toks[0] { case "create": case "unlink": case "use": res = append(res, newCachedArg(pref, append(d.termsharkProfiles, "default"))) case "delete": res = append(res, newCachedArg(pref, d.termsharkProfiles)) case "link": res = append(res, newCachedArg(pref, d.wiresharkProfiles)) } } return res } //====================================================================== type mapCommand struct { w *mapkeys.Widget } var _ minibuffer.IAction = mapCommand{} func (d mapCommand) Run(app gowid.IApp, args ...string) error { var err error if len(args) == 3 { key1 := vim.VimStringToKeys(args[1]) if len(key1) != 1 { err = fmt.Errorf("Invalid: first map argument must be a single key (got '%s')", args[1]) } else { keys2 := vim.VimStringToKeys(args[2]) termshark.AddKeyMapping(termshark.KeyMapping{From: key1[0], To: keys2}) mappings := termshark.LoadKeyMappings() for _, mapping := range mappings { d.w.AddMapping(mapping.From, mapping.To, app) } } } else if len(args) == 1 { OpenTemplatedDialogExt(appView, "Key Mappings", fixed, ratio(0.6), app) } else { err = invalidMapCommandErr } if err != nil { OpenMessage(fmt.Sprintf("Error: %s", err), appView, app) } return err } func (d mapCommand) OfferCompletion() bool { return true } func (d mapCommand) Arguments(toks []string, app gowid.IApp) []minibuffer.IArg { res := make([]minibuffer.IArg, 0) if len(toks) == 2 { res = append(res, unhelpfulArg{}, unhelpfulArg{}) } return res } //====================================================================== type unmapCommand struct { w *mapkeys.Widget } var _ minibuffer.IAction = unmapCommand{} func (d unmapCommand) Run(app gowid.IApp, args ...string) error { var err error if len(args) != 2 { err = invalidMapCommandErr } else { key1 := vim.VimStringToKeys(args[1]) d.w.ClearMappings(app) termshark.RemoveKeyMapping(key1[0]) mappings := termshark.LoadKeyMappings() for _, mapping := range mappings { d.w.AddMapping(mapping.From, mapping.To, app) } } if err != nil { OpenMessage(fmt.Sprintf("Error: %s", err), appView, app) } return err } func (d unmapCommand) OfferCompletion() bool { return true } func (d unmapCommand) Arguments(toks []string, app gowid.IApp) []minibuffer.IArg { res := make([]minibuffer.IArg, 0) res = append(res, unhelpfulArg{}) return res } //====================================================================== type helpCommand struct{} var _ minibuffer.IAction = helpCommand{} func (d helpCommand) Run(app gowid.IApp, args ...string) error { var err error switch len(args) { case 2: switch args[1] { case "cmdline": OpenTemplatedDialog(appView, "CmdLineHelp", app) case "map": OpenTemplatedDialog(appView, "MapHelp", app) case "set": OpenTemplatedDialog(appView, "SetHelp", app) default: OpenTemplatedDialog(appView, "VimHelp", app) } default: OpenTemplatedDialog(appView, "UIHelp", app) } return err } func (d helpCommand) OfferCompletion() bool { return true } func (d helpCommand) Arguments(toks []string, app gowid.IApp) []minibuffer.IArg { res := make([]minibuffer.IArg, 0) if len(toks) == 1 { res = append(res, newHelpArg(toks[0])) } return res } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.4.0/ui/logsui.go000066400000000000000000000053011426312004500157570ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // +build !windows // Package ui contains user-interface functions and helpers for termshark. package ui import ( "fmt" "io/ioutil" "os" "github.com/gcla/gowid" "github.com/gcla/gowid/widgets/holder" "github.com/gcla/gowid/widgets/terminal" "github.com/gcla/termshark/v2" "github.com/gcla/termshark/v2/configs/profiles" "github.com/gcla/termshark/v2/widgets/fileviewer" log "github.com/sirupsen/logrus" ) //====================================================================== func pager() string { res := profiles.ConfString("main.pager", "") if res == "" { res = os.Getenv("PAGER") } return res } // Dynamically load conv. If the convs window was last opened with a different filter, and the "limit to // filter" checkbox is checked, then the data needs to be reloaded. func openLogsUi(app gowid.IApp) { openFileUi(termshark.CacheFile("termshark.log"), false, fileviewer.Options{ Name: "Logs", GoToBottom: true, Pager: pager(), }, app) } func openConfigUi(app gowid.IApp) { tmp, err := ioutil.TempFile("", "termshark-*.toml") if err != nil { OpenError(fmt.Sprintf("Could not create temp file: %v", err), app) return } tmp.Close() err = profiles.WriteConfigAs(tmp.Name()) if err != nil { OpenError(fmt.Sprintf("Could not run config viewer\n\n%v", err), app) } else { openFileUi(tmp.Name(), true, fileviewer.Options{ Name: "Config", Pager: pager(), }, app) } } func openFileUi(file string, delete bool, opt fileviewer.Options, app gowid.IApp) { logsUi, err := fileviewer.New(file, gowid.WidgetCallback{"cb", func(app gowid.IApp, w gowid.IWidget) { t := w.(*terminal.Widget) ecode := t.Cmd.ProcessState.ExitCode() // -1 for signals - don't show an error for that if ecode != 0 && ecode != -1 { d := OpenError(fmt.Sprintf("Could not run file viewer\n\n%s", t.Cmd.ProcessState), app) d.OnOpenClose(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, w gowid.IWidget) { closeFileUi(app) })) } else { closeFileUi(app) } if delete && false { err := os.Remove(file) if err != nil { log.Warnf("Problem deleting %s: %v", file, err) } } }, }, opt, ) if err != nil { OpenError(fmt.Sprintf("Error launching terminal: %v", err), app) return } logsView := holder.New(logsUi) appViewNoKeys.SetSubWidget(logsView, app) } func closeFileUi(app gowid.IApp) { appViewNoKeys.SetSubWidget(mainView, app) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.4.0/ui/logsui_windows.go000066400000000000000000000013441426312004500175340ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package ui contains user-interface functions and helpers for termshark. package ui import ( "github.com/gcla/gowid" ) // Dynamically load conv. If the convs window was last opened with a different filter, and the "limit to // filter" checkbox is checked, then the data needs to be reloaded. func openLogsUi(app gowid.IApp) { } func openConfigUi(app gowid.IApp) { } //====================================================================== //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.4.0/ui/menu.go000066400000000000000000000124611426312004500154260ustar00rootroot00000000000000// Copyright 2019-2022 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/v2" ) //====================================================================== 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.4.0/ui/menuutil/000077500000000000000000000000001426312004500157715ustar00rootroot00000000000000termshark-2.4.0/ui/menuutil/menu.go000066400000000000000000000150101426312004500172610ustar00rootroot00000000000000// Copyright 2019-2022 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/list" "github.com/gcla/gowid/widgets/menu" "github.com/gcla/gowid/widgets/selectable" "github.com/gcla/gowid/widgets/styled" "github.com/gcla/gowid/widgets/text" "github.com/gcla/gowid/widgets/vpadding" "github.com/gcla/termshark/v2/widgets/appkeys" "github.com/gdamore/tcell/v2" ) //====================================================================== type SimpleMenuActionFunction func(app gowid.IApp, site menu.ISite) type SimpleMenuItem struct { Txt string Key gowid.IKey CB gowid.WidgetChangedFunction } func MakeMenuDivider() SimpleMenuItem { return SimpleMenuItem{} } type SiteMap map[gowid.IWidget]menu.ISite func MakeMenuWithHotKeys(items []SimpleMenuItem, sites SiteMap) (gowid.IWidget, int) { return makeMenuWithHotKeys(items, true, sites) } func MakeMenu(items []SimpleMenuItem, sites SiteMap) (gowid.IWidget, int) { return makeMenuWithHotKeys(items, false, sites) } type widgetWithSite struct { gowid.IWidget menu.ISite } func (w *widgetWithSite) String() string { return fmt.Sprintf("withsite[%v@%v]", w.ISite, w.IWidget) } func makeMenuWithHotKeys(items []SimpleMenuItem, showKeys bool, sites SiteMap) (gowid.IWidget, int) { menu1Widgets := make([]gowid.IWidget, len(items)) menu1HotKeys := make([]gowid.IWidget, len(items)) menu1Sites := make([]*menu.SiteWidget, len(items)) // Figure out the length of the longest hotkey string representation maxHK := 0 for _, w := range items { if w.Txt != "" { k := fmt.Sprintf("%v", w.Key) if len(k) > maxHK { maxHK = len(k) } } } maxVal := 0 // 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)) if len(w.Txt) > maxVal { maxVal = len(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 menu1Sites[i] = menu.NewSite() if sites != nil { sites[load1B] = menu1Sites[i] } } } for i, w := range menu1Widgets { if w != nil { menu1Widgets[i] = &widgetWithSite{ IWidget: styled.NewInvertedFocus(selectable.New(w), gowid.MakePaletteRef("default")), ISite: menu1Sites[i], } } } for i, w := range menu1HotKeys { if w != nil { menu1HotKeys[i] = styled.NewInvertedFocus(w, gowid.MakePaletteRef("default")) } } fixed := gowid.RenderFixed{} startCol := 0 if showKeys { startCol = 2 } // Build the menu "row" for each menu entry menu1Widgets2 := make([]gowid.IWidget, len(menu1Widgets)) for i, w := range menu1Widgets { if w == nil { menu1Widgets2[i] = vpadding.New(divider.NewUnicode(), gowid.VAlignTop{}, gowid.RenderFlow{}) } else { containerWidgets := make([]gowid.IContainerWidget, 0, 3) if showKeys { containerWidgets = append(containerWidgets, &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: maxHK}, ), D: fixed, }, &gowid.ContainerWidget{ IWidget: text.New("| "), D: fixed, }, ) } containerWidgets = append(containerWidgets, &gowid.ContainerWidget{ IWidget: w, D: fixed, }, &gowid.ContainerWidget{ IWidget: menu1Sites[i], D: fixed, }, ) menu1Widgets2[i] = columns.New( containerWidgets, columns.Options{ StartColumn: startCol, }, ) } } 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( list.New(list.NewSimpleListWalker(menu1Widgets2)), ), 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, maxHK + maxVal + 2 + 2 // "| " + border } //====================================================================== 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 MenuOpener menu.IOpener // For integrating with UI app - the menu needs to be told what's underneath when opened } 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.MenuOpener.CloseMenu(left.Cur, app) left.MenuOpener.OpenMenu(left.Next, left.Site, app) left.Container.SetFocus(app, left.Focus) // highlight next menu selector res = true } case tcell.KeyRight: if right != nil { right.MenuOpener.CloseMenu(right.Cur, app) right.MenuOpener.OpenMenu(right.Next, 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.4.0/ui/messages.go000066400000000000000000000174611426312004500162760ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package ui contains user-interface functions and helpers for termshark. package ui import ( "fmt" "io" "log" "sync" "text/template" "github.com/blang/semver" "github.com/gcla/termshark/v2" "github.com/jessevdk/go-flags" ) //====================================================================== // For fixing off-by-one errors in packet marks - NOT NEEDED now var funcMap = template.FuncMap{ "inc": func(i int) int { return i + 1 }, } var TemplateData map[string]interface{} var Templates = template.Must(template.New("Help").Funcs(funcMap).Parse(` {{define "NameVer"}}termshark {{.Version}}{{end}} {{define "TsharkVer"}}using tshark {{.TsharkVersion}} (from {{.TsharkAbsolutePath}}){{end}} {{define "OneLine"}}A wireshark-inspired terminal user interface for tshark. Analyze network traffic interactively from your terminal.{{end}} {{define "Header"}}{{template "NameVer" .}} {{template "OneLine"}} See https://termshark.io for more information.{{end}} {{define "Footer"}} If --pass-thru is true (or auto, and stdout is not a tty), tshark will be executed with the supplied command-line flags. You can provide tshark-specific flags and they will be passed through to tshark (-n, -d, -T, etc). For example: $ termshark -r file.pcap -T psml -n | less{{end}} {{define "UIUserGuide"}}{{.UserGuideURL}} {{.CopyCommandMessage}}{{end}} {{define "UIFAQ"}}{{.FAQURL}} {{.CopyCommandMessage}}{{end}} {{define "UIBug"}}{{.BugURL}} {{.CopyCommandMessage}}{{end}} {{define "UIFeature"}}{{.FeatureURL}} {{.CopyCommandMessage}}{{end}} {{define "UIHelp"}}{{template "NameVer" .}} A wireshark-inspired tui for tshark. Analyze network traffic interactively from your terminal. /__ - Go to display filter/stream search q__ - Quit tab - Switch panes c__ - Switch to copy-mode |__ - Cycle through pane layouts \__ - Toggle pane zoom esc - Activate menu +/- - Adjust horizontal split - Adjust vertical split :__ - Activate cmdline mode (see help cmdline) z__ - Maximize/restore any modal dialog ?__ - Display help In the filter, type a wireshark display filter expression. Most terminals will support using the mouse! Try clicking the Close button. Use shift-left-mouse to copy and shift-right-mouse to paste.{{end}} {{define "VimHelp"}}{{template "NameVer" .}} Navigate the UI using vim-style keys. hjkl___ - Move left/down/up/right in various views gg_____ - Go to the top of the current table G______ - Go to the bottom of the current table 5gg____ - Go to the 5th row of the table C-w C-w - Switch panes (same as tab) C-w_=__ - Equalize pane spacing ma_____ - Mark current packet (use a through z) 'a_____ - Jump to packet marked 'a' mA_____ - Mark current packet + pcap (use A through Z) 'A_____ - Jump to packet + pcap marked 'A' ''_____ - After a jump; jump back to prior packet ZZ_____ - Quit without confirmation See also help cmdline.{{end}} {{define "CmdLineHelp"}}{{template "NameVer" .}} Activate cmdline mode with the : key. Hit tab to see and choose possible completions. capinfo______ - Capture file properties clear-filter_ - Clear the display filter and apply clear-packets - Clear the current pcap columns______ - Choose the columns to display config_______ - Show termshark's config file (Unix-only) convs________ - Open conversations view filter_______ - Choose a display filter from recently-used help_________ - Various help dialogs load_________ - Load a pcap from the filesystem logs_________ - Show termshark's log file (Unix-only) map__________ - Map a keypress to a key sequence (see help map) marks________ - Show file-local and global packet marks menu_________ - Open the UI Misc menu no-theme_____ - Clear theme for the current terminal color mode profile______ - Profile actions - create, use, delete, etc quit_________ - Quit termshark recents______ - Load a pcap from those recently-used set__________ - Set various config properties (see help set) streams______ - Open stream reassembly view theme________ - Choose a theme for the current terminal color mode unmap________ - Remove a keypress mapping wormhole_____ - Prepare to transfer the current pcap{{end}} {{define "SetHelp"}}{{template "NameVer" .}} Use the cmdline set command to change configuration. Type :set and hit tab for options. auto-scroll___________ - scroll during live captures copy-timeout__________ - wait this long before failing a copy dark-mode_____________ - enable or disable dark-mode disable-shark-fin_____ - switch off the secret shark fin packet-colors_________ - use colors in the packet list view pager_________________ - pager (used for termshark's log file) nopager_______________ - disable the pager (use PAGER instead) suppress-tshark-errors - don't show tshark errors in the UI term__________________ - make termshark assume this terminal type noterm________________ - disable the terminal type (use TERM){{end}} {{define "MapHelp"}}{{template "NameVer" .}} Use the cmdline map command to set key macros e.g. map ZZ - hit f1 key to quit Use vim-style syntax for key-presses. Printable characters represent themselves. Compound keys can be: - , , , , , , Use the unmap command to remove a mapping.{{end}} {{define "CopyModeHelp"}}{{template "NameVer" .}} termshark is in copy-mode. You can press: 'q', 'c' - Exit copy-mode ctrl-c__ - Copy from selected widget left____ - Widen selection right___ - Narrow selection '?'_____ - Display copy-mode help {{end}} {{define "Marks"}}{{if not .Marks}}No local marks are set{{else}}Mark Packet Summary{{range $key, $value := .Marks }} {{printf " %c" $key}}{{printf "%6d" $value.Pos}} {{printf "%s" $value.Summary}}{{end}}{{end}} {{if not .GlobalMarks}}No cross-file marks are set{{else}}Mark Packet File Summary{{range $key, $value := .GlobalMarks }} {{printf " %-4c" $key}} {{printf "%-7d" $value.Pos}}{{printf "%-18s" $value.Base}}{{printf "%s" $value.Summary}}{{end}}{{end}}{{end}} {{define "Key Mappings"}}{{if .Maps.None}}No key mappings are set{{else}} From To {{range $mapping := .Maps.Get }} {{printf " %-14v" $mapping.From}}{{printf "%v" $mapping.To}} {{end}}{{end}} {{end}} `)) //====================================================================== var DoOnce sync.Once func EnsureTemplateData() { DoOnce.Do(func() { TemplateData = make(map[string]interface{}) }) } func init() { EnsureTemplateData() TemplateData["Version"] = termshark.Version TemplateData["FAQURL"] = termshark.FAQURL TemplateData["UserGuideURL"] = termshark.UserGuideURL TemplateData["BugURL"] = termshark.BugURL TemplateData["FeatureURL"] = termshark.FeatureURL } func WriteHelp(p *flags.Parser, w io.Writer) { if err := Templates.ExecuteTemplate(w, "Header", TemplateData); err != nil { log.Fatal(err) } fmt.Fprintln(w) fmt.Fprintln(w) p.WriteHelp(w) if err := Templates.ExecuteTemplate(w, "Footer", TemplateData); err != nil { log.Fatal(err) } fmt.Fprintln(w) fmt.Fprintln(w) } func WriteVersion(p *flags.Parser, w io.Writer) { if err := Templates.ExecuteTemplate(w, "NameVer", TemplateData); err != nil { log.Fatal(err) } fmt.Fprintln(w) } func WriteTsharkVersion(p *flags.Parser, bin string, ver semver.Version, w io.Writer) { TemplateData["TsharkVersion"] = ver.String() TemplateData["TsharkAbsolutePath"] = bin if err := Templates.ExecuteTemplate(w, "TsharkVer", TemplateData); err != nil { log.Fatal(err) } fmt.Fprintln(w) } func WriteMarks(p *flags.Parser, marks map[rune]int, w io.Writer) { if err := Templates.ExecuteTemplate(w, "Marks", TemplateData); err != nil { log.Fatal(err) } fmt.Fprintln(w) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.4.0/ui/newprofile.go000066400000000000000000000160661426312004500166410ustar00rootroot00000000000000// Copyright 2019-2022 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 ( "fmt" "github.com/flytam/filenamify" "github.com/gcla/gowid" "github.com/gcla/gowid/widgets/button" "github.com/gcla/gowid/widgets/checkbox" "github.com/gcla/gowid/widgets/columns" "github.com/gcla/gowid/widgets/dialog" "github.com/gcla/gowid/widgets/disable" "github.com/gcla/gowid/widgets/divider" "github.com/gcla/gowid/widgets/edit" "github.com/gcla/gowid/widgets/framed" "github.com/gcla/gowid/widgets/menu" "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/configs/profiles" "github.com/gcla/termshark/v2/ui/menuutil" "github.com/gcla/termshark/v2/widgets/appkeys" "github.com/gdamore/tcell/v2" ) //====================================================================== var invalidNameErr = fmt.Errorf("Please ensure profile name can be used as a filename.") // validProfileName makes sure the profile name is not in use AND can be embedded // in a directory path. It's easier that way and not much of a limitation. func validProfileName(name string) error { if name == "" { return fmt.Errorf("No profile name provided.") } if termshark.StringInSlice(name, profiles.AllNames()) { return fmt.Errorf("Profile %s already exists.", name) } fname, err := filenamify.Filenamify(name, filenamify.Options{}) if err != nil { return invalidNameErr } if name != fname { return invalidNameErr } return nil } func openNewProfile(app gowid.IApp) { var copyProfileDialog *dialog.Widget var redoDialog *dialog.Widget var basedOff string var invalidWidget *text.Widget var invalidText string var okFunc func(gowid.IApp, gowid.IWidget) openCopy := func() { copyProfileDialog.Open(appView, units(len(basedOff)+20), app) } openRedo := func() { invalidWidget.SetText(invalidText, app) redoDialog.Open(appView, units(len(invalidText)+10), app) } nameWidget := edit.New() nameWidgetExt := appkeys.New( nameWidget, func(ev *tcell.EventKey, app gowid.IApp) bool { res := false switch ev.Key() { case tcell.KeyEnter: okFunc(app, nameWidget) res = true } return res }, appkeys.Options{ ApplyBefore: true, }, ) invalidText = "Placeholder" invalidWidget = text.New(invalidText) openAgainBtn := dialog.Button{ Msg: "Ok", Action: gowid.MakeWidgetCallback("exec", gowid.WidgetChangedFunction(func(app gowid.IApp, _ gowid.IWidget) { redoDialog.Close(app) openCopy() })), } redoView := framed.NewSpace(invalidWidget) redoDialog = dialog.New( redoView, dialog.Options{ Buttons: []dialog.Button{openAgainBtn}, NoShadow: true, BackgroundStyle: gowid.MakePaletteRef("dialog"), BorderStyle: gowid.MakePaletteRef("dialog"), ButtonStyle: gowid.MakePaletteRef("dialog-button"), Modal: true, FocusOnWidget: true, }, ) var profileString string // what's displayed in the drop down button var profileDropDown *button.Widget var linkCheckBox *checkbox.Widget var profileMenu *menu.Widget okFunc = func(app gowid.IApp, _ gowid.IWidget) { copyProfileDialog.Close(app) name := nameWidget.Text() err := validProfileName(name) if err != nil { invalidText = err.Error() openRedo() return } prev := profiles.Current() err = profiles.CopyToAndUse(name) if err != nil { OpenError(err.Error(), app) return } cur := profiles.Current() // Set this after CopyAndUse so I can distinguish the prior Wireshark // profile link (if any) from the current. If there's a change, the current // source is reloaded. if linkCheckBox.IsChecked() && profileString != "" { profiles.SetConfIn(cur, "main.wireshark-profile", profileString) } err = ApplyCurrentProfile(app, prev, cur) if err != nil { OpenError(err.Error(), app) return } OpenMessage(fmt.Sprintf("Now using new profile %s.", name), appView, app) } okBtn := dialog.Button{ Msg: "Ok", Action: gowid.MakeWidgetCallback("exec", gowid.WidgetChangedFunction(okFunc)), } basedOff = fmt.Sprintf("This profile will be based off of %s.", profiles.CurrentName()) linkCheckBox = checkbox.New(false) linkWidget := columns.NewFixed(text.New("Link to Wireshark: "), linkCheckBox) folderStrings := termshark.WiresharkProfileNames() enableWireshark := len(folderStrings) > 0 if enableWireshark { profileString = folderStrings[0] profileDropDown = button.New(text.New(profileString)) } menuItems := make([]menuutil.SimpleMenuItem, 0) for i, folder := range folderStrings { folderCopy := folder menuItems = append(menuItems, menuutil.SimpleMenuItem{ Txt: folder, Key: gowid.MakeKey('1' + rune(i)), CB: func(app gowid.IApp, w2 gowid.IWidget) { profileString = folderCopy profileDropDown.SetSubWidget(text.New(profileString), app) }, }, ) } lb, _ := menuutil.MakeMenuWithHotKeys(menuItems, nil) profileMenu = menu.New("profilemenu", lb, fixed, menu.Options{ Modal: true, OpenCloser: &multiMenu1Opener, CloseKeysProvided: true, CloseKeys: []gowid.IKey{ gowid.MakeKey('q'), gowid.MakeKeyExt(tcell.KeyLeft), gowid.MakeKeyExt(tcell.KeyEscape), gowid.MakeKeyExt(tcell.KeyCtrlC), }, }) profileMenuSite := menu.NewSite(menu.SiteOptions{YOffset: 1}) if enableWireshark { profileDropDown.OnClick(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, w2 gowid.IWidget) { multiMenu1Opener.OpenMenu(profileMenu, profileMenuSite, app) })) } s2Btn := disable.NewDisabled(profileDropDown) styledProfileBtn := styled.NewExt( s2Btn, gowid.MakePaletteRef("button"), gowid.MakePaletteRef("button-focus"), ) linkCheckBox.OnClick(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, w2 gowid.IWidget) { s2Btn.Set(!linkCheckBox.IsChecked()) })) dialogWidgets := make([]interface{}, 0, 8) dialogWidgets = append(dialogWidgets, text.New(basedOff), divider.NewBlank(), text.New("Please enter a name for this profile:"), divider.NewBlank(), framed.NewUnicode(nameWidgetExt), ) // Add the option to link to a wireshark profile if enableWireshark { dialogWidgets = append(dialogWidgets, divider.NewBlank(), columns.NewWithDim( gowid.RenderWithWeight{1}, &gowid.ContainerWidget{ IWidget: linkWidget, D: gowid.RenderFixed{}, }, text.New(" "), columns.NewFixed(profileMenuSite, styledProfileBtn), ), ) } newProfileView := framed.NewSpace(pile.NewFlow(dialogWidgets...)) copyProfileDialog = dialog.New( newProfileView, dialog.Options{ Buttons: []dialog.Button{okBtn, dialog.Cancel}, NoShadow: true, BackgroundStyle: gowid.MakePaletteRef("dialog"), BorderStyle: gowid.MakePaletteRef("dialog"), ButtonStyle: gowid.MakePaletteRef("dialog-button"), Modal: true, FocusOnWidget: true, }, ) openCopy() } //====================================================================== // Local Variables: // mode: Go // fill-column: 78 // End: termshark-2.4.0/ui/palette.go000066400000000000000000000236061426312004500161230ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package ui contains user-interface functions and helpers for termshark. package ui import ( "fmt" "github.com/gcla/gowid" "github.com/gcla/termshark/v2/pkg/theme" log "github.com/sirupsen/logrus" ) //====================================================================== var ( RegularPalette gowid.Palette DarkModePalette gowid.Palette ) func SetupColors() { //====================================================================== // Regular mode // RegularPalette = gowid.Palette{ "default": gowid.MakePaletteEntry(lfg("default"), lbg("default")), "title": gowid.MakeForeground(lfg("title")), "packet-list-row-focus": gowid.MakePaletteEntry(lfg("packet-list-row-focus"), lbg("packet-list-row-focus")), "packet-list-row-selected": gowid.MakePaletteEntry(lfg("packet-list-row-selected"), lbg("packet-list-row-selected")), "packet-list-cell-focus": gowid.MakePaletteEntry(lfg("packet-list-cell-focus"), lbg("packet-list-cell-focus")), "packet-list-cell-selected": gowid.MakePaletteEntry(lfg("packet-list-cell-selected"), lbg("packet-list-cell-selected")), "packet-struct-focus": gowid.MakePaletteEntry(lfg("packet-struct-focus"), lbg("packet-struct-focus")), "packet-struct-selected": gowid.MakePaletteEntry(lfg("packet-struct-selected"), lbg("packet-struct-selected")), "filter-menu": gowid.MakeStyledPaletteEntry(lfg("filter-menu"), lbg("filter-menu"), gowid.StyleBold), "filter-valid": gowid.MakePaletteEntry(lfg("filter-valid"), lbg("filter-valid")), "filter-invalid": gowid.MakePaletteEntry(lfg("filter-invalid"), lbg("filter-invalid")), "filter-intermediate": gowid.MakePaletteEntry(lfg("filter-intermediate"), lbg("filter-intermediate")), "filter-empty": gowid.MakePaletteEntry(dfg("filter-empty"), dbg("filter-empty")), "dialog": gowid.MakePaletteEntry(lfg("dialog"), lbg("dialog")), "dialog-button": gowid.MakePaletteEntry(lfg("dialog-button"), lbg("dialog-button")), "cmdline": gowid.MakePaletteEntry(lfg("cmdline"), lbg("cmdline")), "cmdline-button": gowid.MakePaletteEntry(lfg("cmdline-button"), lbg("cmdline-button")), "cmdline-border": gowid.MakePaletteEntry(lfg("cmdline-border"), lbg("cmdline-border")), "button": gowid.MakePaletteEntry(lfg("button"), lbg("button")), "button-focus": gowid.MakePaletteEntry(lfg("button-focus"), lbg("button-focus")), "button-selected": gowid.MakePaletteEntry(lfg("button-selected"), lbg("button-selected")), "progress-default": gowid.MakeStyledPaletteEntry(lfg("progress-default"), lbg("progress-default"), gowid.StyleBold), "progress-complete": gowid.MakeStyleMod(gowid.MakePaletteRef("progress-default"), gowid.MakeBackground(lbg("progress-complete"))), "progress-spinner": gowid.MakePaletteEntry(lfg("progress-spinner"), lbg("progress-spinner")), "hex-byte-selected": gowid.MakePaletteEntry(lfg("hex-byte-selected"), lbg("hex-byte-selected")), "hex-byte-unselected": gowid.MakePaletteEntry(lfg("hex-byte-unselected"), lbg("hex-byte-unselected")), "hex-field-selected": gowid.MakePaletteEntry(lfg("hex-field-selected"), lbg("hex-field-selected")), "hex-field-unselected": gowid.MakePaletteEntry(lfg("hex-field-unselected"), lbg("hex-field-unselected")), "hex-layer-selected": gowid.MakePaletteEntry(lfg("hex-layer-selected"), lbg("hex-layer-selected")), "hex-layer-unselected": gowid.MakePaletteEntry(lfg("hex-layer-unselected"), lbg("hex-layer-unselected")), "hex-interval-selected": gowid.MakePaletteEntry(lfg("hex-interval-selected"), lbg("hex-interval-selected")), "hex-interval-unselected": gowid.MakePaletteEntry(lfg("hex-interval-unselected"), lbg("hex-interval-unselected")), "copy-mode-label": gowid.MakePaletteEntry(lfg("copy-mode-label"), lbg("copy-mode-label")), "copy-mode": gowid.MakePaletteEntry(lfg("copy-mode"), lbg("copy-mode")), "copy-mode-alt": gowid.MakePaletteEntry(lfg("copy-mode-alt"), lbg("copy-mode-alt")), "stream-client": gowid.MakePaletteEntry(lfg("stream-client"), lbg("stream-client")), "stream-server": gowid.MakePaletteEntry(lfg("stream-server"), lbg("stream-server")), "stream-match": gowid.MakePaletteEntry(lfg("stream-match"), lbg("stream-match")), "stream-search": gowid.MakePaletteEntry(lfg("stream-search"), lbg("stream-search")), } //====================================================================== // Dark mode // DarkModePalette = gowid.Palette{ "default": gowid.MakePaletteEntry(dfg("default"), dbg("default")), "title": gowid.MakeForeground(dfg("title")), "current-capture": gowid.MakeForeground(dfg("current-capture")), "packet-list-row-focus": gowid.MakePaletteEntry(dfg("packet-list-row-focus"), dbg("packet-list-row-focus")), "packet-list-row-selected": gowid.MakePaletteEntry(dfg("packet-list-row-selected"), dbg("packet-list-row-selected")), "packet-list-cell-focus": gowid.MakePaletteEntry(dfg("packet-list-cell-focus"), dbg("packet-list-cell-focus")), "packet-list-cell-selected": gowid.MakePaletteEntry(dfg("packet-list-cell-selected"), dbg("packet-list-cell-selected")), "packet-struct-focus": gowid.MakePaletteEntry(dfg("packet-struct-focus"), dbg("packet-struct-focus")), "packet-struct-selected": gowid.MakePaletteEntry(dfg("packet-struct-selected"), dbg("packet-struct-selected")), "filter-menu": gowid.MakeStyledPaletteEntry(dfg("filter-menu"), dbg("filter-menu"), gowid.StyleBold), "filter-valid": gowid.MakePaletteEntry(dfg("filter-valid"), dbg("filter-valid")), "filter-invalid": gowid.MakePaletteEntry(dfg("filter-invalid"), dbg("filter-invalid")), "filter-intermediate": gowid.MakePaletteEntry(dfg("filter-intermediate"), dbg("filter-intermediate")), "filter-empty": gowid.MakePaletteEntry(dfg("filter-empty"), dbg("filter-empty")), "dialog": gowid.MakePaletteEntry(dfg("dialog"), dbg("dialog")), "dialog-button": gowid.MakePaletteEntry(dfg("dialog-button"), dbg("dialog-button")), "cmdline": gowid.MakePaletteEntry(dfg("cmdline"), dbg("cmdline")), "cmdline-button": gowid.MakePaletteEntry(dfg("cmdline-button"), dbg("cmdline-button")), "cmdline-border": gowid.MakePaletteEntry(dfg("cmdline-border"), dbg("cmdline-border")), "button": gowid.MakePaletteEntry(dfg("button"), dbg("button")), "button-focus": gowid.MakePaletteEntry(dfg("button-focus"), dbg("button-focus")), "button-selected": gowid.MakePaletteEntry(dfg("button-selected"), dbg("button-selected")), "progress-default": gowid.MakeStyledPaletteEntry(dfg("progress-default"), dbg("progress-default"), gowid.StyleBold), "progress-complete": gowid.MakeStyleMod(gowid.MakePaletteRef("progress-default"), gowid.MakeBackground(dbg("progress-complete"))), "progress-spinner": gowid.MakePaletteEntry(dfg("spinner"), dbg("spinner")), "hex-byte-selected": gowid.MakeStyledPaletteEntry(dfg("hex-byte-selected"), dbg("hex-byte-selected"), gowid.StyleBold), "hex-byte-unselected": gowid.MakeStyledPaletteEntry(dfg("hex-byte-unselected"), dbg("hex-byte-unselected"), gowid.StyleBold), "hex-field-selected": gowid.MakePaletteEntry(dfg("hex-field-selected"), dbg("hex-field-selected")), "hex-field-unselected": gowid.MakePaletteEntry(dfg("hex-field-unselected"), dbg("hex-field-unselected")), "hex-layer-selected": gowid.MakePaletteEntry(dfg("hex-layer-selected"), dbg("hex-layer-selected")), "hex-layer-unselected": gowid.MakePaletteEntry(dfg("hex-layer-unselected"), dbg("hex-layer-unselected")), "hex-interval-selected": gowid.MakePaletteEntry(dfg("hex-interval-selected"), dbg("hex-interval-selected")), "hex-interval-unselected": gowid.MakePaletteEntry(dfg("hex-interval-unselected"), dbg("hex-interval-unselected")), "stream-client": gowid.MakePaletteEntry(dfg("stream-client"), dbg("stream-client")), "stream-server": gowid.MakePaletteEntry(dfg("stream-server"), dbg("stream-server")), "copy-mode-label": gowid.MakePaletteEntry(dfg("copy-mode-label"), dbg("copy-mode-label")), "copy-mode": gowid.MakePaletteEntry(dfg("copy-mode"), dbg("copy-mode")), "copy-mode-alt": gowid.MakePaletteEntry(dfg("copy-mode-alt"), dbg("copy-mode-alt")), "stream-match": gowid.MakePaletteEntry(dfg("stream-match"), dbg("stream-match")), "stream-search": gowid.MakePaletteEntry(dfg("stream-search"), dbg("stream-search")), } } func dfg(key string) gowid.IColor { return tomlCol(key, theme.Foreground, "dark") } func dbg(key string) gowid.IColor { return tomlCol(key, theme.Background, "dark") } func lfg(key string) gowid.IColor { return tomlCol(key, theme.Foreground, "light") } func lbg(key string) gowid.IColor { return tomlCol(key, theme.Background, "light") } func tomlCol(key string, layer theme.Layer, hue string) gowid.IColor { rule := fmt.Sprintf("%s.%s", hue, key) col, err := theme.MakeColorSafe(rule, layer) if err == nil { return col } else { // Warn if the user has defined themes.rules.etcetc, but the resulting // color can't be resolved. When this is called, we should always have a // theme loaded because we fall back to the "default" theme if no other is // specified. log.Warnf("Could not understand configured theme color '%s'", key) } return gowid.ColorBlack } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.4.0/ui/prochandlers.go000066400000000000000000000227501426312004500171500ustar00rootroot00000000000000// Copyright 2019-2022 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" "time" "github.com/gcla/gowid" "github.com/gcla/gowid/widgets/table" "github.com/gcla/termshark/v2" "github.com/gcla/termshark/v2/configs/profiles" "github.com/gcla/termshark/v2/pkg/pcap" log "github.com/sirupsen/logrus" ) //====================================================================== type NoHandlers struct{} //====================================================================== type updateCurrentCaptureInTitle struct { Ld *pcap.PacketLoader } var _ pcap.IBeforeBegin = updateCurrentCaptureInTitle{} var _ pcap.IClear = updateCurrentCaptureInTitle{} func MakeUpdateCurrentCaptureInTitle() updateCurrentCaptureInTitle { return updateCurrentCaptureInTitle{ Ld: Loader, } } func (t updateCurrentCaptureInTitle) BeforeBegin(code pcap.HandlerCode, app gowid.IApp) { if code&pcap.PsmlCode != 0 { cap := t.Ld.String() if t.Ld.CaptureFilter() != "" { cap = fmt.Sprintf("%s (%s)", cap, t.Ld.CaptureFilter()) } currentCapture.SetText(cap, app) currentCaptureWidgetHolder.SetSubWidget(currentCaptureWidget, app) } } func (t updateCurrentCaptureInTitle) OnClear(code pcap.HandlerCode, app gowid.IApp) { currentCaptureWidgetHolder.SetSubWidget(nullw, app) } //====================================================================== type updatePacketViews struct { Ld *pcap.PacketLoader } var _ pcap.IOnError = updatePacketViews{} var _ pcap.IClear = updatePacketViews{} var _ pcap.IBeforeBegin = updatePacketViews{} var _ pcap.IAfterEnd = updatePacketViews{} func MakePacketViewUpdater() updatePacketViews { res := updatePacketViews{} res.Ld = Loader return res } func (t updatePacketViews) OnClear(code pcap.HandlerCode, app gowid.IApp) { clearPacketViews(app) if packetListView != nil { updatePacketListWithData(t.Ld, app) } } func (t updatePacketViews) BeforeBegin(code pcap.HandlerCode, app gowid.IApp) { if code&pcap.PsmlCode == 0 { return } ch2 := Loader.PsmlFinishedChan clearPacketViews(app) t.Ld.PsmlLoader.Lock() defer t.Ld.PsmlLoader.Unlock() setPacketListWidgets(t.Ld, app) // Start this after widgets have been cleared, to get focus change termshark.TrackedGo(func() { fn2 := func() { app.Run(gowid.RunFunction(func(app gowid.IApp) { updatePacketListWithData(Loader, app) })) } termshark.RunOnDoubleTicker(ch2, fn2, time.Duration(100)*time.Millisecond, time.Duration(2000)*time.Millisecond, 10) }, Goroutinewg) } func (t updatePacketViews) AfterEnd(code pcap.HandlerCode, app gowid.IApp) { if code&pcap.PsmlCode == 0 { return } updatePacketListWithData(t.Ld, app) StopEmptyStructViewTimer() StopEmptyHexViewTimer() log.Infof("Load operation complete") } func (t updatePacketViews) OnError(code pcap.HandlerCode, app gowid.IApp, err error) { if code&pcap.PsmlCode == 0 { return } log.Error(err) if !Running { fmt.Fprintf(os.Stderr, "%v\n", err) RequestQuit() } else { if !profiles.ConfBool("main.suppress-tshark-errors", true) { var errstr string if kverr, ok := err.(gowid.KeyValueError); ok { errstr = termshark.KeyValueErrorString(kverr) } else { errstr = fmt.Sprintf("%v", err) } OpenLongError(errstr, app) } StopEmptyStructViewTimer() StopEmptyHexViewTimer() } } //====================================================================== type SimpleErrors struct{} var _ pcap.IOnError = SimpleErrors{} func (t SimpleErrors) OnError(code pcap.HandlerCode, app gowid.IApp, err error) { if code&pcap.NoneCode == 0 { return } log.Error(err) // Hack to avoid picking up errors at other parts of the load // cycle. There should be specific handlers for specific errors. if !profiles.ConfBool("main.suppress-tshark-errors", true) { app.Run(gowid.RunFunction(func(app gowid.IApp) { OpenError(fmt.Sprintf("%v", err), app) })) } } //====================================================================== type SaveRecents struct { Pcap string Filter string } var _ pcap.IBeforeBegin = SaveRecents{} func MakeSaveRecents(pcap string, filter string) SaveRecents { return SaveRecents{ Pcap: pcap, Filter: filter, } } func (t SaveRecents) BeforeBegin(code pcap.HandlerCode, app gowid.IApp) { if code&pcap.PsmlCode == 0 { return } // Run on main goroutine to avoid problems flagged by -race if t.Pcap != "" { termshark.AddToRecentFiles(t.Pcap) } if t.Filter != "" { // Run on main goroutine to avoid problems flagged by -race termshark.AddToRecentFilters(t.Filter) } } //====================================================================== type CancelledMessage struct{} var _ pcap.IAfterEnd = CancelledMessage{} func (t CancelledMessage) AfterEnd(code pcap.HandlerCode, app gowid.IApp) { if code&pcap.PsmlCode == 0 { return } // Run on main goroutine to avoid problems flagged by -race if Loader.LoadWasCancelled() { // Only do this if the user isn't quitting the app, // otherwise it looks clumsy. if !QuitRequested { OpenError("Loading was cancelled.", app) } } } //====================================================================== type StartUIWhenThereArePackets struct{} var _ pcap.IPsmlHeader = StartUIWhenThereArePackets{} func (t StartUIWhenThereArePackets) OnPsmlHeader(code pcap.HandlerCode, app gowid.IApp) { StartUIOnce.Do(func() { close(StartUIChan) }) } //====================================================================== type ClearWormholeState struct{} var _ pcap.INewSource = ClearWormholeState{} func (t ClearWormholeState) OnNewSource(code pcap.HandlerCode, app gowid.IApp) { if CurrentWormholeWidget != nil { CurrentWormholeWidget.Close() } CurrentWormholeWidget = nil } //====================================================================== type ClearMarksHandler struct{} var _ pcap.IClear = checkGlobalJumpAfterPsml{} var _ pcap.INewSource = checkGlobalJumpAfterPsml{} func clearMarks() { for k := range marksMap { delete(marksMap, k) } lastJumpPos = -1 } func (t checkGlobalJumpAfterPsml) OnNewSource(code pcap.HandlerCode, app gowid.IApp) { clearMarks() } func (t checkGlobalJumpAfterPsml) OnClear(code pcap.HandlerCode, app gowid.IApp) { clearMarks() } //====================================================================== func clearSearchData(app gowid.IApp) { if SearchWidget != nil { SearchWidget.Clear(app) } } type ManageSearchData struct{} var _ pcap.INewSource = ManageSearchData{} var _ pcap.IClear = ManageSearchData{} // Make sure that existing stream widgets are discarded if the user loads a new pcap. func (t ManageSearchData) OnNewSource(c pcap.HandlerCode, app gowid.IApp) { clearSearchData(app) } func (t ManageSearchData) OnClear(c pcap.HandlerCode, app gowid.IApp) { clearSearchData(app) } //====================================================================== type checkGlobalJumpAfterPsml struct { Jump termshark.GlobalJumpPos } var _ pcap.IAfterEnd = checkGlobalJumpAfterPsml{} func MakeCheckGlobalJumpAfterPsml(jmp termshark.GlobalJumpPos) checkGlobalJumpAfterPsml { return checkGlobalJumpAfterPsml{ Jump: jmp, } } func (t checkGlobalJumpAfterPsml) AfterEnd(code pcap.HandlerCode, app gowid.IApp) { // Run on main goroutine to avoid problems flagged by -race if code&pcap.PsmlCode == 0 { return } if QuitRequested { return } if t.Jump.Filename == Loader.Pcap() { if packetListView != nil { tableRow, err := tableRowFromPacketNumber(t.Jump.Pos) if err != nil { OpenError(err.Error(), app) } else { tableCol := 0 curTablePos, err := packetListView.FocusXY() if err == nil { tableCol = curTablePos.Column } packetListView.SetFocusXY(app, table.Coords{Column: tableCol, Row: tableRow}) } } } } //====================================================================== // used for the pdml loader type SetStructWidgets struct { Ld *pcap.PacketLoader } var _ pcap.IOnError = SetStructWidgets{} var _ pcap.IBeforeBegin = SetStructWidgets{} var _ pcap.IAfterEnd = SetStructWidgets{} func (s SetStructWidgets) BeforeBegin(code pcap.HandlerCode, app gowid.IApp) { if code&pcap.PdmlCode == 0 { return } s2ch := s.Ld.Stage2FinishedChan termshark.TrackedGo(func() { fn2 := func() { app.Run(gowid.RunFunction(func(app gowid.IApp) { setLowerWidgets(app) })) } termshark.RunOnDoubleTicker(s2ch, fn2, time.Duration(100)*time.Millisecond, time.Duration(2000)*time.Millisecond, 10) }, Goroutinewg) } // Close the channel before the callback. When the global loader state is idle, // app.Quit() will stop accepting app callbacks, so the goroutine that waits // for ch to be closed will never terminate. func (s SetStructWidgets) AfterEnd(code pcap.HandlerCode, app gowid.IApp) { if code&pcap.PdmlCode == 0 { return } setLowerWidgets(app) StopEmptyHexViewTimer() StopEmptyStructViewTimer() } func (s SetStructWidgets) OnError(code pcap.HandlerCode, app gowid.IApp, err error) { if code&pcap.PdmlCode == 0 { return } log.Error(err) // Hack to avoid picking up errors at other parts of the load // cycle. There should be specific handlers for specific errors. if s.Ld.PdmlLoader.IsLoading() { if !profiles.ConfBool("main.suppress-tshark-errors", true) { app.Run(gowid.RunFunction(func(app gowid.IApp) { OpenLongError(fmt.Sprintf("%v", err), app) })) } } } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.4.0/ui/psmlcols.go000066400000000000000000000235221426312004500163160ustar00rootroot00000000000000// Copyright 2019-2020 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" "reflect" "sort" "github.com/gcla/gowid" "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/divider" "github.com/gcla/gowid/widgets/framed" "github.com/gcla/gowid/widgets/holder" "github.com/gcla/gowid/widgets/hpadding" "github.com/gcla/gowid/widgets/menu" "github.com/gcla/gowid/widgets/null" "github.com/gcla/gowid/widgets/pile" "github.com/gcla/gowid/widgets/styled" "github.com/gcla/gowid/widgets/table" "github.com/gcla/gowid/widgets/text" "github.com/gcla/termshark/v2/configs/profiles" "github.com/gcla/termshark/v2/pkg/shark" "github.com/gcla/termshark/v2/pkg/shark/wiresharkcfg" "github.com/gcla/termshark/v2/ui/menuutil" "github.com/gdamore/tcell/v2" log "github.com/sirupsen/logrus" ) //====================================================================== var colNamesMenu *menu.Widget var colFieldsMenu *menu.Widget // These are global variables used to hold the current model for the edit-columns // widget, and the current line selected. This is hacky but it's so that I can tell, // when a menu button is clicked within this PSML columns widget, which column // it should apply to. I could generate unique menus for each row of the table, as // an alternative... var CurrentColsWidget *psmlColumnsModel var colsCurrentModelRow int var colNamesMenuListBoxHolder *holder.Widget var colFieldsMenuListBoxHolder *holder.Widget //====================================================================== // psmlColumnInfoArraySortLong allows for sorting an array of PsmlColumnInfo by the // longer name - for use in the long-name drop down menu type psmlColumnInfoArraySortLong []shark.PsmlColumnInfo func (a psmlColumnInfoArraySortLong) Len() int { return len(a) } func (a psmlColumnInfoArraySortLong) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a psmlColumnInfoArraySortLong) Less(i, j int) bool { return a[i].Long < a[j].Long } //====================================================================== func buildNamesMenu(app gowid.IApp) { colNamesMenuListBoxHolder = holder.New(null.New()) wid, hei := rebuildPsmlNamesListBox(CurrentColsWidget, app) colNamesMenu = menu.New("psmlcols", colNamesMenuListBoxHolder, gowid.RenderWithUnits{U: wid}, menu.Options{ Modal: true, CloseKeysProvided: true, OpenCloser: &multiMenu1Opener, CloseKeys: []gowid.IKey{ gowid.MakeKeyExt(tcell.KeyEscape), gowid.MakeKeyExt(tcell.KeyCtrlC), }, }) colNamesMenu.SetHeight(units(hei), app) } func buildFieldsMenu(app gowid.IApp) { colFieldsMenuListBoxHolder = holder.New(null.New()) wid, hei := rebuildPsmlFieldListBox(app) colFieldsMenu = menu.New("psmlfieldscols", colFieldsMenuListBoxHolder, gowid.RenderWithUnits{U: wid}, menu.Options{ Modal: true, CloseKeysProvided: true, OpenCloser: &multiMenu1Opener, CloseKeys: []gowid.IKey{ gowid.MakeKeyExt(tcell.KeyEscape), gowid.MakeKeyExt(tcell.KeyCtrlC), }, }) colFieldsMenu.SetHeight(units(hei), app) } // return width needed func rebuildPsmlNamesListBox(p *psmlColumnsModel, app gowid.IApp) (int, int) { colsMenuItems := make([]menuutil.SimpleMenuItem, 0) specs := make(psmlColumnInfoArraySortLong, 0) for _, v := range shark.AllowedColumnFormats { specs = append(specs, v) } sort.Sort(specs) for _, spec := range specs { specCopy := spec colsMenuItems = append(colsMenuItems, menuutil.SimpleMenuItem{ Txt: spec.Long, CB: func(app gowid.IApp, w gowid.IWidget) { multiMenu1Opener.CloseMenu(colNamesMenu, app) p.UpdateFromField(specCopy.Field, colsCurrentModelRow, app) }, }, ) } colsMenuListBox, wid := menuutil.MakeMenu(colsMenuItems, nil) colNamesMenuListBoxHolder.SetSubWidget(colsMenuListBox, nil) return wid, len(specs) } func rebuildPsmlFieldListBox(app gowid.IApp) (int, int) { p := CurrentColsWidget colsMenuItems := make([]menuutil.SimpleMenuItem, 0) columnNames := make([]string, 0) for k, _ := range shark.AllowedColumnFormats { columnNames = append(columnNames, k) } sort.Strings(columnNames) for _, cname := range columnNames { cnameCopy := cname colsMenuItems = append(colsMenuItems, menuutil.SimpleMenuItem{ Txt: cname, CB: func(app gowid.IApp, w gowid.IWidget) { multiMenu1Opener.CloseMenu(colFieldsMenu, app) p.UpdateFromField(cnameCopy, colsCurrentModelRow, app) }, }, ) } colsMenuListBox, wid := menuutil.MakeMenu(colsMenuItems, nil) colFieldsMenuListBoxHolder.SetSubWidget(colsMenuListBox, nil) return wid, len(columnNames) } //====================================================================== func openEditColumns(app gowid.IApp) { pcols := NewPsmlColumnsModel(app) CurrentColsWidget = pcols var mainw gowid.IWidget newPsmlCol := button.New(text.New("+")) newPsmlCol.OnClick(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, widget gowid.IWidget) { pcols.AddRow() })) newPsmlColStyled := hpadding.New(clicktracker.New( styled.NewExt( newPsmlCol, gowid.MakePaletteRef("button"), gowid.MakePaletteRef("button-focus"), ), ), gowid.HAlignLeft{}, gowid.RenderFixed{}) colWidgets := make([]interface{}, 0) pileWidgets := make([]interface{}, 0) pileWidgets = append(pileWidgets, pcols, divider.NewBlank(), newPsmlColStyled) wcfg, err := wiresharkcfg.NewDefault() if err == nil { cols := wcfg.ColumnFormat() if cols != nil { btn := button.New(text.New("Import")) btn.OnClick(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, widget gowid.IWidget) { newPcols := NewPsmlColumnsModel(app) err = newPcols.ReadFromWireshark(app) if err != nil { OpenError(err.Error(), app) return } *pcols = *newPcols pcols.Widget = table.New(pcols) OpenMessage("Imported column preferences from Wireshark", appView, app) })) cols := hpadding.New( columns.NewFixed( clicktracker.New( styled.NewExt( btn, gowid.MakePaletteRef("button"), gowid.MakePaletteRef("button-focus"), ), ), text.New(" from Wireshark"), ), gowid.HAlignLeft{}, gowid.RenderFixed{}, ) colWidgets = append(colWidgets, cols) } } bakCols := profiles.ConfStringSlice("main.column-format-bak", []string{}) if len(bakCols) != 0 { btn := button.New(text.New("Restore")) btn.OnClick(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, widget gowid.IWidget) { newPcols := NewPsmlColumnsModelFrom("main.column-format-bak", app) if len(newPcols.spec) == 0 { OpenMessage("Error: backup column-format is empty in toml file", appView, app) return } *pcols = *newPcols pcols.Widget = table.New(pcols) OpenMessage("Imported previous column preferences", appView, app) })) cols := hpadding.New( columns.NewFixed( clicktracker.New( styled.NewExt( btn, gowid.MakePaletteRef("button"), gowid.MakePaletteRef("button-focus"), ), ), text.New(" previous columns"), ), gowid.HAlignLeft{}, gowid.RenderFixed{}, ) colWidgets = append(colWidgets, cols) } restoreBtn := button.New(text.New("Restore")) restoreBtn.OnClick(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, widget gowid.IWidget) { *pcols = *NewDefaultPsmlColumnsModel(app) pcols.Widget = table.New(pcols) OpenMessage("Imported default column preferences", appView, app) })) cols := hpadding.New( columns.NewFixed( clicktracker.New( styled.NewExt( restoreBtn, gowid.MakePaletteRef("button"), gowid.MakePaletteRef("button-focus"), ), ), text.New(" default columns"), ), gowid.HAlignLeft{}, gowid.RenderFixed{}, ) colWidgets = append(colWidgets, cols) buttonRow := columns.NewWithDim(gowid.RenderWithWeight{W: 1}, colWidgets...) pileWidgets = append(pileWidgets, divider.NewBlank(), buttonRow) mainw = pile.NewFlow(pileWidgets...) var editColsDialog *dialog.Widget okButton := dialog.Button{ Msg: "Ok", Action: gowid.MakeWidgetCallback("cb", gowid.WidgetChangedFunction(func(app gowid.IApp, widget gowid.IWidget) { for i := 0; i < len(pcols.spec); i++ { if pcols.spec[i].Field.Token == "%Cus" && !pcols.widgets[i].customFilter.IsValid() { OpenMessage(fmt.Sprintf("Custom column %d is invalid", i+1), appView, app) return } } newcols := pcols.ToConfigList() curcols := profiles.ConfStringSlice("main.column-format", []string{}) updated := false if !reflect.DeepEqual(newcols, curcols) { profiles.SetConf("main.column-format-bak", curcols) profiles.SetConf("main.column-format", newcols) updated = true } editColsDialog.Close(app) if !updated { OpenMessage("No change - same columns configured", appView, app) } else { RequestReload(app) } }), ), } editColsDialog = dialog.New( framed.NewSpace( mainw, ), dialog.Options{ Buttons: []dialog.Button{okButton, dialog.Cancel}, Modal: true, NoShadow: true, TabToButtons: true, BackgroundStyle: gowid.MakePaletteRef("dialog"), BorderStyle: gowid.MakePaletteRef("dialog"), ButtonStyle: gowid.MakePaletteRef("dialog-button"), }, ) dialogOpen := false editColsDialog.OnOpenClose(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, widget gowid.IWidget) { dialogOpen = !dialogOpen if !dialogOpen { CurrentColsWidget = nil err := pcols.Close() if err != nil { log.Warnf("Unexpected result closing PSML columns dialog: %v", err) } } })) editColsDialog.Open(appView, ratio(0.7), app) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.4.0/ui/psmlcolsmodel.go000066400000000000000000000336251426312004500173440ustar00rootroot00000000000000// Copyright 2019-2020 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/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/edit" "github.com/gcla/gowid/widgets/fill" "github.com/gcla/gowid/widgets/hpadding" "github.com/gcla/gowid/widgets/menu" "github.com/gcla/gowid/widgets/styled" "github.com/gcla/gowid/widgets/table" "github.com/gcla/gowid/widgets/text" "github.com/gcla/gowid/widgets/vpadding" "github.com/gcla/termshark/v2/pkg/shark" "github.com/gcla/termshark/v2/pkg/shark/wiresharkcfg" "github.com/gcla/termshark/v2/widgets/filter" "github.com/gcla/termshark/v2/widgets/number" log "github.com/sirupsen/logrus" ) //====================================================================== var ColumnsFormatError = fmt.Errorf("The supplied list of columns and names is invalid") var filler *fill.Widget // psmlColumnsModel is itself a gowid table widget. This allows me to use the model // directly in the widget hierarchy, and has the advantage that if I update the model, // I can regenerate the embedded table widget because I have a handle to it. Otherwise // I would need to build a more complicated model with callbacks when data changes, and // have those callbacks tied to the table displaying the data. type psmlDialogWidgets struct { customName *edit.Widget // save the user-configured name customFilter *filter.Widget occurrence *number.Widget visible *checkbox.Widget // save the visible selections } type psmlColumnsModel struct { spec []shark.PsmlColumnSpec // the actual rows - field name, long name widgets []psmlDialogWidgets haveCustom bool *table.Widget } var _ table.IBoundedModel = (*psmlColumnsModel)(nil) var _ table.IInvertible = (*psmlColumnsModel)(nil) var ( left = gowid.HAlignLeft{} mid = gowid.HAlignMiddle{} right = gowid.HAlignRight{} ) //====================================================================== func init() { filler = fill.New(' ') } func NewDefaultPsmlColumnsModel(app gowid.IApp) *psmlColumnsModel { spec := shark.DefaultPsmlColumnSpec // copy it to protect from alterations specCopy := make([]shark.PsmlColumnSpec, len(spec)) for i := 0; i < len(spec); i++ { specCopy[i] = spec[i] } res := &psmlColumnsModel{ spec: specCopy, } res.fixup(app) return res } func NewPsmlColumnsModel(app gowid.IApp) *psmlColumnsModel { spec := shark.GetPsmlColumnFormat() res := &psmlColumnsModel{ spec: spec, } res.fixup(app) return res } func NewPsmlColumnsModelFrom(colsKey string, app gowid.IApp) *psmlColumnsModel { spec := shark.GetPsmlColumnFormatFrom(colsKey) res := &psmlColumnsModel{ spec: spec, } res.fixup(app) return res } //====================================================================== func (p *psmlColumnsModel) Close() error { var err error for i := 0; i < len(p.widgets); i++ { if p.widgets[i].customFilter != nil { err2 := p.widgets[i].customFilter.Close() if err == nil { err = err2 } } } return err } func (p *psmlColumnsModel) String() string { return fmt.Sprintf("%v", p.spec) } func (p *psmlColumnsModel) FieldToString(i int) string { field := p.spec[i].Field.Token if field == "%Cus" { field = fmt.Sprintf("%s:%s:%d:R", field, p.widgets[i].customFilter.Value(), p.widgets[i].occurrence.Value, ) } return field } // ToConfigList converts the information in the current PSML columns model to // a slice of strings suitable for writing to the termshark toml file. func (p *psmlColumnsModel) ToConfigList() []string { res := make([]string, 0, len(p.spec)) for i := 0; i < len(p.spec); i++ { res = append(res, p.FieldToString(i)) res = append(res, p.widgets[i].customName.Text()) res = append(res, fmt.Sprintf("%v", p.widgets[i].visible.IsChecked())) } return res } type specToWidgets shark.PsmlColumnSpec func (sp *specToWidgets) widgets() psmlDialogWidgets { return psmlDialogWidgets{ customName: edit.New(edit.Options{ Text: sp.Name, }), customFilter: filter.New("psmlfilter", filter.Options{ Completer: savedCompleter{def: FieldCompleter}, MenuOpener: &multiMenu1Opener, Position: filter.Below, }), visible: checkbox.New(!sp.Hidden), occurrence: number.New(number.Options{ Value: sp.Field.Occurrence, Min: gwutil.SomeInt(0), Styler: func(w gowid.IWidget) gowid.IWidget { return styled.NewInvertedFocus(w, gowid.MakePaletteRef("dialog")) }, }), } } func (p *psmlColumnsModel) AddRow() { p.spec = append(p.spec, shark.PsmlColumnSpec{Field: shark.PsmlField{Token: "%m"}, Name: "No."}) sp := specToWidgets(p.spec[len(p.spec)-1]) w := sp.widgets() p.widgets = append(p.widgets, w) if sp.Field.Token == "%Cus" { p.haveCustom = true } p.Widget = table.New(p) } // cacheHaveCustom keeps track of whther the current model has any custom columns. This // is done each time the model is updated. If custom columns are present, the table is // displayed with two extra columns. func (p *psmlColumnsModel) cacheHaveCustom() { for _, w := range p.spec { if w.Field.Token == "%Cus" { p.haveCustom = true return } } p.haveCustom = false } // Make rest of data structure consistent with recent changes func (p *psmlColumnsModel) fixup(app gowid.IApp) { p.haveCustom = false p.widgets = make([]psmlDialogWidgets, 0) for i := 0; i < len(p.spec); i++ { sp := specToWidgets(p.spec[i]) w := sp.widgets() if p.spec[i].Field.Token == "%Cus" { p.haveCustom = true w.customFilter.SetValue(p.spec[i].Field.Filter, app) } p.widgets = append(p.widgets, w) } p.Widget = table.New(p) } func stripQuotes(s string) string { if len(s) > 0 && s[0] == '"' { s = s[1:] } if len(s) > 0 && s[len(s)-1] == '"' { s = s[:len(s)-1] } return s } func (p *psmlColumnsModel) ReadFromWireshark(app gowid.IApp) error { wcfg, err := wiresharkcfg.NewDefault() if err != nil { return err } wcols := wcfg.ColumnFormat() if wcols == nil { return fmt.Errorf("Could not read Wireshark column preferences") } if (len(wcols)/2)*2 != len(wcols) { return gowid.WithKVs(ColumnsFormatError, map[string]interface{}{ "columns": wcols, }) } specs := make([]shark.PsmlColumnSpec, 0) for i := 0; i < len(wcols); i += 2 { var field shark.PsmlField err := field.FromString(stripQuotes(wcols[i+1])) if err != nil { return err } specs = append(specs, shark.PsmlColumnSpec{ Field: field, Name: stripQuotes(wcols[i]), }) } p.spec = specs p.fixup(app) return nil } // Called when user chooses from a menu - so will only have PsmlField Token name func (p *psmlColumnsModel) UpdateFromField(field string, idx int, app gowid.IApp) { if p.spec[idx].Field.Token == "%Cus" && field != "%Cus" { err := p.widgets[idx].customFilter.Close() if err != nil { log.Warnf("Unexpected response when closing filter: %v", err) } } p.spec[idx].Field.Token = field p.spec[idx].Name = shark.AllowedColumnFormats[field].Long p.cacheHaveCustom() p.Widget = table.New(p) } func (m *psmlColumnsModel) moveDown(row int, app gowid.IApp) { i := row j := row + 1 m.spec[i], m.spec[j] = m.spec[j], m.spec[i] m.widgets[i], m.widgets[j] = m.widgets[j], m.widgets[i] m.Widget = table.New(m) } func (m *psmlColumnsModel) moveUp(row int, app gowid.IApp) { i := row j := row - 1 m.spec[i], m.spec[j] = m.spec[j], m.spec[i] m.widgets[i], m.widgets[j] = m.widgets[j], m.widgets[i] m.Widget = table.New(m) } // row is the screen position func (m *psmlColumnsModel) deleteRow(trow table.RowId, app gowid.IApp) { row := int(trow) // Do this to close the filter goroutines err := m.widgets[row].customFilter.Close() if err != nil { log.Warnf("Unexpected response when closing filter: %v", err) } m.spec = append(m.spec[:row], m.spec[row+1:]...) m.widgets = append(m.widgets[:row], m.widgets[row+1:]...) m.cacheHaveCustom() m.Widget = table.New(m) } // construct the widgets for each row in the dialog used to configure PSML // columns. func (p *psmlColumnsModel) CellWidgets(row table.RowId) []gowid.IWidget { rowi := int(row) res := make([]gowid.IWidget, 0) pad := func(w gowid.IWidget, pos gowid.IHAlignment, r int, fn func(int) gowid.WidgetChangedFunction) gowid.IWidget { btn := button.NewAlt(w) btn.OnClick(gowid.MakeWidgetCallback(gowid.ClickCB{}, fn(r))) return hpadding.New( clicktracker.New( styled.NewExt( btn, gowid.MakePaletteRef("dialog"), gowid.MakePaletteRef("dialog-button"), ), ), pos, fixed, ) } colsMenuFieldsSite := menu.NewSite(menu.SiteOptions{YOffset: 1}) // Field name colsMenuFieldsButton := button.NewBare(text.New(p.spec[rowi].Field.Token)) colsMenuFieldsButton.OnClick(gowid.MakeWidgetCallback(gowid.ClickCB{}, func(app gowid.IApp, target gowid.IWidget) { wid, hei := rebuildPsmlFieldListBox(app) colsCurrentModelRow = rowi colFieldsMenu.SetWidth(units(wid), app) colFieldsMenu.SetHeight(units(hei), app) multiMenu1Opener.OpenMenu(colFieldsMenu, colsMenuFieldsSite, app) })) // "^" if rowi == 0 { res = append(res, nullw) } else { res = append(res, pad(text.New("^"), left, rowi, func(r int) gowid.WidgetChangedFunction { return func(app gowid.IApp, target gowid.IWidget) { p.moveUp(r, app) } })) } // "v" if rowi == len(p.spec)-1 { res = append(res, nullw) } else { res = append(res, pad(text.New("v"), left, rowi, func(r int) gowid.WidgetChangedFunction { return func(app gowid.IApp, target gowid.IWidget) { p.moveDown(r, app) } })) } // "[X]" res = append(res, hpadding.New( clicktracker.New( styled.NewExt( p.widgets[row].visible, gowid.MakePaletteRef("dialog"), gowid.MakePaletteRef("dialog-button"), ), ), left, fixed, )) // %Yut prep fcols := columns.New([]gowid.IContainerWidget{ &gowid.ContainerWidget{ IWidget: colsMenuFieldsSite, D: fixed, }, &gowid.ContainerWidget{ IWidget: clicktracker.New( styled.NewExt( colsMenuFieldsButton, gowid.MakePaletteRef("button"), gowid.MakePaletteRef("button-focus"), ), ), D: fixed, }, }) // %Yut res = append(res, fcols) // Filter if p.haveCustom { if p.spec[row].Field.Token == "%Cus" { res = append(res, p.widgets[row].customFilter) res = append(res, hpadding.New(p.widgets[row].occurrence, mid, fixed)) } else { res = append(res, nullw) res = append(res, nullw) } } // "gcla1" res = append(res, p.widgets[row].customName) colsMenuSite := menu.NewSite(menu.SiteOptions{YOffset: 1}) colsMenuButton := button.NewBare(text.New(shark.AllowedColumnFormats[p.spec[row].Field.Token].Long)) colsMenuButton.OnClick(gowid.MakeWidgetCallback(gowid.ClickCB{}, func(app gowid.IApp, target gowid.IWidget) { wid, hei := rebuildPsmlNamesListBox(p, app) colsCurrentModelRow = rowi colNamesMenu.SetWidth(units(wid), app) colNamesMenu.SetHeight(units(hei), app) multiMenu1Opener.OpenMenu(colNamesMenu, colsMenuSite, app) })) // res = append(res, columns.NewFixed( colsMenuSite, clicktracker.New( styled.NewExt( colsMenuButton, gowid.MakePaletteRef("button"), gowid.MakePaletteRef("button-focus"), ), ), ), ) // "X" if len(p.spec) <= 1 { res = append(res, nullw) } else { res = append(res, pad(text.New("X"), mid, rowi, func(r int) gowid.WidgetChangedFunction { return func(app gowid.IApp, target gowid.IWidget) { p.deleteRow(row, app) } })) } return res } func (p *psmlColumnsModel) Columns() int { if !p.haveCustom { return 7 } else { return 7 + 2 } } func (p *psmlColumnsModel) Widths() []gowid.IWidgetDimension { if !p.haveCustom { return []gowid.IWidgetDimension{ gowid.RenderWithUnits{U: 3}, gowid.RenderWithUnits{U: 4}, gowid.RenderWithWeight{W: 1}, gowid.RenderWithWeight{W: 1}, gowid.RenderWithWeight{W: 2}, gowid.RenderWithWeight{W: 3}, gowid.RenderWithUnits{U: 9}, } } else { return []gowid.IWidgetDimension{ gowid.RenderWithUnits{U: 3}, gowid.RenderWithUnits{U: 4}, gowid.RenderWithWeight{W: 1}, gowid.RenderWithWeight{W: 1}, // gowid.RenderWithWeight{W: 4}, gowid.RenderWithUnits{U: 8}, // gowid.RenderWithWeight{W: 2}, gowid.RenderWithWeight{W: 3}, gowid.RenderWithUnits{U: 9}, } } } func (p *psmlColumnsModel) Rows() int { return len(p.spec) } func (p *psmlColumnsModel) HorizontalSeparator() gowid.IWidget { return nil } func (p *psmlColumnsModel) HeaderSeparator() gowid.IWidget { return nil } func (p *psmlColumnsModel) HeaderWidgets() []gowid.IWidget { pr := gowid.MakePaletteRef("dialog") st := func(w gowid.IWidget) gowid.IWidget { return vpadding.New( styled.NewExt(w, gowid.ColorInverter{pr}, gowid.ColorInverter{pr}), gowid.VAlignTop{}, gowid.RenderWithUnits{U: 1}, ) } if !p.haveCustom { return []gowid.IWidget{ st(text.New("")), st(text.New("")), st(text.New("Show")), st(text.New("Field")), st(text.New("Your Name")), st(text.New("Official")), st(text.New("Remove")), } } else { return []gowid.IWidget{ st(text.New("")), st(text.New("")), st(text.New("Show")), st(text.New("Field")), // st(text.New("Filter")), st(text.New("#Occ")), // st(text.New("Your Name")), st(text.New("Official")), st(text.New("Remove")), } } } func (p *psmlColumnsModel) VerticalSeparator() gowid.IWidget { return nil } func (p *psmlColumnsModel) RowIdentifier(row int) (table.RowId, bool) { if row < 0 || row >= len(p.spec) { return -1, false } return table.RowId(row), true } func (p *psmlColumnsModel) IdentifierToRow(rowid table.RowId) (int, bool) { if rowid < 0 || int(rowid) >= len(p.spec) { return -1, false } else { return int(rowid), true } } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.4.0/ui/searchalg.go000066400000000000000000000073051426312004500164140ustar00rootroot00000000000000// Copyright 2019-2022 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" "time" "github.com/gcla/gowid" "github.com/gcla/termshark/v2" "github.com/gcla/termshark/v2/pkg/pcap" "github.com/gcla/termshark/v2/widgets/search" ) //====================================================================== // PacketSearcher coordinates a packet search and communicates results back from the // search implementations via resultChan. type PacketSearcher struct { resultChan chan search.IntermediateResult } var _ search.IAlgorithm = (*PacketSearcher)(nil) //====================================================================== // SearchPackets looks for the given search term in the currently loaded packets. It // is written generically, with the specifics of the packet details to be searched provided // by a set of callbacks. These give the search algorithm the starting position, the mechanics // of the search, and so on. An instance of a search can return a matching position, or a // value indicating that the algorithm needs to wait until packet data is available (e.g. // if PDML data needs to be searched but is not currently loaded). If a match is found, the // callbacks also determine how to update the UI to represent the match. func (w *PacketSearcher) SearchPackets(term search.INeedle, cbs search.ICallbacks, app gowid.IApp) { if packetListView == nil { cbs.OnError(fmt.Errorf("No packets to search"), app) return } cbs.Reset(app) currentPos, err := cbs.StartingPosition() startPos := currentPos // currentPacket will be 1-based if err != nil { cbs.OnError(err, app) return } stopCurrentSearch = cbs progressOwner = SearchOwns searchStop.RemoveOnClick(gowid.CallbackID{ Name: "searchstop", }) searchStop.OnClick(gowid.MakeWidgetCallback("searchstop", func(app gowid.IApp, _ gowid.IWidget) { cbs.RequestStop(app) })) tickInterval := time.Duration(200) * time.Millisecond tick := time.NewTicker(tickInterval) resumeAt := -1 var resAt interface{} // Computationally bound searching goroutine - may have to terminate if it runs out of // packets to search while they're loaded termshark.TrackedGo(func() { cbs.SearchPacketsFrom(currentPos, startPos, term, app) }, Goroutinewg) // This goroutine exists so that at a regular interval, I can update progress. I want // the main searching goroutine to be doing the computation and not having to cooperate // with a timer interrupt termshark.TrackedGo(func() { res := search.Result{} defer func() { stopCurrentSearch = nil cbs.SearchPacketsResult(res, app) }() Loop: for { select { case <-tick.C: cbs.OnTick(app) if resumeAt != -1 { termshark.TrackedGo(func() { cbs.SearchPacketsFrom(resAt, startPos, term, app) }, Goroutinewg) resumeAt = -1 } case sres := <-w.resultChan: if sres.ResumeAt == nil { // Search is finished res = sres.Res break Loop } else { resumeAt = sres.ResumeAt.PacketNumber() resAt = sres.ResumeAt // go to 0-based for cache lookup resumeAtZeroBased := resumeAt - 1 app.Run(gowid.RunFunction(func(app gowid.IApp) { pktsPerLoad := Loader.PacketsPerLoad() CacheRequests = append(CacheRequests, pcap.LoadPcapSlice{ Row: (resumeAtZeroBased / pktsPerLoad) * pktsPerLoad, CancelCurrent: true, }) CacheRequestsChan <- struct{}{} })) } } } }, Goroutinewg) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.4.0/ui/searchbyfilter.go000066400000000000000000000323661426312004500174760ustar00rootroot00000000000000// Copyright 2019-2022 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 ( "context" "encoding/xml" "fmt" "io" "os" "os/exec" "strconv" "sync" "github.com/gcla/gowid" "github.com/gcla/gowid/widgets/table" "github.com/gcla/termshark/v2" "github.com/gcla/termshark/v2/configs/profiles" "github.com/gcla/termshark/v2/pkg/format" "github.com/gcla/termshark/v2/pkg/pcap" "github.com/gcla/termshark/v2/widgets/search" log "github.com/sirupsen/logrus" "gitlab.com/jonas.jasas/condchan" ) //====================================================================== // FilterResult represents a match via a display filter search. This is no more // specific than a packet/frame number to jump to. type FilterResult struct { PacketNum int } func (s FilterResult) PacketNumber() int { return s.PacketNum } //====================================================================== // Search via a display filter // type FilterSearchCallbacks struct { *commonSearchCallbacks curSearchTerm string searchMap map[string]*filterSearchState mapLock sync.Mutex searchResChan chan search.IntermediateResult } type filterSearchState struct { cmd pcap.IPcapCommand ctx context.Context // cancelling this cancels the dependent contexts - used to close whole loader. cancelFn context.CancelFunc pcapInfo os.FileInfo // To protect below: cc *condchan.CondChan // State covered by cc and ccMtx first int // first packet found; from here, jump into nextMap nextMap map[int]int // map from actual packet row 12 to pos in unsorted table finished bool // true if filter search process and mapping goroutine have done their work. interrupted bool errorFromUser error } func newFilterSearchState(filename string, cmd pcap.IPcapCommand) (*filterSearchState, error) { ctx, cancelFn := context.WithCancel(Loader.Context()) info, err := os.Stat(filename) if err != nil { return nil, err } res := &filterSearchState{ cmd: cmd, nextMap: make(map[int]int), ctx: ctx, cancelFn: cancelFn, pcapInfo: info, } res.cc = condchan.New(&sync.Mutex{}) return res, nil } var _ search.IRequestStop = (*FilterSearchCallbacks)(nil) var _ search.ICallbacks = (*FilterSearchCallbacks)(nil) func NewFilterSearchCallbacks( callbacks *commonSearchCallbacks, searchCh chan search.IntermediateResult) *FilterSearchCallbacks { res := &FilterSearchCallbacks{ commonSearchCallbacks: callbacks, searchResChan: searchCh, searchMap: make(map[string]*filterSearchState), } return res } func (w *FilterSearchCallbacks) Reset(app gowid.IApp) { } func (w *FilterSearchCallbacks) StartingPosition() (interface{}, error) { p, err := packetNumberFromCurrentTableRow() if err != nil { return FilterResult{}, err } return FilterResult{PacketNum: p.Pos}, nil } func (w *FilterSearchCallbacks) RequestStop(app gowid.IApp) { w.mapLock.Lock() defer w.mapLock.Unlock() if mpval, ok := w.searchMap[w.curSearchTerm]; ok { mpval.cc.L.Lock() mpval.cancelFn() if !mpval.finished { mpval.interrupted = true } mpval.cc.L.Unlock() } } // Assumes lock is held func (w *FilterSearchCallbacks) searchStartedFor(search string) bool { _, ok := w.searchMap[search] return ok } // startPacketNumber >= 1 func (w *FilterSearchCallbacks) SearchPacketsFrom(ifrom interface{}, istart interface{}, term search.INeedle, app gowid.IApp) { // We are in control if these types by agreement with searchalg.go from := ifrom.(FilterResult) searchFor := term.(fmt.Stringer).String() if Loader.DisplayFilter() != "" { searchFor = fmt.Sprintf("(%s) && (%s)", Loader.DisplayFilter(), searchFor) } var mpval *filterSearchState killCurrent := false filename := getPsmlFile() var saveMap map[int]int w.mapLock.Lock() if w.searchStartedFor(searchFor) { // The mpval is guaranteed created here mpval = w.searchMap[searchFor] fi, err := os.Stat(filename) if err != nil { w.OnError(fmt.Errorf("Could not open pcap: %w", err), app) } else { if fi.Size() != mpval.pcapInfo.Size() { killCurrent = true } } if killCurrent { mpval.cancelFn() // We don't need to throw away the existing data - because the pcap will only // grow, meaning any search matches in the smaller pcap will match in the larger - I think! saveMap = mpval.nextMap delete(w.searchMap, searchFor) mpval = nil } } if mpval == nil { var err error psmlCmd := makePsmlCommand(filename, searchFor) mpval, err = newFilterSearchState(filename, psmlCmd) if err != nil { w.OnError(fmt.Errorf("Could not open pcap: %w", err), app) } else { if saveMap != nil { mpval.nextMap = saveMap } w.curSearchTerm = searchFor w.searchMap[searchFor] = mpval termshark.TrackedGo(func() { // When this returns, the process has finished running, and if it started, Wait() // has been called. err := w.runProcess(mpval.ctx, psmlCmd, app, func(prev int, cur int) { mpval.cc.L.Lock() if mpval.first == 0 { mpval.first = cur } if prev != 0 { if cur == 0 { // Only make last link at end, when all are loaded - so we don't cycle round // when the load is still ongoing, misleading the user mpval.nextMap[prev] = mpval.first } else { mpval.nextMap[prev] = cur } } mpval.cc.L.Unlock() mpval.cc.Broadcast() }) mpval.cc.L.Lock() mpval.cancelFn() mpval.finished = true mpval.errorFromUser = err mpval.cc.L.Unlock() }, Goroutinewg) } } w.mapLock.Unlock() // Note - mpval might be nil here, if we couldn't open the pcap file res := search.Result{} //================================================== // We'll definitely return a result of some kind - found or nothing defer func() { w.searchResChan <- search.IntermediateResult{ Res: res, } }() // Calculate result from this call, or wait for result. This is pretty lame and would benefit // from some better data structures e.g. some sort of interval map. getClosest := func() int { closest := -1 distance := -1 if from.PacketNum < mpval.first { closest = mpval.first } else { for k := range mpval.nextMap { if distance == -1 || (k-from.PacketNum < distance && k > from.PacketNum) { distance = k - from.PacketNum closest = k } } if closest == -1 { // If nothing in the map, use the first value which is stored here. If that // is zero, it means nothing has been found yet... closest = mpval.first } } return closest } if mpval == nil { return } // Find the closest matching packet from the current position in the table mpval.cc.L.Lock() defer mpval.cc.L.Unlock() Loop: for { res.ErrorForUser = mpval.errorFromUser res.Interrupted = mpval.interrupted if next, ok := mpval.nextMap[from.PacketNum]; ok { res.Position = FilterResult{ PacketNum: next, } res.Success = true break Loop } else { closest := getClosest() if closest != 0 { res.Position = FilterResult{ PacketNum: closest, } res.Success = true break Loop } else if mpval.finished { // No hits and load finished - return search failed break Loop } } if !mpval.finished { mpval.cc.Select(func(c <-chan struct{}) { // Waiting with select // Either of these two channels mean we should proceed. The first // means that some search state has changed - maybe a new result, // maybe a cancellation, maybe the end of the process execution. The // second means something else interrupted - e.g. user hit the stop // button. select { case <-c: case <-mpval.ctx.Done(): } }) } } } func (s *FilterSearchCallbacks) SearchPacketsResult(res search.Result, app gowid.IApp) { app.Run(gowid.RunFunction(func(app gowid.IApp) { s.ticks = 0 // Do this because we might be on the same listview packet, so the change callback // won't run and adjust the lower view // // UPDATE - this assumes the original start position in the table is the same as // the one now. ClearProgressWidgetFor(app, SearchOwns) if res.Interrupted { // If interrupted, delete all state. Then the next time Find is hit, it will all be // kicked off again from a clean slate s.mapLock.Lock() delete(s.searchMap, s.curSearchTerm) s.curSearchTerm = "" s.mapLock.Unlock() return } if !res.Success { if res.ErrorForUser != nil { OpenError(res.ErrorForUser.Error(), app) } else { OpenError("Not found.", app) } return } filterRes := res.Position.(FilterResult) tableCol := 0 curTablePos, err := packetListView.FocusXY() if err == nil { tableCol = curTablePos.Column } tableRow, err := tableRowFromPacketNumber(filterRes.PacketNum) if err != nil { OpenError(fmt.Sprintf("Could not move to packet %d\n\n%v", filterRes.PacketNum, err), app) return } packetListView.SetFocusXY(app, table.Coords{Column: tableCol, Row: tableRow}) // Don't continue to jump to the end AutoScroll = false // Callback might not run if focus position in table is the same e.g. if we find a match // on the same row that we started. So in that case, to expand the lower widgets, call // setLowerWidgets explicitly - don't rely on the focus-changed callback. And I can't // do a shortcut and call this if start == current because the starting position for the // search may not be the same as the list-view row on display - maybe the search has // resumed not that some extra PDML data has been loaded setLowerWidgets(app) // It looks better than having the found packet be at the top of the view packetListView.GoToMiddle(app) })) } // makePsmlCommand generates the tshark command to run to generate the sequence of results from search, // according to the filter value. func makePsmlCommand(filename string, displayFilter string) pcap.IPcapCommand { args := []string{ "-T", "psml", "-o", fmt.Sprintf("gui.column.format:\"No.\",\"%%m\""), } // read from cmdline file args = append(args, "-r", filename) if displayFilter != "" { args = append(args, "-Y", displayFilter) } psmlArgs := profiles.ConfStringSlice("main.psml-args", []string{}) tsharkArgs := profiles.ConfStringSlice("main.tshark-args", []string{}) args = append(args, psmlArgs...) args = append(args, tsharkArgs...) cmd := exec.Command(termshark.TSharkBin(), args...) return &pcap.Command{Cmd: cmd} } func getPsmlFile() string { res := Loader.InterfaceFile() // Might be loading from interface or fifo if res == "" { res = Loader.PcapPsml.(string) } return res } func (w *FilterSearchCallbacks) runProcess(ctx context.Context, psmlCmd pcap.IPcapCommand, app gowid.IApp, addRes func(int, int)) (err error) { var psmlOut io.ReadCloser psmlOut, err = psmlCmd.StdoutReader() if err != nil { return } err = psmlCmd.Start() if err != nil { return } defer func() { err := psmlCmd.Wait() if err != nil { w.OnError(fmt.Errorf("Error waiting: %w", err), app) } }() termshark.TrackedGo(func() { // Do this in a goroutine because if we try in the for loop below, we might block endlessly // waiting for d.Token() to return, even though the context is cancelled select { case <-ctx.Done(): psmlCmd.Kill() } }, Goroutinewg) log.Infof("Started PSML search command %v with pid %d", psmlCmd, psmlCmd.Pid()) d := xml.NewDecoder(psmlOut) // //
1
//
var curPsml []string var pidx int ppidx := 0 // the previous packet number read; 0 means no packet. I can use 0 because // the psml I read will start at packet 1 so - map[0] => 1st packet ready := false empty := true structure := false addLast := true defer func() { if addLast { addRes(ppidx, 0) } }() for { err = ctx.Err() if err != nil { if err == io.EOF { err = nil } addLast = false break } var tok xml.Token tok, err = d.Token() if err != nil { if err != io.EOF { err = fmt.Errorf("Could not read PSML data: %v", err) addLast = false } else { // Don't show IO errors to the user err = nil } break } switch tok := tok.(type) { case xml.EndElement: switch tok.Name.Local { case "structure": structure = false case "packet": // Track the mapping of packet number
12
to position // in the table e.g. 5th element. This is so that I can jump to the correct // row with marks even if a filter is currently applied. pidx, err = strconv.Atoi(curPsml[0]) if err != nil { break } addRes(ppidx, pidx) ppidx = pidx case "section": ready = false // Means we got
without any char data i.e. empty
if empty { curPsml = append(curPsml, "") } } case xml.StartElement: switch tok.Name.Local { case "structure": structure = true case "packet": curPsml = make([]string, 0, 10) case "section": ready = true empty = true } case xml.CharData: if ready { if !structure { curPsml = append(curPsml, string(format.TranslateHexCodes(tok))) empty = false } } } } return } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.4.0/ui/searchcommon.go000066400000000000000000000026021426312004500171340ustar00rootroot00000000000000// Copyright 2019-2022 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 ( "sync" "github.com/gcla/gowid" ) //====================================================================== type commonSearchCallbacks struct { ticks int } func (s *commonSearchCallbacks) OnTick(app gowid.IApp) { s.ticks += 1 if s.ticks >= 2 { app.Run(gowid.RunFunction(func(app gowid.IApp) { SetProgressIndeterminateFor(app, SearchOwns) SetSearchProgressWidget(app) loadSpinner.Update() })) } } func (s *commonSearchCallbacks) OnError(err error, app gowid.IApp) { app.Run(gowid.RunFunction(func(app gowid.IApp) { OpenError(err.Error(), app) })) } //====================================================================== type SearchStopper struct { RequestedMutex sync.Mutex Requested bool } func (s *SearchStopper) RequestStop(app gowid.IApp) { s.RequestedMutex.Lock() defer s.RequestedMutex.Unlock() s.Requested = true } func (s *SearchStopper) DoIfStopped(f func()) { s.RequestedMutex.Lock() defer s.RequestedMutex.Unlock() if s.Requested { f() s.Requested = false } } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.4.0/ui/searchpktbytes.go000066400000000000000000000147531426312004500175230ustar00rootroot00000000000000// Copyright 2019-2022 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/table" "github.com/gcla/termshark/v2/pkg/pcap" "github.com/gcla/termshark/v2/widgets/search" ) //====================================================================== // BytesResult represents a match for a search within the bytes of the packets loaded. A match // is a packet number, and the position within the byte array representing the packet's data. type BytesResult struct { PacketNum int PacketPos int PacketRow int } func (s BytesResult) PacketNumber() int { return s.PacketNum } //====================================================================== // Search in the packet hex view // type BytesSearchCallbacks struct { *commonSearchCallbacks *SearchStopper term search.INeedle search chan search.IntermediateResult } var _ search.IRequestStop = (*BytesSearchCallbacks)(nil) var _ search.ICallbacks = (*BytesSearchCallbacks)(nil) func (w *BytesSearchCallbacks) Reset(app gowid.IApp) { w.SearchStopper.Requested = false w.ticks = 0 } func (w *BytesSearchCallbacks) StartingPosition() (interface{}, error) { tablePos, err := packetListView.FocusXY() // e.g. table position 5 if err != nil { return BytesResult{}, err } p, err := packetNumberFromTableRow(tablePos.Row) if err != nil { return BytesResult{}, err } var pos int hex := getHexWidgetToDisplay(tablePos.Row) if hex != nil { pos = hex.Position() } return BytesResult{ PacketNum: p.Pos, PacketPos: pos, }, nil } // own goroutine // startPacketNumber >= 1 func (w *BytesSearchCallbacks) SearchPacketsFrom(ifrom interface{}, istart interface{}, term search.INeedle, app gowid.IApp) { from := ifrom.(BytesResult) res := search.Result{} searchRes := BytesResult{} // True if we have packets in the current batch to search (and we aren't blocked waiting for them to load) curPacketNumber := from.PacketNum packetPos := from.PacketPos var resumeAt *BytesResult defer func() { if resumeAt != nil { w.search <- search.IntermediateResult{ Res: res, ResumeAt: *resumeAt, } } else { w.search <- search.IntermediateResult{ Res: res, } } }() searchCount := 0 pktsPerLoad := Loader.PacketsPerLoad() Loop: for { w.DoIfStopped(func() { res.Interrupted = true }) if res.Interrupted { break Loop } Loader.PsmlLoader.Lock() // curPacketNumber is the packet number from the pdml 24. Remember there might // be a display filter in place. packetIndex, ok := Loader.PacketNumberMap[curPacketNumber] if !ok { // 1-based - packet number e.g. 24 resumeAt = &BytesResult{ PacketNum: curPacketNumber, } Loader.PsmlLoader.Unlock() break } if packetIndex >= len(Loader.PsmlData()) { panic(nil) } Loader.PsmlLoader.Unlock() //====================================================================== rowMod := (packetIndex / pktsPerLoad) * pktsPerLoad if ws, ok := Loader.PacketCache.Get(rowMod); ok { srca := ws.(pcap.CacheEntry).Pcap if packetIndex%pktsPerLoad < len(srca) { src := string(srca[packetIndex%pktsPerLoad]) if len(src) > packetPos+1 { // Start at +1 matchPos := term.Search(src[packetPos+1:]) if matchPos != -1 { searchRes.PacketNum = curPacketNumber searchRes.PacketPos = matchPos + packetPos + 1 searchRes.PacketRow = packetIndex res.Position = searchRes res.Success = true // Terminate the search break Loop } } } } //====================================================================== packetPos = 0 searchCount += 1 // Returns a new object. Already takes the loader lock // Can this be more sophisticated? Loader.PsmlLoader.Lock() // 32, 44, 45, 134, 209,... curPacketNumber, ok = Loader.PacketNumberOrder[curPacketNumber] if !ok { // PacketNumberOrder is set up by the PSML loader, so if there is no next // value, it means we're at the end of the packets and we should loop back. curPacketNumber = Loader.PacketNumberOrder[0] } Loader.PsmlLoader.Unlock() // Go 1 past because if we loop round, we should search the original packet again // in case there is a hit earlier in its structure if searchCount > len(Loader.PacketNumberMap) { break Loop } } } func (s *BytesSearchCallbacks) SearchPacketsResult(res search.Result, app gowid.IApp) { app.Run(gowid.RunFunction(func(app gowid.IApp) { // Do this because we might be on the same listview packet, so the change callback // won't run and adjust the lower view // // UPDATE - this assumes the original start position in the table is the same as // the one now. ClearProgressWidgetFor(app, SearchOwns) if res.Interrupted { return } if !res.Success { OpenError("Not found.", app) return } tableCol := 0 bytesRes := res.Position.(BytesResult) curTablePos, err := packetListView.FocusXY() if err == nil { tableCol = curTablePos.Column } tableRow, err := tableRowFromPacketNumber(bytesRes.PacketNum) if err != nil { OpenError(fmt.Sprintf("Could not move to packet %d\n\n%v", bytesRes.PacketNum, err), app) return } packetListView.SetFocusXY(app, table.Coords{Column: tableCol, Row: tableRow}) // Don't continue to jump to the end AutoScroll = false //======================================== // Callback might not run if focus position in table is the same e.g. if we find a match // on the same row that we started. So in that case, to expand the lower widgets, call // setLowerWidgets explicitly - don't rely on the focus-changed callback. And I can't // do a shortcut and call this if start == current because the starting position for the // search may not be the same as the list-view row on display - maybe the search has // resumed not that some extra PDML data has been loaded // It looks better than having the found packet be at the top of the view packetListView.GoToMiddle(app) hex := getHexWidgetToDisplay(bytesRes.PacketRow) if hex != nil { allowHexToStructRepositioning = true hex.SetPosition(bytesRes.PacketPos, app) } curPacketStructWidget.GoToMiddle(app) curStructWidgetState = curPacketStructWidget.State() })) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.4.0/ui/searchpktlist.go000066400000000000000000000136661426312004500173520ustar00rootroot00000000000000// Copyright 2019-2022 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/table" "github.com/gcla/termshark/v2/widgets/search" ) //====================================================================== // ListResult represents a match for a search within the packet list view. This is really // just the PSML already generated by tshark. The result is a packet number and a column; // on a match, the packet list view will be updated to show the matched row and column. type ListResult struct { Column int PacketNum int } func (s ListResult) PacketNumber() int { return s.PacketNum } //====================================================================== // Search in the packet list view: // 1 0.000000 10.44.10.228 8.8.4.4 DNS 77 Standard query 0x51fe A ac.duckduckgo.com // 2 0.024306 10.44.10.228 176.103.130. DNS 77 Standard query 0x51fe A ac.duckduckgo.com // 3 0.024875 8.8.4.4 10.44.10.228 DNS 139 Standard query response 0x51fe A ac.duckduckgo.com CNAME // 4 0.025500 10.44.10.228 184.72.104.1 TCP 66 55408 → 443 [SYN] Seq=0 Win=8192 Len=0 MSS=1460 WS=256 S 0 type ListSearchCallbacks struct { *commonSearchCallbacks *SearchStopper samePacket bool // set to false if we have advanced beyond the current packet, so don't need to check list pos search chan search.IntermediateResult } var _ search.IRequestStop = (*ListSearchCallbacks)(nil) var _ search.ICallbacks = (*ListSearchCallbacks)(nil) func (w *ListSearchCallbacks) Reset(app gowid.IApp) { w.SearchStopper.Requested = false w.ticks = 0 } func (w *ListSearchCallbacks) StartingPosition() (interface{}, error) { p, err := packetNumberFromCurrentTableRow() if err != nil { return ListResult{}, err } if packetListView == nil { return ListResult{}, fmt.Errorf("No packets loaded") } coords, err := packetListView.FocusXY() if err != nil { return ListResult{}, err } return ListResult{ PacketNum: p.Pos, Column: coords.Column + 1, }, nil } // own goroutine // startPacketNumber >= 1 func (w *ListSearchCallbacks) SearchPacketsFrom(ifrom interface{}, istart interface{}, term search.INeedle, app gowid.IApp) { start := istart.(ListResult) from := ifrom.(ListResult) res := search.Result{} // True if we have packets in the current batch to search (and we aren't blocked waiting for them to load) curPacketNumber := from.PacketNum column := from.Column var resumeAt *ListResult defer func() { if resumeAt != nil { w.search <- search.IntermediateResult{ Res: res, ResumeAt: *resumeAt, } } else { w.search <- search.IntermediateResult{ Res: res, } } }() Loop: for { // Map from packet number to index in pdml array w.DoIfStopped(func() { res.Interrupted = true }) if res.Interrupted { break Loop } Loader.PsmlLoader.Lock() // curPacketNumber is the packet number from the pdml 24. Remember there might // be a display filter in place. packetIndex, ok := Loader.PacketNumberMap[curPacketNumber] if !ok { // 1-based - packet number e.g. 24 resumeAt = &ListResult{ PacketNum: curPacketNumber, } Loader.PsmlLoader.Unlock() break } if packetIndex >= len(Loader.PsmlData()) { panic(nil) } datas := Loader.PsmlData()[packetIndex] Loader.PsmlLoader.Unlock() if column > len(datas) { column = len(datas) } for j, data := range datas[column:len(datas)] { mpos := term.Search(data) if mpos != -1 { coords := ListResult{ Column: j + column, PacketNum: curPacketNumber, } res.Position = coords res.Success = true // Terminate the search break Loop } } column = 0 w.samePacket = false // Can this be more sophisticated? Loader.PsmlLoader.Lock() // 32, 44, 45, 134, 209,... curPacketNumber, ok = Loader.PacketNumberOrder[curPacketNumber] if !ok { curPacketNumber = Loader.PacketNumberOrder[0] } Loader.PsmlLoader.Unlock() if curPacketNumber == start.PacketNum { break Loop } } } func (s *ListSearchCallbacks) SearchPacketsResult(res search.Result, app gowid.IApp) { app.Run(gowid.RunFunction(func(app gowid.IApp) { // Do this because we might be on the same listview packet, so the change callback // won't run and adjust the lower view // // UPDATE - this assumes the original start position in the table is the same as // the one now. ClearProgressWidgetFor(app, SearchOwns) if res.Interrupted { return } if !res.Success { OpenError("Not found.", app) return } coords := res.Position.(ListResult) trow, err := tableRowFromPacketNumber(coords.PacketNum) if err != nil { OpenError(fmt.Sprintf("Could not move to packet %d\n\n%v", coords.PacketNum, err), app) return } packetListView.SetFocusXY(app, table.Coords{Column: coords.Column, Row: trow}) // Don't continue to jump to the end AutoScroll = false // Callback might not run if focus position in table is the same e.g. if we find a match // on the same row that we started. So in that case, to expand the lower widgets, call // setLowerWidgets explicitly - don't rely on the focus-changed callback. And I can't // do a shortcut and call this if start == current because the starting position for the // search may not be the same as the list-view row on display - maybe the search has // resumed not that some extra PDML data has been loaded setLowerWidgets(app) // It looks better than having the found packet be at the top of the view packetListView.GoToMiddle(app) curPacketStructWidget.GoToMiddle(app) curStructWidgetState = curPacketStructWidget.State() })) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.4.0/ui/searchpktstruct.go000066400000000000000000000210501426312004500177050ustar00rootroot00000000000000// Copyright 2019-2022 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/table" "github.com/gcla/gowid/widgets/tree" "github.com/gcla/termshark/v2/pkg/pdmltree" "github.com/gcla/termshark/v2/widgets/search" ) //====================================================================== // StructResult represents a match for a search within the packet structure. The // result is a specific location in the packet struct model. The UI will be updated // to show this match, expanded, when a search succeeds. type StructResult struct { PacketNum int TreePos tree.IPos Model *pdmltree.Model } func (s StructResult) PacketNumber() int { return s.PacketNum } //====================================================================== // Search in the packet struct view: // // // // // // // // // type StructSearchCallbacks struct { *commonSearchCallbacks *SearchStopper term search.INeedle samePacketPos tree.IPos samePacket bool // set to false if we have advanced beyond the current packet, so don't need to check list pos search chan search.IntermediateResult } var _ tree.ISearchPred = (*StructSearchCallbacks)(nil) var _ search.IRequestStop = (*StructSearchCallbacks)(nil) var _ search.ICallbacks = (*StructSearchCallbacks)(nil) func (w *StructSearchCallbacks) Reset(app gowid.IApp) { w.SearchStopper.Requested = false w.ticks = 0 } func (w *StructSearchCallbacks) StartingPosition() (interface{}, error) { p, err := packetNumberFromCurrentTableRow() if err != nil { return StructResult{}, err } return StructResult{ PacketNum: p.Pos, }, nil } // own goroutine // startPacketNumber >= 1 func (w *StructSearchCallbacks) SearchPacketsFrom(ifrom interface{}, istart interface{}, term search.INeedle, app gowid.IApp) { start := istart.(StructResult) from := ifrom.(StructResult) res := search.Result{} searchRes := StructResult{} // Same position, same packet - so set flags to ensure the depth first search skips until we're // past the current position if from.PacketNum == start.PacketNum && ((from.TreePos == nil && start.TreePos == nil) || (from.TreePos != nil && start.TreePos != nil && from.TreePos.Equal(start.TreePos))) { w.samePacket = true w.samePacketPos = curStructPosition } // True if we have packets in the current batch to search (and we aren't blocked waiting for them to load) curPacketNumber := from.PacketNum var resumeAt *StructResult defer func() { if resumeAt != nil { w.search <- search.IntermediateResult{ Res: res, ResumeAt: *resumeAt, } } else { w.search <- search.IntermediateResult{ Res: res, } } }() searchCount := 0 Loop: for { w.DoIfStopped(func() { res.Interrupted = true }) if res.Interrupted { break Loop } Loader.PsmlLoader.Lock() // curPacketNumber is the packet number from the pdml 24. Remember there might // be a display filter in place. packetIndex, ok := Loader.PacketNumberMap[curPacketNumber] if !ok { // 1-based - packet number e.g. 24 resumeAt = &StructResult{ PacketNum: curPacketNumber, } Loader.PsmlLoader.Unlock() break } if packetIndex >= len(Loader.PsmlData()) { panic(nil) } Loader.PsmlLoader.Unlock() //====================================================================== // Returns a new object. Already takes the loader lock model := getCurrentStructModel(packetIndex) if model == nil { // This means the PDML doesn't exist in the cache. We need to request it and wait resumeAt = &StructResult{ PacketNum: curPacketNumber, } break Loop } expModel := (*pdmltree.ExpandedModel)(model) w.term = term fpos := tree.DepthFirstSearch(expModel, w) if fpos != nil { searchRes.PacketNum = curPacketNumber searchRes.TreePos = fpos searchRes.Model = model res.Position = searchRes res.Success = true // Terminate the search break Loop } //====================================================================== w.samePacket = false w.samePacketPos = nil searchCount += 1 // Can this be more sophisticated? Loader.PsmlLoader.Lock() // 32, 44, 45, 134, 209,... curPacketNumber, ok = Loader.PacketNumberOrder[curPacketNumber] if !ok { // PacketNumberOrder is set up by the PSML loader, so if there is no next // value, it means we're at the end of the packets and we should loop back. curPacketNumber = Loader.PacketNumberOrder[0] } Loader.PsmlLoader.Unlock() // Go 1 past because if we loop round, we should search the original packet again // in case there is a hit earlier in its structure if searchCount > len(Loader.PacketNumberMap) { break Loop } } } func (s *StructSearchCallbacks) SearchPacketsResult(res search.Result, app gowid.IApp) { app.Run(gowid.RunFunction(func(app gowid.IApp) { // Do this because we might be on the same listview packet, so the change callback // won't run and adjust the lower view // // UPDATE - this assumes the original start position in the table is the same as // the one now. ClearProgressWidgetFor(app, SearchOwns) if res.Interrupted { return } if !res.Success { OpenError("Not found.", app) return } tableCol := 0 structRes := res.Position.(StructResult) curTablePos, err := packetListView.FocusXY() if err == nil { tableCol = curTablePos.Column } tableRow, err := tableRowFromPacketNumber(structRes.PacketNum) if err != nil { OpenError(fmt.Sprintf("Could not move to packet %d\n\n%v", structRes.PacketNum, err), app) return } packetListView.SetFocusXY(app, table.Coords{Column: tableCol, Row: tableRow}) // Don't continue to jump to the end AutoScroll = false //======================================== curSearchPosition = structRes.TreePos.(*tree.TreePos) subIModel := structRes.TreePos.GetSubStructure((*pdmltree.ExpandedModel)(structRes.Model)) expSubModel := subIModel.(*pdmltree.ExpandedModel) subModel := (*pdmltree.Model)(expSubModel) structRes.Model.MakeParentLinks(&curExpandedStructNodes) subModel.SetCollapsed(app, false) expRootModel := (*pdmltree.ExpandedModel)(structRes.Model) treeAtCurPos := curSearchPosition.GetSubStructure(expRootModel) // Save [/, tcp, tcp.srcport] - so we can apply if user moves in packet list curPdmlPosition = (*pdmltree.Model)(treeAtCurPos.(*pdmltree.ExpandedModel)).PathToRoot() //======================================== // Callback might not run if focus position in table is the same e.g. if we find a match // on the same row that we started. So in that case, to expand the lower widgets, call // setLowerWidgets explicitly - don't rely on the focus-changed callback. And I can't // do a shortcut and call this if start == current because the starting position for the // search may not be the same as the list-view row on display - maybe the search has // resumed not that some extra PDML data has been loaded setLowerWidgets(app) // It looks better than having the found packet be at the top of the view packetListView.GoToMiddle(app) curPacketStructWidget.GoToMiddle(app) curStructWidgetState = curPacketStructWidget.State() })) } // CheckNode is provided to implement ISearchPred for the gowid tree's depth first search. func (w *StructSearchCallbacks) CheckNode(tr tree.IModel, pos tree.IPos) bool { if w.samePacket { if w.samePacketPos != nil && !pos.GreaterThan(w.samePacketPos) { return false } } return w.term.Search(tr.Leaf()) != -1 } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.4.0/ui/streamui.go000066400000000000000000000343031426312004500163120ustar00rootroot00000000000000// Copyright 2019-2022 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" "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/configs/profiles" "github.com/gcla/termshark/v2/pkg/pcap" "github.com/gcla/termshark/v2/pkg/pdmltree" "github.com/gcla/termshark/v2/pkg/streams" "github.com/gcla/termshark/v2/widgets/appkeys" "github.com/gcla/termshark/v2/widgets/streamwidget" "github.com/gdamore/tcell/v2" lru "github.com/hashicorp/golang-lru" log "github.com/sirupsen/logrus" ) var streamViewNoKeysHolder *holder.Widget var streamView *appkeys.KeyWidget var conversationMenu *menu.Widget var conversationMenuHolder *holder.Widget var streamsPcapSize int64 var currentStreamKey *streamKey var streamWidgets *lru.Cache // map[streamKey]*streamwidget.Widget var StreamLoader *streams.Loader // DOC - one because it holds stream index state for pcap //====================================================================== // The index for the stream widget cache e.g. UDP stream 6 type streamKey struct { proto streams.Protocol idx int } //====================================================================== type ManageStreamCache struct{} var _ pcap.INewSource = ManageStreamCache{} var _ pcap.IClear = ManageStreamCache{} // Make sure that existing stream widgets are discarded if the user loads a new pcap. func (t ManageStreamCache) OnNewSource(pcap.HandlerCode, gowid.IApp) { clearStreamState() } func (t ManageStreamCache) OnClear(pcap.HandlerCode, gowid.IApp) { clearStreamState() } //====================================================================== func streamKeyPress(evk *tcell.EventKey, app gowid.IApp) bool { handled := false if evk.Rune() == 'q' || evk.Rune() == 'Q' || evk.Key() == tcell.KeyEscape { closeStreamUi(app, true) StreamLoader.StopLoad() handled = true } return handled } func startStreamReassembly(app gowid.IApp) { var model *pdmltree.Model if packetListView != nil { if fxy, err := packetListView.FocusXY(); err == nil { rid, _ := packetListView.Model().RowIdentifier(fxy.Row) row := int(rid) model = getCurrentStructModel(row) } } if model == nil { OpenError("No packets available.", app) return } proto := streams.TCP streamIndex := model.TCPStreamIndex() if streamIndex.IsNone() { proto = streams.UDP streamIndex = model.UDPStreamIndex() if streamIndex.IsNone() { OpenError("Please select a TCP or UDP packet.", app) return } } filterProto := gwutil.If(proto == streams.TCP, "tcp", "udp").(string) filter := fmt.Sprintf("%s.stream eq %d", filterProto, streamIndex.Val()) previousFilterValue := FilterWidget.Value() FilterWidget.SetValue(filter, app) RequestNewFilter(filter, app) currentStreamKey = &streamKey{proto: proto, idx: streamIndex.Val()} newSize, reset := termshark.FileSizeDifferentTo(Loader.PcapPdml, streamsPcapSize) if reset { streamWidgets = nil } // we maintain an lru.Cache of stream widgets so that we can quickly re-open // the UI for streams that have been calculated before. if streamWidgets == nil { initStreamWidgetCache() streamsPcapSize = newSize } var swid *streamwidget.Widget swid2, ok := streamWidgets.Get(*currentStreamKey) if ok { swid = swid2.(*streamwidget.Widget) ok = swid.Finished() } if ok { openStreamUi(swid, app) } else { swid = makeStreamWidget(previousFilterValue, filter, Loader.String(), proto) streamWidgets.Add(*currentStreamKey, swid) // Use the source context. At app shutdown, canceling main will cancel src which will cancel the stream // loader. And changing source should also cancel the stream loader on all occasions. StreamLoader = streams.NewLoader(streams.MakeCommands(), Loader.Context()) sh := &streamParseHandler{ app: app, name: Loader.String(), proto: proto, idx: streamIndex.Val(), wid: swid, } StreamLoader.StartLoad( Loader.PcapPdml, filterProto, streamIndex.Val(), app, sh, ) } } //====================================================================== type streamParseHandler struct { app gowid.IApp tick *time.Ticker // for updating the spinner stopChunks chan struct{} stopIndices chan struct{} chunks chan streams.IChunk pktIndices chan int name string proto streams.Protocol idx int wid *streamwidget.Widget pleaseWaitClosed bool openedStreams bool sync.Mutex } var _ streams.IOnStreamChunk = (*streamParseHandler)(nil) var _ streams.IOnStreamHeader = (*streamParseHandler)(nil) var _ pcap.IBeforeBegin = (*streamParseHandler)(nil) var _ pcap.IAfterEnd = (*streamParseHandler)(nil) var _ pcap.IOnError = (*streamParseHandler)(nil) // Run from the app goroutine func (t *streamParseHandler) drainChunks() int { curLen := len(t.chunks) for i := 0; i < curLen; i++ { chunk := <-t.chunks if !t.pleaseWaitClosed { t.pleaseWaitClosed = true ClosePleaseWait(t.app) } t.wid.AddChunkEntire(chunk, t.app) } return curLen } // Run from the app goroutine func (t *streamParseHandler) drainPacketIndices() int { curLen := len(t.pktIndices) for i := 0; i < curLen; i++ { packet := <-t.pktIndices t.wid.TrackPayloadPacket(packet) } return curLen } func (t *streamParseHandler) BeforeBegin(code pcap.HandlerCode, app gowid.IApp) { if code&pcap.StreamCode == 0 { return } app.Run(gowid.RunFunction(func(app gowid.IApp) { OpenPleaseWait(appView, app) })) t.tick = time.NewTicker(time.Duration(200) * time.Millisecond) t.stopChunks = make(chan struct{}) t.stopIndices = make(chan struct{}) t.chunks = make(chan streams.IChunk, 1000) t.pktIndices = make(chan int, 1000) // Start this after widgets have been cleared, to get focus change termshark.TrackedGo(func() { fn := func() { app.Run(gowid.RunFunction(func(app gowid.IApp) { t.drainChunks() if !t.openedStreams { appViewNoKeys.SetSubWidget(streamView, app) openStreamUi(t.wid, app) t.openedStreams = true } })) } termshark.RunOnDoubleTicker(t.stopChunks, fn, time.Duration(200)*time.Millisecond, time.Duration(200)*time.Millisecond, 10) }, Goroutinewg) termshark.TrackedGo(func() { fn := func() { app.Run(gowid.RunFunction(func(app gowid.IApp) { t.drainPacketIndices() })) } termshark.RunOnDoubleTicker(t.stopIndices, fn, time.Duration(200)*time.Millisecond, time.Duration(200)*time.Millisecond, 10) }, Goroutinewg) termshark.TrackedGo(func() { Loop: for { select { case <-t.tick.C: app.Run(gowid.RunFunction(func(app gowid.IApp) { pleaseWaitSpinner.Update() })) case <-t.stopChunks: break Loop } } }, Goroutinewg) } func (t *streamParseHandler) AfterIndexEnd(success bool) { t.wid.SetFinished(success) close(t.stopIndices) for { if t.drainPacketIndices() == 0 { break } } } func (t *streamParseHandler) AfterEnd(code pcap.HandlerCode, app gowid.IApp) { if code&pcap.StreamCode == 0 { return } app.Run(gowid.RunFunction(func(app gowid.IApp) { if !t.pleaseWaitClosed { t.pleaseWaitClosed = true ClosePleaseWait(app) } if !t.openedStreams { openStreamUi(t.wid, app) t.openedStreams = true } // Clear out anything lingering from last ticker run to now for { if t.drainChunks() == 0 { break } } if t.wid.NumChunks() == 0 { OpenMessage("No stream payloads found.", appView, app) } })) close(t.stopChunks) } func (t *streamParseHandler) TrackPayloadPacket(packet int) { t.Lock() defer t.Unlock() t.pktIndices <- packet } func (t *streamParseHandler) OnStreamHeader(hdr streams.FollowHeader) { t.app.Run(gowid.RunFunction(func(app gowid.IApp) { t.wid.AddHeader(hdr, app) })) } // Handle a line/chunk of input - one piece of reassembled data, which comes with // a client/server direction. func (t *streamParseHandler) OnStreamChunk(chunk streams.IChunk) { t.Lock() defer t.Unlock() t.chunks <- chunk } func (t *streamParseHandler) OnError(code pcap.HandlerCode, app gowid.IApp, err error) { if code&pcap.StreamCode == 0 { return } log.Error(err) if !Running { fmt.Fprintf(os.Stderr, "%v\n", err) RequestQuit() } else if !profiles.ConfBool("main.suppress-tshark-errors", true) { var errstr string if kverr, ok := err.(gowid.KeyValueError); ok { errstr = termshark.KeyValueErrorString(kverr) } else { errstr = fmt.Sprintf("%v", err) } app.Run(gowid.RunFunction(func(app gowid.IApp) { OpenLongError(errstr, app) })) } } func initStreamWidgetCache() { widgetCacheSize := profiles.ConfInt("main.stream-cache-size", 100) var err error streamWidgets, err = lru.New(widgetCacheSize) if err != nil { log.Fatal(err) } log.Infof("Initialized stream widget cache with %d entries.", widgetCacheSize) } func clearStreamState() { initStreamWidgetCache() currentStreamKey = nil } type streamClicker struct{} var _ streamwidget.IChunkClicked = streamClicker{} func (c streamClicker) OnPacketClicked(pkt int, app gowid.IApp) error { if packetListView != nil { coords, err := packetListView.FocusXY() if err == nil { // Need to go from row identifier ("9th packet") to display order which might be sorted. // Note that for our pcap table, the row identifier (logical id) for each table row is // itself an int i.e. packet #0, packet #1 (although the packet's *frame number* might // be different if there's a filter). When OnChunkClicked() is called, it means react // to a click on the logical packet #N (where the stream loader tracks pcap packet -> // packet-with-payload). So // // - user clicks on 9th item in stream view // - the stream loader translates this to the 15th packet in the pcap (rest are SYN, etc) // - the inverted table model translates this to display row #5 in the table (because it's sorted) // - then we set the display row and switch to the data/away from the table header // if row, ok := packetListView.InvertedModel().IdentifierToRow(table.RowId(pkt)); !ok { OpenError(fmt.Sprintf("Unexpected error looking up packet %v.", pkt), app) } else { coords.Row = row // cast to int - we want row == #item in list packetListView.SetFocusXY(app, coords) packetListTable.SetFocusOnData(app) packetListTable.GoToMiddle(app) setFocusOnPacketList(app) packetListData := packetListView.Model().(table.ISimpleDataProvider).GetData() // This condition should always be true. I just feel cautious because accessing the psml data // in this way feels fragile. Also, take note: there's an open issue to make it possible to // customize the packet headers, in which case the item at index 0 in the psml might not be // the frame number (though this check doesn't guard against that...). It's more useful // to display the actual frame number if possible, so do that if we can, otherwise just // display which segment of the stream this is. if len(packetListData) > row && len(packetListData[row]) > 0 { OpenMessage(fmt.Sprintf("Selected packet %s.", packetListData[row][0]), appView, app) } else { OpenMessage(fmt.Sprintf("Selected segment %d.", pkt+1), appView, app) } } } } return nil } func (c streamClicker) HandleError(row table.RowId, err error, app gowid.IApp) { OpenError(fmt.Sprintf("Packet at row %v is not loaded yet. Try again in a few seconds.", row), app) } //====================================================================== type simpleOnError struct{} func (s simpleOnError) OnError(msg string, app gowid.IApp) { OpenError(msg, app) } func makeStreamWidget(previousFilter string, filter string, cap string, proto streams.Protocol) *streamwidget.Widget { return streamwidget.New(filter, cap, proto, conversationMenu, conversationMenuHolder, &keyState, streamwidget.Options{ MenuOpener: &multiMenu1Opener, DefaultDisplay: func() streamwidget.DisplayFormat { view := streamwidget.Hex choice := profiles.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) RequestNewFilter(newFilter, app) }, CopyModeWidget: CopyModeWidget, ChunkClicker: streamClicker{}, ErrorHandler: simpleOnError{}, }) } //====================================================================== func openStreamUi(swid *streamwidget.Widget, app gowid.IApp) { streamViewNoKeysHolder.SetSubWidget(swid, app) appViewNoKeys.SetSubWidget(streamView, app) // When opening, put focus on the list of stream chunks. There may be none in which case // this won't work. But when UI is constructed, there are no chunks, even though it's not // open yet, so focus on the pile goes to the bottom, even when the chunk table becomes populated. // That's not ideal. swid.SetFocusOnChunksIfPossible(app) } func closeStreamUi(app gowid.IApp, refocus bool) { appViewNoKeys.SetSubWidget(mainView, app) // Do this if the user starts reassembly from the menu - better UX if refocus { setFocusOnPacketList(app) } } //====================================================================== func buildStreamUi() { conversationMenuHolder, conversationMenu = streamwidget.MakeConvMenu(&multiMenu1Opener) 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.4.0/ui/switchterm.go000066400000000000000000000120041426312004500166440ustar00rootroot00000000000000// Copyright 2019-2022 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" "time" "github.com/gcla/gowid" "github.com/gcla/gowid/widgets/dialog" "github.com/gcla/gowid/widgets/framed" "github.com/gcla/gowid/widgets/holder" "github.com/gcla/gowid/widgets/paragraph" "github.com/gcla/termshark/v2" "github.com/gcla/termshark/v2/configs/profiles" ) //====================================================================== // SuggestSwitchingTerm will open a dialog asking the user if they would like to try // a more colorful TERM setting. func SuggestSwitchingTerm(app gowid.IApp) { var switchTerm *dialog.Widget Yes := dialog.Button{ Msg: "Yes", Action: gowid.MakeWidgetCallback("exec", gowid.WidgetChangedFunction(func(app gowid.IApp, w gowid.IWidget) { termshark.ShouldSwitchTerminal = true switchTerm.Close(app) RequestQuit() })), } No := dialog.Button{ Msg: "No", } NoAsk := dialog.Button{ Msg: "No, don't ask", Action: gowid.MakeWidgetCallback("exec", gowid.WidgetChangedFunction(func(app gowid.IApp, w gowid.IWidget) { profiles.SetConf("main.disable-term-helper", true) switchTerm.Close(app) })), } term := os.Getenv("TERM") term256 := term + "-256color" switchTerm = dialog.New( framed.NewSpace(paragraph.New(fmt.Sprintf("Termshark is running with TERM=%s. The terminal database contains %s. Would you like to switch for a more colorful experience? Termshark will need to restart.", term, term256))), dialog.Options{ Buttons: []dialog.Button{Yes, No, NoAsk}, NoShadow: true, BackgroundStyle: gowid.MakePaletteRef("dialog"), BorderStyle: gowid.MakePaletteRef("dialog"), ButtonStyle: gowid.MakePaletteRef("dialog-button"), Modal: true, FocusOnWidget: false, }, ) switchTerm.Open(appView, gowid.RenderWithRatio{R: 0.5}, app) } //====================================================================== // IsTerminalLegible will open up a dialog asking the user to confirm that their // running termshark is legible, having upgraded the TERM variable to a 256-color // version and restarted. func IsTerminalLegible(app gowid.IApp) { var saveTerm *dialog.Widget YesSave := dialog.Button{ Msg: "Yes", Action: gowid.MakeWidgetCallback("exec", gowid.WidgetChangedFunction(func(app gowid.IApp, w gowid.IWidget) { profiles.SetConf("main.term", os.Getenv("TERM")) saveTerm.Close(app) })), } NoSave := dialog.Button{ Msg: "No", } saveTerm = dialog.New( framed.NewSpace(paragraph.New(fmt.Sprintf("Do you want to save TERM=%s in termshark's config to use as the default?", os.Getenv("TERM")))), dialog.Options{ Buttons: []dialog.Button{YesSave, NoSave}, NoShadow: true, BackgroundStyle: gowid.MakePaletteRef("dialog"), BorderStyle: gowid.MakePaletteRef("dialog"), ButtonStyle: gowid.MakePaletteRef("dialog-button"), Modal: true, FocusOnWidget: false, }, ) tick := time.NewTicker(time.Duration(1) * time.Second) stopC := make(chan struct{}) var legibleTerm *dialog.Widget No := dialog.Button{ Msg: "No", Action: gowid.MakeWidgetCallback("exec", gowid.WidgetChangedFunction(func(app gowid.IApp, w gowid.IWidget) { close(stopC) termshark.ShouldSwitchBack = true legibleTerm.Close(app) RequestQuit() })), } Yes := dialog.Button{ Msg: "Yes", Action: gowid.MakeWidgetCallback("exec", gowid.WidgetChangedFunction(func(app gowid.IApp, w gowid.IWidget) { close(stopC) legibleTerm.Close(app) saveTerm.Open(appView, gowid.RenderWithRatio{R: 0.5}, app) })), } secs := 10 tw := func(count int) *paragraph.Widget { return paragraph.New(fmt.Sprintf("Is the terminal legible? If no selection is made, termshark will revert to its original TERM setting in %d seconds.", secs)) } message := holder.New(tw(secs)) termshark.TrackedGo(func() { Loop: for { select { case <-tick.C: secs-- switch { case secs >= 0: app.Run(gowid.RunFunction(func(app gowid.IApp) { message.SetSubWidget(tw(secs), app) })) case secs < 0: tick.Stop() close(stopC) app.Run(gowid.RunFunction(func(app gowid.IApp) { termshark.ShouldSwitchBack = true legibleTerm.Close(app) RequestQuit() })) } case <-stopC: break Loop } } }, Goroutinewg) legibleTerm = dialog.New( framed.NewSpace(message), dialog.Options{ Buttons: []dialog.Button{Yes, No}, NoShadow: true, BackgroundStyle: gowid.MakePaletteRef("dialog"), BorderStyle: gowid.MakePaletteRef("dialog"), ButtonStyle: gowid.MakePaletteRef("dialog-button"), Modal: true, FocusOnWidget: false, StartIdx: 1, }, ) legibleTerm.Open(appView, gowid.RenderWithRatio{R: 0.5}, app) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.4.0/ui/tableutil/000077500000000000000000000000001426312004500161145ustar00rootroot00000000000000termshark-2.4.0/ui/tableutil/tableutil.go000066400000000000000000000043731426312004500204370ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package tableutil contains user-interface functions and helpers for termshark's // tables - in particular, helpers for vim key sequences like 5gg and G package tableutil import ( "github.com/gcla/gowid" "github.com/gcla/gowid/widgets/table" "github.com/gcla/termshark/v2" "github.com/gcla/termshark/v2/widgets/appkeys" "github.com/gdamore/tcell/v2" ) //====================================================================== type GoToAdapter struct { *table.BoundedWidget *termshark.KeyState } var _ IGoToLineRequested = (*GoToAdapter)(nil) func (t *GoToAdapter) GoToLineOrTop(evk *tcell.EventKey) (bool, int) { num := -1 if t.NumberPrefix != -1 { num = t.NumberPrefix - 1 } return evk.Key() == tcell.KeyRune && evk.Rune() == 'g' && t.PartialgCmd, num } func (t *GoToAdapter) GoToLineOrBottom(evk *tcell.EventKey) (bool, int) { num := -1 if t.NumberPrefix != -1 { num = t.NumberPrefix - 1 } return evk.Key() == tcell.KeyRune && evk.Rune() == 'G', num } type IGoToLineRequested interface { GoToLineOrTop(evk *tcell.EventKey) (bool, int) // -1 means top GoToLineOrBottom(evk *tcell.EventKey) (bool, int) // -1 means bottom GoToFirst(gowid.IApp) bool GoToLast(gowid.IApp) bool GoToNth(gowid.IApp, int) bool } // GotoHander retrusn a function suitable for the appkeys widget - it will // check to see if the key represents a supported action on the table and // then runs the action if so. func GotoHandler(t IGoToLineRequested) appkeys.KeyInputFn { return func(evk *tcell.EventKey, app gowid.IApp) bool { handled := false if t != nil { handled = true if doit, line := t.GoToLineOrTop(evk); doit { if line == -1 { t.GoToFirst(app) } else { // psml starts counting at 1 t.GoToNth(app, line) } } else if doit, line := t.GoToLineOrBottom(evk); doit { if line == -1 { t.GoToLast(app) } else { // psml starts counting at 1 t.GoToNth(app, line) } } else { handled = false } } return handled } } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.4.0/ui/ui.go000066400000000000000000003766121426312004500151120ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package ui contains user-interface functions and helpers for termshark. package ui import ( "encoding/xml" "fmt" "math" "os" "reflect" "regexp" "runtime" "strconv" "strings" "sync" "time" "github.com/gcla/deep" "github.com/gcla/gowid" "github.com/gcla/gowid/gwutil" "github.com/gcla/gowid/widgets/button" "github.com/gcla/gowid/widgets/clicktracker" "github.com/gcla/gowid/widgets/columns" "github.com/gcla/gowid/widgets/dialog" "github.com/gcla/gowid/widgets/disable" "github.com/gcla/gowid/widgets/divider" "github.com/gcla/gowid/widgets/fill" "github.com/gcla/gowid/widgets/framed" "github.com/gcla/gowid/widgets/holder" "github.com/gcla/gowid/widgets/hpadding" "github.com/gcla/gowid/widgets/isselected" "github.com/gcla/gowid/widgets/list" "github.com/gcla/gowid/widgets/menu" "github.com/gcla/gowid/widgets/null" "github.com/gcla/gowid/widgets/overlay" "github.com/gcla/gowid/widgets/pile" "github.com/gcla/gowid/widgets/progress" "github.com/gcla/gowid/widgets/selectable" "github.com/gcla/gowid/widgets/spinner" "github.com/gcla/gowid/widgets/styled" "github.com/gcla/gowid/widgets/table" "github.com/gcla/gowid/widgets/text" "github.com/gcla/gowid/widgets/tree" "github.com/gcla/gowid/widgets/vpadding" "github.com/gcla/termshark/v2" "github.com/gcla/termshark/v2/configs/profiles" "github.com/gcla/termshark/v2/pkg/fields" "github.com/gcla/termshark/v2/pkg/noroot" "github.com/gcla/termshark/v2/pkg/pcap" "github.com/gcla/termshark/v2/pkg/pdmltree" "github.com/gcla/termshark/v2/pkg/psmlmodel" "github.com/gcla/termshark/v2/pkg/shark" "github.com/gcla/termshark/v2/pkg/system" "github.com/gcla/termshark/v2/pkg/theme" "github.com/gcla/termshark/v2/ui/menuutil" "github.com/gcla/termshark/v2/ui/tableutil" "github.com/gcla/termshark/v2/widgets" "github.com/gcla/termshark/v2/widgets/appkeys" "github.com/gcla/termshark/v2/widgets/copymodetree" "github.com/gcla/termshark/v2/widgets/enableselected" "github.com/gcla/termshark/v2/widgets/expander" "github.com/gcla/termshark/v2/widgets/filter" "github.com/gcla/termshark/v2/widgets/hexdumper2" "github.com/gcla/termshark/v2/widgets/ifwidget" "github.com/gcla/termshark/v2/widgets/mapkeys" "github.com/gcla/termshark/v2/widgets/minibuffer" "github.com/gcla/termshark/v2/widgets/resizable" "github.com/gcla/termshark/v2/widgets/rossshark" "github.com/gcla/termshark/v2/widgets/search" "github.com/gcla/termshark/v2/widgets/withscrollbar" "github.com/gdamore/tcell/v2" lru "github.com/hashicorp/golang-lru" "github.com/pkg/errors" log "github.com/sirupsen/logrus" "github.com/spf13/viper" ) //====================================================================== var Goroutinewg *sync.WaitGroup type WidgetOwner int const ( NoOwner WidgetOwner = iota LoaderOwns SearchOwns ) // Global so that we can change the displayed packet in the struct view, etc // test var appViewNoKeys *holder.Widget var appView *holder.Widget var mbView *holder.Widget var mainViewNoKeys *holder.Widget var mainView *appkeys.KeyWidget var pleaseWaitSpinner *spinner.Widget var mainviewRows *resizable.PileWidget var mainview gowid.IWidget var altview1 gowid.IWidget var altview1OuterRows *resizable.PileWidget var altview1Pile *resizable.PileWidget var altview1Cols *resizable.ColumnsWidget var altview2 gowid.IWidget var altview2OuterRows *resizable.PileWidget var altview2Pile *resizable.PileWidget var altview2Cols *resizable.ColumnsWidget var viewOnlyPacketList *pile.Widget var viewOnlyPacketStructure *pile.Widget var viewOnlyPacketHex *pile.Widget var filterCols *columns.Widget var loadProg *columns.Widget var loadStop *button.Widget var searchProg *columns.Widget var searchStop *button.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 searchPathMain []interface{} var searchPathAlt []interface{} var searchPathMax []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 profileMenu *menu.Widget var FilterWidget *filter.Widget var Fin *rossshark.Widget var CopyModeWidget gowid.IWidget var CopyModePredicate ifwidget.Predicate var openMenuSite *menu.SiteWidget var openAnalysisSite *menu.SiteWidget var packetListViewHolder *holder.Widget var packetListTable *table.BoundedWidget var packetStructureViewHolder *holder.Widget var packetHexViewHolder *holder.Widget var progressHolder *holder.Widget var progressOwner WidgetOwner var stopCurrentSearch search.IRequestStop var loadProgress *progress.Widget var loadSpinner *spinner.Widget var savedListBoxWidgetHolder *holder.Widget var singlePacketViewMsgHolder *holder.Widget // either empty or "loading..." var keyMapper *mapkeys.Widget // For deconstructing the @showname PDML attribute into a short form for the UI var shownameRe = regexp.MustCompile(`(.*?= )?([^:]+)`) type MenuHolder struct { gowid.IMenuCompatible } var multiMenu *MenuHolder = &MenuHolder{} var multiMenuWidget *holder.Widget var multiMenu2 *MenuHolder = &MenuHolder{} var multiMenu2Widget *holder.Widget var multiMenu1Opener MultiMenuOpener var multiMenu2Opener MultiMenuOpener var tabViewsForward map[gowid.IWidget]gowid.IWidget var tabViewsBackward map[gowid.IWidget]gowid.IWidget var currentProfile *text.Widget var currentProfileWidget *columns.Widget var currentProfileWidgetHolder *holder.Widget var openProfileSite *menu.SiteWidget var currentCapture *text.Widget var currentCaptureWidget *columns.Widget var currentCaptureWidgetHolder *holder.Widget var nullw *null.Widget // empty var fillSpace *fill.Widget var fillVBar *fill.Widget var colSpace *gowid.ContainerWidget var curPacketStructWidget *copymodetree.Widget var packetHexWidgets *lru.Cache var packetListView *psmlTableRowWidget // Usually false. When the user moves the cursor in the hex widget, a callback will update the // struct widget's current expansion. That results in a callback to the current hex widget to // update its position - ad inf. The hex widget callback checks to see whether or not the hex // widget has "focus". If it doesn't, the callback is suppressed - to short circuit the callback // loop. BUT - after a packet search, we reposition the hex widget and want the callback from // hex to struct to happen once. So this is a workaround to allow it in that case. // // This variable has two effects: // - when the hex widget is positioned programmatically, and focus is not on the hex widget, // the struct widget is nevertheless updated accordingly // - but when the struct widget is updated, if the innermost layer does not capture the // current hex location (the search destination), DON'T update the hex position to be // inside the PDML's innermost layer, which maybe somewhere else in the packet. // var allowHexToStructRepositioning bool var filterWithSearch gowid.IWidget var filterWithoutSearch gowid.IWidget var filterHolder *holder.Widget var SearchWidget *search.Widget var Loadingw gowid.IWidget // "loading..." var MissingMsgw gowid.IWidget // centered, holding singlePacketViewMsgHolder var EmptyStructViewTimer *time.Timer var EmptyHexViewTimer *time.Timer var curSearchPosition tree.IPos // e.g. [0, 4] -> the indices of the struct layer 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 curColumnFilter string // e.g. tcp.port - updated as the user moves through the struct widget var curColumnFilterName string // e.g. "TCP port" - from the showname attribute in the PDML var curColumnFilterValue string // e.g. "80" - from the show attribute var CacheRequests []pcap.LoadPcapSlice var CacheRequestsChan chan struct{} // false means started, true means finished var QuitRequestedChan chan struct{} var StartUIChan chan struct{} var StartUIOnce sync.Once // Store this for vim-like keypresses that are a sequence e.g. "ZZ" var keyState termshark.KeyState var marksMap map[rune]termshark.JumpPos var globalMarksMap map[rune]termshark.GlobalJumpPos var lastJumpPos int var NoGlobalJump termshark.GlobalJumpPos // leave as default, like a placeholder var Loader *pcap.PacketLoader var FieldCompleter *fields.TSharkFields // share this - safe once constructed var WriteToSelected bool // true if the user provided the -w flag var WriteToDeleted bool // true if the user deleted the temporary pcap before quitting var DarkMode bool // global state in app var PacketColors bool // global state in app var PacketColorsSupported bool // global state in app - true if it's even possible var AutoScroll bool // true if the packet list should auto-scroll when listening on an interface. var newPacketsArrived bool // true if current updates are due to new packets when listening on an interface. var reenableAutoScroll bool // set to true by keypress processing widgets - used with newPacketsArrived var Running bool // true if gowid/tcell is controlling the terminal var QuitRequested bool // true if a quit has been issued, but not yet processed. Stops some handlers displaying errors. //====================================================================== func init() { curExpandedStructNodes = make(pdmltree.ExpandedPaths, 0, 20) QuitRequestedChan = make(chan struct{}, 1) // buffered because send happens from ui goroutine, which runs global select CacheRequestsChan = make(chan struct{}, 1000) CacheRequests = make([]pcap.LoadPcapSlice, 0) // Buffered because I might send something in this goroutine StartUIChan = make(chan struct{}, 1) keyState.NumberPrefix = -1 // 0 might be meaningful marksMap = make(map[rune]termshark.JumpPos) globalMarksMap = make(map[rune]termshark.GlobalJumpPos) lastJumpPos = -1 EnsureTemplateData() TemplateData["Marks"] = marksMap TemplateData["GlobalMarks"] = globalMarksMap TemplateData["Maps"] = getMappings{} } type globalJump struct { file string pos int } type getMappings struct{} func (g getMappings) Get() []termshark.KeyMapping { return termshark.LoadKeyMappings() } func (g getMappings) None() bool { return len(termshark.LoadKeyMappings()) == 0 } //====================================================================== type MultiMenuOpener struct { under gowid.IWidget mm *MenuHolder } var _ menu.IOpener = (*MultiMenuOpener)(nil) func (o *MultiMenuOpener) OpenMenu(mnu *menu.Widget, site menu.ISite, app gowid.IApp) bool { if o.mm.IMenuCompatible != mnu { // Adds the menu to the render tree - when not open, under is here instead o.mm.IMenuCompatible = mnu // Now make under the lower layer of the menu mnu.SetSubWidget(o.under, app) mnu.OpenImpl(site, app) app.Redraw() return true } else { return false } } func (o *MultiMenuOpener) CloseMenu(mnu *menu.Widget, app gowid.IApp) { if o.mm.IMenuCompatible == mnu { mnu.CloseImpl(app) o.mm.IMenuCompatible = holder.New(o.under) } } //====================================================================== // // Handle examples like // .... ..1. .... .... .... .... = LG bit: Locally administered address (this is NOT the factory default) // Extract just // LG bit // // I'm trying to copy what Wireshark does, more or less // func columnNameFromShowname(showname string) string { matches := shownameRe.FindStringSubmatch(showname) if len(matches) >= 3 { return matches[2] } return showname } func useAsColumn(filter string, name string, app gowid.IApp) { newCols := profiles.ConfStringSlice("main.column-format", []string{}) colsBak := make([]string, len(newCols)) for i, col := range newCols { colsBak[i] = col } newCols = append(newCols, fmt.Sprintf("%%Cus:%s:0:R", filter), columnNameFromShowname(name), "true", ) profiles.SetConf("main.column-format-bak", colsBak) profiles.SetConf("main.column-format", newCols) RequestReload(app) } // Build the menu dynamically when needed so I can include the filter in the widgets func makePdmlFilterMenu(filter string, val string) *menu.Widget { sites := make(menuutil.SiteMap) needQuotes := false ok, field := FieldCompleter.LookupField(filter) // should be ok, because this filter comes from the PDML, so the filter should // be valid. But if it isn't e.g. newer tshark perhaps, then assume no quotes // are needed. if ok { switch field.Type { case fields.FT_STRING: needQuotes = true case fields.FT_STRINGZ: needQuotes = true case fields.FT_STRINGZPAD: needQuotes = true } } filterStr := filter if val != "" { if needQuotes { filterStr = fmt.Sprintf("%s == \"%s\"", filter, strings.ReplaceAll(val, "\\", "\\\\")) } else { filterStr = fmt.Sprintf("%s == %s", filter, val) } } var pdmlFilterMenu *menu.Widget openPdmlFilterMenu2 := func(prep bool, w gowid.IWidget, app gowid.IApp) { st, ok := sites[w] if !ok { log.Warnf("Unexpected application state: missing menu site for %v", w) return } // This contains logic to close the two PDML menus opened from the struct // view and then to either apply or prepare a new display filter based on // the one that is currently selected by the user (i.e. the one associated // with the open menu) actor := &pdmlFilterActor{ filter: filterStr, prepare: prep, menu1: pdmlFilterMenu, } menuBox := makeFilterCombineMenuWidget(actor) m2 := menu.New("pdmlfilter2", menuBox, 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), }, }) // I need to set this up after constructing m2; m2 itself needs // the menu box widget to display; that needs the actor to process // the clicks of buttons within that widget, and that actor needs // the menu m2 so that it can close it. actor.menu2 = m2 multiMenu2Opener.OpenMenu(m2, st, app) } pdmlFilterItems := []menuutil.SimpleMenuItem{ menuutil.SimpleMenuItem{ Txt: fmt.Sprintf("Apply as Column: %s", filter), Key: gowid.MakeKey('c'), CB: func(app gowid.IApp, w gowid.IWidget) { multiMenu1Opener.CloseMenu(pdmlFilterMenu, app) useAsColumn(curColumnFilter, curColumnFilterName, app) }, }, menuutil.MakeMenuDivider(), menuutil.SimpleMenuItem{ Txt: fmt.Sprintf("Apply Filter: %s", filterStr), Key: gowid.MakeKey('a'), CB: func(app gowid.IApp, w gowid.IWidget) { openPdmlFilterMenu2(false, w, app) }, }, menuutil.SimpleMenuItem{ Txt: fmt.Sprintf("Prep Filter: %s", filterStr), Key: gowid.MakeKey('p'), CB: func(app gowid.IApp, w gowid.IWidget) { openPdmlFilterMenu2(true, w, app) }, }, } pdmlFilterListBox, pdmlFilterWidth := menuutil.MakeMenuWithHotKeys(pdmlFilterItems, sites) // this menu is opened from the PDML struct view and has, as context, the current PDML node. I // need a name for it because I use that var in the closure above. pdmlFilterMenu = menu.New("pdmlfiltermenu", pdmlFilterListBox, units(pdmlFilterWidth), menu.Options{ Modal: true, CloseKeysProvided: true, OpenCloser: &multiMenu1Opener, CloseKeys: []gowid.IKey{ gowid.MakeKey('q'), gowid.MakeKeyExt(tcell.KeyLeft), gowid.MakeKeyExt(tcell.KeyEscape), gowid.MakeKeyExt(tcell.KeyCtrlC), }, }) return pdmlFilterMenu } //====================================================================== func RequestQuit() { select { case QuitRequestedChan <- struct{}{}: default: // Ok for the send not to succeed - there is a buffer of one, and it only // needs one message to start the shutdown sequence. So this means a // message has already been sent (before the main loop gets round to processing // this channel) } } // Runs in app goroutine func UpdateProgressBarForInterface(c *pcap.InterfaceLoader, app gowid.IApp) { SetProgressIndeterminateFor(app, LoaderOwns) loadSpinner.Update() } // Runs in app goroutine func UpdateProgressBarForFile(c *pcap.PacketLoader, prevRatio float64, app gowid.IApp) float64 { SetProgressDeterminateFor(app, LoaderOwns) psmlProg := Prog{0, 100} pdmlPacketProg := Prog{0, 100} pdmlIdxProg := Prog{0, 100} pcapPacketProg := Prog{0, 100} pcapIdxProg := Prog{0, 100} curRowProg := Prog{100, 100} var err error var c2 int64 var m int64 var x int // This shows where we are in the packet list. We want progress to be active only // as long as our view has missing widgets. So this can help predict when our little // view into the list of packets will be populated. Note that if a new pcap is loading, // the packet list view should always be further away than the last packet, so we won't // need the progress bar to tell the user how long until packets appear in the packet // list view; but the packet struct and hex views are populated using a different // mechanism (separate tshark processes) and may leave their views blank while the // packet list view shows data - so the progress bar is useful to indicate when info // will show up in the struct and hex views. currentDisplayedRow := -1 var currentDisplayedRowMod int64 = -1 var currentDisplayedRowDiv int = -1 if packetListView != nil { if fxy, err := packetListView.FocusXY(); err == nil { currentRowId, ok := packetListView.Model().RowIdentifier(fxy.Row) if ok { pktsPerLoad := c.PacketsPerLoad() currentDisplayedRow = int(currentRowId) currentDisplayedRowMod = int64(currentDisplayedRow % pktsPerLoad) // Rounded to 1000 by default currentDisplayedRowDiv = (currentDisplayedRow / pktsPerLoad) * pktsPerLoad c.PsmlLoader.Lock() curRowProg.cur, curRowProg.max = int64(currentDisplayedRow), int64(len(c.PsmlData())) c.PsmlLoader.Unlock() } } } // Progress determined by how many of the (up to) pktsPerLoad pdml packets are read // If it's not the same chunk of rows, assume it won't affect our view, so no progress needed if c.PdmlLoader.IsLoading() { if c.LoadingRow() == currentDisplayedRowDiv { // Data being loaded from pdml + pcap may overlap the current view if x, err = c.LengthOfPdmlCacheEntry(c.LoadingRow()); err == nil { pdmlPacketProg.cur = int64(x) pdmlPacketProg.max = int64(c.KillAfterReadingThisMany) if currentDisplayedRow != -1 && currentDisplayedRowMod < pdmlPacketProg.max { pdmlPacketProg.max = currentDisplayedRowMod + 1 // zero-based if pdmlPacketProg.cur > pdmlPacketProg.max { pdmlPacketProg.cur = pdmlPacketProg.max } } } // Progress determined by how far through the pcap the pdml reader is. c.PdmlLoader.Lock() c2, m, err = system.ProcessProgress(c.PdmlPid, c.PcapPdml) c.PdmlLoader.Unlock() if err == nil { pdmlIdxProg.cur, pdmlIdxProg.max = c2, m if currentDisplayedRow != -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 currentDisplayedRow != -1 && currentDisplayedRowMod < pcapPacketProg.max { pcapPacketProg.max = currentDisplayedRowMod + 1 // zero-based if pcapPacketProg.cur > pcapPacketProg.max { pcapPacketProg.cur = pcapPacketProg.max } } } // Progress determined by how far through the pcap the pcap reader is. c.PdmlLoader.Lock() c2, m, err = system.ProcessProgress(c.PcapPid, c.PcapPcap) c.PdmlLoader.Unlock() if err == nil { pcapIdxProg.cur, pcapIdxProg.max = c2, m if currentDisplayedRow != -1 { // Only need to look this far into the psml file before my view is populated m = m * (curRowProg.cur / curRowProg.max) } } } } if psml, ok := c.PcapPsml.(string); ok && c.PsmlLoader.IsLoading() { c.PsmlLoader.Lock() c2, m, err = system.ProcessProgress(termshark.SafePid(c.PsmlCmd), psml) c.PsmlLoader.Unlock() if err == nil { psmlProg.cur, psmlProg.max = c2, m } } var prog Prog // state is guaranteed not to include pcap.Loadingiface if we showing a determinate progress bar switch { case c.PsmlLoader.IsLoading() && c.PdmlLoader.IsLoading() && c.PdmlLoader.LoadIsVisible(): select { case <-c.StartStage2ChanFn(): prog = psmlProg.Add( progMax(pcapPacketProg, pcapIdxProg).Add( progMax(pdmlPacketProg, pdmlIdxProg), ), ) default: prog = psmlProg.Div(2) // temporarily divide in 2. Leave original for case above - so that the 50% } case c.PsmlLoader.IsLoading(): prog = psmlProg case c.PdmlLoader.IsLoading() && c.PdmlLoader.LoadIsVisible(): prog = progMax(pcapPacketProg, pcapIdxProg).Add( progMax(pdmlPacketProg, pdmlIdxProg), ) } curRatio := float64(prog.cur) / float64(prog.max) if prevRatio < curRatio { loadProgress.SetTarget(app, int(prog.max)) loadProgress.SetProgress(app, int(prog.cur)) } return math.Max(prevRatio, curRatio) } //====================================================================== // psmlSummary is used to generate a summary for the marks dialog type psmlSummary []string func (p psmlSummary) String() string { // Skip packet number return strings.Join([]string(p)[1:], " : ") } //====================================================================== type RenderWeightUpTo struct { gowid.RenderWithWeight max int } func (s RenderWeightUpTo) MaxUnits() int { return s.max } func weightupto(w int, max int) RenderWeightUpTo { return RenderWeightUpTo{gowid.RenderWithWeight{W: w}, max} } func units(n int) gowid.RenderWithUnits { return gowid.RenderWithUnits{U: n} } func weight(n int) gowid.RenderWithWeight { return gowid.RenderWithWeight{W: n} } func ratio(r float64) gowid.RenderWithRatio { return gowid.RenderWithRatio{R: r} } type RenderRatioUpTo struct { gowid.RenderWithRatio max int } func (r RenderRatioUpTo) String() string { return fmt.Sprintf("upto(%v,%d)", r.RenderWithRatio, r.max) } func (r RenderRatioUpTo) MaxUnits() int { return r.max } func ratioupto(f float64, max int) RenderRatioUpTo { return RenderRatioUpTo{gowid.RenderWithRatio{R: f}, max} } //====================================================================== // 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}))) } // Create an empty one here because the selectIf widget needs to have a pointer // to it, and it's constructed below as a child. rememberSel := &rememberSelected{} inner := wmaker.MakeWidget(pos, tr) inner = &selectIf{ IWidget: inner, iWasSelected: rememberSel, } 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) rememberSel.IWidget = res res = expander.New( isselected.New( rememberSel, styled.New(rememberSel, gowid.MakePaletteRef("packet-struct-selected")), styled.New(rememberSel, gowid.MakePaletteRef("packet-struct-focus")), ), ) return res } // rememberSelected, when rendered, will save whether or not the selected flag was set. // Another widget (deeper in the hierarchy) can then consult it to see whether it should // render differently as the grandchild of a selected widget. type rememberSelected struct { gowid.IWidget selectedThisTime bool } type iWasSelected interface { WasSelected() bool } func (w *rememberSelected) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { w.selectedThisTime = focus.Selected return w.IWidget.Render(size, focus, app) } func (w *rememberSelected) WasSelected() bool { return w.selectedThisTime } // selectIf sets the selected flag on its child if its iWasSelected type returns true type selectIf struct { gowid.IWidget iWasSelected } func (w *selectIf) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { if w.iWasSelected.WasSelected() { focus = focus.SelectIf(true) } return w.IWidget.Render(size, focus, app) } // 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 { pdmlMenuButton := button.NewBare(text.New("[=]")) pdmlMenuButtonSite := menu.NewSite(menu.SiteOptions{YOffset: 1}) pdmlMenuButton.OnClick(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, w gowid.IWidget) { curColumnFilter = tr.(*pdmltree.Model).Name curColumnFilterValue = tr.(*pdmltree.Model).Show curColumnFilterName = tr.(*pdmltree.Model).UiName pdmlFilterMenu := makePdmlFilterMenu(curColumnFilter, curColumnFilterValue) multiMenu1Opener.OpenMenu(pdmlFilterMenu, pdmlMenuButtonSite, app) })) styledButton1 := styled.New(pdmlMenuButton, gowid.MakePaletteRef("packet-struct-selected")) styledButton2 := styled.New( pdmlMenuButton, gowid.MakeStyledAs(gowid.StyleBold), ) structText := text.New(tr.Leaf()) structIfNotSel := columns.NewFixed(structText) structIfSel := columns.NewFixed(structText, colSpace, pdmlMenuButtonSite, styledButton1) structIfFocus := columns.NewFixed(structText, colSpace, pdmlMenuButtonSite, styledButton2) return selectable.New(isselected.New(structIfNotSel, structIfSel, structIfFocus)) } //====================================================================== // I want to have preferred position work on this, but you have to choose a subwidget // to navigate to. We have three. I know that my use of them is very similar, so I'll // just pick the first type selectedComposite struct { *isselected.Widget } var _ gowid.IComposite = (*selectedComposite)(nil) func (w *selectedComposite) SubWidget() gowid.IWidget { return w.Not } //====================================================================== // An ugly interface that captures what sort of type will be suitable // as a table widget to which a row focus can be applied. type iRowFocusTableWidgetNeeds interface { gowid.IWidget list.IBoundedWalker table.IFocus table.IGoToMiddle table.ISetFocus list.IWalkerHome list.IWalkerEnd SetPos(pos list.IBoundedWalkerPosition, app gowid.IApp) FocusXY() (table.Coords, error) SetFocusXY(gowid.IApp, table.Coords) SetModel(table.IModel, gowid.IApp) Lower() *table.ListWithPreferedColumn SetFocusOnData(app gowid.IApp) bool OnFocusChanged(f gowid.IWidgetChangedCallback) } // rowFocusTableWidget provides a table that highlights the selected row or // focused row. type rowFocusTableWidget struct { iRowFocusTableWidgetNeeds rowSelected string rowFocus string } func NewRowFocusTableWidget(w iRowFocusTableWidgetNeeds, rs string, rf string) *rowFocusTableWidget { res := &rowFocusTableWidget{ iRowFocusTableWidgetNeeds: w, rowSelected: rs, rowFocus: rf, } res.Lower().IWidget = list.NewBounded(res) return res } var _ gowid.IWidget = (*rowFocusTableWidget)(nil) func (t *rowFocusTableWidget) SubWidget() gowid.IWidget { return t.iRowFocusTableWidgetNeeds } func (t *rowFocusTableWidget) InvertedModel() table.IInvertible { return t.Model().(table.IInvertible) } func (t *rowFocusTableWidget) Rows() int { return t.Model().(table.IBoundedModel).Rows() } // Implement withscrollbar.IScrollValues func (t *rowFocusTableWidget) ScrollLength() int { return t.Rows() } // Implement withscrollbar.IScrollValues func (t *rowFocusTableWidget) ScrollPosition() int { return t.CurrentRow() } func (t *rowFocusTableWidget) Up(lines int, size gowid.IRenderSize, app gowid.IApp) { for i := 0; i < lines; i++ { t.UserInput(tcell.NewEventKey(tcell.KeyUp, ' ', tcell.ModNone), size, gowid.Focused, app) } } func (t *rowFocusTableWidget) Down(lines int, size gowid.IRenderSize, app gowid.IApp) { for i := 0; i < lines; i++ { t.UserInput(tcell.NewEventKey(tcell.KeyDown, ' ', tcell.ModNone), size, gowid.Focused, app) } } func (t *rowFocusTableWidget) UpPage(num int, size gowid.IRenderSize, app gowid.IApp) { for i := 0; i < num; i++ { t.UserInput(tcell.NewEventKey(tcell.KeyPgUp, ' ', tcell.ModNone), size, gowid.Focused, app) } } func (t *rowFocusTableWidget) DownPage(num int, size gowid.IRenderSize, app gowid.IApp) { for i := 0; i < num; i++ { t.UserInput(tcell.NewEventKey(tcell.KeyPgDn, ' ', tcell.ModNone), size, gowid.Focused, app) } } // list.IWalker func (t *rowFocusTableWidget) At(lpos list.IWalkerPosition) gowid.IWidget { pos := int(lpos.(table.Position)) w := t.AtRow(pos) if w == nil { return nil } // Composite so it passes through preferred column var res gowid.IWidget = &selectedComposite{ Widget: isselected.New(w, styled.New(w, gowid.MakePaletteRef(t.rowSelected)), styled.New(w, gowid.MakePaletteRef(t.rowFocus)), ), } return res } // Needed for WidgetAt above to work - otherwise t.Table.Focus() is called, table is the receiver, // then it calls WidgetAt so ours is not used. func (t *rowFocusTableWidget) Focus() list.IWalkerPosition { return table.Focus(t) } //====================================================================== // A rowFocusTableWidget that adds colors to rows type psmlTableRowWidget struct { *rowFocusTableWidget // set to true after the first time we move focus from the table header to the data. We do this // once and that this happens quickly, but then assume the user might want to move back to the // table header manually, and it would be strange if the table keeps jumping back to the data... didFirstAutoFocus bool colors []pcap.PacketColors } func NewPsmlTableRowWidget(w *rowFocusTableWidget, c []pcap.PacketColors) *psmlTableRowWidget { res := &psmlTableRowWidget{ rowFocusTableWidget: w, colors: c, } res.Lower().IWidget = list.NewBounded(res) return res } func (t *psmlTableRowWidget) At(lpos list.IWalkerPosition) gowid.IWidget { res := t.rowFocusTableWidget.At(lpos) if res == nil { return nil } pos := int(lpos.(table.Position)) // Check the color array length because it might not yet be adequately // populated from the arriving psml. if pos >= 0 && PacketColors && pos < len(t.colors) { res = styled.New(res, gowid.MakePaletteEntry(t.colors[pos].FG, t.colors[pos].BG), ) } return res } func (t *psmlTableRowWidget) Focus() list.IWalkerPosition { return table.Focus(t) } //====================================================================== type pleaseWaitCallbacks struct { w *spinner.Widget app gowid.IApp open bool } func (s *pleaseWaitCallbacks) ProcessWaitTick() error { s.app.Run(gowid.RunFunction(func(app gowid.IApp) { s.w.Update() if !s.open { OpenPleaseWait(appView, s.app) s.open = true } })) return nil } // Call in app context func (s *pleaseWaitCallbacks) closeWaitDialog(app gowid.IApp) { if s.open { ClosePleaseWait(app) s.open = false } } func (s *pleaseWaitCallbacks) ProcessCommandDone() { s.app.Run(gowid.RunFunction(func(app gowid.IApp) { s.closeWaitDialog(app) })) } //====================================================================== // Wait until the copy command has finished, then open up a dialog with the results. type urlCopiedCallbacks struct { app gowid.IApp tmplName string *pleaseWaitCallbacks } var ( _ termshark.ICommandOutput = urlCopiedCallbacks{} _ termshark.ICommandError = urlCopiedCallbacks{} _ termshark.ICommandDone = urlCopiedCallbacks{} _ termshark.ICommandKillError = urlCopiedCallbacks{} _ termshark.ICommandTimeout = urlCopiedCallbacks{} _ termshark.ICommandWaitTicker = urlCopiedCallbacks{} ) func (h urlCopiedCallbacks) displayDialog(output string) { TemplateData["CopyCommandMessage"] = output h.app.Run(gowid.RunFunction(func(app gowid.IApp) { h.closeWaitDialog(app) OpenTemplatedDialog(appView, h.tmplName, app) delete(TemplateData, "CopyCommandMessage") })) } func (h urlCopiedCallbacks) ProcessOutput(output string) error { var msg string if len(output) == 0 { msg = "URL copied to clipboard." } else { msg = output } h.displayDialog(msg) return nil } func (h urlCopiedCallbacks) ProcessCommandTimeout() error { h.displayDialog("") return nil } func (h urlCopiedCallbacks) ProcessCommandError(err error) error { h.displayDialog("") return nil } func (h urlCopiedCallbacks) ProcessKillError(err error) error { h.displayDialog("") return nil } //====================================================================== type userCopiedCallbacks struct { app gowid.IApp copyCmd []string *pleaseWaitCallbacks } var ( _ termshark.ICommandOutput = userCopiedCallbacks{} _ termshark.ICommandError = userCopiedCallbacks{} _ termshark.ICommandDone = userCopiedCallbacks{} _ termshark.ICommandKillError = userCopiedCallbacks{} _ termshark.ICommandTimeout = userCopiedCallbacks{} _ termshark.ICommandWaitTicker = userCopiedCallbacks{} ) func (h userCopiedCallbacks) ProcessCommandTimeout() error { h.app.Run(gowid.RunFunction(func(app gowid.IApp) { h.closeWaitDialog(app) OpenError(fmt.Sprintf("Copy command \"%v\" timed out", strings.Join(h.copyCmd, " ")), app) })) return nil } func (h userCopiedCallbacks) ProcessCommandError(err error) error { h.app.Run(gowid.RunFunction(func(app gowid.IApp) { h.closeWaitDialog(app) OpenError(fmt.Sprintf("Copy command \"%v\" failed: %v", strings.Join(h.copyCmd, " "), err), app) })) return nil } func (h userCopiedCallbacks) ProcessKillError(err error) error { h.app.Run(gowid.RunFunction(func(app gowid.IApp) { h.closeWaitDialog(app) OpenError(fmt.Sprintf("Timed out, but could not kill copy command: %v", err), app) })) return nil } func (h userCopiedCallbacks) ProcessOutput(output string) error { h.app.Run(gowid.RunFunction(func(app gowid.IApp) { h.closeWaitDialog(app) if len(output) == 0 { OpenMessage(" Copied! ", appView, app) } else { OpenMessage(fmt.Sprintf("Copied! Output was:\n%s\n", output), appView, app) } })) return nil } //====================================================================== type OpenErrorDialog struct{} func (f OpenErrorDialog) OnError(err error, app gowid.IApp) { OpenError(err.Error(), app) } func OpenError(msgt string, app gowid.IApp) *dialog.Widget { // the same, for now return OpenMessage(msgt, appView, app) } func OpenLongError(msgt string, app gowid.IApp) *dialog.Widget { // the same, for now return OpenLongMessage(msgt, appView, app) } func openResultsAfterCopy(tmplName string, tocopy string, app gowid.IApp) { v := urlCopiedCallbacks{ app: app, tmplName: tmplName, pleaseWaitCallbacks: &pleaseWaitCallbacks{ w: pleaseWaitSpinner, app: app, }, } termshark.CopyCommand(strings.NewReader(tocopy), v) } func processCopyChoices(copyLen int, app gowid.IApp) { var cc *dialog.Widget copyCmd := profiles.ConfStringSlice( "main.copy-command", system.CopyToClipboard, ) if len(copyCmd) == 0 { OpenError("Config file has an invalid copy-command entry! Please remove it.", app) return } clips := app.Clips() // No need to display a choice dialog with one choice - just copy right away if len(clips) == 1 { app.InCopyMode(false) termshark.CopyCommand(strings.NewReader(clips[0].ClipValue()), userCopiedCallbacks{ app: app, copyCmd: copyCmd, pleaseWaitCallbacks: &pleaseWaitCallbacks{ w: pleaseWaitSpinner, app: app, }, }) return } cws := make([]gowid.IWidget, 0, len(clips)) for _, clip := range clips { c2 := clip lbl := text.New(clip.ClipName() + ":") btxt1 := clip.ClipValue() if copyLen > 0 { blines := strings.Split(btxt1, "\n") if len(blines) > copyLen { blines[copyLen-1] = "..." blines = blines[0:copyLen] } btxt1 = strings.Join(blines, "\n") } btn := button.NewBare(text.New(btxt1, text.Options{ Wrap: text.WrapClip, ClipIndicator: "...", })) btn.OnClick(gowid.MakeWidgetCallback("cb", gowid.WidgetChangedFunction(func(app gowid.IApp, w gowid.IWidget) { cc.Close(app) app.InCopyMode(false) termshark.CopyCommand(strings.NewReader(c2.ClipValue()), userCopiedCallbacks{ app: app, copyCmd: copyCmd, pleaseWaitCallbacks: &pleaseWaitCallbacks{ w: pleaseWaitSpinner, app: app, }, }) }))) btn2 := styled.NewFocus(btn, gowid.MakeStyledAs(gowid.StyleReverse)) tog := pile.NewFlow(lbl, btn2, divider.NewUnicode()) cws = append(cws, tog) } walker := list.NewSimpleListWalker(cws) clipList := list.New(walker) // Do this so the list box scrolls inside the dialog view2 := &gowid.ContainerWidget{ IWidget: clipList, D: weight(1), } var view1 gowid.IWidget = pile.NewFlow(text.New("Select option to copy:"), divider.NewUnicode(), view2) cc = dialog.New(view1, dialog.Options{ Buttons: dialog.CloseOnly, NoShadow: true, BackgroundStyle: gowid.MakePaletteRef("dialog"), BorderStyle: gowid.MakePaletteRef("dialog"), ButtonStyle: gowid.MakePaletteRef("dialog-button"), FocusOnWidget: true, }, ) cc.OnOpenClose(gowid.MakeWidgetCallback("cb", gowid.WidgetChangedFunction(func(app gowid.IApp, w gowid.IWidget) { if !cc.IsOpen() { app.InCopyMode(false) } }))) dialog.OpenExt(cc, appView, ratio(0.5), ratio(0.8), app) } type callWithAppFn func(gowid.IApp) func askToSave(app gowid.IApp, next callWithAppFn) { msgt := fmt.Sprintf("Current capture saved to %s", Loader.InterfaceFile()) msg := text.New(msgt) var keepPackets *dialog.Widget keepPackets = dialog.New( framed.NewSpace(hpadding.New(msg, hmiddle, fixed)), dialog.Options{ Buttons: []dialog.Button{ dialog.Button{ Msg: "Keep", Action: gowid.MakeWidgetCallback("cb", func(app gowid.IApp, widget gowid.IWidget) { keepPackets.Close(app) next(app) }, ), }, dialog.Button{ Msg: "Delete", Action: gowid.MakeWidgetCallback("cb", func(app gowid.IApp, widget gowid.IWidget) { WriteToDeleted = true err := os.Remove(Loader.InterfaceFile()) if err != nil { log.Errorf("Could not delete file %s: %v", Loader.InterfaceFile(), err) } keepPackets.Close(app) next(app) }, ), }, dialog.Cancel, }, NoShadow: true, BackgroundStyle: gowid.MakePaletteRef("dialog"), BorderStyle: gowid.MakePaletteRef("dialog"), ButtonStyle: gowid.MakePaletteRef("dialog-button"), }, ) keepPackets.Open(appView, units(len(msgt)+20), app) } func reallyQuit(app gowid.IApp) { msgt := "Do you want to quit?" msg := text.New(msgt) YesNo = dialog.New( framed.NewSpace(hpadding.New(msg, hmiddle, fixed)), dialog.Options{ Buttons: []dialog.Button{ dialog.Button{ Msg: "Ok", Action: gowid.MakeWidgetCallback("cb", func(app gowid.IApp, widget gowid.IWidget) { YesNo.Close(app) // (a) Loader is in interface mode (b) User did not set -w flag // (c) always-keep-pcap setting is unset (def false) or false if Loader.InterfaceFile() != "" && !WriteToSelected && !profiles.ConfBool("main.always-keep-pcap", false) { askToSave(app, func(app gowid.IApp) { RequestQuit() }) } else { RequestQuit() } }, ), }, dialog.Cancel, }, NoShadow: true, BackgroundStyle: gowid.MakePaletteRef("dialog"), BorderStyle: gowid.MakePaletteRef("dialog"), ButtonStyle: gowid.MakePaletteRef("dialog-button"), Modal: true, }, ) YesNo.Open(appView, units(len(msgt)+20), app) } func lastLineMode(app gowid.IApp) { MiniBuffer = minibuffer.New() MiniBuffer.Register("quit", minibufferFn(func(gowid.IApp, ...string) error { reallyQuit(app) return nil })) // force quit MiniBuffer.Register("q!", quietMinibufferFn(func(gowid.IApp, ...string) error { RequestQuit() return nil })) MiniBuffer.Register("no-theme", minibufferFn(func(app gowid.IApp, s ...string) error { mode := theme.Mode(app.GetColorMode()).String() // more concise profiles.DeleteConf(fmt.Sprintf("main.theme-%s", mode)) ApplyCurrentTheme(app) SetupColors() var prof string if profiles.Current() != profiles.Default() { prof = fmt.Sprintf("in profile %s ", profiles.CurrentName()) } OpenMessage(fmt.Sprintf("Cleared theme %sfor terminal mode %v.", prof, app.GetColorMode()), appView, app) return nil })) MiniBuffer.Register("convs", minibufferFn(func(gowid.IApp, ...string) error { openConvsUi(app) return nil })) MiniBuffer.Register("streams", minibufferFn(func(gowid.IApp, ...string) error { startStreamReassembly(app) return nil })) MiniBuffer.Register("capinfo", minibufferFn(func(gowid.IApp, ...string) error { startCapinfo(app) return nil })) MiniBuffer.Register("columns", minibufferFn(func(gowid.IApp, ...string) error { openEditColumns(app) return nil })) MiniBuffer.Register("wormhole", minibufferFn(func(gowid.IApp, ...string) error { openWormhole(app) return nil })) MiniBuffer.Register("menu", minibufferFn(func(gowid.IApp, ...string) error { openGeneralMenu(app) return nil })) MiniBuffer.Register("clear-packets", minibufferFn(func(gowid.IApp, ...string) error { reallyClear(app) return nil })) MiniBuffer.Register("clear-filter", minibufferFn(func(gowid.IApp, ...string) error { FilterWidget.SetValue("", app) RequestNewFilter(FilterWidget.Value(), app) return nil })) MiniBuffer.Register("marks", minibufferFn(func(gowid.IApp, ...string) error { OpenTemplatedDialogExt(appView, "Marks", fixed, ratio(0.6), app) return nil })) if runtime.GOOS != "windows" { MiniBuffer.Register("logs", minibufferFn(func(gowid.IApp, ...string) error { openLogsUi(app) return nil })) MiniBuffer.Register("config", minibufferFn(func(gowid.IApp, ...string) error { openConfigUi(app) return nil })) } MiniBuffer.Register("set", setCommand{}) // read new pcap MiniBuffer.Register("r", readCommand{complete: false}) MiniBuffer.Register("e", readCommand{complete: false}) MiniBuffer.Register("load", readCommand{complete: true}) MiniBuffer.Register("recents", recentsCommand{}) MiniBuffer.Register("filter", filterCommand{}) MiniBuffer.Register("theme", themeCommand{}) MiniBuffer.Register("profile", newProfileCommand()) MiniBuffer.Register("map", mapCommand{w: keyMapper}) MiniBuffer.Register("unmap", unmapCommand{w: keyMapper}) MiniBuffer.Register("help", helpCommand{}) minibuffer.Open(MiniBuffer, mbView, ratio(1.0), app) } //====================================================================== func getCurrentStructModel(row int) *pdmltree.Model { return getCurrentStructModelWith(row, Loader.PsmlLoader) } // getCurrentStructModelWith 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 getCurrentStructModelWith(row int, lock sync.Locker) *pdmltree.Model { var res *pdmltree.Model pktsPerLoad := Loader.PacketsPerLoad() row2 := (row / pktsPerLoad) * pktsPerLoad lock.Lock() defer lock.Unlock() if ws, ok := Loader.PacketCache.Get(row2); ok { srca := ws.(pcap.CacheEntry).Pdml if len(srca) > row%pktsPerLoad { data, err := xml.Marshal(srca[row%pktsPerLoad].Packet()) if err != nil { log.Fatal(err) } res = pdmltree.DecodePacket(data) } } return res } //====================================================================== func reallyClear(app gowid.IApp) { confirmAction( "Do you want to clear current capture?", func(app gowid.IApp) { Loader.ClearPcap( pcap.HandlerList{ SimpleErrors{}, MakePacketViewUpdater(), MakeUpdateCurrentCaptureInTitle(), ManageStreamCache{}, ManageCapinfoCache{}, SetStructWidgets{Loader}, // for OnClear ClearMarksHandler{}, ManageSearchData{}, CancelledMessage{}, }, ) }, app, ) } func confirmAction(msgt string, ok func(gowid.IApp), app gowid.IApp) { msg := text.New(msgt) YesNo = dialog.New( framed.NewSpace(hpadding.New(msg, hmiddle, fixed)), dialog.Options{ Buttons: []dialog.Button{ dialog.Button{ Msg: "Ok", Action: gowid.MakeWidgetCallback("cb", func(app gowid.IApp, w gowid.IWidget) { YesNo.Close(app) ok(app) }, ), }, dialog.Cancel, }, NoShadow: true, BackgroundStyle: gowid.MakePaletteRef("dialog"), BorderStyle: gowid.MakePaletteRef("dialog"), ButtonStyle: gowid.MakePaletteRef("dialog-button"), }, ) YesNo.Open(mainViewNoKeys, units(len(msgt)+28), app) } //====================================================================== func appKeysResize1(evk *tcell.EventKey, app gowid.IApp) bool { handled := true if evk.Rune() == '+' { mainviewRows.AdjustOffset(2, 6, resizable.Add1, app) } else if evk.Rune() == '-' { mainviewRows.AdjustOffset(2, 6, resizable.Subtract1, app) } else { handled = false } return handled } func appKeysResize2(evk *tcell.EventKey, app gowid.IApp) bool { handled := true if evk.Rune() == '+' { mainviewRows.AdjustOffset(4, 6, resizable.Add1, app) } else if evk.Rune() == '-' { mainviewRows.AdjustOffset(4, 6, resizable.Subtract1, app) } else { handled = false } return handled } func altview1ColsKeyPress(evk *tcell.EventKey, app gowid.IApp) bool { handled := true if evk.Rune() == '>' { altview1Cols.AdjustOffset(0, 2, resizable.Add1, app) } else if evk.Rune() == '<' { altview1Cols.AdjustOffset(0, 2, resizable.Subtract1, app) } else { handled = false } return handled } func altview1PileKeyPress(evk *tcell.EventKey, app gowid.IApp) bool { handled := true if evk.Rune() == '+' { altview1Pile.AdjustOffset(0, 2, resizable.Add1, app) } else if evk.Rune() == '-' { altview1Pile.AdjustOffset(0, 2, resizable.Subtract1, app) } else { handled = false } return handled } func altview2ColsKeyPress(evk *tcell.EventKey, app gowid.IApp) bool { handled := true if evk.Rune() == '>' { altview2Cols.AdjustOffset(0, 2, resizable.Add1, app) } else if evk.Rune() == '<' { altview2Cols.AdjustOffset(0, 2, resizable.Subtract1, app) } else { handled = false } return handled } func altview2PileKeyPress(evk *tcell.EventKey, app gowid.IApp) bool { handled := true if evk.Rune() == '+' { altview2Pile.AdjustOffset(0, 2, resizable.Add1, app) } else if evk.Rune() == '-' { altview2Pile.AdjustOffset(0, 2, resizable.Subtract1, app) } else { handled = false } return handled } func copyModeExitKeys(evk *tcell.EventKey, app gowid.IApp) bool { return copyModeExitKeysClipped(evk, 0, app) } // Used for limiting samples of reassembled streams func copyModeExitKeys20(evk *tcell.EventKey, app gowid.IApp) bool { return copyModeExitKeysClipped(evk, 20, app) } func copyModeExitKeysClipped(evk *tcell.EventKey, copyLen int, app gowid.IApp) bool { handled := false if app.InCopyMode() { handled = true switch evk.Key() { case tcell.KeyRune: switch evk.Rune() { case 'q', 'c': app.InCopyMode(false) case '?': OpenTemplatedDialog(appView, "CopyModeHelp", app) } case tcell.KeyEscape: app.InCopyMode(false) case tcell.KeyCtrlC: processCopyChoices(copyLen, app) case tcell.KeyRight: cl := app.CopyModeClaimedAt() app.CopyModeClaimedAt(cl + 1) app.RefreshCopyMode() case tcell.KeyLeft: cl := app.CopyModeClaimedAt() if cl > 0 { app.CopyModeClaimedAt(cl - 1) app.RefreshCopyMode() } } } return handled } func copyModeEnterKeys(evk *tcell.EventKey, app gowid.IApp) bool { handled := false if !app.InCopyMode() { switch evk.Key() { case tcell.KeyRune: switch evk.Rune() { case 'c': app.InCopyMode(true) handled = true } } } return handled } func setFocusOnPacketList(app gowid.IApp) { gowid.SetFocusPath(mainview, mainviewPaths[0], app) gowid.SetFocusPath(altview1, altview1Paths[0], app) gowid.SetFocusPath(altview2, altview2Paths[0], app) gowid.SetFocusPath(viewOnlyPacketList, maxViewPath, app) } func setFocusOnPacketStruct(app gowid.IApp) { gowid.SetFocusPath(mainview, mainviewPaths[1], app) gowid.SetFocusPath(altview1, altview1Paths[1], app) gowid.SetFocusPath(altview2, altview2Paths[1], app) gowid.SetFocusPath(viewOnlyPacketStructure, maxViewPath, app) } func setFocusOnPacketHex(app gowid.IApp) { gowid.SetFocusPath(mainview, mainviewPaths[2], app) gowid.SetFocusPath(altview1, altview1Paths[2], app) gowid.SetFocusPath(altview2, altview2Paths[2], app) gowid.SetFocusPath(viewOnlyPacketHex, maxViewPath, app) } func setFocusOnDisplayFilter(app gowid.IApp) { gowid.SetFocusPath(mainview, filterPathMain, app) gowid.SetFocusPath(altview1, filterPathAlt, app) gowid.SetFocusPath(altview2, filterPathAlt, app) gowid.SetFocusPath(viewOnlyPacketList, filterPathMax, app) gowid.SetFocusPath(viewOnlyPacketStructure, filterPathMax, app) gowid.SetFocusPath(viewOnlyPacketHex, filterPathMax, app) } func setFocusOnSearch(app gowid.IApp) { gowid.SetFocusPath(mainview, searchPathMain, app) gowid.SetFocusPath(altview1, searchPathAlt, app) gowid.SetFocusPath(altview2, searchPathAlt, app) gowid.SetFocusPath(viewOnlyPacketList, searchPathMax, app) gowid.SetFocusPath(viewOnlyPacketStructure, searchPathMax, app) gowid.SetFocusPath(viewOnlyPacketHex, searchPathMax, app) } func clearOffsets(app gowid.IApp) { if mainViewNoKeys.SubWidget() == mainview { mainviewRows.SetOffsets([]resizable.Offset{}, app) } else if mainViewNoKeys.SubWidget() == altview1 { altview1Cols.SetOffsets([]resizable.Offset{}, app) altview1Pile.SetOffsets([]resizable.Offset{}, app) } else { altview2Cols.SetOffsets([]resizable.Offset{}, app) altview2Pile.SetOffsets([]resizable.Offset{}, app) } } func packetNumberFromCurrentTableRow() (termshark.JumpPos, error) { tablePos, err := packetListView.FocusXY() // e.g. table position 5 if err != nil { return termshark.JumpPos{}, fmt.Errorf("No packet in focus: %v", err) } return packetNumberFromTableRow(tablePos.Row) } func tableRowFromPacketNumber(savedPacket int) (int, error) { // Map e.g. packet number #123 to the index in the PSML array - e.g. index 10 (order of psml load) packetRowId, ok := Loader.PacketNumberMap[savedPacket] if !ok { return -1, fmt.Errorf("Error finding packet %v", savedPacket) } // This psml order is also the table RowId order. The table might be sorted though, so // map this RowId to the actual table row, so we can change focus to it tableRow, ok := packetListView.InvertedModel().IdentifierToRow(table.RowId(packetRowId)) if !ok { return -1, fmt.Errorf("Error looking up packet %v", packetRowId) } return tableRow, nil } func packetNumberFromTableRow(tableRow int) (termshark.JumpPos, error) { packetRowId, ok := packetListView.Model().RowIdentifier(tableRow) if !ok { return termshark.JumpPos{}, fmt.Errorf("Error looking up packet at row %v", tableRow) } // e.g. packet #123 var summary string if len(Loader.PsmlData()) > int(packetRowId) { summary = psmlSummary(Loader.PsmlData()[packetRowId]).String() } if int(packetRowId) >= len(Loader.PsmlData()) { return termshark.JumpPos{}, fmt.Errorf("Packet %d is not loaded.", packetRowId) } packetNum, err := strconv.Atoi(Loader.PsmlData()[packetRowId][0]) if err != nil { return termshark.JumpPos{}, fmt.Errorf("Unexpected error determining no. of packet %d: %v.", tableRow, err) } return termshark.JumpPos{ Pos: packetNum, Summary: summary, }, nil } func searchOpen() bool { return filterHolder.SubWidget() == filterWithSearch } func searchIsActive() bool { return stopCurrentSearch != nil } func searchPacketsMainView(evk *tcell.EventKey, app gowid.IApp) bool { handled := true if evk.Key() == tcell.KeyCtrlF { if !searchOpen() { filterHolder.SetSubWidget(filterWithSearch, app) setFocusOnSearch(app) } else { // If it's open and focus is on the text area for search, then close it if SearchWidget != nil && SearchWidget.FocusIsOnFilter() { filterHolder.SetSubWidget(filterWithoutSearch, app) // This seems to make the most sense setFocusOnPacketList(app) } else { // Otherwise, put focus on the text area. This provides for a quick // way to get control back on the search text area - ctrl-f will do it. // Closing search is then just two ctrl-f keypresses at most. setFocusOnSearch(app) } } } else { handled = false } return handled } // These only apply to the traditional wireshark-like main view func vimKeysMainView(evk *tcell.EventKey, app gowid.IApp) bool { handled := true if evk.Key() == tcell.KeyCtrlW && keyState.PartialCtrlWCmd { cycleView(app, true, tabViewsForward) } else if evk.Key() == tcell.KeyRune && evk.Rune() == '=' && keyState.PartialCtrlWCmd { clearOffsets(app) } else if evk.Key() == tcell.KeyRune && evk.Rune() >= 'a' && evk.Rune() <= 'z' && keyState.PartialmCmd { if packetListView != nil { tablePos, err := packetListView.FocusXY() // e.g. table position 5 if err != nil { OpenError(fmt.Sprintf("No packet in focus: %v", err), app) } else { jpos, err := packetNumberFromTableRow(tablePos.Row) if err != nil { OpenError(err.Error(), app) } else { marksMap[evk.Rune()] = jpos OpenMessage(fmt.Sprintf("Local mark '%c' set to packet %v.", evk.Rune(), jpos.Pos), appView, app) } } } } else if evk.Key() == tcell.KeyRune && evk.Rune() >= 'A' && evk.Rune() <= 'Z' && keyState.PartialmCmd { if Loader != nil { if Loader.Pcap() != "" { if packetListView != nil { tablePos, err := packetListView.FocusXY() if err != nil { OpenError(fmt.Sprintf("No packet in focus: %v", err), app) } else { jpos, err := packetNumberFromTableRow(tablePos.Row) if err != nil { OpenError(err.Error(), app) } else { globalMarksMap[evk.Rune()] = termshark.GlobalJumpPos{ JumpPos: jpos, Filename: Loader.Pcap(), } termshark.SaveGlobalMarks(globalMarksMap) OpenMessage(fmt.Sprintf("Global mark '%c' set to packet %v.", evk.Rune(), jpos.Pos), appView, app) } } } } } } else if evk.Key() == tcell.KeyRune && evk.Rune() >= 'a' && evk.Rune() <= 'z' && keyState.PartialQuoteCmd { if packetListView != nil { markedPacket, ok := marksMap[evk.Rune()] if ok { tableRow, err := tableRowFromPacketNumber(markedPacket.Pos) if err != nil { OpenError(err.Error(), app) } else { tableCol := 0 curTablePos, err := packetListView.FocusXY() if err == nil { tableCol = curTablePos.Column } pn, _ := packetNumberFromCurrentTableRow() // save for '' lastJumpPos = pn.Pos packetListView.SetFocusXY(app, table.Coords{Column: tableCol, Row: tableRow}) } } } } else if evk.Key() == tcell.KeyRune && evk.Rune() >= 'A' && evk.Rune() <= 'Z' && keyState.PartialQuoteCmd { markedPacket, ok := globalMarksMap[evk.Rune()] if !ok { OpenError("Mark not found.", app) } else { if Loader.Pcap() != markedPacket.Filename { MaybeKeepThenRequestLoadPcap(markedPacket.Filename, FilterWidget.Value(), markedPacket, app) } else { if packetListView != nil { tableRow, err := tableRowFromPacketNumber(markedPacket.Pos) if err != nil { OpenError(err.Error(), app) } else { tableCol := 0 curTablePos, err := packetListView.FocusXY() if err == nil { tableCol = curTablePos.Column } pn, _ := packetNumberFromCurrentTableRow() // save for '' lastJumpPos = pn.Pos packetListView.SetFocusXY(app, table.Coords{Column: tableCol, Row: tableRow}) } } } } } else if evk.Key() == tcell.KeyRune && evk.Rune() == '\'' && keyState.PartialQuoteCmd { if packetListView != nil { tablePos, err := packetListView.FocusXY() if err != nil { OpenError(fmt.Sprintf("No packet in focus: %v", err), app) } else { // which packet number was saved as a mark savedPacket := lastJumpPos if savedPacket != -1 { // Map that packet number #123 to the index in the PSML array - e.g. index 10 (order of psml load) if packetRowId, ok := Loader.PacketNumberMap[savedPacket]; !ok { OpenError(fmt.Sprintf("Error finding packet %v", savedPacket), app) } else { // This psml order is also the table RowId order. The table might be sorted though, so // map this RowId to the actual table row, so we can change focus to it if tableRow, ok := packetListView.InvertedModel().IdentifierToRow(table.RowId(packetRowId)); !ok { OpenError(fmt.Sprintf("Error looking up packet %v", packetRowId), app) } else { pn, _ := packetNumberFromCurrentTableRow() // save for '' lastJumpPos = pn.Pos packetListView.SetFocusXY(app, table.Coords{Column: tablePos.Column, Row: tableRow}) } } } } } } else { handled = false } return handled } func currentlyFocusedViewNotHex() bool { return currentlyFocusedViewNotByIndex(2) } func currentlyFocusedViewNotStruct() bool { return currentlyFocusedViewNotByIndex(1) } func currentlyFocusedViewNotByIndex(idx int) bool { if mainViewNoKeys.SubWidget() == mainview { v1p := gowid.FocusPath(mainview) if deep.Equal(v1p, mainviewPaths[idx]) != nil { // it's not hex return true } } else if mainViewNoKeys.SubWidget() == altview1 { v2p := gowid.FocusPath(altview1) if deep.Equal(v2p, altview1Paths[idx]) != nil { // it's not hex return true } } else { // altview2 v3p := gowid.FocusPath(altview2) if deep.Equal(v3p, altview2Paths[idx]) != nil { // it's not hex return true } } return false } // Move focus among the packet list view, structure view and hex view func cycleView(app gowid.IApp, forward bool, tabMap map[gowid.IWidget]gowid.IWidget) { if v, ok := tabMap[mainViewNoKeys.SubWidget()]; ok { mainViewNoKeys.SetSubWidget(v, app) } gowid.SetFocusPath(viewOnlyPacketList, maxViewPath, app) gowid.SetFocusPath(viewOnlyPacketStructure, maxViewPath, app) gowid.SetFocusPath(viewOnlyPacketHex, maxViewPath, app) if packetStructureViewHolder.SubWidget() == MissingMsgw { setFocusOnPacketList(app) } else { newidx := -1 if mainViewNoKeys.SubWidget() == mainview { v1p := gowid.FocusPath(mainview) if deep.Equal(v1p, mainviewPaths[0]) == nil { newidx = gwutil.If(forward, 1, 2).(int) } else if deep.Equal(v1p, mainviewPaths[1]) == nil { newidx = gwutil.If(forward, 2, 0).(int) } else { newidx = gwutil.If(forward, 0, 1).(int) } } else if mainViewNoKeys.SubWidget() == altview1 { v2p := gowid.FocusPath(altview1) if deep.Equal(v2p, altview1Paths[0]) == nil { newidx = gwutil.If(forward, 1, 2).(int) } else if deep.Equal(v2p, altview1Paths[1]) == nil { newidx = gwutil.If(forward, 2, 0).(int) } else { newidx = gwutil.If(forward, 0, 1).(int) } } else if mainViewNoKeys.SubWidget() == altview2 { v3p := gowid.FocusPath(altview2) if deep.Equal(v3p, altview2Paths[0]) == nil { newidx = gwutil.If(forward, 1, 2).(int) } else if deep.Equal(v3p, altview2Paths[1]) == nil { newidx = gwutil.If(forward, 2, 0).(int) } else { newidx = gwutil.If(forward, 0, 1).(int) } } if newidx != -1 { // Keep the views in sync gowid.SetFocusPath(mainview, mainviewPaths[newidx], app) gowid.SetFocusPath(altview1, altview1Paths[newidx], app) gowid.SetFocusPath(altview2, altview2Paths[newidx], app) } } } // Keys for the main view - packet list, structure, etc func mainKeyPress(evk *tcell.EventKey, app gowid.IApp) bool { handled := true isrune := evk.Key() == tcell.KeyRune if evk.Key() == tcell.KeyCtrlC && searchIsActive() { stopCurrentSearch.RequestStop(app) } else if evk.Key() == tcell.KeyCtrlC && Loader.PsmlLoader.IsLoading() { Loader.StopLoadPsmlAndIface(NoHandlers{}) // iface and psml } else if evk.Key() == tcell.KeyTAB || evk.Key() == tcell.KeyBacktab { isTab := (evk.Key() == tcell.KeyTab) var tabMap map[gowid.IWidget]gowid.IWidget if isTab { tabMap = tabViewsForward } else { tabMap = tabViewsBackward } cycleView(app, isTab, tabMap) } else if isrune && evk.Rune() == '|' { if mainViewNoKeys.SubWidget() == mainview { mainViewNoKeys.SetSubWidget(altview1, app) profiles.SetConf("main.layout", "altview1") } else if mainViewNoKeys.SubWidget() == altview1 { mainViewNoKeys.SetSubWidget(altview2, app) profiles.SetConf("main.layout", "altview2") } else { mainViewNoKeys.SetSubWidget(mainview, app) profiles.SetConf("main.layout", "mainview") } } else if isrune && evk.Rune() == '\\' { w := mainViewNoKeys.SubWidget() fp := gowid.FocusPath(w) if w == viewOnlyPacketList || w == viewOnlyPacketStructure || w == viewOnlyPacketHex { switch profiles.ConfString("main.layout", "mainview") { case "altview1": mainViewNoKeys.SetSubWidget(altview1, app) case "altview2": mainViewNoKeys.SetSubWidget(altview2, app) default: mainViewNoKeys.SetSubWidget(mainview, app) } if deep.Equal(fp, maxViewPath) == nil { switch w { case viewOnlyPacketList: setFocusOnPacketList(app) case viewOnlyPacketStructure: setFocusOnPacketStruct(app) case viewOnlyPacketHex: setFocusOnPacketList(app) } } } else { gotov := 0 if mainViewNoKeys.SubWidget() == mainview { v1p := gowid.FocusPath(mainview) if deep.Equal(v1p, mainviewPaths[0]) == nil { gotov = 0 } else if deep.Equal(v1p, mainviewPaths[1]) == nil { gotov = 1 } else { gotov = 2 } } else if mainViewNoKeys.SubWidget() == altview1 { v2p := gowid.FocusPath(altview1) if deep.Equal(v2p, altview1Paths[0]) == nil { gotov = 0 } else if deep.Equal(v2p, altview1Paths[1]) == nil { gotov = 1 } else { gotov = 2 } } else if mainViewNoKeys.SubWidget() == altview2 { v3p := gowid.FocusPath(altview2) if deep.Equal(v3p, altview2Paths[0]) == nil { gotov = 0 } else if deep.Equal(v3p, altview2Paths[1]) == nil { gotov = 1 } else { gotov = 2 } } switch gotov { case 0: mainViewNoKeys.SetSubWidget(viewOnlyPacketList, app) if deep.Equal(fp, maxViewPath) == nil { gowid.SetFocusPath(viewOnlyPacketList, maxViewPath, app) } case 1: mainViewNoKeys.SetSubWidget(viewOnlyPacketStructure, app) if deep.Equal(fp, maxViewPath) == nil { gowid.SetFocusPath(viewOnlyPacketStructure, maxViewPath, app) } case 2: mainViewNoKeys.SetSubWidget(viewOnlyPacketHex, app) if deep.Equal(fp, maxViewPath) == nil { gowid.SetFocusPath(viewOnlyPacketHex, maxViewPath, app) } } } } else if isrune && evk.Rune() == '/' { setFocusOnDisplayFilter(app) } else { handled = false } return handled } func focusOnMenuButton(app gowid.IApp) { gowid.SetFocusPath(mainview, menuPathMain, app) gowid.SetFocusPath(altview1, menuPathAlt, app) gowid.SetFocusPath(altview2, menuPathAlt, app) gowid.SetFocusPath(viewOnlyPacketList, menuPathMax, app) gowid.SetFocusPath(viewOnlyPacketStructure, menuPathMax, app) gowid.SetFocusPath(viewOnlyPacketHex, menuPathMax, app) } func openGeneralMenu(app gowid.IApp) { focusOnMenuButton(app) multiMenu1Opener.OpenMenu(generalMenu, openMenuSite, app) } // Keys for the whole app, applicable whichever view is frontmost func appKeyPress(evk *tcell.EventKey, app gowid.IApp) bool { handled := true isrune := evk.Key() == tcell.KeyRune if evk.Key() == tcell.KeyCtrlC { reallyQuit(app) } else if evk.Key() == tcell.KeyCtrlL { app.Sync() } else if isrune && (evk.Rune() == 'q' || evk.Rune() == 'Q') { reallyQuit(app) } else if isrune && evk.Rune() == ':' { lastLineMode(app) } else if evk.Key() == tcell.KeyEscape { focusOnMenuButton(app) } else if isrune && evk.Rune() == '?' { OpenTemplatedDialog(appView, "UIHelp", app) } else if isrune && evk.Rune() == 'Z' && keyState.PartialZCmd { RequestQuit() } else if isrune && evk.Rune() == 'Z' { keyState.PartialZCmd = true } else if isrune && evk.Rune() == 'm' { keyState.PartialmCmd = true } else if isrune && evk.Rune() == '\'' { keyState.PartialQuoteCmd = true } else if isrune && evk.Rune() == 'g' { keyState.PartialgCmd = true } else if evk.Key() == tcell.KeyCtrlW { keyState.PartialCtrlWCmd = true } else if isrune && evk.Rune() >= '0' && evk.Rune() <= '9' { if keyState.NumberPrefix == -1 { keyState.NumberPrefix = int(evk.Rune() - '0') } else { keyState.NumberPrefix = (10 * keyState.NumberPrefix) + (int(evk.Rune() - '0')) } } else { handled = false } return handled } type LoadResult struct { packetTree []*pdmltree.Model headers []string packetList [][]string } func IsProgressIndeterminate() bool { return progressHolder.SubWidget() == loadSpinner } func SetProgressDeterminateFor(app gowid.IApp, owner WidgetOwner) { if progressOwner == 0 || progressOwner == owner { progressOwner = owner progressHolder.SetSubWidget(loadProgress, app) } } func SetProgressIndeterminateFor(app gowid.IApp, owner WidgetOwner) { if progressOwner == 0 || progressOwner == owner { progressOwner = owner progressHolder.SetSubWidget(loadSpinner, app) } } func ClearProgressWidgetFor(app gowid.IApp, owner WidgetOwner) { if progressOwner != owner { return } ds := filterCols.Dimensions() sw := filterCols.SubWidgets() sw[progWidgetIdx] = nullw ds[progWidgetIdx] = fixed filterCols.SetSubWidgets(sw, app) filterCols.SetDimensions(ds, app) progressOwner = NoOwner } func createLoaderProgressWidget() (*button.Widget, *columns.Widget) { btn, cols := createProgressWidget() btn.OnClick(gowid.MakeWidgetCallback("loaderstop", func(app gowid.IApp, w gowid.IWidget) { Loader.StopLoadPsmlAndIface(NoHandlers{}) // psml and iface })) return btn, cols } func createProgressWidget() (*button.Widget, *columns.Widget) { stop := button.New(text.New("Stop")) stop2 := styled.NewExt(stop, gowid.MakePaletteRef("button"), gowid.MakePaletteRef("button-focus")) 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, }, }) return stop, prog2 } func SetProgressWidget(app gowid.IApp) { SetProgressWidgetCustom(app, loadProg, LoaderOwns) } func SetSearchProgressWidget(app gowid.IApp) { SetProgressWidgetCustom(app, searchProg, SearchOwns) } func SetProgressWidgetCustom(app gowid.IApp, c *columns.Widget, owner WidgetOwner) { if progressOwner != owner && progressOwner != 0 { return } ds := filterCols.Dimensions() sw := filterCols.SubWidgets() sw[progWidgetIdx] = c ds[progWidgetIdx] = weight(33) filterCols.SetSubWidgets(sw, app) filterCols.SetDimensions(ds, app) } // setLowerWidgets will set the packet structure and packet hex views, if there // is suitable data to display. If not, they are left as-is. func setLowerWidgets(app gowid.IApp) { var sw1 gowid.IWidget var sw2 gowid.IWidget if packetListView != nil { if fxy, err := packetListView.FocusXY(); err == nil { row2, _ := packetListView.Model().RowIdentifier(fxy.Row) row := int(row2) hex := getHexWidgetToDisplay(row) if hex != nil { sw1 = enableselected.New( withscrollbar.New( hex, withscrollbar.Options{ HideIfContentFits: true, }, ), ) } str := getStructWidgetToDisplay(row, app) if str != nil { sw2 = enableselected.New(str) } } } if sw1 != nil { packetHexViewHolder.SetSubWidget(sw1, app) StopEmptyHexViewTimer() } else { // If autoscroll is on, it's annoying to see the constant loading message, so // suppress and just remain on the last displayed hex timer := false if AutoScroll { // Only displaying loading if the current panel is blank. If it's data, leave the data if packetHexViewHolder.SubWidget() == nullw { timer = true } } else { if packetHexViewHolder.SubWidget() != MissingMsgw { timer = true } } if timer { if EmptyHexViewTimer == nil { EmptyHexViewTimer = time.AfterFunc(time.Duration(1000)*time.Millisecond, func() { app.Run(gowid.RunFunction(func(app gowid.IApp) { singlePacketViewMsgHolder.SetSubWidget(Loadingw, app) packetHexViewHolder.SetSubWidget(MissingMsgw, app) })) }) } } } if sw2 != nil { packetStructureViewHolder.SetSubWidget(sw2, app) StopEmptyStructViewTimer() } else { timer := false if AutoScroll { if packetStructureViewHolder.SubWidget() == nullw { timer = true } } else { if packetStructureViewHolder.SubWidget() != MissingMsgw { timer = true } } // If autoscroll is on, it's annoying to see the constant loading message, so // suppress and just remain on the last displayed hex if timer { if EmptyStructViewTimer == nil { EmptyStructViewTimer = time.AfterFunc(time.Duration(1000)*time.Millisecond, func() { app.Run(gowid.RunFunction(func(app gowid.IApp) { singlePacketViewMsgHolder.SetSubWidget(Loadingw, app) packetStructureViewHolder.SetSubWidget(MissingMsgw, app) })) }) } } } } func makePacketListModel(psml iPsmlInfo, app gowid.IApp) *psmlmodel.Model { headers := psml.PsmlHeaders() avgs := psml.PsmlAverageLengths() maxs := psml.PsmlMaxLengths() widths := make([]gowid.IWidgetDimension, 0, len(avgs)) for i := 0; i < len(avgs); i++ { titleLen := 0 if i < len(headers) { titleLen = len(headers[i]) + 1 // add 1 because the table clears the last cell } max := gwutil.Max(maxs[i], titleLen) // in case there isn't any data yet avg := titleLen if !avgs[i].IsNone() { avg = gwutil.Max(avgs[i].Val(), titleLen) } // This makes the UI look nicer - an extra column of space when the columns are // packed tightly and each column is usually full. if avg == max { widths = append(widths, weightupto(avg, max+1)) } else { widths = append(widths, weightupto(avg, max)) } } packetPsmlTableModel := table.NewSimpleModel( headers, psml.PsmlData(), table.SimpleOptions{ Style: table.StyleOptions{ VerticalSeparator: fill.New(' '), HeaderStyleProvided: true, HeaderStyleFocus: gowid.MakePaletteRef("packet-list-cell-focus"), CellStyleProvided: true, CellStyleSelected: gowid.MakePaletteRef("packet-list-cell-selected"), CellStyleFocus: gowid.MakePaletteRef("packet-list-cell-focus"), }, Layout: table.LayoutOptions{ Widths: widths, }, }, ) expandingModel := psmlmodel.New( packetPsmlTableModel, gowid.MakePaletteRef("packet-list-row-focus"), ) // No need to refetch the information from the TOML file each time this is // called. Use a globally cached version cols := shark.GetPsmlColumnFormatCached() if len(expandingModel.Comparators) > 0 { for i, _ := range expandingModel.Comparators { if i < len(widths) && i < len(cols) { if field, ok := shark.AllowedColumnFormats[cols[i].Field.Token]; ok { if field.Comparator != nil { expandingModel.Comparators[i] = field.Comparator } } } } } return expandingModel } func updatePacketListWithData(psml iPsmlInfo, app gowid.IApp) { packetListView.colors = psml.PsmlColors() // otherwise this isn't updated model := makePacketListModel(psml, app) newPacketsArrived = true packetListTable.SetModel(model, app) newPacketsArrived = false if AutoScroll { coords, err := packetListView.FocusXY() if err == nil { coords.Row = packetListTable.Length() - 1 newPacketsArrived = true // Set focus on the last item in the view, then... packetListView.SetFocusXY(app, coords) newPacketsArrived = false } // ... adjust the widget so it is rendering with the last item at the bottom. packetListTable.GoToBottom(app) } // Only do this once, the first time. if !packetListView.didFirstAutoFocus && len(psml.PsmlData()) > 0 { packetListView.SetFocusOnData(app) packetListView.didFirstAutoFocus = true } } // don't claim the keypress func ApplyAutoScroll(ev *tcell.EventKey, app gowid.IApp) bool { doit := false reenableAutoScroll = false switch ev.Key() { case tcell.KeyRune: if ev.Rune() == 'G' { doit = true } case tcell.KeyEnd: doit = true } if doit { if profiles.ConfBool("main.auto-scroll", true) { AutoScroll = true reenableAutoScroll = true // when packet updates come, helps // understand that AutoScroll should not be disabled again } } return false } type iPsmlInfo interface { PsmlData() [][]string PsmlHeaders() []string PsmlColors() []pcap.PacketColors PsmlAverageLengths() []gwutil.IntOption PsmlMaxLengths() []int } func setPacketListWidgets(psml iPsmlInfo, app gowid.IApp) { expandingModel := makePacketListModel(psml, app) packetListTable = &table.BoundedWidget{Widget: table.New(expandingModel)} packetListView = NewPsmlTableRowWidget( NewRowFocusTableWidget( packetListTable, "packet-list-row-selected", "packet-list-row-focus", ), psml.PsmlColors(), ) packetListView.OnFocusChanged(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, w gowid.IWidget) { fxy, err := packetListView.FocusXY() if err != nil { return } if !newPacketsArrived && !reenableAutoScroll { // this focus change must've been user-initiated, so stop auto-scrolling with new packets. // This mimics Wireshark's behavior. Note that if the user hits the end key, this may // update the view and run this callback, but end means to resume auto-scrolling if it's // enabled, so we should not promptly disable it again AutoScroll = false } row2 := fxy.Row row3, gotrow := packetListView.Model().RowIdentifier(row2) row := int(row3) if gotrow && row >= 0 { pktsPerLoad := Loader.PacketsPerLoad() rowm := row % pktsPerLoad CacheRequests = CacheRequests[:0] CacheRequests = append(CacheRequests, pcap.LoadPcapSlice{ Row: (row / pktsPerLoad) * pktsPerLoad, CancelCurrent: true, }) if rowm > pktsPerLoad/2 { // Optimistically load the batch below this one CacheRequests = append(CacheRequests, pcap.LoadPcapSlice{ Row: ((row / pktsPerLoad) + 1) * pktsPerLoad, }) } else { // Optimistically load the batch above this one row2 := ((row / pktsPerLoad) - 1) * pktsPerLoad if row2 < 0 { row2 = 0 } CacheRequests = append(CacheRequests, pcap.LoadPcapSlice{ Row: row2, }) } CacheRequestsChan <- struct{}{} } // When the focus changes, update the hex and struct view. If they cannot // be populated, display a loading message setLowerWidgets(app) })) withScrollbar := withscrollbar.New(packetListView, withscrollbar.Options{ HideIfContentFits: true, }) selme := enableselected.New(withScrollbar) keys := appkeys.New( selme, tableutil.GotoHandler(&tableutil.GoToAdapter{ BoundedWidget: packetListTable, KeyState: &keyState, }), ) packetListViewHolder.SetSubWidget(keys, app) } func expandStructWidgetAtPosition(row int, pos int, app gowid.IApp) { if curPacketStructWidget != nil { walker := curPacketStructWidget.Walker().(*noroot.Walker) 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) { updateCurrentPdmlPositionFrom(tr, curStructPosition) } func updateCurrentPdmlPositionFrom(tr tree.IModel, pos tree.IPos) { treeAtCurPos := pos.GetSubStructure(tr) // Save [/, tcp, tcp.srcport] - so we can apply if user moves in packet list curPdmlPosition = treeAtCurPos.(*pdmltree.Model).PathToRoot() } func getLayersFromStructWidget(row int, pos int) []hexdumper2.LayerStyler { layers := make([]hexdumper2.LayerStyler, 0) model := getCurrentStructModel(row) if model != nil { layers = model.HexLayers(pos, false) } return layers } func getHexWidgetKey(row int) []byte { return []byte(fmt.Sprintf("p%d", row)) } // Can return nil func getHexWidgetToDisplay(row int) *hexdumper2.Widget { var res2 *hexdumper2.Widget if val, ok := packetHexWidgets.Get(row); ok { res2 = val.(*hexdumper2.Widget) } else { pktsPerLoad := Loader.PacketsPerLoad() row2 := (row / pktsPerLoad) * pktsPerLoad if ws, ok := Loader.PacketCache.Get(row2); ok { srca := ws.(pcap.CacheEntry).Pcap if len(srca) > row%pktsPerLoad { src := srca[row%pktsPerLoad] b := make([]byte, len(src)) copy(b, src) layers := getLayersFromStructWidget(row, 0) res2 = hexdumper2.New(b, hexdumper2.Options{ StyledLayers: layers, CursorUnselected: "hex-byte-unselected", CursorSelected: "hex-byte-selected", LineNumUnselected: "hex-interval-unselected", LineNumSelected: "hex-interval-selected", PaletteIfCopying: "copy-mode", }) // If the user moves the cursor in the hexdump, this callback will adjust the corresponding // pdml tree/struct widget's currently selected layer. That in turn will result in a callback // to the hex widget to set the active layers. res2.OnPositionChanged(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, target gowid.IWidget) { // If we're not focused on hex, then don't expand the struct widget. That's because if // we're focused on struct, then changing the struct position causes a callback to the // hex to update layers - which can update the hex position - which invokes a callback // to change the struct again. So ultimately, moving the struct moves the hex position // which moves the struct and causes the struct to jump around. I need to check // the alt view too because the user can click with the mouse and in one view have // struct selected but in the other view have hex selected. // Propagate the adjustments to pane positions if: // - this was initiated from a non-struct pane e.g. the hex pane. Then we update struct // - this was via an override e.g. search for packet bytes. Then hex is updated on match // which should then update the struct view if currentlyFocusedViewNotStruct() || allowHexToStructRepositioning { expandStructWidgetAtPosition(row, res2.Position(), app) } // Ensure the behavior is reset after this callback runs. Just do it once. allowHexToStructRepositioning = false })) packetHexWidgets.Add(row, res2) } } } return res2 } //====================================================================== // pdmlFilterActor closes the menus opened via the PDML struct view, then // either applies or preps the appropriate display filter type pdmlFilterActor struct { filter string prepare bool menu1 *menu.Widget menu2 *menu.Widget } var _ iFilterMenuActor = (*pdmlFilterActor)(nil) func (p *pdmlFilterActor) HandleFilterMenuSelection(comb FilterCombinator, app gowid.IApp) { multiMenu2Opener.CloseMenu(p.menu2, app) multiMenu1Opener.CloseMenu(p.menu1, app) filter := ComputeFilterCombOp(comb, p.filter, FilterWidget.Value()) FilterWidget.SetValue(filter, app) if p.prepare { // Don't run the filter, just add to the displayfilter widget. Leave focus there setFocusOnDisplayFilter(app) } else { RequestNewFilter(filter, app) } } //====================================================================== 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 := noroot.NewWalker(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, doCursor bool, twalker tree.ITreeWalker) { newhex := getHexWidgetToDisplay(row) if newhex != nil { newtree := twalker.Tree().(*pdmltree.Model) newpos := twalker.Focus().(tree.IPos) expTree := (*pdmltree.ExpandedModel)(twalker.Tree().(*pdmltree.Model)) leaf := newpos.GetSubStructure(expTree).(*pdmltree.ExpandedModel) 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) // If the hex view is changed by the user (or SetPosition), which then causes a callback to update // the struct view - which then causes a callback to update the hex layer - the cursor can move in // such a way it stays inside a valid layer and can't get outside. This boolean controls that if doCursor { curhexpos := newhex.Position() smallestlayer := newLayers[len(newLayers)-1] if !(smallestlayer.Start <= curhexpos && curhexpos < smallestlayer.End) { // The reason for this is to ensure the hex cursor moves according to the current struct // layer - otherwise when you tab to the hex layer, you immediately lose your struct layer // when you move the cursor - because it's outside of your struct context. 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 we're re-rendering this part of the UI because of a hit in a packet // struct search. So set the struct position, and then the next block of code will adjust the // UI to focus on the part of the struct that matched the search. if curSearchPosition != nil { curStructPosition = curSearchPosition } if curStructPosition != nil { // 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 programmatically 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 tree position, 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. curPos := curStructPosition // e.g. [0, 2, 1] expTree := (*pdmltree.ExpandedModel)(walker.Tree().(*pdmltree.Model)) treeAtCurPos := curPos.GetSubStructure(expTree) // e.g. the TCP *pdmltree.Model if treeAtCurPos != nil { // If curSearchPosition != nil, it means out saved path will definitely be a hit in this // struct, so we are guaranteed to be able to apply it to the current tree walker. If // curSearchPosition == nil, it means we try our best to apply the previous position to // the current struct, knowing that the packet structure might be different. if curSearchPosition != nil || deep.Equal(curPdmlPosition, (*pdmltree.Model)(treeAtCurPos.(*pdmltree.ExpandedModel)).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) } if curSearchPosition != nil { curPacketStructWidget.GoToMiddle(app) // Reset so as we move up and down, after a search, we do our best to preserve the // position, but we're not misled into thinking we have a guaranteed hit on the position. curSearchPosition = nil } 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, currentlyFocusedViewNotHex(), 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()) })) } return res } //====================================================================== type copyModePalette struct{} var _ gowid.IClipboardSelected = copyModePalette{} func (r copyModePalette) AlterWidget(w gowid.IWidget, app gowid.IApp) gowid.IWidget { return styled.New(w, gowid.MakePaletteRef("copy-mode"), styled.Options{ OverWrite: true, }, ) } //====================================================================== func RequestLoadInterfaces(psrcs []pcap.IPacketSource, captureFilter string, displayFilter string, tmpfile string, app gowid.IApp) { Loader.Renew() Loader.LoadInterfaces(psrcs, captureFilter, displayFilter, tmpfile, pcap.HandlerList{ StartUIWhenThereArePackets{}, SimpleErrors{}, MakeSaveRecents("", displayFilter), MakePacketViewUpdater(), MakeUpdateCurrentCaptureInTitle(), ManageStreamCache{}, ManageCapinfoCache{}, SetStructWidgets{Loader}, // for OnClear ClearWormholeState{}, ClearMarksHandler{}, ManageSearchData{}, CancelledMessage{}, }, app, ) } //====================================================================== // MaybeKeepThenRequestLoadPcap loads a pcap after first checking to see whether // the current load is a live load and the packets need to be kept. func MaybeKeepThenRequestLoadPcap(pcapf string, displayFilter string, jump termshark.GlobalJumpPos, app gowid.IApp) { if Loader.InterfaceFile() != "" && !WriteToSelected && !profiles.ConfBool("main.always-keep-pcap", false) { askToSave(app, func(app gowid.IApp) { RequestLoadPcap(pcapf, displayFilter, jump, app) }) } else { RequestLoadPcap(pcapf, displayFilter, jump, app) } } // Call from app goroutine context func RequestLoadPcap(pcapf string, displayFilter string, jump termshark.GlobalJumpPos, app gowid.IApp) { handlers := pcap.HandlerList{ SimpleErrors{}, MakeSaveRecents(pcapf, displayFilter), MakePacketViewUpdater(), MakeUpdateCurrentCaptureInTitle(), ManageStreamCache{}, ManageCapinfoCache{}, SetStructWidgets{Loader}, // for OnClear MakeCheckGlobalJumpAfterPsml(jump), ClearWormholeState{}, ClearMarksHandler{}, ManageSearchData{}, CancelledMessage{}, } if _, err := os.Stat(pcapf); os.IsNotExist(err) { pcap.HandleError(pcap.NoneCode, app, err, handlers) } else { // no auto-scroll when reading a file AutoScroll = false Loader.LoadPcap(pcapf, displayFilter, handlers, app) } } //====================================================================== func RequestNewFilter(displayFilter string, app gowid.IApp) { handlers := pcap.HandlerList{ SimpleErrors{}, MakeSaveRecents("", displayFilter), MakePacketViewUpdater(), MakeUpdateCurrentCaptureInTitle(), SetStructWidgets{Loader}, // for OnClear ClearMarksHandler{}, ManageSearchData{}, // Don't use this one - we keep the cancelled flag set so that we // don't restart live captures on clear if ctrl-c has been issued // so we don't want this handler on a new filter because we don't // want to be told again after applying the filter that the load // was cancelled //MakeCancelledMessage(), } if Loader.DisplayFilter() == displayFilter { log.Infof("No operation - same filter applied ('%s').", displayFilter) } else { Loader.Reload(displayFilter, handlers, app) } } func RequestReload(app gowid.IApp) { handlers := pcap.HandlerList{ SimpleErrors{}, MakePacketViewUpdater(), MakeUpdateCurrentCaptureInTitle(), SetStructWidgets{Loader}, // for OnClear ClearMarksHandler{}, ManageSearchData{}, // Don't use this one - we keep the cancelled flag set so that we // don't restart live captures on clear if ctrl-c has been issued // so we don't want this handler on a new filter because we don't // want to be told again after applying the filter that the load // was cancelled //MakeCancelledMessage(), } Loader.Reload(Loader.DisplayFilter(), handlers, app) } //====================================================================== // Prog hold a progress model - a current value on the way up to the max value type Prog struct { cur int64 max int64 } func (p Prog) Complete() bool { return p.cur >= p.max } func (p Prog) String() string { return fmt.Sprintf("cur=%d max=%d", p.cur, p.max) } func (p Prog) Div(y int64) Prog { p.cur /= y return p } func (p Prog) Add(y Prog) Prog { return Prog{cur: p.cur + y.cur, max: p.max + y.max} } func progMin(x, y Prog) Prog { if float64(x.cur)/float64(x.max) < float64(y.cur)/float64(y.max) { return x } else { return y } } func progMax(x, y Prog) Prog { if float64(x.cur)/float64(x.max) > float64(y.cur)/float64(y.max) { return x } else { return y } } //====================================================================== func makeRecentMenuWidget() (gowid.IWidget, int) { savedItems := make([]menuutil.SimpleMenuItem, 0) cfiles := profiles.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) { multiMenu1Opener.CloseMenu(savedMenu, app) // capFilter global, set up in cmain() MaybeKeepThenRequestLoadPcap(scopy, FilterWidget.Value(), NoGlobalJump, app) }, }, ) } } return menuutil.MakeMenuWithHotKeys(savedItems, nil) } func UpdateRecentMenu(app gowid.IApp) { savedListBox, _ := makeRecentMenuWidget() savedListBoxWidgetHolder.SetSubWidget(savedListBox, app) } //====================================================================== type savedCompleterCallback struct { prefix string comp fields.IPrefixCompleterCallback } var _ fields.IPrefixCompleterCallback = (*savedCompleterCallback)(nil) func (s *savedCompleterCallback) Call(orig []string) { if s.prefix == "" { comps := profiles.ConfStrings("main.recent-filters") if len(comps) == 0 { comps = orig } s.comp.Call(comps) } else { s.comp.Call(orig) } } type savedCompleter struct { def fields.IPrefixCompleter } var _ fields.IPrefixCompleter = (*savedCompleter)(nil) func (s savedCompleter) Completions(prefix string, cb fields.IPrefixCompleterCallback) { ncomp := &savedCompleterCallback{ prefix: prefix, comp: cb, } s.def.Completions(prefix, ncomp) } //====================================================================== func StopEmptyStructViewTimer() { if EmptyStructViewTimer != nil { EmptyStructViewTimer.Stop() EmptyStructViewTimer = nil } } func StopEmptyHexViewTimer() { if EmptyHexViewTimer != nil { EmptyHexViewTimer.Stop() EmptyHexViewTimer = nil } } //====================================================================== func assignTo(wp interface{}, w gowid.IWidget) gowid.IWidget { reflect.ValueOf(wp).Elem().Set(reflect.ValueOf(w)) return w } //====================================================================== // prefixKeyWidget wraps a widget, and adjusts the state of the variables tracking // "partial" key chords e.g. the first Z in ZZ, the first g in gg. It also resets // the number prefix (which some commands use) - this is done if they key is not // a number, and the last keypress wasn't the start of a key chord. type prefixKeyWidget struct { gowid.IWidget } func (w *prefixKeyWidget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { // Save these first. If they are enabled now, any key should cancel them, so cancel // at the end. startingKeyState := keyState handled := w.IWidget.UserInput(ev, size, focus, app) switch ev := ev.(type) { case *tcell.EventKey: // If it was set this time around, whatever key was pressed resets it if startingKeyState.PartialgCmd { keyState.PartialgCmd = false } if startingKeyState.PartialZCmd { keyState.PartialZCmd = false } if startingKeyState.PartialCtrlWCmd { keyState.PartialCtrlWCmd = false } if startingKeyState.PartialmCmd { keyState.PartialmCmd = false } if startingKeyState.PartialQuoteCmd { keyState.PartialQuoteCmd = false } if ev.Key() != tcell.KeyRune || ev.Rune() < '0' || ev.Rune() > '9' { if !keyState.PartialZCmd && !keyState.PartialgCmd && !keyState.PartialCtrlWCmd { keyState.NumberPrefix = -1 } } } return handled } //====================================================================== func SetDarkMode(mode bool) { DarkMode = mode profiles.SetConf("main.dark-mode", DarkMode) } func UpdateProfileWidget(name string, app gowid.IApp) { currentProfile.SetText(name, app) if name != "" && name != "default" { currentProfileWidgetHolder.SetSubWidget(currentProfileWidget, app) } else { currentProfileWidgetHolder.SetSubWidget(nullw, app) } } // vp and vc guaranteed to be non-nil func ApplyCurrentProfile(app gowid.IApp, vp *viper.Viper, vc *viper.Viper) error { UpdateProfileWidget(profiles.CurrentName(), app) reload := false SetDarkMode(profiles.ConfBool("main.dark-mode", true)) curWireshark := profiles.ConfStringFrom(vp, profiles.Default(), "main.wireshark-profile", "") newWireshark := profiles.ConfStringFrom(vc, profiles.Default(), "main.wireshark-profile", "") if curWireshark != newWireshark { reload = true } curcols := profiles.ConfStringSliceFrom(vp, profiles.Default(), "main.column-format", []string{}) newcols := profiles.ConfStringSliceFrom(vc, profiles.Default(), "main.column-format", []string{}) if !reflect.DeepEqual(newcols, curcols) { reload = true } if reload { RequestReload(app) } ApplyCurrentTheme(app) SetupColors() return nil } func ApplyCurrentTheme(app gowid.IApp) { var err error mode := app.GetColorMode() modeStr := theme.Mode(mode) // more concise themeName := profiles.ConfString(fmt.Sprintf("main.theme-%s", modeStr), "default") loaded := false if themeName != "" { err = theme.Load(themeName, app) if err != nil { log.Warnf("Theme %s could not be loaded: %v", themeName, err) } else { loaded = true } } if !loaded && themeName != "default" { err = theme.Load("default", app) if err != nil { log.Warnf("Theme %s could not be loaded: %v", themeName, err) } } } //====================================================================== func Build(tty string) (*gowid.App, error) { var err error var app *gowid.App widgetCacheSize := profiles.ConfInt("main.ui-cache-size", 1000) if widgetCacheSize < 64 { widgetCacheSize = 64 } packetHexWidgets, err = lru.New(widgetCacheSize) if err != nil { return nil, gowid.WithKVs(termshark.InternalErr, map[string]interface{}{ "err": err, }) } nullw = null.New() Loadingw = text.New("Loading, please wait...") singlePacketViewMsgHolder = holder.New(nullw) fillSpace = fill.New(' ') if runtime.GOOS == "windows" { fillVBar = fill.New('|') } else { fillVBar = fill.New('┃') } colSpace = &gowid.ContainerWidget{ IWidget: fillSpace, D: units(1), } MissingMsgw = vpadding.New( // centred hpadding.New(singlePacketViewMsgHolder, hmiddle, fixed), vmiddle, flow, ) pleaseWaitSpinner = spinner.New(spinner.Options{ Styler: gowid.MakePaletteRef("progress-spinner"), }) PleaseWait = dialog.New(framed.NewSpace( pile.NewFlow( &gowid.ContainerWidget{ IWidget: text.New(" Please wait... "), D: gowid.RenderFixed{}, }, fillSpace, pleaseWaitSpinner, )), dialog.Options{ Buttons: dialog.NoButtons, NoShadow: true, BackgroundStyle: gowid.MakePaletteRef("dialog"), BorderStyle: gowid.MakePaletteRef("dialog"), ButtonStyle: gowid.MakePaletteRef("dialog-button"), }, ) title := styled.New(text.New(termshark.TemplateToString(Templates, "NameVer", TemplateData)), gowid.MakePaletteRef("title")) currentCapture = text.New("") currentCaptureStyled := styled.New( currentCapture, gowid.MakePaletteRef("current-capture"), ) sp := text.New(" ") currentCaptureWidget = columns.NewFixed( sp, &gowid.ContainerWidget{ IWidget: fill.New('|'), D: gowid.MakeRenderBox(1, 1), }, sp, currentCaptureStyled, ) currentCaptureWidgetHolder = holder.New(nullw) CopyModePredicate = func() bool { return app != nil && app.InCopyMode() } CopyModeWidget = styled.New( ifwidget.New( text.New(" COPY-MODE "), null.New(), CopyModePredicate, ), gowid.MakePaletteRef("copy-mode-label"), ) //====================================================================== openMenu := button.NewBare(text.New(" Misc ")) openMenu2 := clicktracker.New( styled.NewExt( openMenu, gowid.MakePaletteRef("button"), gowid.MakePaletteRef("button-focus"), ), ) openMenuSite = menu.NewSite(menu.SiteOptions{YOffset: 1}) openMenu.OnClick(gowid.MakeWidgetCallback(gowid.ClickCB{}, func(app gowid.IApp, target gowid.IWidget) { multiMenu1Opener.OpenMenu(generalMenu, openMenuSite, app) })) //====================================================================== generalMenuItems := make([]menuutil.SimpleMenuItem, 0) generalMenuItems = append(generalMenuItems, []menuutil.SimpleMenuItem{ menuutil.SimpleMenuItem{ Txt: "Refresh Screen", Key: gowid.MakeKeyExt2(0, tcell.KeyCtrlL, ' '), CB: func(app gowid.IApp, w gowid.IWidget) { multiMenu1Opener.CloseMenu(generalMenu, 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) { multiMenu1Opener.CloseMenu(generalMenu, app) SetDarkMode(!DarkMode) }, }, menuutil.MakeMenuDivider(), menuutil.SimpleMenuItem{ Txt: "Search Packets", Key: gowid.MakeKeyExt2(0, tcell.KeyCtrlF, ' '), CB: func(app gowid.IApp, w gowid.IWidget) { multiMenu1Opener.CloseMenu(generalMenu, app) if !searchOpen() { filterHolder.SetSubWidget(filterWithSearch, app) } setFocusOnSearch(app) }, }, menuutil.SimpleMenuItem{ Txt: "Clear Packets", Key: gowid.MakeKeyExt2(0, tcell.KeyCtrlW, ' '), CB: func(app gowid.IApp, w gowid.IWidget) { multiMenu1Opener.CloseMenu(generalMenu, app) reallyClear(app) }, }, menuutil.SimpleMenuItem{ Txt: "Send Pcap", Key: gowid.MakeKey('s'), CB: func(app gowid.IApp, w gowid.IWidget) { multiMenu1Opener.CloseMenu(generalMenu, app) openWormhole(app) }, }, menuutil.SimpleMenuItem{ Txt: "Edit Columns", Key: gowid.MakeKey('e'), CB: func(app gowid.IApp, w gowid.IWidget) { multiMenu1Opener.CloseMenu(generalMenu, app) openEditColumns(app) }, }}...) if runtime.GOOS != "windows" { generalMenuItems = append(generalMenuItems, menuutil.SimpleMenuItem{ Txt: "Show Log", Key: gowid.MakeKey('l'), CB: func(app gowid.IApp, w gowid.IWidget) { multiMenu1Opener.CloseMenu(generalMenu, app) openLogsUi(app) }, }) generalMenuItems = append(generalMenuItems, menuutil.SimpleMenuItem{ Txt: "Show Config", CB: func(app gowid.IApp, w gowid.IWidget) { multiMenu1Opener.CloseMenu(generalMenu, app) openConfigUi(app) }, }) } generalMenuItems = append(generalMenuItems, []menuutil.SimpleMenuItem{ menuutil.MakeMenuDivider(), menuutil.SimpleMenuItem{ Txt: "Help", Key: gowid.MakeKey('?'), CB: func(app gowid.IApp, w gowid.IWidget) { multiMenu1Opener.CloseMenu(generalMenu, app) OpenTemplatedDialog(appView, "UIHelp", app) }, }, menuutil.SimpleMenuItem{ Txt: "User Guide", Key: gowid.MakeKey('u'), CB: func(app gowid.IApp, w gowid.IWidget) { multiMenu1Opener.CloseMenu(generalMenu, 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) { multiMenu1Opener.CloseMenu(generalMenu, app) if !termshark.RunningRemotely() { termshark.BrowseUrl(termshark.FAQURL) } openResultsAfterCopy("UIFAQ", termshark.FAQURL, app) }, }, menuutil.MakeMenuDivider(), menuutil.SimpleMenuItem{ Txt: "Found a Bug?", Key: gowid.MakeKey('b'), CB: func(app gowid.IApp, w gowid.IWidget) { multiMenu1Opener.CloseMenu(generalMenu, app) if !termshark.RunningRemotely() { termshark.BrowseUrl(termshark.BugURL) } openResultsAfterCopy("UIBug", termshark.BugURL, app) }, }, menuutil.SimpleMenuItem{ Txt: "Feature Request?", Key: gowid.MakeKey('f'), CB: func(app gowid.IApp, w gowid.IWidget) { multiMenu1Opener.CloseMenu(generalMenu, app) if !termshark.RunningRemotely() { termshark.BrowseUrl(termshark.FeatureURL) } openResultsAfterCopy("UIFeature", termshark.FeatureURL, app) }, }, menuutil.MakeMenuDivider(), menuutil.SimpleMenuItem{ Txt: "Quit", Key: gowid.MakeKey('q'), CB: func(app gowid.IApp, w gowid.IWidget) { multiMenu1Opener.CloseMenu(generalMenu, 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) { multiMenu1Opener.CloseMenu(generalMenu, app) PacketColors = !PacketColors profiles.SetConf("main.packet-colors", PacketColors) }, }, }, generalMenuItems[2:]..., )..., ) } generalMenuListBox, generalMenuWidth := menuutil.MakeMenuWithHotKeys(generalMenuItems, nil) var generalNext menuutil.NextMenu generalMenuListBoxWithKeys := appkeys.New( generalMenuListBox, menuutil.MakeMenuNavigatingKeyPress( &generalNext, nil, ), ) // Hack. What's a more general way of doing this? The length of the button I suppose openMenuSite.Options.XOffset = -generalMenuWidth + 8 generalMenu = menu.New("main", generalMenuListBoxWithKeys, units(generalMenuWidth), menu.Options{ Modal: true, CloseKeysProvided: true, OpenCloser: &multiMenu1Opener, 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{XOffset: -12, YOffset: 1}) openAnalysis.OnClick(gowid.MakeWidgetCallback(gowid.ClickCB{}, func(app gowid.IApp, target gowid.IWidget) { multiMenu1Opener.OpenMenu(analysisMenu, openAnalysisSite, app) })) analysisMenuItems := []menuutil.SimpleMenuItem{ menuutil.SimpleMenuItem{ Txt: "Capture file properties", Key: gowid.MakeKey('p'), CB: func(app gowid.IApp, w gowid.IWidget) { multiMenu1Opener.CloseMenu(analysisMenu, app) startCapinfo(app) }, }, menuutil.SimpleMenuItem{ Txt: "Reassemble stream", Key: gowid.MakeKey('f'), CB: func(app gowid.IApp, w gowid.IWidget) { multiMenu1Opener.CloseMenu(analysisMenu, app) startStreamReassembly(app) }, }, menuutil.SimpleMenuItem{ Txt: "Conversations", Key: gowid.MakeKey('c'), CB: func(app gowid.IApp, w gowid.IWidget) { multiMenu1Opener.CloseMenu(analysisMenu, app) openConvsUi(app) }, }, } analysisMenuListBox, analysisMenuWidth := menuutil.MakeMenuWithHotKeys(analysisMenuItems, nil) var analysisNext menuutil.NextMenu analysisMenuListBoxWithKeys := appkeys.New( analysisMenuListBox, menuutil.MakeMenuNavigatingKeyPress( nil, &analysisNext, ), ) analysisMenu = menu.New("analysis", analysisMenuListBoxWithKeys, units(analysisMenuWidth), menu.Options{ Modal: true, CloseKeysProvided: true, OpenCloser: &multiMenu1Opener, 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, OpenCloser: &multiMenu1Opener, CloseKeys: []gowid.IKey{ gowid.MakeKeyExt(tcell.KeyLeft), gowid.MakeKeyExt(tcell.KeyEscape), gowid.MakeKeyExt(tcell.KeyCtrlC), }, }) //====================================================================== currentProfile = text.New("default") currentProfileWidget = columns.NewFixed( text.New("Profile: "), currentProfile, sp, &gowid.ContainerWidget{ IWidget: fill.New('|'), D: gowid.MakeRenderBox(1, 1), }, sp, ) currentProfileWidgetHolder = holder.New(currentProfileWidget) // Update display to show the profile if it isn't the default UpdateProfileWidget(profiles.CurrentName(), app) 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), }, &gowid.ContainerWidget{ IWidget: currentProfileWidgetHolder, D: fixed, // give it priority when the window isn't wide enough }, 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.MenuOpener = &multiMenu1Opener generalNext.Focus = 5 // should really find by ID // <> analysisNext.Cur = analysisMenu analysisNext.Next = generalMenu analysisNext.Site = openMenuSite analysisNext.Container = titleCols analysisNext.MenuOpener = &multiMenu1Opener analysisNext.Focus = 7 // 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"), ), ), ) // For completing filter expressions FieldCompleter = fields.New() FieldCompleter.Init() FilterWidget = filter.New("filter", filter.Options{ Completer: savedCompleter{def: FieldCompleter}, MenuOpener: &multiMenu1Opener, }) validFilterCb := gowid.MakeWidgetCallback("cb", func(app gowid.IApp, w gowid.IWidget) { if Loader.DisplayFilter() == FilterWidget.Value() { OpenError("Same filter - nothing to do", app) } else { RequestNewFilter(FilterWidget.Value(), app) } }) // Will only be enabled to click if filter is valid applyw.OnClick(validFilterCb) // Will only fire OnSubmit if filter is valid FilterWidget.OnSubmit(validFilterCb) FilterWidget.OnValid(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, w gowid.IWidget) { applyWidget.Enable() })) FilterWidget.OnInvalid(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, w gowid.IWidget) { applyWidget.Disable() })) filterLabel := text.New("Filter: ") savedw := button.New(text.New("Recent")) savedWidget := clicktracker.New( styled.NewExt( savedw, gowid.MakePaletteRef("button"), gowid.MakePaletteRef("button-focus"), ), ) savedBtnSite := menu.NewSite(menu.SiteOptions{YOffset: 1}) savedw.OnClick(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, w gowid.IWidget) { multiMenu1Opener.OpenMenu(savedMenu, savedBtnSite, app) // if !multiMenu1Opener.OpenMenu(savedMenu, savedBtnSite, app) { // multiMenu1Opener.CloseMenu(savedMenu, 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) //====================================================================== loadStop, loadProg = createLoaderProgressWidget() searchStop, searchProg = createProgressWidget() //====================================================================== searchCh := make(chan search.IntermediateResult) cbs := &commonSearchCallbacks{} listSearchCallbacks := func() search.ICallbacks { return &ListSearchCallbacks{ commonSearchCallbacks: cbs, SearchStopper: &SearchStopper{}, search: searchCh, } } structSearchCallbacks := func() search.ICallbacks { return &StructSearchCallbacks{ commonSearchCallbacks: cbs, SearchStopper: &SearchStopper{}, search: searchCh, } } bytesSearchCallbacks := func() search.ICallbacks { return &BytesSearchCallbacks{ commonSearchCallbacks: cbs, SearchStopper: &SearchStopper{}, search: searchCh, } } filterSearchCallbacks := func() search.ICallbacks { return NewFilterSearchCallbacks(cbs, searchCh) } SearchWidget = search.New( &PacketSearcher{ resultChan: searchCh, }, listSearchCallbacks, structSearchCallbacks, bytesSearchCallbacks, filterSearchCallbacks, &multiMenu1Opener, savedCompleter{def: FieldCompleter}, OpenErrorDialog{}, ) //====================================================================== filterWithoutSearch = pile.New([]gowid.IContainerWidget{ &gowid.ContainerWidget{ IWidget: filterCols, D: units(1), }, }) filterWithSearch = pile.New([]gowid.IContainerWidget{ &gowid.ContainerWidget{ IWidget: filterCols, D: units(1), }, &gowid.ContainerWidget{ IWidget: fill.New('━'), D: units(1), }, &gowid.ContainerWidget{ IWidget: SearchWidget, D: units(1), }, }) filterHolder = holder.New(filterWithoutSearch) filterView := framed.NewUnicode(filterHolder) // swallowMovementKeys will prevent cursor movement that is not accepted // by the main views (column or pile) to change focus e.g. moving from the // packet structure view to the packet list view. Often you'd want this // movement to be possible, but in termshark it's more often annoying - // you navigate to the top of the packet structure, hit up one more time // and you're in the packet list view accidentally, hit down instinctively // to go back and you change the selected packet. packetListViewWithKeys := appkeys.NewMouse( appkeys.New( appkeys.New( appkeys.New( packetListViewHolder, ApplyAutoScroll, appkeys.Options{ ApplyBefore: true, }, ), appKeysResize1, ), widgets.SwallowMovementKeys, ), widgets.SwallowMouseScroll, ) packetStructureViewWithKeys := appkeys.New( appkeys.New( appkeys.NewMouse( appkeys.New( appkeys.New( packetStructureViewHolder, appKeysResize2, ), widgets.SwallowMovementKeys, ), widgets.SwallowMouseScroll, ), copyModeEnterKeys, appkeys.Options{ ApplyBefore: true, }, ), copyModeExitKeys, appkeys.Options{ ApplyBefore: true, }, ) packetHexViewHolderWithKeys := appkeys.New( appkeys.New( appkeys.NewMouse( appkeys.New( packetHexViewHolder, widgets.SwallowMovementKeys, ), widgets.SwallowMouseScroll, ), copyModeEnterKeys, appkeys.Options{ ApplyBefore: true, }, ), copyModeExitKeys, appkeys.Options{ ApplyBefore: true, }, ) mainviewRows = resizable.NewPile([]gowid.IContainerWidget{ &gowid.ContainerWidget{ IWidget: titleView, D: units(1), }, &gowid.ContainerWidget{ IWidget: filterView, D: flow, }, &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: flow, }, &gowid.ContainerWidget{ IWidget: packetListViewHolder, D: weight(1), }, }) viewOnlyPacketStructure = pile.New([]gowid.IContainerWidget{ &gowid.ContainerWidget{ IWidget: titleView, D: units(1), }, &gowid.ContainerWidget{ IWidget: filterView, D: flow, }, &gowid.ContainerWidget{ IWidget: packetStructureViewHolder, D: weight(1), }, }) viewOnlyPacketHex = pile.New([]gowid.IContainerWidget{ &gowid.ContainerWidget{ IWidget: titleView, D: units(1), }, &gowid.ContainerWidget{ IWidget: filterView, D: flow, }, &gowid.ContainerWidget{ IWidget: packetHexViewHolder, D: weight(1), }, }) tabViewsForward = make(map[gowid.IWidget]gowid.IWidget) tabViewsBackward = make(map[gowid.IWidget]gowid.IWidget) tabViewsForward[viewOnlyPacketList] = viewOnlyPacketStructure tabViewsForward[viewOnlyPacketStructure] = viewOnlyPacketHex tabViewsForward[viewOnlyPacketHex] = viewOnlyPacketList tabViewsBackward[viewOnlyPacketList] = viewOnlyPacketHex tabViewsBackward[viewOnlyPacketStructure] = viewOnlyPacketList tabViewsBackward[viewOnlyPacketHex] = viewOnlyPacketStructure //====================================================================== altview1Pile = resizable.NewPile([]gowid.IContainerWidget{ &gowid.ContainerWidget{ IWidget: packetListViewWithKeys, D: weight(1), }, &gowid.ContainerWidget{ IWidget: divider.NewUnicode(), D: flow, }, &gowid.ContainerWidget{ IWidget: packetStructureViewWithKeys, D: weight(1), }, }) altview1Pile.OnOffsetsSet(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, w gowid.IWidget) { termshark.SaveOffsetToConfig("altviewleft", altview1Pile.GetOffsets()) })) altview1PileAndKeys := appkeys.New(altview1Pile, altview1PileKeyPress) altview1Cols = resizable.NewColumns([]gowid.IContainerWidget{ &gowid.ContainerWidget{ IWidget: altview1PileAndKeys, D: weight(1), }, &gowid.ContainerWidget{ IWidget: fillVBar, D: units(1), }, &gowid.ContainerWidget{ IWidget: packetHexViewHolderWithKeys, D: weight(1), }, }) altview1Cols.OnOffsetsSet(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, w gowid.IWidget) { termshark.SaveOffsetToConfig("altviewright", altview1Cols.GetOffsets()) })) altview1ColsAndKeys := appkeys.New(altview1Cols, altview1ColsKeyPress) altview1OuterRows = resizable.NewPile([]gowid.IContainerWidget{ &gowid.ContainerWidget{ IWidget: titleView, D: units(1), }, &gowid.ContainerWidget{ IWidget: filterView, D: flow, }, &gowid.ContainerWidget{ IWidget: altview1ColsAndKeys, D: weight(1), }, }) //====================================================================== altview2ColsAndKeys := appkeys.New( assignTo(&altview2Cols, resizable.NewColumns([]gowid.IContainerWidget{ &gowid.ContainerWidget{ IWidget: packetStructureViewWithKeys, D: weight(1), }, &gowid.ContainerWidget{ IWidget: fillVBar, D: units(1), }, &gowid.ContainerWidget{ IWidget: packetHexViewHolderWithKeys, D: weight(1), }, }), ), altview2ColsKeyPress, ) altview2Cols.OnOffsetsSet(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, w gowid.IWidget) { termshark.SaveOffsetToConfig("altview2vertical", altview2Cols.GetOffsets()) })) altview2PileAndKeys := appkeys.New( assignTo(&altview2Pile, resizable.NewPile([]gowid.IContainerWidget{ &gowid.ContainerWidget{ IWidget: packetListViewWithKeys, D: weight(1), }, &gowid.ContainerWidget{ IWidget: divider.NewUnicode(), D: flow, }, &gowid.ContainerWidget{ IWidget: altview2ColsAndKeys, D: weight(1), }, }), ), altview2PileKeyPress, ) altview2Pile.OnOffsetsSet(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, w gowid.IWidget) { termshark.SaveOffsetToConfig("altview2horizontal", altview2Pile.GetOffsets()) })) altview2OuterRows = resizable.NewPile([]gowid.IContainerWidget{ &gowid.ContainerWidget{ IWidget: titleView, D: units(1), }, &gowid.ContainerWidget{ IWidget: filterView, D: flow, }, &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, 0, 1} filterPathAlt = []interface{}{1, 0, 1} filterPathMax = []interface{}{1, 0, 1} searchPathMain = []interface{}{1, 2, 6} // 6 is the index of the filter in the search widget searchPathAlt = []interface{}{1, 2, 6} searchPathMax = []interface{}{1, 2, 6} mainview = mainviewRows altview1 = altview1OuterRows altview2 = altview2OuterRows mainViewNoKeys = holder.New(mainview) defaultLayout := profiles.ConfString("main.layout", "") switch defaultLayout { case "altview1": mainViewNoKeys = holder.New(altview1) case "altview2": mainViewNoKeys = holder.New(altview2) } // <> menuPathMain = []interface{}{0, 7} menuPathAlt = []interface{}{0, 7} menuPathMax = []interface{}{0, 7} buildStreamUi() buildFilterConvsMenu() buildNamesMenu(app) buildFieldsMenu(app) mainView = appkeys.New( appkeys.New( appkeys.New( mainViewNoKeys, mainKeyPress, // applied after mainViewNoKeys processes the input ), vimKeysMainView, appkeys.Options{ ApplyBefore: true, }, ), searchPacketsMainView, appkeys.Options{ ApplyBefore: true, }, ) //====================================================================== palette := PaletteSwitcher{ P1: &DarkModePalette, P2: &RegularPalette, ChooseOne: &DarkMode, } appViewWithKeys := &prefixKeyWidget{ IWidget: appkeys.New( assignTo(&appViewNoKeys, holder.New(mainView)), appKeyPress, ), } // For minibuffer mbView = holder.New(appViewWithKeys) if !profiles.ConfBool("main.disable-shark-fin", false) { Fin = rossshark.New(mbView) steerableFin := appkeys.NewMouse( appkeys.New( Fin, func(evk *tcell.EventKey, app gowid.IApp) bool { if Fin.Active() { switch evk.Key() { case tcell.KeyLeft: Fin.Dir = rossshark.Backward case tcell.KeyRight: Fin.Dir = rossshark.Forward default: Fin.Deactivate() } return true } return false }, appkeys.Options{ ApplyBefore: true, }, ), func(evm *tcell.EventMouse, app gowid.IApp) bool { if Fin.Active() { Fin.Deactivate() return true } return false }, appkeys.Options{ ApplyBefore: true, }, ) appView = holder.New(steerableFin) } else { appView = holder.New(mbView) } // A restriction on the multiMenu is that it only holds one open menu, so using // this trick, only one menu can be open at a time per multiMenu variable. So // I am making two because all I need at the moment is two levels of menu. multiMenu.IMenuCompatible = holder.New(appView) multiMenu2.IMenuCompatible = holder.New(multiMenu) multiMenu1Opener.under = appView multiMenu1Opener.mm = multiMenu multiMenu2Opener.under = multiMenu multiMenu2Opener.mm = multiMenu2 var lastMenu gowid.IWidget = multiMenu2 menus := []gowid.IMenuCompatible{ // These menus can both be open at the same time, so I have special // handling here. I should use a more general method for all menus. The // current method only allows one menu to be open at a time. filterConvsMenu1, filterConvsMenu2, } for _, w := range menus { w.SetSubWidget(lastMenu, app) lastMenu = w } keyMapper = mapkeys.New(lastMenu) keyMappings := termshark.LoadKeyMappings() for _, km := range keyMappings { log.Infof("Applying keymapping %v --> %v", km.From, km.To) keyMapper.AddMapping(km.From, km.To, app) } if err = termshark.LoadGlobalMarks(globalMarksMap); err != nil { // Not fatal log.Error(err) } // Create app, etc, but don't init screen which sets ICANON, etc app, err = gowid.NewApp(gowid.AppArgs{ View: keyMapper, Palette: palette, Log: log.StandardLogger(), EnableBracketedPaste: true, DontActivate: true, Tty: tty, }) if err != nil { return nil, err } gowid.SetFocusPath(mainview, mainviewPaths[0], app) gowid.SetFocusPath(altview1, altview1Paths[0], app) gowid.SetFocusPath(altview2, altview2Paths[0], app) if offs, err := termshark.LoadOffsetFromConfig("mainview"); err == nil { mainviewRows.SetOffsets(offs, app) } if offs, err := termshark.LoadOffsetFromConfig("altviewleft"); err == nil { altview1Pile.SetOffsets(offs, app) } if offs, err := termshark.LoadOffsetFromConfig("altviewright"); err == nil { altview1Cols.SetOffsets(offs, app) } if offs, err := termshark.LoadOffsetFromConfig("altview2horizontal"); err == nil { altview2Pile.SetOffsets(offs, app) } if offs, err := termshark.LoadOffsetFromConfig("altview2vertical"); err == nil { altview2Cols.SetOffsets(offs, app) } return app, err } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.4.0/ui/wormhole.go000066400000000000000000000041601426312004500163130ustar00rootroot00000000000000// Copyright 2019-2020 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/dialog" "github.com/gcla/gowid/widgets/framed" "github.com/gcla/termshark/v2/configs/profiles" "github.com/gcla/termshark/v2/widgets/wormhole" log "github.com/sirupsen/logrus" ) //====================================================================== var CurrentWormholeWidget *wormhole.Widget func openWormhole(app gowid.IApp) { var numWords int if CurrentWormholeWidget == nil { numWords = profiles.ConfInt("main.wormhole-length", 2) } else { numWords = CurrentWormholeWidget.CodeLength() } if CurrentWormholeWidget == nil { var err error CurrentWormholeWidget, err = wormhole.New(Loader.PcapPdml, app, wormhole.Options{ ErrorHandler: func(err error, app gowid.IApp) { msg := fmt.Sprintf("Problem sending pcap: %v", err) log.Error(msg) OpenError(msg, app) }, CodeLength: numWords, TransitRelayAddress: profiles.ConfString("main.wormhole-transit-relay", ""), RendezvousURL: profiles.ConfString("main.wormhole-rendezvous-url", ""), }) if err != nil { msg := fmt.Sprintf("%v", err) log.Error(msg) OpenError(msg, app) return } } wormholeDialog := dialog.New( framed.NewSpace( CurrentWormholeWidget, ), dialog.Options{ Buttons: []dialog.Button{dialog.CloseD}, NoShadow: true, BackgroundStyle: gowid.MakePaletteRef("dialog"), BorderStyle: gowid.MakePaletteRef("dialog"), ButtonStyle: gowid.MakePaletteRef("dialog-button"), }, ) // space for the frame; then XXX-word1-word2-... - max length of word in // pgp word list is 11. Yuck. maxl := (2 * 3) + len(" - cancelled!") + wormhole.UpperBoundOnLength(numWords) wormholeDialog.Open(appView, ratioupto(0.8, maxl), app) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.4.0/utils.go000066400000000000000000001043141426312004500152040ustar00rootroot00000000000000// Copyright 2019-2022 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" "sort" "strconv" "strings" "sync" "syscall" "text/template" "time" "unicode" "github.com/adam-hanna/arrayOperations" "github.com/blang/semver" "github.com/gcla/gowid" "github.com/gcla/gowid/gwutil" "github.com/gcla/gowid/vim" "github.com/gcla/gowid/widgets/table" "github.com/gcla/termshark/v2/configs/profiles" "github.com/gcla/termshark/v2/pkg/system" "github.com/gcla/termshark/v2/widgets/resizable" "github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2/terminfo" "github.com/gdamore/tcell/v2/terminfo/dynamic" "github.com/mattn/go-isatty" "github.com/pkg/errors" "github.com/shibukawa/configdir" log "github.com/sirupsen/logrus" "github.com/tevino/abool" ) //====================================================================== type BadStateError struct{} var _ error = BadStateError{} func (e BadStateError) Error() string { return "Bad state" } var BadState = BadStateError{} //====================================================================== type BadCommandError struct{} var _ error = BadCommandError{} func (e BadCommandError) Error() string { return "Error running command" } var BadCommand = BadCommandError{} //====================================================================== type ConfigError struct{} var _ error = ConfigError{} func (e ConfigError) Error() string { return "Configuration error" } var ConfigErr = ConfigError{} //====================================================================== type InternalError struct{} var _ error = InternalError{} func (e InternalError) Error() string { return "Internal error" } var InternalErr = InternalError{} //====================================================================== var ( UserGuideURL string = "https://termshark.io/userguide" FAQURL string = "https://termshark.io/faq" BugURL string = "https://github.com/gcla/termshark/issues/new?assignees=&labels=&template=bug_report.md&title=" FeatureURL string = "https://github.com/gcla/termshark/issues/new?assignees=&labels=&template=feature_request.md&title=" OriginalEnv []string ShouldSwitchTerminal bool ShouldSwitchBack bool unitsRe *regexp.Regexp = regexp.MustCompile(`^([0-9,]+)\s*(bytes|kB|MB)?`) ) //====================================================================== func IsCommandInPath(bin string) bool { _, err := exec.LookPath(bin) return err == nil } func DirOfPathCommandUnsafe(bin string) string { d, err := DirOfPathCommand(bin) if err != nil { panic(err) } return d } func DirOfPathCommand(bin string) (string, error) { return exec.LookPath(bin) } //====================================================================== func ReverseStringSlice(s []string) { for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 { s[i], s[j] = s[j], s[i] } } //====================================================================== var TSharkVersionUnknown = fmt.Errorf("Could not determine version of tshark") func TSharkVersionFromOutput(output string) (semver.Version, error) { var ver = regexp.MustCompile(`^TShark .*?(\d+\.\d+\.\d+)`) res := ver.FindStringSubmatch(output) if len(res) > 0 { if v, err := semver.Make(res[1]); err == nil { return v, nil } else { return semver.Version{}, err } } return semver.Version{}, errors.WithStack(TSharkVersionUnknown) } func TSharkVersion(tshark string) (semver.Version, error) { cmd := exec.Command(tshark, "--version") cmdOutput := &bytes.Buffer{} cmd.Stdout = cmdOutput cmd.Run() // don't check error - older versions return error code 1. Just search output. output := cmdOutput.Bytes() return TSharkVersionFromOutput(string(output)) } // Depends on empty.pcap being present func TSharkSupportsColor(tshark string) (bool, error) { exitCode, err := RunForExitCode( tshark, []string{"-r", CacheFile("empty.pcap"), "-T", "psml", "--color"}, nil, ) return exitCode == 0, err } // TSharkPath will return the full path of the tshark binary, if it's found in the path, otherwise an error func TSharkPath() (string, *gowid.KeyValueError) { tsharkBin := profiles.ConfString("main.tshark", "") if tsharkBin != "" { confirmedTshark := false if _, err := os.Stat(tsharkBin); err == nil { confirmedTshark = true } else if IsCommandInPath(tsharkBin) { confirmedTshark = true } // This message is for a configured tshark binary that is invalid if !confirmedTshark { err := gowid.WithKVs(ConfigErr, map[string]interface{}{ "msg": fmt.Sprintf("Could not run tshark binary '%s'. The tshark binary is required to run termshark.\n", tsharkBin) + fmt.Sprintf("Check your config file %s\n", ConfFile("toml")), }) return "", &err } } else { tsharkBin = "tshark" if !IsCommandInPath(tsharkBin) { // This message is for an unconfigured tshark bin (via PATH) that is invalid errstr := fmt.Sprintf("Could not find tshark in your PATH. The tshark binary is required to run termshark.\n") if strings.Contains(os.Getenv("PREFIX"), "com.termux") { errstr += fmt.Sprintf("Try installing with: pkg install root-repo && pkg install tshark") } else if IsCommandInPath("apt") { errstr += fmt.Sprintf("Try installing with: apt install tshark") } else if IsCommandInPath("apt-get") { errstr += fmt.Sprintf("Try installing with: apt-get install tshark") } else if IsCommandInPath("yum") { errstr += fmt.Sprintf("Try installing with: yum install wireshark") } else if IsCommandInPath("brew") { errstr += fmt.Sprintf("Try installing with: brew install wireshark") } errstr += "\n" err := gowid.WithKVs(ConfigErr, map[string]interface{}{ "msg": errstr, }) return "", &err } } // Here we know it's in PATH tsharkBin = DirOfPathCommandUnsafe(tsharkBin) return tsharkBin, nil } func RunForExitCode(prog string, args []string, env []string) (int, error) { return RunForStderr(prog, args, env, ioutil.Discard) } func RunForStderr(prog string, args []string, env []string, stderr io.Writer) (int, error) { var err error exitCode := -1 // default bad cmd := exec.Command(prog, args...) if env != nil { cmd.Env = env } cmd.Stdout = ioutil.Discard cmd.Stderr = stderr 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 { var res string // If use-tshark-temp-for-cache is set, use that if profiles.ConfBool("main.use-tshark-temp-for-pcap-cache", false) { tmp, err := TsharkSetting("Temp") if err == nil { res = tmp } } // Otherwise try the user's preference if res == "" { res = profiles.ConfString("main.pcap-cache-dir", "") } if res == "" { res = DefaultPcapDir() } return res } // DefaultPcapDir returns ~/.cache/pcaps by default. Termshark will check a // couple of user settings first before using this. func DefaultPcapDir() string { return path.Join(CacheDir(), "pcaps") } func TSharkBin() string { return profiles.ConfString("main.tshark", "tshark") } func DumpcapBin() string { return profiles.ConfString("main.dumpcap", "dumpcap") } func CapinfosBin() string { return profiles.ConfString("main.capinfos", "capinfos") } // CaptureBin is the binary the user intends to use to capture // packets i.e. with the -i switch. This might be distinct from // DumpcapBin because dumpcap can't capture on extcap interfaces // like randpkt, but while tshark can, it can drop packets more // readily than dumpcap. This value is interpreted as the name // of a binary, resolved against PATH. Note that the default is // termshark - this invokes termshark in a special mode where it // first tries DumpcapBin, then if that fails, TSharkBin - for // the best of both worlds. To detect this, termshark will run // CaptureBin with TERMSHARK_CAPTURE_MODE=1 in the environment, // so when termshark itself is invoked with this in the environment, // it switches to capture mode. func CaptureBin() string { if runtime.GOOS == "windows" { return profiles.ConfString("main.capture-command", DumpcapBin()) } else { return profiles.ConfString("main.capture-command", os.Args[0]) } } // PrivilegedBin returns a capture binary that may require setcap // privileges on Linux. This is a simple UI to cover the fact that // termshark's default capture method is to run dumpcap and tshark // as a fallback. I don't want to tell the user the capture binary // is termshark - that'd be confusing. We know that on Linux, termshark // will run dumpcap first, then fall back to tshark if needed. Only // dumpcap should need access to live interfaces; tshark is needed // for extcap interfaces only. This is used to provide advice to // the user if packet capture fails. func PrivilegedBin() string { cap := CaptureBin() if cap == "termshark" { return DumpcapBin() } else { return cap } } func TailCommand() []string { def := []string{"tail", "-f", "-c", "+0"} if runtime.GOOS == "windows" { def = []string{os.Args[0], "--tail"} } return profiles.ConfStringSlice("main.tail-command", def) } func KeyPressIsPrintable(key gowid.IKey) bool { return unicode.IsPrint(key.Rune()) && key.Modifiers() & ^tcell.ModShift == 0 } type KeyMapping struct { From vim.KeyPress To vim.KeySequence } func AddKeyMapping(km KeyMapping) { mappings := LoadKeyMappings() newMappings := make([]KeyMapping, 0) for _, mapping := range mappings { if mapping.From != km.From { newMappings = append(newMappings, mapping) } } newMappings = append(newMappings, km) SaveKeyMappings(newMappings) } func RemoveKeyMapping(kp vim.KeyPress) { mappings := LoadKeyMappings() newMappings := make([]KeyMapping, 0) for _, mapping := range mappings { if mapping.From != kp { newMappings = append(newMappings, mapping) } } SaveKeyMappings(newMappings) } func LoadKeyMappings() []KeyMapping { mappings := profiles.ConfStringSlice("main.key-mappings", []string{}) res := make([]KeyMapping, 0) for _, mapping := range mappings { pair := strings.Split(mapping, " ") if len(pair) != 2 { log.Warnf("Could not parse vim key mapping (missing separator?): %s", mapping) continue } from := vim.VimStringToKeys(pair[0]) if len(from) != 1 { log.Warnf("Could not parse 'source' vim keypress: %s", pair[0]) continue } to := vim.VimStringToKeys(pair[1]) if len(to) < 1 { log.Warnf("Could not parse 'target' vim keypresses: %s", pair[1]) continue } res = append(res, KeyMapping{From: from[0], To: to}) } return res } func SaveKeyMappings(mappings []KeyMapping) { ser := make([]string, 0, len(mappings)) for _, mapping := range mappings { ser = append(ser, fmt.Sprintf("%v %v", mapping.From, vim.KeySequence(mapping.To))) } profiles.SetConf("main.key-mappings", ser) } func RemoveFromStringSlice(pcap string, comps []string) []string { var newcomps []string for _, v := range comps { if v == pcap { continue } else { newcomps = append(newcomps, v) } } newcomps = append([]string{pcap}, newcomps...) return newcomps } const magicMicroseconds = 0xA1B2C3D4 const versionMajor = 2 const versionMinor = 4 const dlt_en10mb = 1 func WriteEmptyPcap(filename string) error { var buf [24]byte binary.LittleEndian.PutUint32(buf[0:4], magicMicroseconds) binary.LittleEndian.PutUint16(buf[4:6], versionMajor) binary.LittleEndian.PutUint16(buf[6:8], versionMinor) // bytes 8:12 stay 0 (timezone = UTC) // bytes 12:16 stay 0 (sigfigs is always set to zero, according to // http://wiki.wireshark.org/Development/LibpcapFileFormat binary.LittleEndian.PutUint32(buf[16:20], 10000) binary.LittleEndian.PutUint32(buf[20:24], uint32(dlt_en10mb)) err := ioutil.WriteFile(filename, buf[:], 0644) return err } func FileNewerThan(f1, f2 string) (bool, error) { file1, err := os.Open(f1) if err != nil { return false, err } defer file1.Close() file2, err := os.Open(f2) if err != nil { return false, err } defer file2.Close() f1s, err := file1.Stat() if err != nil { return false, err } f2s, err := file2.Stat() if err != nil { return false, err } return f1s.ModTime().After(f2s.ModTime()), nil } func ReadGob(filePath string, object interface{}) error { file, err := os.Open(filePath) if err == nil { defer file.Close() gr, err := gzip.NewReader(file) if err != nil { return err } defer gr.Close() decoder := gob.NewDecoder(gr) err = decoder.Decode(object) } return err } func WriteGob(filePath string, object interface{}) error { file, err := os.Create(filePath) if err == nil { defer file.Close() gzipper := gzip.NewWriter(file) defer gzipper.Close() encoder := gob.NewEncoder(gzipper) err = encoder.Encode(object) } return err } func StringInSlice(a string, list []string) bool { for _, b := range list { if b == a { return true } } return false } // Must succeed - use on internal templates func TemplateToString(tmpl *template.Template, name string, data interface{}) string { var res bytes.Buffer if err := tmpl.ExecuteTemplate(&res, name, data); err != nil { log.Fatal(err) } return res.String() } func StringIsArgPrefixOf(a string, list []string) bool { for _, b := range list { if strings.HasPrefix(a, fmt.Sprintf("%s=", b)) { return true } } return false } func RunOnDoubleTicker(ch <-chan struct{}, fn func(), dur1 time.Duration, dur2 time.Duration, loops int) { ticker := time.NewTicker(dur1) counter := 0 Loop: for { select { case <-ticker.C: fn() counter++ if counter == loops { ticker.Stop() ticker = time.NewTicker(dur2) } case <-ch: ticker.Stop() break Loop } } } func TrackedGo(fn func(), wgs ...*sync.WaitGroup) { for _, wg := range wgs { wg.Add(1) } go func() { for _, wg := range wgs { defer wg.Done() } fn() }() } type IProcess interface { Kill() error Pid() int } func KillIfPossible(p IProcess) error { if p == nil { return nil } err := p.Kill() if errProcessAlreadyFinished(err) { return nil } else { return err } } func errProcessAlreadyFinished(err error) bool { if err == nil { return false } // Terrible hack - but the error isn't published return err.Error() == "os: process already finished" } func SafePid(p IProcess) int { if p == nil { return -1 } return p.Pid() } func SetConvTypes(convs []string) { profiles.SetConf("main.conv-types", convs) } func ConvTypes() []string { defs := []string{"eth", "ip", "ipv6", "tcp", "udp"} ctypes := profiles.ConfStrings("main.conv-types") if len(ctypes) > 0 { z, ok := arrayOperations.Intersect(defs, ctypes) if ok { res, ok := z.Interface().([]string) if ok { return res } } } return defs } func AddToRecentFiles(pcap string) { comps := profiles.ConfStrings("main.recent-files") if len(comps) == 0 || comps[0] != pcap { comps = RemoveFromStringSlice(pcap, comps) if len(comps) > 16 { comps = comps[0 : 16-1] } profiles.SetConf("main.recent-files", comps) } } func AddToRecentFilters(val string) { addToRecent("main.recent-filters", val) } func addToRecent(field string, val string) { comps := profiles.ConfStrings(field) if (len(comps) == 0 || comps[0] != val) && strings.TrimSpace(val) != "" { comps = RemoveFromStringSlice(val, comps) if len(comps) > 64 { comps = comps[0 : 64-1] } profiles.SetConf(field, comps) } } func LoadOffsetFromConfig(name string) ([]resizable.Offset, error) { offsStr := profiles.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 { profiles.DeleteConf("main." + name) } else { offs, err := json.Marshal(offsets) if err != nil { log.Fatal(err) } profiles.SetConf("main."+name, string(offs)) } // Hack to make viper save if I only deleted from the map profiles.SetConf("main.lastupdate", time.Now().String()) } //====================================================================== func ErrLogger(key string, val string) *io.PipeWriter { l := log.StandardLogger() return log.NewEntry(l).WithField(key, val).WriterLevel(log.ErrorLevel) } // KeyValueErrorString returns a string representation of // a gowid KeyValueError intended to be suitable for displaying in // a termshark error dialog. func KeyValueErrorString(err gowid.KeyValueError) string { res := fmt.Sprintf("%v\n\n", err.Cause()) kvs := make([]string, 0, len(err.KeyVals)) ks := make([]string, 0, len(err.KeyVals)) for k := range err.KeyVals { ks = append(ks, k) } sort.Sort(sort.StringSlice(ks)) for _, k := range ks { kvs = append(kvs, fmt.Sprintf("%v: %v", k, err.KeyVals[k])) } res = res + strings.Join(kvs, "\n\n") return res } //====================================================================== // Need to publish fields for template use type JumpPos struct { Summary string `json:"summary"` Pos int `json:"position"` } type GlobalJumpPos struct { JumpPos Filename string `json:"filename"` } // For ease of use in the template func (g GlobalJumpPos) Base() string { return filepath.Base(g.Filename) } type globalJumpPosMapping struct { Key rune `json:"key"` GlobalJumpPos // embedding without a field name makes the json more concise } func LoadGlobalMarks(m map[rune]GlobalJumpPos) error { marksStr := profiles.ConfString("main.marks", "") if marksStr == "" { return nil } mappings := make([]globalJumpPosMapping, 0) err := json.Unmarshal([]byte(marksStr), &mappings) if err != nil { return errors.WithStack(gowid.WithKVs(ConfigErr, map[string]interface{}{ "name": "marks", "msg": "Could not unmarshal marks", })) } for _, mapping := range mappings { m[mapping.Key] = mapping.GlobalJumpPos } return nil } func SaveGlobalMarks(m map[rune]GlobalJumpPos) { marks := make([]globalJumpPosMapping, 0) for k, v := range m { marks = append(marks, globalJumpPosMapping{Key: k, GlobalJumpPos: v}) } if len(marks) == 0 { profiles.DeleteConf("main.marks") } else { marksJ, err := json.Marshal(marks) if err != nil { log.Fatal(err) } profiles.SetConf("main.marks", string(marksJ)) } // Hack to make viper save if I only deleted from the map profiles.SetConf("main.lastupdate", time.Now().String()) } //====================================================================== // IPCompare is a unit type that satisfies ICompare, and can be used // for numerically comparing IP addresses. type IPCompare struct{} func (s IPCompare) Less(i, j string) bool { x := net.ParseIP(i) y := net.ParseIP(j) if x != nil && y != nil { if len(x) != len(y) { return len(x) < len(y) } else { for i := 0; i < len(x); i++ { switch { case x[i] < y[i]: return true case y[i] < x[i]: return false } } return false } } else if x != nil { return true } else if y != nil { return false } else { return i < j } } var _ table.ICompare = IPCompare{} //====================================================================== // MacCompare is a unit type that satisfies ICompare, and can be used // for numerically comparing MAC addresses. type MACCompare struct{} func (s MACCompare) Less(i, j string) bool { x, errx := net.ParseMAC(i) y, erry := net.ParseMAC(j) if errx == nil && erry == nil { for i := 0; i < len(x); i++ { switch { case x[i] < y[i]: return true case y[i] < x[i]: return false } } return false } else if errx == nil { return true } else if erry == nil { return false } else { return i < j } } var _ table.ICompare = MACCompare{} //====================================================================== // ConvPktsCompare is a unit type that satisfies ICompare, and can be used // for numerically comparing values emitted by the tshark -z conv,... e.g. // "2,456 kB" type ConvPktsCompare struct{} func (s ConvPktsCompare) Less(i, j string) bool { mi := unitsRe.FindStringSubmatch(i) if len(mi) <= 2 { return false } mx, err := strconv.ParseUint(strings.Replace(mi[1], ",", "", -1), 10, 64) if err != nil { return false } if mi[2] == "kB" { mx *= 1024 } else if mi[2] == "MB" { mx *= (1024 * 1024) } mj := unitsRe.FindStringSubmatch(j) if len(mj) <= 2 { return false } my, err := strconv.ParseUint(strings.Replace(mj[1], ",", "", -1), 10, 64) if err != nil { return false } if mj[2] == "kB" { my *= 1024 } else if mj[2] == "MB" { my *= (1024 * 1024) } return mx < my } var _ table.ICompare = ConvPktsCompare{} //====================================================================== func PrunePcapCache() error { // This is a new option. Best to err on the side of caution and, if not, present // assume the cache can grow indefinitely - in case users are now relying on this // to keep old pcaps around. I don't want to delete any files without the user's // explicit permission. var diskCacheSize int64 = int64(profiles.ConfInt("main.disk-cache-size-mb", -1)) if diskCacheSize == -1 { log.Infof("No pcap disk cache size set. Skipping cache pruning.") return nil } // Let user use MB as the most sensible unit of disk size. Convert to // bytes for comparing to file sizes. diskCacheSize = diskCacheSize * 1024 * 1024 log.Infof("Pruning termshark's pcap disk cache at %s...", PcapDir()) var totalSize int64 var fileInfos []os.FileInfo err := filepath.Walk(PcapDir(), func(path string, info os.FileInfo, err error) error { if err == nil { totalSize += info.Size() fileInfos = append(fileInfos, info) } return nil }, ) if err != nil { return err } sort.Slice(fileInfos, func(i, j int) bool { return fileInfos[i].ModTime().Before(fileInfos[j].ModTime()) }) filesRemoved := 0 curCacheSize := totalSize for len(fileInfos) > 0 && curCacheSize > diskCacheSize { err = os.Remove(filepath.Join(PcapDir(), fileInfos[0].Name())) if err != nil { log.Warnf("Could not remove pcap cache file %s while pruning - %v", fileInfos[0].Name(), err) } else { curCacheSize = curCacheSize - fileInfos[0].Size() filesRemoved++ } fileInfos = fileInfos[1:] } if filesRemoved > 0 { log.Infof("Pruning complete. Removed %d old pcaps. Cache size is now %d MB", filesRemoved, curCacheSize/(1024*1024)) } else { log.Infof("Pruning complete. No old pcaps removed. Cache size is %d MB", curCacheSize/(1024*1024)) } return nil } //====================================================================== 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, because the indices can then be used to select // an interface to sniff on, and net.Interfaces returns the interfaces in // a different order. func Interfaces() (map[int][]string, error) { cmd := exec.Command(TSharkBin(), "-D") out, err := cmd.Output() if err != nil { return nil, err } return interfacesFrom(bytes.NewReader(out)) } func interfacesFrom(reader io.Reader) (map[int][]string, error) { re := regexp.MustCompile(`^(?P[0-9]+)\.\s+(?P[^\s]+)(\s*\((?P[^)]+)\))?`) res := make(map[int][]string) scanner := bufio.NewScanner(reader) for scanner.Scan() { line := scanner.Text() match := re.FindStringSubmatch(line) if len(match) < 2 { return nil, gowid.WithKVs(UnexpectedOutput, map[string]interface{}{"Output": line}) } result := make(map[string]string) for i, name := range re.SubexpNames() { if i != 0 && match[i] != "" { result[name] = match[i] } } i, err := strconv.ParseInt(result["index"], 10, 32) if err != nil { return nil, gowid.WithKVs(UnexpectedOutput, map[string]interface{}{"Output": line}) } val := make([]string, 0) val = append(val, result["name1"]) if name2, ok := result["name2"]; ok { val = append([]string{name2}, val...) } res[int(i)] = val } return res, nil } //====================================================================== var foldersRE = regexp.MustCompile(`:\s*`) // $ env TMPDIR=/foo tshark -G folders Temp // Temp: /foo // Personal configuration: /home/gcla/.config/wireshark // Global configuration: /usr/share/wireshark // func TsharkSetting(field string) (string, error) { res, err := TsharkSettings(field) if err != nil { return "", err } val, ok := res[field] if !ok { return "", fmt.Errorf("Field %s not found in output of tshark -G folders", field) } return val, nil } func TsharkSettings(fields ...string) (map[string]string, error) { out, err := exec.Command(TSharkBin(), []string{"-G", "folders"}...).Output() if err != nil { return nil, err } res := make(map[string]string) scanner := bufio.NewScanner(strings.NewReader(string(out))) for scanner.Scan() { line := scanner.Text() pieces := foldersRE.Split(line, 2) for _, field := range fields { if len(pieces) == 2 && pieces[0] == field { res[field] = pieces[1] } } } return res, nil } //====================================================================== func WiresharkProfileNames() []string { res := make([]string, 0, 8) folders, _ := TsharkSettings("Personal configuration", "Global configuration") for _, folder := range folders { profFolder := filepath.Join(folder, "profiles") files, err := ioutil.ReadDir(profFolder) if err != nil { log.Warnf("Could not read wireshark config folder %s: %v", profFolder, err) continue } for _, file := range files { if file.IsDir() { res = append(res, file.Name()) } } } return res } //====================================================================== 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 := profiles.ConfStringSlice( "main.browse-command", system.OpenURL, ) if len(urlCmd) == 0 { return errors.WithStack(gowid.WithKVs(BadCommand, map[string]interface{}{"message": "browse command is nil"})) } urlCmdPP, changed := ApplyArguments(urlCmd, []string{url}) if changed == 0 { urlCmdPP = append(urlCmd, url) } cmd := exec.Command(urlCmdPP[0], urlCmdPP[1:]...) return cmd.Run() } //====================================================================== type KeyState struct { NumberPrefix int PartialgCmd bool PartialZCmd bool PartialCtrlWCmd bool PartialmCmd bool PartialQuoteCmd bool } //====================================================================== type ICommandOutput interface { ProcessOutput(output string) error } type ICommandError interface { ProcessCommandError(err error) error } type ICommandDone interface { ProcessCommandDone() } type ICommandKillError interface { ProcessKillError(err error) error } type ICommandTimeout interface { ProcessCommandTimeout() error } type ICommandWaitTicker interface { ProcessWaitTick() error } func CopyCommand(input io.Reader, cb interface{}) error { var err error copyCmd := profiles.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 := profiles.ConfInt("main.copy-command-timeout", 5) if err := cmd.Start(); err != nil { return errors.WithStack(gowid.WithKVs(BadCommand, map[string]interface{}{"err": err})) } TrackedGo(func() { defer func() { if po, ok := cb.(ICommandDone); ok { po.ProcessCommandDone() } }() done := make(chan error, 1) go func() { done <- cmd.Wait() }() tick := time.NewTicker(time.Duration(200) * time.Millisecond) defer tick.Stop() tchan := time.After(time.Duration(cmdTimeout) * time.Second) Loop: for { select { case <-tick.C: if po, ok := cb.(ICommandWaitTicker); ok { err = po.ProcessWaitTick() if err != nil { break Loop } } case <-tchan: if err := cmd.Process.Kill(); err != nil { if po, ok := cb.(ICommandKillError); ok { err = po.ProcessKillError(err) if err != nil { break Loop } } } else { if po, ok := cb.(ICommandTimeout); ok { err = po.ProcessCommandTimeout() if err != nil { break Loop } } } break Loop case err := <-done: if err != nil { if po, ok := cb.(ICommandError); ok { po.ProcessCommandError(err) } } else { if po, ok := cb.(ICommandOutput); ok { outStr := outBuf.String() po.ProcessOutput(outStr) } } break Loop } } }) return nil } // Returns true if error, too func FileSizeDifferentTo(filename string, cur int64) (int64, bool) { var newSize int64 diff := true fi, err := os.Stat(filename) if err == nil { newSize = fi.Size() if cur == newSize { diff = false } } return newSize, diff } func Does256ColorTermExist() error { return ValidateTerm(fmt.Sprintf("%s-256color", os.Getenv("TERM"))) } func ValidateTerm(term string) error { var err error _, err = terminfo.LookupTerminfo(term) if err != nil { _, _, err = dynamic.LoadTerminfo(term) } return err } //====================================================================== // Local Variables: // mode: Go // fill-column: 78 // End: termshark-2.4.0/utils_test.go000066400000000000000000000131321426312004500162400ustar00rootroot00000000000000// Copyright 2019-2022 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" "os" "testing" "github.com/blang/semver" "github.com/gcla/termshark/v2/pkg/format" "github.com/stretchr/testify/assert" ) //====================================================================== func TestApplyArgs(t *testing.T) { cmd := []string{"echo", "something", "$3", "else", "$1", "$3"} args := []string{"a1", "a2"} eres := []string{"echo", "something", "$3", "else", "a1", "$3"} res, total := ApplyArguments(cmd, args) assert.Equal(t, eres, res) assert.Equal(t, total, 1) args = []string{"a1", "a2", "a3"} eres = []string{"echo", "something", "a3", "else", "a1", "a3"} res, total = ApplyArguments(cmd, args) assert.Equal(t, eres, res) assert.Equal(t, total, 3) } func TestArgConv(t *testing.T) { var tests = []struct { arg string flag string val string res bool }{ {"--tshark-d=foo", "d", "foo", true}, {"--tshark-abc=foo", "", "", false}, {"--tshark-V=true", "V", "", true}, {"--tshark-V=false", "", "", false}, {"--ts-V=wow", "", "", false}, } for _, test := range tests { f, v, ok := ConvertArgToTShark(test.arg) assert.Equal(t, test.res, ok) if test.res { assert.Equal(t, test.flag, f) assert.Equal(t, test.val, v) } } } func TestVer1(t *testing.T) { out1 := `TShark (Wireshark) 2.6.6 (Git v2.6.6 packaged as 2.6.6-1~ubuntu18.04.0) Copyright 1998-2019 Gerald Combs and contributors.` v1, err := TSharkVersionFromOutput(out1) assert.NoError(t, err) res, _ := semver.Make("2.6.6") assert.Equal(t, res, v1) } func TestVer2(t *testing.T) { out1 := `TShark 1.6.7 Copyright 1998-2012 Gerald Combs and contributors. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Compiled (64-bit) with GLib 2.32.0, with libpcap (version unknown), with libz 1.2.3.4, with POSIX capabilities (Linux), without libpcre, with SMI 0.4.8, with c-ares 1.7.5, with Lua 5.1, without Python, with GnuTLS 2.12.14, with Gcrypt 1.5.0, with MIT Kerberos, with GeoIP. Running on Linux 3.2.0-126-generic, with libpcap version 1.1.1, with libz 1.2.3.4. ` v1, err := TSharkVersionFromOutput(out1) assert.NoError(t, err) res, _ := semver.Make("1.6.7") assert.Equal(t, res, v1) } func TestInterfaces1(t *testing.T) { out1 := ` 1. \Device\NPF_{BAC1CFBD-DE27-4023-B478-0C490B99DC5E} (Local Area Connection 2) 2. \Device\NPF_{78032B7E-4968-42D3-9F37-287EA86C0AAA} (Local Area Connection* 10) 3. \Device\NPF_{84E7CAE6-E96F-4F31-96FD-170B0F514AB2} (Npcap Loopback Adapter) 4. \Device\NPF_NdisWanIpv6 (NdisWan Adapter) 5. \Device\NPF_{503E1F71-C57C-438D-B004-EA5563723C16} (Local Area Connection 5) 6. \Device\NPF_{15DDE443-C208-4328-8919-9666682EE804} (Local Area Connection* 11) `[1:] interfaces, err := interfacesFrom(bytes.NewReader([]byte(out1))) assert.NoError(t, err) assert.Equal(t, 6, len(interfaces)) v := interfaces[2] assert.Equal(t, `\Device\NPF_{78032B7E-4968-42D3-9F37-287EA86C0AAA}`, v[1]) assert.Equal(t, `Local Area Connection* 10`, v[0]) } func TestInterfaces2(t *testing.T) { out1 := ` 1. eth0 2. ham0 3. docker0 4. vethd45103d 5. lo (Loopback) 6. mpqemubr0-dummy 7. nflog 8. nfqueue 9. bluetooth0 10. virbr0-nic 11. vboxnet0 12. ciscodump (Cisco remote capture) 13. dpauxmon (DisplayPort AUX channel monitor capture) 14. randpkt (Random packet generator) 15. sdjournal (systemd Journal Export) 16. sshdump (SSH remote capture) 17. udpdump (UDP Listener remote capture) `[1:] interfaces, err := interfacesFrom(bytes.NewReader([]byte(out1))) assert.NoError(t, err) assert.Equal(t, 17, len(interfaces)) v := interfaces[3] assert.Equal(t, `docker0`, v[0]) v = interfaces[12] assert.Equal(t, `Cisco remote capture`, v[0]) assert.Equal(t, `ciscodump`, v[1]) } func TestConv1(t *testing.T) { var tests = []struct { arg string res string }{ {"hello\x41world\x42", "helloAworldB"}, {"80 \xe2\x86\x92 53347", "80 → 53347"}, {"hello\x41world\x42 foo \\000 bar", "helloAworldB foo \\000 bar"}, } for _, test := range tests { outs := format.TranslateHexCodes([]byte(test.arg)) assert.Equal(t, string(outs), test.res) } } func TestIPComp1(t *testing.T) { var ip IPCompare assert.True(t, ip.Less("x", "y")) assert.True(t, ip.Less("192.168.0.4", "y")) assert.False(t, ip.Less("y", "192.168.0.4")) assert.True(t, ip.Less("192.168.0.253", "192.168.1.4")) assert.False(t, ip.Less("192.168.1.4", "192.168.0.253")) assert.True(t, ip.Less("192.168.0.253", "::ffff:192.168.1.4")) assert.True(t, ip.Less("::ffff:192.168.0.253", "192.168.1.4")) assert.True(t, ip.Less("192.168.0.253", "2001:db8::68")) assert.False(t, ip.Less("2001:db8::68", "192.168.0.253")) } func TestMACComp1(t *testing.T) { var mac MACCompare assert.True(t, mac.Less("x", "y")) assert.True(t, mac.Less("11:22:33:44:55:66", "y")) assert.True(t, mac.Less("xx:22:33:44:55:66", "y")) assert.False(t, mac.Less("xx:22:33:44:55:66", "11:22:33:44:55:66")) assert.True(t, mac.Less("11:22:33:44:55:66", "11:22:33:44:55:67")) assert.False(t, mac.Less("11:22:33:44:55:66", "11:22:33:44:54:66")) } func TestFolders(t *testing.T) { tmp := os.Getenv("TMPDIR") os.Setenv("TMPDIR", "/foo") defer os.Setenv("TMPDIR", tmp) val, err := TsharkSetting("Temp") assert.NoError(t, err) assert.Equal(t, "/foo", val) val, err = TsharkSetting("Deliberately missing") assert.Error(t, err) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.4.0/version.go000066400000000000000000000005631426312004500155320ustar00rootroot00000000000000// Copyright 2019-2022 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.4.0" //====================================================================== // Local Variables: // indent-tabs-mode: nil // tab-width: 4 // fill-column: 78 // End: termshark-2.4.0/widgets/000077500000000000000000000000001426312004500151605ustar00rootroot00000000000000termshark-2.4.0/widgets/appkeys/000077500000000000000000000000001426312004500166345ustar00rootroot00000000000000termshark-2.4.0/widgets/appkeys/appkeys.go000066400000000000000000000101331426312004500206350ustar00rootroot00000000000000// Copyright 2019-2022 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/v2" ) //====================================================================== 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.4.0/widgets/copymodetable/000077500000000000000000000000001426312004500200075ustar00rootroot00000000000000termshark-2.4.0/widgets/copymodetable/copymodetable.go000066400000000000000000000111021426312004500231600ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package copymodetable provides a wrapper around a table that supports copy mode. // The implementation currently supports clipping a whole row and also the whole // table by providing these as interfaces to the New function. It's easy to imagine // supporting narrowing the copy selection to a single column, but I don't need // that yet... package copymodetable import ( "github.com/gcla/gowid" "github.com/gcla/gowid/widgets/list" "github.com/gcla/gowid/widgets/table" "github.com/gcla/termshark/v2/widgets/withscrollbar" lru "github.com/hashicorp/golang-lru" ) //====================================================================== type IRowCopier interface { CopyRow(id table.RowId) []gowid.ICopyResult } type ITableCopier interface { CopyTable() []gowid.ICopyResult } type ICopyModeTableNeeds interface { gowid.IWidget list.IWalker table.IGoToMiddle withscrollbar.IScrollOneLine withscrollbar.IScrollOnePage CurrentRow() int SetCurrentRow(table.Position) Model() table.IModel SetModel(table.IModel, gowid.IApp) Cache() *lru.Cache OnFocusChanged(gowid.IWidgetChangedCallback) } type Widget struct { ICopyModeTableNeeds RowClip IRowCopier // Knows how to make a clip result set given a row AllClip ITableCopier // Knows how to make a clip result set from the whole table name string // for widget "id" clip gowid.IClipboardSelected // function to modify selected widget for copying } type idstring string // Needed to satisfy copy mode func (i idstring) ID() interface{} { return i } func New(wrapped ICopyModeTableNeeds, rowClip IRowCopier, allClip ITableCopier, name string, clip gowid.IClipboardSelected) *Widget { return &Widget{ ICopyModeTableNeeds: wrapped, RowClip: rowClip, AllClip: allClip, name: name, clip: clip, } } func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { if app.InCopyMode() && app.CopyModeClaimedBy().ID() == w.ID() && focus.Focus { row := w.CurrentRow() if app.CopyModeClaimedAt() == 0 { row = -1 // all rows } origModel := w.Model() model := copyModeTableModel{ IModel: origModel, clip: w.clip, app: app, row: row, } w.SetModel(model, app) res := w.ICopyModeTableNeeds.Render(size, focus, app) w.SetModel(origModel, app) return res } else { return w.ICopyModeTableNeeds.Render(size, focus, app) } } // The app stores which widget claims copy mode, and so each widget must check whether it's the // one when it render itself. func (w *Widget) ID() interface{} { return idstring(w.name) } func (w *Widget) SubWidget() gowid.IWidget { return w.ICopyModeTableNeeds } func (w *Widget) CopyModeLevels() int { return 1 // one row, all rows } func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { return gowid.CopyModeUserInput(w, ev, size, focus, app) } func (w *Widget) Clips(app gowid.IApp) []gowid.ICopyResult { // 1 is whole table // 0 is just row diff := w.CopyModeLevels() - (app.CopyModeClaimedAt() - app.CopyLevel()) var rd []gowid.ICopyResult if diff == 0 { cur := w.CurrentRow() rid, ok := w.Model().RowIdentifier(cur) if ok { rd = w.RowClip.CopyRow(rid) } } else { rd = w.AllClip.CopyTable() } return rd } //====================================================================== // copyModeTableModel exists solely to provide an "overridden" implementation of CellWidgets e.g. to color the // selected row yellow. To do this, it needs clip for the AlterWidget function, and the row to alter (or // all). This model is set on the underlying table before Render() is called on the underlying table. type copyModeTableModel struct { table.IModel clip gowid.IClipboardSelected app gowid.IApp row int } var _ table.IModel = copyModeTableModel{} func (c copyModeTableModel) CellWidgets(row table.RowId) []gowid.IWidget { res := c.IModel.CellWidgets(row) dothisrow := false if c.row == -1 { dothisrow = true // do every row i.e. every call to CellWidgets() } else { rid, ok := c.IModel.RowIdentifier(c.row) if ok && (row == rid) { dothisrow = true } } if dothisrow { for col := 0; col < len(res); col++ { res[col] = c.clip.AlterWidget(res[col], c.app) } } return res } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.4.0/widgets/copymodetree/000077500000000000000000000000001426312004500176575ustar00rootroot00000000000000termshark-2.4.0/widgets/copymodetree/copymodetree.go000066400000000000000000000075051426312004500227140ustar00rootroot00000000000000// Copyright 2019-2022 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/pkg/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.4.0/widgets/enableselected/000077500000000000000000000000001426312004500201175ustar00rootroot00000000000000termshark-2.4.0/widgets/enableselected/enableselected.go000066400000000000000000000032331426312004500234060ustar00rootroot00000000000000// Copyright 2019-2022 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.4.0/widgets/expander/000077500000000000000000000000001426312004500167665ustar00rootroot00000000000000termshark-2.4.0/widgets/expander/expander.go000066400000000000000000000037031426312004500211260ustar00rootroot00000000000000// Copyright 2019-2022 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 ( "fmt" "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() } func (w *Widget) String() string { return fmt.Sprintf("expander[%v]", w.w.IWidget) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.4.0/widgets/fileviewer/000077500000000000000000000000001426312004500173215ustar00rootroot00000000000000termshark-2.4.0/widgets/fileviewer/fileviewer.go000066400000000000000000000037171426312004500220210ustar00rootroot00000000000000// Copyright 2019-2022 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 fileviewer provides a widget to view a text file in a terminal // via a pager program. package fileviewer import ( "fmt" "github.com/gcla/gowid" "github.com/gcla/gowid/widgets/hpadding" "github.com/gcla/gowid/widgets/null" "github.com/gcla/gowid/widgets/pile" "github.com/gcla/gowid/widgets/terminal" "github.com/gcla/gowid/widgets/text" ) //====================================================================== type Options struct { Name string GoToBottom bool Pager string } type Widget struct { gowid.IWidget opt Options } // New - a bit clumsy, UI will always be legit, but error represents terminal failure func New(vfile string, cb gowid.IWidgetChangedCallback, opts ...Options) (*Widget, error) { var opt Options if len(opts) > 0 { opt = opts[0] } var args []string if opt.Pager == "" { if opt.GoToBottom { args = []string{"less", "+G", vfile} } else { args = []string{"less", vfile} } } else { args = []string{"sh", "-c", fmt.Sprintf("%s %s", opt.Pager, vfile)} } var term gowid.IWidget var termC *terminal.Widget var errTerm error termC, errTerm = terminal.New(args) if errTerm != nil { term = null.New() } else { termC.OnProcessExited(cb) term = termC } header := hpadding.New( text.New(fmt.Sprintf("%s - %s", opt.Name, vfile)), gowid.HAlignMiddle{}, gowid.RenderFixed{}, ) main := pile.New([]gowid.IContainerWidget{ &gowid.ContainerWidget{ IWidget: header, D: gowid.RenderWithUnits{U: 2}, }, &gowid.ContainerWidget{ IWidget: term, D: gowid.RenderWithWeight{W: 1.0}, }, }) res := &Widget{ IWidget: main, opt: opt, } return res, errTerm } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.4.0/widgets/filter/000077500000000000000000000000001426312004500164455ustar00rootroot00000000000000termshark-2.4.0/widgets/filter/filter.go000066400000000000000000000536321426312004500202720ustar00rootroot00000000000000// Copyright 2019-2022 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 provides a termshark-specific edit widget which changes // color according to the validity of its input, and which activates a // drop-down menu of possible completions for the term at point. package filter import ( "context" "fmt" "io" "os/exec" "sync" "syscall" "time" "unicode" "github.com/gcla/gowid" "github.com/gcla/gowid/gwutil" "github.com/gcla/gowid/vim" "github.com/gcla/gowid/widgets/button" "github.com/gcla/gowid/widgets/cellmod" "github.com/gcla/gowid/widgets/columns" "github.com/gcla/gowid/widgets/edit" "github.com/gcla/gowid/widgets/framed" "github.com/gcla/gowid/widgets/holder" "github.com/gcla/gowid/widgets/hpadding" "github.com/gcla/gowid/widgets/list" "github.com/gcla/gowid/widgets/menu" "github.com/gcla/gowid/widgets/pile" "github.com/gcla/gowid/widgets/styled" "github.com/gcla/gowid/widgets/text" "github.com/gcla/termshark/v2" "github.com/gcla/termshark/v2/pkg/fields" "github.com/gcla/termshark/v2/widgets/appkeys" "github.com/gdamore/tcell/v2" ) //====================================================================== // 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 var fixed gowid.RenderFixed 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 empty gowid.IWidget // what to display when the filter value's is empty (special state) edCtx context.Context edCancelFn context.CancelFunc edCtxLock sync.Mutex fields fields.IPrefixCompleter // provides completions, given a prefix completionsList *list.Widget // the filter widget replaces the list walker when new completions are generated completionsActivator *activatorWidget // used to disable focus going to drop down completions []string // the current set of completions, used when rendering runthisfilterchan chan *filtStruct filterchangedchan chan *filtStruct quitchan chan struct{} readytorunchan chan struct{} temporarilyDisabled *bool // set to true right after submitting a new filter, so the menu disappears enterPending bool // set to true if the user has hit enter; process if the filter goes to valid before another change. For slow validity processing. *gowid.Callbacks gowid.IsSelectable } var _ gowid.IWidget = (*Widget)(nil) var _ io.Closer = (*Widget)(nil) type IntermediateCB struct{} type ValidCB struct{} type InvalidCB struct{} type EmptyCB struct{} type SubmitCB struct{} type Pos int const ( Left Pos = iota Below Pos = iota ) type Options struct { Completer fields.IPrefixCompleter MenuOpener menu.IOpener Position Pos Validator IValidator MaxCompletions int } type stringNamer string func (f stringNamer) Name() string { return string(f) } func New(name string, opt Options) *Widget { res := &Widget{} ed := edit.New() ed.OnTextSet(gowid.WidgetCallback{"cb", func(app gowid.IApp, w gowid.IWidget) { // every time the filter changes, drop any pending enter - we don't want to // apply a filter to a stale value res.enterPending = false }}) validator := opt.Validator if validator == nil { validator = &DisplayFilterValidator{} } filterList := list.New(list.NewSimpleListWalker([]gowid.IWidget{})) filterActivator := &activatorWidget{ IWidget: filterList, } if opt.MaxCompletions == 0 { opt.MaxCompletions = 20 } menuListBox2 := styled.New( framed.NewUnicode(cellmod.Opaque(filterActivator)), gowid.MakePaletteRef("filter-menu"), ) ign := make([]gowid.IKey, 0, len(vim.AllDownKeys)+len(vim.AllUpKeys)) for _, k := range vim.AllDownKeys { if !termshark.KeyPressIsPrintable(gowid.Key(k)) { ign = append(ign, gowid.Key(k)) } } for _, k := range vim.AllUpKeys { if !termshark.KeyPressIsPrintable(gowid.Key(k)) { ign = append(ign, gowid.Key(k)) } } drop := menu.New(name, menuListBox2, fixed, menu.Options{ IgnoreKeysProvided: true, IgnoreKeys: ign, CloseKeysProvided: true, CloseKeys: []gowid.IKey{}, OpenCloser: opt.MenuOpener, }, ) yOff := 1 if opt.Position == Below { yOff = 0 } site := menu.NewSite(menu.SiteOptions{ Namer: stringNamer("filtersite"), YOffset: yOff, }) cb := gowid.NewCallbacks() onelineEd := appkeys.New(ed, handleEnter(cb, res), appkeys.Options{ ApplyBefore: true, }) valid := styled.New(onelineEd, gowid.MakePaletteRef("filter-valid"), ) invalid := styled.New(onelineEd, gowid.MakePaletteRef("filter-invalid"), ) intermediate := styled.New(onelineEd, gowid.MakePaletteRef("filter-intermediate"), ) empty := styled.New(onelineEd, gowid.MakePaletteRef("filter-empty"), ) var placeholder *holder.Widget placeholder = holder.New(empty) var wrapped gowid.IWidget switch opt.Position { case Below: wrapped = pile.New([]gowid.IContainerWidget{ &gowid.ContainerWidget{IWidget: placeholder, D: gowid.RenderFlow{}}, &gowid.ContainerWidget{IWidget: site, D: fixed}, }) default: wrapped = 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, empty: empty, fields: opt.Completer, completionsList: filterList, completionsActivator: filterActivator, completions: []string{}, filterchangedchan: filterchangedchan, runthisfilterchan: runthisfilterchan, quitchan: quitchan, readytorunchan: readytorunchan, temporarilyDisabled: new(bool), Callbacks: cb, } validcb := &ValidateCB{ Fn: func(app gowid.IApp) { app.Run(gowid.RunFunction(func(app gowid.IApp) { res.validitySite.SetSubWidget(res.valid, app) gowid.RunWidgetCallbacks(res.Callbacks, ValidCB{}, app, res) if res.enterPending { var dummy gowid.IWidget gowid.RunWidgetCallbacks(cb, SubmitCB{}, app, dummy) *res.temporarilyDisabled = true res.enterPending = false } })) }, } invalidcb := &ValidateCB{ Fn: func(app gowid.IApp) { app.Run(gowid.RunFunction(func(app gowid.IApp) { res.validitySite.SetSubWidget(res.invalid, app) gowid.RunWidgetCallbacks(res.Callbacks, InvalidCB{}, app, res) res.enterPending = false })) }, } killedcb := &ValidateCB{ Fn: func(app gowid.IApp) { app.Run(gowid.RunFunction(func(app gowid.IApp) { res.validitySite.SetSubWidget(res.intermediate, app) gowid.RunWidgetCallbacks(res.Callbacks, IntermediateCB{}, app, res) res.enterPending = false })) }, } emptycb := &ValidateCB{ Fn: func(app gowid.IApp) { app.Run(gowid.RunFunction(func(app gowid.IApp) { res.validitySite.SetSubWidget(res.empty, app) gowid.RunWidgetCallbacks(res.Callbacks, EmptyCB{}, app, res) res.enterPending = false })) }, } validator.SetValid(validcb) validator.SetInvalid(invalidcb) validator.SetKilled(killedcb) validator.SetEmpty(emptycb) // 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 emptycb.App = fs.app validator.Validate(fs.txt) } } }, Goroutinewg) ed.OnTextSet(gowid.MakeWidgetCallback("cb2", gowid.WidgetChangedFunction(func(app gowid.IApp, ew gowid.IWidget) { res.UpdateCompletions(app) }))) return res } //====================================================================== type iFilterEnter interface { setDisabled() setEnterPending() isValid() bool } // if the filter is valid when enter is pressed, submit the SubmitCB callback. Those // registered will be able to respond e.g. start handling the valid filter value. func handleEnter(cb *gowid.Callbacks, fe iFilterEnter) appkeys.KeyInputFn { return func(evk *tcell.EventKey, app gowid.IApp) bool { handled := false switch evk.Key() { case tcell.KeyEnter: if fe.isValid() { var dummy gowid.IWidget gowid.RunWidgetCallbacks(cb, SubmitCB{}, app, dummy) fe.setDisabled() } else { fe.setEnterPending() // remember in case the filter goes valid shortly } handled = true } return handled } } func isValidFilterRune(r rune) bool { res := true switch { case unicode.IsLetter(r): case unicode.IsNumber(r): case r == '-': case r == '_': case r == '.': default: res = false } return res } func newMenuWidgets(ed *edit.Widget, completions []string) []gowid.IWidget { menu2Widgets := make([]gowid.IWidget, 0) for _, s := range completions { scopy := s clickme := button.New( hpadding.New( text.New(s), gowid.HAlignLeft{}, gowid.RenderWithUnits{U: gwutil.Max(12, len(s))}, ), button.Options{ Decoration: button.BareDecoration, SelectKeysProvided: true, SelectKeys: []gowid.IKey{gowid.MakeKeyExt(tcell.KeyEnter)}, }, ) clickmeStyled := styled.NewInvertedFocus(clickme, gowid.MakePaletteRef("filter-menu")) 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 _ fields.IPrefixCompleterCallback = fnCallback{} func (f fnCallback) Call(res []string) { f.fn(res, f.app) } func makeCompletions(comp fields.IPrefixCompleter, txt string, max int, app gowid.IApp, fn func([]string, gowid.IApp)) { if comp != nil { cb := fnCallback{ app: app, fn: func(completions []string, app gowid.IApp) { completions = completions[0:gwutil.Min(max, len(completions))] fn(completions, app) }, } comp.Completions(txt, cb) } } func (w *Widget) setDisabled() { *w.temporarilyDisabled = true } func (w *Widget) setEnterPending() { w.enterPending = true } // isCurrentlyValid returns true if the current state of the filter is valid (green) func (w *Widget) isValid() bool { return w.validitySite.SubWidget() == w.valid } // Start an asynchronous routine to update the drop-down menu with completion // options. Runs on a small delay so it can be cancelled and restarted if the // user is typing quickly. func (w *Widget) UpdateCompletions(app gowid.IApp) { app.Run(gowid.RunFunction(func(app gowid.IApp) { if w.ed.Text() != "" { w.validitySite.SetSubWidget(w.intermediate, app) gowid.RunWidgetCallbacks(w.Callbacks, IntermediateCB{}, app, w) } })) // UpdateCompletions can be called outside of the app goroutine, so we // need to protect the context w.edCtxLock.Lock() defer w.edCtxLock.Unlock() if w.edCancelFn != nil { w.edCancelFn() } w.edCtx, w.edCancelFn = context.WithCancel(context.Background()) // don't kick things off right away in case user is typing fast go func(ctx context.Context) { select { case <-ctx.Done(): return case <-time.After(time.Millisecond * 200): break } // Send the value to be run by tshark. This will kill any other one in progress. w.filterchangedchan <- &filtStruct{w.ed.Text(), app} app.Run(gowid.RunFunction(func(app gowid.IApp) { _, y := app.GetScreen().Size() txt := w.ed.Text() end := w.ed.CursorPos() start := end Loop: for { if start == 0 { break } start-- if !isValidFilterRune(rune(txt[start])) { start++ break Loop } } makeCompletions(w.fields, txt[start:end], y, app, func(completions []string, app gowid.IApp) { app.Run(gowid.RunFunction(func(app gowid.IApp) { w.processCompletions(completions, app) })) }) })) }(w.edCtx) } func (w *Widget) processCompletions(completions []string, app gowid.IApp) { max := w.opts.MaxCompletions for _, c := range completions { max = gwutil.Max(max, len(c)) } menu2Widgets := newMenuWidgets(w.ed, completions) w.completions = completions app.Run(gowid.RunFunction(func(app gowid.IApp) { w.completionsList.SetWalker(list.NewSimpleListWalker(menu2Widgets), app) // whenever there's an update, take focus away from drop down. This means enter // can be used to submit a new filter. w.completionsActivator.active = false w.dropDown.SetWidth(gowid.RenderWithUnits{U: max + 2}, app) // This makes for a better experience. The menu is rendered as a box because an // explicit height is set; this results in the overlay either rendering as the // full-height box requested; or if there's not enough vertical room, a shorter // box. Either way, the list will render in the space provided (and the frame), // and scroll if necessary. This means the menu isn't cut off at the bottom of // the screen. This assumes I'm not displaying the individual widgets in flow // mode because then each might take more than one line if len(w.completions) >= 0 { // account for the frame... w.dropDown.SetHeight(gowid.RenderWithUnits{U: len(w.completions) + 2}, app) } else { w.dropDown.SetHeight(fixed, 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) OnEmpty(f gowid.IWidgetChangedCallback) { gowid.AddWidgetCallback(w, EmptyCB{}, f) } func (w *Widget) IsValid() bool { return w.validitySite.SubWidget() == w.valid } func (w *Widget) Value() string { return w.ed.Text() } func (w *Widget) SetValue(v string, app gowid.IApp) { w.ed.SetText(v, app) w.ed.SetCursorPos(len(v), app) } func (w *Widget) Menus() []gowid.IMenuCompatible { return []gowid.IMenuCompatible{w.dropDown} } func (w *Widget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { return gowid.RenderSize(w.wrapped, size, focus, app) } func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { // It can be disabled if e.g. the user's last input caused the filter value to // be submitted. Then the best UX is to not display the drop down until further input // or cursor movement. if w.opts.MenuOpener != nil { if focus.Focus && len(w.completions) > 0 && !*w.temporarilyDisabled { w.opts.MenuOpener.OpenMenu(w.dropDown, w.dropDownSite, app) } else { w.opts.MenuOpener.CloseMenu(w.dropDown, app) } } return w.wrapped.Render(size, focus, app) } // Reject tab because I want it to switch views. Not intended to be transferable. Reject down because // accepting it triggers the OnCursorSet callback, which re-evaluates the filter value - the user sees // it go orange briefly, which is unpleasant. func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { if evk, ok := ev.(*tcell.EventKey); ok { if evk.Key() == tcell.KeyTAB || (vim.KeyIn(evk, vim.AllDownKeys) && !termshark.KeyPressIsPrintable(evk)) { return false } } *w.temporarilyDisabled = false // any input should start the appearance of the drop down again return w.wrapped.UserInput(ev, size, focus, app) } //====================================================================== // activatorWidget is intended to wrap a ListBox, and will suppress focus to the listbox by // default, which has the effect of not highlighting any listbox items. The intended effect // is for the cursor to be "above" the first item. When the user hits down, then focus // is passed through, so the top item is highlighted. If the key pressed is up, and the // listbox doesn't handle it, that must mean it's at the top of its range, so the effect is // start suppressing focus again. type activatorWidget struct { gowid.IWidget active bool } func (w *activatorWidget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { if _, ok := ev.(*tcell.EventPaste); ok && !w.active { return false } if ev, ok := ev.(*tcell.EventKey); ok && !w.active { if vim.KeyIn(ev, vim.AllDownKeys) && !termshark.KeyPressIsPrintable(ev) { w.active = true return true } else { return false } } res := w.IWidget.UserInput(ev, size, focus, app) if !res { if ev, ok := ev.(*tcell.EventKey); ok && w.active { if vim.KeyIn(ev, vim.AllUpKeys) && !termshark.KeyPressIsPrintable(ev) { w.active = false return true } else { return false } } } return res } func (w *activatorWidget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { newf := focus if !w.active { newf = gowid.NotSelected } return w.IWidget.Render(size, newf, app) } //====================================================================== // IValidator is passed to the filter constructor type IValidator interface { SetValid(cb IValidateCB) SetInvalid(cb IValidateCB) SetKilled(cb IValidateCB) SetEmpty(cv IValidateCB) Kill() (bool, error) Validate(filter string) } //====================================================================== 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 DisplayFilterValidator struct { Valid IValidateCB Invalid IValidateCB KilledCB IValidateCB EmptyCB IValidateCB Cmd *exec.Cmd } var _ IValidator = (*DisplayFilterValidator)(nil) func (f *DisplayFilterValidator) SetValid(cb IValidateCB) { f.Valid = cb } func (f *DisplayFilterValidator) SetInvalid(cb IValidateCB) { f.Invalid = cb } func (f *DisplayFilterValidator) SetKilled(cb IValidateCB) { f.KilledCB = cb } func (f *DisplayFilterValidator) SetEmpty(cb IValidateCB) { f.EmptyCB = cb } func (f *DisplayFilterValidator) 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 *DisplayFilterValidator) Validate(filter string) { var err error if filter == "" { if f.EmptyCB != nil { f.EmptyCB.Call(filter) } return } 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) } } } } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.4.0/widgets/framefocus/000077500000000000000000000000001426312004500173125ustar00rootroot00000000000000termshark-2.4.0/widgets/framefocus/framefocus.go000066400000000000000000000035441426312004500220010ustar00rootroot00000000000000// Copyright 2019-2022 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.4.0/widgets/hexdumper/000077500000000000000000000000001426312004500171615ustar00rootroot00000000000000termshark-2.4.0/widgets/hexdumper/hexdumper.go000066400000000000000000000347401426312004500215210ustar00rootroot00000000000000// Copyright 2019-2022 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/pkg/format" "github.com/gcla/termshark/v2/widgets/renderfocused" "github.com/gdamore/tcell/v2" "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.4.0/widgets/hexdumper/hexdumper_test.go000066400000000000000000000021241426312004500225470ustar00rootroot00000000000000// Copyright 2019-2022 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.4.0/widgets/hexdumper2/000077500000000000000000000000001426312004500172435ustar00rootroot00000000000000termshark-2.4.0/widgets/hexdumper2/hexdumper2.go000066400000000000000000000446411426312004500216660ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package hexdumper2 provides a widget which displays selectable hexdump-like // output. Because it's built for termshark, it also allows styling to be // applied to ranges of data intended to correspond to packet structure selected // in another termshark view. package hexdumper2 import ( "encoding/hex" "fmt" "io" "github.com/gcla/gowid" "github.com/gcla/gowid/gwutil" "github.com/gcla/gowid/vim" "github.com/gcla/gowid/widgets/list" "github.com/gcla/gowid/widgets/styled" "github.com/gcla/termshark/v2/pkg/format" "github.com/gdamore/tcell/v2" ) //====================================================================== type LayerStyler struct { Start int End int ColUnselected string ColSelected string } type PositionChangedCB struct{} //====================================================================== type Options struct { StyledLayers []LayerStyler CursorUnselected string CursorSelected string LineNumUnselected string LineNumSelected string PaletteIfCopying string DownKeys []vim.KeyPress UpKeys []vim.KeyPress LeftKeys []vim.KeyPress RightKeys []vim.KeyPress } type Widget struct { data []byte layers []LayerStyler position int cursorUnselected string cursorSelected string lineNumUnselected string lineNumSelected string offset int // scroll position, bit of a hack adjustOffsetsNextRender bool // adjust offsets next render to keep position in view e.g. after SetPosition paletteIfCopying string opt Options gowid.AddressProvidesID styled.UsePaletteIfSelectedForCopy Callbacks *gowid.Callbacks gowid.IsSelectable } var _ gowid.IWidget = (*Widget)(nil) var _ gowid.IIdentityWidget = (*Widget)(nil) var _ gowid.IClipboard = (*Widget)(nil) var _ gowid.IClipboardSelected = (*Widget)(nil) func New(data []byte, opts ...Options) *Widget { var opt Options if len(opts) > 0 { opt = opts[0] } if opt.DownKeys == nil { opt.DownKeys = vim.AllDownKeys } if opt.UpKeys == nil { opt.UpKeys = vim.AllUpKeys } if opt.LeftKeys == nil { opt.LeftKeys = vim.AllLeftKeys } if opt.RightKeys == nil { opt.RightKeys = vim.AllRightKeys } res := &Widget{ data: data, layers: opt.StyledLayers, cursorUnselected: opt.CursorUnselected, cursorSelected: opt.CursorSelected, lineNumUnselected: opt.LineNumUnselected, lineNumSelected: opt.LineNumSelected, paletteIfCopying: opt.PaletteIfCopying, UsePaletteIfSelectedForCopy: styled.UsePaletteIfSelectedForCopy{Entry: opt.PaletteIfCopying}, opt: opt, Callbacks: gowid.NewCallbacks(), } return res } func (w *Widget) OnPositionChanged(f gowid.IWidgetChangedCallback) { gowid.AddWidgetCallback(w.Callbacks, PositionChangedCB{}, f) } func (w *Widget) RemoveOnPositionChanged(f gowid.IIdentity) { gowid.RemoveWidgetCallback(w.Callbacks, PositionChangedCB{}, f) } func (w *Widget) String() string { return "hexdump2" } func (w *Widget) CursorUnselected() string { return w.cursorUnselected } func (w *Widget) CursorSelected() string { return w.cursorSelected } func (w *Widget) LineNumUnselected() string { return w.lineNumUnselected } func (w *Widget) LineNumSelected() string { return w.lineNumSelected } func (w *Widget) Layers() []LayerStyler { return w.layers } func (w *Widget) SetLayers(layers []LayerStyler, app gowid.IApp) { w.layers = layers } func (w *Widget) Data() []byte { return w.data } func (w *Widget) SetData(data []byte, app gowid.IApp) { w.data = data } func (w *Widget) Position() int { return w.position } func (w *Widget) SetPosition(pos int, app gowid.IApp) { curpos := w.Position() w.position = pos w.adjustOffsetsNextRender = true if curpos != pos { gowid.RunWidgetCallbacks(w.Callbacks, PositionChangedCB{}, app, w) } } func (w *Widget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { // 1<-4><3><----------(8 * 3)-1---<2<-------(8 * 3)-1-----><3><---8-->1<---8--> var cols int if sz, ok := size.(gowid.IColumns); ok { cols = sz.Columns() } else { cols = 1 + 4 + 3 + ((8 * 3) - 1) + 2 + ((8 * 3) - 1) + 3 + 8 + 1 + 8 } var rows int if box, ok := size.(gowid.IRows); ok { rows = box.Rows() } else { rows = (len(w.data) + 15) / 16 } return gowid.MakeRenderBox(cols, rows) } // 1<-4><3><----------(8 * 3)-1---<2<-------(8 * 3)-1-----><3><---8-->1<---8--> // 0660 72 6f 72 73 2e 57 69 74 68 53 74 61 63 6b 28 67 rors.Wit hStack(g // 0670 6f 77 69 64 2e 57 69 74 68 4b 56 73 28 4f 70 65 owid.Wit hKVs(Ope // 0680 6e 45 72 72 6f 72 2c 20 6d 61 70 5b 73 74 72 69 nError, map[stri // 0690 6e 67 5d 69 6e 74 65 72 66 61 63 65 7b 7d 7b 0a ng]inter face{}{. // 06a0 09 09 09 09 22 64 65 73 63 72 69 70 74 6f 72 22 ...."des criptor" // 06b0 3a 20 6e 65 77 73 74 64 69 6e 2c 0a 09 09 09 09 : newstd in,..... // func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { var canvasRows int if box, ok := size.(gowid.IRows); ok { canvasRows = box.Rows() } else { canvasRows = (len(w.data) + 15) / 16 } if canvasRows == 0 { return gowid.NewCanvas() } // -1 means not copy mode. 0 means the whole hexdump. 2 means the smallest layer, 1 the next biggest diff := -1 if app.InCopyMode() && app.CopyModeClaimedBy().ID() == w.ID() && focus.Focus { diff = app.CopyModeClaimedAt() - app.CopyLevel() } cols := 1 + 4 + 3 + ((8 * 3) - 1) + 2 + ((8 * 3) - 1) + 3 + 8 + 1 + 8 ccols := cols if sz, ok := size.(gowid.IColumns); ok { ccols = gwutil.Max(ccols, sz.Columns()) } c := gowid.NewCanvasOfSize(ccols, canvasRows) // Adjust in case it's been set too high e.g. via a scrollbar drows := (len(w.data) + 15) / 16 if w.offset >= drows { w.offset = drows } rows := gwutil.Min(canvasRows, drows) if w.adjustOffsetsNextRender { if w.Position() < w.offset*16 { w.offset = w.Position() / 16 } if w.Position() >= ((canvasRows + w.offset) * 16) { w.offset = (w.Position() / 16) - (canvasRows - 1) } w.adjustOffsetsNextRender = false } var lineNumStyle convertedStyle var cursorStyle convertedStyle var copyModeStyle convertedStyle if focus.Focus { lineNumStyle = convertStyle(gowid.MakePaletteRef(w.LineNumSelected()), app) cursorStyle = convertStyle(gowid.MakePaletteRef(w.CursorSelected()), app) } else { lineNumStyle = convertStyle(gowid.MakePaletteRef(w.LineNumUnselected()), app) cursorStyle = convertStyle(gowid.MakePaletteRef(w.CursorUnselected()), app) } copyModeStyle = convertStyle(gowid.MakePaletteRef(w.Entry), app) twoByteWriter := CanvasSlice{ C: c, } asciiWriter := CanvasSlice{ C: c, } var active *convertedStyle // for styling the hex data "41" and the ascii "A" var spactive *convertedStyle // for styling the spaces between data e.g. "41 42" // nil // [1, 5] // [2, 3] // // Deliberately add a blank layer at the beginning for index 0 layers := w.Layers() var layer *LayerStyler layerStyles := make([]convertedLayer, len(layers)) for i := 0; i < len(layers); i++ { layerStyles[i].u = convertStyle(gowid.MakePaletteRef(layers[i].ColUnselected), app) layerStyles[i].s = convertStyle(gowid.MakePaletteRef(layers[i].ColSelected), app) } var i int Loop: for row := w.offset; row < rows+w.offset; row++ { twoByteWriter.XOffset = 1 + 4 + 3 asciiWriter.XOffset = 1 + 4 + 3 + ((8 * 3) - 1) + 2 + ((8 * 3) - 1) + 3 for col := 0; col < 16; col++ { i = (row * 16) + col if i == len(w.data) { break Loop } active = nil spactive = nil if w.Position() == i { if diff != -1 { active = ©ModeStyle } else { active = &cursorStyle } } else { for j := 0; j < len(layers); j++ { layer = &layers[j] if i >= layer.Start && i < layer.End { if j+1 == diff { active = ©ModeStyle break } else { if focus.Focus { active = &layerStyles[j].s } else { active = &layerStyles[j].u } } } } } for j := 0; j < len(layers); j++ { layer = &layers[j] if i >= layer.Start && i < layer.End-1 { if j+1 == diff { spactive = ©ModeStyle break } else { if focus.Focus { spactive = &layerStyles[j].s } else { spactive = &layerStyles[j].u } } } } fmt.Fprintf(twoByteWriter, "%02x", w.data[i]) if active != nil { styleAt(c, twoByteWriter.XOffset, twoByteWriter.YOffset, *active) styleAt(c, twoByteWriter.XOffset+1, twoByteWriter.YOffset, *active) } if spactive != nil { styleAt(c, twoByteWriter.XOffset+2, twoByteWriter.YOffset, *spactive) if col == 7 { styleAt(c, twoByteWriter.XOffset+3, twoByteWriter.YOffset, *spactive) } } twoByteWriter.XOffset += 3 if col == 7 { twoByteWriter.XOffset += 1 } ch := '.' r := w.data[i] if r >= 32 && r <= 126 { ch = rune(byte(r)) } fmt.Fprintf(asciiWriter, "%c", ch) if active != nil { styleAt(c, asciiWriter.XOffset, asciiWriter.YOffset, *active) } if spactive != nil && col == 7 { styleAt(c, asciiWriter.XOffset+1, asciiWriter.YOffset, *spactive) } asciiWriter.XOffset += 1 if col == 7 { asciiWriter.XOffset += 1 } } twoByteWriter.YOffset += 1 asciiWriter.YOffset += 1 } lineNumWriter := CanvasSlice{ C: c, XOffset: 1, YOffset: 0, } Loop2: for k, rk := w.offset, 0; k < rows+w.offset; k, rk = k+1, rk+1 { if k*16 >= len(w.data) { break Loop2 } fmt.Fprintf(lineNumWriter, "%04x", k*16) lineNumWriter.YOffset += 1 active := false for _, layer := range layers { if (k+1)*16 >= layer.Start && k*16 < layer.End { active = true break } } if active { for x := 0; x < 6; x++ { styleAt(c, x, rk, lineNumStyle) } } } if sz, ok := size.(gowid.IColumns); ok { c.TrimRight(sz.Columns()) } if diff == 0 { gowid.RangeOverCanvas(c, gowid.CellRangeFunc(func(cell gowid.Cell) gowid.Cell { return cell.WithBackgroundColor(copyModeStyle.b).WithForegroundColor(copyModeStyle.f).WithStyle(copyModeStyle.s) })) } return c } func clipsForBytes(data []byte, start int, end int) []gowid.ICopyResult { dump := hex.Dump(data[start:end]) dump2 := format.MakeEscapedString(data[start:end]) dump3 := format.MakePrintableString(data[start:end]) dump4 := format.MakeHexStream(data[start:end]) return []gowid.ICopyResult{ gowid.CopyResult{ Name: fmt.Sprintf("Copy bytes %d-%d as hex + ascii", start, end), Val: dump, }, gowid.CopyResult{ Name: fmt.Sprintf("Copy bytes %d-%d as escaped string", start, end), Val: dump2, }, gowid.CopyResult{ Name: fmt.Sprintf("Copy bytes %d-%d as printable string", start, end), Val: dump3, }, gowid.CopyResult{ Name: fmt.Sprintf("Copy bytes %d-%d as hex stream", start, end), Val: dump4, }, } } func (w *Widget) CopyModeLevels() int { return len(w.layers) } func (w *Widget) Clips(app gowid.IApp) []gowid.ICopyResult { diff := app.CopyModeClaimedAt() - app.CopyLevel() if diff == 0 { return clipsForBytes(w.Data(), 0, len(w.Data())) } else { return clipsForBytes(w.Data(), w.layers[diff-1].Start, w.layers[diff-1].End) } } func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { return gowid.CopyModeUserInput(forCopyModeWidget{forUserInputWidget{Widget: w}}, ev, size, focus, app) } type forCopyModeWidget struct { forUserInputWidget } // CopyModeUserInput calls UserInput() on w.SubWidget() - which is this. Then... func (w forCopyModeWidget) SubWidget() gowid.IWidget { return w.forUserInputWidget } type forUserInputWidget struct { *Widget } // ... UserInput is sent to the hexdumper's UserInput logic. func (w forUserInputWidget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { return w.Widget.realUserInput(ev, size, focus, app) } // Reject tab because I want it to switch views. Not intended to be transferable. func (w *Widget) realUserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { res := false scrollDown := false scrollUp := false moveCursor := true switch ev := ev.(type) { case *tcell.EventKey: switch { case vim.KeyIn(ev, w.opt.RightKeys): //res = Scroll(w, 1, w.Wrap(), app) pos := w.Position() if pos < len(w.data) { w.SetPosition(pos+1, app) res = true } case vim.KeyIn(ev, w.opt.LeftKeys): pos := w.Position() if pos > 0 { w.SetPosition(pos-1, app) res = true } case vim.KeyIn(ev, w.opt.DownKeys): scrollDown = true case vim.KeyIn(ev, w.opt.UpKeys): scrollUp = true } case *tcell.EventMouse: switch ev.Buttons() { case tcell.WheelDown: scrollDown = true moveCursor = false case tcell.WheelUp: scrollUp = true moveCursor = false 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 + w.offset) * 16) + xp if pos < len(w.data) { w.SetPosition(pos, app) res = true } } } } pos := w.Position() atBottom := false atTop := false var canvasRows int if box, ok := size.(gowid.IRows); ok { canvasRows = box.Rows() } else { canvasRows = (len(w.data) + 15) / 16 } atTop = ((pos / 16) == w.offset) atBottom = ((pos / 16) == (w.offset + canvasRows - 1)) if scrollDown { if moveCursor { if pos+16 < len(w.data) { w.SetPosition(pos+16, app) if atBottom { w.offset += 1 } res = true } } else { w.offset += 1 if w.offset > (len(w.data)/16)-(canvasRows-1) { w.offset -= 1 } res = true } } else if scrollUp { if moveCursor { if pos-16 >= 0 { w.SetPosition(pos-16, app) if atTop { w.offset -= 1 } res = true } } else { w.offset -= 1 if w.offset < 0 { w.offset = 0 } res = true } } return res } // Implement withscrollbar.IScrollValues func (t *Widget) ScrollLength() int { return (len(t.data) + 15) / 16 } // Implement withscrollbar.IScrollValues func (t *Widget) ScrollPosition() int { return t.Position() / 16 } // Implements withscrollbar.iSetPosition. Note that position is a row. func (t *Widget) SetPos(pos list.IBoundedWalkerPosition, app gowid.IApp) { t.offset = pos.ToInt() } // Can leave the cursor out of sight func (t *Widget) Up(lines int, size gowid.IRenderSize, app gowid.IApp) { t.offset -= lines if t.offset < 0 { t.offset = 0 } } // Adjusts cursor pos too func (t *Widget) GoHome(size gowid.IRenderSize, app gowid.IApp) { t.offset = 0 t.SetPosition(0, app) } func (t *Widget) GoToEnd(size gowid.IRenderSize, app gowid.IApp) { var canvasRows int if box, ok := size.(gowid.IRows); ok { canvasRows = box.Rows() } else { canvasRows = (len(t.data) + 15) / 16 } dataRows := (len(t.data) + 15) / 16 t.offset += gwutil.Max(0, dataRows-(canvasRows+t.offset)) t.SetPosition(len(t.data)-1, app) } // Can leave the cursor out of sight func (t *Widget) Down(lines int, size gowid.IRenderSize, app gowid.IApp) { var canvasRows int if box, ok := size.(gowid.IRows); ok { canvasRows = box.Rows() } else { canvasRows = (len(t.data) + 15) / 16 } dataRows := (len(t.data) + 15) / 16 t.offset += gwutil.Max(0, gwutil.Min(lines, dataRows-(canvasRows+t.offset))) } func (t *Widget) DownPage(num int, size gowid.IRenderSize, app gowid.IApp) { units := 1 if size, ok := size.(gowid.IRows); ok { units = size.Rows() } for i := 0; i < num; i++ { t.Down(units, size, app) } } func (t *Widget) UpPage(num int, size gowid.IRenderSize, app gowid.IApp) { units := 1 if size, ok := size.(gowid.IRows); ok { units = size.Rows() } for i := 0; i < num; i++ { t.Up(units, size, app) } } //====================================================================== // Optimization - convert the styles for use in the canvas once per call // to Render() type convertedStyle struct { f gowid.TCellColor b gowid.TCellColor s gowid.StyleAttrs } type convertedLayer struct { u convertedStyle s convertedStyle } func convertStyle(style gowid.ICellStyler, app gowid.IApp) convertedStyle { f, b, s := style.GetStyle(app) f1 := gowid.IColorToTCell(f, gowid.ColorNone, app.GetColorMode()) b1 := gowid.IColorToTCell(b, gowid.ColorNone, app.GetColorMode()) return convertedStyle{ f: f1, b: b1, s: s, } } //====================================================================== type CanvasSlice struct { C *gowid.Canvas XOffset int YOffset int } var _ io.Writer = CanvasSlice{} var _ gowid.IRangeOverCanvas = CanvasSlice{} func NewCanvasSlice(c *gowid.Canvas, xoff, yoff int) CanvasSlice { res := CanvasSlice{ C: c, XOffset: xoff, YOffset: yoff, } return res } func (c CanvasSlice) Write(p []byte) (n int, err error) { return gowid.WriteToCanvas(c, p) } func (c CanvasSlice) ImplementsWidgetDimension() {} func (c CanvasSlice) BoxColumns() int { return c.C.BoxColumns() } func (c CanvasSlice) BoxRows() int { return c.C.BoxRows() } func (c CanvasSlice) CellAt(col, row int) gowid.Cell { return c.C.CellAt(col+c.XOffset, row+c.YOffset) } func (c CanvasSlice) SetCellAt(col, row int, cell gowid.Cell) { c.C.SetCellAt(col+c.XOffset, row+c.YOffset, cell) } func styleAt(canvas gowid.ICanvas, col int, row int, st convertedStyle) { c := canvas.CellAt(col, row) c = c.MergeDisplayAttrsUnder(c.WithForegroundColor(st.f).WithBackgroundColor(st.b).WithStyle(st.s)) canvas.SetCellAt(col, row, c) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.4.0/widgets/ifwidget/000077500000000000000000000000001426312004500167625ustar00rootroot00000000000000termshark-2.4.0/widgets/ifwidget/ifwidget.go000066400000000000000000000042231426312004500211140ustar00rootroot00000000000000// Copyright 2019-2022 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.4.0/widgets/keepselected/000077500000000000000000000000001426312004500176155ustar00rootroot00000000000000termshark-2.4.0/widgets/keepselected/keepselected.go000066400000000000000000000027211426312004500226030ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package keepselected turns on the selected bit when Render or UserInput is called. package keepselected import "github.com/gcla/gowid" // A widget to ensure that its subwidget is always rendered as "selected", even if it's // not in focus. This allows a composite widget to style its selected child even without // focus so the user can see which child is active. type Widget struct { sub gowid.IWidget } func New(w gowid.IWidget) *Widget { return &Widget{ sub: w, } } func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { return w.sub.Render(size, focus.SelectIf(true), app) } func (w *Widget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { return w.sub.RenderSize(size, focus.SelectIf(true), app) } func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { return w.sub.UserInput(ev, size, focus.SelectIf(true), app) } func (w *Widget) Selectable() bool { return w.sub.Selectable() } func (w *Widget) SubWidget() gowid.IWidget { return w.sub } func (w *Widget) SetSubWidget(wi gowid.IWidget, app gowid.IApp) { w.sub = wi } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.4.0/widgets/mapkeys/000077500000000000000000000000001426312004500166315ustar00rootroot00000000000000termshark-2.4.0/widgets/mapkeys/mapkeys.go000066400000000000000000000042551426312004500206370ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package mapkeys provides a widget that can map one keypress to a sequence of // keypresses. If the user pnovides as input a key that is mapped, the sequence of // resulting keypresses is played to the subwidget before control returns. If the key is // not mapped, it is passed through as normal. I'm going to use this to provide a vim-like // macro feature in termshark. package mapkeys import ( "github.com/gcla/gowid" "github.com/gcla/gowid/vim" "github.com/gdamore/tcell/v2" ) //====================================================================== type Widget struct { gowid.IWidget kmap map[vim.KeyPress]vim.KeySequence } var _ gowid.IWidget = (*Widget)(nil) func New(w gowid.IWidget) *Widget { res := &Widget{ IWidget: w, kmap: make(map[vim.KeyPress]vim.KeySequence), } return res } func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { switch ev := ev.(type) { case *tcell.EventKey: kp := vim.KeyPressFromTcell(ev) if seq, ok := w.kmap[kp]; ok { var res bool for _, vk := range seq { k := gowid.Key(vk) // What should the handled value be?? res = w.IWidget.UserInput(tcell.NewEventKey(k.Key(), k.Rune(), k.Modifiers()), size, focus, app) } return res } else { return w.IWidget.UserInput(ev, size, focus, app) } default: return w.IWidget.UserInput(ev, size, focus, app) } } func (w *Widget) AddMapping(from vim.KeyPress, to vim.KeySequence, app gowid.IApp) { w.kmap[from] = to } func (w *Widget) RemoveMapping(from vim.KeyPress, app gowid.IApp) { delete(w.kmap, from) } // ClearMappings will remove all mappings. I deliberately preserve the same dictionary, // though in case I decide in the future it's useful to let clients have direct access to // the map (and so maybe store it somewhere). func (w *Widget) ClearMappings(app gowid.IApp) { for k := range w.kmap { delete(w.kmap, k) } } //====================================================================== // Local Variables: // mode: Go // fill-column: 90 // End: termshark-2.4.0/widgets/minibuffer/000077500000000000000000000000001426312004500173065ustar00rootroot00000000000000termshark-2.4.0/widgets/minibuffer/minibuffer.go000066400000000000000000000331111426312004500217620ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package minibuffer todo package minibuffer import ( "regexp" "sort" "strings" "github.com/gcla/gowid" "github.com/gcla/gowid/widgets/button" "github.com/gcla/gowid/widgets/dialog" "github.com/gcla/gowid/widgets/edit" "github.com/gcla/gowid/widgets/holder" "github.com/gcla/gowid/widgets/hpadding" "github.com/gcla/gowid/widgets/isselected" "github.com/gcla/gowid/widgets/list" "github.com/gcla/gowid/widgets/null" "github.com/gcla/gowid/widgets/overlay" "github.com/gcla/gowid/widgets/pile" "github.com/gcla/gowid/widgets/styled" "github.com/gcla/gowid/widgets/text" "github.com/gcla/termshark/v2/widgets/keepselected" "github.com/gdamore/tcell/v2" ) //====================================================================== // Widget represents a termshark-specific "minibuffer" widget, expected to be opened // as a dialog near the bottom of the screen. It allows the user to type commands and // supports tab completion and listing completions. type Widget struct { *dialog.Widget compl *holder.Widget selections *list.Widget ed *edit.Widget pl *pile.Widget ov *overlay.Widget showAll bool // true if the user hits tab with nothing in the minibuffer. I don't // want to display all completions if the buffer is empty because it fills the screen // and looks ugly. So this is a hack to allow the completions to be displayed // via the tab key actions map[string]IAction } var _ gowid.IWidget = (*Widget)(nil) var nullw *null.Widget var wordExp *regexp.Regexp func init() { nullw = null.New() wordExp = regexp.MustCompile(`( *)((?:")[^"]*(?:")|[^\s]*)`) } // IAction represents a command that can be run in the minibuffer e.g. "set". It // can decide whether or not to show in the list of completions e.g. if the user // types "s". type IAction interface { Run(gowid.IApp, ...string) error // nil means success Arguments([]string, gowid.IApp) []IArg OfferCompletion() bool } // IArg represents an argument to a minibuffer command e.g. "dark-mode" in the // command "set dark-mode on". type IArg interface { OfferCompletion() bool Completions() []string } type partial struct { word string qword string before string after string } func (p *partial) Line() string { return p.before + p.qword + p.after } func (p *partial) CursorPos() int { return len(p.before + p.qword) } // keysWidget redirects printable characters to the edit widget (the lower), but navigational // commands like up/down will control the selection widget. type keysWidget struct { *pile.Widget top gowid.IWidget bottom gowid.IWidget outer *Widget } var _ gowid.IWidget = (*keysWidget)(nil) func (w *keysWidget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { res := false switch ev := ev.(type) { case *tcell.EventKey: switch ev.Key() { case tcell.KeyRune: res = w.bottom.UserInput(ev, size, focus, app) case tcell.KeyDown, tcell.KeyCtrlN, tcell.KeyUp, tcell.KeyCtrlP: res = w.top.UserInput(ev, size, focus, app) case tcell.KeyTAB, tcell.KeyEnter: w.outer.handleSelection(ev.Key() == tcell.KeyEnter, app) res = true case tcell.KeyBackspace, tcell.KeyBackspace2: if w.outer.ed.Text() == "" { if w.outer.IsOpen() { w.outer.Close(app) } res = true } } } if !res { res = w.Widget.UserInput(ev, size, focus, app) } w.Widget.SetFocus(app, 1) return res } func New() *Widget { res := &Widget{} editW := edit.New(edit.Options{ Caption: ":", }) editW.OnTextSet(gowid.MakeWidgetCallback("cb", gowid.WidgetChangedFunction(func(app gowid.IApp, ew gowid.IWidget) { res.updateCompletions(app) }))) // If the cursor pos changes, we might not be displaying the right set of completions editW.OnCursorPosSet(gowid.MakeWidgetCallback("cb", gowid.WidgetChangedFunction(func(app gowid.IApp, ew gowid.IWidget) { res.updateCompletions(app) }))) top := holder.New(nullw) bottom := hpadding.New(editW, gowid.HAlignLeft{}, gowid.RenderFlow{}) bufferW := pile.New( []gowid.IContainerWidget{ &gowid.ContainerWidget{ IWidget: top, D: gowid.RenderWithWeight{W: 1}, }, &gowid.ContainerWidget{ IWidget: bottom, D: gowid.RenderWithUnits{U: 1}, }, }, pile.Options{ StartRow: 1, }, ) keys := &keysWidget{ Widget: bufferW, top: top, bottom: bottom, outer: res, } *res = Widget{ Widget: dialog.New( keys, dialog.Options{ Buttons: []dialog.Button{}, NoShadow: true, NoFrame: false, BackgroundStyle: gowid.MakePaletteRef("cmdline"), ButtonStyle: gowid.MakePaletteRef("cmdline-button"), BorderStyle: gowid.MakePaletteRef("cmdline-border"), Modal: true, }, ), compl: top, ed: editW, pl: bufferW, actions: make(map[string]IAction), } return res } func (w *Widget) handleSelection(keyIsEnter bool, app gowid.IApp) { // Break edit text up into "words" // gcla later todo - need to check it's not nil selectedIdx := 0 if w.selections != nil { selectedIdx = int(w.selections.Walker().Focus().(list.ListPos)) } wordMatchesS := wordExp.FindAllStringSubmatch(w.ed.Text(), -1) words := make([]string, 0, len(wordMatchesS)) for _, m := range wordMatchesS { if m[2] != "" { words = append(words, strings.TrimPrefix(strings.TrimSuffix(m[2], "\""), "\"")) // make a list of the words in the minibuffer } } switch { // how many words are in the edit box case len(words) > 1: // a command with args, so command itself must be provided in full. partials := w.getPartialsCompletions(false, app) switch len(partials) { // case 1: case 0: // "load /tmp/foo" and [] - just run what the user typed if the key was enter if act, ok := w.actions[words[0]]; ok && keyIsEnter { err := act.Run(app, words...) if err == nil { // Run the command, let it handle errors if w.IsOpen() { w.Close(app) } } } default: // if the last word exactly equals the one selected in the partials, just run on enter if words[len(words)-1] == partials[selectedIdx].word && keyIsEnter { // "load /tmp/foo.pcap" and ["/tmp/foo.pcap"] if act, ok := w.actions[words[0]]; ok { err := act.Run(app, words...) if err == nil { // Run the command, let it handle errors if w.IsOpen() { w.Close(app) } } } } else { // Otherwise, tab complete // // "load /tmp/foo" and ["/tmp/foo2.pcap", "/tmp/foo.pcap"] // ^^^^^^^^^^^^^ w.ed.SetText(partials[selectedIdx].Line(), app) w.ed.SetCursorPos(partials[selectedIdx].CursorPos(), app) } //default: } case len(words) == 1: // command itself may be partially provided. If there is only // one way for the command to be completed, allow it to be run. // User types "cl", partials would be ["clear-packets", "clear-filter"] partials := w.getPartialsCompletions(false, app) switch len(partials) { case 0: if act, ok := w.actions[words[0]]; ok && keyIsEnter { err := act.Run(app, words...) if err == nil { // Run the command, let it handle errors if w.IsOpen() { w.Close(app) } } } default: if words[len(words)-1] == partials[selectedIdx].word && keyIsEnter { act := w.actions[partials[selectedIdx].word] if len(act.Arguments([]string{}, app)) == 0 { err := w.actions[partials[selectedIdx].word].Run(app, partials[selectedIdx].word) if err == nil { if w.IsOpen() { w.Close(app) } } } } else { if keyIsEnter { w.ed.SetText(partials[selectedIdx].Line(), app) w.ed.SetCursorPos(partials[selectedIdx].CursorPos(), app) } else { // This is tab - complete to the longest common prefix extraPrefix := "" loop: for j := len(words[len(words)-1]); true; j += 1 { var c rune for _, partial := range partials { if len(partial.word) <= j { break loop } if c == 0 { c = rune(partial.word[j]) } else { if c != rune(partial.word[j]) { break loop } } } extraPrefix += string(c) } longestPrefixPartial := partials[selectedIdx] // if the user types e.g. "help " then hits tab, extraPrefix will be "" (no characters typed // to start selection of next argument) so don't complete. if extraPrefix != "" { // e.g. "cl" + "ear-" from ["clear-packets", "clear-filter"] longestPrefixPartial.qword = words[len(words)-1] + extraPrefix w.ed.SetText(longestPrefixPartial.Line(), app) w.ed.SetCursorPos(longestPrefixPartial.CursorPos(), app) } } } } default: if w.selections == nil { w.showAll = true w.updateCompletions(app) } else if keyIsEnter { // if the selections are being displayed and enter is hit, complete that selection partials := w.getPartialsCompletions(true, app) w.ed.SetText(partials[selectedIdx].Line(), app) w.ed.SetCursorPos(partials[selectedIdx].CursorPos(), app) } } } // Not thread-safe, manage via App perhaps func (w *Widget) Register(name string, action IAction) { w.actions[name] = action } func (w *Widget) getPartialsCompletions(checkOffer bool, app gowid.IApp) []partial { txt := w.ed.Text() partials := make([]partial, 0) // e.g. "demo false" -> [[0 4 0 0 0 4] [4 10 4 5 5 10]] wordMatches := wordExp.FindAllStringSubmatchIndex(txt, -1) wordMatchesS := wordExp.FindAllStringSubmatch(txt, -1) wordIdx := 0 wordStart := 0 wordEnd := 0 cp := w.ed.CursorPos() // for : prompt? for mIdx, wordMatch := range wordMatches { wordIdx = mIdx wordStart = wordMatch[4] wordEnd = wordMatch[5] if wordMatch[2] <= cp && cp <= wordMatch[5] { // within the range of whitespace+word if wordMatch[2] < cp && cp < wordMatch[3] { // within the range of whitespace only // fake match - this is so the correct completion is displayed in the following situation: // "set dark-mode" // " ^ " wordMatchesS = append(wordMatchesS[0:wordIdx], append([][]string{{"", "", ""}}, wordMatchesS[wordIdx:len(wordMatchesS)]...)...) wordStart = cp wordEnd = cp } break } } toks := make([]string, 0) for _, s := range wordMatchesS { toks = append(toks, s[2]) } if wordIdx > 0 { argIdx := wordIdx - 1 // first argument to command if word, ok := w.actions[wordMatchesS[0][2]]; ok { // wordArgs := word.Arguments(toks[1:], app) if argIdx < len(wordArgs) { if !checkOffer || wordArgs[argIdx].OfferCompletion() { for _, complV := range wordArgs[argIdx].Completions() { // to bind properly compl := complV qcompl := compl if strings.Contains(qcompl, " ") { qcompl = "\"" + qcompl + "\"" } partials = append(partials, partial{ word: compl, qword: qcompl, before: txt[0:wordStart], after: txt[wordEnd:len(txt)], }) } } } } } else { // This is the first word matching keys := make([]string, 0, len(w.actions)) for k, _ := range w.actions { keys = append(keys, k) } sort.Strings(keys) for _, keyV := range keys { key := keyV act := w.actions[key] if (!checkOffer || act.OfferCompletion()) && strings.HasPrefix(key, txt) { partials = append(partials, partial{ word: key, qword: key, before: "", after: "", }) } } } return partials } func (w *Widget) updateCompletions(app gowid.IApp) { txt := w.ed.Text() complWidgets := make([]gowid.IWidget, 0) partials := make([]partial, 0) if txt != "" || w.showAll { partials = w.getPartialsCompletions(true, app) } w.showAll = false for _, partialV := range partials { partial := partialV // avoid gotcha compBtn := button.NewBare(text.New(partial.word)) compBtn.OnClick(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, widget gowid.IWidget) { w.ed.SetText(partial.Line(), app) w.ed.SetCursorPos(partial.CursorPos(), app) w.pl.SetFocus(app, 1) })) complWidgets = append(complWidgets, isselected.New( compBtn, styled.New(compBtn, gowid.MakePaletteRef("cmdline-button")), styled.New(compBtn, gowid.MakePaletteRef("cmdline-button")), ), ) } w.ov.SetHeight(gowid.RenderWithUnits{U: 3 + len(complWidgets)}, app) walker := list.NewSimpleListWalker(complWidgets) if len(complWidgets) > 0 { walker.SetFocus(walker.Last(), app) selections := list.New(walker) selections.GoToBottom(app) sl2 := keepselected.New(selections) w.compl.SetSubWidget(sl2, app) w.selections = selections } else { // don't want anything to take focus if there are no completions w.compl.SetSubWidget(nullw, app) w.selections = nil } } // w is minibuffer // container is main view func Open(w *Widget, container gowid.ISettableComposite, width gowid.IWidgetDimension, app gowid.IApp) { w.ov = overlay.New(w, container.SubWidget(), gowid.VAlignBottom{}, gowid.RenderWithUnits{U: 3}, // Intended to mean use as much vertical space as you need gowid.HAlignLeft{Margin: 5, MarginRight: 5}, width, overlay.Options{ IgnoreLowerStyle: true, }, ) if _, ok := width.(gowid.IRenderFixed); ok { w.SetContentWidth(gowid.RenderFixed{}, app) // fixed or weight:1, ratio:0.5 } else { w.SetContentWidth(gowid.RenderWithWeight{W: 1}, app) // fixed or weight:1, ratio:0.5 } w.SetSavedSubWidget(container.SubWidget(), app) w.SetSavedContainer(container, app) container.SetSubWidget(w.ov, app) w.SetOpen(true, app) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.4.0/widgets/number/000077500000000000000000000000001426312004500164505ustar00rootroot00000000000000termshark-2.4.0/widgets/number/number.go000066400000000000000000000061231426312004500202710ustar00rootroot00000000000000// Copyright 2019-2022 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 numeric widget with a couple of buttons that increase or decrease its value. package number import ( "fmt" "github.com/gcla/gowid" "github.com/gcla/gowid/gwutil" "github.com/gcla/gowid/widgets/button" "github.com/gcla/gowid/widgets/columns" "github.com/gcla/gowid/widgets/fill" "github.com/gcla/gowid/widgets/holder" "github.com/gcla/gowid/widgets/hpadding" "github.com/gcla/gowid/widgets/text" ) //====================================================================== type Options struct { Value int Max gwutil.IntOption Min gwutil.IntOption Styler func(gowid.IWidget) gowid.IWidget } type Widget struct { gowid.IWidget up *button.Widget down *button.Widget valHolder *holder.Widget Value int Opt Options } var _ gowid.IWidget = (*Widget)(nil) var blank *hpadding.Widget var upArrow *text.Widget var downArrow *text.Widget var leftbr *text.Widget var rightbr *text.Widget func init() { blank = hpadding.New( fill.New(' '), gowid.HAlignLeft{}, gowid.RenderWithUnits{U: 1}, ) leftbr = text.New("[") rightbr = text.New("]") upArrow = text.New("^") downArrow = text.New("v") } func New(opts ...Options) *Widget { var opt Options if len(opts) > 0 { opt = opts[0] } res := &Widget{ valHolder: holder.New(text.New(fmt.Sprintf("%d", opt.Value))), Value: opt.Value, } up := button.NewBare(upArrow) up.OnClick(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, widget gowid.IWidget) { if !res.Opt.Max.IsNone() && res.Value >= res.Opt.Max.Val() { res.Value = res.Opt.Max.Val() } else { res.Value += 1 } res.valHolder.SetSubWidget(text.New(fmt.Sprintf("%d", res.Value)), app) })) down := button.NewBare(downArrow) down.OnClick(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, widget gowid.IWidget) { if !res.Opt.Min.IsNone() && res.Value <= res.Opt.Min.Val() { res.Value = res.Opt.Min.Val() } else { res.Value -= 1 } res.valHolder.SetSubWidget(text.New(fmt.Sprintf("%d", res.Value)), app) })) styler := opt.Styler if styler == nil { styler = func(w gowid.IWidget) gowid.IWidget { return w } } cols := columns.New([]gowid.IContainerWidget{ &gowid.ContainerWidget{ IWidget: res.valHolder, D: gowid.RenderFixed{}, //D: gowid.RenderWithWeight{W: 1}, }, &gowid.ContainerWidget{ IWidget: blank, D: gowid.RenderWithUnits{U: 1}, }, &gowid.ContainerWidget{ IWidget: leftbr, D: gowid.RenderFixed{}, }, &gowid.ContainerWidget{ IWidget: styler(up), D: gowid.RenderFixed{}, }, &gowid.ContainerWidget{ IWidget: styler(down), D: gowid.RenderFixed{}, }, &gowid.ContainerWidget{ IWidget: rightbr, D: gowid.RenderFixed{}, }, }) res.IWidget = cols res.Opt = opt res.up = up res.down = down return res } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.4.0/widgets/number/number_test.go000066400000000000000000000026531426312004500213340ustar00rootroot00000000000000// Copyright 2019-2022 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 number import ( "testing" "github.com/gcla/gowid" "github.com/gcla/gowid/gwtest" "github.com/gdamore/tcell/v2" log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" ) //====================================================================== func evclick(x, y int) *tcell.EventMouse { return tcell.NewEventMouse(x, y, tcell.Button1, 0) } func evunclick(x, y int) *tcell.EventMouse { return tcell.NewEventMouse(x, y, tcell.ButtonNone, 0) } func TestNumber1(t *testing.T) { v := 2 w := New(Options{ Value: v, }) sz := gowid.RenderFixed{} c1 := w.Render(sz, gowid.NotSelected, gwtest.D) log.Infof("Canvas is %s", c1.String()) // "0 [^v]" assert.Equal(t, 1, c1.BoxRows()) clickat := func(x, y int) { w.UserInput(evclick(x, y), sz, gowid.Focused, gwtest.D) gwtest.D.SetLastMouseState(gowid.MouseState{true, false, false}) w.UserInput(evunclick(x, y), sz, gowid.Focused, gwtest.D) gwtest.D.SetLastMouseState(gowid.MouseState{false, false, false}) } clickat(2, 0) assert.Equal(t, v, w.Value) clickat(3, 0) assert.Equal(t, v+1, w.Value) clickat(4, 0) clickat(4, 0) assert.Equal(t, v+1-2, w.Value) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.4.0/widgets/regexstyle/000077500000000000000000000000001426312004500173535ustar00rootroot00000000000000termshark-2.4.0/widgets/regexstyle/regexstyle.go000066400000000000000000000055501426312004500221020ustar00rootroot00000000000000// Copyright 2019-2022 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.4.0/widgets/renderfocused/000077500000000000000000000000001426312004500200105ustar00rootroot00000000000000termshark-2.4.0/widgets/renderfocused/renderfocused.go000066400000000000000000000030231426312004500231650ustar00rootroot00000000000000// Copyright 2019-2022 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.4.0/widgets/resizable/000077500000000000000000000000001426312004500171405ustar00rootroot00000000000000termshark-2.4.0/widgets/resizable/resizable.go000066400000000000000000000156001426312004500214510ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package resizable provides columns and piles that can be adjusted. package resizable import ( "github.com/gcla/gowid" "github.com/gcla/gowid/widgets/columns" "github.com/gcla/gowid/widgets/pile" ) //====================================================================== type Offset struct { Col1 int `json:"col1"` Col2 int `json:"col2"` Adjust int `json:"adjust"` } type IOffsets interface { GetOffsets() []Offset SetOffsets([]Offset, gowid.IApp) } type OffsetsCB struct{} type ColumnsWidget struct { *columns.Widget Offsets []Offset Callbacks *gowid.Callbacks } var _ IOffsets = (*ColumnsWidget)(nil) func NewColumns(widgets []gowid.IContainerWidget) *ColumnsWidget { res := &ColumnsWidget{ Widget: columns.New(widgets), Offsets: make([]Offset, 0, 2), } return res } func (w *ColumnsWidget) GetOffsets() []Offset { return w.Offsets } func (w *ColumnsWidget) SetOffsets(offs []Offset, app gowid.IApp) { w.Offsets = offs gowid.RunWidgetCallbacks(w.Callbacks, OffsetsCB{}, app, w) } func (w *ColumnsWidget) OnOffsetsSet(cb gowid.IWidgetChangedCallback) { if w.Callbacks == nil { w.Callbacks = gowid.NewCallbacks() } gowid.AddWidgetCallback(w.Callbacks, OffsetsCB{}, cb) } func (w *ColumnsWidget) RemoveOnOffsetsSet(cb gowid.IIdentity) { if w.Callbacks == nil { w.Callbacks = gowid.NewCallbacks() } gowid.RemoveWidgetCallback(w.Callbacks, OffsetsCB{}, cb) } type AdjustFn func(x int) int var Add1 AdjustFn = func(x int) int { return x + 1 } var Subtract1 AdjustFn = func(x int) int { return x - 1 } func (w *ColumnsWidget) AdjustOffset(col1 int, col2 int, fn AdjustFn, app gowid.IApp) { AdjustOffset(w, col1, col2, fn, app) gowid.RunWidgetCallbacks(w.Callbacks, OffsetsCB{}, app, w) } func AdjustOffset(w IOffsets, col1 int, col2 int, fn AdjustFn, app gowid.IApp) { idx := -1 var off Offset for i, o := range w.GetOffsets() { if o.Col1 == col1 && o.Col2 == col2 { idx = i break } } if idx == -1 { off.Col1 = col1 off.Col2 = col2 w.SetOffsets(append(w.GetOffsets(), off), app) idx = len(w.GetOffsets()) - 1 } w.GetOffsets()[idx].Adjust = fn(w.GetOffsets()[idx].Adjust) } func (w *ColumnsWidget) WidgetWidths(size gowid.IRenderSize, focus gowid.Selector, focusIdx int, app gowid.IApp) []int { widths := w.Widget.WidgetWidths(size, focus, focusIdx, app) for _, off := range w.Offsets { addme := off.Adjust if widths[off.Col1]+addme < 0 { addme = -widths[off.Col1] } else if widths[off.Col2]-addme < 0 { addme = widths[off.Col2] } widths[off.Col1] += addme widths[off.Col2] -= addme } return widths } func (w *ColumnsWidget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { return columns.Render(w, size, focus, app) } func (w *ColumnsWidget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { return columns.UserInput(w, ev, size, focus, app) } func (w *ColumnsWidget) RenderSubWidgets(size gowid.IRenderSize, focus gowid.Selector, focusIdx int, app gowid.IApp) []gowid.ICanvas { return columns.RenderSubWidgets(w, size, focus, focusIdx, app) } func (w *ColumnsWidget) RenderedSubWidgetsSizes(size gowid.IRenderSize, focus gowid.Selector, focusIdx int, app gowid.IApp) []gowid.IRenderBox { return columns.RenderedSubWidgetsSizes(w, size, focus, focusIdx, app) } func (w *ColumnsWidget) SubWidgetSize(size gowid.IRenderSize, newX int, sub gowid.IWidget, dim gowid.IWidgetDimension) gowid.IRenderSize { return w.Widget.SubWidgetSize(size, newX, sub, dim) } //====================================================================== type PileWidget struct { *pile.Widget Offsets []Offset Callbacks *gowid.Callbacks } func NewPile(widgets []gowid.IContainerWidget) *PileWidget { res := &PileWidget{ Widget: pile.New(widgets), Offsets: make([]Offset, 0, 2), Callbacks: gowid.NewCallbacks(), } return res } var _ IOffsets = (*ColumnsWidget)(nil) func (w *PileWidget) GetOffsets() []Offset { return w.Offsets } func (w *PileWidget) SetOffsets(offs []Offset, app gowid.IApp) { w.Offsets = offs gowid.RunWidgetCallbacks(w.Callbacks, OffsetsCB{}, app, w) } func (w *PileWidget) OnOffsetsSet(cb gowid.IWidgetChangedCallback) { gowid.AddWidgetCallback(w.Callbacks, OffsetsCB{}, cb) } func (w *PileWidget) RemoveOnOffsetsSet(cb gowid.IIdentity) { gowid.RemoveWidgetCallback(w.Callbacks, OffsetsCB{}, cb) } func (w *PileWidget) AdjustOffset(col1 int, col2 int, fn AdjustFn, app gowid.IApp) { AdjustOffset(w, col1, col2, fn, app) gowid.RunWidgetCallbacks(w.Callbacks, OffsetsCB{}, app, w) } type PileAdjuster struct { widget *PileWidget origSizer pile.IPileBoxMaker } func (f PileAdjuster) MakeBox(w gowid.IWidget, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { adjustedSize := size var box gowid.RenderBox isbox := false switch size := size.(type) { case gowid.IRenderBox: box.C = size.BoxColumns() box.R = size.BoxRows() isbox = true } i := 0 for ; i < len(f.widget.SubWidgets()); i++ { if w == f.widget.SubWidgets()[i] { break } } if i == len(f.widget.SubWidgets()) { panic("Unexpected pile state!") } if isbox { for _, off := range f.widget.Offsets { if i == off.Col1 { if box.R+off.Adjust < 0 { off.Adjust = -box.R } box.R += off.Adjust } else if i == off.Col2 { if box.R-off.Adjust < 0 { off.Adjust = box.R } box.R -= off.Adjust } } adjustedSize = box } return f.origSizer.MakeBox(w, adjustedSize, focus, app) } func (w *PileWidget) FindNextSelectable(dir gowid.Direction, wrap bool) (int, bool) { return gowid.FindNextSelectableFrom(w, w.Focus(), dir, wrap) } func (w *PileWidget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { return pile.UserInput(w, ev, size, focus, app) } func (w *PileWidget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { return pile.Render(w, size, focus, app) } func (w *PileWidget) RenderedSubWidgetsSizes(size gowid.IRenderSize, focus gowid.Selector, focusIdx int, app gowid.IApp) []gowid.IRenderBox { res, _ := pile.RenderedChildrenSizes(w, size, focus, focusIdx, app) return res } func (w *PileWidget) RenderSubWidgets(size gowid.IRenderSize, focus gowid.Selector, focusIdx int, app gowid.IApp) []gowid.ICanvas { return pile.RenderSubwidgets(w, size, focus, focusIdx, app) } func (w *PileWidget) RenderBoxMaker(size gowid.IRenderSize, focus gowid.Selector, focusIdx int, app gowid.IApp, sizer pile.IPileBoxMaker) ([]gowid.IRenderBox, []gowid.IRenderSize) { x := &PileAdjuster{ widget: w, origSizer: sizer, } return pile.RenderBoxMaker(w, size, focus, focusIdx, app, x) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.4.0/widgets/resizable/resizable_test.go000066400000000000000000000017501426312004500225110ustar00rootroot00000000000000// Copyright 2019-2022 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.4.0/widgets/rossshark/000077500000000000000000000000001426312004500171775ustar00rootroot00000000000000termshark-2.4.0/widgets/rossshark/rossshark.go000066400000000000000000000101501426312004500215420ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package rossshark provides a widget that draws a hi-tech shark fin over the // background and allows it to move across the screen. I hope this is faithful // to Ross Jacobs' vision :-) package rossshark import ( "math/rand" "time" "github.com/gcla/gowid" "github.com/gcla/gowid/gwutil" ) //====================================================================== var maskF []string var maskB = []string{ "00000000000000000000000000111111111", "00000000000000000000011111111111110", "00000000000000000111111111111111100", "00000000000000111111111111111111000", "00000000000011111111111111111110000", "00000000001111111111111111111110000", "00000000111111111111111111111100000", "00000001111111111111111111111100000", "00000011111111111111111111111100000", "00000111111111111111111111111100000", "00001111111111111111111111111100000", "00011111111111111111111111111100000", "00111111111111111111111111111100000", "01111111111111111111111111111100000", "01111111111111111111111111111110000", "11111111111111111111111111111110000", "11111111111111111111111111111111000", "11111111111111111111111111111111100", } func init() { maskF = make([]string, 0, len(maskB)) for _, line := range maskB { maskF = append(maskF, reverseString(line)) } } type Direction int const ( Backward Direction = 0 Forward Direction = iota ) type Widget struct { gowid.IWidget Dir Direction active bool xOffset int mask [][]string backg []string ticker *time.Ticker } var _ gowid.IWidget = (*Widget)(nil) func New(w gowid.IWidget) *Widget { backg := make([]string, 0, 48) for i := 0; i < cap(backg); i++ { backg = append(backg, randomString(110)) } res := &Widget{ IWidget: w, mask: [][]string{maskB, maskF}, backg: backg, xOffset: 100000, } return res } func (w *Widget) Advance() { switch w.Dir { case Backward: w.xOffset -= 1 if w.xOffset <= -len(w.mask[0]) { w.xOffset = 100000 // big enough } case Forward: w.xOffset += 1 } } func (w *Widget) Activate() { w.ticker = time.NewTicker(time.Duration(150) * time.Millisecond) } func (w *Widget) Deactivate() { w.ticker = nil } func (w *Widget) Active() bool { return w.ticker != nil } func (w *Widget) C() <-chan time.Time { return w.ticker.C } func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { c := w.IWidget.Render(size, focus, app) if w.Active() { // Adjust here to account for the fact the screen can be resized if w.xOffset >= c.BoxColumns() { switch w.Dir { case Backward: w.xOffset = c.BoxColumns() - 1 case Forward: w.xOffset = -len(w.mask[0]) } } mask := w.mask[w.Dir] yOffset := c.BoxRows()/2 - len(mask)/2 // in the middle for y, sy := gwutil.Max(0, yOffset), gwutil.Max(0, -yOffset); y < c.BoxRows() && sy < len(mask); y, sy = y+1, sy+1 { for x, sx := gwutil.Max(0, w.xOffset), gwutil.Max(0, -w.xOffset); x < c.BoxColumns() && sx < len(mask[0]); x, sx = x+1, sx+1 { if mask[sy][sx] == '1' { cell := c.CellAt(x, y) r := w.backg[y%len(w.backg)][x%len(w.backg[0])] c.SetCellAt(x, y, cell.WithRune(rune(r))) } } } } return c } //====================================================================== // Use charset [a-f0-9] to mirror tshark -x/xxd hex output const charset = "abcdef0123456789" var seededRand *rand.Rand = rand.New(rand.NewSource(time.Now().UnixNano())) func randomStringWithCharset(length int, charset string) string { b := make([]byte, length) for i := range b { b[i] = charset[seededRand.Intn(len(charset))] } return string(b) } func randomString(length int) string { return randomStringWithCharset(length, charset) } // Plagiarized from https://stackoverflow.com/a/4965535 - the most straightforward answer func reverseString(s string) (result string) { for _, v := range s { result = string(v) + result } return } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.4.0/widgets/scrollabletable/000077500000000000000000000000001426312004500203125ustar00rootroot00000000000000termshark-2.4.0/widgets/scrollabletable/scrollabletable.go000066400000000000000000000024141426312004500237740ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package scrollabletable makes a widget that some scrollbar interfaces // suitable for passing to withscrollbar.New() package scrollabletable import ( "github.com/gcla/gowid" "github.com/gcla/gowid/widgets/table" "github.com/gcla/termshark/v2/widgets/withscrollbar" ) //====================================================================== type IScrollableTable interface { gowid.IWidget withscrollbar.IScrollOneLine withscrollbar.IScrollOnePage CurrentRow() int Model() table.IModel } // To implement withscrollbar.IScrollValues type Widget struct { IScrollableTable } // makes a IScrollableTable suitable for passing to withscrollbar.New() var _ withscrollbar.IScrollSubWidget = Widget{} var _ withscrollbar.IScrollSubWidget = (*Widget)(nil) func New(t IScrollableTable) *Widget { return &Widget{ IScrollableTable: t, } } func (s Widget) ScrollLength() int { return s.Model().(table.IBoundedModel).Rows() } func (s Widget) ScrollPosition() int { return s.CurrentRow() } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.4.0/widgets/scrollabletext/000077500000000000000000000000001426312004500202075ustar00rootroot00000000000000termshark-2.4.0/widgets/scrollabletext/scrollabletext.go000066400000000000000000000071731426312004500235750ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package scrollabletext provides a text widget that can be placed inside // withscrollbar.Widget package scrollabletext import ( "strings" "github.com/gcla/gowid" "github.com/gcla/gowid/gwutil" "github.com/gcla/gowid/widgets/selectable" "github.com/gcla/gowid/widgets/text" "github.com/gdamore/tcell/v2" ) //====================================================================== // Widget constructs a text widget and allows it to be scrolled. But this widget is limited - it assumes no // line will wrap. To make this happen it ensures that any lines that are too long are clipped. It makes this // assumption because my scrollbar APIs are not well designed, and functions like ScrollPosition and // ScrollLength don't understand the current rendering context. That means if the app is resized, and a line // now takes two screen lines to render and not one, the scrollbar can't be built accurately. Until I design // a better scrollbar API, this will work - I'm only using it for limited information dialogs at the moment. type Widget struct { *selectable.Widget splitText []string linesFromTop int // how many lines down we are cachedLength int } var _ gowid.IWidget = (*Widget)(nil) func New(txt string) *Widget { splitText := strings.Split(txt, "\n") res := &Widget{ splitText: splitText, cachedLength: len(splitText), } res.makeText() return res } func (w *Widget) makeText() { w.Widget = selectable.New( text.New( strings.Join(w.splitText[w.linesFromTop:], "\n"), text.Options{ Wrap: text.WrapClip, ClipIndicator: "...", }, ), ) } func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { handled := true linesFromTop := w.linesFromTop switch ev := ev.(type) { case *tcell.EventKey: switch ev.Key() { case tcell.KeyPgUp: w.UpPage(1, size, app) case tcell.KeyUp, tcell.KeyCtrlP: w.Up(1, size, app) case tcell.KeyDown, tcell.KeyCtrlN: w.Down(1, size, app) case tcell.KeyPgDn: w.DownPage(1, size, app) default: handled = false } } if handled && linesFromTop == w.linesFromTop { handled = false } if !handled { handled = w.Widget.UserInput(ev, size, focus, app) } return handled } // Implement functions for withscrollbar.Widget func (w *Widget) ScrollPosition() int { return w.linesFromTop } func (w *Widget) ScrollLength() int { return w.cachedLength } func (w *Widget) Up(lines int, size gowid.IRenderSize, app gowid.IApp) { pos := w.linesFromTop w.linesFromTop = gwutil.Max(0, w.linesFromTop-lines) if pos != w.linesFromTop { w.makeText() } } func (w *Widget) Down(lines int, size gowid.IRenderSize, app gowid.IApp) { pos := w.linesFromTop w.linesFromTop = gwutil.Min(w.cachedLength-1, w.linesFromTop+lines) if pos != w.linesFromTop { w.makeText() } } func (w *Widget) UpPage(num int, size gowid.IRenderSize, app gowid.IApp) { pos := w.linesFromTop pg := 1 if size, ok := size.(gowid.IRows); ok { pg = size.Rows() } w.linesFromTop = gwutil.Max(0, w.linesFromTop-(pg*num)) if pos != w.linesFromTop { w.makeText() } } func (w *Widget) DownPage(num int, size gowid.IRenderSize, app gowid.IApp) { pos := w.linesFromTop pg := 1 if size, ok := size.(gowid.IRows); ok { pg = size.Rows() } w.linesFromTop = gwutil.Min(w.cachedLength-1, w.linesFromTop+(pg*num)) if pos != w.linesFromTop { w.makeText() } } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.4.0/widgets/search/000077500000000000000000000000001426312004500164255ustar00rootroot00000000000000termshark-2.4.0/widgets/search/search.go000066400000000000000000000555271426312004500202370ustar00rootroot00000000000000// Copyright 2019-2022 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 search provides termshark's search widget including the various // drop down menus to control the type of search to be issued. package search import ( "bytes" "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/disable" "github.com/gcla/gowid/widgets/fill" "github.com/gcla/gowid/widgets/holder" "github.com/gcla/gowid/widgets/hpadding" "github.com/gcla/gowid/widgets/menu" "github.com/gcla/gowid/widgets/null" "github.com/gcla/gowid/widgets/styled" "github.com/gcla/gowid/widgets/text" "github.com/gcla/termshark/v2/configs/profiles" "github.com/gcla/termshark/v2/pkg/fields" "github.com/gcla/termshark/v2/ui/menuutil" "github.com/gcla/termshark/v2/widgets/filter" "github.com/gcla/termshark/v2/widgets/ifwidget" "github.com/gdamore/tcell/v2" ) //====================================================================== var ( // map internal names to user-visible names searchTypeMap = map[string]string{ "filter": "Filter", "hex": "Hex", "string": "String", "regex": "Regex", } // map internal names to user-visible names searchTargetMap = map[string]string{ "bytes": "Pkt Bytes", "list": "Pkt List", "details": "Pkt Details", } ) type INeedle interface { Search(data string) int } //====================================================================== // simpleTerm represents a string that can be searched-for in provided data. type simpleTerm string func (s simpleTerm) String() string { return string(s) } func (s simpleTerm) Search(data string) int { return strings.Index(data, string(s)) } var _ fmt.Stringer = simpleTerm("") var _ INeedle = simpleTerm("") //====================================================================== // stringTerm represents a string that can be searched-for in provided data with a toggle to control whether // or not the search is case-sensitive. type stringTerm struct { term simpleTerm ciTerm simpleTerm caseSensitive bool } func newStringTerm(t string, cs bool) stringTerm { return stringTerm{ term: simpleTerm(t), ciTerm: simpleTerm(strings.ToUpper(t)), caseSensitive: cs, } } func (t stringTerm) CaseSensitive() bool { return t.caseSensitive } func (s stringTerm) Search(data string) int { if s.caseSensitive { return s.term.Search(data) } else { return s.ciTerm.Search(strings.ToUpper(data)) } } //====================================================================== // hexTerm holds a string that represents a bytes to be searched for in binary data. The syntax follows // Wireshark - each byte is given as two ASCII characters in the range [0-9a-fA-F], and a sequence of bytes // is the concatenation of these two characters e.g. "54AD090f" is a 4-byte search. type hexTerm struct { user string bytes []byte } func newHexTerm(user string) hexTerm { return hexTerm{ user: user, bytes: hexTermToBytes(user), } } func hexToByte(b byte) int { r := rune(b) switch { case r >= 'a' && r <= 'f': return int(r + 10 - 'a') case r >= 'A' && r <= 'F': return int(r + 10 - 'A') case r >= '0' && r <= '9': return int(r - '0') default: panic(nil) } } func hexTermToBytes(s string) []byte { res := make([]byte, 0, 16) if (len(s)/2)*2 != len(s) { panic(nil) } for i := 0; i < len(s); i += 2 { res = append(res, byte(hexToByte(s[i])<<4+hexToByte(s[i+1]))) } return res } func (s hexTerm) Search(data string) int { return bytes.Index([]byte(data), s.bytes) } var _ INeedle = hexTerm{} //====================================================================== // regexTerm represents a regex to be searched-for in packet data, with a flag that determines // whether or not the search is case-sensitive. type regexTerm struct { re *regexp.Regexp cire *regexp.Regexp caseSensitive bool } func newRegexTerm(rest string, cs bool) (regexTerm, error) { re, err := regexp.Compile(rest) if err != nil { return regexTerm{}, err } re2, err := regexp.Compile("(?i)" + rest) if err != nil { return regexTerm{}, err } return regexTerm{re: re, cire: re2, caseSensitive: cs}, nil } func (s regexTerm) Search(data string) int { var res []int if s.caseSensitive { res = s.re.FindStringIndex(data) } else { res = s.cire.FindStringIndex(data) } if res == nil { return -1 } else { return res[0] } } var _ INeedle = regexTerm{} //====================================================================== type IRequestStop interface { RequestStop(app gowid.IApp) } type IErrorHandler interface { OnError(err error, app gowid.IApp) } type IResult interface { PacketNumber() int } type Result struct { Interrupted bool Success bool ErrorForUser error Position interface{} } type IntermediateResult struct { Res Result ResumeAt IResult } type IAlgorithm interface { SearchPackets(term INeedle, cb ICallbacks, app gowid.IApp) } // ICallbacks is intended to be a callback issued when the Find invocation yields a // result of some kind type ICallbacks interface { Reset(app gowid.IApp) // Intended to make things ready for next "Find" invocation StartingPosition() (interface{}, error) SearchPacketsFrom(from interface{}, start interface{}, term INeedle, app gowid.IApp) SearchPacketsResult(res Result, app gowid.IApp) RequestStop(app gowid.IApp) OnTick(app gowid.IApp) OnError(err error, app gowid.IApp) } var fixed gowid.RenderFixed //====================================================================== // Widget represents a composite UI element for driving packet search operations. It comprises a menu // allowing the type of data to be searched - PSML, PDML or raw bytes; a menu determining the search // method - string, regex, hex or filter; the input field for the search, and a button to start the // search. Note that the filter search is a special case - the search value is used as a display filter // to select packets from the current source, rather than for searching within packet data. type Widget struct { gowid.IWidget *gowid.Callbacks filterHolder *holder.Widget filt *filter.Widget menuOpener menu.IOpener completer fields.IPrefixCompleter cols *columns.Widget findBtn *disable.Widget searchTargetBtn *button.Widget dataBtn *button.Widget validator filter.IValidator errHandler IErrorHandler alg IAlgorithm listFn func() ICallbacks structFn func() ICallbacks bytesFn func() ICallbacks filterFn func() ICallbacks listAlg ICallbacks structAlg ICallbacks bytesAlg ICallbacks filtAlg ICallbacks currentAlg ICallbacks // Which search algorithm to use - psml, pdml, hex, filter } var _ gowid.IWidget = (*Widget)(nil) var _ gowid.IFocus = (*Widget)(nil) var _ gowid.ICompositeMultipleFocus = (*Widget)(nil) // for SetFocusPath support var _ gowid.IPreferedPosition = (*Widget)(nil) //====================================================================== type enableSearchButton struct { ICallbacks btn *disable.Widget } func (f enableSearchButton) SearchPacketsResult(res Result, app gowid.IApp) { f.ICallbacks.SearchPacketsResult(res, app) f.btn.Enable() } func (f enableSearchButton) OnError(err error, app gowid.IApp) { f.ICallbacks.OnError(err, app) f.btn.Enable() } //====================================================================== func weight(n int) gowid.RenderWithWeight { return gowid.RenderWithWeight{W: n} } func units(n int) gowid.RenderWithUnits { return gowid.RenderWithUnits{U: n} } func getSearchType() string { res := profiles.ConfString("main.search-type", "filter") if _, ok := searchTypeMap[res]; !ok { res = "filter" } return res } func getSearchTarget() string { res := profiles.ConfString("main.search-target", "list") if _, ok := searchTargetMap[res]; !ok { res = "list" } return res } func getValidator() filter.IValidator { var validator filter.IValidator s2 := getSearchType() switch s2 { case "filter": validator = &filter.DisplayFilterValidator{} case "hex": validator = &HexSearchValidator{} case "string": validator = &StringSearchValidator{} case "regex": validator = &RegexSearchValidator{} default: panic(nil) } return validator } func New(alg IAlgorithm, searchPktList func() ICallbacks, searchPktStruct func() ICallbacks, searchPktDetails func() ICallbacks, searchByFilter func() ICallbacks, men menu.IOpener, comp fields.IPrefixCompleter, errHandler IErrorHandler) *Widget { res := &Widget{ alg: alg, listFn: searchPktList, structFn: searchPktStruct, bytesFn: searchPktDetails, filterFn: searchByFilter, listAlg: searchPktList(), structAlg: searchPktStruct(), bytesAlg: searchPktDetails(), filtAlg: searchByFilter(), validator: getValidator(), errHandler: errHandler, menuOpener: men, completer: comp, } colSpaceW := hpadding.New( fill.New(' '), gowid.HAlignLeft{}, gowid.RenderWithUnits{U: 1}, ) colSpace := &gowid.ContainerWidget{ IWidget: colSpaceW, D: units(1), } searchTypeB := button.New(text.New(searchTypeMap[getSearchType()])) searchTypeBtn := disable.NewEnabled( clicktracker.New( styled.NewExt( searchTypeB, gowid.MakePaletteRef("button"), gowid.MakePaletteRef("button-focus"), ), ), ) mymenu := buildSearchTypeMenu(searchTypeB, men, res) searchTypeBtnSite := menu.NewSite(menu.SiteOptions{YOffset: 1}) searchTypeB.OnClick(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, _ gowid.IWidget) { if !men.OpenMenu(mymenu, searchTypeBtnSite, app) { // Close if already open men.CloseMenu(mymenu, app) } })) findB := button.New(text.New("Find")) res.findBtn = disable.NewDisabled( clicktracker.New( styled.NewExt( findB, gowid.MakePaletteRef("button"), gowid.MakePaletteRef("button-focus"), ), ), ) // Start disableINeedleter is empty and we know that's invalid. This should be // enforced somewhere, via some sort of data-binding. findB.OnClick(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, _ gowid.IWidget) { res.invokeSearch(app) })) // Make sure that when the final result of the search is issued, the button // is re-enabled res.listAlg = enableSearchButton{ICallbacks: res.listAlg, btn: res.findBtn} res.structAlg = enableSearchButton{ICallbacks: res.structAlg, btn: res.findBtn} res.bytesAlg = enableSearchButton{ICallbacks: res.bytesAlg, btn: res.findBtn} res.filtAlg = enableSearchButton{ICallbacks: res.filtAlg, btn: res.findBtn} dataB := button.New(text.New("Wait...")) dataBtn := disable.NewEnabled( clicktracker.New( styled.NewExt( dataB, gowid.MakePaletteRef("button"), gowid.MakePaletteRef("button-focus"), ), ), ) res.searchTargetBtn = dataB res.searchTargetBtn.SetSubWidget(text.New(searchTargetMap[getSearchTarget()]), nil) // Do after res.searchTargetBtn set up res.updateSearchTargetFromConf(nil) mymenu2 := buildSearchTargetMenu(dataB, men, res) dataBtnSite := menu.NewSite(menu.SiteOptions{YOffset: 1}) dataB.OnClick(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, _ gowid.IWidget) { if !men.OpenMenu(mymenu2, dataBtnSite, app) { // Close if already open men.CloseMenu(mymenu2, app) } //men.OpenMenu(mymenu2, dataBtnSite, app) })) res.dataBtn = dataB caseCheck := checkbox.New(res.CaseSensitive()) caseCheck.OnClick(gowid.WidgetCallback{"cb", func(app gowid.IApp, _ gowid.IWidget) { res.SetCaseSensitive(caseCheck.IsChecked()) }}) caseLabel := text.New(" Case Sens ") caseW := hpadding.New( columns.NewFixed(caseCheck, caseLabel), gowid.HAlignMiddle{}, gowid.RenderFixed{}, ) selectiveCols := hpadding.New( columns.NewFixed(dataBtnSite, dataBtn, colSpace, caseW), gowid.HAlignMiddle{}, gowid.RenderFixed{}, ) res.filterHolder = holder.New(null.New()) res.setFilter(getValidator(), res.getCompleter(), nil) // Will only be enabled to click if filter is valid selcols := ifwidget.New( selectiveCols, null.New(), func() bool { st := getSearchType() return st == "string" || st == "regex" }, ) // If this structure changes, see [[focusOnFilter]] res.cols = columns.New([]gowid.IContainerWidget{ &gowid.ContainerWidget{ IWidget: text.New("Find:"), D: fixed, }, colSpace, &gowid.ContainerWidget{ IWidget: selcols, D: fixed, }, &gowid.ContainerWidget{ IWidget: searchTypeBtnSite, D: fixed, }, &gowid.ContainerWidget{ IWidget: searchTypeBtn, D: fixed, }, colSpace, &gowid.ContainerWidget{ IWidget: res.filterHolder, D: weight(1), }, &gowid.ContainerWidget{ IWidget: res.findBtn, D: fixed, }, colSpace, }) res.IWidget = res.cols return res } func (w *Widget) invokeSearch(app gowid.IApp) { var searchTerm INeedle switch w.validator.(type) { case *filter.DisplayFilterValidator: searchTerm = simpleTerm(w.filt.Value()) case *HexSearchValidator: searchTerm = newHexTerm(w.filt.Value()) case *StringSearchValidator: searchTerm = newStringTerm(w.filt.Value(), w.CaseSensitive()) case *RegexSearchValidator: var err error searchTerm, err = newRegexTerm(w.filt.Value(), w.CaseSensitive()) if err != nil { w.errHandler.OnError(fmt.Errorf("Could not validate: %w", err), app) return } default: panic(nil) } w.findBtn.Disable() w.alg.SearchPackets(searchTerm, w.currentAlg, app) } func (w *Widget) getCompleter() fields.IPrefixCompleter { var completer fields.IPrefixCompleter s2 := getSearchType() switch s2 { case "filter": completer = w.completer } return completer } // <> func (w *Widget) focusOnFilter(app gowid.IApp) { w.cols.SetFocus(app, 6) } func (w *Widget) setFilter(validator filter.IValidator, completer fields.IPrefixCompleter, app gowid.IApp) { filt := filter.New("searchfilter", filter.Options{ MenuOpener: w.menuOpener, Completer: completer, Position: filter.Below, Validator: validator, }) filt.OnValid(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, _ gowid.IWidget) { w.findBtn.Enable() })) filt.OnInvalid(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, _ gowid.IWidget) { w.findBtn.Disable() })) validFilterCb := gowid.MakeWidgetCallback("cb", func(app gowid.IApp, _ gowid.IWidget) { w.invokeSearch(app) }) filt.OnSubmit(validFilterCb) w.filt = filt w.filterHolder.SetSubWidget(filt, app) } func (w *Widget) SetValue(val string, app gowid.IApp) { w.filt.SetValue(val, app) } func (w *Widget) Value() string { return w.filt.Value() } func (w *Widget) CaseSensitive() bool { return profiles.ConfBool("main.search-case-sensitive", false) } func (w *Widget) SetCaseSensitive(val bool) { profiles.SetConf("main.search-case-sensitive", val) } func (w *Widget) Open(app gowid.IApp) { filt := filter.New("searchfilter", filter.Options{ MenuOpener: w.menuOpener, Completer: w.completer, Position: filter.Below, Validator: getValidator(), }) w.filt = filt w.filterHolder.SetSubWidget(filt, app) } func (w *Widget) Close(app gowid.IApp) error { // Stop any search going on w.currentAlg.RequestStop(app) filt := w.filterHolder.SubWidget().(*filter.Widget) return filt.Close() } func (w *Widget) Clear(app gowid.IApp) { w.currentAlg.RequestStop(app) // Throw away all search state, cached results w.listAlg = w.listFn() w.bytesAlg = w.bytesFn() w.structAlg = w.structFn() w.filtAlg = w.filterFn() w.listAlg = enableSearchButton{ICallbacks: w.listAlg, btn: w.findBtn} w.structAlg = enableSearchButton{ICallbacks: w.structAlg, btn: w.findBtn} w.bytesAlg = enableSearchButton{ICallbacks: w.bytesAlg, btn: w.findBtn} w.filtAlg = enableSearchButton{ICallbacks: w.filtAlg, btn: w.findBtn} w.updateSearchTargetFromConf(app) } func (w *Widget) FocusIsOnFilter() bool { return w.cols.Focus() == 6 } func (w *Widget) Focus() int { return w.cols.Focus() } func (w *Widget) SetFocus(app gowid.IApp, i int) { w.cols.SetFocus(app, i) } func (w *Widget) SubWidgets() []gowid.IWidget { return w.cols.SubWidgets() } func (w *Widget) GetPreferedPosition() gwutil.IntOption { return w.cols.GetPreferedPosition() } func (w *Widget) SetPreferedPosition(cols int, app gowid.IApp) { w.cols.SetPreferedPosition(cols, app) } //====================================================================== type indirect struct { *holder.Widget } func buildSearchTypeMenu(btn *button.Widget, men menu.IOpener, res *Widget) *menu.Widget { searchTypeMenu1Holder := &indirect{} searchTypeMenu := menu.New("searchtype", searchTypeMenu1Holder, 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), }, }) menuItems := make([]menuutil.SimpleMenuItem, 0) for i, stype_ := range []string{ "filter", "hex", "string", "regex", } { stype := stype_ menuItems = append(menuItems, menuutil.SimpleMenuItem{ Txt: searchTypeMap[stype], Key: gowid.MakeKey('1' + rune(i)), CB: func(app gowid.IApp, _ gowid.IWidget) { btn.SetSubWidget(text.New(searchTypeMap[stype]), app) // Save the default search type profiles.SetConf("main.search-type", stype) res.validator = getValidator() // Save the old value fval := res.Value() res.Close(app) // Use the new validating filter res.setFilter(getValidator(), res.getCompleter(), app) // Update the validity for the current value - maybe it's not valid with the new filter res.SetValue(fval, app) switch stype { case "hex": profiles.SetConf("main.search-target", "bytes") } // read main.search-type and adjust which search algorithm to use res.updateSearchTargetFromConf(app) // This is probably what the user will do next res.focusOnFilter(app) men.CloseMenu(searchTypeMenu, app) }, }, ) } lb, _ := menuutil.MakeMenuWithHotKeys(menuItems, nil) searchTypeMenu1Holder.Widget = holder.New(lb) return searchTypeMenu } //====================================================================== func (w *Widget) updateSearchTargetFromConf(app gowid.IApp) { sAlg := profiles.ConfString("main.search-type", "filter") if sAlg != "filter" && sAlg != "hex" { sAlg = profiles.ConfString("main.search-target", "list") } switch sAlg { case "list": w.currentAlg = w.listAlg case "details": w.currentAlg = w.structAlg case "bytes": w.currentAlg = w.bytesAlg case "hex": w.currentAlg = w.bytesAlg case "filter": w.currentAlg = w.filtAlg default: panic(nil) } } //====================================================================== func buildSearchTargetMenu(btn *button.Widget, men menu.IOpener, res *Widget) *menu.Widget { dataMenu1Holder := &indirect{} dataMenu := menu.New("datatype", dataMenu1Holder, 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), }, }) menuItems := make([]menuutil.SimpleMenuItem, 0) for i, target_ := range []string{ "list", "details", "bytes", } { target := target_ menuItems = append(menuItems, menuutil.SimpleMenuItem{ Txt: searchTargetMap[target], Key: gowid.MakeKey('1' + rune(i)), CB: func(app gowid.IApp, _ gowid.IWidget) { profiles.SetConf("main.search-target", target) btn.SetSubWidget(text.New(searchTargetMap[target]), app) res.updateSearchTargetFromConf(app) men.CloseMenu(dataMenu, app) }, }, ) } lb, _ := menuutil.MakeMenuWithHotKeys(menuItems, nil) dataMenu1Holder.Widget = holder.New(lb) return dataMenu } //====================================================================== type RegexSearchValidator struct { Valid filter.IValidateCB Invalid filter.IValidateCB KilledCB filter.IValidateCB EmptyCB filter.IValidateCB } var _ filter.IValidator = (*RegexSearchValidator)(nil) func (f *RegexSearchValidator) SetValid(cb filter.IValidateCB) { f.Valid = cb } func (f *RegexSearchValidator) SetInvalid(cb filter.IValidateCB) { f.Invalid = cb } func (f *RegexSearchValidator) SetKilled(cb filter.IValidateCB) { f.KilledCB = cb } func (f *RegexSearchValidator) SetEmpty(cb filter.IValidateCB) { f.EmptyCB = cb } func (f *RegexSearchValidator) Kill() (bool, error) { return true, nil } func (f *RegexSearchValidator) Validate(filter string) { if filter == "" { if f.EmptyCB != nil { f.EmptyCB.Call(filter) } return } _, err := regexp.Compile(filter) if err == nil { if f.Valid != nil { f.Valid.Call(filter) } } else { if f.Invalid != nil { f.Invalid.Call(filter) } } } //====================================================================== type StringSearchValidator struct { Valid filter.IValidateCB Invalid filter.IValidateCB KilledCB filter.IValidateCB EmptyCB filter.IValidateCB } var _ filter.IValidator = (*StringSearchValidator)(nil) func (f *StringSearchValidator) SetValid(cb filter.IValidateCB) { f.Valid = cb } func (f *StringSearchValidator) SetInvalid(cb filter.IValidateCB) { f.Invalid = cb } func (f *StringSearchValidator) SetKilled(cb filter.IValidateCB) { f.KilledCB = cb } func (f *StringSearchValidator) SetEmpty(cb filter.IValidateCB) { f.EmptyCB = cb } func (f *StringSearchValidator) Kill() (bool, error) { return true, nil } func (f *StringSearchValidator) Validate(filter string) { if filter == "" { if f.EmptyCB != nil { f.EmptyCB.Call(filter) } return } if f.Valid != nil { f.Valid.Call(filter) } } //====================================================================== var hexre *regexp.Regexp func init() { hexre = regexp.MustCompile(`^([0-9a-fA-F]{2})+$`) // do each line } type HexSearchValidator struct { Valid filter.IValidateCB Invalid filter.IValidateCB KilledCB filter.IValidateCB EmptyCB filter.IValidateCB } var _ filter.IValidator = (*HexSearchValidator)(nil) func (f *HexSearchValidator) SetValid(cb filter.IValidateCB) { f.Valid = cb } func (f *HexSearchValidator) SetInvalid(cb filter.IValidateCB) { f.Invalid = cb } func (f *HexSearchValidator) SetKilled(cb filter.IValidateCB) { f.KilledCB = cb } func (f *HexSearchValidator) SetEmpty(cb filter.IValidateCB) { f.EmptyCB = cb } func (f *HexSearchValidator) Kill() (bool, error) { return true, nil } func (f *HexSearchValidator) Validate(filter string) { if filter == "" { if f.EmptyCB != nil { f.EmptyCB.Call(filter) } return } if hexre.MatchString(filter) { if f.Valid != nil { f.Valid.Call(filter) } } else { if f.Invalid != nil { f.Invalid.Call(filter) } } } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.4.0/widgets/streamwidget/000077500000000000000000000000001426312004500176575ustar00rootroot00000000000000termshark-2.4.0/widgets/streamwidget/streamwidget.go000066400000000000000000001063251426312004500227140ustar00rootroot00000000000000// Copyright 2019-2022 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/configs/profiles" "github.com/gcla/termshark/v2/pkg/format" "github.com/gcla/termshark/v2/pkg/streams" "github.com/gcla/termshark/v2/ui/menuutil" "github.com/gcla/termshark/v2/ui/tableutil" "github.com/gcla/termshark/v2/widgets" "github.com/gcla/termshark/v2/widgets/appkeys" "github.com/gcla/termshark/v2/widgets/copymodetable" "github.com/gcla/termshark/v2/widgets/framefocus" "github.com/gcla/termshark/v2/widgets/keepselected" "github.com/gcla/termshark/v2/widgets/regexstyle" "github.com/gcla/termshark/v2/widgets/scrollabletable" "github.com/gcla/termshark/v2/widgets/trackfocus" "github.com/gcla/termshark/v2/widgets/withscrollbar" "github.com/gdamore/tcell/v2" ) //====================================================================== 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. MenuOpener menu.IOpener // For integrating with UI app - the menu needs to be told what's underneath when opened } //====================================================================== type Widget struct { gowid.IWidget opt Options tblWidgets []*copymodetable.Widget // the table for all data, client data, server data viewWidgets []gowid.IWidget // the table for all data, client data, server data selectedConv ConversationFilter // both sides (entire), client only, server only data *Data // the rest of the data from tshark -z follow stats streamStats // track turns, client packets, bytes, etc streamHeader streams.FollowHeader // first chunk of data back from tshark -z follow displayAs DisplayFormat // display as hex, ascii, raw captureDevice string // it's a very feature-specific widget so I don't care about supporting callbacks displayFilter string // "tcp.stream eq 1" Proto streams.Protocol // TCP, UDP tableHolder *holder.Widget // hold the chunk UI table convBtn *button.Widget // "Entire conversation" -> click this to open conv menu turnTxt *text.Widget // "26 clients pkts, 0 server pkts, 5 turns" sections *pile.Widget // the vertical ui layout convMenuHolder *holder.Widget // actually holds the listbox used for the open "menu" - entire, client, server convMenu *menu.Widget // the menu that opens when you hit the conversation button (entire, client, server) clickActive bool // if true, clicking in stream list will display packet selected keyState *termshark.KeyState // for vim key chords that are intended for table navigation doMenuUpdate bool // Set to true if new data has arrived and the menu needs to be regenerated. Do this // because if I regenerate each click, I lose the list state which shows the item I last clicked on. searchState // track the current highlighted search term } func New(displayFilter string, captureDevice string, proto streams.Protocol, convMenu *menu.Widget, convMenuHolder *holder.Widget, keyState *termshark.KeyState, opts ...Options) *Widget { var opt Options if len(opts) > 0 { opt = opts[0] } var mode DisplayFormat if opt.DefaultDisplay != nil { mode = opt.DefaultDisplay() } if opt.MenuOpener == nil { opt.MenuOpener = menu.OpenerFunc(widgets.OpenSimpleMenu) } res := &Widget{ opt: opt, displayFilter: displayFilter, captureDevice: captureDevice, Proto: proto, displayAs: mode, convMenu: convMenu, convMenuHolder: convMenuHolder, tblWidgets: make([]*copymodetable.Widget, 3), viewWidgets: make([]gowid.IWidget, 3), clickActive: true, keyState: keyState, } res.construct() return res } var _ gowid.IWidget = (*Widget)(nil) var _ iHighlight = (*Widget)(nil) func (w *Widget) PreviousFilter() string { return w.opt.PreviousFilter } func (w *Widget) DisplayFilter() string { return w.displayFilter } func (w *Widget) clickIsActive() bool { return w.clickActive } // Used by the stream chunk structures, which act as table models; they apply the returned highlight structure // to a a regexstyle which wraps the contents of each chunk displayed in the table. If this row is not the row // currently being searched (e.g. chunk #5 instead of chunk #2), then a default Highlight is returned which // will have no effect on the rendering of the stream chunk. func (w *Widget) highlightThis(pos table.Position) regexstyle.Highlight { if pos == w.searchRow { return regexstyle.Highlight{ Re: w.searchRe, Occ: w.searchOccurrence, Style: gowid.MakePaletteRef("stream-match"), } } return regexstyle.Highlight{} } // The widget displayed in the first line of the stream reassembly UI. func (w *Widget) makeHeaderWidget() gowid.IWidget { var headerText string var headerText1 string var headerText2 string var headerText3 string if w.Proto != streams.Unspecified { headerText1 = fmt.Sprintf("Follow %s Stream", w.Proto) } if w.displayFilter != "" { headerText2 = fmt.Sprintf("(%s)", w.displayFilter) } if w.captureDevice != "" { headerText3 = fmt.Sprintf("- %s", w.captureDevice) } headerText = strings.Join([]string{headerText1, headerText2, headerText3}, " ") headerView := overlay.New( hpadding.New(w.opt.CopyModeWidget, gowid.HAlignMiddle{}, fixed), hpadding.New( text.New(headerText), gowid.HAlignMiddle{}, fixed, ), gowid.VAlignTop{}, gowid.RenderWithRatio{R: 1}, gowid.HAlignMiddle{}, gowid.RenderWithRatio{R: 1}, overlay.Options{ BottomGetsFocus: true, TopGetsNoFocus: true, BottomGetsCursor: true, }, ) return headerView } func MakeConvMenu(opener menu.IOpener) (*holder.Widget, *menu.Widget) { convListBoxWidgetHolder := holder.New(null.New()) convMenu := menu.New("conv", convListBoxWidgetHolder, fixed, menu.Options{ Modal: true, CloseKeysProvided: true, OpenCloser: opener, CloseKeys: []gowid.IKey{ gowid.MakeKey('q'), gowid.MakeKeyExt(tcell.KeyLeft), gowid.MakeKeyExt(tcell.KeyEscape), gowid.MakeKeyExt(tcell.KeyCtrlC), }, }) return convListBoxWidgetHolder, convMenu } func (w *Widget) updateConvMenuWidget(app gowid.IApp) { convListBox, _ := w.makeConvMenuWidget() w.convMenuHolder.SetSubWidget(convListBox, app) w.setConvButtonText(app) w.setTurnText(app) w.doMenuUpdate = false } func (w *Widget) makeConvMenuWidget() (gowid.IWidget, int) { 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.opt.MenuOpener.CloseMenu(w.convMenu, 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.opt.MenuOpener.CloseMenu(w.convMenu, 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.opt.MenuOpener.CloseMenu(w.convMenu, app) w.selectedConv = ServerOnly w.tableHolder.SetSubWidget(w.viewWidgets[w.selectedConv], app) w.setConvButtonText(app) }, }, ) } return menuutil.MakeMenuWithHotKeys(savedItems, nil) } // Turns an array of stream chunks into a pair of (a) a scrollable table // widget to be displayed, and (b) the underlying table so that its model // can be manipulated. func (w *Widget) makeTable(i int) (gowid.IWidget, *copymodetable.Widget) { data := w.data.vdata[i].hexChunks btbl := &table.BoundedWidget{Widget: table.New(data)} cmtbl := copymodetable.New( btbl, data, data, "streamtable", copyModePalette{}, ) sc := appkeys.New( keepselected.New( withscrollbar.New( scrollabletable.New(cmtbl), withscrollbar.Options{ HideIfContentFits: true, }, ), ), tableutil.GotoHandler(&tableutil.GoToAdapter{ BoundedWidget: btbl, KeyState: w.keyState, }), ) return sc, cmtbl } // "26 clients pkts, 0 server pkts, 5 turns" func (w *Widget) getTurnContent() *text.Content { cpkts := gwutil.If(w.stats.clientPackets == 1, "pkt", "pkts").(string) spkts := gwutil.If(w.stats.serverPackets == 1, "pkt", "pkts").(string) turns := gwutil.If(w.stats.turns == 1, "turn", "turns").(string) return text.NewContent([]text.ContentSegment{ text.StyledContent(fmt.Sprintf("%d client %s", w.stats.clientPackets, cpkts), gowid.MakePaletteRef("stream-client")), text.StringContent(", "), text.StyledContent(fmt.Sprintf("%d server %s", w.stats.serverPackets, spkts), gowid.MakePaletteRef("stream-server")), text.StringContent(fmt.Sprintf(", %d %s", w.stats.turns, turns)), }) } func (w *Widget) setTurnText(app gowid.IApp) { w.turnTxt.SetContent(app, w.getTurnContent()) } func (w *Widget) getConvButtonText(typ ConversationFilter) string { var txt string switch typ { case Entire: txt = fmt.Sprintf("Entire conversation (%d bytes)", w.stats.clientBytes+w.stats.serverBytes) case ClientOnly: txt = fmt.Sprintf("%s → %s (%d bytes)", w.streamHeader.Node0, w.streamHeader.Node1, w.stats.clientBytes) case ServerOnly: txt = fmt.Sprintf("%s → %s (%d bytes)", w.streamHeader.Node1, w.streamHeader.Node0, w.stats.serverBytes) } return txt } // Set the text of the button showing "entire conversation", client only, server only func (w *Widget) setConvButtonText(app gowid.IApp) { w.convBtn.SetSubWidget(text.New(w.getConvButtonText(w.selectedConv)), app) } func (w *Widget) construct() { w.data = newData(w.opt.ChunkClicker, w, w, w) fixed := fixed rbgroup := make([]radio.IWidget, 0) rb1 := radio.New(&rbgroup) rbt1 := text.New(" hex ") rb2 := radio.New(&rbgroup) rbt2 := text.New(" ascii ") rb3 := radio.New(&rbgroup) rbt3 := text.New(" raw ") switch w.displayAs { case Hex: rb1.Select(nil) case Ascii: rb2.Select(nil) default: rb3.Select(nil) } c2cols := []gowid.IContainerWidget{ &gowid.ContainerWidget{rb1, fixed}, &gowid.ContainerWidget{rbt1, fixed}, &gowid.ContainerWidget{rb2, fixed}, &gowid.ContainerWidget{rbt2, fixed}, &gowid.ContainerWidget{rb3, fixed}, &gowid.ContainerWidget{rbt3, fixed}, } cols2 := columns.New(c2cols) rb1.OnClick(gowid.WidgetCallback{"cb", func(app gowid.IApp, w2 gowid.IWidget) { if rb1.Selected { w.displayAs = Hex for i := 0; i < len(w.tblWidgets); i++ { w.updateChunkModel(i, w.displayAs, app) } profiles.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) } profiles.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) } profiles.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) { if w.doMenuUpdate { w.updateConvMenuWidget(app) } w.opt.MenuOpener.OpenMenu(w.convMenu, 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.doMenuUpdate = true convCols := columns.NewFixed(convBtnSite, styledConvBtn) searchBox := edit.New(edit.Options{ Caption: "Find: ", }) reCheck := checkbox.New(false) caseCheck := checkbox.New(true) nextClick := func(app gowid.IApp, w2 gowid.IWidget) { txt := searchBox.Text() if txt == "" { w.opt.ErrorHandler.OnError("Enter a search string.", app) return } if !reCheck.IsChecked() { txt = regexp.QuoteMeta(txt) } if !caseCheck.IsChecked() { txt = fmt.Sprintf("(?i)%s", txt) } newre, err := regexp.Compile(txt) if err != nil { w.opt.ErrorHandler.OnError(fmt.Sprintf("Invalid regex: %s", searchBox.Text()), app) return } if w.searchReTxt != txt { w.initSearch(newre, txt) } i := w.selectedConv w.tblWidgets[i].Cache().Purge() // Start from current table focus position w.searchRow = table.Position(w.tblWidgets[i].Focus().(list.IBoundedWalkerPosition).ToInt()) origstate := w.searchState searchDone := false fixTablePosition := false for !searchDone { rem := findMatcher(w.tblWidgets[i].At(w.searchRow)) if rem == nil { searchDone = true w.searchState = origstate // maintain old position } else { if w.maxOccurrences.IsNone() { w.maxOccurrences = gwutil.SomeInt(rem.RegexMatches()) if w.maxOccurrences.Value() == 0 { w.goToNextSearchRow() fixTablePosition = true continue } w.searchOccurrence = 0 searchDone = true } else if w.searchOccurrence < w.maxOccurrences.Val()-1 { w.searchOccurrence += 1 searchDone = true } else { w.goToNextSearchRow() fixTablePosition = true continue } } } w.tblWidgets[i].Cache().Purge() if fixTablePosition { w.tblWidgets[i].SetCurrentRow(w.searchRow) } } searchBox2 := appkeys.New( searchBox, func(ev *tcell.EventKey, app gowid.IApp) bool { res := false switch ev.Key() { case tcell.KeyEnter: nextClick(app, nil) res = true } return res }, appkeys.Options{ ApplyBefore: true, }, ) searchBoxStyled := styled.New(searchBox2, gowid.MakePaletteRef("stream-search"), ) nextBtn := button.New(text.New("Next")) nextBtn.OnClick(gowid.MakeWidgetCallback("cb", nextClick)) pad := text.New(" ") hpad := hpadding.New(pad, gowid.HAlignLeft{}, fixed) vline := &gowid.ContainerWidget{ IWidget: fill.New('|'), D: gowid.RenderWithUnits{U: 1}, } streamsFooter1 := columns.NewWithDim( fixed, hpad, hpadding.New( w.turnTxt, gowid.HAlignLeft{}, fixed, ), hpad, vline, hpad, &gowid.ContainerWidget{ IWidget: searchBoxStyled, D: gowid.RenderWithWeight{W: 1}, }, hpad, hpadding.New( clicktracker.New( styled.NewExt( nextBtn, gowid.MakePaletteRef("button"), gowid.MakePaletteRef("button-focus"), ), ), gowid.HAlignLeft{}, fixed, ), hpad, vline, hpad, reCheck, hpad, text.New("Regex"), hpad, caseCheck, hpad, text.New("Case"), hpad, ) streamsFooter := columns.NewWithDim( gowid.RenderWithWeight{1}, hpadding.New( convCols, gowid.HAlignMiddle{}, fixed, ), hpadding.New( cols2, gowid.HAlignMiddle{}, fixed, ), hpadding.New( //styled.NewInvertedFocus(filterOutBtn, gowid.MakePaletteRef("default")), styled.NewExt( filterOutBtn, gowid.MakePaletteRef("button"), gowid.MakePaletteRef("button-focus"), ), gowid.HAlignMiddle{}, fixed, ), ) // In case it's not made header := w.makeHeaderWidget() //w.headerHolder = holder.New(w.header) streamsHeader := columns.NewWithDim( gowid.RenderWithWeight{1}, //w.headerHolder, header, ) w.tableHolder = holder.New(null.New()) mainpane := trackfocus.New( styled.New( framed.NewUnicode( w.tableHolder, ), gowid.MakePaletteRef("mainpane"), ), ) // Track whether the stream chunk list has focus. If it is clicked when it doesn't have focus, // don't annoy the user by displaying the selected packet in the underlying packet list. We // assume the user is just clicking to change the focus. mainpane.OnFocusLost(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, w2 gowid.IWidget) { w.clickActive = false })) mainpane.OnFocusGained(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, w2 gowid.IWidget) { w.clickActive = true })) mainPaneWithKeys := appkeys.NewMouse( appkeys.New( mainpane, widgets.SwallowMovementKeys, ), widgets.SwallowMouseScroll, // So we don't scroll out of the stream chunk list - it's annoying. Use tab instead. ) streamView := pile.New( []gowid.IContainerWidget{ &gowid.ContainerWidget{ streamsHeader, gowid.RenderWithUnits{U: 1}, }, &gowid.ContainerWidget{ mainPaneWithKeys, gowid.RenderWithWeight{W: 1}, }, &gowid.ContainerWidget{ streamsFooter1, gowid.RenderWithUnits{U: 1}, }, &gowid.ContainerWidget{ divider.NewUnicode(), gowid.RenderFlow{}, }, &gowid.ContainerWidget{ streamsFooter, gowid.RenderWithUnits{U: 1}, }, }, ) streamViewWithKeysAfter := appkeys.New( streamView, func(ev *tcell.EventKey, app gowid.IApp) bool { return streamViewKeyPressAfter(streamView, ev, app) }, appkeys.Options{ ApplyBefore: true, }, ) w.sections = streamView w.IWidget = streamViewWithKeysAfter for i := 0; i < len(w.tblWidgets); i++ { j := i // avoid loop variable gotcha w.viewWidgets[i], w.tblWidgets[i] = w.makeTable(i) w.tblWidgets[i].OnFocusChanged(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, w2 gowid.IWidget) { // reset search on manual moving of table w.goToSearchRow(table.Position(w.tblWidgets[j].Focus().(list.IBoundedWalkerPosition).ToInt())) })) w.updateChunkModel(i, w.displayAs, nil) } w.tableHolder.SetSubWidget(w.viewWidgets[w.selectedConv], nil) } func (w *Widget) updateChunkModel(i int, f DisplayFormat, app gowid.IApp) { switch f { case Hex: w.tblWidgets[i].SetModel(w.data.vdata[i].hexChunks, app) w.tblWidgets[i].RowClip = w.data.vdata[i].hexChunks w.tblWidgets[i].AllClip = w.data.vdata[i].hexChunks case Ascii: w.tblWidgets[i].SetModel(w.data.vdata[i].asciiChunks, app) w.tblWidgets[i].RowClip = w.data.vdata[i].asciiChunks w.tblWidgets[i].AllClip = w.data.vdata[i].asciiChunks case Raw: w.tblWidgets[i].SetModel(w.data.vdata[i].rawChunks, app) w.tblWidgets[i].RowClip = w.data.vdata[i].rawChunks w.tblWidgets[i].AllClip = w.data.vdata[i].rawChunks } } //====================================================================== type iMatcher interface { RegexMatches() int // the number of times the regex matchesd in the data SetRegexOccurrence(i int) // highlight the ith occurrence of the match SetRegex(re *regexp.Regexp) // use this regular expression } // A utility to find the regex matching widget within the stream reassembly table // widget hierarchy. func findMatcher(w gowid.IWidget) iMatcher { res := gowid.FindInHierarchy(w, true, gowid.WidgetPredicate(func(w gowid.IWidget) bool { var res bool if _, ok := w.(iMatcher); ok { res = true } return res })) if res == nil { return nil } else { return res.(iMatcher) } } func (w *Widget) TrackPayloadPacket(packet int) { w.data.pktIndices = append(w.data.pktIndices, packet) } func (w *Widget) NumChunks() int { return len(w.data.vdata[Entire].hexChunks.chunks) } func (w *Widget) Finished() bool { return w.data.finished } func (w *Widget) SetFinished(f bool) { w.data.finished = f } func (w *Widget) SetFocusOnChunksIfPossible(app gowid.IApp) { if w.NumChunks() == 0 { w.sections.SetFocus(app, 4) } else { w.sections.SetFocus(app, 1) } } func (w *Widget) SetCurrentRow(row table.Position) { w.tblWidgets[w.selectedConv].SetCurrentRow(row) } func (w *Widget) GoToMiddle(app gowid.IApp) { w.tblWidgets[w.selectedConv].GoToMiddle(app) } // Hardcoded - yuck! func setFocusOnSearchBox(app gowid.IApp, view gowid.IWidget) { gowid.SetFocusPath(view, []interface{}{2, 5}, app) } func streamViewKeyPressAfter(sections *pile.Widget, evk *tcell.EventKey, app gowid.IApp) bool { handled := false if evk.Key() == tcell.KeyTAB { if next, ok := sections.FindNextSelectable(gowid.Forwards, true); ok { sections.SetFocus(app, next) handled = true } } else if evk.Key() == tcell.KeyBacktab { if next, ok := sections.FindNextSelectable(gowid.Backwards, true); ok { sections.SetFocus(app, next) handled = true } } else if evk.Rune() == '/' { setFocusOnSearchBox(app, sections) handled = true } return handled } func (w *Widget) String() string { return "streamreassembly" } func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { return w.IWidget.UserInput(ev, size, focus, app) } func (w *Widget) AddHeader(hdr streams.FollowHeader, app gowid.IApp) { w.streamHeader = hdr w.doMenuUpdate = true } func (w *Widget) MapChunkToTableRow(chunk int) (int, error) { if chunk < len(w.data.vdata[w.selectedConv].subIndices) { gchunk := w.data.vdata[w.selectedConv].subIndices[chunk] if gchunk < len(w.data.pktIndices) { return w.data.pktIndices[gchunk], nil } } err := gowid.WithKVs(PacketRowNotLoadedError, map[string]interface{}{ "row": chunk, }) return -1, err } func (w *Widget) AddChunkEntire(ch streams.IChunk, app gowid.IApp) { dir := ch.Direction() if w.stats.lastTurn != dir && w.stats.started { w.stats.turns++ } w.stats.lastTurn = dir w.stats.started = true switch dir { case streams.Client: w.stats.clientPackets++ w.stats.clientBytes += len(ch.StreamData()) case streams.Server: w.stats.serverPackets++ w.stats.serverBytes += len(ch.StreamData()) } w.data.vdata[Entire].hexChunks.chunks = append(w.data.vdata[Entire].hexChunks.chunks, ch) w.data.vdata[Entire].update() w.data.vdata[Entire].subIndices = append(w.data.vdata[Entire].subIndices, w.data.currentChunk) switch dir { case streams.Client: w.data.vdata[ClientOnly].hexChunks.chunks = append(w.data.vdata[ClientOnly].hexChunks.chunks, ch) w.data.vdata[ClientOnly].update() w.data.vdata[ClientOnly].subIndices = append(w.data.vdata[ClientOnly].subIndices, w.data.currentChunk) case streams.Server: w.data.vdata[ServerOnly].hexChunks.chunks = append(w.data.vdata[ServerOnly].hexChunks.chunks, ch) w.data.vdata[ServerOnly].update() w.data.vdata[ServerOnly].subIndices = append(w.data.vdata[ServerOnly].subIndices, w.data.currentChunk) } // Update the copymodetable's data - otherwise the slice is stale for i := 0; i < len(w.tblWidgets); i++ { // Loop over all conmv views - entire, client, server w.updateChunkModel(i, w.displayAs, app) } w.updateConvMenuWidget(app) w.data.currentChunk++ } //====================================================================== type iClickIsActive interface { clickIsActive() bool } type iHighlight interface { highlightThis(pos table.Position) regexstyle.Highlight } type IFilterOut interface { PreviousFilter() string DisplayFilter() string } type IOnError interface { OnError(msg string, app gowid.IApp) } type iMapChunkToTableRow interface { MapChunkToTableRow(chunk int) (int, error) } // Supplied by user of widget - what UI changes to make when packet is clicked type IChunkClicked interface { OnPacketClicked(pkt int, app gowid.IApp) error HandleError(row table.RowId, err error, app gowid.IApp) } // Used by widget - first map table click to packet number, then use IChunkClicked type iChunkClicker interface { IChunkClicked iClickIsActive iMapChunkToTableRow iHighlight } type chunkList struct { clicker iChunkClicker chunks []streams.IChunk } type asciiChunkList struct { *chunkList } type rawChunkList struct { *chunkList } var _ table.IBoundedModel = chunkList{} var _ table.IBoundedModel = asciiChunkList{} var _ table.IBoundedModel = rawChunkList{} var _ copymodetable.IRowCopier = chunkList{} var _ copymodetable.IRowCopier = asciiChunkList{} var _ copymodetable.IRowCopier = rawChunkList{} var _ copymodetable.ITableCopier = chunkList{} var _ copymodetable.ITableCopier = asciiChunkList{} var _ copymodetable.ITableCopier = rawChunkList{} // CopyTable is here to implement copymodetable.IRowCopier func (c chunkList) CopyRow(rowid table.RowId) []gowid.ICopyResult { hexd := format.HexDump(c.chunks[int(rowid)].StreamData()) return []gowid.ICopyResult{ gowid.CopyResult{ Name: "Copy hexdump", Val: hexd, }, } } func (c asciiChunkList) CopyRow(rowid table.RowId) []gowid.ICopyResult { prt := format.MakePrintableStringWithNewlines(c.chunks[int(rowid)].StreamData()) return []gowid.ICopyResult{ gowid.CopyResult{ Name: "Copy ascii", Val: prt, }, } } func (c rawChunkList) CopyRow(rowid table.RowId) []gowid.ICopyResult { raw := format.MakeHexStream(c.chunks[int(rowid)].StreamData()) return []gowid.ICopyResult{ gowid.CopyResult{ Name: "Copy raw", Val: raw, }, } } // CopyTable is here to implement copymodetable.ITableCopier func (c asciiChunkList) CopyTable() []gowid.ICopyResult { prtl := make([]string, 0, len(c.chunks)) for i := 0; i < len(c.chunks); i++ { prtl = append(prtl, format.MakePrintableStringWithNewlines(c.chunks[i].StreamData())) } prt := strings.Join(prtl, "\n") return []gowid.ICopyResult{ gowid.CopyResult{ Name: "Copy ascii", Val: prt, }, } } // CopyTable is here to implement copymodetable.ITableCopier func (c chunkList) CopyTable() []gowid.ICopyResult { hexdl := make([]string, 0, len(c.chunks)) for i := 0; i < len(c.chunks); i++ { hex := format.HexDump(c.chunks[i].StreamData()) if c.chunks[i].Direction() == streams.Server { hex = indentRe.ReplaceAllString(hex, ` $1`) } hexdl = append(hexdl, hex) } hexd := strings.Join(hexdl, "\n") return []gowid.ICopyResult{ gowid.CopyResult{ Name: "Copy hexdump", Val: hexd, }, } } // CopyTable is here to implement copymodetable.ITableCopier func (c rawChunkList) CopyTable() []gowid.ICopyResult { rawl := make([]string, 0, len(c.chunks)) for i := 0; i < len(c.chunks); i++ { rawl = append(rawl, format.MakeHexStream(c.chunks[i].StreamData())) } raw := strings.Join(rawl, "\n") return []gowid.ICopyResult{ gowid.CopyResult{ Name: "Copy raw", Val: raw, }, } } // makeButton constructs a row for the stream list that if clicked will select the // appropriate packet in the packet list func (c chunkList) makeButton(row table.RowId, ch gowid.IWidget) *button.Widget { btn := button.NewBare(ch) //btn.OnClickDown(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, widget gowid.IWidget) { btn.OnClick(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, widget gowid.IWidget) { if c.clicker != nil && c.clicker.clickIsActive() { if irow, err := c.clicker.MapChunkToTableRow(int(row)); err != nil { c.clicker.HandleError(row, err, app) } else { c.clicker.OnPacketClicked(irow, app) } } }), ) return btn } func (c chunkList) CellWidgets(row table.RowId) []gowid.IWidget { res := make([]gowid.IWidget, 1) var ch gowid.IWidget // not sorted hilite := c.clicker.highlightThis(table.Position(row)) datastr := format.HexDump(c.chunks[row].StreamData()) if c.chunks[row].Direction() == streams.Server { datastr = indentRe.ReplaceAllString(datastr, ` $1`) } dataw := framefocus.New( selectable.New( regexstyle.New( text.New(datastr), hilite, ), ), ) if c.chunks[row].Direction() == streams.Client { ch = styled.New( dataw, gowid.MakePaletteRef("stream-client"), ) } else { ch = styled.New( dataw, gowid.MakePaletteRef("stream-server"), ) } res[0] = c.makeButton(row, ch) return res } //====================================================================== func (c asciiChunkList) CellWidgets(row table.RowId) []gowid.IWidget { res := make([]gowid.IWidget, 1) hl := c.clicker.highlightThis(table.Position(row)) str := framefocus.NewSlim( selectable.New( regexstyle.New( text.New(strings.TrimSuffix(format.MakePrintableStringWithNewlines((*c.chunkList).chunks[row].StreamData()), "\n")), hl, ), ), ) var ch gowid.IWidget if (*c.chunkList).chunks[row].Direction() == streams.Client { ch = styled.New( str, gowid.MakePaletteRef("stream-client"), ) } else { ch = styled.New( str, gowid.MakePaletteRef("stream-server"), ) } res[0] = c.makeButton(row, ch) return res } func (c rawChunkList) CellWidgets(row table.RowId) []gowid.IWidget { res := make([]gowid.IWidget, 1) hl := c.clicker.highlightThis(table.Position(row)) str := framefocus.New( selectable.New( regexstyle.New( text.New(format.MakeHexStream((*c.chunkList).chunks[row].StreamData())), hl, ), ), ) var ch gowid.IWidget if (*c.chunkList).chunks[row].Direction() == streams.Client { ch = styled.New( str, gowid.MakePaletteRef("stream-client"), ) } else { ch = styled.New( str, gowid.MakePaletteRef("stream-server"), ) } res[0] = c.makeButton(row, ch) return res } func (c asciiChunkList) Widths() []gowid.IWidgetDimension { return []gowid.IWidgetDimension{gowid.RenderWithWeight{W: 1}} } func (c chunkList) Widths() []gowid.IWidgetDimension { return []gowid.IWidgetDimension{gowid.RenderWithWeight{W: 1}} } func (c chunkList) Columns() int { return 1 } func (c chunkList) Rows() int { return len(c.chunks) } func (c chunkList) HorizontalSeparator() gowid.IWidget { return nil } func (c chunkList) HeaderSeparator() gowid.IWidget { return nil } func (c chunkList) HeaderWidgets() []gowid.IWidget { return nil } func (c chunkList) VerticalSeparator() gowid.IWidget { return nil } func (c chunkList) RowIdentifier(row int) (table.RowId, bool) { if row < 0 || row >= len(c.chunks) { return -1, false } return table.RowId(row), true } //====================================================================== // TODO - duplicated from termshark type copyModePalette struct{} var _ gowid.IClipboardSelected = copyModePalette{} func (r copyModePalette) AlterWidget(w gowid.IWidget, app gowid.IApp) gowid.IWidget { return styled.New(w, gowid.MakePaletteRef("copy-mode"), styled.Options{ OverWrite: true, }, ) } //====================================================================== type searchState struct { searchReTxt string searchRe *regexp.Regexp searchRow table.Position searchOccurrence int maxOccurrences gwutil.IntOption } func (s *searchState) initSearch(re *regexp.Regexp, txt string) { s.searchReTxt = txt s.searchRe = re s.searchRow = 0 } func (s *searchState) goToSearchRow(row table.Position) { s.searchRow = row s.searchOccurrence = 0 s.maxOccurrences = gwutil.NoneInt() } func (s *searchState) goToNextSearchRow() { s.goToSearchRow(s.searchRow + 1) } func (w searchState) String() string { return fmt.Sprintf("[re='%s' row=%d occ=%d maxocc=%v]", w.searchReTxt, w.searchRow, w.searchOccurrence, w.maxOccurrences) } //====================================================================== // Represents the view of the data from either both sides, client side or server side type ViewData struct { subIndices []int // [0,1,2,3,...] - index into pktIndices hexChunks chunkList asciiChunks asciiChunkList rawChunks rawChunkList } func newViewData(clicker IChunkClicked, ca iClickIsActive, mapper iMapChunkToTableRow, hiliter iHighlight) *ViewData { clickMapper := struct { IChunkClicked iClickIsActive iMapChunkToTableRow iHighlight }{ IChunkClicked: clicker, iClickIsActive: ca, iMapChunkToTableRow: mapper, iHighlight: hiliter, } res := &ViewData{ subIndices: make([]int, 0, 16), hexChunks: chunkList{ clicker: clickMapper, chunks: make([]streams.IChunk, 0, 16), }, } res.update() return res } func (v *ViewData) update() { v.asciiChunks = asciiChunkList{ chunkList: &v.hexChunks, } v.rawChunks = rawChunkList{ chunkList: &v.hexChunks, } } //====================================================================== // Represents all the streamed data type Data struct { pktIndices []int // [0,2,5,12...] - frame numbers (-1) for each packet of this stream vdata []*ViewData // for each of (a) whole view (b) client (c) server currentChunk int // add to client or server view finished bool } func newData(clicker IChunkClicked, ca iClickIsActive, mapper iMapChunkToTableRow, hiliter iHighlight) *Data { vdata := make([]*ViewData, 0, 3) for i := 0; i < 3; i++ { vdata = append(vdata, newViewData(clicker, ca, mapper, hiliter)) } res := &Data{ pktIndices: make([]int, 0, 16), vdata: vdata, } return res } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.4.0/widgets/trackfocus/000077500000000000000000000000001426312004500173245ustar00rootroot00000000000000termshark-2.4.0/widgets/trackfocus/trackfocus.go000066400000000000000000000040701426312004500220200ustar00rootroot00000000000000// Copyright 2019-2022 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.4.0/widgets/trackfocus/trackfocus_test.go000066400000000000000000000022671426312004500230650ustar00rootroot00000000000000// Copyright 2019-2022 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.4.0/widgets/utils.go000066400000000000000000000031311426312004500166450ustar00rootroot00000000000000// Copyright 2019-2022 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/gcla/gowid/widgets/menu" "github.com/gdamore/tcell/v2" ) //====================================================================== func SwallowMouseScroll(ev *tcell.EventMouse, app gowid.IApp) bool { res := false switch ev.Buttons() { case tcell.WheelDown: res = true case tcell.WheelUp: res = true } return res } func SwallowMovementKeys(ev *tcell.EventKey, app gowid.IApp) bool { res := false switch ev.Key() { case tcell.KeyDown, tcell.KeyCtrlN, tcell.KeyUp, tcell.KeyCtrlP, tcell.KeyRight, tcell.KeyCtrlF, tcell.KeyLeft, tcell.KeyCtrlB: res = true case tcell.KeyRune: switch ev.Rune() { case 'h', 'j', 'k', 'l': res = true } } return res } //====================================================================== // Return false if it was already open type MenuOpenerFunc func(bool, *menu.Widget, menu.ISite, gowid.IApp) bool func (m MenuOpenerFunc) OpenMenu(mu *menu.Widget, site *menu.SiteWidget, app gowid.IApp) bool { return m(true, mu, site, app) } func (m MenuOpenerFunc) CloseMenu(mu *menu.Widget, app gowid.IApp) { m(false, mu, nil, app) } func OpenSimpleMenu(open bool, mu *menu.Widget, site menu.ISite, app gowid.IApp) bool { if open { mu.Open(site, app) return true } else { mu.Close(app) return true } } //====================================================================== // Local Variables: // mode: Go // fill-column: 78 // End: termshark-2.4.0/widgets/withscrollbar/000077500000000000000000000000001426312004500200375ustar00rootroot00000000000000termshark-2.4.0/widgets/withscrollbar/withscrollbar.go000066400000000000000000000145031426312004500232500ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package withscrollbar provides a widget that renders with a scrollbar on the right package withscrollbar import ( "github.com/gcla/gowid" "github.com/gcla/gowid/widgets/columns" "github.com/gcla/gowid/widgets/list" "github.com/gcla/gowid/widgets/selectable" "github.com/gcla/gowid/widgets/table" "github.com/gcla/gowid/widgets/vscroll" "github.com/gdamore/tcell/v2" ) //====================================================================== type Widget struct { always *columns.Widget // use if scrollbar is to be shown w IScrollSubWidget sb *vscroll.Widget goUpDown int // positive means down pgUpDown int // positive means down frac float32 // positive means down fracSet bool opt Options } var _ gowid.IWidget = (*Widget)(nil) type Options struct { HideIfContentFits bool } type IScrollValues interface { ScrollPosition() int ScrollLength() int } // Implemented by widgets that can scroll type IScrollOneLine interface { Up(lines int, size gowid.IRenderSize, app gowid.IApp) Down(lines int, size gowid.IRenderSize, app gowid.IApp) } type IScrollOnePage interface { UpPage(num int, size gowid.IRenderSize, app gowid.IApp) DownPage(num int, size gowid.IRenderSize, app gowid.IApp) } type IScrollHome interface { GoHome(size gowid.IRenderSize, app gowid.IApp) } type IScrollToEnd interface { GoToEnd(size gowid.IRenderSize, app gowid.IApp) } type IScrollSubWidget interface { gowid.IWidget IScrollValues IScrollOneLine IScrollOnePage } func New(w IScrollSubWidget, opts ...Options) *Widget { var opt Options if len(opts) > 0 { opt = opts[0] } sb := vscroll.NewExt(vscroll.VerticalScrollbarUnicodeRunes) res := &Widget{ always: columns.New([]gowid.IContainerWidget{ &gowid.ContainerWidget{ IWidget: w, D: gowid.RenderWithWeight{W: 1}, }, // So that the vscroll doesn't take the focus when moving from above // and below in the main termshark window &gowid.ContainerWidget{ IWidget: selectable.NewUnselectable(sb), D: gowid.RenderWithUnits{U: 1}, }, }), w: w, sb: sb, opt: opt, } sb.OnClickAbove(gowid.MakeWidgetCallback("cb", res.clickUp)) sb.OnClickBelow(gowid.MakeWidgetCallback("cb", res.clickDown)) sb.OnRightClick(gowid.MakeWidgetCallbackExt("cb", res.rightClick)) sb.OnClickUpArrow(gowid.MakeWidgetCallback("cb", res.clickUpArrow)) sb.OnClickDownArrow(gowid.MakeWidgetCallback("cb", res.clickDownArrow)) return res } func (e *Widget) clickUp(app gowid.IApp, w gowid.IWidget) { e.pgUpDown -= 1 } func (e *Widget) clickDown(app gowid.IApp, w gowid.IWidget) { e.pgUpDown += 1 } func (e *Widget) rightClick(app gowid.IApp, w gowid.IWidget, data ...interface{}) { frac := data[0].(float32) e.frac = frac e.fracSet = true } func (e *Widget) clickUpArrow(app gowid.IApp, w gowid.IWidget) { e.goUpDown -= 1 } func (e *Widget) clickDownArrow(app gowid.IApp, w gowid.IWidget) { e.goUpDown += 1 } // Don't attempt to calculate actual rendered rows - it's terribly slow, and O(n) rows. func CalculateMenuRows(vals IScrollValues, rows int, focus gowid.Selector, app gowid.IApp) (int, int, int) { return vals.ScrollPosition(), 1, vals.ScrollLength() - (vals.ScrollPosition() + 1) } func (w *Widget) contentFits(size gowid.IRenderSize) bool { res := true if rower, ok := size.(gowid.IRows); ok { res = (w.w.ScrollLength() <= rower.Rows()) } return res } func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { if w.opt.HideIfContentFits && w.contentFits(size) { return w.w.UserInput(ev, size, focus, app) } box, ok := size.(gowid.IRenderBox) if !ok { panic(gowid.WidgetSizeError{Widget: w, Size: size, Required: "gowid.IRenderBox"}) } x, y, z := CalculateMenuRows(w.w, box.BoxRows(), focus, app) w.sb.Top = x w.sb.Middle = y w.sb.Bottom = z if ws, ok := w.w.(IScrollOnePage); ok { if ev, ok := ev.(*tcell.EventKey); ok { switch ev.Key() { case tcell.KeyPgUp: ws.UpPage(1, size, app) return true case tcell.KeyPgDn: ws.DownPage(1, size, app) return true } } } if ws, ok := w.w.(IScrollHome); ok { if ev, ok := ev.(*tcell.EventKey); ok { switch ev.Key() { case tcell.KeyHome: ws.GoHome(size, app) return true } } } if ws, ok := w.w.(IScrollToEnd); ok { if ev, ok := ev.(*tcell.EventKey); ok { switch ev.Key() { case tcell.KeyEnd: ws.GoToEnd(size, app) return true } } } res := w.always.UserInput(ev, size, focus, app) if res { w.always.SetFocus(app, 0) } return res } type iSetPosition interface { SetPos(pos list.IBoundedWalkerPosition, app gowid.IApp) } func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { if w.opt.HideIfContentFits && w.contentFits(size) { return w.w.Render(size, focus, app) } var box gowid.IRenderBox var ok bool box, ok = size.(gowid.IRenderBox) if !ok { box = w.always.Render(size, focus, app) } ecols := box.BoxColumns() - 1 var x int var y int var z int if ecols >= 1 { ebox := gowid.MakeRenderBox(ecols, box.BoxRows()) if w.goUpDown != 0 || w.pgUpDown != 0 || w.fracSet { if w.goUpDown > 0 { w.w.Down(w.goUpDown, ebox, app) } else if w.goUpDown < 0 { w.w.Up(-w.goUpDown, ebox, app) } if w.pgUpDown > 0 { w.w.DownPage(w.pgUpDown, ebox, app) } else if w.pgUpDown < 0 { w.w.UpPage(-w.pgUpDown, ebox, app) } if w.fracSet { if wp, ok := w.w.(iSetPosition); ok { wp.SetPos(table.Position(int(float32(w.w.ScrollLength()-1)*w.frac)), app) } w.fracSet = false } } w.goUpDown = 0 w.pgUpDown = 0 x, y, z = CalculateMenuRows(w.w, box.BoxRows(), focus, app) } w.sb.Top = x w.sb.Middle = y w.sb.Bottom = z canvas := w.always.Render(size, focus, app) return canvas } func (w *Widget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { if w.opt.HideIfContentFits && w.contentFits(size) { return w.w.RenderSize(size, focus, app) } return w.always.RenderSize(size, focus, app) } func (w *Widget) Selectable() bool { return w.w.Selectable() } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: termshark-2.4.0/widgets/withscrollbar/withscrollbar_test.go000066400000000000000000000034541426312004500243120ustar00rootroot00000000000000package 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()) } termshark-2.4.0/widgets/wormhole/000077500000000000000000000000001426312004500170145ustar00rootroot00000000000000termshark-2.4.0/widgets/wormhole/wormhole.go000066400000000000000000000160711426312004500212040ustar00rootroot00000000000000// Copyright 2019-2020 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 wormhole contains a widget that provides the UI for termshark's // magic-wormhole pcap sending feature. package wormhole import ( "context" "fmt" "os" "path/filepath" "sync" "time" "github.com/gcla/gowid" "github.com/gcla/gowid/widgets/button" "github.com/gcla/gowid/widgets/clicktracker" "github.com/gcla/gowid/widgets/columns" "github.com/gcla/gowid/widgets/divider" "github.com/gcla/gowid/widgets/holder" "github.com/gcla/gowid/widgets/hpadding" "github.com/gcla/gowid/widgets/null" "github.com/gcla/gowid/widgets/pile" "github.com/gcla/gowid/widgets/progress" "github.com/gcla/gowid/widgets/styled" "github.com/gcla/gowid/widgets/text" "github.com/gcla/termshark/v2" "github.com/psanford/wormhole-william/wormhole" log "github.com/sirupsen/logrus" ) //====================================================================== var Goroutinewg *sync.WaitGroup type state uint const ( NotStarted state = 0 Sending state = iota Complete state = iota ) type ErrorFunc func(err error, app gowid.IApp) type Options struct { ErrorHandler ErrorFunc CodeLength int RendezvousURL string TransitRelayAddress string } type Widget struct { client wormhole.Client ctx context.Context cancel context.CancelFunc file *os.File code string state state status chan wormhole.SendResult opt Options // progLock sync.Mutex cancelled bool sentBytes int64 totalBytes int64 progUpdateChan chan struct{} once sync.Once // finHolder *holder.Widget view1 *holder.Widget view2 *holder.Widget view3 *holder.Widget *holder.Widget } var _ gowid.IWidget = (*Widget)(nil) var fixed gowid.RenderFixed //====================================================================== func New(filename string, app gowid.IApp, opts ...Options) (*Widget, error) { res := &Widget{} if err := newWidget(filename, app, res, opts...); err != nil { return nil, err } return res, nil } // I set up a helper function because when "Send Again" is clicked, I need to // reset the struct pointed to by the same *Widget pointer func newWidget(filename string, app gowid.IApp, w *Widget, opts ...Options) error { var opt Options if len(opts) > 0 { opt = opts[0] } if opt.ErrorHandler == nil { opt.ErrorHandler = logError } switch { case opt.CodeLength == 0: opt.CodeLength = 2 // default case opt.CodeLength < 1: opt.CodeLength = 1 case opt.CodeLength > 8: opt.CodeLength = 8 } file, err := os.Open(filename) if err != nil { return fmt.Errorf("Could not open pcap file %s: %w", filename, err) } prog := progress.New(progress.Options{ Normal: gowid.MakePaletteRef("progress-default"), Complete: gowid.MakePaletteRef("progress-complete"), }) *w = Widget{ file: file, opt: opt, } w.client.PassPhraseComponentLength = opt.CodeLength if w.opt.RendezvousURL != "" { w.client.RendezvousURL = w.opt.RendezvousURL } if w.opt.TransitRelayAddress != "" { w.client.TransitRelayAddress = w.opt.TransitRelayAddress } w.ctx, w.cancel = context.WithCancel(context.Background()) code, status, err := w.client.SendFile( w.ctx, filepath.Base(filename), file, wormhole.WithProgress(func(sentBytes int64, totalBytes int64) { w.progLock.Lock() defer w.progLock.Unlock() w.sentBytes = sentBytes w.totalBytes = totalBytes w.once.Do(func() { termshark.TrackedGo(func() { fn2 := func() { app.Run(gowid.RunFunction(func(app gowid.IApp) { w.progLock.Lock() defer w.progLock.Unlock() prog.SetTarget(app, int(w.totalBytes)) prog.SetProgress(app, int(w.sentBytes)) if w.Widget == w.view1 { w.Widget = w.view2 } })) } termshark.RunOnDoubleTicker(w.progUpdateChan, fn2, time.Duration(500)*time.Millisecond, time.Duration(500)*time.Millisecond, 1) }, Goroutinewg) }) }), ) if err != nil { return fmt.Errorf("Error initializing wormhole: %w", err) } w.code = code w.status = status w.progUpdateChan = make(chan struct{}) codew := hpadding.New( text.New(w.code), gowid.HAlignMiddle{}, gowid.RenderFixed{}, ) w.view1 = holder.New(codew) stopb := button.New(text.New("X")) stopb.OnClick(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, w2 gowid.IWidget) { w.Close() })) styledStopB := clicktracker.New( styled.NewExt( stopb, gowid.MakePaletteRef("dialog"), gowid.MakePaletteRef("dialog-button"), ), ) cls := columns.New([]gowid.IContainerWidget{ &gowid.ContainerWidget{ IWidget: prog, D: gowid.RenderWithWeight{W: 1}, }, &gowid.ContainerWidget{ IWidget: text.New(" "), D: fixed, }, &gowid.ContainerWidget{ IWidget: styledStopB, D: fixed, }, }) w.view2 = holder.New( pile.NewFlow( codew, divider.NewBlank(), cls, ), ) newb := button.New(text.New("Send Again")) newb.OnClick(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, w2 gowid.IWidget) { w.Close() newWidget(filename, app, w, w.opt) })) w.finHolder = holder.New(null.New()) w.view3 = holder.New( pile.NewFlow( hpadding.New( w.finHolder, gowid.HAlignMiddle{}, gowid.RenderFixed{}, ), divider.NewBlank(), hpadding.New( styled.NewExt( newb, gowid.MakePaletteRef("button"), gowid.MakePaletteRef("button-focus"), ), gowid.HAlignLeft{}, gowid.RenderFixed{}, ), ), ) termshark.TrackedGo(func() { // should always terminate because either status will complete or // the stop button will call cancel leading to this state change select { case wres := <-status: app.Run(gowid.RunFunction(func(app gowid.IApp) { w.progLock.Lock() defer w.progLock.Unlock() switch { case w.cancelled: w.finHolder.SetSubWidget(text.New(fmt.Sprintf("%s - cancelled!", w.code)), app) case !wres.OK: w.finHolder.SetSubWidget(text.New(fmt.Sprintf("%s - error!", w.code)), app) default: w.finHolder.SetSubWidget(text.New(fmt.Sprintf("%s - done!", w.code)), app) } w.Widget = w.view3 // Stop the progress ticket goroutine close(w.progUpdateChan) if !wres.OK && !w.cancelled { w.opt.ErrorHandler(wres.Error, app) } if w.file != nil { w.file.Close() // what would I do with error w.file = nil } })) } }, Goroutinewg) w.Widget = w.view1 return nil } func (w *Widget) CodeLength() int { return w.opt.CodeLength } func (w *Widget) Close() error { w.progLock.Lock() defer w.progLock.Unlock() w.cancelled = true w.cancel() return nil } //====================================================================== func logError(err error, app gowid.IApp) { log.Infof("Error sending via wormhole: %v", err) } // XXX-word1-word2-... - max length of word in // pgp word list is 11 func UpperBoundOnLength(words int) int { return 3 + (words * (11 + 1)) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: