Compare commits
25 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3f6edc7662 | |||
| aad9c74a63 | |||
| eede5b0e50 | |||
| 3430cae957 | |||
| a4df8fc588 | |||
| 9e8fa4d0a6 | |||
| 940e573468 | |||
| f13139e79b | |||
| ff92002dcf | |||
| 2d1c8a4ceb | |||
| 241e8e1eea | |||
| 666456e456 | |||
| 73126ba770 | |||
| 9c9f7cfa82 | |||
| 7a40326719 | |||
| 74e2da2f29 | |||
| cfde429786 | |||
| afa13bf920 | |||
| 38fbfbd918 | |||
| 663ce42016 | |||
| e44d4dea7a | |||
| 4fe255e7d2 | |||
| b381f71692 | |||
| 7e047626df | |||
| 68f3c4fa4e |
13 changed files with 1578 additions and 535 deletions
119
.github/workflows/release.yaml
vendored
119
.github/workflows/release.yaml
vendored
|
|
@ -1,10 +1,3 @@
|
||||||
# From https://github.com/BurntSushi/ripgrep/blob/master/.github/workflows/release.yml
|
|
||||||
# Which is also via https://eugene-babichenko.github.io/blog/2020/05/09/github-actions-cross-platform-auto-releases/
|
|
||||||
# ...both of which are very good.
|
|
||||||
#
|
|
||||||
# I'm sure I don't need half the stuff I have in here (around cargo
|
|
||||||
# customization and whatnot) but.
|
|
||||||
#
|
|
||||||
name: release
|
name: release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
|
@ -13,112 +6,96 @@ on:
|
||||||
tags:
|
tags:
|
||||||
- "v[0-9]+.[0-9]+.[0-9]+"
|
- "v[0-9]+.[0-9]+.[0-9]+"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
create_release:
|
create_release:
|
||||||
name: Create release
|
name: Create release
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
outputs:
|
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Get the release version
|
||||||
|
if: env.VERSION == ''
|
||||||
|
run: echo "VERSION=${{ github.ref_name }}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Show the version
|
||||||
|
run: |
|
||||||
|
echo "version is: $VERSION"
|
||||||
|
|
||||||
- name: Create GitHub release
|
- name: Create GitHub release
|
||||||
id: create_release
|
|
||||||
uses: actions/create-release@v1
|
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
run: gh release create $VERSION --draft --verify-tag --title $VERSION
|
||||||
tag_name: ${{ github.ref }}
|
|
||||||
release_name: Release ${{ github.ref }}
|
|
||||||
draft: true
|
|
||||||
|
|
||||||
release_assets:
|
outputs:
|
||||||
name: Release assets
|
version: ${{ env.VERSION }}
|
||||||
|
|
||||||
|
build_release:
|
||||||
|
name: Build all the stuff
|
||||||
needs: ['create_release'] # We need to know the upload URL
|
needs: ['create_release'] # We need to know the upload URL
|
||||||
runs-on: ${{ matrix.os }} # We run many different builds
|
runs-on: ${{ matrix.os }} # We run many different builds
|
||||||
env:
|
env:
|
||||||
# For some builds, we use cross to test on 32-bit and big-endian
|
|
||||||
# systems.
|
|
||||||
CARGO: cargo
|
|
||||||
# When CARGO is set to CROSS, this is set to `--target matrix.target`.
|
|
||||||
TARGET_FLAGS: ""
|
|
||||||
# When CARGO is set to CROSS, TARGET_DIR includes matrix.target.
|
|
||||||
TARGET_DIR: ./target
|
|
||||||
# Emit backtraces on panics.
|
# Emit backtraces on panics.
|
||||||
RUST_BACKTRACE: 1
|
RUST_BACKTRACE: 1
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
build: ['linux', 'macos', 'arm-macos', 'windows']
|
build: ['linux', 'debian', 'macos', 'arm-macos', 'windows']
|
||||||
include:
|
include:
|
||||||
- build: linux
|
- build: linux
|
||||||
os: ubuntu-22.04
|
os: ubuntu-22.04
|
||||||
target: x86_64-unknown-linux-musl
|
target: x86_64-unknown-linux-musl
|
||||||
|
packages: apt
|
||||||
|
|
||||||
|
- build: debian
|
||||||
|
os: ubuntu-22.04
|
||||||
|
target: x86_64-unknown-linux-musl
|
||||||
|
packages: apt
|
||||||
|
|
||||||
- build: macos
|
- build: macos
|
||||||
os: macos-latest
|
os: macos-latest
|
||||||
target: x86_64-apple-darwin
|
target: x86_64-apple-darwin
|
||||||
|
packages: brew
|
||||||
|
|
||||||
- build: arm-macos
|
- build: arm-macos
|
||||||
os: macos-latest
|
os: macos-latest
|
||||||
target: aarch64-apple-darwin
|
target: aarch64-apple-darwin
|
||||||
|
packages: brew
|
||||||
|
|
||||||
- build: windows
|
- build: windows
|
||||||
os: windows-2022
|
os: windows-2022
|
||||||
target: x86_64-pc-windows-msvc
|
target: x86_64-pc-windows-msvc
|
||||||
|
packages: none
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Rust
|
- name: Install packages (linux)
|
||||||
|
if: matrix.packages == 'apt'
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y pandoc
|
||||||
|
|
||||||
|
- name: Install packages (macos)
|
||||||
|
if: matrix.packages == 'brew'
|
||||||
|
run: |
|
||||||
|
brew update
|
||||||
|
brew install pandoc
|
||||||
|
|
||||||
|
- name: Install rust
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
target: ${{ matrix.target }}
|
target: ${{ matrix.target }}
|
||||||
|
|
||||||
- name: Use Cross
|
- name: Run the release automation
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
|
||||||
cargo install cross
|
|
||||||
echo "CARGO=cross" >> $GITHUB_ENV
|
|
||||||
echo "TARGET_FLAGS=--target ${{ matrix.target }}" >> $GITHUB_ENV
|
|
||||||
echo "TARGET_DIR=./target/${{ matrix.target }}" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Run the tests
|
|
||||||
run: ${{ env.CARGO }} test --verbose --release ${{ env.TARGET_FLAGS }}
|
|
||||||
|
|
||||||
- name: Build release binary
|
|
||||||
run: ${{ env.CARGO }} build --verbose --release ${{ env.TARGET_FLAGS }}
|
|
||||||
|
|
||||||
- name: Strip release binary (linux and macos)
|
|
||||||
if: matrix.build == 'linux' || matrix.build == 'macos' || matrix.build == 'arm-macos'
|
|
||||||
run: |
|
|
||||||
strip "target/${{ matrix.target }}/release/fwd"
|
|
||||||
strip "target/${{ matrix.target }}/release/fwd-browse"
|
|
||||||
|
|
||||||
- name: Build archive
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
staging="fwd-${{ matrix.target }}"
|
|
||||||
mkdir -p "$staging"
|
|
||||||
|
|
||||||
if [ "${{ matrix.os }}" = "windows-2022" ]; then
|
|
||||||
cp "target/${{ matrix.target }}/release/fwd.exe" "$staging/"
|
|
||||||
7z a "$staging.zip" "$staging"
|
|
||||||
echo "ASSET=$staging.zip" >> $GITHUB_ENV
|
|
||||||
else
|
|
||||||
cp "target/${{ matrix.target }}/release/fwd" "$staging/"
|
|
||||||
cp "target/${{ matrix.target }}/release/fwd-browse" "$staging/"
|
|
||||||
tar czf "$staging.tar.gz" "$staging"
|
|
||||||
echo "ASSET=$staging.tar.gz" >> $GITHUB_ENV
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Upload release archive
|
|
||||||
uses: actions/upload-release-asset@v1.0.2
|
|
||||||
env:
|
env:
|
||||||
|
RELEASE_TAG: ${{ needs.create_release.outputs.version }}
|
||||||
|
BUILD: ${{ matrix.build }}
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
run: python3 release.py
|
||||||
upload_url: ${{ needs.create_release.outputs.upload_url }}
|
|
||||||
asset_name: ${{ env.ASSET }}
|
|
||||||
asset_path: ${{ env.ASSET }}
|
|
||||||
asset_content_type: application/octet-stream
|
|
||||||
|
|
|
||||||
734
Cargo.lock
generated
734
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
37
Cargo.toml
37
Cargo.toml
|
|
@ -1,12 +1,14 @@
|
||||||
[package]
|
[package]
|
||||||
name = "fwd"
|
name = "fwd"
|
||||||
version = "0.9.1"
|
version = "0.9.2"
|
||||||
|
authors = ["John Doty <john@d0ty.me>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
description = "Automatically forward ports to a remote server over ssh"
|
description = "Automatically forward ports to a remote server"
|
||||||
|
readme = "README.md"
|
||||||
|
documentation = "https://github.com/DeCarabas/fwd"
|
||||||
homepage = "https://github.com/DeCarabas/fwd"
|
homepage = "https://github.com/DeCarabas/fwd"
|
||||||
repository = "https://github.com/DeCarabas/fwd"
|
repository = "https://github.com/DeCarabas/fwd"
|
||||||
readme = "README.md"
|
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "fwd-browse"
|
name = "fwd-browse"
|
||||||
|
|
@ -17,27 +19,46 @@ bench = false
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
bytes = "1"
|
bytes = "1"
|
||||||
copypasta = "0.10.1"
|
copypasta = "0.10.1"
|
||||||
crossterm = { version = "0.25", features = ["event-stream"] }
|
crossterm = { version = "0.28.1", features = ["event-stream"] }
|
||||||
|
directories-next = "2"
|
||||||
env_logger = { version = "0.11.5", default-features = false }
|
env_logger = { version = "0.11.5", default-features = false }
|
||||||
home = "0.5.4"
|
|
||||||
indoc = "1"
|
indoc = "1"
|
||||||
log = { version = "0.4", features = ["std"] }
|
log = { version = "0.4", features = ["std"] }
|
||||||
open = "3"
|
open = "3"
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
|
ratatui = "0.28.0"
|
||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
tokio = { version = "1", features = ["io-std", "io-util", "macros", "net", "process", "rt", "rt-multi-thread", "fs"] }
|
tokio = { version = "1", features = ["io-std", "io-util", "macros", "net", "process", "rt", "rt-multi-thread", "fs"] }
|
||||||
tokio-stream = "0.1"
|
tokio-stream = "0.1"
|
||||||
toml = "0.5"
|
toml = "0.5"
|
||||||
tui = "0.19"
|
|
||||||
xdg = "2"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
assert_matches = "1"
|
assert_matches = "1"
|
||||||
pretty_assertions = "1"
|
pretty_assertions = "1"
|
||||||
tempdir = "0.3"
|
tempfile = "3"
|
||||||
|
|
||||||
[target.'cfg(target_os="linux")'.dependencies]
|
[target.'cfg(target_os="linux")'.dependencies]
|
||||||
procfs = "0.14.1"
|
procfs = "0.14.1"
|
||||||
|
|
||||||
[target.'cfg(target_family="unix")'.dependencies]
|
[target.'cfg(target_family="unix")'.dependencies]
|
||||||
libc = "0.2.155"
|
libc = "0.2.155"
|
||||||
|
|
||||||
|
[package.metadata.deb]
|
||||||
|
section = "net"
|
||||||
|
depends = [] # No auto deps?
|
||||||
|
assets = [
|
||||||
|
["target/release/fwd", "usr/bin/", "755"],
|
||||||
|
["target/release/fwd-browse", "usr/bin/", "755"],
|
||||||
|
["LICENSE", "usr/share/doc/fwd/", "644"],
|
||||||
|
["README.md", "usr/share/doc/fwd/README", "644"],
|
||||||
|
# The man page is automatically generated by fwd's build process. See
|
||||||
|
# release.py for details.
|
||||||
|
["target/release/fwd.1", "usr/share/man/man1/fwd.1", "644"],
|
||||||
|
]
|
||||||
|
extended-description = """\
|
||||||
|
fwd enumerates the listening ports the remote server and automatically listens
|
||||||
|
for connections on the same ports on the local machine. When fwd receives a
|
||||||
|
connection on the local machine, it automatically forwards that connection to
|
||||||
|
the remote machine.
|
||||||
|
"""
|
||||||
|
|
|
||||||
2
LICENSE
2
LICENSE
|
|
@ -1,4 +1,4 @@
|
||||||
Copyright 2022 John Doty
|
Copyright 2024 John Doty
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|
|
||||||
11
README.md
11
README.md
|
|
@ -4,7 +4,7 @@ A port-forwarding utility.
|
||||||
|
|
||||||
Here's how it works:
|
Here's how it works:
|
||||||
1. Get the latest [release](https://github.com/DeCarabas/fwd/releases) of `fwd`
|
1. Get the latest [release](https://github.com/DeCarabas/fwd/releases) of `fwd`
|
||||||
2. You install `fwd` on the server somewhere in your `$PATH` (like `/usr/bin/`)
|
2. You install `fwd` on the server somewhere in your `$PATH` (like `/usr/bin/`, or `.local/bin`)
|
||||||
3. You install `fwd` on the client (like your laptop)
|
3. You install `fwd` on the client (like your laptop)
|
||||||
4. You run `fwd` on the client to connect to the server, like so:
|
4. You run `fwd` on the client to connect to the server, like so:
|
||||||
|
|
||||||
|
|
@ -13,6 +13,9 @@ doty@my.laptop$ fwd some.server
|
||||||
```
|
```
|
||||||
|
|
||||||
`fwd` will connect to `some.server` via ssh, and then show you a screen listing all of the ports that the server is listening on locally.
|
`fwd` will connect to `some.server` via ssh, and then show you a screen listing all of the ports that the server is listening on locally.
|
||||||
|
|
||||||
|
<img width="1337" src="doc/screenshot-01.png" alt="A terminal displaying a list of ports and descriptions. Some are dimmed and one is highlighted." />
|
||||||
|
|
||||||
Use the up and down arrow keys (or `j`/`k`) to select the port you're interested in and press `e` to toggle forwarding of that port.
|
Use the up and down arrow keys (or `j`/`k`) to select the port you're interested in and press `e` to toggle forwarding of that port.
|
||||||
Now, connections to that port locally will be forwarded to the remote server.
|
Now, connections to that port locally will be forwarded to the remote server.
|
||||||
|
|
||||||
|
|
@ -21,9 +24,3 @@ If the port is something that might be interesting to a web browser, you can pre
|
||||||
If something is going wrong, pressing `l` will toggle logs that might explain it.
|
If something is going wrong, pressing `l` will toggle logs that might explain it.
|
||||||
|
|
||||||
Press `q` to quit.
|
Press `q` to quit.
|
||||||
|
|
||||||
## Future Improvements:
|
|
||||||
|
|
||||||
- Clipboard integration: send something from the remote end of the pipe to the host's clipboard. (Sometimes you *really* want to copy some big buffer from the remote side and your terminal just can't make that work.)
|
|
||||||
|
|
||||||
- Client heartbeats: I frequently wind up in a situation where the pipe is stalled: not broken but nothing is getting through. (This happens with my coder.com pipes all the time.)
|
|
||||||
|
|
|
||||||
156
doc/fwd.man.md
Normal file
156
doc/fwd.man.md
Normal file
|
|
@ -0,0 +1,156 @@
|
||||||
|
% fwd(1)
|
||||||
|
% John Doty <john@d0ty.me>
|
||||||
|
% August 2024
|
||||||
|
|
||||||
|
# NAME
|
||||||
|
|
||||||
|
fwd - Automatically forward connections to remote machines
|
||||||
|
|
||||||
|
# SYNOPSIS
|
||||||
|
|
||||||
|
**fwd** [OPTIONS] SERVER
|
||||||
|
|
||||||
|
**fwd** [OPTIONS] browse URL
|
||||||
|
|
||||||
|
**fwd** [OPTIONS] clip FILE
|
||||||
|
|
||||||
|
**fwd-browse** URL
|
||||||
|
|
||||||
|
# DESCRIPTION
|
||||||
|
|
||||||
|
**fwd** enumerates the listening ports the remote server and automatically listens for connections on the same ports on the local machine.
|
||||||
|
When **fwd** receives a connection on the local machine, it automatically forwards that connection to the remote machine.
|
||||||
|
|
||||||
|
**-s**, **-\-sudo**
|
||||||
|
: Run the server side of fwd with `sudo`.
|
||||||
|
: This allows the client to forward ports that are open by processes being run under other accounts (e.g., docker containers being run as root), but requires sudo access on the server and *might* end up forwarding ports that you do not want forwarded (e.g., port 22 for sshd, or port 53 for systemd.)
|
||||||
|
|
||||||
|
**-\-log-filter** **FILTER**
|
||||||
|
: Set remote server's log level. Default is `warn`.
|
||||||
|
: Supports all of Rust's env_logger filter syntax, e.g. `--log-filter=fwd::trace`.
|
||||||
|
|
||||||
|
**-\-version**
|
||||||
|
: Print the version of fwd and exit.
|
||||||
|
|
||||||
|
# INTERACTIVE COMMANDS
|
||||||
|
|
||||||
|
Once **fwd** is connected, it displays an interactive list of the ports available on the remote server.
|
||||||
|
|
||||||
|
- Ports that **fwd** is listening on are displayed in the default terminal color.
|
||||||
|
- Ports that **fwd** is aware of but which are disabled are displayed in dark gray.
|
||||||
|
- Ports that **fwd** has tried to listen on but which have failed are displayed in red.
|
||||||
|
Details on the error may be found in the log window.
|
||||||
|
Disabling and re-enabling the port will cause **fwd** to try again.
|
||||||
|
|
||||||
|
The following commands are available while **fwd** is connected:
|
||||||
|
|
||||||
|
**Esc, q, Ctrl-C**
|
||||||
|
: Exit **fwd**.
|
||||||
|
|
||||||
|
**?, h**
|
||||||
|
: Display the help window.
|
||||||
|
|
||||||
|
**Up, k**
|
||||||
|
: Select the previous port in the list.
|
||||||
|
|
||||||
|
**Down, j**
|
||||||
|
: Select the next port in the list.
|
||||||
|
|
||||||
|
**Enter**
|
||||||
|
: Attempt to browse to localhost on the specified port with the default browser.
|
||||||
|
|
||||||
|
**a**
|
||||||
|
: Hide or show anonymous ports.
|
||||||
|
: (See "identifying ports" below for more information on anonymous ports.)
|
||||||
|
|
||||||
|
**e**
|
||||||
|
: Enable or disable the selected port.
|
||||||
|
|
||||||
|
**l**
|
||||||
|
: Show or hide the log window.
|
||||||
|
|
||||||
|
|
||||||
|
# IDENTIFYING PORTS
|
||||||
|
|
||||||
|
**fwd** enumerates all of the ports that the remote server is listening on, and attempts to identify the process that is listening on each port.
|
||||||
|
It can identify ports in the following ways:
|
||||||
|
|
||||||
|
*docker*
|
||||||
|
: **fwd** will attempt to find and connect to a docker engine on the remote machine.
|
||||||
|
: If successful, it will list all of the forwarded ports, and identify each port as belonging to that docker container.
|
||||||
|
|
||||||
|
*procfs*
|
||||||
|
: On Linux, the listening ports are found by reading procfs and mapping them back to process command lines.
|
||||||
|
: **fwd** can only identify processes that the user it is connected as has permissions to read on the remote machine.
|
||||||
|
|
||||||
|
(Earlier methods take precedence over later methods.)
|
||||||
|
|
||||||
|
If **fwd** cannot identify the process that is listening on a given port, then the port is *anonymous*.
|
||||||
|
Anonymous ports are not enabled by default, but can be enabled manually, either with the UI or by configuration.
|
||||||
|
|
||||||
|
# OPENING BROWSERS
|
||||||
|
**fwd** can be used to open URLs in the default browser on the local machine.
|
||||||
|
Run **fwd browse URL** on the remote server to open the `URL` in the default browser on the local machine.
|
||||||
|
|
||||||
|
This only works if **fwd** is connected, and if the user running **fwd browse** is the same as the user that connected the **fwd** session.
|
||||||
|
|
||||||
|
The **fwd-browse** program acts as a wrapper around **fwd browse**, to be used with configurations that can't handle a browser being a program with an argument.
|
||||||
|
|
||||||
|
# CLIPBOARD
|
||||||
|
**fwd** can be used from the remote machine to place text on the clipboard of the local machine.
|
||||||
|
Run **fwd clip FILE** to copy the contents of the named file to the clipboard.
|
||||||
|
If **FILE** is **-**, this reads text from stdin instead.
|
||||||
|
|
||||||
|
# CONFIGURATION
|
||||||
|
**fwd** can be configured with a configuration file.
|
||||||
|
|
||||||
|
- On Windows, the config file will be in your roaming AppData folder.
|
||||||
|
(e.g., *c:\\Users\\Winifred\\AppData\\Roaming\\fwd\\config\\config.toml*)
|
||||||
|
- On MacOS, the config file will be in *$HOME/Library/Application Support/fwd/config.toml*.
|
||||||
|
(e.g., /Users/Margarie/Library/Application Support/fwd/config.toml)
|
||||||
|
- On XDG-ish systems (like Linux), the config file is in *~/.config/fwd/config.toml*.
|
||||||
|
(e.g., */home/lynette/.config/fwd/config.toml*)
|
||||||
|
|
||||||
|
The following is an example of a *config.toml* file:
|
||||||
|
|
||||||
|
```
|
||||||
|
auto=true # should `fwd` should enable identified ports (default true)
|
||||||
|
|
||||||
|
[servers.foo] # Server-specific settings for foo
|
||||||
|
auto=true # defaults to the global setting
|
||||||
|
ports=[1080, 1082] # ports that are always present
|
||||||
|
|
||||||
|
[servers.bar.ports] # `ports` can also be a table with port numbers as keys
|
||||||
|
1080=true # the values can be booleans (for enabled)...
|
||||||
|
1081="My program" # or strings (for descriptions).
|
||||||
|
|
||||||
|
[servers.bar.ports.1082] # port values can also be tables
|
||||||
|
enabled=true
|
||||||
|
description="A humble python"
|
||||||
|
```
|
||||||
|
|
||||||
|
Ports that are specified in the configuration file will always be present in the list of ports for a given server, even if no process is listening on that port.
|
||||||
|
|
||||||
|
# TROUBLESHOOTING
|
||||||
|
|
||||||
|
Connections are made via the **ssh** command.
|
||||||
|
Your **ssh** must:
|
||||||
|
|
||||||
|
- Be on your path, so that **fwd** can find it to invoke it
|
||||||
|
- Be able to authenticate you to the remote server.
|
||||||
|
(Interactive authentication is fine.)
|
||||||
|
- Understand the **-D** command line option, to operate as a SOCKS5 server
|
||||||
|
- Be able to start the **fwd** command on the remote server
|
||||||
|
|
||||||
|
A typical ssh invocation from **fwd** looks like:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh -T -D XXXX me@server FWD_LOG=warning FWD_SEND_ANONYMOUS=1 fwd --server
|
||||||
|
```
|
||||||
|
|
||||||
|
**fwd** only enumerates ports that are listening on loopback addresses (e.g., 127.0.0.1) or on all addresses (e.g., 0.0.0.0).
|
||||||
|
If it cannot find a particular port, check to make sure that the process listening on that port is accessible via localhost.
|
||||||
|
|
||||||
|
# SEE ALSO
|
||||||
|
|
||||||
|
ssh(1)
|
||||||
BIN
doc/screenshot-01.png
Normal file
BIN
doc/screenshot-01.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 83 KiB |
252
fuzz/Cargo.lock
generated
252
fuzz/Cargo.lock
generated
|
|
@ -17,6 +17,24 @@ version = "1.0.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ahash"
|
||||||
|
version = "0.8.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"once_cell",
|
||||||
|
"version_check",
|
||||||
|
"zerocopy",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "allocator-api2"
|
||||||
|
version = "0.2.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "android-tzdata"
|
name = "android-tzdata"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
|
|
@ -136,6 +154,15 @@ version = "0.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
|
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "castaway"
|
||||||
|
version = "0.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5"
|
||||||
|
dependencies = [
|
||||||
|
"rustversion",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.1.10"
|
version = "1.1.10"
|
||||||
|
|
@ -174,6 +201,20 @@ dependencies = [
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "compact_str"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6050c3a16ddab2e412160b31f2c871015704239bca62f72f6e5f0be631d3f644"
|
||||||
|
dependencies = [
|
||||||
|
"castaway",
|
||||||
|
"cfg-if",
|
||||||
|
"itoa",
|
||||||
|
"rustversion",
|
||||||
|
"ryu",
|
||||||
|
"static_assertions",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "concurrent-queue"
|
name = "concurrent-queue"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
|
|
@ -220,16 +261,16 @@ checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossterm"
|
name = "crossterm"
|
||||||
version = "0.25.0"
|
version = "0.28.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67"
|
checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 1.3.2",
|
"bitflags 2.6.0",
|
||||||
"crossterm_winapi",
|
"crossterm_winapi",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"libc",
|
"mio",
|
||||||
"mio 0.8.11",
|
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
|
"rustix 0.38.34",
|
||||||
"signal-hook",
|
"signal-hook",
|
||||||
"signal-hook-mio",
|
"signal-hook-mio",
|
||||||
"winapi",
|
"winapi",
|
||||||
|
|
@ -276,6 +317,12 @@ version = "1.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
|
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "either"
|
||||||
|
version = "1.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "env_filter"
|
name = "env_filter"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
|
|
@ -323,7 +370,7 @@ checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fwd"
|
name = "fwd"
|
||||||
version = "0.9.1"
|
version = "0.9.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
|
@ -332,16 +379,16 @@ dependencies = [
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"home",
|
"home",
|
||||||
"indoc",
|
"indoc",
|
||||||
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"open",
|
"open",
|
||||||
"procfs",
|
"procfs",
|
||||||
"rand",
|
"rand",
|
||||||
|
"ratatui",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"toml",
|
"toml",
|
||||||
"tui",
|
|
||||||
"users",
|
|
||||||
"xdg",
|
"xdg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -382,6 +429,22 @@ version = "0.29.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd"
|
checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.14.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||||
|
dependencies = [
|
||||||
|
"ahash",
|
||||||
|
"allocator-api2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "heck"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hermit-abi"
|
name = "hermit-abi"
|
||||||
version = "0.3.9"
|
version = "0.3.9"
|
||||||
|
|
@ -438,6 +501,16 @@ version = "1.0.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306"
|
checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "instability"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b23a0c8dfe501baac4adf6ebbfa6eddf8f0c07f56b058cc1288017e32397846c"
|
||||||
|
dependencies = [
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "io-lifetimes"
|
name = "io-lifetimes"
|
||||||
version = "1.0.11"
|
version = "1.0.11"
|
||||||
|
|
@ -449,6 +522,15 @@ dependencies = [
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itertools"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.11"
|
version = "1.0.11"
|
||||||
|
|
@ -466,9 +548,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.69"
|
version = "0.3.70"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d"
|
checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
@ -540,6 +622,15 @@ version = "0.4.22"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
|
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lru"
|
||||||
|
version = "0.12.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "37ee39891760e7d94734f6f63fedc29a2e4a152f836120753a72503f09fcf904"
|
||||||
|
dependencies = [
|
||||||
|
"hashbrown",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "malloc_buf"
|
name = "malloc_buf"
|
||||||
version = "0.0.6"
|
version = "0.0.6"
|
||||||
|
|
@ -573,18 +664,6 @@ dependencies = [
|
||||||
"adler",
|
"adler",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "mio"
|
|
||||||
version = "0.8.11"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"log",
|
|
||||||
"wasi",
|
|
||||||
"windows-sys 0.48.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mio"
|
name = "mio"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
|
|
@ -593,6 +672,7 @@ checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hermit-abi 0.3.9",
|
"hermit-abi 0.3.9",
|
||||||
"libc",
|
"libc",
|
||||||
|
"log",
|
||||||
"wasi",
|
"wasi",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
@ -683,6 +763,12 @@ dependencies = [
|
||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "paste"
|
||||||
|
version = "1.0.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pathdiff"
|
name = "pathdiff"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
|
|
@ -797,6 +883,27 @@ dependencies = [
|
||||||
"getrandom",
|
"getrandom",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ratatui"
|
||||||
|
version = "0.28.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5ba6a365afbe5615999275bea2446b970b10a41102500e27ce7678d50d978303"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.6.0",
|
||||||
|
"cassowary",
|
||||||
|
"compact_str",
|
||||||
|
"crossterm",
|
||||||
|
"instability",
|
||||||
|
"itertools",
|
||||||
|
"lru",
|
||||||
|
"paste",
|
||||||
|
"strum",
|
||||||
|
"strum_macros",
|
||||||
|
"unicode-segmentation",
|
||||||
|
"unicode-truncate",
|
||||||
|
"unicode-width",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.5.3"
|
version = "0.5.3"
|
||||||
|
|
@ -839,6 +946,12 @@ dependencies = [
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustversion"
|
||||||
|
version = "1.0.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ryu"
|
name = "ryu"
|
||||||
version = "1.0.18"
|
version = "1.0.18"
|
||||||
|
|
@ -859,18 +972,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.206"
|
version = "1.0.207"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5b3e4cd94123dd520a128bcd11e34d9e9e423e7e3e50425cb1b4b1e3549d0284"
|
checksum = "5665e14a49a4ea1b91029ba7d3bca9f299e1f7cfa194388ccc20f14743e784f2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive"
|
name = "serde_derive"
|
||||||
version = "1.0.206"
|
version = "1.0.207"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fabfb6138d2383ea8208cf98ccf69cdfb1aff4088460681d84189aa259762f97"
|
checksum = "6aea2634c86b0e8ef2cfdc0c340baede54ec27b1e46febd7f80dffb2aa44a00e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|
@ -906,7 +1019,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd"
|
checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"mio 0.8.11",
|
"mio",
|
||||||
"signal-hook",
|
"signal-hook",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -980,6 +1093,34 @@ dependencies = [
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "static_assertions"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strum"
|
||||||
|
version = "0.26.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
|
||||||
|
dependencies = [
|
||||||
|
"strum_macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strum_macros"
|
||||||
|
version = "0.26.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"rustversion",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.74"
|
version = "2.0.74"
|
||||||
|
|
@ -1020,7 +1161,7 @@ dependencies = [
|
||||||
"backtrace",
|
"backtrace",
|
||||||
"bytes",
|
"bytes",
|
||||||
"libc",
|
"libc",
|
||||||
"mio 1.0.2",
|
"mio",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"signal-hook-registry",
|
"signal-hook-registry",
|
||||||
"socket2",
|
"socket2",
|
||||||
|
|
@ -1075,19 +1216,6 @@ version = "0.1.32"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
|
checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tui"
|
|
||||||
version = "0.19.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ccdd26cbd674007e649a272da4475fb666d3aa0ad0531da7136db6fab0e5bad1"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags 1.3.2",
|
|
||||||
"cassowary",
|
|
||||||
"crossterm",
|
|
||||||
"unicode-segmentation",
|
|
||||||
"unicode-width",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.12"
|
version = "1.0.12"
|
||||||
|
|
@ -1100,6 +1228,17 @@ version = "1.11.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
|
checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-truncate"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf"
|
||||||
|
dependencies = [
|
||||||
|
"itertools",
|
||||||
|
"unicode-segmentation",
|
||||||
|
"unicode-width",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-width"
|
name = "unicode-width"
|
||||||
version = "0.1.13"
|
version = "0.1.13"
|
||||||
|
|
@ -1107,14 +1246,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d"
|
checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "users"
|
name = "version_check"
|
||||||
version = "0.11.0"
|
version = "0.9.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "24cc0f6d6f267b73e5a2cadf007ba8f9bc39c6a6f9666f8cf25ea809a153b032"
|
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"log",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasi"
|
name = "wasi"
|
||||||
|
|
@ -1124,19 +1259,20 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen"
|
name = "wasm-bindgen"
|
||||||
version = "0.2.92"
|
version = "0.2.93"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
|
checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
|
"once_cell",
|
||||||
"wasm-bindgen-macro",
|
"wasm-bindgen-macro",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-backend"
|
name = "wasm-bindgen-backend"
|
||||||
version = "0.2.92"
|
version = "0.2.93"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
|
checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bumpalo",
|
"bumpalo",
|
||||||
"log",
|
"log",
|
||||||
|
|
@ -1149,9 +1285,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro"
|
name = "wasm-bindgen-macro"
|
||||||
version = "0.2.92"
|
version = "0.2.93"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
|
checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"wasm-bindgen-macro-support",
|
"wasm-bindgen-macro-support",
|
||||||
|
|
@ -1159,9 +1295,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro-support"
|
name = "wasm-bindgen-macro-support"
|
||||||
version = "0.2.92"
|
version = "0.2.93"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
|
checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|
@ -1172,9 +1308,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-shared"
|
name = "wasm-bindgen-shared"
|
||||||
version = "0.2.92"
|
version = "0.2.93"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
|
checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wayland-backend"
|
name = "wayland-backend"
|
||||||
|
|
|
||||||
161
release.py
Normal file
161
release.py
Normal file
|
|
@ -0,0 +1,161 @@
|
||||||
|
"""A script to automate building and uploading a release archive.
|
||||||
|
|
||||||
|
This is in python instead of bash because I abhor bash. Even though it's a
|
||||||
|
little nicer for running commands, it's worse at everything else.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import dataclasses
|
||||||
|
import enum
|
||||||
|
import os
|
||||||
|
import os.path
|
||||||
|
import pathlib
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
RELEASE_TAG = os.getenv("RELEASE_TAG")
|
||||||
|
|
||||||
|
BUILD = os.getenv("BUILD")
|
||||||
|
if BUILD is None:
|
||||||
|
raise Exception("you *must* set the BUILD environment variable")
|
||||||
|
|
||||||
|
|
||||||
|
class Archive(enum.Enum):
|
||||||
|
TARBALL = 1
|
||||||
|
ZIP = 2
|
||||||
|
DEB = 3
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class BuildSettings:
|
||||||
|
target: str # The rust target to build for
|
||||||
|
test: bool = True # Whether or not to run tests
|
||||||
|
man_page: bool = True # Whether or not to generate a man page
|
||||||
|
strip: bool = True # Whether or not to strip binaries
|
||||||
|
archive: Archive = Archive.TARBALL # Archive type
|
||||||
|
ext: str = "" # The file extension of the binary
|
||||||
|
|
||||||
|
|
||||||
|
print(f"doing release: {BUILD}")
|
||||||
|
build = {
|
||||||
|
"linux": BuildSettings(
|
||||||
|
target="x86_64-unknown-linux-musl",
|
||||||
|
),
|
||||||
|
"debian": BuildSettings(
|
||||||
|
target="x86_64-unknown-linux-musl",
|
||||||
|
test=False,
|
||||||
|
archive=Archive.DEB,
|
||||||
|
),
|
||||||
|
"macos": BuildSettings(
|
||||||
|
target="x86_64-apple-darwin",
|
||||||
|
),
|
||||||
|
"arm-macos": BuildSettings(
|
||||||
|
target="aarch64-apple-darwin",
|
||||||
|
),
|
||||||
|
"windows": BuildSettings(
|
||||||
|
target="x86_64-pc-windows-msvc",
|
||||||
|
strip=False,
|
||||||
|
man_page=False,
|
||||||
|
archive=Archive.ZIP,
|
||||||
|
ext=".exe",
|
||||||
|
),
|
||||||
|
}[BUILD]
|
||||||
|
|
||||||
|
print(f"settings: {build}")
|
||||||
|
|
||||||
|
|
||||||
|
target_dir = pathlib.Path("target") / build.target / "release"
|
||||||
|
bins = [(target_dir / bin).with_suffix(build.ext) for bin in ["fwd", "fwd-browse"]]
|
||||||
|
|
||||||
|
|
||||||
|
def build_and_test(staging: pathlib.Path):
|
||||||
|
# Tools
|
||||||
|
subprocess.run(
|
||||||
|
["rustup", "target", "add", build.target],
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test...?
|
||||||
|
if build.test:
|
||||||
|
subprocess.run(
|
||||||
|
["cargo", "test", "--verbose", "--release", "--target", build.target],
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Build
|
||||||
|
subprocess.run(
|
||||||
|
["cargo", "build", "--verbose", "--release", "--target", build.target],
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Strip
|
||||||
|
if build.strip:
|
||||||
|
for bin in bins:
|
||||||
|
subprocess.run(["strip", bin], check=True)
|
||||||
|
|
||||||
|
# Copy
|
||||||
|
for bin in bins:
|
||||||
|
shutil.copyfile(bin, os.path.join(staging, os.path.basename(bin)))
|
||||||
|
|
||||||
|
|
||||||
|
def build_docs(staging: pathlib.Path):
|
||||||
|
shutil.copyfile("README.md", staging / "README.md")
|
||||||
|
if build.man_page:
|
||||||
|
print("Creating man page...")
|
||||||
|
proc = subprocess.run(
|
||||||
|
["pandoc", "-s", "-tman", os.path.join("doc", "fwd.man.md")],
|
||||||
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
|
encoding="utf8",
|
||||||
|
)
|
||||||
|
contents = proc.stdout
|
||||||
|
with open(staging / "fwd.1", "w", encoding="utf-8") as f:
|
||||||
|
f.write(contents)
|
||||||
|
|
||||||
|
|
||||||
|
def build_archive(staging: pathlib.Path) -> pathlib.Path:
|
||||||
|
print("Creating archive...")
|
||||||
|
if build.archive == Archive.ZIP:
|
||||||
|
archive = pathlib.Path(f"{staging}.zip")
|
||||||
|
subprocess.run(["7z", "a", archive, f"{staging}"], check=True)
|
||||||
|
|
||||||
|
elif build.archive == Archive.DEB:
|
||||||
|
subprocess.run(["cargo", "install", "cargo-deb"], check=True)
|
||||||
|
shutil.copyfile(staging / "fwd.1", target_dir / "fwd.1")
|
||||||
|
subprocess.run(["cargo", "deb", "--target", build.target], check=True)
|
||||||
|
|
||||||
|
# Knowing the deb path means knowing the target version but I don't
|
||||||
|
# actually have the version here. (Or, like, I have the release tag
|
||||||
|
# but not in testing.) So just find the hopefully singular .deb that
|
||||||
|
# we made.
|
||||||
|
deb_path = pathlib.Path("target") / build.target / "debian"
|
||||||
|
archives = list(deb_path.glob("*.deb"))
|
||||||
|
assert len(archives) == 1
|
||||||
|
archive = archives[0]
|
||||||
|
|
||||||
|
else:
|
||||||
|
assert build.archive == Archive.TARBALL
|
||||||
|
archive = pathlib.Path(f"{staging}.tar.gz")
|
||||||
|
subprocess.run(["tar", "czf", archive, f"{staging}"], check=True)
|
||||||
|
|
||||||
|
return archive
|
||||||
|
|
||||||
|
|
||||||
|
staging = pathlib.Path(f"fwd-{build.target}")
|
||||||
|
os.makedirs(staging, exist_ok=True)
|
||||||
|
|
||||||
|
build_and_test(staging)
|
||||||
|
build_docs(staging)
|
||||||
|
archive = build_archive(staging)
|
||||||
|
|
||||||
|
shutil.rmtree(staging)
|
||||||
|
|
||||||
|
assert archive.exists()
|
||||||
|
if RELEASE_TAG is None:
|
||||||
|
print(f"Not releasing {archive} to github, RELEASE_TAG is none.")
|
||||||
|
else:
|
||||||
|
print(f"Uploading {archive} to github release {RELEASE_TAG}...")
|
||||||
|
subprocess.run(
|
||||||
|
["gh", "release", "upload", RELEASE_TAG, archive, "--clobber"],
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
os.unlink(archive)
|
||||||
|
|
@ -1,15 +1,17 @@
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
use std::collections::hash_map;
|
use std::collections::hash_map;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use toml::Value;
|
use toml::value::{Table, Value};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
#[cfg_attr(test, derive(PartialEq, Eq))]
|
||||||
pub struct PortConfig {
|
pub struct PortConfig {
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
#[cfg_attr(test, derive(PartialEq, Eq))]
|
||||||
pub struct ServerConfig {
|
pub struct ServerConfig {
|
||||||
auto: bool,
|
auto: bool,
|
||||||
ports: HashMap<u16, PortConfig>,
|
ports: HashMap<u16, PortConfig>,
|
||||||
|
|
@ -45,6 +47,7 @@ impl ServerConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
#[cfg_attr(test, derive(PartialEq, Eq))]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
auto: bool,
|
auto: bool,
|
||||||
servers: HashMap<String, ServerConfig>,
|
servers: HashMap<String, ServerConfig>,
|
||||||
|
|
@ -62,13 +65,15 @@ impl Config {
|
||||||
pub fn load_config() -> Result<Config> {
|
pub fn load_config() -> Result<Config> {
|
||||||
use std::io::ErrorKind;
|
use std::io::ErrorKind;
|
||||||
|
|
||||||
let mut home = match home::home_dir() {
|
let Some(directories) = directories_next::ProjectDirs::from("", "", "fwd")
|
||||||
Some(h) => h,
|
else {
|
||||||
None => return Ok(default()),
|
return Ok(default());
|
||||||
};
|
};
|
||||||
home.push(".fwd");
|
|
||||||
|
|
||||||
let contents = match std::fs::read_to_string(home) {
|
let mut config_path = directories.config_dir().to_path_buf();
|
||||||
|
config_path.push("config.toml");
|
||||||
|
|
||||||
|
let contents = match std::fs::read_to_string(config_path) {
|
||||||
Ok(contents) => contents,
|
Ok(contents) => contents,
|
||||||
Err(e) => match e.kind() {
|
Err(e) => match e.kind() {
|
||||||
ErrorKind::NotFound => return Ok(default()),
|
ErrorKind::NotFound => return Ok(default()),
|
||||||
|
|
@ -83,85 +88,101 @@ fn default() -> Config {
|
||||||
Config { auto: true, servers: HashMap::new() }
|
Config { auto: true, servers: HashMap::new() }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_config(value: &Value) -> Result<Config> {
|
fn get_bool(table: &Table, key: &str, default: bool) -> Result<bool> {
|
||||||
match value {
|
match table.get(key) {
|
||||||
Value::Table(table) => Ok({
|
None => Ok(default),
|
||||||
let auto = match table.get("auto") {
|
Some(Value::Boolean(v)) => Ok(*v),
|
||||||
None => true,
|
Some(v) => bail!("expected a true or false, got {v:?}"),
|
||||||
Some(Value::Boolean(v)) => *v,
|
|
||||||
Some(v) => bail!("expected a true or false, got {:?}", v),
|
|
||||||
};
|
|
||||||
Config { auto, servers: get_servers(table, auto)? }
|
|
||||||
}),
|
|
||||||
_ => bail!("top level must be a table"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_servers(
|
fn parse_config(value: &Value) -> Result<Config> {
|
||||||
table: &toml::value::Table,
|
let Value::Table(table) = value else {
|
||||||
|
bail!("top level must be a table")
|
||||||
|
};
|
||||||
|
|
||||||
|
let auto = get_bool(table, "auto", true)?;
|
||||||
|
let servers = match table.get("servers") {
|
||||||
|
None => &Table::new(),
|
||||||
|
Some(Value::Table(t)) => t,
|
||||||
|
Some(v) => bail!("Expected a table in the servers key, got {v:?}"),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Config {
|
||||||
|
auto,
|
||||||
|
servers: parse_servers(servers, auto)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_servers(
|
||||||
|
table: &Table,
|
||||||
auto: bool,
|
auto: bool,
|
||||||
) -> Result<HashMap<String, ServerConfig>> {
|
) -> Result<HashMap<String, ServerConfig>> {
|
||||||
match table.get("servers") {
|
let mut servers = HashMap::new();
|
||||||
None => Ok(HashMap::new()),
|
for (k, v) in table {
|
||||||
Some(Value::Table(table)) => Ok({
|
let Value::Table(table) = v else {
|
||||||
let mut servers = HashMap::new();
|
bail!("expected a table for server {k}, got {v:?}");
|
||||||
for (k, v) in table {
|
};
|
||||||
servers.insert(k.clone(), get_server(v, auto)?);
|
|
||||||
}
|
servers.insert(k.clone(), parse_server(table, auto)?);
|
||||||
servers
|
|
||||||
}),
|
|
||||||
v => bail!("expected a table in the servers key, got {:?}", v),
|
|
||||||
}
|
}
|
||||||
|
Ok(servers)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_server(value: &Value, auto: bool) -> Result<ServerConfig> {
|
fn parse_server(table: &Table, auto: bool) -> Result<ServerConfig> {
|
||||||
|
let auto = get_bool(table, "auto", auto)?;
|
||||||
|
let ports = match table.get("ports") {
|
||||||
|
None => HashMap::new(),
|
||||||
|
Some(v) => parse_ports(v)?,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(ServerConfig { auto, ports })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_ports(value: &Value) -> Result<HashMap<u16, PortConfig>> {
|
||||||
match value {
|
match value {
|
||||||
Value::Table(table) => Ok(ServerConfig {
|
Value::Array(array) => {
|
||||||
auto: match table.get("auto") {
|
|
||||||
None => auto, // Default to global default
|
|
||||||
Some(Value::Boolean(v)) => *v,
|
|
||||||
Some(v) => bail!("expected true or false, got {:?}", v),
|
|
||||||
},
|
|
||||||
ports: get_ports(table)?,
|
|
||||||
}),
|
|
||||||
value => bail!("expected a table, got {:?}", value),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_ports(table: &toml::value::Table) -> Result<HashMap<u16, PortConfig>> {
|
|
||||||
match table.get("ports") {
|
|
||||||
None => Ok(HashMap::new()),
|
|
||||||
Some(Value::Table(table)) => Ok({
|
|
||||||
let mut ports = HashMap::new();
|
|
||||||
for (k,v) in table {
|
|
||||||
let port:u16 = k.parse()?;
|
|
||||||
let config = match v {
|
|
||||||
Value::Boolean(enabled) => PortConfig{enabled:*enabled, description:None},
|
|
||||||
Value::Table(table) => PortConfig{
|
|
||||||
enabled: match table.get("enabled") {
|
|
||||||
Some(Value::Boolean(enabled)) => *enabled,
|
|
||||||
_ => bail!("not implemented"),
|
|
||||||
},
|
|
||||||
description: match table.get("description") {
|
|
||||||
Some(Value::String(desc)) => Some(desc.clone()),
|
|
||||||
Some(v) => bail!("expect a string description, got {:?}", v),
|
|
||||||
None => None,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
_ => bail!("expected either a boolean (enabled) or a table for a port config, got {:?}", v),
|
|
||||||
};
|
|
||||||
ports.insert(port, config);
|
|
||||||
}
|
|
||||||
ports
|
|
||||||
}),
|
|
||||||
Some(Value::Array(array)) => Ok({
|
|
||||||
let mut ports = HashMap::new();
|
let mut ports = HashMap::new();
|
||||||
for v in array {
|
for v in array {
|
||||||
ports.insert(get_port_number(v)?, PortConfig{enabled:true, description:None});
|
ports.insert(
|
||||||
|
get_port_number(v)?,
|
||||||
|
PortConfig { enabled: true, description: None },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
ports
|
Ok(ports)
|
||||||
|
}
|
||||||
|
|
||||||
|
Value::Table(table) => {
|
||||||
|
let mut ports = HashMap::new();
|
||||||
|
for (k, v) in table {
|
||||||
|
let port: u16 = k.parse()?;
|
||||||
|
let config = parse_port_config(v)?;
|
||||||
|
ports.insert(port, config);
|
||||||
|
}
|
||||||
|
Ok(ports)
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => bail!("ports must be either an array or a table, got {value:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_port_config(value: &Value) -> Result<PortConfig> {
|
||||||
|
match value {
|
||||||
|
Value::Boolean(enabled) => Ok(PortConfig{enabled:*enabled, description:None}),
|
||||||
|
Value::String(description) => Ok(PortConfig{
|
||||||
|
enabled: true,
|
||||||
|
description: Some(description.clone()),
|
||||||
}),
|
}),
|
||||||
Some(v) => bail!("ports must be either a table of '<port> = ...' or an array of ports, got {:?}", v),
|
Value::Table(table) => {
|
||||||
|
let enabled = get_bool(table, "enabled", true)?;
|
||||||
|
let description = match table.get("description") {
|
||||||
|
Some(Value::String(desc)) => Some(desc.clone()),
|
||||||
|
Some(v) => bail!("expect a string description, got {v:?}"),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
Ok(PortConfig { enabled, description })
|
||||||
|
},
|
||||||
|
_ => bail!("expected either a boolean (enabled), a string (description), or a table for a port config, got {value:?}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -172,3 +193,258 @@ fn get_port_number(v: &Value) -> Result<u16> {
|
||||||
};
|
};
|
||||||
Ok(port)
|
Ok(port)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
fn config_test(config: &str, expected: Config) {
|
||||||
|
let config = config.parse::<Value>().expect("case not toml");
|
||||||
|
let config = parse_config(&config).expect("unable to parse config");
|
||||||
|
|
||||||
|
assert_eq!(expected, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn config_error_test(config: &str) {
|
||||||
|
let config = config.parse::<Value>().expect("case not toml");
|
||||||
|
assert!(parse_config(&config).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty() {
|
||||||
|
config_test("", Config { auto: true, servers: HashMap::new() });
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn auto_false() {
|
||||||
|
config_test(
|
||||||
|
"
|
||||||
|
auto=false
|
||||||
|
",
|
||||||
|
Config { auto: false, servers: HashMap::new() },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn auto_not_boolean() {
|
||||||
|
config_error_test(
|
||||||
|
"
|
||||||
|
auto='what is going on'
|
||||||
|
",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn servers_not_table() {
|
||||||
|
config_error_test("servers=1234");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn servers_default() {
|
||||||
|
config_test("servers.foo={}", {
|
||||||
|
let mut servers = HashMap::new();
|
||||||
|
servers.insert(
|
||||||
|
"foo".to_string(),
|
||||||
|
ServerConfig { auto: true, ports: HashMap::new() },
|
||||||
|
);
|
||||||
|
Config { auto: true, servers }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn servers_auto_false() {
|
||||||
|
config_test(
|
||||||
|
"
|
||||||
|
[servers.foo]
|
||||||
|
auto=false
|
||||||
|
",
|
||||||
|
{
|
||||||
|
let mut servers = HashMap::new();
|
||||||
|
servers.insert(
|
||||||
|
"foo".to_string(),
|
||||||
|
ServerConfig { auto: false, ports: HashMap::new() },
|
||||||
|
);
|
||||||
|
Config { auto: true, servers }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn servers_auto_not_bool() {
|
||||||
|
config_error_test(
|
||||||
|
"
|
||||||
|
[servers.foo]
|
||||||
|
auto=1234
|
||||||
|
",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn servers_ports_list() {
|
||||||
|
config_test(
|
||||||
|
"
|
||||||
|
[servers.foo]
|
||||||
|
ports=[1,2,3]
|
||||||
|
",
|
||||||
|
{
|
||||||
|
let mut servers = HashMap::new();
|
||||||
|
servers.insert(
|
||||||
|
"foo".to_string(),
|
||||||
|
ServerConfig {
|
||||||
|
auto: true,
|
||||||
|
ports: {
|
||||||
|
let mut ports = HashMap::new();
|
||||||
|
ports.insert(
|
||||||
|
1,
|
||||||
|
PortConfig { enabled: true, description: None },
|
||||||
|
);
|
||||||
|
ports.insert(
|
||||||
|
2,
|
||||||
|
PortConfig { enabled: true, description: None },
|
||||||
|
);
|
||||||
|
ports.insert(
|
||||||
|
3,
|
||||||
|
PortConfig { enabled: true, description: None },
|
||||||
|
);
|
||||||
|
ports
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
Config { auto: true, servers }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn servers_ports_table_variations() {
|
||||||
|
config_test(
|
||||||
|
"
|
||||||
|
[servers.foo.ports]
|
||||||
|
1=true
|
||||||
|
2={enabled=false}
|
||||||
|
3=false
|
||||||
|
",
|
||||||
|
{
|
||||||
|
let mut servers = HashMap::new();
|
||||||
|
servers.insert(
|
||||||
|
"foo".to_string(),
|
||||||
|
ServerConfig {
|
||||||
|
auto: true,
|
||||||
|
ports: {
|
||||||
|
let mut ports = HashMap::new();
|
||||||
|
ports.insert(
|
||||||
|
1,
|
||||||
|
PortConfig { enabled: true, description: None },
|
||||||
|
);
|
||||||
|
ports.insert(
|
||||||
|
2,
|
||||||
|
PortConfig {
|
||||||
|
enabled: false,
|
||||||
|
description: None,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
ports.insert(
|
||||||
|
3,
|
||||||
|
PortConfig {
|
||||||
|
enabled: false,
|
||||||
|
description: None,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
ports
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
Config { auto: true, servers }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn servers_ports_table_descriptions() {
|
||||||
|
config_test(
|
||||||
|
"
|
||||||
|
[servers.foo.ports]
|
||||||
|
1={enabled=false}
|
||||||
|
2={description='humble'}
|
||||||
|
",
|
||||||
|
{
|
||||||
|
let mut servers = HashMap::new();
|
||||||
|
servers.insert(
|
||||||
|
"foo".to_string(),
|
||||||
|
ServerConfig {
|
||||||
|
auto: true,
|
||||||
|
ports: {
|
||||||
|
let mut ports = HashMap::new();
|
||||||
|
ports.insert(
|
||||||
|
1,
|
||||||
|
PortConfig {
|
||||||
|
enabled: false,
|
||||||
|
description: None,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
ports.insert(
|
||||||
|
2,
|
||||||
|
PortConfig {
|
||||||
|
enabled: true,
|
||||||
|
description: Some("humble".to_string()),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
ports
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
Config { auto: true, servers }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn servers_ports_raw_desc() {
|
||||||
|
config_test(
|
||||||
|
"
|
||||||
|
[servers.foo.ports]
|
||||||
|
1='humble'
|
||||||
|
",
|
||||||
|
{
|
||||||
|
let mut servers = HashMap::new();
|
||||||
|
servers.insert(
|
||||||
|
"foo".to_string(),
|
||||||
|
ServerConfig {
|
||||||
|
auto: true,
|
||||||
|
ports: {
|
||||||
|
let mut ports = HashMap::new();
|
||||||
|
ports.insert(
|
||||||
|
1,
|
||||||
|
PortConfig {
|
||||||
|
enabled: true,
|
||||||
|
description: Some("humble".to_string()),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
ports
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
Config { auto: true, servers }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn servers_inherit_auto() {
|
||||||
|
config_test(
|
||||||
|
"
|
||||||
|
auto=false
|
||||||
|
servers.foo={}
|
||||||
|
",
|
||||||
|
{
|
||||||
|
let mut servers = HashMap::new();
|
||||||
|
servers.insert(
|
||||||
|
"foo".to_string(),
|
||||||
|
ServerConfig { auto: false, ports: HashMap::new() },
|
||||||
|
);
|
||||||
|
Config { auto: false, servers }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
197
src/client/ui.rs
197
src/client/ui.rs
|
|
@ -14,15 +14,8 @@ use crossterm::{
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use log::{error, info, warn, Level, Metadata, Record};
|
use log::{error, info, warn, Level, Metadata, Record};
|
||||||
use std::collections::vec_deque::VecDeque;
|
use ratatui::{
|
||||||
use std::collections::{HashMap, HashSet};
|
backend::CrosstermBackend,
|
||||||
use std::io::stdout;
|
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
use tokio::sync::mpsc;
|
|
||||||
use tokio::sync::oneshot;
|
|
||||||
use tokio_stream::StreamExt;
|
|
||||||
use tui::{
|
|
||||||
backend::{Backend, CrosstermBackend},
|
|
||||||
layout::{Constraint, Direction, Layout, Margin, Rect},
|
layout::{Constraint, Direction, Layout, Margin, Rect},
|
||||||
style::{Color, Modifier, Style},
|
style::{Color, Modifier, Style},
|
||||||
widgets::{
|
widgets::{
|
||||||
|
|
@ -30,6 +23,13 @@ use tui::{
|
||||||
},
|
},
|
||||||
Frame, Terminal,
|
Frame, Terminal,
|
||||||
};
|
};
|
||||||
|
use std::collections::vec_deque::VecDeque;
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
use std::io::stdout;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
use tokio::sync::oneshot;
|
||||||
|
use tokio_stream::StreamExt;
|
||||||
|
|
||||||
pub enum UIEvent {
|
pub enum UIEvent {
|
||||||
Connected(u16),
|
Connected(u16),
|
||||||
|
|
@ -155,6 +155,16 @@ impl Listener {
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_anonymous(&self) -> bool {
|
||||||
|
// Anonynous ports are not configured and came from the server but
|
||||||
|
// had no description there.
|
||||||
|
self.config.is_none()
|
||||||
|
&& match self.desc.as_ref() {
|
||||||
|
Some(desc) => desc.desc.is_empty(),
|
||||||
|
None => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn state(&self) -> State {
|
fn state(&self) -> State {
|
||||||
*self.state.lock().unwrap()
|
*self.state.lock().unwrap()
|
||||||
}
|
}
|
||||||
|
|
@ -223,6 +233,7 @@ pub struct UI {
|
||||||
show_help: bool,
|
show_help: bool,
|
||||||
alternate_screen: bool,
|
alternate_screen: bool,
|
||||||
raw_mode: bool,
|
raw_mode: bool,
|
||||||
|
show_anonymous: bool,
|
||||||
clipboard: Option<ClipboardContext>,
|
clipboard: Option<ClipboardContext>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -247,6 +258,7 @@ impl UI {
|
||||||
config,
|
config,
|
||||||
alternate_screen: false,
|
alternate_screen: false,
|
||||||
raw_mode: false,
|
raw_mode: false,
|
||||||
|
show_anonymous: true,
|
||||||
clipboard,
|
clipboard,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -301,7 +313,7 @@ impl UI {
|
||||||
Ok(code)
|
Ok(code)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_connected<T: Backend>(&mut self, frame: &mut Frame<T>) {
|
fn render_connected(&mut self, frame: &mut Frame) {
|
||||||
let constraints = if self.show_logs {
|
let constraints = if self.show_logs {
|
||||||
vec![Constraint::Percentage(50), Constraint::Percentage(50)]
|
vec![Constraint::Percentage(50), Constraint::Percentage(50)]
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -311,7 +323,7 @@ impl UI {
|
||||||
let chunks = Layout::default()
|
let chunks = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints(constraints)
|
.constraints(constraints)
|
||||||
.split(frame.size());
|
.split(frame.area());
|
||||||
|
|
||||||
self.render_ports(frame, chunks[0]);
|
self.render_ports(frame, chunks[0]);
|
||||||
if self.show_logs {
|
if self.show_logs {
|
||||||
|
|
@ -322,11 +334,11 @@ impl UI {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_ports<B: Backend>(&mut self, frame: &mut Frame<B>, size: Rect) {
|
fn render_ports(&mut self, frame: &mut Frame, size: Rect) {
|
||||||
let enabled_port_style = Style::default();
|
let enabled_port_style = Style::reset();
|
||||||
let disabled_port_style = Style::default().fg(Color::DarkGray);
|
let disabled_port_style = Style::reset().fg(Color::DarkGray);
|
||||||
let broken_port_style =
|
let broken_port_style =
|
||||||
Style::default().fg(Color::Red).add_modifier(Modifier::DIM);
|
Style::reset().fg(Color::Red).add_modifier(Modifier::DIM);
|
||||||
|
|
||||||
let mut rows = Vec::new();
|
let mut rows = Vec::new();
|
||||||
let ports = self.get_ui_ports();
|
let ports = self.get_ui_ports();
|
||||||
|
|
@ -334,6 +346,10 @@ impl UI {
|
||||||
ports.iter().map(|p| format!("{p}")).collect();
|
ports.iter().map(|p| format!("{p}")).collect();
|
||||||
for (index, port) in ports.into_iter().enumerate() {
|
for (index, port) in ports.into_iter().enumerate() {
|
||||||
let listener = self.ports.get(&port).unwrap();
|
let listener = self.ports.get(&port).unwrap();
|
||||||
|
if !self.should_render_listener(listener) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let (symbol, style) = match listener.state() {
|
let (symbol, style) = match listener.state() {
|
||||||
State::Enabled => (" ✓ ", enabled_port_style),
|
State::Enabled => (" ✓ ", enabled_port_style),
|
||||||
State::Broken => (" ✗ ", broken_port_style),
|
State::Broken => (" ✗ ", broken_port_style),
|
||||||
|
|
@ -358,17 +374,16 @@ impl UI {
|
||||||
Constraint::Length(size.width),
|
Constraint::Length(size.width),
|
||||||
];
|
];
|
||||||
|
|
||||||
let port_list = Table::new(rows)
|
let port_list = Table::new(rows, &widths)
|
||||||
.header(Row::new(vec!["fwd", "Port", "Description"]))
|
.header(Row::new(vec!["fwd", "Port", "Description"]))
|
||||||
.block(Block::default().title("Ports").borders(Borders::ALL))
|
.block(Block::default().title("Ports").borders(Borders::ALL))
|
||||||
.column_spacing(1)
|
.column_spacing(1)
|
||||||
.widths(&widths)
|
|
||||||
.highlight_symbol(">> ");
|
.highlight_symbol(">> ");
|
||||||
|
|
||||||
frame.render_stateful_widget(port_list, size, &mut self.selection);
|
frame.render_stateful_widget(port_list, size, &mut self.selection);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_help<B: Backend>(&mut self, frame: &mut Frame<B>) {
|
fn render_help(&mut self, frame: &mut Frame) {
|
||||||
let keybindings = vec![
|
let keybindings = vec![
|
||||||
Row::new(vec!["↑ / k", "Move cursor up"]),
|
Row::new(vec!["↑ / k", "Move cursor up"]),
|
||||||
Row::new(vec!["↓ / j", "Move cursor down"]),
|
Row::new(vec!["↓ / j", "Move cursor down"]),
|
||||||
|
|
@ -380,6 +395,7 @@ impl UI {
|
||||||
Row::new(vec!["ESC / q", "Quit"]),
|
Row::new(vec!["ESC / q", "Quit"]),
|
||||||
Row::new(vec!["? / h", "Show this help text"]),
|
Row::new(vec!["? / h", "Show this help text"]),
|
||||||
Row::new(vec!["l", "Show fwd's logs"]),
|
Row::new(vec!["l", "Show fwd's logs"]),
|
||||||
|
Row::new(vec!["a", "Hide/show anonymous ports"]),
|
||||||
];
|
];
|
||||||
|
|
||||||
let border_lines = 3;
|
let border_lines = 3;
|
||||||
|
|
@ -387,10 +403,10 @@ impl UI {
|
||||||
let help_popup_area = centered_rect(
|
let help_popup_area = centered_rect(
|
||||||
65,
|
65,
|
||||||
keybindings.len() as u16 + border_lines,
|
keybindings.len() as u16 + border_lines,
|
||||||
frame.size(),
|
frame.area(),
|
||||||
);
|
);
|
||||||
let inner_area =
|
let inner_area =
|
||||||
help_popup_area.inner(&Margin { vertical: 1, horizontal: 1 });
|
help_popup_area.inner(Margin { vertical: 1, horizontal: 1 });
|
||||||
let key_width = 7;
|
let key_width = 7;
|
||||||
let binding_width = inner_area.width.saturating_sub(key_width);
|
let binding_width = inner_area.width.saturating_sub(key_width);
|
||||||
|
|
||||||
|
|
@ -398,16 +414,16 @@ impl UI {
|
||||||
Constraint::Length(key_width),
|
Constraint::Length(key_width),
|
||||||
Constraint::Length(binding_width),
|
Constraint::Length(binding_width),
|
||||||
];
|
];
|
||||||
let keybindings = Table::new(keybindings)
|
let keybindings = Table::new(keybindings, keybindings_widths)
|
||||||
.widths(keybindings_widths)
|
|
||||||
.column_spacing(1)
|
.column_spacing(1)
|
||||||
.block(Block::default().title("Keys").borders(Borders::ALL));
|
.block(Block::default().title("Keys").borders(Borders::ALL))
|
||||||
|
.style(Style::reset());
|
||||||
|
|
||||||
// keybindings
|
// keybindings
|
||||||
frame.render_widget(keybindings, inner_area);
|
frame.render_widget(keybindings, inner_area);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_logs<B: Backend>(&mut self, frame: &mut Frame<B>, size: Rect) {
|
fn render_logs(&mut self, frame: &mut Frame, size: Rect) {
|
||||||
let items: Vec<_> =
|
let items: Vec<_> =
|
||||||
self.lines.iter().map(|l| ListItem::new(&l[..])).collect();
|
self.lines.iter().map(|l| ListItem::new(&l[..])).collect();
|
||||||
|
|
||||||
|
|
@ -463,7 +479,7 @@ impl UI {
|
||||||
fn enter_alternate_screen(&mut self) -> Result<()> {
|
fn enter_alternate_screen(&mut self) -> Result<()> {
|
||||||
if !self.alternate_screen {
|
if !self.alternate_screen {
|
||||||
enable_raw_mode()?;
|
enable_raw_mode()?;
|
||||||
execute!(stdout(), EnterAlternateScreen, DisableLineWrap)?;
|
execute!(stdout(), EnterAlternateScreen, DisableLineWrap,)?;
|
||||||
self.alternate_screen = true;
|
self.alternate_screen = true;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -478,6 +494,19 @@ impl UI {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn toggle_show_anonymous(&mut self) {
|
||||||
|
self.show_anonymous = !self.show_anonymous;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_render_listener(&self, listener: &Listener) -> bool {
|
||||||
|
// Named/Configured ports are always rendered
|
||||||
|
!listener.is_anonymous()
|
||||||
|
// ...or we might be explicitly asked to render everything
|
||||||
|
|| self.show_anonymous
|
||||||
|
// ...or the port might be enabled or errored
|
||||||
|
|| listener.state() != State::Disabled
|
||||||
|
}
|
||||||
|
|
||||||
async fn handle_events(&mut self, console_events: &mut EventStream) {
|
async fn handle_events(&mut self, console_events: &mut EventStream) {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
ev = console_events.next() => self.handle_console_event(ev),
|
ev = console_events.next() => self.handle_console_event(ev),
|
||||||
|
|
@ -585,6 +614,10 @@ impl UI {
|
||||||
_ = open::that(format!("http://127.0.0.1:{}/", p));
|
_ = open::that(format!("http://127.0.0.1:{}/", p));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
KeyEvent { code: KeyCode::Char('a'), .. } => {
|
||||||
|
self.toggle_show_anonymous()
|
||||||
|
}
|
||||||
|
|
||||||
_ => (),
|
_ => (),
|
||||||
},
|
},
|
||||||
Some(Ok(_)) => (), // Don't care about this event...
|
Some(Ok(_)) => (), // Don't care about this event...
|
||||||
|
|
@ -1251,4 +1284,118 @@ mod tests {
|
||||||
|
|
||||||
drop(sender);
|
drop(sender);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn listener_anonymous() {
|
||||||
|
let (sender, receiver) = mpsc::channel(64);
|
||||||
|
let mut config = ServerConfig::default();
|
||||||
|
config.insert(
|
||||||
|
8079,
|
||||||
|
PortConfig {
|
||||||
|
enabled: false,
|
||||||
|
description: Some("body once told me".to_string()),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut ui = UI::new(receiver, config);
|
||||||
|
|
||||||
|
ui.handle_internal_event(Some(UIEvent::Ports(vec![
|
||||||
|
PortDesc {
|
||||||
|
port: 8080,
|
||||||
|
desc: "python3 blaster.py".to_string(),
|
||||||
|
},
|
||||||
|
PortDesc { port: 8081, desc: "".to_string() },
|
||||||
|
PortDesc { port: 8082, desc: "".to_string() },
|
||||||
|
])));
|
||||||
|
|
||||||
|
// (Pretend that 8082 broke.)
|
||||||
|
ui.ports.get_mut(&8082).unwrap().state = State::Broken.boxed();
|
||||||
|
|
||||||
|
let listener = ui.ports.get(&8079).unwrap();
|
||||||
|
assert!(
|
||||||
|
!listener.is_anonymous(),
|
||||||
|
"Configured ports should not be anonymous"
|
||||||
|
);
|
||||||
|
|
||||||
|
let listener = ui.ports.get(&8080).unwrap();
|
||||||
|
assert!(
|
||||||
|
!listener.is_anonymous(),
|
||||||
|
"Ports with descriptions should not be anonymous"
|
||||||
|
);
|
||||||
|
|
||||||
|
let listener = ui.ports.get(&8081).unwrap();
|
||||||
|
assert!(
|
||||||
|
listener.is_anonymous(),
|
||||||
|
"Not configured, disabled, no description should be anonymous"
|
||||||
|
);
|
||||||
|
|
||||||
|
drop(sender);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn render_anonymous() {
|
||||||
|
let (sender, receiver) = mpsc::channel(64);
|
||||||
|
let mut config = ServerConfig::default();
|
||||||
|
config.insert(
|
||||||
|
8079,
|
||||||
|
PortConfig {
|
||||||
|
enabled: false,
|
||||||
|
description: Some("body once told me".to_string()),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut ui = UI::new(receiver, config);
|
||||||
|
|
||||||
|
ui.handle_internal_event(Some(UIEvent::Ports(vec![
|
||||||
|
PortDesc {
|
||||||
|
port: 8080,
|
||||||
|
desc: "python3 blaster.py".to_string(),
|
||||||
|
},
|
||||||
|
PortDesc { port: 8081, desc: "".to_string() },
|
||||||
|
PortDesc { port: 8082, desc: "".to_string() },
|
||||||
|
PortDesc { port: 8083, desc: "".to_string() },
|
||||||
|
])));
|
||||||
|
|
||||||
|
// (Pretend that 8082 broke.)
|
||||||
|
ui.ports.get_mut(&8082).unwrap().state = State::Broken.boxed();
|
||||||
|
|
||||||
|
// No showing anonymous ports!
|
||||||
|
ui.show_anonymous = false;
|
||||||
|
|
||||||
|
let listener = ui.ports.get(&8079).unwrap();
|
||||||
|
assert!(
|
||||||
|
ui.should_render_listener(listener),
|
||||||
|
"Configured ports should always be rendered"
|
||||||
|
);
|
||||||
|
|
||||||
|
let listener = ui.ports.get(&8080).unwrap();
|
||||||
|
assert!(
|
||||||
|
ui.should_render_listener(listener),
|
||||||
|
"Ports with descriptions should be rendered"
|
||||||
|
);
|
||||||
|
|
||||||
|
let listener = ui.ports.get(&8081).unwrap();
|
||||||
|
assert!(
|
||||||
|
!ui.should_render_listener(listener),
|
||||||
|
"Not configured, disabled, no description should be hidden"
|
||||||
|
);
|
||||||
|
|
||||||
|
ui.enable_disable_port(8081);
|
||||||
|
|
||||||
|
let listener = ui.ports.get(&8081).unwrap();
|
||||||
|
assert_eq!(listener.state(), State::Enabled);
|
||||||
|
assert!(
|
||||||
|
ui.should_render_listener(listener),
|
||||||
|
"Enabled ports should be rendered"
|
||||||
|
);
|
||||||
|
|
||||||
|
let listener = ui.ports.get(&8082).unwrap();
|
||||||
|
assert_eq!(listener.state(), State::Broken);
|
||||||
|
assert!(
|
||||||
|
ui.should_render_listener(listener),
|
||||||
|
"Broken ports should be rendered"
|
||||||
|
);
|
||||||
|
|
||||||
|
drop(sender);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,10 +19,10 @@ to send the the contents of `file`.
|
||||||
Options:
|
Options:
|
||||||
--version Print the version of fwd and exit
|
--version Print the version of fwd and exit
|
||||||
--sudo, -s Run the server side of fwd with `sudo`. This allows the
|
--sudo, -s Run the server side of fwd with `sudo`. This allows the
|
||||||
client to forward ports that are open by processes being
|
client to identify the ports that are open by processes
|
||||||
run under other accounts (e.g., docker containers being
|
being run under other accounts (e.g., docker containers
|
||||||
run as root), but requires sudo access on the server and
|
being run as root), but requires sudo access on the server
|
||||||
*might* end up forwarding ports that you do not want
|
and *might* end up forwarding ports that you do not want
|
||||||
forwarded (e.g., port 22 for sshd, or port 53 for systemd.)
|
forwarded (e.g., port 22 for sshd, or port 53 for systemd.)
|
||||||
--log-filter FILTER
|
--log-filter FILTER
|
||||||
Set remote server's log level. Default is `warn`. Supports
|
Set remote server's log level. Default is `warn`. Supports
|
||||||
|
|
|
||||||
|
|
@ -49,11 +49,11 @@ pub fn socket_path() -> Result<PathBuf> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn socket_directory() -> Result<std::path::PathBuf> {
|
fn socket_directory() -> Result<std::path::PathBuf> {
|
||||||
let base_directories = xdg::BaseDirectories::new()
|
match directories_next::ProjectDirs::from("", "", "fwd")
|
||||||
.context("Error creating BaseDirectories")?;
|
.and_then(|p| p.runtime_dir().map(|p| p.to_path_buf()))
|
||||||
match base_directories.place_runtime_file("fwd") {
|
{
|
||||||
Ok(path) => Ok(path),
|
Some(p) => Ok(p),
|
||||||
Err(_) => {
|
None => {
|
||||||
let mut path = std::env::temp_dir();
|
let mut path = std::env::temp_dir();
|
||||||
let uid = unsafe { libc::getuid() };
|
let uid = unsafe { libc::getuid() };
|
||||||
path.push(format!("fwd{}", uid));
|
path.push(format!("fwd{}", uid));
|
||||||
|
|
@ -117,7 +117,7 @@ async fn handle_connection(
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::message::MessageWriter;
|
use crate::message::MessageWriter;
|
||||||
use tempdir::TempDir;
|
use tempfile::TempDir;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn socket_path_repeats() {
|
fn socket_path_repeats() {
|
||||||
|
|
@ -131,8 +131,8 @@ mod tests {
|
||||||
async fn url_to_message() {
|
async fn url_to_message() {
|
||||||
let (sender, mut receiver) = mpsc::channel(64);
|
let (sender, mut receiver) = mpsc::channel(64);
|
||||||
|
|
||||||
let tmp_dir =
|
let tmp_dir = TempDir::with_prefix("url_to_message")
|
||||||
TempDir::new("url_to_message").expect("Error getting tmpdir");
|
.expect("Error getting tmpdir");
|
||||||
let path = tmp_dir.path().join("socket");
|
let path = tmp_dir.path().join("socket");
|
||||||
|
|
||||||
let path_override = path.clone();
|
let path_override = path.clone();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue