mirror of
https://github.com/ergosteur/ssh-protocol-handler-win.git
synced 2026-10-28 16:10:36 -04:00
Rebase Rust handler's protocol registration on the .bat's Default Apps model
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GYCT2RpUjFfBfp6CD4rREw
This commit is contained in:
+72
-19
@@ -3,8 +3,8 @@
|
|||||||
use windows::core::{w, PCWSTR};
|
use windows::core::{w, PCWSTR};
|
||||||
use windows::Win32::Foundation::{HWND, LPARAM, WPARAM};
|
use windows::Win32::Foundation::{HWND, LPARAM, WPARAM};
|
||||||
use windows::Win32::System::Registry::{
|
use windows::Win32::System::Registry::{
|
||||||
RegCreateKeyExW, RegSetValueExW, HKEY, HKEY_CLASSES_ROOT, KEY_WRITE, REG_OPTION_NON_VOLATILE,
|
RegCreateKeyExW, RegSetValueExW, HKEY, HKEY_CLASSES_ROOT, HKEY_LOCAL_MACHINE, KEY_WRITE,
|
||||||
REG_SZ,
|
REG_OPTION_NON_VOLATILE, REG_SZ,
|
||||||
};
|
};
|
||||||
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
|
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
|
||||||
use windows::Win32::UI::Controls::IsDlgButtonChecked;
|
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 \
|
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";
|
-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<u16> {
|
fn to_wide(s: &str) -> Vec<u16> {
|
||||||
s.encode_utf16().chain(std::iter::once(0)).collect()
|
s.encode_utf16().chain(std::iter::once(0)).collect()
|
||||||
}
|
}
|
||||||
@@ -157,17 +162,26 @@ fn setup_handler() {
|
|||||||
let command_str = format!("\"{exe_path}\" \"%1\"");
|
let command_str = format!("\"{exe_path}\" \"%1\"");
|
||||||
|
|
||||||
let prompt = format!(
|
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);
|
let result = message_box(&prompt, "Registry Setup", MB_YESNO | MB_ICONQUESTION);
|
||||||
if result != IDYES {
|
if result != IDYES {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
match register_protocol(&command_str) {
|
match register_protocol(&exe_path, &command_str) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
message_box(
|
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",
|
"Success",
|
||||||
MB_OK | MB_ICONINFORMATION,
|
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<()> {
|
/// Sets a REG_SZ value. `value_name = None` targets the key's default (unnamed) value.
|
||||||
let wide = to_wide(data);
|
fn reg_set_sz(hkey: HKEY, value_name: Option<&str>, data: &str) -> windows::core::Result<()> {
|
||||||
let bytes = unsafe {
|
let name_wide = value_name.map(to_wide);
|
||||||
std::slice::from_raw_parts(wide.as_ptr() as *const u8, wide.len() * 2)
|
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<HKEY> {
|
fn reg_create_key(root: HKEY, subkey: &str) -> windows::core::Result<HKEY> {
|
||||||
|
let subkey_wide = to_wide(subkey);
|
||||||
let mut key = HKEY::default();
|
let mut key = HKEY::default();
|
||||||
unsafe {
|
unsafe {
|
||||||
RegCreateKeyExW(
|
RegCreateKeyExW(
|
||||||
HKEY_CLASSES_ROOT,
|
root,
|
||||||
subkey,
|
PCWSTR(subkey_wide.as_ptr()),
|
||||||
0,
|
0,
|
||||||
None,
|
None,
|
||||||
REG_OPTION_NON_VOLATILE,
|
REG_OPTION_NON_VOLATILE,
|
||||||
@@ -209,13 +230,45 @@ fn reg_create_key(subkey: PCWSTR) -> windows::core::Result<HKEY> {
|
|||||||
Ok(key)
|
Ok(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn register_protocol(command_str: &str) -> windows::core::Result<()> {
|
/// Registers via the "Default Programs" capability model (RegisteredApplications +
|
||||||
let ssh_key = reg_create_key(w!("ssh"))?;
|
/// a distinct ProgID with Capabilities\UrlAssociations), the same pattern
|
||||||
reg_set_sz(ssh_key, PCWSTR::null(), "URL:ssh Protocol")?;
|
/// `openssh_protocol_handler.bat` uses — this is what actually shows up in and gets
|
||||||
reg_set_sz(ssh_key, w!("URL Protocol"), "")?;
|
/// 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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user