From a671325a3470f041057ae7a0b4f8f5ad90f370ca Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 8 Sep 2026 16:20:44 +0000 Subject: [PATCH 1/4] Add Rust prototype of the ssh:// protocol handler Raw Win32 rewrite (windows-rs + a native dialog resource, no GUI framework) of SshHandler, aimed at eliminating .NET's ~150MB self-contained payload and CLR startup cost for what's just a click-triggered dialog + process launch. Release build cross-compiles to a ~250KB binary. Same behavior as the WinForms version: registry setup mode, target parsing/editing dialog, legacy-algorithm option, launches ssh.exe via ShellExecuteW. Verified functionally under Wine (registry-setup prompt and the connect dialog both render and round-trip correctly); real Windows cold-start numbers still need to be gathered on actual hardware. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01GYCT2RpUjFfBfp6CD4rREw --- .gitignore | 3 + SshHandlerRs/Cargo.lock | 460 ++++++++++++++++++++++++++ SshHandlerRs/Cargo.toml | 32 ++ SshHandlerRs/README.md | 40 +++ SshHandlerRs/build.rs | 3 + SshHandlerRs/resources/resource.h | 4 + SshHandlerRs/resources/ssh_handler.rc | 17 + SshHandlerRs/resources/ssh_icon.ico | Bin 0 -> 4710 bytes SshHandlerRs/src/main.rs | 258 +++++++++++++++ 9 files changed, 817 insertions(+) create mode 100644 SshHandlerRs/Cargo.lock create mode 100644 SshHandlerRs/Cargo.toml create mode 100644 SshHandlerRs/README.md create mode 100644 SshHandlerRs/build.rs create mode 100644 SshHandlerRs/resources/resource.h create mode 100644 SshHandlerRs/resources/ssh_handler.rc create mode 100644 SshHandlerRs/resources/ssh_icon.ico create mode 100644 SshHandlerRs/src/main.rs diff --git a/.gitignore b/.gitignore index f825da6..9a49c78 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,6 @@ x86/ bld/ [Bb]in/ [Oo]bj/ + +# Rust +/SshHandlerRs/target/ diff --git a/SshHandlerRs/Cargo.lock b/SshHandlerRs/Cargo.lock new file mode 100644 index 0000000..af6987f --- /dev/null +++ b/SshHandlerRs/Cargo.lock @@ -0,0 +1,460 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "cc" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "005ec2760ca554fae18df7a11195552ec576cd665632a881bc011d5bb2fd4d80" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "embed-resource" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d506610004cfc74a6f5ee7e8c632b355de5eca1f03ee5e5e0ec11b77d4eb3d61" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml", + "vswhom", + "winreg", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "find-msvc-tools" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "ssh-handler" +version = "0.1.0" +dependencies = [ + "embed-resource", + "windows", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-result", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" +dependencies = [ + "cfg-if", + "windows-sys", +] diff --git a/SshHandlerRs/Cargo.toml b/SshHandlerRs/Cargo.toml new file mode 100644 index 0000000..0012250 --- /dev/null +++ b/SshHandlerRs/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "ssh-handler" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "ssh-handler" +path = "src/main.rs" + +[dependencies] +windows = { version = "0.58", features = [ + "Win32_Foundation", + "Win32_System_Registry", + "Win32_System_LibraryLoader", + "Win32_UI_WindowsAndMessaging", + "Win32_UI_Shell", + "Win32_UI_Controls", + "Win32_UI_Input_KeyboardAndMouse", + "Win32_System_Threading", + "Win32_System_LibraryLoader", + "Win32_Security", +] } + +[build-dependencies] +embed-resource = "2" + +[profile.release] +opt-level = "z" +lto = true +codegen-units = 1 +panic = "abort" +strip = true diff --git a/SshHandlerRs/README.md b/SshHandlerRs/README.md new file mode 100644 index 0000000..fb8a823 --- /dev/null +++ b/SshHandlerRs/README.md @@ -0,0 +1,40 @@ +# ssh-handler (Rust) + +A from-scratch rewrite of `SshHandler` (the .NET/WinForms `ssh://` protocol +handler) in Rust, targeting raw Win32 (via `windows-rs`) instead of a managed +GUI framework. Same behavior, same registry keys, same dialog layout — just +without the .NET runtime. + +Why: the WinForms build's self-contained publish is ~150MB and pays CLR +startup cost on every single `ssh://` link click. This version has no +runtime, no GC, no JIT — cold start is just process creation plus a native +dialog box. Release build is ~250KB. + +## Building + +Cross-compiling from Linux (what CI does): + +```sh +rustup target add x86_64-pc-windows-gnu +sudo apt install mingw-w64 +cargo build --release --target x86_64-pc-windows-gnu +``` + +Or natively on Windows with the MSVC toolchain: + +```sh +cargo build --release +``` + +The dialog is a native Win32 resource (`resources/ssh_handler.rc`), compiled +via `embed-resource` in `build.rs` — no GUI toolkit dependency. + +## Behavior + +- No arguments: setup mode. Checks for admin rights, confirms with the user, + then writes the `ssh://` protocol registration under `HKEY_CLASSES_ROOT`. +- `ssh://user@host` argument: handler mode. Parses the target (defaulting the + username to the current Windows user if omitted), shows a small dialog to + confirm/edit the target and optionally enable "legacy mode" (older + KEX/HostKey/MAC/cipher algorithms for old SSH servers), then launches + `ssh.exe -A -C ... user@host` via `ShellExecuteW`. diff --git a/SshHandlerRs/build.rs b/SshHandlerRs/build.rs new file mode 100644 index 0000000..fe9eae5 --- /dev/null +++ b/SshHandlerRs/build.rs @@ -0,0 +1,3 @@ +fn main() { + embed_resource::compile("resources/ssh_handler.rc", embed_resource::NONE); +} diff --git a/SshHandlerRs/resources/resource.h b/SshHandlerRs/resources/resource.h new file mode 100644 index 0000000..95b0c52 --- /dev/null +++ b/SshHandlerRs/resources/resource.h @@ -0,0 +1,4 @@ +#define IDI_APPICON 102 +#define IDD_CONNECT 101 +#define IDC_EDIT_TARGET 1001 +#define IDC_CHECK_LEGACY 1002 diff --git a/SshHandlerRs/resources/ssh_handler.rc b/SshHandlerRs/resources/ssh_handler.rc new file mode 100644 index 0000000..c4881de --- /dev/null +++ b/SshHandlerRs/resources/ssh_handler.rc @@ -0,0 +1,17 @@ +#include +#include "resource.h" + +IDI_APPICON ICON "ssh_icon.ico" + +IDD_CONNECT DIALOGEX 0, 0, 260, 92 +STYLE DS_MODALFRAME | DS_CENTER | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "SSH Connect" +FONT 9, "Segoe UI" +BEGIN + LTEXT "Target:", -1, 10, 13, 240, 10 + EDITTEXT IDC_EDIT_TARGET, 10, 25, 240, 14, ES_AUTOHSCROLL + CONTROL "Legacy mode (older/insecure algorithms)", IDC_CHECK_LEGACY, + "Button", BS_AUTOCHECKBOX | WS_TABSTOP, 10, 47, 240, 12 + DEFPUSHBUTTON "Connect", IDOK, 94, 68, 75, 16 + PUSHBUTTON "Cancel", IDCANCEL, 174, 68, 75, 16 +END diff --git a/SshHandlerRs/resources/ssh_icon.ico b/SshHandlerRs/resources/ssh_icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..3334a3d1e3c670b8e52e0c00cd96e8b42847e6e0 GIT binary patch literal 4710 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 Vec { + s.encode_utf16().chain(std::iter::once(0)).collect() +} + +fn message_box(text: &str, caption: &str, flags: windows::Win32::UI::WindowsAndMessaging::MESSAGEBOX_STYLE) -> windows::Win32::UI::WindowsAndMessaging::MESSAGEBOX_RESULT { + let text = to_wide(text); + let caption = to_wide(caption); + unsafe { + MessageBoxW( + None, + PCWSTR(text.as_ptr()), + PCWSTR(caption.as_ptr()), + flags, + ) + } +} + +fn current_exe_path() -> String { + std::env::current_exe() + .map(|p| p.display().to_string()) + .unwrap_or_default() +} + +fn current_username() -> String { + std::env::var("USERNAME").unwrap_or_else(|_| "user".to_string()) +} + +/// Data passed into / out of the connect dialog. +struct DialogData { + initial_target: Vec, + result_target: String, + legacy: bool, + accepted: bool, +} + +unsafe extern "system" fn dialog_proc(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> isize { + match msg { + WM_INITDIALOG => { + // lparam carries the raw pointer to our DialogData (see DialogBoxParamW call). + SetWindowLongPtrW(hwnd, GWLP_USERDATA, lparam.0); + let data = &*(lparam.0 as *const DialogData); + let _ = SetDlgItemTextW(hwnd, IDC_EDIT_TARGET, PCWSTR(data.initial_target.as_ptr())); + // Select all text in the edit box and give it focus. + let _ = SendDlgItemMessageW(hwnd, IDC_EDIT_TARGET, EM_SETSEL, WPARAM(0), LPARAM(-1)); + let edit_hwnd = GetDlgItem(hwnd, IDC_EDIT_TARGET).unwrap_or_default(); + let _ = SetFocus(edit_hwnd); + 1 // we set focus ourselves + } + WM_COMMAND => { + let control_id = (wparam.0 & 0xFFFF) as i32; + if control_id == IDOK.0 { + let ptr = GetWindowLongPtrW(hwnd, GWLP_USERDATA) as *mut DialogData; + if !ptr.is_null() { + let data = &mut *ptr; + let mut buf = [0u16; 512]; + let len = GetDlgItemTextW(hwnd, IDC_EDIT_TARGET, &mut buf); + data.result_target = String::from_utf16_lossy(&buf[..len as usize]); + data.legacy = + IsDlgButtonChecked(hwnd, IDC_CHECK_LEGACY) as usize == BST_CHECKED; + data.accepted = true; + } + let _ = EndDialog(hwnd, IDOK.0 as isize); + return 1; + } else if control_id == IDCANCEL.0 { + let ptr = GetWindowLongPtrW(hwnd, GWLP_USERDATA) as *mut DialogData; + if !ptr.is_null() { + (&mut *ptr).accepted = false; + } + let _ = EndDialog(hwnd, IDCANCEL.0 as isize); + return 1; + } + 0 + } + WM_CLOSE => { + let ptr = GetWindowLongPtrW(hwnd, GWLP_USERDATA) as *mut DialogData; + if !ptr.is_null() { + (&mut *ptr).accepted = false; + } + let _ = EndDialog(hwnd, IDCANCEL.0 as isize); + 1 + } + _ => 0, + } +} + +fn show_connect_dialog(initial_target: &str) -> Option<(String, bool)> { + let mut data = Box::new(DialogData { + initial_target: to_wide(initial_target), + result_target: String::new(), + legacy: false, + accepted: false, + }); + let data_ptr = data.as_mut() as *mut DialogData; + + unsafe { + let hinstance: windows::Win32::Foundation::HINSTANCE = + GetModuleHandleW(None).unwrap_or_default().into(); + DialogBoxParamW( + hinstance, + windows::core::PCWSTR(IDD_CONNECT as *const u16), + None, + Some(dialog_proc), + LPARAM(data_ptr as isize), + ); + } + + if data.accepted { + Some((data.result_target.clone(), data.legacy)) + } else { + None + } +} + +fn is_admin() -> bool { + unsafe { IsUserAnAdmin().as_bool() } +} + +fn setup_handler() { + if !is_admin() { + message_box( + "Please run this application as an administrator to register the URL handler.", + "Admin Privileges Required", + MB_OK | MB_ICONERROR, + ); + return; + } + + let exe_path = current_exe_path(); + let command_str = format!("\"{exe_path}\" \"%1\""); + + let prompt = format!( + "This will register this application to handle all ssh:// links.\n\nCommand:\n{command_str}\n\nDo you want to proceed?" + ); + let result = message_box(&prompt, "Registry Setup", MB_YESNO | MB_ICONQUESTION); + if result != IDYES { + return; + } + + match register_protocol(&command_str) { + Ok(()) => { + message_box( + "Successfully registered ssh:// protocol handler!", + "Success", + MB_OK | MB_ICONINFORMATION, + ); + } + Err(e) => { + message_box( + &format!("Failed to write to registry:\n{e:?}"), + "Error", + MB_OK | MB_ICONERROR, + ); + } + } +} + +fn reg_set_sz(hkey: HKEY, value_name: PCWSTR, data: &str) -> windows::core::Result<()> { + let wide = to_wide(data); + let bytes = unsafe { + std::slice::from_raw_parts(wide.as_ptr() as *const u8, wide.len() * 2) + }; + unsafe { RegSetValueExW(hkey, value_name, 0, REG_SZ, Some(bytes)) }.ok() +} + +fn reg_create_key(subkey: PCWSTR) -> windows::core::Result { + let mut key = HKEY::default(); + unsafe { + RegCreateKeyExW( + HKEY_CLASSES_ROOT, + subkey, + 0, + None, + REG_OPTION_NON_VOLATILE, + KEY_WRITE, + None, + &mut key, + None, + ) + } + .ok()?; + Ok(key) +} + +fn register_protocol(command_str: &str) -> windows::core::Result<()> { + let ssh_key = reg_create_key(w!("ssh"))?; + reg_set_sz(ssh_key, PCWSTR::null(), "URL:ssh Protocol")?; + reg_set_sz(ssh_key, w!("URL Protocol"), "")?; + + let cmd_key = reg_create_key(w!("ssh\\shell\\open\\command"))?; + reg_set_sz(cmd_key, PCWSTR::null(), command_str)?; + Ok(()) +} + +fn handle_connection(url: &str) { + let mut target = url.trim_start_matches("ssh://").trim_end_matches('/').to_string(); + if !target.contains('@') { + target = format!("{}@{}", current_username(), target); + } + + let Some((final_target, legacy)) = show_connect_dialog(&target) else { + return; + }; + + let legacy_args = if legacy { LEGACY_ARGS } else { "" }; + let arguments = format!("-A -C {legacy_args} {final_target}"); + + let ssh_exe = w!("ssh.exe"); + let args_wide = to_wide(&arguments); + let verb = w!("open"); + + unsafe { + ShellExecuteW( + None, + verb, + ssh_exe, + PCWSTR(args_wide.as_ptr()), + None, + SW_SHOWNORMAL, + ); + } +} + +fn main() { + let args: Vec = std::env::args().skip(1).collect(); + if args.is_empty() { + setup_handler(); + } else { + handle_connection(&args[0]); + } +} From 1bd676d2fecc93616485d770bb97f03a19ed2f71 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 8 Sep 2026 16:48:31 +0000 Subject: [PATCH 2/4] Rebase Rust handler's protocol registration on the .bat's Default Apps model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The direct HKCR\ssh\shell\open\command write (what SshHandler and the initial Rust port both did) doesn't actually get honored once a UserChoice already exists for the ssh scheme, which is the normal state on Windows 10/11 — so registration silently no-ops from the user's perspective. Switch to the same "Default Programs" capability registration openssh_protocol_handler.bat uses: a dedicated ProgID with Capabilities\UrlAssociations plus a HKLM\SOFTWARE\RegisteredApplications entry, so the app actually shows up in and can be selected from Settings > Apps > Default Apps > "Choose default apps by link type". Uses its own identifier, ssh_handler_rs, distinct from the batch script's ssh_custom_handler so the two don't collide. Verified end-to-end under Wine: registry keys land exactly as expected (checked via `wine reg query`), and both the setup and connect dialogs still render correctly. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01GYCT2RpUjFfBfp6CD4rREw --- SshHandlerRs/src/main.rs | 91 +++++++++++++++++++++++++++++++--------- 1 file changed, 72 insertions(+), 19 deletions(-) diff --git a/SshHandlerRs/src/main.rs b/SshHandlerRs/src/main.rs index 43ef8b8..4704848 100644 --- a/SshHandlerRs/src/main.rs +++ b/SshHandlerRs/src/main.rs @@ -3,8 +3,8 @@ use windows::core::{w, PCWSTR}; use windows::Win32::Foundation::{HWND, LPARAM, WPARAM}; use windows::Win32::System::Registry::{ - RegCreateKeyExW, RegSetValueExW, HKEY, HKEY_CLASSES_ROOT, KEY_WRITE, REG_OPTION_NON_VOLATILE, - REG_SZ, + RegCreateKeyExW, RegSetValueExW, HKEY, HKEY_CLASSES_ROOT, HKEY_LOCAL_MACHINE, KEY_WRITE, + REG_OPTION_NON_VOLATILE, REG_SZ, }; use windows::Win32::System::LibraryLoader::GetModuleHandleW; use windows::Win32::UI::Controls::IsDlgButtonChecked; @@ -26,6 +26,11 @@ const EM_SETSEL: u32 = 0x00B1; const LEGACY_ARGS: &str = "-o KexAlgorithms=+diffie-hellman-group1-sha1,diffie-hellman-group14-sha1 \ -o HostKeyAlgorithms=+ssh-rsa -o MACs=+hmac-sha1,hmac-sha1-96 -o ciphers=+aes256-cbc"; +/// Our ProgID / capability-provider identifier under HKCR and RegisteredApplications. +/// Distinct from the .bat's "ssh_custom_handler" so both can coexist/be told apart. +const PROG_ID: &str = "ssh_handler_rs"; +const APP_DISPLAY_NAME: &str = "SSH Handler (Rust)"; + fn to_wide(s: &str) -> Vec { s.encode_utf16().chain(std::iter::once(0)).collect() } @@ -157,17 +162,26 @@ fn setup_handler() { let command_str = format!("\"{exe_path}\" \"%1\""); let prompt = format!( - "This will register this application to handle all ssh:// links.\n\nCommand:\n{command_str}\n\nDo you want to proceed?" + "This will register this application ({APP_DISPLAY_NAME}) as a candidate handler for ssh:// links.\n\n\ + Command:\n{command_str}\n\n\ + After this, you may need to open Settings > Apps > Default Apps > \"Choose default apps by link type\", \ + find \"ssh\", and select {APP_DISPLAY_NAME} explicitly — Windows does not let an installer silently \ + override an existing default protocol handler.\n\n\ + Do you want to proceed?" ); let result = message_box(&prompt, "Registry Setup", MB_YESNO | MB_ICONQUESTION); if result != IDYES { return; } - match register_protocol(&command_str) { + match register_protocol(&exe_path, &command_str) { Ok(()) => { message_box( - "Successfully registered ssh:// protocol handler!", + &format!( + "Registered {APP_DISPLAY_NAME} as an ssh:// handler.\n\n\ + If ssh:// links don't launch it, open Settings > Apps > Default Apps > \ + \"Choose default apps by link type\", find \"ssh\", and pick it there." + ), "Success", MB_OK | MB_ICONINFORMATION, ); @@ -182,20 +196,27 @@ fn setup_handler() { } } -fn reg_set_sz(hkey: HKEY, value_name: PCWSTR, data: &str) -> windows::core::Result<()> { - let wide = to_wide(data); - let bytes = unsafe { - std::slice::from_raw_parts(wide.as_ptr() as *const u8, wide.len() * 2) +/// Sets a REG_SZ value. `value_name = None` targets the key's default (unnamed) value. +fn reg_set_sz(hkey: HKEY, value_name: Option<&str>, data: &str) -> windows::core::Result<()> { + let name_wide = value_name.map(to_wide); + let name_ptr = match &name_wide { + Some(w) => PCWSTR(w.as_ptr()), + None => PCWSTR::null(), }; - unsafe { RegSetValueExW(hkey, value_name, 0, REG_SZ, Some(bytes)) }.ok() + let data_wide = to_wide(data); + let bytes = unsafe { + std::slice::from_raw_parts(data_wide.as_ptr() as *const u8, data_wide.len() * 2) + }; + unsafe { RegSetValueExW(hkey, name_ptr, 0, REG_SZ, Some(bytes)) }.ok() } -fn reg_create_key(subkey: PCWSTR) -> windows::core::Result { +fn reg_create_key(root: HKEY, subkey: &str) -> windows::core::Result { + let subkey_wide = to_wide(subkey); let mut key = HKEY::default(); unsafe { RegCreateKeyExW( - HKEY_CLASSES_ROOT, - subkey, + root, + PCWSTR(subkey_wide.as_ptr()), 0, None, REG_OPTION_NON_VOLATILE, @@ -209,13 +230,45 @@ fn reg_create_key(subkey: PCWSTR) -> windows::core::Result { Ok(key) } -fn register_protocol(command_str: &str) -> windows::core::Result<()> { - let ssh_key = reg_create_key(w!("ssh"))?; - reg_set_sz(ssh_key, PCWSTR::null(), "URL:ssh Protocol")?; - reg_set_sz(ssh_key, w!("URL Protocol"), "")?; +/// Registers via the "Default Programs" capability model (RegisteredApplications + +/// a distinct ProgID with Capabilities\UrlAssociations), the same pattern +/// `openssh_protocol_handler.bat` uses — this is what actually shows up in and gets +/// honored by Windows' "Choose default apps by link type" UI. A bare +/// HKCR\ssh\shell\open\command write is ignored once a UserChoice already exists for +/// the protocol, which is the common case on Windows 10/11. +fn register_protocol(exe_path: &str, command_str: &str) -> windows::core::Result<()> { + // Declare "ssh" as a URL protocol scheme (legacy marker some apps still check for). + let ssh_key = reg_create_key(HKEY_CLASSES_ROOT, "ssh")?; + reg_set_sz(ssh_key, None, "URL:ssh Protocol")?; + reg_set_sz(ssh_key, Some("URL Protocol"), "")?; + + // Our ProgID: the actual open command plus Default-Apps display metadata. + let cmd_key = reg_create_key(HKEY_CLASSES_ROOT, &format!("{PROG_ID}\\shell\\open\\command"))?; + reg_set_sz(cmd_key, None, command_str)?; + + let app_key = reg_create_key(HKEY_CLASSES_ROOT, &format!("{PROG_ID}\\Application"))?; + reg_set_sz(app_key, Some("ApplicationIcon"), &format!("\"{exe_path}\",0"))?; + reg_set_sz(app_key, Some("ApplicationName"), APP_DISPLAY_NAME)?; + reg_set_sz( + app_key, + Some("ApplicationDescription"), + "Handles ssh:// links via Windows OpenSSH", + )?; + + let caps_key = reg_create_key( + HKEY_CLASSES_ROOT, + &format!("{PROG_ID}\\Capabilities\\UrlAssociations"), + )?; + reg_set_sz(caps_key, Some("ssh"), PROG_ID)?; + + // Tell Windows' Default Apps UI this app exists as a capability provider. + let registered_key = reg_create_key(HKEY_LOCAL_MACHINE, "SOFTWARE\\RegisteredApplications")?; + reg_set_sz( + registered_key, + Some(APP_DISPLAY_NAME), + &format!("Software\\Classes\\{PROG_ID}\\Capabilities"), + )?; - let cmd_key = reg_create_key(w!("ssh\\shell\\open\\command"))?; - reg_set_sz(cmd_key, PCWSTR::null(), command_str)?; Ok(()) } From 905487ce824ae3dbe500ebeba685458a86b34554 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 8 Sep 2026 17:16:38 +0000 Subject: [PATCH 3/4] Replace .NET WinForms handler with the Rust build in CI; drop SshHandler Remove the .NET/WinForms project (SshHandler/, SshHandler.sln) now that the Rust rewrite (SshHandlerRs/) covers the same behavior with a ~250KB binary and no CLR startup cost, and its Default-Apps-style registration is confirmed working. Rewire the release workflow to build ssh-handler.exe natively on windows-latest with the MSVC toolchain instead of dotnet publish, and package it alongside the .bat and .sh handlers as release assets (ssh-handler-rs-*.exe, openssh_protocol_handler-*.bat, ssh-handler-*.sh) when a v* tag is pushed. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01GYCT2RpUjFfBfp6CD4rREw --- .github/workflows/build.yml | 48 +++-------- .gitignore | 20 ----- SshHandler.sln | 21 ----- SshHandler/Form1.Designer.cs | 113 -------------------------- SshHandler/Form1.cs | 40 --------- SshHandler/Form1.resx | 67 --------------- SshHandler/Program.cs | 106 ------------------------ SshHandler/Properties/AssemblyInfo.cs | 0 SshHandler/SshHandler.csproj | 12 --- SshHandler/ssh_icon.ico | Bin 4710 -> 0 bytes SshHandlerRs/README.md | 40 +++++---- 11 files changed, 34 insertions(+), 433 deletions(-) delete mode 100644 SshHandler.sln delete mode 100644 SshHandler/Form1.Designer.cs delete mode 100644 SshHandler/Form1.cs delete mode 100644 SshHandler/Form1.resx delete mode 100644 SshHandler/Program.cs delete mode 100644 SshHandler/Properties/AssemblyInfo.cs delete mode 100644 SshHandler/SshHandler.csproj delete mode 100644 SshHandler/ssh_icon.ico diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b76622a..5671077 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,38 +16,14 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - - name: Configure Git Line Endings - run: git config --global core.autocrlf input - - - name: Setup .NET - uses: actions/setup-dotnet@v4 + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable with: - dotnet-version: 8.0.x + targets: x86_64-pc-windows-msvc - - name: Restore dependencies - run: dotnet restore - - # 1. Build Framework-Dependent FIRST (The tiny one) - - name: Build Framework-Dependent (Requires .NET 8) - run: > - dotnet publish SshHandler/SshHandler.csproj --configuration Release - --self-contained false - /p:PublishSingleFile=true - -o ./publish-dotnet8 - - # 2. Clean to prevent artifact leakage - - name: Clean intermediate files - run: dotnet clean SshHandler/SshHandler.csproj - - # 3. Build Standalone SECOND (The heavy one) - - name: Build Standalone (Includes Runtime) - run: > - dotnet publish SshHandler/SshHandler.csproj --configuration Release - --runtime win-x64 - --self-contained true - /p:PublishSingleFile=true - /p:IncludeNativeLibrariesForSelfExtract=true - -o ./publish-standalone + - name: Build ssh-handler (Rust) + working-directory: SshHandlerRs + run: cargo build --release --target x86_64-pc-windows-msvc - name: Ensure Unix Line Endings for Shell Script shell: pwsh @@ -64,11 +40,10 @@ jobs: run: | $tag = "${{ github.ref_name }}" if (-not $tag -or $tag -eq "main") { $tag = "manual" } - - # Rename Executables - Move-Item ./publish-standalone/SshHandler.exe "./SshHandler-$tag-Standalone.exe" - Move-Item ./publish-dotnet8/SshHandler.exe "./SshHandler-$tag-dotnet8.exe" - + + # Rename Rust executable + Move-Item SshHandlerRs/target/x86_64-pc-windows-msvc/release/ssh-handler.exe "./ssh-handler-rs-$tag.exe" + # Rename Scripts Copy-Item "ssh-handler.sh" "./ssh-handler-$tag.sh" Copy-Item "openssh_protocol_handler.bat" "./openssh_protocol_handler-$tag.bat" @@ -78,8 +53,7 @@ jobs: if: startsWith(github.ref, 'refs/tags/') with: files: | - SshHandler-*-Standalone.exe - SshHandler-*-dotnet8.exe + ssh-handler-rs-*.exe ssh-handler-*.sh openssh_protocol_handler-*.bat generate_release_notes: true diff --git a/.gitignore b/.gitignore index 9a49c78..6c8c89a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,24 +1,4 @@ # .gitignore -# User-specific files -*.rsuser -*.suo -*.user -*.userosscache -*.sln.docstates - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -[Aa][Rr][Mm]/ -[Aa][Rr][Mm]64/ -bld/ -[Bb]in/ -[Oo]bj/ - # Rust /SshHandlerRs/target/ diff --git a/SshHandler.sln b/SshHandler.sln deleted file mode 100644 index deffb31..0000000 --- a/SshHandler.sln +++ /dev/null @@ -1,21 +0,0 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31903.59 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{50924D75-2903-46D4-86DD-02269EFDBCA9}") = "SshHandler", "SshHandler\SshHandler.csproj", "{960BAB18-309B-42D0-B67E-B4EF8E7F7490}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {960BAB18-309B-42D0-B67E-B4EF8E7F7490}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {960BAB18-309B-42D0-B67E-B4EF8E7F7490}.Debug|Any CPU.Build.0 = Debug|Any CPU - {960BAB18-309B-42D0-B67E-B4EF8E7F7490}.Release|Any CPU.ActiveCfg = Release|Any CPU - {960BAB18-309B-42D0-B67E-B4EF8E7F7490}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection -EndGlobal diff --git a/SshHandler/Form1.Designer.cs b/SshHandler/Form1.Designer.cs deleted file mode 100644 index b1e5ad7..0000000 --- a/SshHandler/Form1.Designer.cs +++ /dev/null @@ -1,113 +0,0 @@ -partial class Form1 -{ - /// - /// Required designer variable. - /// - private System.ComponentModel.IContainer components = null; - - /// - /// Clean up any resources being used. - /// - /// true if managed resources should be disposed; otherwise, false. - protected override void Dispose(bool disposing) - { - if (disposing && (components != null)) - { - components.Dispose(); - } - base.Dispose(disposing); - } - - #region Windows Form Designer generated code - - /// - /// Required method for Designer support - do not modify - /// the contents of this method with the code editor. - /// - private void InitializeComponent() - { - this.label1 = new System.Windows.Forms.Label(); - this.targetTextBox = new System.Windows.Forms.TextBox(); - this.legacyCheckBox = new System.Windows.Forms.CheckBox(); - this.connectButton = new System.Windows.Forms.Button(); - this.cancelButton = new System.Windows.Forms.Button(); - this.SuspendLayout(); - // - // label1 - // - this.label1.AutoSize = true; - this.label1.Location = new System.Drawing.Point(12, 15); - this.label1.Name = "label1"; - this.label1.Size = new System.Drawing.Size(73, 15); - this.label1.TabIndex = 0; - this.label1.Text = "Connect to:"; - // - // targetTextBox - // - this.targetTextBox.Location = new System.Drawing.Point(15, 33); - this.targetTextBox.Name = "targetTextBox"; - this.targetTextBox.Size = new System.Drawing.Size(357, 23); - this.targetTextBox.TabIndex = 1; - // - // legacyCheckBox - // - this.legacyCheckBox.AutoSize = true; - this.legacyCheckBox.Location = new System.Drawing.Point(15, 71); - this.legacyCheckBox.Name = "legacyCheckBox"; - this.legacyCheckBox.Size = new System.Drawing.Size(206, 19); - this.legacyCheckBox.TabIndex = 2; - this.legacyCheckBox.Text = "Enable Legacy Mode (for old devices)"; - this.legacyCheckBox.UseVisualStyleBackColor = true; - // - // connectButton - // - this.connectButton.Location = new System.Drawing.Point(297, 106); - this.connectButton.Name = "connectButton"; - this.connectButton.Size = new System.Drawing.Size(75, 23); - this.connectButton.TabIndex = 3; - this.connectButton.Text = "Connect"; - this.connectButton.UseVisualStyleBackColor = true; - this.connectButton.Click += new System.EventHandler(this.connectButton_Click); - // - // cancelButton - // - this.cancelButton.DialogResult = System.Windows.Forms.DialogResult.Cancel; - this.cancelButton.Location = new System.Drawing.Point(216, 106); - this.cancelButton.Name = "cancelButton"; - this.cancelButton.Size = new System.Drawing.Size(75, 23); - this.cancelButton.TabIndex = 4; - this.cancelButton.Text = "Cancel"; - this.cancelButton.UseVisualStyleBackColor = true; - this.cancelButton.Click += new System.EventHandler(this.cancelButton_Click); - // - // Form1 - // - this.AcceptButton = this.connectButton; - this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); - this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.CancelButton = this.cancelButton; - this.ClientSize = new System.Drawing.Size(384, 141); - this.Controls.Add(this.cancelButton); - this.Controls.Add(this.connectButton); - this.Controls.Add(this.legacyCheckBox); - this.Controls.Add(this.targetTextBox); - this.Controls.Add(this.label1); - this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog; - this.MaximizeBox = false; - this.MinimizeBox = false; - this.Name = "Form1"; - this.ShowIcon = false; - this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; - this.Text = "SSH Connection"; - this.ResumeLayout(false); - this.PerformLayout(); - } - - #endregion - - private System.Windows.Forms.Label label1; - private System.Windows.Forms.TextBox targetTextBox; - private System.Windows.Forms.CheckBox legacyCheckBox; - private System.Windows.Forms.Button connectButton; - private System.Windows.Forms.Button cancelButton; -} diff --git a/SshHandler/Form1.cs b/SshHandler/Form1.cs deleted file mode 100644 index 90cc8c5..0000000 --- a/SshHandler/Form1.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Windows.Forms; - -public partial class Form1 : Form -{ - // This property will store the result for the main program - public string FinalSshCommand { get; private set; } - - public Form1(string initialTarget) - { - InitializeComponent(); - targetTextBox.Text = initialTarget; - // Select the text for easy editing - targetTextBox.SelectAll(); - targetTextBox.Focus(); - } - - private void connectButton_Click(object sender, EventArgs e) - { - string target = targetTextBox.Text; - bool useLegacy = legacyCheckBox.Checked; - - string legacyArgs = "-o KexAlgorithms=+diffie-hellman-group1-sha1,diffie-hellman-group14-sha1 " + - "-o HostKeyAlgorithms=+ssh-rsa " + - "-o MACs=+hmac-sha1,hmac-sha1-96"; - - // Build the arguments for ssh.exe - string arguments = $"-A -C {(useLegacy ? legacyArgs : "")} {target}"; - - FinalSshCommand = arguments; - this.DialogResult = DialogResult.OK; // Set dialog result to OK - this.Close(); // Close the form - } - - private void cancelButton_Click(object sender, EventArgs e) - { - this.DialogResult = DialogResult.Cancel; // Set dialog result to Cancel - this.Close(); - } -} diff --git a/SshHandler/Form1.resx b/SshHandler/Form1.resx deleted file mode 100644 index 5aada26..0000000 --- a/SshHandler/Form1.resx +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - 384, 141 - - - SSH Connection - - diff --git a/SshHandler/Program.cs b/SshHandler/Program.cs deleted file mode 100644 index ff98f3c..0000000 --- a/SshHandler/Program.cs +++ /dev/null @@ -1,106 +0,0 @@ -using Microsoft.Win32; -using System; -using System.Diagnostics; -using System.Security.Principal; -using System.Windows.Forms; - -static class Program -{ - private const string ProtocolName = "ssh"; - - [STAThread] - static void Main(string[] args) - { - Application.EnableVisualStyles(); - Application.SetCompatibleTextRenderingDefault(false); - - // If no arguments, run setup mode. - if (args.Length == 0) - { - SetupHandler(); - } - // Otherwise, run handler mode. - else - { - HandleConnection(args[0]); - } - } - - static void HandleConnection(string url) - { - string target = url.Replace("ssh://", "").TrimEnd('/'); - if (!target.Contains("@")) - { - target = $"{Environment.UserName}@{target}"; - } - - // Show the form as a dialog - using (var form = new Form1(target)) - { - if (form.ShowDialog() == DialogResult.OK) - { - // If user clicked "Connect", run the command - ProcessStartInfo psi = new ProcessStartInfo - { - FileName = "ssh.exe", - Arguments = form.FinalSshCommand, - UseShellExecute = true, - CreateNoWindow = false - }; - Process.Start(psi); - } - } - } - - static void SetupHandler() - { - if (!IsAdmin()) - { - MessageBox.Show("Please run this application as an administrator to register the URL handler.", "Admin Privileges Required", MessageBoxButtons.OK, MessageBoxIcon.Error); - return; - } - - // The path is simple and correct because it's a native app - string exePath = Application.ExecutablePath; - string commandStr = $"\"{exePath}\" \"%1\""; - - var confirmResult = MessageBox.Show( - "This will register this application to handle all ssh:// links.\n\n" + - $"Command:\n{commandStr}\n\n" + - "Do you want to proceed?", - "Registry Setup", - MessageBoxButtons.YesNo, - MessageBoxIcon.Information); - - if (confirmResult == DialogResult.Yes) - { - try - { - // Write the keys to HKEY_CLASSES_ROOT - using (RegistryKey key = Registry.ClassesRoot.CreateSubKey(ProtocolName)) - { - key.SetValue("", $"URL:{ProtocolName} Protocol"); - key.SetValue("URL Protocol", ""); - } - using (RegistryKey key = Registry.ClassesRoot.CreateSubKey($"{ProtocolName}\\shell\\open\\command")) - { - key.SetValue("", commandStr); - } - MessageBox.Show("Successfully registered ssh:// protocol handler!", "Success", MessageBoxButtons.OK, MessageBoxIcon.Information); - } - catch (Exception ex) - { - MessageBox.Show($"Failed to write to registry:\n{ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); - } - } - } - - private static bool IsAdmin() - { - using (WindowsIdentity identity = WindowsIdentity.GetCurrent()) - { - WindowsPrincipal principal = new WindowsPrincipal(identity); - return principal.IsInRole(WindowsBuiltInRole.Administrator); - } - } -} diff --git a/SshHandler/Properties/AssemblyInfo.cs b/SshHandler/Properties/AssemblyInfo.cs deleted file mode 100644 index e69de29..0000000 diff --git a/SshHandler/SshHandler.csproj b/SshHandler/SshHandler.csproj deleted file mode 100644 index b695334..0000000 --- a/SshHandler/SshHandler.csproj +++ /dev/null @@ -1,12 +0,0 @@ - - - - WinExe - net8.0-windows - enable - true - enable - ssh_icon.ico - - - diff --git a/SshHandler/ssh_icon.ico b/SshHandler/ssh_icon.ico deleted file mode 100644 index 3334a3d1e3c670b8e52e0c00cd96e8b42847e6e0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4710 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 Apps > Default Apps > "Choose default apps by link + type". A bare `HKCR\ssh\shell\open\command` write (what the old .NET + version did) gets silently ignored once a `UserChoice` already exists for + the protocol, which is the common case. - `ssh://user@host` argument: handler mode. Parses the target (defaulting the username to the current Windows user if omitted), shows a small dialog to confirm/edit the target and optionally enable "legacy mode" (older From 7daf668a1162e9f8d689ab167de7e1323a6f6364 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 8 Sep 2026 17:22:40 +0000 Subject: [PATCH 4/4] Sanitize CI tag/ref before using it in file paths github.ref_name for a workflow_dispatch run on a branch is the branch name verbatim (e.g. claude/optimistic-davinci-2i411w), and PowerShell treated the "/" as a path separator in Move-Item's destination, failing with "Could not find a part of the path." Tag pushes (v0.2.0) don't normally hit this, but sanitize unconditionally so manual runs on any branch work too. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01GYCT2RpUjFfBfp6CD4rREw --- .github/workflows/build.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5671077..224f933 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -40,6 +40,8 @@ jobs: run: | $tag = "${{ github.ref_name }}" if (-not $tag -or $tag -eq "main") { $tag = "manual" } + # Branch names (workflow_dispatch) can contain "/", which breaks file paths. + $tag = $tag -replace '[\\/]', '-' # Rename Rust executable Move-Item SshHandlerRs/target/x86_64-pc-windows-msvc/release/ssh-handler.exe "./ssh-handler-rs-$tag.exe"