Compare commits
9
Commits
d51cc248e7
...
v0.1.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a202f97704 | ||
|
|
f6ead86db4 | ||
|
|
013d653f99 | ||
|
|
4e7e4cff10 | ||
|
|
fbad776441 | ||
|
|
90d9b583c0 | ||
|
|
f7c4ebaaf7 | ||
|
|
27419f4ca8 | ||
|
|
ec2422bc3e |
Generated
+1
-1
@@ -436,7 +436,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dosh"
|
name = "dosh"
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "dosh"
|
name = "dosh"
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ Install the client on macOS:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -fsSL https://git.palav.dev/Palav/dosh/raw/branch/main/install.sh \
|
curl -fsSL https://git.palav.dev/Palav/dosh/raw/branch/main/install.sh \
|
||||||
| DOSH_REPO=https://git.palav.dev/Palav/dosh.git DOSH_SERVER=palav DOSH_HOST=git.palav.dev DOSH_PORT=50000 sh -s -- client
|
| DOSH_REPO=https://git.palav.dev/Palav/dosh.git DOSH_PORT=50000 sh -s -- client
|
||||||
```
|
```
|
||||||
|
|
||||||
Update an installed client later:
|
Update an installed client later:
|
||||||
@@ -123,28 +123,43 @@ dosh --version
|
|||||||
|
|
||||||
`dosh update` first tries a release tarball named for the platform
|
`dosh update` first tries a release tarball named for the platform
|
||||||
(`dosh-macos-aarch64.tar.gz`, `dosh-linux-x86_64.tar.gz`, etc.) from the latest
|
(`dosh-macos-aarch64.tar.gz`, `dosh-linux-x86_64.tar.gz`, etc.) from the latest
|
||||||
Gitea/GitHub release. If that asset is not published yet, it falls back to the
|
Gitea/GitHub release. Gitea's concrete tag download route is detected when
|
||||||
source build path. If the release also publishes `<artifact>.sha256`, the installer
|
`/releases/latest/download/...` is not supported. If that asset is not published
|
||||||
verifies the archive before installing it.
|
yet, it falls back to the source build path. If the release also publishes
|
||||||
|
`<artifact>.sha256`, the installer verifies the archive before installing it.
|
||||||
|
|
||||||
Install the client on Windows PowerShell:
|
Install the client on Windows PowerShell:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
$env:DOSH_REPO="https://git.palav.dev/Palav/dosh.git"; $env:DOSH_SERVER="palav"; $env:DOSH_HOST="git.palav.dev"; $env:DOSH_PORT="50000"; irm https://git.palav.dev/Palav/dosh/raw/branch/main/install.ps1 | iex
|
$env:DOSH_REPO="https://git.palav.dev/Palav/dosh.git"; $env:DOSH_PORT="50000"; irm https://git.palav.dev/Palav/dosh/raw/branch/main/install.ps1 | iex
|
||||||
```
|
```
|
||||||
|
|
||||||
Attach:
|
Attach:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
dosh palav
|
dosh user@server.example.com
|
||||||
```
|
```
|
||||||
|
|
||||||
Plain `dosh palav` opens a fresh terminal session. Use named sessions when you want
|
First-time setup for an existing SSH alias:
|
||||||
to reattach to the same persistent terminal from multiple clients:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
dosh --session work palav
|
dosh setup homelab
|
||||||
dosh --session logs palav
|
dosh selftest homelab
|
||||||
|
dosh homelab
|
||||||
|
```
|
||||||
|
|
||||||
|
`dosh setup <ssh-alias>` imports the OpenSSH alias into
|
||||||
|
`~/.config/dosh/hosts.toml`, pins the Dosh host key over SSH, runs `dosh doctor`,
|
||||||
|
and prints next commands. `dosh selftest <host>` checks local terminal overlay
|
||||||
|
safety, forwarding syntax, and the remote doctor path.
|
||||||
|
|
||||||
|
Plain `dosh user@server.example.com` opens a fresh terminal session. Use named
|
||||||
|
sessions when you want to reattach to the same persistent terminal from multiple
|
||||||
|
clients:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dosh --session work user@server.example.com
|
||||||
|
dosh --session logs user@server.example.com
|
||||||
```
|
```
|
||||||
|
|
||||||
Press `Ctrl-]` to detach the current client while leaving the server session alive.
|
Press `Ctrl-]` to detach the current client while leaving the server session alive.
|
||||||
@@ -164,13 +179,21 @@ If SSH and UDP use different public names, specify the UDP address:
|
|||||||
dosh-client --dosh-host public.example.com --dosh-port 50000 user@host
|
dosh-client --dosh-host public.example.com --dosh-port 50000 user@host
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Forwarding can use SSH-shaped flags directly or the forward-only wrapper:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dosh -N -L 8080:127.0.0.1:80 homelab
|
||||||
|
dosh forward homelab -L 8080:127.0.0.1:80 -D 1080
|
||||||
|
```
|
||||||
|
|
||||||
Dosh already lets OpenSSH handle SSH aliases, users, keys, ports, known-hosts,
|
Dosh already lets OpenSSH handle SSH aliases, users, keys, ports, known-hosts,
|
||||||
ProxyJump, and other SSH config during bootstrap. It also runs `ssh -G` to infer the
|
ProxyJump, and other SSH config during bootstrap. It also runs `ssh -G` to infer the
|
||||||
UDP host from an SSH alias when `dosh_host` is not configured. To make that explicit
|
UDP host from an SSH alias when `dosh_host` is not configured. To make that explicit
|
||||||
in Dosh's host config:
|
in Dosh's host config:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
dosh import-ssh palav homelab
|
dosh import-ssh homelab
|
||||||
|
dosh homelab
|
||||||
```
|
```
|
||||||
|
|
||||||
Optional command extensions are just config-side startup shortcuts; Dosh has no
|
Optional command extensions are just config-side startup shortcuts; Dosh has no
|
||||||
@@ -184,12 +207,12 @@ command = "tm {args}"
|
|||||||
description = "Open the server-side tmux dashboard"
|
description = "Open the server-side tmux dashboard"
|
||||||
```
|
```
|
||||||
|
|
||||||
Then `dosh palav tm` sends `tm`, and `dosh palav tm dosh` sends `tm 'dosh'`.
|
Then `dosh homelab tm` sends `tm`, and `dosh homelab tm dosh` sends `tm 'dosh'`.
|
||||||
Remove that table to remove the integration. Hosts can override or opt out:
|
Remove that table to remove the integration. Hosts can override or opt out:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
# ~/.config/dosh/hosts.toml
|
# ~/.config/dosh/hosts.toml
|
||||||
[palav.extensions.tm]
|
[homelab.extensions.tm]
|
||||||
command = "/opt/tm/bin/tm {args}"
|
command = "/opt/tm/bin/tm {args}"
|
||||||
|
|
||||||
[other-host.extensions.tm]
|
[other-host.extensions.tm]
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ Dosh also calls `ssh -G <alias>` to infer the UDP target host when no `dosh_host
|
|||||||
configured. To write explicit Dosh host entries from SSH aliases:
|
configured. To write explicit Dosh host entries from SSH aliases:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
dosh import-ssh palav homelab
|
dosh import-ssh homelab
|
||||||
```
|
```
|
||||||
|
|
||||||
This appends entries to `~/.config/dosh/hosts.toml` without trying to become an
|
This appends entries to `~/.config/dosh/hosts.toml` without trying to become an
|
||||||
@@ -112,12 +112,12 @@ command = "tm {args}"
|
|||||||
description = "Open the server-side tmux dashboard"
|
description = "Open the server-side tmux dashboard"
|
||||||
```
|
```
|
||||||
|
|
||||||
With that config, `dosh palav tm` runs `tm` in the remote Dosh shell and
|
With that config, `dosh homelab tm` runs `tm` in the remote Dosh shell and
|
||||||
`dosh palav tm dosh` runs `tm 'dosh'`. Removing the table removes the integration.
|
`dosh homelab tm dosh` runs `tm 'dosh'`. Removing the table removes the integration.
|
||||||
Host config can override a global extension, or disable it:
|
Host config can override a global extension, or disable it:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[palav.extensions.tm]
|
[homelab.extensions.tm]
|
||||||
command = "/opt/tm/bin/tm {args}"
|
command = "/opt/tm/bin/tm {args}"
|
||||||
|
|
||||||
[other-host.extensions.tm]
|
[other-host.extensions.tm]
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
# Dosh Release Evidence - 2026-06-20
|
# Dosh Release Evidence - 2026-06-20
|
||||||
|
|
||||||
Code benchmarked: `b44ff8e Improve release and benchmark tooling`
|
Runtime code benchmarked: `b44ff8e Improve release and benchmark tooling`
|
||||||
|
|
||||||
|
Release docs/default cleanup after that benchmark removed personal host aliases from
|
||||||
|
public examples and test fixtures; it did not change release runtime paths.
|
||||||
|
|
||||||
Environment:
|
Environment:
|
||||||
|
|
||||||
@@ -21,8 +24,10 @@ Artifacts:
|
|||||||
|
|
||||||
| artifact | sha256 |
|
| artifact | sha256 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `dosh-linux-x86_64.tar.gz` | `f421ee56aac61892c9e152d2ef018cf95eb35dec2ca1b2b5cc82eeb1fcc12911` |
|
| `dosh-linux-x86_64.tar.gz` | `9f586852a506cca3caf618743e12967265879ee10f5294d179cdcd668db1e808` |
|
||||||
| `dosh-0.1.0-linux-x86_64.tar.gz` | `f421ee56aac61892c9e152d2ef018cf95eb35dec2ca1b2b5cc82eeb1fcc12911` |
|
| `dosh-0.1.0-linux-x86_64.tar.gz` | `9f586852a506cca3caf618743e12967265879ee10f5294d179cdcd668db1e808` |
|
||||||
|
| `dosh-macos-aarch64.tar.gz` | `dd1151aa2b40be37288dc19a07521b0ec72d1106439eb484cdff01cca3b891c6` |
|
||||||
|
| `dosh-0.1.0-macos-aarch64.tar.gz` | `dd1151aa2b40be37288dc19a07521b0ec72d1106439eb484cdff01cca3b891c6` |
|
||||||
|
|
||||||
The installer verifies `<artifact>.sha256` when the sidecar is published.
|
The installer verifies `<artifact>.sha256` when the sidecar is published.
|
||||||
|
|
||||||
|
|||||||
+7
-2
@@ -6,7 +6,7 @@ param(
|
|||||||
[string]$DoshHost = $(if ($env:DOSH_HOST) { $env:DOSH_HOST } else { $env:DOSH_DOSH_HOST }),
|
[string]$DoshHost = $(if ($env:DOSH_HOST) { $env:DOSH_HOST } else { $env:DOSH_DOSH_HOST }),
|
||||||
[int]$Port = $(if ($env:DOSH_PORT) { [int]$env:DOSH_PORT } else { 50000 }),
|
[int]$Port = $(if ($env:DOSH_PORT) { [int]$env:DOSH_PORT } else { 50000 }),
|
||||||
[string]$Prefix = $(if ($env:PREFIX) { $env:PREFIX } else { Join-Path $HOME ".local" }),
|
[string]$Prefix = $(if ($env:PREFIX) { $env:PREFIX } else { Join-Path $HOME ".local" }),
|
||||||
[switch]$UsePrebuilt = $($env:DOSH_USE_PREBUILT -and $env:DOSH_USE_PREBUILT -ne "0"),
|
[switch]$UsePrebuilt = $(-not $env:DOSH_USE_PREBUILT -or $env:DOSH_USE_PREBUILT -ne "0"),
|
||||||
[string]$BinaryUrl = $env:DOSH_BINARY_URL,
|
[string]$BinaryUrl = $env:DOSH_BINARY_URL,
|
||||||
[string]$BinaryBase = $env:DOSH_BINARY_BASE,
|
[string]$BinaryBase = $env:DOSH_BINARY_BASE,
|
||||||
[string]$BinaryName = $env:DOSH_BINARY_NAME,
|
[string]$BinaryName = $env:DOSH_BINARY_NAME,
|
||||||
@@ -200,7 +200,12 @@ Write-Host "Installed Dosh client to $bindir"
|
|||||||
Write-Host "Configured UDP port $Port"
|
Write-Host "Configured UDP port $Port"
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
$displayServer = if ($Server) { $Server } else { "user@host" }
|
$displayServer = if ($Server) { $Server } else { "user@host" }
|
||||||
Write-Host "Client command:"
|
Write-Host "Client commands:"
|
||||||
Write-Host " $bindir\dosh.exe $displayServer"
|
Write-Host " $bindir\dosh.exe $displayServer"
|
||||||
|
Write-Host " $bindir\dosh.exe setup <ssh-alias>"
|
||||||
|
Write-Host " $bindir\dosh.exe update --check"
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Client config:"
|
||||||
|
Write-Host " $configDir\client.toml"
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host "Open a new terminal for PATH changes to apply."
|
Write-Host "Open a new terminal for PATH changes to apply."
|
||||||
|
|||||||
+41
-11
@@ -12,7 +12,7 @@ start_server=1
|
|||||||
force_config=0
|
force_config=0
|
||||||
update_cache="${DOSH_UPDATE_CACHE:-$HOME/.cache/dosh/source}"
|
update_cache="${DOSH_UPDATE_CACHE:-$HOME/.cache/dosh/source}"
|
||||||
quiet="${DOSH_UPDATE_QUIET:-0}"
|
quiet="${DOSH_UPDATE_QUIET:-0}"
|
||||||
use_prebuilt="${DOSH_USE_PREBUILT:-0}"
|
use_prebuilt="${DOSH_USE_PREBUILT:-1}"
|
||||||
binary_url="${DOSH_BINARY_URL:-}"
|
binary_url="${DOSH_BINARY_URL:-}"
|
||||||
binary_base="${DOSH_BINARY_BASE:-}"
|
binary_base="${DOSH_BINARY_BASE:-}"
|
||||||
binary_name="${DOSH_BINARY_NAME:-}"
|
binary_name="${DOSH_BINARY_NAME:-}"
|
||||||
@@ -38,8 +38,8 @@ Options:
|
|||||||
Environment alternatives:
|
Environment alternatives:
|
||||||
DOSH_REPO, DOSH_ROLE, DOSH_SERVER, DOSH_HOST, DOSH_PORT, PREFIX,
|
DOSH_REPO, DOSH_ROLE, DOSH_SERVER, DOSH_HOST, DOSH_PORT, PREFIX,
|
||||||
DOSH_UPDATE_CACHE
|
DOSH_UPDATE_CACHE
|
||||||
DOSH_USE_PREBUILT=1
|
DOSH_USE_PREBUILT=0
|
||||||
Try a release tarball before building from source
|
Build from source instead of trying a release tarball first
|
||||||
DOSH_BINARY_URL URL
|
DOSH_BINARY_URL URL
|
||||||
Exact release tarball URL to install
|
Exact release tarball URL to install
|
||||||
DOSH_BINARY_BASE URL
|
DOSH_BINARY_BASE URL
|
||||||
@@ -204,6 +204,24 @@ release_download_url() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
release_latest_tag_download_url() {
|
||||||
|
if [ -n "$binary_url" ] || [ -n "$binary_base" ] || [ "$binary_version" != "latest" ] || [ -z "$repo" ]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
web_base="$(repo_web_base "$repo")" || return 1
|
||||||
|
latest_url="$(curl -fsSL -o /dev/null -w '%{url_effective}' "$web_base/releases/latest" 2>/dev/null || true)"
|
||||||
|
case "$latest_url" in
|
||||||
|
"$web_base"/releases/tag/*)
|
||||||
|
tag="${latest_url##"$web_base"/releases/tag/}"
|
||||||
|
[ -n "$tag" ] || return 1
|
||||||
|
printf '%s/releases/download/%s/%s\n' "$web_base" "$tag" "$(release_artifact_name)"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
find_extracted_binary() {
|
find_extracted_binary() {
|
||||||
find "$1" -type f -name "$2" 2>/dev/null | sed -n '1p'
|
find "$1" -type f -name "$2" 2>/dev/null | sed -n '1p'
|
||||||
}
|
}
|
||||||
@@ -254,9 +272,14 @@ try_install_prebuilt() {
|
|||||||
[ "$quiet" = "1" ] && echo "Trying Dosh prebuilt $(release_artifact_name)"
|
[ "$quiet" = "1" ] && echo "Trying Dosh prebuilt $(release_artifact_name)"
|
||||||
need curl
|
need curl
|
||||||
need tar
|
need tar
|
||||||
if ! curl -fL "$download_url" -o "$archive"; then
|
if ! curl -fsL "$download_url" -o "$archive" 2>/dev/null; then
|
||||||
echo "prebuilt unavailable: $download_url" >&2
|
alt_download_url="$(release_latest_tag_download_url || true)"
|
||||||
return 1
|
if [ -z "$alt_download_url" ] || ! curl -fsL "$alt_download_url" -o "$archive"; then
|
||||||
|
echo "prebuilt unavailable: $download_url" >&2
|
||||||
|
[ -z "$alt_download_url" ] || echo "prebuilt unavailable: $alt_download_url" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
download_url="$alt_download_url"
|
||||||
fi
|
fi
|
||||||
verify_archive_checksum "$download_url" "$archive" "$checksum_file" || return 1
|
verify_archive_checksum "$download_url" "$archive" "$checksum_file" || return 1
|
||||||
mkdir -p "$tmpdir/extract"
|
mkdir -p "$tmpdir/extract"
|
||||||
@@ -482,9 +505,9 @@ EOF
|
|||||||
fi
|
fi
|
||||||
cat >"$hosts_config" <<EOF
|
cat >"$hosts_config" <<EOF
|
||||||
# Example:
|
# Example:
|
||||||
# [palav]
|
# [homelab]
|
||||||
# ssh = "palav"
|
# ssh = "homelab"
|
||||||
# dosh_host = "palav.dev"
|
# dosh_host = "server.example.com"
|
||||||
# port = 50000
|
# port = 50000
|
||||||
# default_command = "tm"
|
# default_command = "tm"
|
||||||
# predict = true
|
# predict = true
|
||||||
@@ -504,10 +527,17 @@ Configured UDP port $port
|
|||||||
EOF
|
EOF
|
||||||
|
|
||||||
if [ "$role" = "client" ] || [ "$role" = "both" ]; then
|
if [ "$role" = "client" ] || [ "$role" = "both" ]; then
|
||||||
|
next_server="${server:-user@host}"
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
|
|
||||||
Client command:
|
Client commands:
|
||||||
$bindir/dosh ${server:-user@host}
|
$bindir/dosh $next_server
|
||||||
|
$bindir/dosh setup <ssh-alias>
|
||||||
|
$bindir/dosh update --check
|
||||||
|
|
||||||
|
Client config:
|
||||||
|
$config_dir/client.toml
|
||||||
|
$config_dir/hosts.toml
|
||||||
EOF
|
EOF
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ dosh doctor homelab # host resolution, trust state, UDP reachability, server
|
|||||||
dosh sessions homelab # list live sessions
|
dosh sessions homelab # list live sessions
|
||||||
dosh trust homelab # fetch + pin the Dosh host key (via SSH fallback)
|
dosh trust homelab # fetch + pin the Dosh host key (via SSH fallback)
|
||||||
dosh trust --remove homelab
|
dosh trust --remove homelab
|
||||||
dosh import-ssh palav homelab # write a hosts.toml entry from an SSH alias
|
dosh import-ssh homelab # write a hosts.toml entry from an SSH alias
|
||||||
dosh update # update the installed client
|
dosh update # update the installed client
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -43,13 +43,22 @@ fi
|
|||||||
api="$base/api/v1/repos/$repo"
|
api="$base/api/v1/repos/$repo"
|
||||||
auth_header="Authorization: token $token"
|
auth_header="Authorization: token $token"
|
||||||
|
|
||||||
release_json="$(curl -fsS -H "$auth_header" "$api/releases/tags/$tag" || true)"
|
release_json="$(curl -fsS -H "$auth_header" "$api/releases/tags/$tag" 2>/dev/null || true)"
|
||||||
release_id="$(printf '%s\n' "$release_json" | sed -n 's/.*"id":[[:space:]]*\([0-9][0-9]*\).*/\1/p' | sed -n '1p')"
|
release_id=""
|
||||||
|
if [ -n "$release_json" ] && command -v jq >/dev/null 2>&1; then
|
||||||
|
release_id="$(printf '%s\n' "$release_json" | jq -r '.id // empty')"
|
||||||
|
elif [ -n "$release_json" ]; then
|
||||||
|
release_id="$(printf '%s\n' "$release_json" | sed -n 's/^[[:space:]]*{"id":[[:space:]]*\([0-9][0-9]*\).*/\1/p' | sed -n '1p')"
|
||||||
|
fi
|
||||||
|
|
||||||
if [ -z "$release_id" ]; then
|
if [ -z "$release_id" ]; then
|
||||||
payload="$(printf '{"tag_name":"%s","target_commitish":"main","name":"%s","body":"Dosh release %s","draft":false,"prerelease":false}\n' "$tag" "$title" "$tag")"
|
payload="$(printf '{"tag_name":"%s","target_commitish":"main","name":"%s","body":"Dosh release %s","draft":false,"prerelease":false}\n' "$tag" "$title" "$tag")"
|
||||||
release_json="$(curl -fsS -H "$auth_header" -H 'Content-Type: application/json' -X POST "$api/releases" -d "$payload")"
|
release_json="$(curl -fsS -H "$auth_header" -H 'Content-Type: application/json' -X POST "$api/releases" -d "$payload")"
|
||||||
release_id="$(printf '%s\n' "$release_json" | sed -n 's/.*"id":[[:space:]]*\([0-9][0-9]*\).*/\1/p' | sed -n '1p')"
|
if command -v jq >/dev/null 2>&1; then
|
||||||
|
release_id="$(printf '%s\n' "$release_json" | jq -r '.id // empty')"
|
||||||
|
else
|
||||||
|
release_id="$(printf '%s\n' "$release_json" | sed -n 's/^[[:space:]]*{"id":[[:space:]]*\([0-9][0-9]*\).*/\1/p' | sed -n '1p')"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -z "$release_id" ]; then
|
if [ -z "$release_id" ]; then
|
||||||
@@ -57,11 +66,29 @@ if [ -z "$release_id" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
delete_existing_asset() {
|
||||||
|
name="$1"
|
||||||
|
if ! command -v jq >/dev/null 2>&1; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
printf '%s\n' "$release_json" \
|
||||||
|
| jq -r --arg name "$name" '.assets[]? | select(.name == $name) | .id' \
|
||||||
|
| while IFS= read -r asset_id; do
|
||||||
|
[ -n "$asset_id" ] || continue
|
||||||
|
echo "replacing $name"
|
||||||
|
curl -fsS \
|
||||||
|
-H "$auth_header" \
|
||||||
|
-X DELETE \
|
||||||
|
"$api/releases/$release_id/assets/$asset_id" >/dev/null
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
for artifact in "$@"; do
|
for artifact in "$@"; do
|
||||||
if [ ! -f "$artifact" ]; then
|
if [ ! -f "$artifact" ]; then
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
name="$(basename "$artifact")"
|
name="$(basename "$artifact")"
|
||||||
|
delete_existing_asset "$name"
|
||||||
echo "uploading $name"
|
echo "uploading $name"
|
||||||
curl -fsS \
|
curl -fsS \
|
||||||
-H "$auth_header" \
|
-H "$auth_header" \
|
||||||
|
|||||||
+440
-44
@@ -66,7 +66,7 @@ fn terminal_size() -> (u16, u16) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Parser)]
|
#[derive(Debug, Clone, Parser)]
|
||||||
#[command(name = "dosh-client", version)]
|
#[command(name = "dosh-client", version)]
|
||||||
struct Args {
|
struct Args {
|
||||||
#[arg()]
|
#[arg()]
|
||||||
@@ -189,11 +189,20 @@ struct PendingStreamChunk {
|
|||||||
|
|
||||||
#[tokio::main(flavor = "current_thread")]
|
#[tokio::main(flavor = "current_thread")]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
let args = Args::parse();
|
let mut args = Args::parse();
|
||||||
let config = load_client_config(None).unwrap_or_default();
|
let config = load_client_config(None).unwrap_or_default();
|
||||||
if args.server.as_deref() == Some("update") {
|
if args.server.as_deref() == Some("update") {
|
||||||
return run_update(&config, update_check_requested(&args.command)?);
|
return run_update(&config, update_check_requested(&args.command)?);
|
||||||
}
|
}
|
||||||
|
if args.server.as_deref() == Some("setup") {
|
||||||
|
return run_setup_command(&config, &args).await;
|
||||||
|
}
|
||||||
|
if args.server.as_deref() == Some("selftest") {
|
||||||
|
return run_selftest_command(&config, &args).await;
|
||||||
|
}
|
||||||
|
if args.server.as_deref() == Some("forward") {
|
||||||
|
args = rewrite_forward_command(args)?;
|
||||||
|
}
|
||||||
if args.server.as_deref() == Some("import-ssh") {
|
if args.server.as_deref() == Some("import-ssh") {
|
||||||
return run_import_ssh(&config, &args.command);
|
return run_import_ssh(&config, &args.command);
|
||||||
}
|
}
|
||||||
@@ -587,15 +596,113 @@ fn run_trust_command(config: &dosh::config::ClientConfig, args: &Args) -> Result
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn run_setup_command(config: &dosh::config::ClientConfig, args: &Args) -> Result<()> {
|
||||||
|
let host = args
|
||||||
|
.command
|
||||||
|
.first()
|
||||||
|
.cloned()
|
||||||
|
.ok_or_else(|| anyhow!("usage: dosh setup <ssh-host>"))?;
|
||||||
|
println!("Dosh setup for {host}");
|
||||||
|
|
||||||
|
let imported = import_ssh_aliases(config, std::slice::from_ref(&host))?;
|
||||||
|
for entry in imported {
|
||||||
|
match entry.action {
|
||||||
|
ImportAction::Added => println!(
|
||||||
|
"[ok] host config: [{}] ssh={} dosh_host={} port={}",
|
||||||
|
entry.alias, entry.ssh, entry.dosh_host, entry.port
|
||||||
|
),
|
||||||
|
ImportAction::Skipped => println!("[ok] host config: [{}] already exists", entry.alias),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let hosts = load_hosts_config(None).unwrap_or_default();
|
||||||
|
let host_config = hosts.hosts.get(&host).cloned().unwrap_or_default();
|
||||||
|
let raw_server = host_config.ssh.clone().unwrap_or_else(|| host.clone());
|
||||||
|
let server = ssh_with_user(&raw_server, host_config.user.as_deref());
|
||||||
|
let ssh_port = args.ssh_port.or(host_config.ssh_port).or(config.ssh_port);
|
||||||
|
let ssh_auth_command = args
|
||||||
|
.ssh_auth_command
|
||||||
|
.clone()
|
||||||
|
.or_else(|| config.ssh_auth_command.clone())
|
||||||
|
.unwrap_or_else(|| "~/.local/bin/dosh-auth".to_string());
|
||||||
|
let known_hosts = expand_tilde(&config.known_hosts);
|
||||||
|
let public_key = fetch_host_key_over_ssh(
|
||||||
|
&server,
|
||||||
|
ssh_port,
|
||||||
|
&ssh_auth_command,
|
||||||
|
args.ssh_key.as_deref(),
|
||||||
|
args.ssh_known_hosts.as_deref(),
|
||||||
|
args.ssh_control_path.as_deref(),
|
||||||
|
)
|
||||||
|
.with_context(|| {
|
||||||
|
format!("fetch Dosh host key over SSH; make sure dosh-auth is installed on {server}")
|
||||||
|
})?;
|
||||||
|
match trust_host(&known_hosts, &host, &public_key, "ssh", args.replace)? {
|
||||||
|
TrustResult::AlreadyTrusted => println!(
|
||||||
|
"[ok] trust: already trusted {}",
|
||||||
|
host_fingerprint(&public_key)
|
||||||
|
),
|
||||||
|
TrustResult::Trusted => println!(
|
||||||
|
"[ok] trust: pinned {} in {}",
|
||||||
|
host_fingerprint(&public_key),
|
||||||
|
known_hosts.display()
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
println!();
|
||||||
|
run_doctor_for_host(config, args, &host).await?;
|
||||||
|
println!();
|
||||||
|
println!("Next:");
|
||||||
|
println!(" dosh {host}");
|
||||||
|
println!(" dosh update --check");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_selftest_command(config: &dosh::config::ClientConfig, args: &Args) -> Result<()> {
|
||||||
|
let host = args
|
||||||
|
.command
|
||||||
|
.first()
|
||||||
|
.cloned()
|
||||||
|
.ok_or_else(|| anyhow!("usage: dosh selftest <host>"))?;
|
||||||
|
println!("Dosh selftest for {host}");
|
||||||
|
println!("[ok] terminal size fallback: {:?}", terminal_size());
|
||||||
|
|
||||||
|
let overlay = render_status_overlay("[dosh] reconnecting — last contact 3s ago", 24);
|
||||||
|
ensure_tui_safe_status_overlay(&overlay)?;
|
||||||
|
println!("[ok] disconnect UI: save/restore bottom-row overlay");
|
||||||
|
|
||||||
|
parse_local_forward("18080:127.0.0.1:80")?;
|
||||||
|
parse_remote_forward("19090:127.0.0.1:5432")?;
|
||||||
|
parse_dynamic_forward("11080")?;
|
||||||
|
println!("[ok] forwarding parser: -L, -R, -D");
|
||||||
|
|
||||||
|
println!();
|
||||||
|
run_doctor_for_host(config, args, &host).await?;
|
||||||
|
println!();
|
||||||
|
println!("[ok] selftest complete");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn run_doctor_command(config: &dosh::config::ClientConfig, args: &Args) -> Result<()> {
|
async fn run_doctor_command(config: &dosh::config::ClientConfig, args: &Args) -> Result<()> {
|
||||||
let requested = args
|
let requested = args
|
||||||
.command
|
.command
|
||||||
.first()
|
.first()
|
||||||
.cloned()
|
.cloned()
|
||||||
.ok_or_else(|| anyhow!("usage: dosh doctor <host>"))?;
|
.ok_or_else(|| anyhow!("usage: dosh doctor <host>"))?;
|
||||||
|
run_doctor_for_host(config, args, &requested).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_doctor_for_host(
|
||||||
|
config: &dosh::config::ClientConfig,
|
||||||
|
args: &Args,
|
||||||
|
requested: &str,
|
||||||
|
) -> Result<()> {
|
||||||
let hosts = load_hosts_config(None).unwrap_or_default();
|
let hosts = load_hosts_config(None).unwrap_or_default();
|
||||||
let host_config = hosts.hosts.get(&requested).cloned().unwrap_or_default();
|
let host_config = hosts.hosts.get(requested).cloned().unwrap_or_default();
|
||||||
let raw_server = host_config.ssh.clone().unwrap_or_else(|| requested.clone());
|
let raw_server = host_config
|
||||||
|
.ssh
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| requested.to_string());
|
||||||
let server = ssh_with_user(&raw_server, host_config.user.as_deref());
|
let server = ssh_with_user(&raw_server, host_config.user.as_deref());
|
||||||
let ssh_port = args.ssh_port.or(host_config.ssh_port).or(config.ssh_port);
|
let ssh_port = args.ssh_port.or(host_config.ssh_port).or(config.ssh_port);
|
||||||
let dosh_port = args
|
let dosh_port = args
|
||||||
@@ -603,6 +710,20 @@ async fn run_doctor_command(config: &dosh::config::ClientConfig, args: &Args) ->
|
|||||||
.or(host_config.port)
|
.or(host_config.port)
|
||||||
.unwrap_or(config.dosh_port);
|
.unwrap_or(config.dosh_port);
|
||||||
println!("Dosh doctor for {requested}");
|
println!("Dosh doctor for {requested}");
|
||||||
|
println!(
|
||||||
|
"[info] config: client={} hosts={} known_hosts={}",
|
||||||
|
expand_tilde("~/.config/dosh/client.toml").display(),
|
||||||
|
expand_tilde("~/.config/dosh/hosts.toml").display(),
|
||||||
|
expand_tilde(&config.known_hosts).display()
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"[info] client: auth={} predict={}({}) disconnect_status={} cache_tickets={}",
|
||||||
|
config.auth_preference,
|
||||||
|
config.predict,
|
||||||
|
config.predict_mode,
|
||||||
|
config.disconnect_status,
|
||||||
|
config.cache_attach_tickets
|
||||||
|
);
|
||||||
|
|
||||||
let parsed_ssh_config = match ssh_config(&server, ssh_port) {
|
let parsed_ssh_config = match ssh_config(&server, ssh_port) {
|
||||||
Ok(parsed) => {
|
Ok(parsed) => {
|
||||||
@@ -652,6 +773,14 @@ async fn run_doctor_command(config: &dosh::config::ClientConfig, args: &Args) ->
|
|||||||
.unwrap_or_else(|| ssh_destination_host(&server))
|
.unwrap_or_else(|| ssh_destination_host(&server))
|
||||||
});
|
});
|
||||||
println!("[info] native endpoint: {udp_host}:{dosh_port}");
|
println!("[info] native endpoint: {udp_host}:{dosh_port}");
|
||||||
|
match resolve_addr(&udp_host, dosh_port) {
|
||||||
|
Ok(addr) => println!("[ok] udp resolve: {addr}"),
|
||||||
|
Err(err) => println!("[fail] udp resolve: {err:#}"),
|
||||||
|
}
|
||||||
|
println!(
|
||||||
|
"[info] forwarding requested by config: agent={} default_session={}",
|
||||||
|
config.forward_agent, config.default_session
|
||||||
|
);
|
||||||
let socket = UdpSocket::bind("0.0.0.0:0").await?;
|
let socket = UdpSocket::bind("0.0.0.0:0").await?;
|
||||||
match try_native_auth_check(
|
match try_native_auth_check(
|
||||||
&socket,
|
&socket,
|
||||||
@@ -693,6 +822,26 @@ async fn run_doctor_command(config: &dosh::config::ClientConfig, args: &Args) ->
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn ensure_tui_safe_status_overlay(bytes: &[u8]) -> Result<()> {
|
||||||
|
anyhow::ensure!(
|
||||||
|
bytes.starts_with(b"\x1b7"),
|
||||||
|
"status overlay must save cursor"
|
||||||
|
);
|
||||||
|
anyhow::ensure!(
|
||||||
|
bytes.ends_with(b"\x1b8"),
|
||||||
|
"status overlay must restore cursor"
|
||||||
|
);
|
||||||
|
anyhow::ensure!(
|
||||||
|
!contains_bytes(bytes, b"\n") && !contains_bytes(bytes, b"\r"),
|
||||||
|
"status overlay must not scroll"
|
||||||
|
);
|
||||||
|
anyhow::ensure!(
|
||||||
|
!contains_bytes(bytes, b"\x1b[?25l") && !contains_bytes(bytes, b"\x1b[?25h"),
|
||||||
|
"status overlay must not toggle cursor visibility"
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn ssh_fallback_probe(
|
fn ssh_fallback_probe(
|
||||||
server: &str,
|
server: &str,
|
||||||
ssh_port: Option<u16>,
|
ssh_port: Option<u16>,
|
||||||
@@ -905,6 +1054,63 @@ fn spawn_background_forwarder(args: &Args, forwarding_requested: bool) -> Result
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn rewrite_forward_command(mut args: Args) -> Result<Args> {
|
||||||
|
let mut tokens = std::mem::take(&mut args.command).into_iter();
|
||||||
|
let host = tokens.next().ok_or_else(|| {
|
||||||
|
anyhow!("usage: dosh forward <host> [-L spec] [-R spec] [-D spec] [-A] [-f]")
|
||||||
|
})?;
|
||||||
|
let mut trailing_command = Vec::new();
|
||||||
|
while let Some(token) = tokens.next() {
|
||||||
|
match token.as_str() {
|
||||||
|
"-L" | "--local-forward" => {
|
||||||
|
let spec = tokens
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| anyhow!("dosh forward: {token} requires a forwarding spec"))?;
|
||||||
|
args.local_forward.push(spec);
|
||||||
|
}
|
||||||
|
"-R" | "--remote-forward" => {
|
||||||
|
let spec = tokens
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| anyhow!("dosh forward: {token} requires a forwarding spec"))?;
|
||||||
|
args.remote_forward.push(spec);
|
||||||
|
}
|
||||||
|
"-D" | "--dynamic-forward" => {
|
||||||
|
let spec = tokens
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| anyhow!("dosh forward: {token} requires a forwarding spec"))?;
|
||||||
|
args.dynamic_forward.push(spec);
|
||||||
|
}
|
||||||
|
"-A" | "--forward-agent" => args.forward_agent = true,
|
||||||
|
"-f" | "--background" => args.background = true,
|
||||||
|
"-N" | "--forward-only" => args.forward_only = true,
|
||||||
|
"--" => {
|
||||||
|
trailing_command.extend(tokens);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
other => return Err(anyhow!("dosh forward: unknown option {other:?}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !trailing_command.is_empty() {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"dosh forward does not run remote commands; remove {:?}",
|
||||||
|
trailing_command
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if args.local_forward.is_empty()
|
||||||
|
&& args.remote_forward.is_empty()
|
||||||
|
&& args.dynamic_forward.is_empty()
|
||||||
|
&& !args.forward_agent
|
||||||
|
{
|
||||||
|
return Err(anyhow!(
|
||||||
|
"dosh forward requires at least one -L, -R, -D, or -A request"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
args.server = Some(host);
|
||||||
|
args.command = Vec::new();
|
||||||
|
args.forward_only = true;
|
||||||
|
Ok(args)
|
||||||
|
}
|
||||||
|
|
||||||
fn signal_background_ready() {
|
fn signal_background_ready() {
|
||||||
let Some(addr) = std::env::var_os("DOSH_BACKGROUND_READY_ADDR") else {
|
let Some(addr) = std::env::var_os("DOSH_BACKGROUND_READY_ADDR") else {
|
||||||
return;
|
return;
|
||||||
@@ -958,12 +1164,20 @@ fn parse_local_forward(raw: &str) -> Result<LocalForward> {
|
|||||||
let target_port = target_port
|
let target_port = target_port
|
||||||
.parse::<u16>()
|
.parse::<u16>()
|
||||||
.with_context(|| format!("invalid local forward target port in {raw:?}"))?;
|
.with_context(|| format!("invalid local forward target port in {raw:?}"))?;
|
||||||
if target_host.is_empty() {
|
if !valid_forward_host(target_host) {
|
||||||
return Err(anyhow!("invalid -L {raw:?}; target host cannot be empty"));
|
return Err(anyhow!("invalid -L {raw:?}; target host cannot be empty"));
|
||||||
}
|
}
|
||||||
if bind_host.is_empty() {
|
if !valid_forward_host(&bind_host) {
|
||||||
return Err(anyhow!("invalid -L {raw:?}; bind host cannot be empty"));
|
return Err(anyhow!("invalid -L {raw:?}; bind host cannot be empty"));
|
||||||
}
|
}
|
||||||
|
anyhow::ensure!(
|
||||||
|
listen_port != 0,
|
||||||
|
"invalid -L {raw:?}; listen port cannot be 0"
|
||||||
|
);
|
||||||
|
anyhow::ensure!(
|
||||||
|
target_port != 0,
|
||||||
|
"invalid -L {raw:?}; target port cannot be 0"
|
||||||
|
);
|
||||||
Ok(LocalForward {
|
Ok(LocalForward {
|
||||||
bind_host,
|
bind_host,
|
||||||
listen_port,
|
listen_port,
|
||||||
@@ -996,15 +1210,26 @@ fn parse_dynamic_forward(raw: &str) -> Result<DynamicForward> {
|
|||||||
let listen_port = listen
|
let listen_port = listen
|
||||||
.parse::<u16>()
|
.parse::<u16>()
|
||||||
.with_context(|| format!("invalid dynamic forward listen port in {raw:?}"))?;
|
.with_context(|| format!("invalid dynamic forward listen port in {raw:?}"))?;
|
||||||
if bind_host.is_empty() {
|
if !valid_forward_host(&bind_host) {
|
||||||
return Err(anyhow!("invalid -D {raw:?}; bind host cannot be empty"));
|
return Err(anyhow!("invalid -D {raw:?}; bind host cannot be empty"));
|
||||||
}
|
}
|
||||||
|
anyhow::ensure!(
|
||||||
|
listen_port != 0,
|
||||||
|
"invalid -D {raw:?}; listen port cannot be 0"
|
||||||
|
);
|
||||||
Ok(DynamicForward {
|
Ok(DynamicForward {
|
||||||
bind_host,
|
bind_host,
|
||||||
listen_port,
|
listen_port,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn valid_forward_host(host: &str) -> bool {
|
||||||
|
!host.is_empty()
|
||||||
|
&& !host
|
||||||
|
.bytes()
|
||||||
|
.any(|byte| byte == 0 || byte == b'\n' || byte == b'\r' || byte == b'\t')
|
||||||
|
}
|
||||||
|
|
||||||
fn forwarding_requests(
|
fn forwarding_requests(
|
||||||
local_forwards: &[LocalForward],
|
local_forwards: &[LocalForward],
|
||||||
remote_forwards: &[RemoteForward],
|
remote_forwards: &[RemoteForward],
|
||||||
@@ -1110,6 +1335,38 @@ fn latest_release_download_url(repo: &str, artifact: &str) -> Option<String> {
|
|||||||
Some(format!("{web}/releases/latest/download/{artifact}"))
|
Some(format!("{web}/releases/latest/download/{artifact}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn latest_release_tag_download_url(repo: &str, artifact: &str) -> Result<Option<String>> {
|
||||||
|
let web = repo
|
||||||
|
.strip_suffix(".git")
|
||||||
|
.unwrap_or(repo)
|
||||||
|
.trim_end_matches('/');
|
||||||
|
if !web.starts_with("http://") && !web.starts_with("https://") {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
let output = Command::new("curl")
|
||||||
|
.arg("-fsSL")
|
||||||
|
.arg("-o")
|
||||||
|
.arg("/dev/null")
|
||||||
|
.arg("-w")
|
||||||
|
.arg("%{url_effective}")
|
||||||
|
.arg(format!("{web}/releases/latest"))
|
||||||
|
.stdin(Stdio::null())
|
||||||
|
.output()
|
||||||
|
.with_context(|| format!("resolve latest release for {web}"))?;
|
||||||
|
if !output.status.success() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
let effective = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let prefix = format!("{web}/releases/tag/");
|
||||||
|
let Some(tag) = effective.strip_prefix(&prefix) else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
if tag.is_empty() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
Ok(Some(format!("{web}/releases/download/{tag}/{artifact}")))
|
||||||
|
}
|
||||||
|
|
||||||
fn url_reachable(url: &str) -> Result<bool> {
|
fn url_reachable(url: &str) -> Result<bool> {
|
||||||
let status = Command::new("curl")
|
let status = Command::new("curl")
|
||||||
.arg("-fsIL")
|
.arg("-fsIL")
|
||||||
@@ -1130,19 +1387,23 @@ fn run_update(config: &dosh::config::ClientConfig, check_only: bool) -> Result<(
|
|||||||
let raw_base = raw_base_from_repo(&repo);
|
let raw_base = raw_base_from_repo(&repo);
|
||||||
let installer = format!("{raw_base}/raw/branch/main/install.sh");
|
let installer = format!("{raw_base}/raw/branch/main/install.sh");
|
||||||
let artifact = release_artifact_name();
|
let artifact = release_artifact_name();
|
||||||
let binary_url = latest_release_download_url(&repo, artifact);
|
|
||||||
if check_only {
|
if check_only {
|
||||||
println!("dosh {}", env!("CARGO_PKG_VERSION"));
|
println!("dosh {}", env!("CARGO_PKG_VERSION"));
|
||||||
println!("repo: {repo}");
|
println!("repo: {repo}");
|
||||||
println!("installer: {installer}");
|
println!("installer: {installer}");
|
||||||
if let Some(binary_url) = &binary_url {
|
if let Some(latest_url) = latest_release_download_url(&repo, artifact) {
|
||||||
let status = if url_reachable(binary_url)? {
|
let mut status = "missing";
|
||||||
"available"
|
let mut display_url = latest_url.clone();
|
||||||
} else {
|
if url_reachable(&latest_url)? {
|
||||||
"missing"
|
status = "available";
|
||||||
};
|
} else if let Some(tag_url) = latest_release_tag_download_url(&repo, artifact)?
|
||||||
|
&& url_reachable(&tag_url)?
|
||||||
|
{
|
||||||
|
status = "available";
|
||||||
|
display_url = tag_url;
|
||||||
|
}
|
||||||
println!("prebuilt: {status} ({artifact})");
|
println!("prebuilt: {status} ({artifact})");
|
||||||
println!("prebuilt_url: {binary_url}");
|
println!("prebuilt_url: {display_url}");
|
||||||
} else {
|
} else {
|
||||||
println!("prebuilt: unavailable for non-HTTP repo");
|
println!("prebuilt: unavailable for non-HTTP repo");
|
||||||
}
|
}
|
||||||
@@ -1177,10 +1438,51 @@ fn run_update(config: &dosh::config::ClientConfig, check_only: bool) -> Result<(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
enum ImportAction {
|
||||||
|
Added,
|
||||||
|
Skipped,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
struct ImportedHost {
|
||||||
|
alias: String,
|
||||||
|
ssh: String,
|
||||||
|
dosh_host: String,
|
||||||
|
port: u16,
|
||||||
|
action: ImportAction,
|
||||||
|
}
|
||||||
|
|
||||||
fn run_import_ssh(config: &dosh::config::ClientConfig, aliases: &[String]) -> Result<()> {
|
fn run_import_ssh(config: &dosh::config::ClientConfig, aliases: &[String]) -> Result<()> {
|
||||||
if aliases.is_empty() {
|
if aliases.is_empty() {
|
||||||
return Err(anyhow!("usage: dosh import-ssh <ssh-host> [ssh-host...]"));
|
return Err(anyhow!("usage: dosh import-ssh <ssh-host> [ssh-host...]"));
|
||||||
}
|
}
|
||||||
|
let imported = import_ssh_aliases(config, aliases)?;
|
||||||
|
for entry in imported {
|
||||||
|
match entry.action {
|
||||||
|
ImportAction::Added => eprintln!(
|
||||||
|
"dosh import-ssh: added [{}] ssh={} dosh_host={}",
|
||||||
|
entry.alias, entry.ssh, entry.dosh_host
|
||||||
|
),
|
||||||
|
ImportAction::Skipped => {
|
||||||
|
eprintln!(
|
||||||
|
"dosh import-ssh: [{}] already exists, skipping",
|
||||||
|
entry.alias
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
eprintln!(
|
||||||
|
"dosh import-ssh: wrote {}",
|
||||||
|
expand_tilde("~/.config/dosh/hosts.toml").display()
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn import_ssh_aliases(
|
||||||
|
config: &dosh::config::ClientConfig,
|
||||||
|
aliases: &[String],
|
||||||
|
) -> Result<Vec<ImportedHost>> {
|
||||||
let path = expand_tilde("~/.config/dosh/hosts.toml");
|
let path = expand_tilde("~/.config/dosh/hosts.toml");
|
||||||
let mut raw = if path.exists() {
|
let mut raw = if path.exists() {
|
||||||
fs::read_to_string(&path).with_context(|| format!("read {}", path.display()))?
|
fs::read_to_string(&path).with_context(|| format!("read {}", path.display()))?
|
||||||
@@ -1190,6 +1492,7 @@ fn run_import_ssh(config: &dosh::config::ClientConfig, aliases: &[String]) -> Re
|
|||||||
if !raw.ends_with('\n') && !raw.is_empty() {
|
if !raw.ends_with('\n') && !raw.is_empty() {
|
||||||
raw.push('\n');
|
raw.push('\n');
|
||||||
}
|
}
|
||||||
|
let mut imported = Vec::new();
|
||||||
for alias in aliases {
|
for alias in aliases {
|
||||||
let ssh_config = ssh_config(alias, None)
|
let ssh_config = ssh_config(alias, None)
|
||||||
.with_context(|| format!("read SSH config for {alias} with ssh -G"))?;
|
.with_context(|| format!("read SSH config for {alias} with ssh -G"))?;
|
||||||
@@ -1198,7 +1501,13 @@ fn run_import_ssh(config: &dosh::config::ClientConfig, aliases: &[String]) -> Re
|
|||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_else(|| ssh_destination_host(alias));
|
.unwrap_or_else(|| ssh_destination_host(alias));
|
||||||
if raw_contains_host_table(&raw, alias) {
|
if raw_contains_host_table(&raw, alias) {
|
||||||
eprintln!("dosh import-ssh: [{alias}] already exists, skipping");
|
imported.push(ImportedHost {
|
||||||
|
alias: alias.clone(),
|
||||||
|
ssh: alias.clone(),
|
||||||
|
dosh_host: udp_host,
|
||||||
|
port: config.dosh_port,
|
||||||
|
action: ImportAction::Skipped,
|
||||||
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
raw.push('\n');
|
raw.push('\n');
|
||||||
@@ -1215,13 +1524,19 @@ fn run_import_ssh(config: &dosh::config::ClientConfig, aliases: &[String]) -> Re
|
|||||||
raw.push_str(&format!("ssh_port = {port}\n"));
|
raw.push_str(&format!("ssh_port = {port}\n"));
|
||||||
}
|
}
|
||||||
raw.push_str("predict = true\n");
|
raw.push_str("predict = true\n");
|
||||||
|
imported.push(ImportedHost {
|
||||||
|
alias: alias.clone(),
|
||||||
|
ssh: alias.clone(),
|
||||||
|
dosh_host: udp_host,
|
||||||
|
port: config.dosh_port,
|
||||||
|
action: ImportAction::Added,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if let Some(parent) = path.parent() {
|
if let Some(parent) = path.parent() {
|
||||||
fs::create_dir_all(parent)?;
|
fs::create_dir_all(parent)?;
|
||||||
}
|
}
|
||||||
fs::write(&path, raw).with_context(|| format!("write {}", path.display()))?;
|
fs::write(&path, raw).with_context(|| format!("write {}", path.display()))?;
|
||||||
eprintln!("dosh import-ssh: wrote {}", path.display());
|
Ok(imported)
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn raw_contains_host_table(raw: &str, alias: &str) -> bool {
|
fn raw_contains_host_table(raw: &str, alias: &str) -> bool {
|
||||||
@@ -4166,10 +4481,12 @@ impl DisconnectStatus {
|
|||||||
/// Pure: render the status text for a given elapsed silence. Kept separate
|
/// Pure: render the status text for a given elapsed silence. Kept separate
|
||||||
/// from any I/O so it can be asserted in tests.
|
/// from any I/O so it can be asserted in tests.
|
||||||
fn render_text(elapsed: Duration) -> String {
|
fn render_text(elapsed: Duration) -> String {
|
||||||
format!(
|
let secs = elapsed.as_secs();
|
||||||
"[dosh] last contact {}s ago — reconnecting…",
|
if secs >= 30 {
|
||||||
elapsed.as_secs()
|
format!("[dosh] still reconnecting — last contact {secs}s ago")
|
||||||
)
|
} else {
|
||||||
|
format!("[dosh] reconnecting — last contact {secs}s ago")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pure decision for a status timer tick: given how long the link has been
|
/// Pure decision for a status timer tick: given how long the link has been
|
||||||
@@ -4348,12 +4665,13 @@ const TERMINAL_CLEANUP: &[u8] = concat!(
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::{
|
use super::{
|
||||||
DisconnectStatus, DynamicForward, FrameBuffer, LocalForward, PredictMode, Predictor,
|
DisconnectStatus, DynamicForward, FrameBuffer, LocalForward, PredictMode, Predictor,
|
||||||
RemoteForward, SshConfig, StatusAction, auth_allows, latest_release_download_url,
|
RemoteForward, SshConfig, StatusAction, auth_allows, ensure_tui_safe_status_overlay,
|
||||||
load_first_native_identity_with_prompt, parse_dynamic_forward, parse_local_forward,
|
latest_release_download_url, load_first_native_identity_with_prompt, parse_dynamic_forward,
|
||||||
parse_remote_forward, parse_ssh_config, raw_contains_host_table, recv_response_until,
|
parse_local_forward, parse_remote_forward, parse_ssh_config, raw_contains_host_table,
|
||||||
render_status_clear, render_status_overlay, requested_env, resolved_startup_command,
|
recv_response_until, render_status_clear, render_status_overlay, requested_env,
|
||||||
ssh_destination_host, ssh_username, ssh_with_user, startup_command,
|
resolved_startup_command, rewrite_forward_command, ssh_destination_host, ssh_username,
|
||||||
toml_bare_key_or_quoted, update_check_requested,
|
ssh_with_user, startup_command, toml_bare_key_or_quoted, update_check_requested,
|
||||||
|
valid_forward_host,
|
||||||
};
|
};
|
||||||
use dosh::config::{ClientConfig, CommandExtension, HostConfig};
|
use dosh::config::{ClientConfig, CommandExtension, HostConfig};
|
||||||
use dosh::native::EnvVar;
|
use dosh::native::EnvVar;
|
||||||
@@ -4361,12 +4679,43 @@ mod tests {
|
|||||||
use ed25519_dalek::{SigningKey, VerifyingKey};
|
use ed25519_dalek::{SigningKey, VerifyingKey};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
fn test_args(server: &str, command: &[&str]) -> super::Args {
|
||||||
|
super::Args {
|
||||||
|
server: Some(server.to_string()),
|
||||||
|
command: command.iter().map(|value| value.to_string()).collect(),
|
||||||
|
session: None,
|
||||||
|
new: false,
|
||||||
|
view_only: false,
|
||||||
|
local_auth: false,
|
||||||
|
auth: None,
|
||||||
|
no_cache: false,
|
||||||
|
remove: false,
|
||||||
|
replace: false,
|
||||||
|
attach_only: false,
|
||||||
|
local_forward: Vec::new(),
|
||||||
|
remote_forward: Vec::new(),
|
||||||
|
dynamic_forward: Vec::new(),
|
||||||
|
forward_agent: false,
|
||||||
|
forward_only: false,
|
||||||
|
background: false,
|
||||||
|
predict: false,
|
||||||
|
ssh_port: None,
|
||||||
|
ssh_auth_command: None,
|
||||||
|
ssh_key: None,
|
||||||
|
ssh_known_hosts: None,
|
||||||
|
ssh_control_path: None,
|
||||||
|
dosh_port: None,
|
||||||
|
dosh_host: None,
|
||||||
|
verbose: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parses_ssh_config_hostname() {
|
fn parses_ssh_config_hostname() {
|
||||||
let raw = "user palav\nhostname palav.dev\nport 22\n";
|
let raw = "user alice\nhostname server.example.com\nport 22\n";
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_ssh_config(raw).hostname,
|
parse_ssh_config(raw).hostname,
|
||||||
Some("palav.dev".to_string())
|
Some("server.example.com".to_string())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4389,9 +4738,9 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn parses_ssh_config_user_and_identity_files() {
|
fn parses_ssh_config_user_and_identity_files() {
|
||||||
let parsed = parse_ssh_config(
|
let parsed = parse_ssh_config(
|
||||||
"user palav\nidentityfile ~/.ssh/id_ed25519\nidentityfile ~/.ssh/work\n",
|
"user alice\nidentityfile ~/.ssh/id_ed25519\nidentityfile ~/.ssh/work\n",
|
||||||
);
|
);
|
||||||
assert_eq!(parsed.user, Some("palav".to_string()));
|
assert_eq!(parsed.user, Some("alice".to_string()));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parsed.identity_files,
|
parsed.identity_files,
|
||||||
vec!["~/.ssh/id_ed25519".to_string(), "~/.ssh/work".to_string()]
|
vec!["~/.ssh/id_ed25519".to_string(), "~/.ssh/work".to_string()]
|
||||||
@@ -4478,37 +4827,37 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn detects_existing_imported_host_tables() {
|
fn detects_existing_imported_host_tables() {
|
||||||
assert!(raw_contains_host_table(
|
assert!(raw_contains_host_table(
|
||||||
"[palav]\nssh = \"palav\"\n",
|
"[homelab]\nssh = \"homelab\"\n",
|
||||||
"palav"
|
"homelab"
|
||||||
));
|
));
|
||||||
assert!(raw_contains_host_table("[\"home box\"]\n", "home box"));
|
assert!(raw_contains_host_table("[\"home box\"]\n", "home box"));
|
||||||
assert_eq!(toml_bare_key_or_quoted("palav"), "palav");
|
assert_eq!(toml_bare_key_or_quoted("homelab"), "homelab");
|
||||||
assert_eq!(toml_bare_key_or_quoted("home box"), "\"home box\"");
|
assert_eq!(toml_bare_key_or_quoted("home box"), "\"home box\"");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn strips_user_from_fallback_udp_host() {
|
fn strips_user_from_fallback_udp_host() {
|
||||||
assert_eq!(ssh_destination_host("palav@example.com"), "example.com");
|
assert_eq!(ssh_destination_host("alice@example.com"), "example.com");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parses_user_from_ssh_destination() {
|
fn parses_user_from_ssh_destination() {
|
||||||
assert_eq!(ssh_username("palav@example.com"), Some("palav".to_string()));
|
assert_eq!(ssh_username("alice@example.com"), Some("alice".to_string()));
|
||||||
assert_eq!(ssh_username("example.com"), None);
|
assert_eq!(ssh_username("example.com"), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn applies_host_config_user_to_plain_ssh_destination() {
|
fn applies_host_config_user_to_plain_ssh_destination() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ssh_with_user("example.com", Some("palav")),
|
ssh_with_user("example.com", Some("alice")),
|
||||||
"palav@example.com"
|
"alice@example.com"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ssh_with_user("root@example.com", Some("palav")),
|
ssh_with_user("root@example.com", Some("alice")),
|
||||||
"root@example.com"
|
"root@example.com"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ssh_with_user("ssh://example.com", Some("palav")),
|
ssh_with_user("ssh://example.com", Some("alice")),
|
||||||
"ssh://example.com"
|
"ssh://example.com"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -5069,6 +5418,40 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn forward_command_rewrites_to_forward_only_connection() {
|
||||||
|
let args = test_args(
|
||||||
|
"forward",
|
||||||
|
&["homelab", "-L", "8080:127.0.0.1:80", "-D", "1080", "-f"],
|
||||||
|
);
|
||||||
|
let rewritten = rewrite_forward_command(args).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(rewritten.server.as_deref(), Some("homelab"));
|
||||||
|
assert!(rewritten.forward_only);
|
||||||
|
assert!(rewritten.background);
|
||||||
|
assert_eq!(rewritten.command, Vec::<String>::new());
|
||||||
|
assert_eq!(rewritten.local_forward, vec!["8080:127.0.0.1:80"]);
|
||||||
|
assert_eq!(rewritten.dynamic_forward, vec!["1080"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn forward_command_requires_a_forward_request() {
|
||||||
|
let err = rewrite_forward_command(test_args("forward", &["homelab"]))
|
||||||
|
.unwrap_err()
|
||||||
|
.to_string();
|
||||||
|
assert!(err.contains("requires at least one"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn forward_parser_rejects_zero_ports_and_control_chars() {
|
||||||
|
assert!(parse_local_forward("0:127.0.0.1:80").is_err());
|
||||||
|
assert!(parse_local_forward("8080:127.0.0.1:0").is_err());
|
||||||
|
assert!(parse_dynamic_forward("127.0.0.1:0").is_err());
|
||||||
|
assert!(!valid_forward_host("bad\nhost"));
|
||||||
|
assert!(!valid_forward_host("bad\thost"));
|
||||||
|
assert!(valid_forward_host("server.example.com"));
|
||||||
|
}
|
||||||
|
|
||||||
// --- Item 1: disconnect status line state machine ---
|
// --- Item 1: disconnect status line state machine ---
|
||||||
|
|
||||||
/// The status line stays hidden while the link is fresh and only appears once
|
/// The status line stays hidden while the link is fresh and only appears once
|
||||||
@@ -5085,7 +5468,7 @@ mod tests {
|
|||||||
let action = status.on_tick(Duration::from_secs(3));
|
let action = status.on_tick(Duration::from_secs(3));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
action,
|
action,
|
||||||
StatusAction::Show("[dosh] last contact 3s ago — reconnecting…".to_string())
|
StatusAction::Show("[dosh] reconnecting — last contact 3s ago".to_string())
|
||||||
);
|
);
|
||||||
status.applied(&action);
|
status.applied(&action);
|
||||||
assert!(status.shown);
|
assert!(status.shown);
|
||||||
@@ -5104,7 +5487,7 @@ mod tests {
|
|||||||
// Next second -> repaint with updated text.
|
// Next second -> repaint with updated text.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
status.on_tick(Duration::from_secs(4)),
|
status.on_tick(Duration::from_secs(4)),
|
||||||
StatusAction::Show("[dosh] last contact 4s ago — reconnecting…".to_string())
|
StatusAction::Show("[dosh] reconnecting — last contact 4s ago".to_string())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5149,7 +5532,8 @@ mod tests {
|
|||||||
/// the application's cursor and full-screen TUIs are never disturbed.
|
/// the application's cursor and full-screen TUIs are never disturbed.
|
||||||
#[test]
|
#[test]
|
||||||
fn disconnect_status_overlay_is_save_restore_bottom_row() {
|
fn disconnect_status_overlay_is_save_restore_bottom_row() {
|
||||||
let bytes = render_status_overlay("[dosh] last contact 3s ago — reconnecting…", 24);
|
let bytes = render_status_overlay("[dosh] reconnecting — last contact 3s ago", 24);
|
||||||
|
ensure_tui_safe_status_overlay(&bytes).unwrap();
|
||||||
assert!(bytes.starts_with(b"\x1b7"), "must save cursor first");
|
assert!(bytes.starts_with(b"\x1b7"), "must save cursor first");
|
||||||
assert!(bytes.ends_with(b"\x1b8"), "must restore cursor last");
|
assert!(bytes.ends_with(b"\x1b8"), "must restore cursor last");
|
||||||
// Positions to the last row (row 24), column 1.
|
// Positions to the last row (row 24), column 1.
|
||||||
@@ -5166,6 +5550,18 @@ mod tests {
|
|||||||
assert!(clear.windows(4).any(|w| w == b"\x1b[2K"));
|
assert!(clear.windows(4).any(|w| w == b"\x1b[2K"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn disconnect_status_text_escalates_after_long_silence() {
|
||||||
|
assert_eq!(
|
||||||
|
DisconnectStatus::render_text(Duration::from_secs(29)),
|
||||||
|
"[dosh] reconnecting — last contact 29s ago"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
DisconnectStatus::render_text(Duration::from_secs(30)),
|
||||||
|
"[dosh] still reconnecting — last contact 30s ago"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// --- Item 2: predictor policy re-evaluated on a timer tick ---
|
// --- Item 2: predictor policy re-evaluated on a timer tick ---
|
||||||
|
|
||||||
/// A latency spike must surface predictions on a *timer tick*, not only on
|
/// A latency spike must surface predictions on a *timer tick*, not only on
|
||||||
|
|||||||
+10
-10
@@ -1279,16 +1279,16 @@ mod tests {
|
|||||||
let second = host_public_key(&SigningKey::from_bytes(&[2u8; 32]));
|
let second = host_public_key(&SigningKey::from_bytes(&[2u8; 32]));
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
trust_host(&path, "palav", &first, "ssh", false).unwrap(),
|
trust_host(&path, "homelab", &first, "ssh", false).unwrap(),
|
||||||
TrustResult::Trusted
|
TrustResult::Trusted
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
trust_host(&path, "palav", &first, "ssh", false).unwrap(),
|
trust_host(&path, "homelab", &first, "ssh", false).unwrap(),
|
||||||
TrustResult::AlreadyTrusted
|
TrustResult::AlreadyTrusted
|
||||||
);
|
);
|
||||||
assert!(trust_host(&path, "palav", &second, "ssh", false).is_err());
|
assert!(trust_host(&path, "homelab", &second, "ssh", false).is_err());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
trust_host(&path, "palav", &second, "ssh", true).unwrap(),
|
trust_host(&path, "homelab", &second, "ssh", true).unwrap(),
|
||||||
TrustResult::Trusted
|
TrustResult::Trusted
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1301,16 +1301,16 @@ mod tests {
|
|||||||
let second = host_public_key(&SigningKey::from_bytes(&[2u8; 32]));
|
let second = host_public_key(&SigningKey::from_bytes(&[2u8; 32]));
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
verify_known_host(&path, "palav", &first).unwrap(),
|
verify_known_host(&path, "homelab", &first).unwrap(),
|
||||||
KnownHostStatus::Unknown
|
KnownHostStatus::Unknown
|
||||||
);
|
);
|
||||||
trust_host(&path, "palav", &first, "ssh", false).unwrap();
|
trust_host(&path, "homelab", &first, "ssh", false).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
verify_known_host(&path, "palav", &first).unwrap(),
|
verify_known_host(&path, "homelab", &first).unwrap(),
|
||||||
KnownHostStatus::Trusted
|
KnownHostStatus::Trusted
|
||||||
);
|
);
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
verify_known_host(&path, "palav", &second).unwrap(),
|
verify_known_host(&path, "homelab", &second).unwrap(),
|
||||||
KnownHostStatus::Mismatch { .. }
|
KnownHostStatus::Mismatch { .. }
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -1625,8 +1625,8 @@ mod tests {
|
|||||||
protocol_version: NATIVE_PROTOCOL_VERSION,
|
protocol_version: NATIVE_PROTOCOL_VERSION,
|
||||||
client_random: [1u8; 32],
|
client_random: [1u8; 32],
|
||||||
client_ephemeral_public: [2u8; 32],
|
client_ephemeral_public: [2u8; 32],
|
||||||
requested_host: "palav".to_string(),
|
requested_host: "homelab".to_string(),
|
||||||
requested_user: "palav".to_string(),
|
requested_user: "alice".to_string(),
|
||||||
requested_session: "term".to_string(),
|
requested_session: "term".to_string(),
|
||||||
requested_mode: "read-write".to_string(),
|
requested_mode: "read-write".to_string(),
|
||||||
terminal_size: (80, 24),
|
terminal_size: (80, 24),
|
||||||
|
|||||||
+2
-2
@@ -291,8 +291,8 @@ mod tests {
|
|||||||
protocol_version: crate::native::NATIVE_PROTOCOL_VERSION,
|
protocol_version: crate::native::NATIVE_PROTOCOL_VERSION,
|
||||||
client_random: [1u8; 32],
|
client_random: [1u8; 32],
|
||||||
client_ephemeral_public: [2u8; 32],
|
client_ephemeral_public: [2u8; 32],
|
||||||
requested_host: "palav".to_string(),
|
requested_host: "homelab".to_string(),
|
||||||
requested_user: "palav".to_string(),
|
requested_user: "alice".to_string(),
|
||||||
requested_session: "term".to_string(),
|
requested_session: "term".to_string(),
|
||||||
requested_mode: "read-write".to_string(),
|
requested_mode: "read-write".to_string(),
|
||||||
terminal_size: (80, 24),
|
terminal_size: (80, 24),
|
||||||
|
|||||||
Reference in New Issue
Block a user