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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GYCT2RpUjFfBfp6CD4rREw
This commit is contained in:
Claude
2026-09-08 16:20:44 +00:00
parent eab8873c14
commit a671325a34
9 changed files with 817 additions and 0 deletions
+3
View File
@@ -19,3 +19,6 @@ x86/
bld/ bld/
[Bb]in/ [Bb]in/
[Oo]bj/ [Oo]bj/
# Rust
/SshHandlerRs/target/
+460
View File
@@ -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",
]
+32
View File
@@ -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
+40
View File
@@ -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`.
+3
View File
@@ -0,0 +1,3 @@
fn main() {
embed_resource::compile("resources/ssh_handler.rc", embed_resource::NONE);
}
+4
View File
@@ -0,0 +1,4 @@
#define IDI_APPICON 102
#define IDD_CONNECT 101
#define IDC_EDIT_TARGET 1001
#define IDC_CHECK_LEGACY 1002
+17
View File
@@ -0,0 +1,17 @@
#include <windows.h>
#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
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

+258
View File
@@ -0,0 +1,258 @@
#![windows_subsystem = "windows"]
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,
};
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
use windows::Win32::UI::Controls::IsDlgButtonChecked;
use windows::Win32::UI::Input::KeyboardAndMouse::SetFocus;
use windows::Win32::UI::Shell::{IsUserAnAdmin, ShellExecuteW};
use windows::Win32::UI::WindowsAndMessaging::{
DialogBoxParamW, EndDialog, GetDlgItem, GetDlgItemTextW, GetWindowLongPtrW, MessageBoxW,
SendDlgItemMessageW, SetDlgItemTextW, SetWindowLongPtrW, GWLP_USERDATA, IDCANCEL, IDOK, IDYES,
MB_ICONERROR, MB_ICONINFORMATION, MB_ICONQUESTION, MB_OK, MB_YESNO, SW_SHOWNORMAL, WM_CLOSE,
WM_COMMAND, WM_INITDIALOG,
};
const IDD_CONNECT: u16 = 101;
const IDC_EDIT_TARGET: i32 = 1001;
const IDC_CHECK_LEGACY: i32 = 1002;
const BST_CHECKED: usize = 1;
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";
fn to_wide(s: &str) -> Vec<u16> {
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<u16>,
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<HKEY> {
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<String> = std::env::args().skip(1).collect();
if args.is_empty() {
setup_handler();
} else {
handle_connection(&args[0]);
}
}