mirror of
https://github.com/veeso/termscp.git
synced 2026-09-30 07:51:20 -07:00
Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99b76f2fd9 | ||
|
|
a57f020e4a | ||
|
|
da23372d4b | ||
|
|
5304bc3224 | ||
|
|
6de1cdb39c | ||
|
|
57a710eff5 | ||
|
|
6e6d077532 | ||
|
|
63f7f0d084 | ||
|
|
9d2c3746d0 | ||
|
|
0b787fee34 | ||
|
|
48d3701678 | ||
|
|
622a64ac58 | ||
|
|
751f68f6d4 | ||
|
|
e966a83220 | ||
|
|
d99c76b43c | ||
|
|
7434c51063 | ||
|
|
9fbd617f88 |
@@ -0,0 +1,154 @@
|
|||||||
|
name: Linux musl probe
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- "test/440-musl-release"
|
||||||
|
paths:
|
||||||
|
- ".github/workflows/musl.yml"
|
||||||
|
- "Cargo.lock"
|
||||||
|
- "Cargo.toml"
|
||||||
|
- "src/**"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: build-${{ matrix.target }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- target: x86_64-unknown-linux-musl
|
||||||
|
runner: ubuntu-24.04
|
||||||
|
- target: aarch64-unknown-linux-musl
|
||||||
|
runner: ubuntu-24.04-arm
|
||||||
|
runs-on: ${{ matrix.runner }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Build and audit static binary
|
||||||
|
env:
|
||||||
|
TARGET: ${{ matrix.target }}
|
||||||
|
run: |
|
||||||
|
docker run --rm \
|
||||||
|
--env TARGET \
|
||||||
|
--volume "$GITHUB_WORKSPACE:/work" \
|
||||||
|
--workdir /work \
|
||||||
|
rust:1.98-alpine3.22 \
|
||||||
|
sh -euxc '
|
||||||
|
apk add --no-cache \
|
||||||
|
bison \
|
||||||
|
binutils \
|
||||||
|
build-base \
|
||||||
|
file \
|
||||||
|
flex \
|
||||||
|
git \
|
||||||
|
gnutls-dev \
|
||||||
|
libgit2-dev \
|
||||||
|
libgit2-static \
|
||||||
|
libunistring-dev \
|
||||||
|
libunistring-static \
|
||||||
|
linux-headers \
|
||||||
|
openssl-dev \
|
||||||
|
openssl-libs-static \
|
||||||
|
perl \
|
||||||
|
perl-parse-yapp \
|
||||||
|
pkgconf \
|
||||||
|
python3 \
|
||||||
|
wget \
|
||||||
|
xz \
|
||||||
|
zlib-dev \
|
||||||
|
zlib-static
|
||||||
|
rustup target add "$TARGET"
|
||||||
|
cargo fetch --locked
|
||||||
|
NATIVE_CFLAGS="-O2 -fPIC"
|
||||||
|
if [ "$TARGET" = "aarch64-unknown-linux-musl" ]; then
|
||||||
|
NATIVE_CFLAGS="$NATIVE_CFLAGS -mno-outline-atomics"
|
||||||
|
fi
|
||||||
|
export CFLAGS="$NATIVE_CFLAGS"
|
||||||
|
|
||||||
|
mkdir -p /tmp/native
|
||||||
|
wget -q https://ftp.gnu.org/gnu/nettle/nettle-3.10.1.tar.gz \
|
||||||
|
-O /tmp/native/nettle.tar.gz
|
||||||
|
tar -xzf /tmp/native/nettle.tar.gz -C /tmp/native
|
||||||
|
cd /tmp/native/nettle-3.10.1
|
||||||
|
./configure \
|
||||||
|
--prefix=/tmp/native/nettle \
|
||||||
|
--disable-shared \
|
||||||
|
--enable-static \
|
||||||
|
--disable-documentation \
|
||||||
|
--enable-mini-gmp
|
||||||
|
make -j$(getconf _NPROCESSORS_ONLN)
|
||||||
|
make install
|
||||||
|
|
||||||
|
wget -q https://www.gnupg.org/ftp/gcrypt/gnutls/v3.8/gnutls-3.8.13.tar.xz \
|
||||||
|
-O /tmp/native/gnutls.tar.xz
|
||||||
|
tar -xf /tmp/native/gnutls.tar.xz -C /tmp/native
|
||||||
|
cd /tmp/native/gnutls-3.8.13
|
||||||
|
PKG_CONFIG_PATH=/tmp/native/nettle/lib/pkgconfig \
|
||||||
|
./configure \
|
||||||
|
--prefix=/tmp/native/gnutls \
|
||||||
|
--disable-shared \
|
||||||
|
--enable-static \
|
||||||
|
--disable-doc \
|
||||||
|
--disable-tests \
|
||||||
|
--disable-nls \
|
||||||
|
--disable-hardware-acceleration \
|
||||||
|
--with-nettle-mini \
|
||||||
|
--with-included-libtasn1 \
|
||||||
|
--with-included-unistring \
|
||||||
|
--without-idn \
|
||||||
|
--without-p11-kit \
|
||||||
|
--without-brotli \
|
||||||
|
--without-zstd \
|
||||||
|
--without-zlib
|
||||||
|
make -j$(getconf _NPROCESSORS_ONLN)
|
||||||
|
make install
|
||||||
|
|
||||||
|
mkdir -p /tmp/native/pkgconfig
|
||||||
|
sed \
|
||||||
|
-e "s#^Libs:.*#Libs: -L/tmp/native/gnutls/lib -lgnutls -latomic -L/tmp/native/nettle/lib -lhogweed -lnettle#" \
|
||||||
|
-e "/^Requires.private:/d" \
|
||||||
|
-e "s#^Cflags:.*#Cflags: -I/tmp/native/gnutls/include -I/tmp/native/nettle/include#" \
|
||||||
|
/tmp/native/gnutls/lib/pkgconfig/gnutls.pc \
|
||||||
|
> /tmp/native/pkgconfig/gnutls.pc
|
||||||
|
|
||||||
|
cd /work
|
||||||
|
PAVAO_SRC=$(find "${CARGO_HOME:-/usr/local/cargo}/registry/src" \
|
||||||
|
-type d -name "pavao-src-4.24.6" -print -quit)
|
||||||
|
test -n "$PAVAO_SRC"
|
||||||
|
cp -R "$PAVAO_SRC" /tmp/pavao-src
|
||||||
|
perl -0pi -e "s#( \\\"lib/replace/replace\\.c\\\",\\n)#\$1 \\\"lib/replace/closefrom.c\\\",\\n \\\"lib/replace/strptime.c\\\",\\n#" \
|
||||||
|
/tmp/pavao-src/src/lib.rs
|
||||||
|
cat >> Cargo.toml <<EOF
|
||||||
|
|
||||||
|
[patch.crates-io]
|
||||||
|
pavao-src = { path = "/tmp/pavao-src" }
|
||||||
|
EOF
|
||||||
|
cargo update -p pavao-src@4.24.6
|
||||||
|
export PKG_CONFIG_ALL_STATIC=1
|
||||||
|
export PKG_CONFIG_PATH=/tmp/native/pkgconfig:/tmp/native/nettle/lib/pkgconfig:/usr/lib/pkgconfig
|
||||||
|
export RUSTFLAGS="-C target-feature=+crt-static -C link-arg=-static"
|
||||||
|
cargo build --locked --release --target "$TARGET" \
|
||||||
|
--features smb-vendored
|
||||||
|
file "target/$TARGET/release/termscp"
|
||||||
|
readelf -l "target/$TARGET/release/termscp" | \
|
||||||
|
tee /tmp/program-headers.txt
|
||||||
|
readelf -d "target/$TARGET/release/termscp" | \
|
||||||
|
tee /tmp/dynamic-section.txt
|
||||||
|
! grep -q INTERP /tmp/program-headers.txt
|
||||||
|
! grep -q NEEDED /tmp/dynamic-section.txt
|
||||||
|
'
|
||||||
|
|
||||||
|
- name: Upload verified binary
|
||||||
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
|
with:
|
||||||
|
name: ${{ matrix.target }}
|
||||||
|
path: target/${{ matrix.target }}/release/termscp
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 7
|
||||||
Generated
+179
-617
File diff suppressed because it is too large
Load Diff
+3
-3
@@ -62,8 +62,8 @@ remotefs = "0.3"
|
|||||||
remotefs-aws-s3 = "0.4"
|
remotefs-aws-s3 = "0.4"
|
||||||
remotefs-gcs = "0.1"
|
remotefs-gcs = "0.1"
|
||||||
remotefs-kube = "0.4"
|
remotefs-kube = "0.4"
|
||||||
remotefs-smb = { version = "0.3", optional = true }
|
remotefs-smb = { version = "0.5", default-features = false, optional = true, features = ["find", "pavao"] }
|
||||||
remotefs-ssh = { version = "0.8", default-features = false, features = ["russh"] }
|
remotefs-ssh = { version = "0.9", default-features = false, features = ["russh"] }
|
||||||
remotefs-webdav = "0.2"
|
remotefs-webdav = "0.2"
|
||||||
rpassword = "7"
|
rpassword = "7"
|
||||||
self_update = { version = "0.42", default-features = false, features = ["archive-tar", "archive-zip", "compression-flate2", "compression-zip-deflate", "rustls"] }
|
self_update = { version = "0.42", default-features = false, features = ["archive-tar", "archive-zip", "compression-flate2", "compression-zip-deflate", "rustls"] }
|
||||||
@@ -72,7 +72,7 @@ serde = { version = "1", features = ["derive"] }
|
|||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
shellexpand = "3"
|
shellexpand = "3"
|
||||||
simplelog = "0.12"
|
simplelog = "0.12"
|
||||||
ssh2-config = "0.7"
|
ssh2-config = "0.8"
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
tokio = { version = "1", features = ["rt"] }
|
tokio = { version = "1", features = ["rt"] }
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import "./just/build.just"
|
|||||||
import "./just/changelog.just"
|
import "./just/changelog.just"
|
||||||
import "./just/code_check.just"
|
import "./just/code_check.just"
|
||||||
import "./just/publish.just"
|
import "./just/publish.just"
|
||||||
|
import "./just/run.just"
|
||||||
import "./just/site.just"
|
import "./just/site.just"
|
||||||
import "./just/test.just"
|
import "./just/test.just"
|
||||||
|
|
||||||
|
|||||||
@@ -33,9 +33,6 @@ ignore = [
|
|||||||
{ id = "RUSTSEC-2026-0195", reason = "owner: termscp maintainers; review by 2026-12-31; upgrade remotefs-webdav/self_update to quick-xml >=0.41" },
|
{ id = "RUSTSEC-2026-0195", reason = "owner: termscp maintainers; review by 2026-12-31; upgrade remotefs-webdav/self_update to quick-xml >=0.41" },
|
||||||
# The SSH stack requires RSA, for which RustSec reports no safe upgrade.
|
# The SSH stack requires RSA, for which RustSec reports no safe upgrade.
|
||||||
{ id = "RUSTSEC-2023-0071", reason = "owner: termscp maintainers; review by 2026-12-31; replace RSA dependency when upstream fix exists" },
|
{ id = "RUSTSEC-2023-0071", reason = "owner: termscp maintainers; review by 2026-12-31; replace RSA dependency when upstream fix exists" },
|
||||||
# russh-keys retains an older russh-cryptovec line and cannot select the
|
|
||||||
# fixed 0.60.3 release without an upstream dependency update.
|
|
||||||
{ id = "RUSTSEC-2026-0153", reason = "owner: termscp maintainers; review by 2026-12-31; upgrade remotefs-ssh/russh-keys to russh-cryptovec >=0.60.3" },
|
|
||||||
# rustls-pemfile is retained by the WebDAV and Kubernetes clients. Both
|
# rustls-pemfile is retained by the WebDAV and Kubernetes clients. Both
|
||||||
# versions are unmaintained but have no reported vulnerability.
|
# versions are unmaintained but have no reported vulnerability.
|
||||||
{ id = "RUSTSEC-2025-0134", reason = "owner: termscp maintainers; review by 2026-12-31; upgrade WebDAV/Kubernetes clients away from rustls-pemfile" },
|
{ id = "RUSTSEC-2025-0134", reason = "owner: termscp maintainers; review by 2026-12-31; upgrade WebDAV/Kubernetes clients away from rustls-pemfile" },
|
||||||
|
|||||||
@@ -48,3 +48,15 @@ The following parameters can be configured:
|
|||||||
attributes supported by termscp are listed at
|
attributes supported by termscp are listed at
|
||||||
[the ssh2-config exposed attributes](https://github.com/veeso/ssh2-config#exposed-attributes).
|
[the ssh2-config exposed attributes](https://github.com/veeso/ssh2-config#exposed-attributes).
|
||||||
See also [SSH key storage](ssh-keys.md).
|
See also [SSH key storage](ssh-keys.md).
|
||||||
|
|
||||||
|
## SSH configuration behavior
|
||||||
|
|
||||||
|
In either SFTP or SCP authentication pane, after you change and leave the
|
||||||
|
`Host` field, termscp keeps the entered alias visible and fills the visible
|
||||||
|
`Port` and `Username` fields from the matching SSH configuration entry. If the
|
||||||
|
new Host does not match an entry, it resets Port to `22` and Username to empty.
|
||||||
|
`HostName` never replaces the alias in the form.
|
||||||
|
|
||||||
|
Bookmarks retain their saved Port and User values; the SSH configuration does
|
||||||
|
not silently override them. The configured file still provides `HostName` and
|
||||||
|
other supported SSH options when termscp connects.
|
||||||
|
|||||||
@@ -18,6 +18,18 @@ When termscp starts without an address, it shows the authentication form. Fill
|
|||||||
in the protocol, address, port, username, and password, then connect. termscp
|
in the protocol, address, port, username, and password, then connect. termscp
|
||||||
will open the dual-pane explorer once the connection succeeds.
|
will open the dual-pane explorer once the connection succeeds.
|
||||||
|
|
||||||
|
## SSH configuration precedence
|
||||||
|
|
||||||
|
For SFTP and SCP connections, termscp resolves CLI Username and Port values in
|
||||||
|
this order: explicit `Username`/`Port`, SSH configuration `User`/`Port`, then
|
||||||
|
the current OS user and Port `22`. Username and Port are resolved independently,
|
||||||
|
so an explicit value for one does not prevent SSH configuration from supplying
|
||||||
|
the other.
|
||||||
|
|
||||||
|
For example, `termscp myhost` uses the configured Port and User for `myhost`.
|
||||||
|
`termscp alice@myhost:22` uses `alice` and `22`, regardless of the SSH
|
||||||
|
configuration.
|
||||||
|
|
||||||
## Address argument syntax
|
## Address argument syntax
|
||||||
|
|
||||||
The generic address argument has the following syntax:
|
The generic address argument has the following syntax:
|
||||||
@@ -30,8 +42,10 @@ This syntax is convenient, and you will probably use it instead of the
|
|||||||
interactive form. Here are some examples.
|
interactive form. Here are some examples.
|
||||||
|
|
||||||
Connect using the default protocol (defined in your configuration) to
|
Connect using the default protocol (defined in your configuration) to
|
||||||
`192.168.1.31`. If the port is not provided, the default port for the selected
|
`192.168.1.31`. For SFTP and SCP, an omitted Port or Username is taken from a
|
||||||
protocol is used. The username is the current user's name.
|
matching SSH configuration entry. If no value is configured, it falls back to
|
||||||
|
the protocol default port or the current OS user. Other protocols use their
|
||||||
|
default port and the current OS user.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
termscp 192.168.1.31
|
termscp 192.168.1.31
|
||||||
|
|||||||
@@ -169,8 +169,28 @@ Authentication-form fields:
|
|||||||
- Password
|
- Password
|
||||||
- Port (other systems only; default `445`)
|
- Port (other systems only; default `445`)
|
||||||
- Workgroup (other systems only)
|
- Workgroup (other systems only)
|
||||||
|
- SMB version (other systems only; default `Auto`)
|
||||||
|
|
||||||
On Windows the port and workgroup fields are not used.
|
On Windows the port, workgroup and SMB version fields are not used: the
|
||||||
|
operating system manages SMB protocol negotiation.
|
||||||
|
|
||||||
|
The SMB version field bounds the dialects offered during negotiation:
|
||||||
|
|
||||||
|
| Selection | Dialects negotiated |
|
||||||
|
| --------- | ----------------------- |
|
||||||
|
| Auto | SMB 2.0.2 through 3.1.1 |
|
||||||
|
| SMB1 | NT1 (CIFS) only |
|
||||||
|
| SMB2 | SMB 2.0.2 through 2.1 |
|
||||||
|
| SMB3 | SMB 3.0 through 3.1.1 |
|
||||||
|
|
||||||
|
`Auto` never negotiates SMB1. SMB1 is deprecated and insecure: select it only
|
||||||
|
for isolated legacy devices that support nothing newer. A warning is shown in
|
||||||
|
the form while SMB1 is selected.
|
||||||
|
|
||||||
|
Bookmarks store the selection under the `dialect` key of the SMB table
|
||||||
|
(`auto`, `smb1`, `smb2` or `smb3`). Bookmarks saved before this option existed
|
||||||
|
have no `dialect` key and behave as `Auto`. The address syntax below does not
|
||||||
|
carry a version; connections started from the command line use `Auto`.
|
||||||
|
|
||||||
Windows address syntax:
|
Windows address syntax:
|
||||||
|
|
||||||
|
|||||||
@@ -25,3 +25,9 @@ termscp 要求以下路径可访问:
|
|||||||
- **启用通知**:如果设置为 `Yes`,则会显示桌面通知。参见 [通知](notifications.md)。
|
- **启用通知**:如果设置为 `Yes`,则会显示桌面通知。参见 [通知](notifications.md)。
|
||||||
- **通知:最小传输大小**:如果传输大小大于或等于指定值,则显示传输通知。可接受的格式为 `{UNSIGNED} B/KB/MB/GB/TB/PB`。
|
- **通知:最小传输大小**:如果传输大小大于或等于指定值,则显示传输通知。可接受的格式为 `{UNSIGNED} B/KB/MB/GB/TB/PB`。
|
||||||
- **SSH 配置路径**:连接到 SCP/SFTP 服务器时使用的 SSH 配置文件。如果留空,则不使用任何文件。你可以指定以 `~` 开头的路径来表示主目录(例如 `~/.ssh/config`)。termscp 支持的属性列于 [ssh2-config 公开的属性](https://github.com/veeso/ssh2-config#exposed-attributes)。另请参见 [SSH 密钥存储](ssh-keys.md)。
|
- **SSH 配置路径**:连接到 SCP/SFTP 服务器时使用的 SSH 配置文件。如果留空,则不使用任何文件。你可以指定以 `~` 开头的路径来表示主目录(例如 `~/.ssh/config`)。termscp 支持的属性列于 [ssh2-config 公开的属性](https://github.com/veeso/ssh2-config#exposed-attributes)。另请参见 [SSH 密钥存储](ssh-keys.md)。
|
||||||
|
|
||||||
|
## SSH 配置行为
|
||||||
|
|
||||||
|
在 SFTP 或 SCP 的任一认证面板中,更改并离开 `Host` 字段后,termscp 会保持输入的别名可见,并使用匹配的 SSH 配置条目填充可见的 `Port` 和 `Username` 字段。如果新的 Host 不匹配任何条目,Port 会重置为 `22`,Username 会重置为空。`HostName` 永远不会替换表单中的别名。
|
||||||
|
|
||||||
|
书签会保留其保存的 Port 和 User 值;SSH 配置不会在幕后覆盖它们。连接时,已配置的文件仍会提供 `HostName` 和其他受支持的 SSH 选项。
|
||||||
|
|||||||
@@ -12,6 +12,12 @@ termscp 可以根据你传入的参数以三种不同的方式启动。
|
|||||||
|
|
||||||
当 termscp 在不带地址的情况下启动时,会显示认证表单。填写协议、地址、端口、用户名和密码,然后进行连接。连接成功后,termscp 将打开双面板浏览器。
|
当 termscp 在不带地址的情况下启动时,会显示认证表单。填写协议、地址、端口、用户名和密码,然后进行连接。连接成功后,termscp 将打开双面板浏览器。
|
||||||
|
|
||||||
|
## SSH 配置优先级
|
||||||
|
|
||||||
|
对于 SFTP 和 SCP 连接,termscp 会按以下顺序解析 CLI 的 Username 和 Port 值:显式指定的 `Username`/`Port`、SSH 配置中的 `User`/`Port`,然后是当前 OS 用户和 Port `22`。Username 和 Port 会独立解析,因此其中一个值被显式指定不会阻止 SSH 配置提供另一个值。
|
||||||
|
|
||||||
|
例如,`termscp myhost` 会使用为 `myhost` 配置的 Port 和 User。无论 SSH 配置为何,`termscp alice@myhost:22` 都会使用 `alice` 和 `22`。
|
||||||
|
|
||||||
## 地址参数语法
|
## 地址参数语法
|
||||||
|
|
||||||
通用地址参数采用以下语法:
|
通用地址参数采用以下语法:
|
||||||
@@ -22,7 +28,7 @@ termscp 可以根据你传入的参数以三种不同的方式启动。
|
|||||||
|
|
||||||
这种语法很方便,你很可能会用它来代替交互式表单。下面是一些示例。
|
这种语法很方便,你很可能会用它来代替交互式表单。下面是一些示例。
|
||||||
|
|
||||||
使用默认协议(在你的配置中定义)连接到 `192.168.1.31`。如果未提供端口,则使用所选协议的默认端口。用户名为当前用户的名称。
|
使用默认协议(在你的配置中定义)连接到 `192.168.1.31`。对于 SFTP 和 SCP,未提供的 Port 或 Username 会从匹配的 SSH 配置条目中获取。如果没有配置相应的值,则会回退到协议默认 Port 或当前 OS 用户。其他协议会使用其默认 Port 和当前 OS 用户。
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
termscp 192.168.1.31
|
termscp 192.168.1.31
|
||||||
|
|||||||
@@ -151,8 +151,22 @@ CLI 连接使用 ADC 和默认端点。如果需要自定义端点或服务账
|
|||||||
- 密码
|
- 密码
|
||||||
- 端口(仅其他系统;默认 `445`)
|
- 端口(仅其他系统;默认 `445`)
|
||||||
- 工作组(仅其他系统)
|
- 工作组(仅其他系统)
|
||||||
|
- SMB 版本(仅其他系统;默认 `Auto`)
|
||||||
|
|
||||||
在 Windows 上,端口和工作组字段不会被使用。
|
在 Windows 上,端口、工作组和 SMB 版本字段不会被使用:SMB 协议协商由操作系统管理。
|
||||||
|
|
||||||
|
SMB 版本字段限定协商时可用的方言:
|
||||||
|
|
||||||
|
| 选项 | 协商的方言 |
|
||||||
|
| ---- | ------------------ |
|
||||||
|
| Auto | SMB 2.0.2 至 3.1.1 |
|
||||||
|
| SMB1 | 仅 NT1(CIFS) |
|
||||||
|
| SMB2 | SMB 2.0.2 至 2.1 |
|
||||||
|
| SMB3 | SMB 3.0 至 3.1.1 |
|
||||||
|
|
||||||
|
`Auto` 永远不会协商 SMB1。SMB1 已弃用且不安全:仅在无法支持更新协议的隔离旧设备上选择它。选择 SMB1 时,表单中会显示警告。
|
||||||
|
|
||||||
|
书签使用 SMB 表中的 `dialect` 键保存所选项(`auto`、`smb1`、`smb2` 或 `smb3`)。在此选项出现之前保存的书签没有 `dialect` 键,其行为等同于 `Auto`。下方的地址语法不包含版本;从命令行发起的连接使用 `Auto`。
|
||||||
|
|
||||||
Windows 地址语法:
|
Windows 地址语法:
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
[group('run')]
|
||||||
|
run *args:
|
||||||
|
@set -m; cargo run --bin termscp -- {{ args }}
|
||||||
|
|
||||||
|
# Build, sign, and run the macOS CLI binary with a stable development identity
|
||||||
|
# Job control gives smista its own foreground process group, so Ctrl-C reaches it
|
||||||
|
# without also interrupting just.
|
||||||
|
[group('run')]
|
||||||
|
run_signed *args:
|
||||||
|
@test "$(uname -s)" = "Darwin" || (echo "run_signed is only supported on macOS." >&2; exit 1)
|
||||||
|
cargo build --bin termscp
|
||||||
|
codesign --force --sign "${TERMSCP_CODESIGN_IDENTITY:-Apple Development}" target/debug/termscp
|
||||||
|
@set -m; target/debug/termscp {{ args }}
|
||||||
+170
-11
@@ -7,12 +7,13 @@ use std::path::PathBuf;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use remotefs_ssh::SshKeyStorage as SshKeyStorageTrait;
|
use remotefs_ssh::SshKeyStorage as SshKeyStorageTrait;
|
||||||
|
use ssh2_config::SshConfig;
|
||||||
|
|
||||||
use crate::cli::{Remote, RemoteArgs};
|
use crate::cli::{Remote, RemoteArgs};
|
||||||
use crate::filetransfer::{
|
use crate::filetransfer::{
|
||||||
FileTransferParams, FileTransferProtocol, HostBridgeParams, ProtocolParams,
|
FileTransferParams, FileTransferProtocol, HostBridgeParams, ProtocolParams,
|
||||||
};
|
};
|
||||||
use crate::host::HostError;
|
use crate::host::{HostError, HostErrorType};
|
||||||
use crate::system::bookmarks_client::BookmarksClient;
|
use crate::system::bookmarks_client::BookmarksClient;
|
||||||
use crate::system::config_client::ConfigClient;
|
use crate::system::config_client::ConfigClient;
|
||||||
use crate::system::environment;
|
use crate::system::environment;
|
||||||
@@ -23,7 +24,7 @@ use crate::ui::activities::filetransfer::FileTransferActivity;
|
|||||||
use crate::ui::activities::setup::SetupActivity;
|
use crate::ui::activities::setup::SetupActivity;
|
||||||
use crate::ui::activities::{Activity, ExitReason};
|
use crate::ui::activities::{Activity, ExitReason};
|
||||||
use crate::ui::context::Context;
|
use crate::ui::context::Context;
|
||||||
use crate::utils::{fmt, tty};
|
use crate::utils::{fmt, ssh as ssh_utils, tty};
|
||||||
|
|
||||||
/// NextActivity identifies the next identity to run once the current has ended
|
/// NextActivity identifies the next identity to run once the current has ended
|
||||||
pub enum NextActivity {
|
pub enum NextActivity {
|
||||||
@@ -67,7 +68,19 @@ impl ActivityManager {
|
|||||||
};
|
};
|
||||||
let error = error_config.or(error_bookmark);
|
let error = error_config.or(error_bookmark);
|
||||||
let theme_provider: ThemeProvider = Self::init_theme_provider();
|
let theme_provider: ThemeProvider = Self::init_theme_provider();
|
||||||
let ctx: Context = Context::new(bookmarks_client, config_client, theme_provider, error);
|
let ssh_config = config_client
|
||||||
|
.get_ssh_config()
|
||||||
|
.map(ssh_utils::parse_ssh2_config)
|
||||||
|
.transpose()
|
||||||
|
.map_err(|err| HostError::from(HostErrorType::InvalidSshConfig(err)))?;
|
||||||
|
|
||||||
|
let ctx: Context = Context::new(
|
||||||
|
bookmarks_client,
|
||||||
|
config_client,
|
||||||
|
theme_provider,
|
||||||
|
ssh_config,
|
||||||
|
error,
|
||||||
|
);
|
||||||
Ok(ActivityManager {
|
Ok(ActivityManager {
|
||||||
context: Some(ctx),
|
context: Some(ctx),
|
||||||
ticks,
|
ticks,
|
||||||
@@ -83,13 +96,20 @@ impl ActivityManager {
|
|||||||
¶ms.name,
|
¶ms.name,
|
||||||
params.password.as_deref(),
|
params.password.as_deref(),
|
||||||
),
|
),
|
||||||
Remote::Host(host_params) => self.set_host_params(
|
Remote::Host(host_params) => {
|
||||||
HostParams::HostBridge(HostBridgeParams::Remote(
|
let params = apply_ssh_config_to_omitted_cli_parameters(
|
||||||
host_params.file_transfer_params.protocol,
|
host_params.file_transfer_params,
|
||||||
host_params.file_transfer_params.params,
|
host_params.port_explicit,
|
||||||
)),
|
self.context_ref()?.ssh_config(),
|
||||||
host_params.password.as_deref(),
|
);
|
||||||
),
|
self.set_host_params(
|
||||||
|
HostParams::HostBridge(HostBridgeParams::Remote(
|
||||||
|
params.protocol,
|
||||||
|
params.params,
|
||||||
|
)),
|
||||||
|
host_params.password.as_deref(),
|
||||||
|
)
|
||||||
|
}
|
||||||
Remote::None => {
|
Remote::None => {
|
||||||
// local dir is remote_args.local_dir if set, otherwise current dir
|
// local dir is remote_args.local_dir if set, otherwise current dir
|
||||||
let local_dir = remote_args
|
let local_dir = remote_args
|
||||||
@@ -112,7 +132,11 @@ impl ActivityManager {
|
|||||||
self.resolve_bookmark_name(Host::Remote, ¶ms.name, params.password.as_deref())
|
self.resolve_bookmark_name(Host::Remote, ¶ms.name, params.password.as_deref())
|
||||||
}
|
}
|
||||||
Remote::Host(host_params) => self.set_host_params(
|
Remote::Host(host_params) => self.set_host_params(
|
||||||
HostParams::Remote(host_params.file_transfer_params),
|
HostParams::Remote(apply_ssh_config_to_omitted_cli_parameters(
|
||||||
|
host_params.file_transfer_params,
|
||||||
|
host_params.port_explicit,
|
||||||
|
self.context_ref()?.ssh_config(),
|
||||||
|
)),
|
||||||
host_params.password.as_deref(),
|
host_params.password.as_deref(),
|
||||||
),
|
),
|
||||||
Remote::None => Ok(()),
|
Remote::None => Ok(()),
|
||||||
@@ -530,3 +554,138 @@ impl ActivityManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Applies SSH configuration values only to CLI parameters omitted by the user.
|
||||||
|
fn apply_ssh_config_to_omitted_cli_parameters(
|
||||||
|
mut file_transfer_params: FileTransferParams,
|
||||||
|
port_explicit: bool,
|
||||||
|
ssh_config: Option<&SshConfig>,
|
||||||
|
) -> FileTransferParams {
|
||||||
|
if !matches!(
|
||||||
|
file_transfer_params.protocol,
|
||||||
|
FileTransferProtocol::Scp | FileTransferProtocol::Sftp,
|
||||||
|
) {
|
||||||
|
return file_transfer_params;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let ProtocolParams::Generic(params) = &mut file_transfer_params.params {
|
||||||
|
let resolved = ssh_utils::resolve_ssh_host_params(ssh_config, params.address.as_str());
|
||||||
|
if !port_explicit {
|
||||||
|
params.port = resolved.port;
|
||||||
|
}
|
||||||
|
if params.username.is_none() {
|
||||||
|
params.username = resolved.username;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
file_transfer_params
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
use super::apply_ssh_config_to_omitted_cli_parameters;
|
||||||
|
use crate::filetransfer::params::GenericProtocolParams;
|
||||||
|
use crate::filetransfer::{FileTransferParams, FileTransferProtocol, ProtocolParams};
|
||||||
|
use crate::utils::ssh::parse_ssh2_config;
|
||||||
|
use crate::utils::test_helpers;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_apply_ssh_config_to_omitted_cli_port_and_username() {
|
||||||
|
let config = ssh_config();
|
||||||
|
let params = ssh_params(22, None);
|
||||||
|
|
||||||
|
let resolved = apply_ssh_config_to_omitted_cli_parameters(params, false, Some(&config));
|
||||||
|
let resolved = resolved.params.generic_params().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(resolved.port, 2222);
|
||||||
|
assert_eq!(resolved.username.as_deref(), Some("configured-user"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_preserve_explicit_cli_port_over_ssh_config() {
|
||||||
|
let config = ssh_config();
|
||||||
|
let params = ssh_params(22, None);
|
||||||
|
|
||||||
|
let resolved = apply_ssh_config_to_omitted_cli_parameters(params, true, Some(&config));
|
||||||
|
let resolved = resolved.params.generic_params().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(resolved.port, 22);
|
||||||
|
assert_eq!(resolved.username.as_deref(), Some("configured-user"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_preserve_explicit_cli_username_over_ssh_config() {
|
||||||
|
let config = ssh_config();
|
||||||
|
let params = ssh_params(22, Some("cli-user"));
|
||||||
|
|
||||||
|
let resolved = apply_ssh_config_to_omitted_cli_parameters(params, false, Some(&config));
|
||||||
|
let resolved = resolved.params.generic_params().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(resolved.port, 2222);
|
||||||
|
assert_eq!(resolved.username.as_deref(), Some("cli-user"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_apply_only_omitted_cli_ssh_parameters() {
|
||||||
|
let config = ssh_config();
|
||||||
|
let params = ssh_params(2200, Some("cli-user"));
|
||||||
|
|
||||||
|
let resolved = apply_ssh_config_to_omitted_cli_parameters(params, true, Some(&config));
|
||||||
|
let resolved = resolved.params.generic_params().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(resolved.port, 2200);
|
||||||
|
assert_eq!(resolved.username.as_deref(), Some("cli-user"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_default_omitted_cli_ssh_port_without_configuration() {
|
||||||
|
let params = ssh_params(22, None);
|
||||||
|
|
||||||
|
let resolved = apply_ssh_config_to_omitted_cli_parameters(params, false, None);
|
||||||
|
|
||||||
|
assert_eq!(resolved.params.generic_params().unwrap().port, 22);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_leave_non_ssh_cli_parameters_unchanged() {
|
||||||
|
let params = FileTransferParams::new(
|
||||||
|
FileTransferProtocol::Ftp(false),
|
||||||
|
ProtocolParams::Generic(
|
||||||
|
GenericProtocolParams::default()
|
||||||
|
.address("configured-host")
|
||||||
|
.port(21)
|
||||||
|
.username(Some("ftp-user")),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
let resolved =
|
||||||
|
apply_ssh_config_to_omitted_cli_parameters(params, false, Some(&ssh_config()));
|
||||||
|
let resolved = resolved.params.generic_params().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(resolved.port, 21);
|
||||||
|
assert_eq!(resolved.username.as_deref(), Some("ftp-user"));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ssh_params(port: u16, username: Option<&str>) -> FileTransferParams {
|
||||||
|
FileTransferParams::new(
|
||||||
|
FileTransferProtocol::Scp,
|
||||||
|
ProtocolParams::Generic(
|
||||||
|
GenericProtocolParams::default()
|
||||||
|
.address("configured-host")
|
||||||
|
.port(port)
|
||||||
|
.username(username),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ssh_config() -> ssh2_config::SshConfig {
|
||||||
|
let config_file = test_helpers::create_sample_file_with_content(
|
||||||
|
"Host configured-host\n Port 2222\n User configured-user\n",
|
||||||
|
);
|
||||||
|
|
||||||
|
parse_ssh2_config(&config_file.path().to_string_lossy())
|
||||||
|
.expect("test SSH configuration should parse")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+35
-5
@@ -70,8 +70,13 @@ impl TryFrom<&Args> for RemoteArgs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let remote = match addr_type {
|
let remote = match addr_type {
|
||||||
AddrType::Address => Self::parse_remote_address(arg)
|
AddrType::Address => Self::parse_remote_address(arg).map(|parsed| {
|
||||||
.map(|x| Remote::Host(HostParams::new(x, password)))?,
|
Remote::Host(HostParams::new(
|
||||||
|
parsed.file_transfer_params,
|
||||||
|
parsed.port_explicit,
|
||||||
|
password,
|
||||||
|
))
|
||||||
|
})?,
|
||||||
AddrType::Bookmark => Remote::Bookmark(BookmarkParams::new(arg, password.as_ref())),
|
AddrType::Bookmark => Remote::Bookmark(BookmarkParams::new(arg, password.as_ref())),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -99,8 +104,9 @@ impl TryFrom<&Args> for RemoteArgs {
|
|||||||
|
|
||||||
impl RemoteArgs {
|
impl RemoteArgs {
|
||||||
/// Parse remote address
|
/// Parse remote address
|
||||||
fn parse_remote_address(remote: &str) -> Result<FileTransferParams, String> {
|
fn parse_remote_address(remote: &str) -> Result<utils::parser::ParsedRemote, String> {
|
||||||
utils::parser::parse_remote_opt(remote).map_err(|e| format!("Bad address option: {e}"))
|
utils::parser::parse_remote_opt_with_metadata(remote)
|
||||||
|
.map_err(|e| format!("Bad address option: {e}"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,6 +143,8 @@ pub struct BookmarkParams {
|
|||||||
pub struct HostParams {
|
pub struct HostParams {
|
||||||
/// file transfer parameters
|
/// file transfer parameters
|
||||||
pub file_transfer_params: FileTransferParams,
|
pub file_transfer_params: FileTransferParams,
|
||||||
|
/// Whether the address explicitly provided a port.
|
||||||
|
pub port_explicit: bool,
|
||||||
/// host password specified in arguments
|
/// host password specified in arguments
|
||||||
pub password: Option<String>,
|
pub password: Option<String>,
|
||||||
}
|
}
|
||||||
@@ -151,9 +159,14 @@ impl BookmarkParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl HostParams {
|
impl HostParams {
|
||||||
pub fn new<S: AsRef<str>>(params: FileTransferParams, password: Option<S>) -> Self {
|
pub fn new<S: AsRef<str>>(
|
||||||
|
params: FileTransferParams,
|
||||||
|
port_explicit: bool,
|
||||||
|
password: Option<S>,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
file_transfer_params: params,
|
file_transfer_params: params,
|
||||||
|
port_explicit,
|
||||||
password: password.map(|x| x.as_ref().to_string()),
|
password: password.map(|x| x.as_ref().to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -179,6 +192,23 @@ mod test {
|
|||||||
assert_eq!(remote_args.local_dir, None);
|
assert_eq!(remote_args.local_dir, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_preserve_explicit_port_from_positional_remote() {
|
||||||
|
for (remote, port_explicit) in [("scp://host", false), ("scp://host:22", true)] {
|
||||||
|
let args = Args {
|
||||||
|
positional: vec![remote.to_string()],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let remote_args = RemoteArgs::try_from(&args).unwrap();
|
||||||
|
let Remote::Host(params) = remote_args.remote else {
|
||||||
|
panic!("expected positional remote to be a host");
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(params.port_explicit, port_explicit, "{remote}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_should_make_remote_args_from_args_two_remotes() {
|
fn test_should_make_remote_args_from_args_two_remotes() {
|
||||||
let args = Args {
|
let args = Args {
|
||||||
|
|||||||
+45
-15
@@ -191,25 +191,25 @@ impl From<Bookmark> for FileTransferParams {
|
|||||||
}
|
}
|
||||||
#[cfg(posix)]
|
#[cfg(posix)]
|
||||||
FileTransferProtocol::Smb => {
|
FileTransferProtocol::Smb => {
|
||||||
let params = TransferSmbParams::new(
|
let smb = bookmark.smb.unwrap_or_default();
|
||||||
bookmark.address.unwrap_or_default(),
|
let params =
|
||||||
bookmark.smb.clone().map(|x| x.share).unwrap_or_default(),
|
TransferSmbParams::new(bookmark.address.unwrap_or_default(), smb.share)
|
||||||
)
|
.port(bookmark.port.unwrap_or(445))
|
||||||
.port(bookmark.port.unwrap_or(445))
|
.username(bookmark.username)
|
||||||
.username(bookmark.username)
|
.password(bookmark.password)
|
||||||
.password(bookmark.password)
|
.workgroup(smb.workgroup)
|
||||||
.workgroup(bookmark.smb.and_then(|x| x.workgroup));
|
.dialect(smb.dialect.unwrap_or_default());
|
||||||
|
|
||||||
Self::new(bookmark.protocol, ProtocolParams::Smb(params))
|
Self::new(bookmark.protocol, ProtocolParams::Smb(params))
|
||||||
}
|
}
|
||||||
#[cfg(win)]
|
#[cfg(win)]
|
||||||
FileTransferProtocol::Smb => {
|
FileTransferProtocol::Smb => {
|
||||||
let params = TransferSmbParams::new(
|
let smb = bookmark.smb.unwrap_or_default();
|
||||||
bookmark.address.unwrap_or_default(),
|
let params =
|
||||||
bookmark.smb.clone().map(|x| x.share).unwrap_or_default(),
|
TransferSmbParams::new(bookmark.address.unwrap_or_default(), smb.share)
|
||||||
)
|
.username(bookmark.username)
|
||||||
.username(bookmark.username)
|
.password(bookmark.password)
|
||||||
.password(bookmark.password);
|
.dialect(smb.dialect.unwrap_or_default());
|
||||||
|
|
||||||
Self::new(bookmark.protocol, ProtocolParams::Smb(params))
|
Self::new(bookmark.protocol, ProtocolParams::Smb(params))
|
||||||
}
|
}
|
||||||
@@ -254,7 +254,7 @@ mod tests {
|
|||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::filetransfer::params::DEFAULT_GCS_ENDPOINT;
|
use crate::filetransfer::params::{DEFAULT_GCS_ENDPOINT, SmbDialect};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_bookmarks_default() {
|
fn test_bookmarks_default() {
|
||||||
@@ -605,6 +605,7 @@ mod tests {
|
|||||||
smb: Some(SmbParams {
|
smb: Some(SmbParams {
|
||||||
share: "test".to_string(),
|
share: "test".to_string(),
|
||||||
workgroup: Some("testone".to_string()),
|
workgroup: Some("testone".to_string()),
|
||||||
|
dialect: Some(SmbDialect::Smb2),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -625,6 +626,7 @@ mod tests {
|
|||||||
assert_eq!(smb_params.password.as_deref().unwrap(), "bar");
|
assert_eq!(smb_params.password.as_deref().unwrap(), "bar");
|
||||||
assert_eq!(smb_params.username.as_deref().unwrap(), "foo");
|
assert_eq!(smb_params.username.as_deref().unwrap(), "foo");
|
||||||
assert_eq!(smb_params.workgroup.as_deref().unwrap(), "testone");
|
assert_eq!(smb_params.workgroup.as_deref().unwrap(), "testone");
|
||||||
|
assert_eq!(smb_params.dialect, SmbDialect::Smb2);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -644,6 +646,7 @@ mod tests {
|
|||||||
smb: Some(SmbParams {
|
smb: Some(SmbParams {
|
||||||
share: "test".to_string(),
|
share: "test".to_string(),
|
||||||
workgroup: None,
|
workgroup: None,
|
||||||
|
dialect: Some(SmbDialect::Smb2),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -660,5 +663,32 @@ mod tests {
|
|||||||
let smb_params = params.params.smb_params().unwrap();
|
let smb_params = params.params.smb_params().unwrap();
|
||||||
assert_eq!(smb_params.address.as_str(), "localhost");
|
assert_eq!(smb_params.address.as_str(), "localhost");
|
||||||
assert_eq!(smb_params.share.as_str(), "test");
|
assert_eq!(smb_params.share.as_str(), "test");
|
||||||
|
assert_eq!(smb_params.dialect, SmbDialect::Smb2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_default_dialect_when_bookmark_has_none() {
|
||||||
|
let bookmark: Bookmark = Bookmark {
|
||||||
|
protocol: FileTransferProtocol::Smb,
|
||||||
|
address: Some("localhost".to_string()),
|
||||||
|
port: Some(445),
|
||||||
|
username: None,
|
||||||
|
password: None,
|
||||||
|
remote_path: None,
|
||||||
|
local_path: None,
|
||||||
|
kube: None,
|
||||||
|
s3: None,
|
||||||
|
gcs: None,
|
||||||
|
smb: Some(SmbParams {
|
||||||
|
share: "test".to_string(),
|
||||||
|
workgroup: None,
|
||||||
|
dialect: None,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
let params = FileTransferParams::from(bookmark);
|
||||||
|
assert_eq!(
|
||||||
|
params.params.smb_params().unwrap().dialect,
|
||||||
|
SmbDialect::Auto
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::filetransfer::params::SmbParams as TransferSmbParams;
|
use crate::filetransfer::params::{SmbDialect, SmbParams as TransferSmbParams};
|
||||||
|
|
||||||
/// Extra Connection parameters for SMB protocol
|
/// Extra Connection parameters for SMB protocol
|
||||||
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq, Default)]
|
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq, Default)]
|
||||||
@@ -13,6 +13,9 @@ pub struct SmbParams {
|
|||||||
pub share: String,
|
pub share: String,
|
||||||
/// Optional SMB workgroup used on POSIX platforms.
|
/// Optional SMB workgroup used on POSIX platforms.
|
||||||
pub workgroup: Option<String>,
|
pub workgroup: Option<String>,
|
||||||
|
/// Requested SMB protocol family. `None` (older bookmarks) means `Auto`.
|
||||||
|
#[serde(default)]
|
||||||
|
pub dialect: Option<SmbDialect>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(posix)]
|
#[cfg(posix)]
|
||||||
@@ -21,6 +24,7 @@ impl From<TransferSmbParams> for SmbParams {
|
|||||||
Self {
|
Self {
|
||||||
share: params.share,
|
share: params.share,
|
||||||
workgroup: params.workgroup,
|
workgroup: params.workgroup,
|
||||||
|
dialect: Some(params.dialect),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -31,6 +35,46 @@ impl From<TransferSmbParams> for SmbParams {
|
|||||||
Self {
|
Self {
|
||||||
share: params.share,
|
share: params.share,
|
||||||
workgroup: None,
|
workgroup: None,
|
||||||
|
dialect: Some(params.dialect),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_deserialize_missing_dialect_as_none() {
|
||||||
|
let params: SmbParams = toml::from_str("share = \"temp\"").unwrap();
|
||||||
|
assert_eq!(params.share.as_str(), "temp");
|
||||||
|
assert_eq!(params.dialect, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_deserialize_dialect() {
|
||||||
|
let params: SmbParams = toml::from_str("share = \"temp\"\ndialect = \"smb1\"").unwrap();
|
||||||
|
assert_eq!(params.dialect, Some(SmbDialect::Smb1));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_round_trip_dialect() {
|
||||||
|
let params = SmbParams {
|
||||||
|
share: "temp".to_string(),
|
||||||
|
workgroup: None,
|
||||||
|
dialect: Some(SmbDialect::Smb2),
|
||||||
|
};
|
||||||
|
let toml_str = toml::to_string(¶ms).unwrap();
|
||||||
|
let decoded: SmbParams = toml::from_str(&toml_str).unwrap();
|
||||||
|
assert_eq!(decoded, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_convert_transfer_params_with_dialect() {
|
||||||
|
let transfer = TransferSmbParams::new("localhost", "temp").dialect(SmbDialect::Smb3);
|
||||||
|
let params = SmbParams::from(transfer);
|
||||||
|
assert_eq!(params.dialect, Some(SmbDialect::Smb3));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -118,7 +118,8 @@ mod tests {
|
|||||||
use crate::config::bookmarks::{Bookmark, KubeParams, S3Params, SmbParams, UserHosts};
|
use crate::config::bookmarks::{Bookmark, KubeParams, S3Params, SmbParams, UserHosts};
|
||||||
use crate::config::params::UserConfig;
|
use crate::config::params::UserConfig;
|
||||||
use crate::config::themes::Theme;
|
use crate::config::themes::Theme;
|
||||||
use crate::filetransfer::FileTransferProtocol;
|
use crate::filetransfer::params::SmbDialect;
|
||||||
|
use crate::filetransfer::{FileTransferParams, FileTransferProtocol};
|
||||||
use crate::utils::test_helpers::create_file_ioers;
|
use crate::utils::test_helpers::create_file_ioers;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -445,6 +446,7 @@ mod tests {
|
|||||||
assert_eq!(smb.share.as_str(), "temp");
|
assert_eq!(smb.share.as_str(), "temp");
|
||||||
#[cfg(posix)]
|
#[cfg(posix)]
|
||||||
assert_eq!(smb.workgroup.as_deref().unwrap(), "test");
|
assert_eq!(smb.workgroup.as_deref().unwrap(), "test");
|
||||||
|
assert_eq!(smb.dialect, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -491,6 +493,66 @@ mod tests {
|
|||||||
assert_eq!(gcs.service_account_key, None);
|
assert_eq!(gcs.service_account_key, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_deserialize_legacy_smb_bookmark_without_dialect() {
|
||||||
|
let toml_file = create_good_toml_bookmarks();
|
||||||
|
toml_file.as_file().sync_all().unwrap();
|
||||||
|
toml_file.as_file().rewind().unwrap();
|
||||||
|
|
||||||
|
let hosts: UserHosts = deserialize(Box::new(toml_file)).unwrap();
|
||||||
|
let host = hosts.bookmarks.get("smb").unwrap();
|
||||||
|
let smb = host.smb.as_ref().unwrap();
|
||||||
|
assert_eq!(smb.share.as_str(), "temp");
|
||||||
|
assert_eq!(smb.dialect, None);
|
||||||
|
|
||||||
|
// Legacy bookmarks resolve to secure Auto at runtime.
|
||||||
|
let params = FileTransferParams::from(host.clone());
|
||||||
|
assert_eq!(
|
||||||
|
params.params.smb_params().unwrap().dialect,
|
||||||
|
SmbDialect::Auto
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_deserialize_smb_bookmark_with_dialect() {
|
||||||
|
let toml_file = create_smb_dialect_toml_bookmark();
|
||||||
|
toml_file.as_file().sync_all().unwrap();
|
||||||
|
toml_file.as_file().rewind().unwrap();
|
||||||
|
|
||||||
|
let hosts: UserHosts = deserialize(Box::new(toml_file)).unwrap();
|
||||||
|
let host = hosts.bookmarks.get("smb-dialect").unwrap();
|
||||||
|
let smb = host.smb.as_ref().unwrap();
|
||||||
|
assert_eq!(smb.dialect, Some(SmbDialect::Smb2));
|
||||||
|
|
||||||
|
let params = FileTransferParams::from(host.clone());
|
||||||
|
assert_eq!(
|
||||||
|
params.params.smb_params().unwrap().dialect,
|
||||||
|
SmbDialect::Smb2
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_reserialize_legacy_smb_bookmark_and_reload() {
|
||||||
|
let toml_file = create_good_toml_bookmarks();
|
||||||
|
toml_file.as_file().sync_all().unwrap();
|
||||||
|
toml_file.as_file().rewind().unwrap();
|
||||||
|
let hosts: UserHosts = deserialize(Box::new(toml_file)).unwrap();
|
||||||
|
|
||||||
|
let output_file = tempfile::NamedTempFile::new().unwrap();
|
||||||
|
let output_path = output_file.path().to_path_buf();
|
||||||
|
serialize(
|
||||||
|
&hosts,
|
||||||
|
Box::new(std::fs::File::create(&output_path).unwrap()),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let reloaded: UserHosts =
|
||||||
|
deserialize(Box::new(std::fs::File::open(&output_path).unwrap())).unwrap();
|
||||||
|
let smb = reloaded.bookmarks.get("smb").unwrap().smb.as_ref().unwrap();
|
||||||
|
assert_eq!(smb.share.as_str(), "temp");
|
||||||
|
assert_eq!(smb.dialect, None);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_serialize_gcs_bookmark_fields() {
|
fn should_serialize_gcs_bookmark_fields() {
|
||||||
let toml_file = create_good_toml_bookmarks();
|
let toml_file = create_good_toml_bookmarks();
|
||||||
@@ -630,6 +692,7 @@ mod tests {
|
|||||||
let smb_params: Option<SmbParams> = Some(SmbParams {
|
let smb_params: Option<SmbParams> = Some(SmbParams {
|
||||||
share: "test".to_string(),
|
share: "test".to_string(),
|
||||||
workgroup: None,
|
workgroup: None,
|
||||||
|
dialect: None,
|
||||||
});
|
});
|
||||||
bookmarks.insert(
|
bookmarks.insert(
|
||||||
String::from("smb"),
|
String::from("smb"),
|
||||||
@@ -789,6 +852,27 @@ mod tests {
|
|||||||
tmpfile
|
tmpfile
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn create_smb_dialect_toml_bookmark() -> tempfile::NamedTempFile {
|
||||||
|
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
|
||||||
|
let file_content: &str = r#"
|
||||||
|
[bookmarks.smb-dialect]
|
||||||
|
protocol = "SMB"
|
||||||
|
address = "localhost"
|
||||||
|
port = 445
|
||||||
|
username = "test"
|
||||||
|
password = "test"
|
||||||
|
|
||||||
|
[bookmarks.smb-dialect.smb]
|
||||||
|
share = "temp"
|
||||||
|
workgroup = "test"
|
||||||
|
dialect = "smb2"
|
||||||
|
|
||||||
|
[recents]
|
||||||
|
"#;
|
||||||
|
tmpfile.write_all(file_content.as_bytes()).unwrap();
|
||||||
|
tmpfile
|
||||||
|
}
|
||||||
|
|
||||||
fn create_v14_pod_bookmark() -> tempfile::NamedTempFile {
|
fn create_v14_pod_bookmark() -> tempfile::NamedTempFile {
|
||||||
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
|
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
|
||||||
let file_content: &str = r#"
|
let file_content: &str = r#"
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use std::path::{Path, PathBuf};
|
|||||||
pub use self::aws_s3::AwsS3Params;
|
pub use self::aws_s3::AwsS3Params;
|
||||||
pub use self::google_cloud_storage::{DEFAULT_GCS_ENDPOINT, GoogleCloudStorageParams};
|
pub use self::google_cloud_storage::{DEFAULT_GCS_ENDPOINT, GoogleCloudStorageParams};
|
||||||
pub use self::kube::KubeProtocolParams;
|
pub use self::kube::KubeProtocolParams;
|
||||||
pub use self::smb::SmbParams;
|
pub use self::smb::{SmbDialect, SmbParams};
|
||||||
pub use self::webdav::WebDAVProtocolParams;
|
pub use self::webdav::WebDAVProtocolParams;
|
||||||
use super::FileTransferProtocol;
|
use super::FileTransferProtocol;
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,26 @@
|
|||||||
//! Defines the runtime connection parameters used to build SMB remote
|
//! Defines the runtime connection parameters used to build SMB remote
|
||||||
//! filesystem clients.
|
//! filesystem clients.
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// SMB protocol family requested for a connection.
|
||||||
|
///
|
||||||
|
/// Each family maps to inclusive dialect bounds when the Unix client is built.
|
||||||
|
/// `Auto` negotiates SMB2 or SMB3 and never falls back to SMB1.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum SmbDialect {
|
||||||
|
/// Negotiate SMB 2.0.2 through SMB 3.1.1.
|
||||||
|
#[default]
|
||||||
|
Auto,
|
||||||
|
/// Force the deprecated NT1 (CIFS) dialect.
|
||||||
|
Smb1,
|
||||||
|
/// Negotiate SMB 2.0.2 through SMB 2.1.
|
||||||
|
Smb2,
|
||||||
|
/// Negotiate SMB 3.0 through SMB 3.1.1.
|
||||||
|
Smb3,
|
||||||
|
}
|
||||||
|
|
||||||
/// Connection parameters for SMB protocol
|
/// Connection parameters for SMB protocol
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct SmbParams {
|
pub struct SmbParams {
|
||||||
@@ -20,6 +40,8 @@ pub struct SmbParams {
|
|||||||
#[cfg(posix)]
|
#[cfg(posix)]
|
||||||
/// Optional workgroup used on POSIX platforms.
|
/// Optional workgroup used on POSIX platforms.
|
||||||
pub workgroup: Option<String>,
|
pub workgroup: Option<String>,
|
||||||
|
/// Requested SMB protocol family. Enforced on POSIX platforms only.
|
||||||
|
pub dialect: SmbDialect,
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- SMB params
|
// -- SMB params
|
||||||
@@ -36,6 +58,7 @@ impl SmbParams {
|
|||||||
password: None,
|
password: None,
|
||||||
#[cfg(posix)]
|
#[cfg(posix)]
|
||||||
workgroup: None,
|
workgroup: None,
|
||||||
|
dialect: SmbDialect::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,6 +84,12 @@ impl SmbParams {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets the SMB protocol family to request.
|
||||||
|
pub fn dialect(mut self, dialect: SmbDialect) -> Self {
|
||||||
|
self.dialect = dialect;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns whether a password is supposed to be required for this protocol params.
|
/// Returns whether a password is supposed to be required for this protocol params.
|
||||||
/// The result true is returned ONLY if the supposed secret is MISSING!!!
|
/// The result true is returned ONLY if the supposed secret is MISSING!!!
|
||||||
pub fn password_missing(&self) -> bool {
|
pub fn password_missing(&self) -> bool {
|
||||||
@@ -82,7 +111,8 @@ mod test {
|
|||||||
|
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
use super::*;
|
use super::SmbParams;
|
||||||
|
use crate::filetransfer::params::SmbDialect;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_init_smb_params() {
|
fn should_init_smb_params() {
|
||||||
@@ -118,6 +148,67 @@ mod test {
|
|||||||
assert_eq!(params.workgroup.as_deref().unwrap(), "baz");
|
assert_eq!(params.workgroup.as_deref().unwrap(), "baz");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_default_dialect_to_auto() {
|
||||||
|
assert_eq!(SmbDialect::default(), SmbDialect::Auto);
|
||||||
|
let params = SmbParams::new("localhost", "temp");
|
||||||
|
assert_eq!(params.dialect, SmbDialect::Auto);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_set_dialect() {
|
||||||
|
let params = SmbParams::new("localhost", "temp").dialect(SmbDialect::Smb1);
|
||||||
|
assert_eq!(params.dialect, SmbDialect::Smb1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_serialize_dialect_lowercase() {
|
||||||
|
assert_eq!(
|
||||||
|
toml::to_string(&Wrapper {
|
||||||
|
dialect: SmbDialect::Auto,
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
.trim(),
|
||||||
|
"dialect = \"auto\""
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
toml::to_string(&Wrapper {
|
||||||
|
dialect: SmbDialect::Smb1,
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
.trim(),
|
||||||
|
"dialect = \"smb1\""
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
toml::to_string(&Wrapper {
|
||||||
|
dialect: SmbDialect::Smb2,
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
.trim(),
|
||||||
|
"dialect = \"smb2\""
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
toml::to_string(&Wrapper {
|
||||||
|
dialect: SmbDialect::Smb3,
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
.trim(),
|
||||||
|
"dialect = \"smb3\""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_deserialize_dialect_lowercase() {
|
||||||
|
let w: Wrapper = toml::from_str("dialect = \"smb3\"").unwrap();
|
||||||
|
assert_eq!(w.dialect, SmbDialect::Smb3);
|
||||||
|
assert!(toml::from_str::<Wrapper>("dialect = \"SMB3\"").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize)]
|
||||||
|
struct Wrapper {
|
||||||
|
dialect: SmbDialect,
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(win)]
|
#[cfg(win)]
|
||||||
fn should_init_smb_params_with_optionals() {
|
fn should_init_smb_params_with_optionals() {
|
||||||
|
|||||||
@@ -11,16 +11,20 @@ use remotefs_ftp::FtpFs;
|
|||||||
use remotefs_gcs::credentials::service_account;
|
use remotefs_gcs::credentials::service_account;
|
||||||
use remotefs_gcs::{GoogleCloudStorageCredentials, GoogleCloudStorageFs};
|
use remotefs_gcs::{GoogleCloudStorageCredentials, GoogleCloudStorageFs};
|
||||||
use remotefs_kube::KubeMultiPodFs as KubeFs;
|
use remotefs_kube::KubeMultiPodFs as KubeFs;
|
||||||
#[cfg(smb_unix)]
|
|
||||||
use remotefs_smb::SmbOptions;
|
|
||||||
#[cfg(smb)]
|
#[cfg(smb)]
|
||||||
use remotefs_smb::{SmbCredentials, SmbFs};
|
use remotefs_smb::{PavaoSmbCredentials as SmbCredentials, PavaoSmbFs as SmbFs};
|
||||||
|
#[cfg(smb_unix)]
|
||||||
|
use remotefs_smb::{PavaoSmbOptions as SmbOptions, SmbDialect as RemoteSmbDialect};
|
||||||
|
#[cfg(smb_windows)]
|
||||||
|
use remotefs_smb::{WNetSmbCredentials as SmbCredentials, WNetSmbFs as SmbFs};
|
||||||
use remotefs_ssh::{
|
use remotefs_ssh::{
|
||||||
NoCheckServerKey, RusshSession as SshSession, ScpFs, SftpFs, SshAgentIdentity,
|
NoCheckServerKey, RusshSession as SshSession, ScpFs, SftpFs, SshAgentIdentity,
|
||||||
SshConfigParseRule, SshOpts,
|
SshConfigParseRule, SshOpts,
|
||||||
};
|
};
|
||||||
use remotefs_webdav::WebDAVFs;
|
use remotefs_webdav::WebDAVFs;
|
||||||
|
|
||||||
|
#[cfg(smb_unix)]
|
||||||
|
use super::params::SmbDialect;
|
||||||
#[cfg(not(smb))]
|
#[cfg(not(smb))]
|
||||||
use super::params::{AwsS3Params, GenericProtocolParams, GoogleCloudStorageParams};
|
use super::params::{AwsS3Params, GenericProtocolParams, GoogleCloudStorageParams};
|
||||||
#[cfg(smb)]
|
#[cfg(smb)]
|
||||||
@@ -29,7 +33,6 @@ use super::params::{KubeProtocolParams, WebDAVProtocolParams};
|
|||||||
use super::{FileTransferProtocol, ProtocolParams};
|
use super::{FileTransferProtocol, ProtocolParams};
|
||||||
use crate::system::config_client::ConfigClient;
|
use crate::system::config_client::ConfigClient;
|
||||||
use crate::system::sshkey_storage::SshKeyStorage;
|
use crate::system::sshkey_storage::SshKeyStorage;
|
||||||
use crate::utils::ssh as ssh_utils;
|
|
||||||
|
|
||||||
/// Remotefs builder
|
/// Remotefs builder
|
||||||
pub struct RemoteFsBuilder;
|
pub struct RemoteFsBuilder;
|
||||||
@@ -190,6 +193,17 @@ impl RemoteFsBuilder {
|
|||||||
Ok(SftpFs::russh(opts, rt))
|
Ok(SftpFs::russh(opts, rt))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Maps the user-facing SMB family to inclusive remotefs dialect bounds.
|
||||||
|
#[cfg(smb_unix)]
|
||||||
|
fn smb_dialect_bounds(dialect: SmbDialect) -> (RemoteSmbDialect, RemoteSmbDialect) {
|
||||||
|
match dialect {
|
||||||
|
SmbDialect::Auto => (RemoteSmbDialect::Smb202, RemoteSmbDialect::Smb311),
|
||||||
|
SmbDialect::Smb1 => (RemoteSmbDialect::Nt1, RemoteSmbDialect::Nt1),
|
||||||
|
SmbDialect::Smb2 => (RemoteSmbDialect::Smb202, RemoteSmbDialect::Smb210),
|
||||||
|
SmbDialect::Smb3 => (RemoteSmbDialect::Smb300, RemoteSmbDialect::Smb311),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(smb_unix)]
|
#[cfg(smb_unix)]
|
||||||
fn smb_client(params: SmbParams) -> Result<SmbFs, String> {
|
fn smb_client(params: SmbParams) -> Result<SmbFs, String> {
|
||||||
let mut credentials = SmbCredentials::default()
|
let mut credentials = SmbCredentials::default()
|
||||||
@@ -206,11 +220,14 @@ impl RemoteFsBuilder {
|
|||||||
credentials = credentials.workgroup(workgroup);
|
credentials = credentials.workgroup(workgroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
SmbFs::try_new(
|
let (min_dialect, max_dialect) = Self::smb_dialect_bounds(params.dialect);
|
||||||
|
SmbFs::try_new_with_dialect(
|
||||||
credentials,
|
credentials,
|
||||||
SmbOptions::default()
|
SmbOptions::default()
|
||||||
.one_share_per_server(true)
|
.one_share_per_server(true)
|
||||||
.case_sensitive(false),
|
.case_sensitive(false),
|
||||||
|
min_dialect,
|
||||||
|
max_dialect,
|
||||||
)
|
)
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
error!("Invalid params for protocol SMB: {e}");
|
error!("Invalid params for protocol SMB: {e}");
|
||||||
@@ -229,6 +246,7 @@ impl RemoteFsBuilder {
|
|||||||
credentials = credentials.password(password);
|
credentials = credentials.password(password);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dialect is OS-managed on Windows.
|
||||||
Ok(SmbFs::new(credentials))
|
Ok(SmbFs::new(credentials))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,37 +260,9 @@ impl RemoteFsBuilder {
|
|||||||
.key_storage(Box::new(Self::make_ssh_storage(config_client)))
|
.key_storage(Box::new(Self::make_ssh_storage(config_client)))
|
||||||
.ssh_agent_identity(Some(SshAgentIdentity::All))
|
.ssh_agent_identity(Some(SshAgentIdentity::All))
|
||||||
.port(params.port);
|
.port(params.port);
|
||||||
// get ssh config
|
|
||||||
let ssh_config = config_client
|
|
||||||
.get_ssh_config()
|
|
||||||
.and_then(|path| {
|
|
||||||
debug!("reading ssh config at {}", path);
|
|
||||||
ssh_utils::parse_ssh2_config(path).ok()
|
|
||||||
})
|
|
||||||
.map(|config| config.query(¶ms.address));
|
|
||||||
|
|
||||||
//* override port
|
|
||||||
if let Some(port) = ssh_config.as_ref().and_then(|config| config.port) {
|
|
||||||
opts = opts.port(port);
|
|
||||||
}
|
|
||||||
|
|
||||||
//* get username. Case 1 provided in params
|
|
||||||
if let Some(username) = params.username {
|
if let Some(username) = params.username {
|
||||||
opts = opts.username(username);
|
opts = opts.username(username);
|
||||||
} else if let Some(ssh_config) = &ssh_config {
|
|
||||||
debug!("no username was provided, checking whether a user is set for this host");
|
|
||||||
if let Some(username) = &ssh_config.user {
|
|
||||||
debug!("found username from config: {username}");
|
|
||||||
opts = opts.username(username);
|
|
||||||
} else {
|
|
||||||
//* case 3: use system username; can't be None
|
|
||||||
debug!("no username was provided, using current username");
|
|
||||||
if let Ok(username) = whoami::username() {
|
|
||||||
opts = opts.username(username);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if let Ok(username) = whoami::username() {
|
} else if let Ok(username) = whoami::username() {
|
||||||
debug!("no username was provided, using current username");
|
|
||||||
opts = opts.username(username);
|
opts = opts.username(username);
|
||||||
}
|
}
|
||||||
// For SSH protocols, only set password if explicitly provided and non-empty.
|
// For SSH protocols, only set password if explicitly provided and non-empty.
|
||||||
@@ -313,6 +303,8 @@ mod test {
|
|||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
#[cfg(smb)]
|
||||||
|
use serial_test::serial;
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -446,12 +438,50 @@ mod test {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(smb)]
|
#[cfg(smb)]
|
||||||
|
#[serial]
|
||||||
fn should_build_smb_fs() {
|
fn should_build_smb_fs() {
|
||||||
let params = ProtocolParams::Smb(SmbParams::new("localhost", "share"));
|
let params = ProtocolParams::Smb(SmbParams::new("localhost", "share"));
|
||||||
let config_client = get_config_client();
|
let config_client = get_config_client();
|
||||||
assert!(RemoteFsBuilder::build(FileTransferProtocol::Smb, params, &config_client).is_ok());
|
assert!(RemoteFsBuilder::build(FileTransferProtocol::Smb, params, &config_client).is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(smb_unix)]
|
||||||
|
fn should_map_smb_dialect_to_bounds() {
|
||||||
|
use remotefs_smb::SmbDialect as RemoteSmbDialect;
|
||||||
|
|
||||||
|
use crate::filetransfer::params::SmbDialect;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
RemoteFsBuilder::smb_dialect_bounds(SmbDialect::Auto),
|
||||||
|
(RemoteSmbDialect::Smb202, RemoteSmbDialect::Smb311)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
RemoteFsBuilder::smb_dialect_bounds(SmbDialect::Smb1),
|
||||||
|
(RemoteSmbDialect::Nt1, RemoteSmbDialect::Nt1)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
RemoteFsBuilder::smb_dialect_bounds(SmbDialect::Smb2),
|
||||||
|
(RemoteSmbDialect::Smb202, RemoteSmbDialect::Smb210)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
RemoteFsBuilder::smb_dialect_bounds(SmbDialect::Smb3),
|
||||||
|
(RemoteSmbDialect::Smb300, RemoteSmbDialect::Smb311)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(smb)]
|
||||||
|
#[serial]
|
||||||
|
fn should_build_smb_fs_with_dialect() {
|
||||||
|
use crate::filetransfer::params::SmbDialect;
|
||||||
|
|
||||||
|
let params =
|
||||||
|
ProtocolParams::Smb(SmbParams::new("localhost", "share").dialect(SmbDialect::Smb1));
|
||||||
|
let config_client = get_config_client();
|
||||||
|
assert!(RemoteFsBuilder::build(FileTransferProtocol::Smb, params, &config_client).is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_not_build_fs() {
|
fn should_not_build_fs() {
|
||||||
let params = ProtocolParams::Generic(
|
let params = ProtocolParams::Generic(
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ pub enum HostErrorType {
|
|||||||
ExecutionFailed,
|
ExecutionFailed,
|
||||||
#[error("Could not delete file")]
|
#[error("Could not delete file")]
|
||||||
DeleteFailed,
|
DeleteFailed,
|
||||||
|
#[error("Invalid SSH configuration: {0}")]
|
||||||
|
InvalidSshConfig(String),
|
||||||
#[cfg(win)]
|
#[cfg(win)]
|
||||||
#[error("Not implemented")]
|
#[error("Not implemented")]
|
||||||
NotImplemented,
|
NotImplemented,
|
||||||
|
|||||||
@@ -101,6 +101,10 @@ pub enum AuthFormId {
|
|||||||
SmbShare,
|
SmbShare,
|
||||||
#[cfg(posix)]
|
#[cfg(posix)]
|
||||||
SmbWorkgroup,
|
SmbWorkgroup,
|
||||||
|
#[cfg(posix)]
|
||||||
|
SmbDialect,
|
||||||
|
#[cfg(posix)]
|
||||||
|
SmbDialectWarning,
|
||||||
Username,
|
Username,
|
||||||
WebDAVUri,
|
WebDAVUri,
|
||||||
}
|
}
|
||||||
@@ -209,6 +213,10 @@ pub enum UiAuthFormMsg {
|
|||||||
SmbWorkgroupDown,
|
SmbWorkgroupDown,
|
||||||
#[cfg(posix)]
|
#[cfg(posix)]
|
||||||
SmbWorkgroupUp,
|
SmbWorkgroupUp,
|
||||||
|
#[cfg(posix)]
|
||||||
|
SmbDialectBlurDown,
|
||||||
|
#[cfg(posix)]
|
||||||
|
SmbDialectBlurUp,
|
||||||
UsernameBlurDown,
|
UsernameBlurDown,
|
||||||
UsernameBlurUp,
|
UsernameBlurUp,
|
||||||
WebDAVUriBlurDown,
|
WebDAVUriBlurDown,
|
||||||
@@ -243,6 +251,18 @@ enum FormTab {
|
|||||||
const STORE_KEY_LATEST_VERSION: &str = "AUTH_LATEST_VERSION";
|
const STORE_KEY_LATEST_VERSION: &str = "AUTH_LATEST_VERSION";
|
||||||
const STORE_KEY_RELEASE_NOTES: &str = "AUTH_RELEASE_NOTES";
|
const STORE_KEY_RELEASE_NOTES: &str = "AUTH_RELEASE_NOTES";
|
||||||
|
|
||||||
|
fn should_resolve_ssh_host_params(
|
||||||
|
protocol: FileTransferProtocol,
|
||||||
|
mounted_address: &str,
|
||||||
|
address: &str,
|
||||||
|
force: bool,
|
||||||
|
) -> bool {
|
||||||
|
matches!(
|
||||||
|
protocol,
|
||||||
|
FileTransferProtocol::Scp | FileTransferProtocol::Sftp
|
||||||
|
) && (force || mounted_address != address)
|
||||||
|
}
|
||||||
|
|
||||||
/// AuthActivity is the data holder for the authentication activity
|
/// AuthActivity is the data holder for the authentication activity
|
||||||
pub struct AuthActivity {
|
pub struct AuthActivity {
|
||||||
app: Application<Id, Msg, NoUserEvent>,
|
app: Application<Id, Msg, NoUserEvent>,
|
||||||
@@ -256,7 +276,11 @@ pub struct AuthActivity {
|
|||||||
redraw: bool,
|
redraw: bool,
|
||||||
/// Host bridge protocol
|
/// Host bridge protocol
|
||||||
host_bridge_protocol: HostBridgeProtocol,
|
host_bridge_protocol: HostBridgeProtocol,
|
||||||
|
/// Last Host address applied to the Host Bridge form.
|
||||||
|
last_host_bridge_address: String,
|
||||||
last_form_tab: FormTab,
|
last_form_tab: FormTab,
|
||||||
|
/// Last Host address applied to the Remote form.
|
||||||
|
last_remote_address: String,
|
||||||
/// Remote file transfer protocol
|
/// Remote file transfer protocol
|
||||||
remote_protocol: FileTransferProtocol,
|
remote_protocol: FileTransferProtocol,
|
||||||
context: Option<Context>,
|
context: Option<Context>,
|
||||||
@@ -273,6 +297,8 @@ impl AuthActivity {
|
|||||||
bookmarks_list: Vec::new(),
|
bookmarks_list: Vec::new(),
|
||||||
exit_reason: None,
|
exit_reason: None,
|
||||||
last_form_tab: FormTab::Remote,
|
last_form_tab: FormTab::Remote,
|
||||||
|
last_host_bridge_address: String::new(),
|
||||||
|
last_remote_address: String::new(),
|
||||||
recents_list: Vec::new(),
|
recents_list: Vec::new(),
|
||||||
redraw: true,
|
redraw: true,
|
||||||
host_bridge_protocol: HostBridgeProtocol::Localhost,
|
host_bridge_protocol: HostBridgeProtocol::Localhost,
|
||||||
@@ -317,6 +343,20 @@ impl AuthActivity {
|
|||||||
self.remote_protocol = protocol;
|
self.remote_protocol = protocol;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn last_mounted_address(&self, form_tab: FormTab) -> &str {
|
||||||
|
match form_tab {
|
||||||
|
FormTab::HostBridge => self.last_host_bridge_address.as_str(),
|
||||||
|
FormTab::Remote => self.last_remote_address.as_str(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_last_mounted_address(&mut self, form_tab: FormTab, address: &str) {
|
||||||
|
match form_tab {
|
||||||
|
FormTab::HostBridge => self.last_host_bridge_address = address.to_string(),
|
||||||
|
FormTab::Remote => self.last_remote_address = address.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Get current input mask to show
|
/// Get current input mask to show
|
||||||
fn host_bridge_input_mask(&self) -> InputMask {
|
fn host_bridge_input_mask(&self) -> InputMask {
|
||||||
match self.host_bridge_protocol {
|
match self.host_bridge_protocol {
|
||||||
@@ -444,4 +484,61 @@ mod tests {
|
|||||||
FileTransferProtocol::GoogleCloudStorage
|
FileTransferProtocol::GoogleCloudStorage
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_resolve_ssh_params_only_after_host_change_or_forced_ssh_transition() {
|
||||||
|
assert!(should_resolve_ssh_host_params(
|
||||||
|
FileTransferProtocol::Sftp,
|
||||||
|
"saved-host",
|
||||||
|
"edited-host",
|
||||||
|
false
|
||||||
|
));
|
||||||
|
assert!(!should_resolve_ssh_host_params(
|
||||||
|
FileTransferProtocol::Sftp,
|
||||||
|
"saved-host",
|
||||||
|
"saved-host",
|
||||||
|
false
|
||||||
|
));
|
||||||
|
assert!(should_resolve_ssh_host_params(
|
||||||
|
FileTransferProtocol::Scp,
|
||||||
|
"saved-host",
|
||||||
|
"saved-host",
|
||||||
|
true
|
||||||
|
));
|
||||||
|
assert!(!should_resolve_ssh_host_params(
|
||||||
|
FileTransferProtocol::Ftp(false),
|
||||||
|
"saved-host",
|
||||||
|
"edited-host",
|
||||||
|
true
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_track_host_bridge_and_remote_addresses_independently() {
|
||||||
|
let mut activity = AuthActivity::new(Duration::ZERO);
|
||||||
|
|
||||||
|
activity.set_last_mounted_address(FormTab::HostBridge, "bookmark-host");
|
||||||
|
activity.set_last_mounted_address(FormTab::Remote, "recent-host");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
activity.last_mounted_address(FormTab::HostBridge),
|
||||||
|
"bookmark-host"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
activity.last_mounted_address(FormTab::Remote),
|
||||||
|
"recent-host"
|
||||||
|
);
|
||||||
|
assert!(!should_resolve_ssh_host_params(
|
||||||
|
FileTransferProtocol::Sftp,
|
||||||
|
activity.last_mounted_address(FormTab::Remote),
|
||||||
|
"recent-host",
|
||||||
|
false
|
||||||
|
));
|
||||||
|
assert!(should_resolve_ssh_host_params(
|
||||||
|
FileTransferProtocol::Sftp,
|
||||||
|
activity.last_mounted_address(FormTab::HostBridge),
|
||||||
|
"edited-host",
|
||||||
|
false
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -315,6 +315,8 @@ impl AuthActivity {
|
|||||||
self.mount_smb_share(form_tab, ¶ms.share);
|
self.mount_smb_share(form_tab, ¶ms.share);
|
||||||
#[cfg(posix)]
|
#[cfg(posix)]
|
||||||
self.mount_smb_workgroup(form_tab, params.workgroup.as_deref().unwrap_or(""));
|
self.mount_smb_workgroup(form_tab, params.workgroup.as_deref().unwrap_or(""));
|
||||||
|
#[cfg(posix)]
|
||||||
|
self.mount_smb_dialect(form_tab, params.dialect);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_bookmark_webdav_into_gui(&mut self, form_tab: FormTab, params: WebDAVProtocolParams) {
|
fn load_bookmark_webdav_into_gui(&mut self, form_tab: FormTab, params: WebDAVProtocolParams) {
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ pub use bookmarks::{
|
|||||||
BookmarkName, BookmarkSavePassword, BookmarksList, DeleteBookmarkPopup, DeleteRecentPopup,
|
BookmarkName, BookmarkSavePassword, BookmarksList, DeleteBookmarkPopup, DeleteRecentPopup,
|
||||||
RecentsList,
|
RecentsList,
|
||||||
};
|
};
|
||||||
#[cfg(posix)]
|
|
||||||
pub use form::InputSmbWorkgroup;
|
|
||||||
pub use form::{
|
pub use form::{
|
||||||
HostBridgeProtocolRadio, InputAddress, InputGcsBucket, InputGcsEndpoint,
|
HostBridgeProtocolRadio, InputAddress, InputGcsBucket, InputGcsEndpoint,
|
||||||
InputGcsServiceAccountKey, InputKubeClientCert, InputKubeClientKey, InputKubeClusterUrl,
|
InputGcsServiceAccountKey, InputKubeClientCert, InputKubeClientKey, InputKubeClusterUrl,
|
||||||
@@ -23,6 +21,8 @@ pub use form::{
|
|||||||
InputS3Region, InputS3SecretAccessKey, InputS3SecurityToken, InputS3SessionToken,
|
InputS3Region, InputS3SecretAccessKey, InputS3SecurityToken, InputS3SessionToken,
|
||||||
InputSmbShare, InputUsername, InputWebDAVUri, RadioS3NewPathStyle, RemoteProtocolRadio,
|
InputSmbShare, InputUsername, InputWebDAVUri, RadioS3NewPathStyle, RemoteProtocolRadio,
|
||||||
};
|
};
|
||||||
|
#[cfg(posix)]
|
||||||
|
pub use form::{InputSmbWorkgroup, RadioSmbDialect, SmbDialectWarning};
|
||||||
pub use popup::{
|
pub use popup::{
|
||||||
ErrorPopup, InfoPopup, InstallUpdatePopup, Keybindings, QuitPopup, ReleaseNotes, WaitPopup,
|
ErrorPopup, InfoPopup, InstallUpdatePopup, Keybindings, QuitPopup, ReleaseNotes, WaitPopup,
|
||||||
WindowSizeError,
|
WindowSizeError,
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ pub use s3::{
|
|||||||
};
|
};
|
||||||
pub use smb::InputSmbShare;
|
pub use smb::InputSmbShare;
|
||||||
#[cfg(posix)]
|
#[cfg(posix)]
|
||||||
pub use smb::InputSmbWorkgroup;
|
pub use smb::{InputSmbWorkgroup, RadioSmbDialect, SmbDialectWarning};
|
||||||
pub use webdav::InputWebDAVUri;
|
pub use webdav::InputWebDAVUri;
|
||||||
|
|
||||||
fn handle_input_ev(
|
fn handle_input_ev(
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
|
#[cfg(posix)]
|
||||||
|
use tui_realm_stdlib::components::Span;
|
||||||
use tuirealm::component::{AppComponent, Component};
|
use tuirealm::component::{AppComponent, Component};
|
||||||
use tuirealm::event::NoUserEvent;
|
use tuirealm::event::NoUserEvent;
|
||||||
|
#[cfg(posix)]
|
||||||
|
use tuirealm::props::SpanStatic;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
#[cfg(posix)]
|
||||||
|
use crate::filetransfer::params::SmbDialect;
|
||||||
|
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
pub struct InputSmbShare {
|
pub struct InputSmbShare {
|
||||||
@@ -85,3 +91,163 @@ impl AppComponent<Msg, NoUserEvent> for InputSmbWorkgroup {
|
|||||||
handle_input_ev(self, ev, on_key_down, on_key_up, form_tab)
|
handle_input_ev(self, ev, on_key_down, on_key_up, form_tab)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(all(test, posix))]
|
||||||
|
mod test {
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::filetransfer::params::SmbDialect;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_map_radio_options_to_dialect() {
|
||||||
|
assert_eq!(RadioSmbDialect::opt_to_dialect(0), SmbDialect::Auto);
|
||||||
|
assert_eq!(RadioSmbDialect::opt_to_dialect(1), SmbDialect::Smb1);
|
||||||
|
assert_eq!(RadioSmbDialect::opt_to_dialect(2), SmbDialect::Smb2);
|
||||||
|
assert_eq!(RadioSmbDialect::opt_to_dialect(3), SmbDialect::Smb3);
|
||||||
|
assert_eq!(RadioSmbDialect::opt_to_dialect(99), SmbDialect::Auto);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_map_dialect_to_radio_options() {
|
||||||
|
for dialect in [
|
||||||
|
SmbDialect::Auto,
|
||||||
|
SmbDialect::Smb1,
|
||||||
|
SmbDialect::Smb2,
|
||||||
|
SmbDialect::Smb3,
|
||||||
|
] {
|
||||||
|
let opt = RadioSmbDialect::dialect_to_opt(dialect);
|
||||||
|
assert_eq!(RadioSmbDialect::opt_to_dialect(opt), dialect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(posix)]
|
||||||
|
const RADIO_SMB_DIALECT_AUTO: usize = 0;
|
||||||
|
#[cfg(posix)]
|
||||||
|
const RADIO_SMB_DIALECT_SMB1: usize = 1;
|
||||||
|
#[cfg(posix)]
|
||||||
|
const RADIO_SMB_DIALECT_SMB2: usize = 2;
|
||||||
|
#[cfg(posix)]
|
||||||
|
const RADIO_SMB_DIALECT_SMB3: usize = 3;
|
||||||
|
|
||||||
|
/// Radio to select the SMB protocol family.
|
||||||
|
#[cfg(posix)]
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct RadioSmbDialect {
|
||||||
|
component: Radio,
|
||||||
|
form_tab: FormTab,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(posix)]
|
||||||
|
impl RadioSmbDialect {
|
||||||
|
pub fn new(dialect: SmbDialect, form_tab: FormTab, color: Color) -> Self {
|
||||||
|
Self {
|
||||||
|
component: Radio::default()
|
||||||
|
.highlight_style(
|
||||||
|
Style::default()
|
||||||
|
.fg(color)
|
||||||
|
.add_modifier(TextModifiers::REVERSED),
|
||||||
|
)
|
||||||
|
.borders(
|
||||||
|
Borders::default()
|
||||||
|
.color(color)
|
||||||
|
.modifiers(BorderType::Rounded),
|
||||||
|
)
|
||||||
|
.choices(["Auto", "SMB1 (insecure)", "SMB2", "SMB3"])
|
||||||
|
.rewind(true)
|
||||||
|
.title(Title::from("SMB version").alignment(HorizontalAlignment::Left))
|
||||||
|
.value(Self::dialect_to_opt(dialect)),
|
||||||
|
form_tab,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts the radio choice index to a dialect. Unknown indexes map to `Auto`.
|
||||||
|
pub fn opt_to_dialect(opt: usize) -> SmbDialect {
|
||||||
|
match opt {
|
||||||
|
RADIO_SMB_DIALECT_SMB1 => SmbDialect::Smb1,
|
||||||
|
RADIO_SMB_DIALECT_SMB2 => SmbDialect::Smb2,
|
||||||
|
RADIO_SMB_DIALECT_SMB3 => SmbDialect::Smb3,
|
||||||
|
_ => SmbDialect::Auto,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dialect_to_opt(dialect: SmbDialect) -> usize {
|
||||||
|
match dialect {
|
||||||
|
SmbDialect::Auto => RADIO_SMB_DIALECT_AUTO,
|
||||||
|
SmbDialect::Smb1 => RADIO_SMB_DIALECT_SMB1,
|
||||||
|
SmbDialect::Smb2 => RADIO_SMB_DIALECT_SMB2,
|
||||||
|
SmbDialect::Smb3 => RADIO_SMB_DIALECT_SMB3,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(posix)]
|
||||||
|
impl AppComponent<Msg, NoUserEvent> for RadioSmbDialect {
|
||||||
|
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||||
|
match ev {
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Left, ..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::Move(Direction::Left));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Right, ..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::Move(Direction::Right));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Enter, ..
|
||||||
|
}) => Some(Msg::Form(FormMsg::Connect)),
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Down, ..
|
||||||
|
}) => Some(if self.form_tab == FormTab::Remote {
|
||||||
|
Msg::Ui(UiMsg::Remote(UiAuthFormMsg::SmbDialectBlurDown))
|
||||||
|
} else {
|
||||||
|
Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::SmbDialectBlurDown))
|
||||||
|
}),
|
||||||
|
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
|
||||||
|
Some(if self.form_tab == FormTab::Remote {
|
||||||
|
Msg::Ui(UiMsg::Remote(UiAuthFormMsg::SmbDialectBlurUp))
|
||||||
|
} else {
|
||||||
|
Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::SmbDialectBlurUp))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => {
|
||||||
|
Some(if self.form_tab == FormTab::Remote {
|
||||||
|
Msg::Ui(UiMsg::Remote(UiAuthFormMsg::ParamsFormBlur))
|
||||||
|
} else {
|
||||||
|
Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::ParamsFormBlur))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One-line warning shown above the dialect radio while SMB1 is selected.
|
||||||
|
#[cfg(posix)]
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct SmbDialectWarning {
|
||||||
|
component: Span,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(posix)]
|
||||||
|
impl SmbDialectWarning {
|
||||||
|
pub fn new(color: Color) -> Self {
|
||||||
|
Self {
|
||||||
|
component: Span::default().foreground(color).spans([SpanStatic::from(
|
||||||
|
"Warning: SMB1 is deprecated and insecure. Use it only for isolated legacy devices.",
|
||||||
|
)]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(posix)]
|
||||||
|
impl AppComponent<Msg, NoUserEvent> for SmbDialectWarning {
|
||||||
|
fn on(&mut self, _ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ use tuirealm::state::{State, StateValue};
|
|||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
AuthActivity, AuthFormId, ExitReason, FormMsg, FormTab, HostBridgeProtocol, Id, InputMask, Msg,
|
AuthActivity, AuthFormId, ExitReason, FormMsg, FormTab, HostBridgeProtocol, Id, InputMask, Msg,
|
||||||
UiAuthFormMsg, UiMsg,
|
UiAuthFormMsg, UiMsg, should_resolve_ssh_host_params,
|
||||||
};
|
};
|
||||||
|
use crate::filetransfer::FileTransferProtocol;
|
||||||
|
use crate::utils::ssh::resolve_ssh_host_params;
|
||||||
|
|
||||||
impl AuthActivity {
|
impl AuthActivity {
|
||||||
pub(super) fn update(&mut self, msg: Option<Msg>) -> Option<Msg> {
|
pub(super) fn update(&mut self, msg: Option<Msg>) -> Option<Msg> {
|
||||||
@@ -24,19 +26,26 @@ impl AuthActivity {
|
|||||||
fn update_form(&mut self, msg: FormMsg) -> Option<Msg> {
|
fn update_form(&mut self, msg: FormMsg) -> Option<Msg> {
|
||||||
match msg {
|
match msg {
|
||||||
FormMsg::Connect => {
|
FormMsg::Connect => {
|
||||||
let Ok(remote_params) = self.collect_remote_host_params() else {
|
self.resolve_ssh_host_params_if_needed(FormTab::Remote, false);
|
||||||
// mount error
|
self.resolve_ssh_host_params_if_needed(FormTab::HostBridge, false);
|
||||||
self.mount_error("Invalid remote params parameters");
|
let remote_params = match self.collect_remote_host_params() {
|
||||||
return None;
|
Ok(remote_params) => remote_params,
|
||||||
|
Err(err) => {
|
||||||
|
// mount error
|
||||||
|
self.mount_error(format!("Invalid remote host parameters: {err}"));
|
||||||
|
return None;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let Ok(host_bridge_params) = self.collect_host_bridge_params() else {
|
|
||||||
// mount error
|
|
||||||
self.mount_error("Invalid host bridge params parameters");
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
|
|
||||||
debug!("Remote params: {:?}", remote_params);
|
debug!("Remote params: {:?}", remote_params);
|
||||||
|
|
||||||
|
let host_bridge_params = match self.collect_host_bridge_params() {
|
||||||
|
Ok(host_bridge_params) => host_bridge_params,
|
||||||
|
Err(err) => {
|
||||||
|
// mount error
|
||||||
|
self.mount_error(format!("Invalid host bridge parameters: {err}"));
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
debug!("Host bridge params: {:?}", host_bridge_params);
|
debug!("Host bridge params: {:?}", host_bridge_params);
|
||||||
|
|
||||||
self.save_recent();
|
self.save_recent();
|
||||||
@@ -137,24 +146,36 @@ impl AuthActivity {
|
|||||||
self.host_bridge_protocol = protocol;
|
self.host_bridge_protocol = protocol;
|
||||||
// Update port
|
// Update port
|
||||||
let port: u16 = self.get_input_port(FormTab::HostBridge);
|
let port: u16 = self.get_input_port(FormTab::HostBridge);
|
||||||
if let HostBridgeProtocol::Remote(remote_protocol) = protocol
|
if let HostBridgeProtocol::Remote(remote_protocol) = protocol {
|
||||||
&& Self::is_port_standard(port)
|
match remote_protocol {
|
||||||
{
|
FileTransferProtocol::Scp | FileTransferProtocol::Sftp => {
|
||||||
self.mount_port(
|
self.resolve_ssh_host_params_if_needed(FormTab::HostBridge, true);
|
||||||
FormTab::HostBridge,
|
}
|
||||||
Self::get_default_port_for_protocol(remote_protocol),
|
_ if Self::is_port_standard(port) => {
|
||||||
);
|
self.mount_port(
|
||||||
|
FormTab::HostBridge,
|
||||||
|
Self::get_default_port_for_protocol(remote_protocol),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FormMsg::RemoteProtocolChanged(protocol) => {
|
FormMsg::RemoteProtocolChanged(protocol) => {
|
||||||
self.remote_protocol = protocol;
|
self.remote_protocol = protocol;
|
||||||
// Update port
|
// Update port
|
||||||
let port: u16 = self.get_input_port(FormTab::Remote);
|
let port: u16 = self.get_input_port(FormTab::Remote);
|
||||||
if Self::is_port_standard(port) {
|
match protocol {
|
||||||
self.mount_port(
|
FileTransferProtocol::Scp | FileTransferProtocol::Sftp => {
|
||||||
FormTab::Remote,
|
self.resolve_ssh_host_params_if_needed(FormTab::Remote, true);
|
||||||
Self::get_default_port_for_protocol(protocol),
|
}
|
||||||
);
|
_ if Self::is_port_standard(port) => {
|
||||||
|
self.mount_port(
|
||||||
|
FormTab::Remote,
|
||||||
|
Self::get_default_port_for_protocol(protocol),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FormMsg::Quit => {
|
FormMsg::Quit => {
|
||||||
@@ -248,6 +269,7 @@ impl AuthActivity {
|
|||||||
fn update_host_bridge_ui(&mut self, msg: UiAuthFormMsg) {
|
fn update_host_bridge_ui(&mut self, msg: UiAuthFormMsg) {
|
||||||
match msg {
|
match msg {
|
||||||
UiAuthFormMsg::AddressBlurDown => {
|
UiAuthFormMsg::AddressBlurDown => {
|
||||||
|
self.resolve_ssh_host_params_if_needed(FormTab::HostBridge, false);
|
||||||
let id = if cfg!(windows) && self.host_bridge_input_mask() == InputMask::Smb {
|
let id = if cfg!(windows) && self.host_bridge_input_mask() == InputMask::Smb {
|
||||||
Id::HostBridge(AuthFormId::SmbShare)
|
Id::HostBridge(AuthFormId::SmbShare)
|
||||||
} else {
|
} else {
|
||||||
@@ -256,9 +278,11 @@ impl AuthActivity {
|
|||||||
self.activate_component(id);
|
self.activate_component(id);
|
||||||
}
|
}
|
||||||
UiAuthFormMsg::AddressBlurUp => {
|
UiAuthFormMsg::AddressBlurUp => {
|
||||||
|
self.resolve_ssh_host_params_if_needed(FormTab::HostBridge, false);
|
||||||
self.activate_component(Id::HostBridge(AuthFormId::Protocol));
|
self.activate_component(Id::HostBridge(AuthFormId::Protocol));
|
||||||
}
|
}
|
||||||
UiAuthFormMsg::ChangeFormTab => {
|
UiAuthFormMsg::ChangeFormTab => {
|
||||||
|
self.resolve_ssh_host_params_if_needed(FormTab::HostBridge, false);
|
||||||
self.last_form_tab = FormTab::Remote;
|
self.last_form_tab = FormTab::Remote;
|
||||||
self.activate_component(Id::Remote(AuthFormId::Protocol));
|
self.activate_component(Id::Remote(AuthFormId::Protocol));
|
||||||
}
|
}
|
||||||
@@ -273,6 +297,7 @@ impl AuthActivity {
|
|||||||
self.activate_component(id);
|
self.activate_component(id);
|
||||||
}
|
}
|
||||||
UiAuthFormMsg::ParamsFormBlur => {
|
UiAuthFormMsg::ParamsFormBlur => {
|
||||||
|
self.resolve_ssh_host_params_if_needed(FormTab::HostBridge, false);
|
||||||
self.activate_component(Id::BookmarksList);
|
self.activate_component(Id::BookmarksList);
|
||||||
}
|
}
|
||||||
UiAuthFormMsg::PasswordBlurDown => {
|
UiAuthFormMsg::PasswordBlurDown => {
|
||||||
@@ -333,7 +358,7 @@ impl AuthActivity {
|
|||||||
InputMask::Localhost => unreachable!(),
|
InputMask::Localhost => unreachable!(),
|
||||||
InputMask::Generic => Id::HostBridge(AuthFormId::Password),
|
InputMask::Generic => Id::HostBridge(AuthFormId::Password),
|
||||||
#[cfg(posix)]
|
#[cfg(posix)]
|
||||||
InputMask::Smb => Id::HostBridge(AuthFormId::SmbWorkgroup),
|
InputMask::Smb => Id::HostBridge(AuthFormId::SmbDialect),
|
||||||
#[cfg(win)]
|
#[cfg(win)]
|
||||||
InputMask::Smb => Id::HostBridge(AuthFormId::Password),
|
InputMask::Smb => Id::HostBridge(AuthFormId::Password),
|
||||||
InputMask::Kube => Id::HostBridge(AuthFormId::KubeClientKey),
|
InputMask::Kube => Id::HostBridge(AuthFormId::KubeClientKey),
|
||||||
@@ -458,12 +483,20 @@ impl AuthActivity {
|
|||||||
}
|
}
|
||||||
#[cfg(posix)]
|
#[cfg(posix)]
|
||||||
UiAuthFormMsg::SmbWorkgroupDown => {
|
UiAuthFormMsg::SmbWorkgroupDown => {
|
||||||
self.activate_component(Id::HostBridge(AuthFormId::RemoteDirectory))
|
self.activate_component(Id::HostBridge(AuthFormId::SmbDialect))
|
||||||
}
|
}
|
||||||
#[cfg(posix)]
|
#[cfg(posix)]
|
||||||
UiAuthFormMsg::SmbWorkgroupUp => {
|
UiAuthFormMsg::SmbWorkgroupUp => {
|
||||||
self.activate_component(Id::HostBridge(AuthFormId::Password))
|
self.activate_component(Id::HostBridge(AuthFormId::Password))
|
||||||
}
|
}
|
||||||
|
#[cfg(posix)]
|
||||||
|
UiAuthFormMsg::SmbDialectBlurDown => {
|
||||||
|
self.activate_component(Id::HostBridge(AuthFormId::RemoteDirectory))
|
||||||
|
}
|
||||||
|
#[cfg(posix)]
|
||||||
|
UiAuthFormMsg::SmbDialectBlurUp => {
|
||||||
|
self.activate_component(Id::HostBridge(AuthFormId::SmbWorkgroup))
|
||||||
|
}
|
||||||
UiAuthFormMsg::UsernameBlurDown => {
|
UiAuthFormMsg::UsernameBlurDown => {
|
||||||
self.activate_component(Id::HostBridge(AuthFormId::Password))
|
self.activate_component(Id::HostBridge(AuthFormId::Password))
|
||||||
}
|
}
|
||||||
@@ -491,6 +524,7 @@ impl AuthActivity {
|
|||||||
fn update_remote_ui(&mut self, msg: UiAuthFormMsg) {
|
fn update_remote_ui(&mut self, msg: UiAuthFormMsg) {
|
||||||
match msg {
|
match msg {
|
||||||
UiAuthFormMsg::AddressBlurDown => {
|
UiAuthFormMsg::AddressBlurDown => {
|
||||||
|
self.resolve_ssh_host_params_if_needed(FormTab::Remote, false);
|
||||||
let id = if cfg!(windows) && self.remote_input_mask() == InputMask::Smb {
|
let id = if cfg!(windows) && self.remote_input_mask() == InputMask::Smb {
|
||||||
Id::Remote(AuthFormId::SmbShare)
|
Id::Remote(AuthFormId::SmbShare)
|
||||||
} else {
|
} else {
|
||||||
@@ -499,9 +533,11 @@ impl AuthActivity {
|
|||||||
self.activate_component(id);
|
self.activate_component(id);
|
||||||
}
|
}
|
||||||
UiAuthFormMsg::AddressBlurUp => {
|
UiAuthFormMsg::AddressBlurUp => {
|
||||||
|
self.resolve_ssh_host_params_if_needed(FormTab::Remote, false);
|
||||||
self.activate_component(Id::Remote(AuthFormId::Protocol));
|
self.activate_component(Id::Remote(AuthFormId::Protocol));
|
||||||
}
|
}
|
||||||
UiAuthFormMsg::ChangeFormTab => {
|
UiAuthFormMsg::ChangeFormTab => {
|
||||||
|
self.resolve_ssh_host_params_if_needed(FormTab::Remote, false);
|
||||||
self.last_form_tab = FormTab::HostBridge;
|
self.last_form_tab = FormTab::HostBridge;
|
||||||
self.activate_component(Id::HostBridge(AuthFormId::Protocol));
|
self.activate_component(Id::HostBridge(AuthFormId::Protocol));
|
||||||
}
|
}
|
||||||
@@ -512,6 +548,7 @@ impl AuthActivity {
|
|||||||
self.activate_component(Id::Remote(AuthFormId::RemoteDirectory));
|
self.activate_component(Id::Remote(AuthFormId::RemoteDirectory));
|
||||||
}
|
}
|
||||||
UiAuthFormMsg::ParamsFormBlur => {
|
UiAuthFormMsg::ParamsFormBlur => {
|
||||||
|
self.resolve_ssh_host_params_if_needed(FormTab::Remote, false);
|
||||||
self.activate_component(Id::BookmarksList);
|
self.activate_component(Id::BookmarksList);
|
||||||
}
|
}
|
||||||
UiAuthFormMsg::PasswordBlurDown => {
|
UiAuthFormMsg::PasswordBlurDown => {
|
||||||
@@ -572,7 +609,7 @@ impl AuthActivity {
|
|||||||
InputMask::Localhost => unreachable!(),
|
InputMask::Localhost => unreachable!(),
|
||||||
InputMask::Generic => Id::Remote(AuthFormId::Password),
|
InputMask::Generic => Id::Remote(AuthFormId::Password),
|
||||||
#[cfg(posix)]
|
#[cfg(posix)]
|
||||||
InputMask::Smb => Id::Remote(AuthFormId::SmbWorkgroup),
|
InputMask::Smb => Id::Remote(AuthFormId::SmbDialect),
|
||||||
#[cfg(win)]
|
#[cfg(win)]
|
||||||
InputMask::Smb => Id::Remote(AuthFormId::Password),
|
InputMask::Smb => Id::Remote(AuthFormId::Password),
|
||||||
InputMask::Kube => Id::Remote(AuthFormId::KubeClientKey),
|
InputMask::Kube => Id::Remote(AuthFormId::KubeClientKey),
|
||||||
@@ -697,12 +734,20 @@ impl AuthActivity {
|
|||||||
}
|
}
|
||||||
#[cfg(posix)]
|
#[cfg(posix)]
|
||||||
UiAuthFormMsg::SmbWorkgroupDown => {
|
UiAuthFormMsg::SmbWorkgroupDown => {
|
||||||
self.activate_component(Id::Remote(AuthFormId::RemoteDirectory))
|
self.activate_component(Id::Remote(AuthFormId::SmbDialect))
|
||||||
}
|
}
|
||||||
#[cfg(posix)]
|
#[cfg(posix)]
|
||||||
UiAuthFormMsg::SmbWorkgroupUp => {
|
UiAuthFormMsg::SmbWorkgroupUp => {
|
||||||
self.activate_component(Id::Remote(AuthFormId::Password))
|
self.activate_component(Id::Remote(AuthFormId::Password))
|
||||||
}
|
}
|
||||||
|
#[cfg(posix)]
|
||||||
|
UiAuthFormMsg::SmbDialectBlurDown => {
|
||||||
|
self.activate_component(Id::Remote(AuthFormId::RemoteDirectory))
|
||||||
|
}
|
||||||
|
#[cfg(posix)]
|
||||||
|
UiAuthFormMsg::SmbDialectBlurUp => {
|
||||||
|
self.activate_component(Id::Remote(AuthFormId::SmbWorkgroup))
|
||||||
|
}
|
||||||
UiAuthFormMsg::UsernameBlurDown => {
|
UiAuthFormMsg::UsernameBlurDown => {
|
||||||
self.activate_component(Id::Remote(AuthFormId::Password))
|
self.activate_component(Id::Remote(AuthFormId::Password))
|
||||||
}
|
}
|
||||||
@@ -727,6 +772,35 @@ impl AuthActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn resolve_ssh_host_params_if_needed(&mut self, form_tab: FormTab, force: bool) {
|
||||||
|
let protocol = match form_tab {
|
||||||
|
FormTab::HostBridge => match self.host_bridge_protocol {
|
||||||
|
HostBridgeProtocol::Localhost => return,
|
||||||
|
HostBridgeProtocol::Remote(protocol) => protocol,
|
||||||
|
},
|
||||||
|
FormTab::Remote => self.remote_protocol,
|
||||||
|
};
|
||||||
|
let address = self.get_input_addr(form_tab);
|
||||||
|
let should_resolve = should_resolve_ssh_host_params(
|
||||||
|
protocol,
|
||||||
|
self.last_mounted_address(form_tab),
|
||||||
|
address.as_str(),
|
||||||
|
force,
|
||||||
|
);
|
||||||
|
if !should_resolve {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let params = resolve_ssh_host_params(self.context().ssh_config(), address.as_str());
|
||||||
|
self.mount_port(form_tab, 22);
|
||||||
|
self.mount_username(form_tab, "");
|
||||||
|
self.mount_port(form_tab, params.port);
|
||||||
|
if let Some(username) = params.username {
|
||||||
|
self.mount_username(form_tab, username.as_str());
|
||||||
|
}
|
||||||
|
self.set_last_mounted_address(form_tab, address.as_str());
|
||||||
|
}
|
||||||
|
|
||||||
fn activate_component(&mut self, id: Id) {
|
fn activate_component(&mut self, id: Id) {
|
||||||
if let Err(err) = self.app.active(&id) {
|
if let Err(err) = self.app.active(&id) {
|
||||||
error!("Failed to activate component: {err}");
|
error!("Failed to activate component: {err}");
|
||||||
|
|||||||
+156
-58
@@ -3,7 +3,7 @@
|
|||||||
//! `auth_activity` is the module which implements the authentication activity
|
//! `auth_activity` is the module which implements the authentication activity
|
||||||
|
|
||||||
use tuirealm::props::Color;
|
use tuirealm::props::Color;
|
||||||
use tuirealm::ratatui::layout::{Constraint, Direction, Layout};
|
use tuirealm::ratatui::layout::{Constraint, Direction, Layout, Rect};
|
||||||
use tuirealm::ratatui::widgets::Clear;
|
use tuirealm::ratatui::widgets::Clear;
|
||||||
use tuirealm::terminal::TerminalAdapter;
|
use tuirealm::terminal::TerminalAdapter;
|
||||||
|
|
||||||
@@ -12,6 +12,8 @@ use super::{
|
|||||||
InputMask, components,
|
InputMask, components,
|
||||||
};
|
};
|
||||||
use crate::filetransfer::params::DEFAULT_GCS_ENDPOINT;
|
use crate::filetransfer::params::DEFAULT_GCS_ENDPOINT;
|
||||||
|
#[cfg(posix)]
|
||||||
|
use crate::filetransfer::params::SmbDialect;
|
||||||
use crate::utils::ui::{Popup, Size};
|
use crate::utils::ui::{Popup, Size};
|
||||||
|
|
||||||
#[path = "view/mounting.rs"]
|
#[path = "view/mounting.rs"]
|
||||||
@@ -73,6 +75,10 @@ impl AuthActivity {
|
|||||||
self.mount_smb_share(FormTab::HostBridge, "");
|
self.mount_smb_share(FormTab::HostBridge, "");
|
||||||
#[cfg(posix)]
|
#[cfg(posix)]
|
||||||
self.mount_smb_workgroup(FormTab::HostBridge, "");
|
self.mount_smb_workgroup(FormTab::HostBridge, "");
|
||||||
|
#[cfg(posix)]
|
||||||
|
self.mount_smb_dialect(FormTab::HostBridge, SmbDialect::default());
|
||||||
|
#[cfg(posix)]
|
||||||
|
self.mount_smb_dialect_warning(FormTab::HostBridge);
|
||||||
self.mount_webdav_uri(FormTab::HostBridge, "");
|
self.mount_webdav_uri(FormTab::HostBridge, "");
|
||||||
|
|
||||||
let remote_default_protocol = self.context().config().get_default_protocol();
|
let remote_default_protocol = self.context().config().get_default_protocol();
|
||||||
@@ -107,6 +113,10 @@ impl AuthActivity {
|
|||||||
self.mount_smb_share(FormTab::Remote, "");
|
self.mount_smb_share(FormTab::Remote, "");
|
||||||
#[cfg(posix)]
|
#[cfg(posix)]
|
||||||
self.mount_smb_workgroup(FormTab::Remote, "");
|
self.mount_smb_workgroup(FormTab::Remote, "");
|
||||||
|
#[cfg(posix)]
|
||||||
|
self.mount_smb_dialect(FormTab::Remote, SmbDialect::default());
|
||||||
|
#[cfg(posix)]
|
||||||
|
self.mount_smb_dialect_warning(FormTab::Remote);
|
||||||
self.mount_webdav_uri(FormTab::Remote, "");
|
self.mount_webdav_uri(FormTab::Remote, "");
|
||||||
|
|
||||||
if let Some(version) = self
|
if let Some(version) = self
|
||||||
@@ -247,8 +257,10 @@ impl AuthActivity {
|
|||||||
f: &mut tuirealm::ratatui::Frame<'_>,
|
f: &mut tuirealm::ratatui::Frame<'_>,
|
||||||
area: tuirealm::ratatui::layout::Rect,
|
area: tuirealm::ratatui::layout::Rect,
|
||||||
) {
|
) {
|
||||||
|
let input_mask_size = Self::input_mask_size(self.host_bridge_input_mask());
|
||||||
|
let input_mask = self.host_bridge_input_mask();
|
||||||
let protocol_and_mask_chunks = Layout::default()
|
let protocol_and_mask_chunks = Layout::default()
|
||||||
.constraints([Constraint::Length(3), Constraint::Length(12)].as_ref())
|
.constraints([Constraint::Length(3), Constraint::Length(input_mask_size)].as_ref())
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.split(area);
|
.split(area);
|
||||||
|
|
||||||
@@ -258,36 +270,25 @@ impl AuthActivity {
|
|||||||
protocol_and_mask_chunks[0],
|
protocol_and_mask_chunks[0],
|
||||||
);
|
);
|
||||||
|
|
||||||
let input_mask = Layout::default()
|
let view_ids = match input_mask {
|
||||||
.constraints(
|
InputMask::AwsS3 => self.get_host_bridge_s3_view(),
|
||||||
[
|
InputMask::Gcs => self.get_host_bridge_gcs_view(),
|
||||||
Constraint::Length(3),
|
InputMask::Generic => self.get_host_bridge_generic_params_view(),
|
||||||
Constraint::Length(3),
|
InputMask::Kube => self.get_host_bridge_kube_view(),
|
||||||
Constraint::Length(3),
|
|
||||||
Constraint::Length(3),
|
|
||||||
]
|
|
||||||
.as_ref(),
|
|
||||||
)
|
|
||||||
.direction(Direction::Vertical)
|
|
||||||
.split(protocol_and_mask_chunks[1]);
|
|
||||||
match self.host_bridge_input_mask() {
|
|
||||||
InputMask::AwsS3 => self.render_view_ids(f, input_mask, self.get_host_bridge_s3_view()),
|
|
||||||
InputMask::Gcs => self.render_view_ids(f, input_mask, self.get_host_bridge_gcs_view()),
|
|
||||||
InputMask::Generic => {
|
|
||||||
self.render_view_ids(f, input_mask, self.get_host_bridge_generic_params_view())
|
|
||||||
}
|
|
||||||
InputMask::Kube => {
|
|
||||||
self.render_view_ids(f, input_mask, self.get_host_bridge_kube_view())
|
|
||||||
}
|
|
||||||
InputMask::Localhost => {
|
InputMask::Localhost => {
|
||||||
let view_ids = self.get_host_bridge_localhost_view();
|
let view_ids = self.get_host_bridge_localhost_view();
|
||||||
self.app.view(&view_ids[0], f, input_mask[0]);
|
self.app.view(&view_ids[0], f, protocol_and_mask_chunks[1]);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
InputMask::Smb => self.render_view_ids(f, input_mask, self.get_host_bridge_smb_view()),
|
InputMask::Smb => self.get_host_bridge_smb_view(),
|
||||||
InputMask::WebDAV => {
|
InputMask::WebDAV => self.get_host_bridge_webdav_view(),
|
||||||
self.render_view_ids(f, input_mask, self.get_host_bridge_webdav_view())
|
};
|
||||||
}
|
self.render_form_rows(
|
||||||
}
|
f,
|
||||||
|
protocol_and_mask_chunks[1],
|
||||||
|
FormTab::HostBridge,
|
||||||
|
view_ids,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_remote_input_mask(
|
fn render_remote_input_mask(
|
||||||
@@ -295,8 +296,10 @@ impl AuthActivity {
|
|||||||
f: &mut tuirealm::ratatui::Frame<'_>,
|
f: &mut tuirealm::ratatui::Frame<'_>,
|
||||||
area: tuirealm::ratatui::layout::Rect,
|
area: tuirealm::ratatui::layout::Rect,
|
||||||
) {
|
) {
|
||||||
|
let input_mask_size = Self::input_mask_size(self.remote_input_mask());
|
||||||
|
let input_mask = self.remote_input_mask();
|
||||||
let protocol_and_mask_chunks = Layout::default()
|
let protocol_and_mask_chunks = Layout::default()
|
||||||
.constraints([Constraint::Length(3), Constraint::Length(12)].as_ref())
|
.constraints([Constraint::Length(3), Constraint::Length(input_mask_size)].as_ref())
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.split(area);
|
.split(area);
|
||||||
|
|
||||||
@@ -306,40 +309,135 @@ impl AuthActivity {
|
|||||||
protocol_and_mask_chunks[0],
|
protocol_and_mask_chunks[0],
|
||||||
);
|
);
|
||||||
|
|
||||||
let input_mask = Layout::default()
|
let view_ids = match input_mask {
|
||||||
.constraints(
|
InputMask::AwsS3 => self.get_remote_s3_view(),
|
||||||
[
|
InputMask::Gcs => self.get_remote_gcs_view(),
|
||||||
Constraint::Length(3),
|
InputMask::Generic => self.get_remote_generic_params_view(),
|
||||||
Constraint::Length(3),
|
InputMask::Kube => self.get_remote_kube_view(),
|
||||||
Constraint::Length(3),
|
|
||||||
Constraint::Length(3),
|
|
||||||
]
|
|
||||||
.as_ref(),
|
|
||||||
)
|
|
||||||
.direction(Direction::Vertical)
|
|
||||||
.split(protocol_and_mask_chunks[1]);
|
|
||||||
match self.remote_input_mask() {
|
|
||||||
InputMask::AwsS3 => self.render_view_ids(f, input_mask, self.get_remote_s3_view()),
|
|
||||||
InputMask::Gcs => self.render_view_ids(f, input_mask, self.get_remote_gcs_view()),
|
|
||||||
InputMask::Generic => {
|
|
||||||
self.render_view_ids(f, input_mask, self.get_remote_generic_params_view())
|
|
||||||
}
|
|
||||||
InputMask::Kube => self.render_view_ids(f, input_mask, self.get_remote_kube_view()),
|
|
||||||
InputMask::Localhost => unreachable!(),
|
InputMask::Localhost => unreachable!(),
|
||||||
InputMask::Smb => self.render_view_ids(f, input_mask, self.get_remote_smb_view()),
|
InputMask::Smb => self.get_remote_smb_view(),
|
||||||
InputMask::WebDAV => self.render_view_ids(f, input_mask, self.get_remote_webdav_view()),
|
InputMask::WebDAV => self.get_remote_webdav_view(),
|
||||||
}
|
};
|
||||||
|
self.render_form_rows(f, protocol_and_mask_chunks[1], FormTab::Remote, view_ids);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_view_ids(
|
/// Splits `area` into four 3-line form rows. When `warning_row` is
|
||||||
|
/// `Some(index)`, a 1-line row is inserted directly above row `index` and
|
||||||
|
/// returned as the second tuple element.
|
||||||
|
fn split_input_mask(area: Rect, warning_row: Option<usize>) -> ([Rect; 4], Option<Rect>) {
|
||||||
|
let mut constraints = Vec::with_capacity(6);
|
||||||
|
for row in 0..4 {
|
||||||
|
if warning_row == Some(row) {
|
||||||
|
constraints.push(Constraint::Length(1));
|
||||||
|
}
|
||||||
|
constraints.push(Constraint::Length(3));
|
||||||
|
}
|
||||||
|
constraints.push(Constraint::Min(0));
|
||||||
|
let chunks = Layout::default()
|
||||||
|
.constraints(constraints)
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.split(area);
|
||||||
|
|
||||||
|
let mut rows = [Rect::default(); 4];
|
||||||
|
let mut warning = None;
|
||||||
|
let mut chunk = 0;
|
||||||
|
for (row, slot) in rows.iter_mut().enumerate() {
|
||||||
|
if warning_row == Some(row) {
|
||||||
|
warning = Some(chunks[chunk]);
|
||||||
|
chunk += 1;
|
||||||
|
}
|
||||||
|
*slot = chunks[chunk];
|
||||||
|
chunk += 1;
|
||||||
|
}
|
||||||
|
(rows, warning)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the visible row index of the SMB dialect radio when the form
|
||||||
|
/// shows SMB and SMB1 is selected; `None` otherwise.
|
||||||
|
#[cfg(posix)]
|
||||||
|
fn smb_dialect_warning_row(&self, form_tab: FormTab, view_ids: &[Id; 4]) -> Option<usize> {
|
||||||
|
let input_mask = match form_tab {
|
||||||
|
FormTab::HostBridge => self.host_bridge_input_mask(),
|
||||||
|
FormTab::Remote => self.remote_input_mask(),
|
||||||
|
};
|
||||||
|
if input_mask != InputMask::Smb || self.get_input_smb_dialect(form_tab) != SmbDialect::Smb1
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let dialect_id = Self::form_tab_id(form_tab, AuthFormId::SmbDialect);
|
||||||
|
view_ids.iter().position(|id| *id == dialect_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(win)]
|
||||||
|
fn smb_dialect_warning_row(&self, _form_tab: FormTab, _view_ids: &[Id; 4]) -> Option<usize> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_form_rows(
|
||||||
&mut self,
|
&mut self,
|
||||||
f: &mut tuirealm::ratatui::Frame<'_>,
|
f: &mut tuirealm::ratatui::Frame<'_>,
|
||||||
input_mask: std::rc::Rc<[tuirealm::ratatui::layout::Rect]>,
|
area: Rect,
|
||||||
|
form_tab: FormTab,
|
||||||
view_ids: [Id; 4],
|
view_ids: [Id; 4],
|
||||||
) {
|
) {
|
||||||
self.app.view(&view_ids[0], f, input_mask[0]);
|
let warning_row = self.smb_dialect_warning_row(form_tab, &view_ids);
|
||||||
self.app.view(&view_ids[1], f, input_mask[1]);
|
let (rows, warning) = Self::split_input_mask(area, warning_row);
|
||||||
self.app.view(&view_ids[2], f, input_mask[2]);
|
#[cfg(posix)]
|
||||||
self.app.view(&view_ids[3], f, input_mask[3]);
|
if let Some(rect) = warning {
|
||||||
|
let id = Self::form_tab_id(form_tab, AuthFormId::SmbDialectWarning);
|
||||||
|
self.app.view(&id, f, rect);
|
||||||
|
}
|
||||||
|
#[cfg(win)]
|
||||||
|
let _ = warning;
|
||||||
|
for (id, rect) in view_ids.iter().zip(rows) {
|
||||||
|
self.app.view(id, f, rect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
use tuirealm::ratatui::layout::Rect;
|
||||||
|
|
||||||
|
use super::AuthActivity;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_split_input_mask_without_warning() {
|
||||||
|
let area = Rect::new(0, 0, 40, 13);
|
||||||
|
|
||||||
|
let (rows, warning) = AuthActivity::split_input_mask(area, None);
|
||||||
|
|
||||||
|
assert_eq!(warning, None);
|
||||||
|
assert_eq!(rows[0], Rect::new(0, 0, 40, 3));
|
||||||
|
assert_eq!(rows[1], Rect::new(0, 3, 40, 3));
|
||||||
|
assert_eq!(rows[2], Rect::new(0, 6, 40, 3));
|
||||||
|
assert_eq!(rows[3], Rect::new(0, 9, 40, 3));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_split_input_mask_with_middle_warning() {
|
||||||
|
let area = Rect::new(0, 0, 40, 13);
|
||||||
|
|
||||||
|
let (rows, warning) = AuthActivity::split_input_mask(area, Some(2));
|
||||||
|
|
||||||
|
assert_eq!(warning, Some(Rect::new(0, 6, 40, 1)));
|
||||||
|
assert_eq!(rows[0], Rect::new(0, 0, 40, 3));
|
||||||
|
assert_eq!(rows[1], Rect::new(0, 3, 40, 3));
|
||||||
|
assert_eq!(rows[2], Rect::new(0, 7, 40, 3));
|
||||||
|
assert_eq!(rows[3], Rect::new(0, 10, 40, 3));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_split_input_mask_with_first_row_warning() {
|
||||||
|
let area = Rect::new(0, 0, 40, 13);
|
||||||
|
|
||||||
|
let (rows, warning) = AuthActivity::split_input_mask(area, Some(0));
|
||||||
|
|
||||||
|
assert_eq!(warning, Some(Rect::new(0, 0, 40, 1)));
|
||||||
|
assert_eq!(rows[0], Rect::new(0, 1, 40, 3));
|
||||||
|
assert_eq!(rows[1], Rect::new(0, 4, 40, 3));
|
||||||
|
assert_eq!(rows[2], Rect::new(0, 7, 40, 3));
|
||||||
|
assert_eq!(rows[3], Rect::new(0, 10, 40, 3));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
#[cfg(posix)]
|
||||||
|
use crate::filetransfer::params::SmbDialect;
|
||||||
use crate::ui::activities::auth::STORE_KEY_RELEASE_NOTES;
|
use crate::ui::activities::auth::STORE_KEY_RELEASE_NOTES;
|
||||||
|
|
||||||
impl AuthActivity {
|
impl AuthActivity {
|
||||||
@@ -320,6 +322,7 @@ impl AuthActivity {
|
|||||||
form_tab: FormTab,
|
form_tab: FormTab,
|
||||||
address: &str,
|
address: &str,
|
||||||
) {
|
) {
|
||||||
|
self.set_last_mounted_address(form_tab, address);
|
||||||
let addr_color = self.theme().auth_address;
|
let addr_color = self.theme().auth_address;
|
||||||
let id = Self::form_tab_id(form_tab, AuthFormId::Address);
|
let id = Self::form_tab_id(form_tab, AuthFormId::Address);
|
||||||
if let Err(err) = self.app.remount(
|
if let Err(err) = self.app.remount(
|
||||||
@@ -704,6 +707,36 @@ impl AuthActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(posix)]
|
||||||
|
pub(in crate::ui::activities::auth) fn mount_smb_dialect(
|
||||||
|
&mut self,
|
||||||
|
form_tab: FormTab,
|
||||||
|
dialect: SmbDialect,
|
||||||
|
) {
|
||||||
|
let color = self.theme().auth_protocol;
|
||||||
|
let id = Self::form_tab_id(form_tab, AuthFormId::SmbDialect);
|
||||||
|
if let Err(err) = self.app.remount(
|
||||||
|
id,
|
||||||
|
Box::new(components::RadioSmbDialect::new(dialect, form_tab, color)),
|
||||||
|
vec![],
|
||||||
|
) {
|
||||||
|
error!("Failed to remount component: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(posix)]
|
||||||
|
pub(in crate::ui::activities::auth) fn mount_smb_dialect_warning(&mut self, form_tab: FormTab) {
|
||||||
|
let color = self.theme().misc_warn_dialog;
|
||||||
|
let id = Self::form_tab_id(form_tab, AuthFormId::SmbDialectWarning);
|
||||||
|
if let Err(err) = self.app.remount(
|
||||||
|
id,
|
||||||
|
Box::new(components::SmbDialectWarning::new(color)),
|
||||||
|
vec![],
|
||||||
|
) {
|
||||||
|
error!("Failed to remount component: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(in crate::ui::activities::auth) fn mount_webdav_uri(
|
pub(in crate::ui::activities::auth) fn mount_webdav_uri(
|
||||||
&mut self,
|
&mut self,
|
||||||
form_tab: FormTab,
|
form_tab: FormTab,
|
||||||
|
|||||||
@@ -5,10 +5,14 @@ use tuirealm::state::{State, StateValue};
|
|||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::filetransfer::FileTransferParams;
|
use crate::filetransfer::FileTransferParams;
|
||||||
|
#[cfg(posix)]
|
||||||
|
use crate::filetransfer::params::SmbDialect;
|
||||||
use crate::filetransfer::params::{
|
use crate::filetransfer::params::{
|
||||||
AwsS3Params, GenericProtocolParams, GoogleCloudStorageParams, KubeProtocolParams,
|
AwsS3Params, GenericProtocolParams, GoogleCloudStorageParams, KubeProtocolParams,
|
||||||
ProtocolParams, SmbParams, WebDAVProtocolParams,
|
ProtocolParams, SmbParams, WebDAVProtocolParams,
|
||||||
};
|
};
|
||||||
|
#[cfg(posix)]
|
||||||
|
use crate::ui::activities::auth::components::RadioSmbDialect;
|
||||||
|
|
||||||
impl AuthActivity {
|
impl AuthActivity {
|
||||||
pub(in crate::ui::activities::auth) fn get_generic_params_input(
|
pub(in crate::ui::activities::auth) fn get_generic_params_input(
|
||||||
@@ -82,6 +86,7 @@ impl AuthActivity {
|
|||||||
) -> SmbParams {
|
) -> SmbParams {
|
||||||
let share = self.get_input_smb_share(form_tab);
|
let share = self.get_input_smb_share(form_tab);
|
||||||
let workgroup = self.get_input_smb_workgroup(form_tab);
|
let workgroup = self.get_input_smb_workgroup(form_tab);
|
||||||
|
let dialect = self.get_input_smb_dialect(form_tab);
|
||||||
let address = self.get_input_addr(form_tab);
|
let address = self.get_input_addr(form_tab);
|
||||||
let port = self.get_input_port(form_tab);
|
let port = self.get_input_port(form_tab);
|
||||||
let username = self.get_input_username(form_tab);
|
let username = self.get_input_username(form_tab);
|
||||||
@@ -92,6 +97,7 @@ impl AuthActivity {
|
|||||||
.username(username)
|
.username(username)
|
||||||
.password(password)
|
.password(password)
|
||||||
.workgroup(workgroup)
|
.workgroup(workgroup)
|
||||||
|
.dialect(dialect)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(win)]
|
#[cfg(win)]
|
||||||
@@ -452,6 +458,20 @@ impl AuthActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(posix)]
|
||||||
|
pub(in crate::ui::activities::auth) fn get_input_smb_dialect(
|
||||||
|
&self,
|
||||||
|
form_tab: FormTab,
|
||||||
|
) -> SmbDialect {
|
||||||
|
match self
|
||||||
|
.app
|
||||||
|
.state(&Self::form_tab_id(form_tab, AuthFormId::SmbDialect))
|
||||||
|
{
|
||||||
|
Ok(State::Single(StateValue::Usize(opt))) => RadioSmbDialect::opt_to_dialect(opt),
|
||||||
|
_ => SmbDialect::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(in crate::ui::activities::auth) fn get_new_bookmark(&self) -> (String, bool) {
|
pub(in crate::ui::activities::auth) fn get_new_bookmark(&self) -> (String, bool) {
|
||||||
let name = match self.app.state(&Id::BookmarkName) {
|
let name = match self.app.state(&Id::BookmarkName) {
|
||||||
Ok(State::Single(StateValue::String(name))) => name,
|
Ok(State::Single(StateValue::String(name))) => name,
|
||||||
@@ -473,8 +493,10 @@ impl AuthActivity {
|
|||||||
+ 3
|
+ 3
|
||||||
}
|
}
|
||||||
|
|
||||||
fn input_mask_size(input_mask: InputMask) -> u16 {
|
pub(in crate::ui::activities::auth) fn input_mask_size(input_mask: InputMask) -> u16 {
|
||||||
match input_mask {
|
match input_mask {
|
||||||
|
// One extra line for the SMB1 warning above the dialect radio.
|
||||||
|
InputMask::Smb if cfg!(posix) => 13,
|
||||||
InputMask::AwsS3
|
InputMask::AwsS3
|
||||||
| InputMask::Gcs
|
| InputMask::Gcs
|
||||||
| InputMask::Generic
|
| InputMask::Generic
|
||||||
|
|||||||
@@ -289,15 +289,21 @@ impl AuthActivity {
|
|||||||
Id::HostBridge(AuthFormId::Password),
|
Id::HostBridge(AuthFormId::Password),
|
||||||
Id::HostBridge(AuthFormId::SmbWorkgroup),
|
Id::HostBridge(AuthFormId::SmbWorkgroup),
|
||||||
],
|
],
|
||||||
Some(&Id::HostBridge(AuthFormId::RemoteDirectory)) => [
|
Some(&Id::HostBridge(AuthFormId::SmbDialect)) => [
|
||||||
Id::HostBridge(AuthFormId::Username),
|
Id::HostBridge(AuthFormId::Username),
|
||||||
Id::HostBridge(AuthFormId::Password),
|
Id::HostBridge(AuthFormId::Password),
|
||||||
Id::HostBridge(AuthFormId::SmbWorkgroup),
|
Id::HostBridge(AuthFormId::SmbWorkgroup),
|
||||||
|
Id::HostBridge(AuthFormId::SmbDialect),
|
||||||
|
],
|
||||||
|
Some(&Id::HostBridge(AuthFormId::RemoteDirectory)) => [
|
||||||
|
Id::HostBridge(AuthFormId::Password),
|
||||||
|
Id::HostBridge(AuthFormId::SmbWorkgroup),
|
||||||
|
Id::HostBridge(AuthFormId::SmbDialect),
|
||||||
Id::HostBridge(AuthFormId::RemoteDirectory),
|
Id::HostBridge(AuthFormId::RemoteDirectory),
|
||||||
],
|
],
|
||||||
Some(&Id::HostBridge(AuthFormId::LocalDirectory)) => [
|
Some(&Id::HostBridge(AuthFormId::LocalDirectory)) => [
|
||||||
Id::HostBridge(AuthFormId::Password),
|
|
||||||
Id::HostBridge(AuthFormId::SmbWorkgroup),
|
Id::HostBridge(AuthFormId::SmbWorkgroup),
|
||||||
|
Id::HostBridge(AuthFormId::SmbDialect),
|
||||||
Id::HostBridge(AuthFormId::RemoteDirectory),
|
Id::HostBridge(AuthFormId::RemoteDirectory),
|
||||||
Id::HostBridge(AuthFormId::LocalDirectory),
|
Id::HostBridge(AuthFormId::LocalDirectory),
|
||||||
],
|
],
|
||||||
@@ -336,15 +342,21 @@ impl AuthActivity {
|
|||||||
Id::Remote(AuthFormId::Password),
|
Id::Remote(AuthFormId::Password),
|
||||||
Id::Remote(AuthFormId::SmbWorkgroup),
|
Id::Remote(AuthFormId::SmbWorkgroup),
|
||||||
],
|
],
|
||||||
Some(&Id::Remote(AuthFormId::RemoteDirectory)) => [
|
Some(&Id::Remote(AuthFormId::SmbDialect)) => [
|
||||||
Id::Remote(AuthFormId::Username),
|
Id::Remote(AuthFormId::Username),
|
||||||
Id::Remote(AuthFormId::Password),
|
Id::Remote(AuthFormId::Password),
|
||||||
Id::Remote(AuthFormId::SmbWorkgroup),
|
Id::Remote(AuthFormId::SmbWorkgroup),
|
||||||
|
Id::Remote(AuthFormId::SmbDialect),
|
||||||
|
],
|
||||||
|
Some(&Id::Remote(AuthFormId::RemoteDirectory)) => [
|
||||||
|
Id::Remote(AuthFormId::Password),
|
||||||
|
Id::Remote(AuthFormId::SmbWorkgroup),
|
||||||
|
Id::Remote(AuthFormId::SmbDialect),
|
||||||
Id::Remote(AuthFormId::RemoteDirectory),
|
Id::Remote(AuthFormId::RemoteDirectory),
|
||||||
],
|
],
|
||||||
Some(&Id::Remote(AuthFormId::LocalDirectory)) => [
|
Some(&Id::Remote(AuthFormId::LocalDirectory)) => [
|
||||||
Id::Remote(AuthFormId::Password),
|
|
||||||
Id::Remote(AuthFormId::SmbWorkgroup),
|
Id::Remote(AuthFormId::SmbWorkgroup),
|
||||||
|
Id::Remote(AuthFormId::SmbDialect),
|
||||||
Id::Remote(AuthFormId::RemoteDirectory),
|
Id::Remote(AuthFormId::RemoteDirectory),
|
||||||
Id::Remote(AuthFormId::LocalDirectory),
|
Id::Remote(AuthFormId::LocalDirectory),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
//!
|
//!
|
||||||
//! `Context` is the module which provides all the functionalities related to the UI data holder, called Context
|
//! `Context` is the module which provides all the functionalities related to the UI data holder, called Context
|
||||||
|
|
||||||
|
use ssh2_config::SshConfig;
|
||||||
// Locals
|
// Locals
|
||||||
use tuirealm::terminal::{CrosstermTerminalAdapter, TerminalAdapter};
|
use tuirealm::terminal::{CrosstermTerminalAdapter, TerminalAdapter};
|
||||||
|
|
||||||
@@ -13,13 +14,23 @@ use crate::system::theme_provider::ThemeProvider;
|
|||||||
|
|
||||||
/// Context holds data structures shared by the activities
|
/// Context holds data structures shared by the activities
|
||||||
pub struct Context {
|
pub struct Context {
|
||||||
|
/// Parameters used to build the host bridge.
|
||||||
host_bridge_params: Option<HostBridgeParams>,
|
host_bridge_params: Option<HostBridgeParams>,
|
||||||
|
/// Parameters used to build the remote file transfer client.
|
||||||
remote_params: Option<FileTransferParams>,
|
remote_params: Option<FileTransferParams>,
|
||||||
|
/// Client for persistent bookmarks, when initialization succeeded.
|
||||||
bookmarks_client: Option<BookmarksClient>,
|
bookmarks_client: Option<BookmarksClient>,
|
||||||
|
/// Client for persisted application configuration.
|
||||||
config_client: ConfigClient,
|
config_client: ConfigClient,
|
||||||
|
/// SSH configuration parsed once during application startup.
|
||||||
|
ssh_config: Option<SshConfig>,
|
||||||
|
/// Shared state managed by UI activities.
|
||||||
pub(crate) store: Store,
|
pub(crate) store: Store,
|
||||||
|
/// Terminal adapter used to render the user interface.
|
||||||
pub(crate) terminal: CrosstermTerminalAdapter,
|
pub(crate) terminal: CrosstermTerminalAdapter,
|
||||||
|
/// Provider for the active user interface theme.
|
||||||
theme_provider: ThemeProvider,
|
theme_provider: ThemeProvider,
|
||||||
|
/// Error pending display to the user.
|
||||||
error: Option<String>,
|
error: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,6 +40,7 @@ impl Context {
|
|||||||
bookmarks_client: Option<BookmarksClient>,
|
bookmarks_client: Option<BookmarksClient>,
|
||||||
config_client: ConfigClient,
|
config_client: ConfigClient,
|
||||||
theme_provider: ThemeProvider,
|
theme_provider: ThemeProvider,
|
||||||
|
ssh_config: Option<SshConfig>,
|
||||||
error: Option<String>,
|
error: Option<String>,
|
||||||
) -> Context {
|
) -> Context {
|
||||||
let mut terminal = CrosstermTerminalAdapter::new().expect("Could not initialize terminal");
|
let mut terminal = CrosstermTerminalAdapter::new().expect("Could not initialize terminal");
|
||||||
@@ -45,6 +57,7 @@ impl Context {
|
|||||||
config_client,
|
config_client,
|
||||||
host_bridge_params: None,
|
host_bridge_params: None,
|
||||||
remote_params: None,
|
remote_params: None,
|
||||||
|
ssh_config,
|
||||||
store: Store::init(),
|
store: Store::init(),
|
||||||
terminal,
|
terminal,
|
||||||
theme_provider,
|
theme_provider,
|
||||||
@@ -78,6 +91,11 @@ impl Context {
|
|||||||
&mut self.config_client
|
&mut self.config_client
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the SSH configuration parsed during application startup.
|
||||||
|
pub fn ssh_config(&self) -> Option<&SshConfig> {
|
||||||
|
self.ssh_config.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn store(&self) -> &Store {
|
pub(crate) fn store(&self) -> &Store {
|
||||||
&self.store
|
&self.store
|
||||||
}
|
}
|
||||||
|
|||||||
+36
-1
@@ -64,6 +64,15 @@ static SEMVER_REGEX: Lazy<Regex> = lazy_regex!(r"v?((0|[1-9]\d*)\.(0|[1-9]\d*)\.
|
|||||||
*/
|
*/
|
||||||
static BYTESIZE_REGEX: Lazy<Regex> = lazy_regex!(r"(:?([0-9])+)( )*(:?[KMGTP])?B$");
|
static BYTESIZE_REGEX: Lazy<Regex> = lazy_regex!(r"(:?([0-9])+)( )*(:?[KMGTP])?B$");
|
||||||
|
|
||||||
|
/// Parsed remote parameters together with CLI syntax metadata.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) struct ParsedRemote {
|
||||||
|
/// Parsed file transfer parameters.
|
||||||
|
pub(crate) file_transfer_params: FileTransferParams,
|
||||||
|
/// Whether the remote address explicitly provided a port.
|
||||||
|
pub(crate) port_explicit: bool,
|
||||||
|
}
|
||||||
|
|
||||||
/// Parse remote option string. Returns in case of success a RemoteOptions struct
|
/// Parse remote option string. Returns in case of success a RemoteOptions struct
|
||||||
/// For ssh if username is not provided, current user will be used.
|
/// For ssh if username is not provided, current user will be used.
|
||||||
/// In case of error, message is returned
|
/// In case of error, message is returned
|
||||||
@@ -99,8 +108,20 @@ static BYTESIZE_REGEX: Lazy<Regex> = lazy_regex!(r"(:?([0-9])+)( )*(:?[KMGTP])?B
|
|||||||
///
|
///
|
||||||
/// `\\<address>\<share>[\path]`
|
/// `\\<address>\<share>[\path]`
|
||||||
///
|
///
|
||||||
|
#[cfg_attr(
|
||||||
|
not(test),
|
||||||
|
expect(
|
||||||
|
dead_code,
|
||||||
|
reason = "The public parser API is retained while CLI callers need port metadata."
|
||||||
|
)
|
||||||
|
)]
|
||||||
pub fn parse_remote_opt(s: &str) -> Result<FileTransferParams, String> {
|
pub fn parse_remote_opt(s: &str) -> Result<FileTransferParams, String> {
|
||||||
remote::parse_remote_opt(s)
|
parse_remote_opt_with_metadata(s).map(|parsed| parsed.file_transfer_params)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a remote option while retaining metadata needed by CLI precedence rules.
|
||||||
|
pub(crate) fn parse_remote_opt_with_metadata(s: &str) -> Result<ParsedRemote, String> {
|
||||||
|
remote::parse_remote_opt_with_metadata(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse semver string
|
/// Parse semver string
|
||||||
@@ -352,6 +373,20 @@ mod tests {
|
|||||||
assert!(result.remote_path.is_none());
|
assert!(result.remote_path.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parsed_remote_should_track_whether_the_port_was_explicit() {
|
||||||
|
for (remote, port_explicit) in [
|
||||||
|
("scp://host", false),
|
||||||
|
("scp://host:/path", false),
|
||||||
|
("scp://host:22", true),
|
||||||
|
("scp://host:2222", true),
|
||||||
|
] {
|
||||||
|
let parsed = remote::parse_remote_opt_with_metadata(remote).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(parsed.port_explicit, port_explicit, "{remote}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_should_parse_webdav_opt() {
|
fn test_should_parse_webdav_opt() {
|
||||||
let result =
|
let result =
|
||||||
|
|||||||
@@ -25,12 +25,13 @@ use crate::filetransfer::{FileTransferParams, FileTransferProtocol};
|
|||||||
use crate::system::config_client::ConfigClient;
|
use crate::system::config_client::ConfigClient;
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
use crate::system::environment;
|
use crate::system::environment;
|
||||||
|
use crate::utils::parser::ParsedRemote;
|
||||||
|
|
||||||
pub(super) fn parse_remote_opt(s: &str) -> Result<FileTransferParams, String> {
|
pub(super) fn parse_remote_opt_with_metadata(s: &str) -> Result<ParsedRemote, String> {
|
||||||
let default_protocol = default_protocol();
|
let default_protocol = default_protocol();
|
||||||
let (protocol, remote) = parse_remote_opt_protocol(s, default_protocol)?;
|
let (protocol, remote) = parse_remote_opt_protocol(s, default_protocol)?;
|
||||||
|
|
||||||
match protocol {
|
let file_transfer_params = match protocol {
|
||||||
FileTransferProtocol::AwsS3 => parse_s3_remote_opt(remote.as_str()),
|
FileTransferProtocol::AwsS3 => parse_s3_remote_opt(remote.as_str()),
|
||||||
FileTransferProtocol::GoogleCloudStorage => parse_gcs_remote_opt(remote.as_str()),
|
FileTransferProtocol::GoogleCloudStorage => parse_gcs_remote_opt(remote.as_str()),
|
||||||
FileTransferProtocol::Kube => parse_kube_remote_opt(remote.as_str()),
|
FileTransferProtocol::Kube => parse_kube_remote_opt(remote.as_str()),
|
||||||
@@ -45,8 +46,13 @@ pub(super) fn parse_remote_opt(s: &str) -> Result<FileTransferParams, String> {
|
|||||||
|
|
||||||
parse_webdav_remote_opt(remote.as_str(), prefix)
|
parse_webdav_remote_opt(remote.as_str(), prefix)
|
||||||
}
|
}
|
||||||
protocol => parse_generic_remote_opt(remote.as_str(), protocol),
|
protocol => return parse_generic_remote_opt(remote.as_str(), protocol),
|
||||||
}
|
}?;
|
||||||
|
|
||||||
|
Ok(ParsedRemote {
|
||||||
|
file_transfer_params,
|
||||||
|
port_explicit: false,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
@@ -71,13 +77,14 @@ fn default_protocol() -> FileTransferProtocol {
|
|||||||
fn parse_generic_remote_opt(
|
fn parse_generic_remote_opt(
|
||||||
s: &str,
|
s: &str,
|
||||||
protocol: FileTransferProtocol,
|
protocol: FileTransferProtocol,
|
||||||
) -> Result<FileTransferParams, String> {
|
) -> Result<ParsedRemote, String> {
|
||||||
let groups = REMOTE_GENERIC_OPT_REGEX
|
let groups = REMOTE_GENERIC_OPT_REGEX
|
||||||
.captures(s)
|
.captures(s)
|
||||||
.ok_or_else(|| String::from("Bad remote host syntax!"))?;
|
.ok_or_else(|| String::from("Bad remote host syntax!"))?;
|
||||||
|
|
||||||
let username = optional_capture(&groups, 1);
|
let username = optional_capture(&groups, 1);
|
||||||
let address = required_capture(&groups, 2, "address")?;
|
let address = required_capture(&groups, 2, "address")?;
|
||||||
|
let port_explicit = groups.get(3).is_some();
|
||||||
let port = parse_port(groups.get(3), default_port_for_protocol(protocol))?;
|
let port = parse_port(groups.get(3), default_port_for_protocol(protocol))?;
|
||||||
let remote_path = groups.get(4).map(|group| PathBuf::from(group.as_str()));
|
let remote_path = groups.get(4).map(|group| PathBuf::from(group.as_str()));
|
||||||
let params = ProtocolParams::Generic(
|
let params = ProtocolParams::Generic(
|
||||||
@@ -87,7 +94,10 @@ fn parse_generic_remote_opt(
|
|||||||
.username(username),
|
.username(username),
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(FileTransferParams::new(protocol, params).remote_path(remote_path))
|
Ok(ParsedRemote {
|
||||||
|
file_transfer_params: FileTransferParams::new(protocol, params).remote_path(remote_path),
|
||||||
|
port_explicit,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_webdav_remote_opt(s: &str, prefix: &str) -> Result<FileTransferParams, String> {
|
fn parse_webdav_remote_opt(s: &str, prefix: &str) -> Result<FileTransferParams, String> {
|
||||||
|
|||||||
+74
-1
@@ -5,6 +5,18 @@
|
|||||||
|
|
||||||
use ssh2_config::{ParseRule, SshConfig};
|
use ssh2_config::{ParseRule, SshConfig};
|
||||||
|
|
||||||
|
/// The standard port used when an SSH configuration does not define one.
|
||||||
|
const DEFAULT_SSH_PORT: u16 = 22;
|
||||||
|
|
||||||
|
/// Connection parameters resolved from an SSH host configuration.
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
pub(crate) struct SshHostParams {
|
||||||
|
/// Resolved SSH port.
|
||||||
|
pub(crate) port: u16,
|
||||||
|
/// Resolved SSH username, when configured.
|
||||||
|
pub(crate) username: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
/// Parses an OpenSSH-style config file into an `ssh2_config::SshConfig`.
|
/// Parses an OpenSSH-style config file into an `ssh2_config::SshConfig`.
|
||||||
pub fn parse_ssh2_config(path: &str) -> Result<SshConfig, String> {
|
pub fn parse_ssh2_config(path: &str) -> Result<SshConfig, String> {
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
@@ -18,10 +30,23 @@ pub fn parse_ssh2_config(path: &str) -> Result<SshConfig, String> {
|
|||||||
.map_err(|e| format!("Failed to parse ssh2 config: {e}"))
|
.map_err(|e| format!("Failed to parse ssh2 config: {e}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolves SSH connection parameters for a host from the startup-parsed configuration.
|
||||||
|
pub(crate) fn resolve_ssh_host_params(config: Option<&SshConfig>, host: &str) -> SshHostParams {
|
||||||
|
let params = config.map(|config| config.query(host));
|
||||||
|
|
||||||
|
SshHostParams {
|
||||||
|
port: params
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|params| params.port)
|
||||||
|
.unwrap_or(DEFAULT_SSH_PORT),
|
||||||
|
username: params.and_then(|params| params.user),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
|
|
||||||
use crate::utils::ssh::parse_ssh2_config;
|
use super::{SshHostParams, parse_ssh2_config, resolve_ssh_host_params};
|
||||||
use crate::utils::test_helpers;
|
use crate::utils::test_helpers;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -53,4 +78,52 @@ Host test
|
|||||||
.is_ok()
|
.is_ok()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ssh_host_params_should_resolve_exact_and_wildcard_hosts() {
|
||||||
|
let ssh_config_file = test_helpers::create_sample_file_with_content(
|
||||||
|
r#"
|
||||||
|
Host exact-host
|
||||||
|
Port 2222
|
||||||
|
User exact-user
|
||||||
|
|
||||||
|
Host *.example.com
|
||||||
|
Port 2200
|
||||||
|
User wildcard-user
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
let config = parse_ssh2_config(&ssh_config_file.path().to_string_lossy())
|
||||||
|
.expect("test SSH configuration should parse");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
resolve_ssh_host_params(Some(&config), "exact-host"),
|
||||||
|
SshHostParams {
|
||||||
|
port: 2222,
|
||||||
|
username: Some("exact-user".to_string()),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resolve_ssh_host_params(Some(&config), "server.example.com"),
|
||||||
|
SshHostParams {
|
||||||
|
port: 2200,
|
||||||
|
username: Some("wildcard-user".to_string()),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ssh_host_params_should_default_when_configuration_has_no_values() {
|
||||||
|
let ssh_config_file =
|
||||||
|
test_helpers::create_sample_file_with_content("Host unconfigured-host\n");
|
||||||
|
let config = parse_ssh2_config(&ssh_config_file.path().to_string_lossy())
|
||||||
|
.expect("test SSH configuration should parse");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
resolve_ssh_host_params(Some(&config), "unconfigured-host"),
|
||||||
|
SshHostParams {
|
||||||
|
port: 22,
|
||||||
|
username: None,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user