mirror of
https://github.com/ergosteur/ssh-protocol-handler-win.git
synced 2026-10-28 16:10:36 -04:00
Compare commits
5
Commits
v0.1.2
..
89ec41a0cd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
89ec41a0cd | ||
|
|
7daf668a11 | ||
|
|
905487ce82 | ||
|
|
1bd676d2fe | ||
|
|
a671325a34 |
+11
-35
@@ -16,38 +16,14 @@ jobs:
|
|||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Configure Git Line Endings
|
- name: Setup Rust
|
||||||
run: git config --global core.autocrlf input
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
- name: Setup .NET
|
|
||||||
uses: actions/setup-dotnet@v4
|
|
||||||
with:
|
with:
|
||||||
dotnet-version: 8.0.x
|
targets: x86_64-pc-windows-msvc
|
||||||
|
|
||||||
- name: Restore dependencies
|
- name: Build ssh-handler (Rust)
|
||||||
run: dotnet restore
|
working-directory: SshHandlerRs
|
||||||
|
run: cargo build --release --target x86_64-pc-windows-msvc
|
||||||
# 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: Ensure Unix Line Endings for Shell Script
|
- name: Ensure Unix Line Endings for Shell Script
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
@@ -64,10 +40,11 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
$tag = "${{ github.ref_name }}"
|
$tag = "${{ github.ref_name }}"
|
||||||
if (-not $tag -or $tag -eq "main") { $tag = "manual" }
|
if (-not $tag -or $tag -eq "main") { $tag = "manual" }
|
||||||
|
# Branch names (workflow_dispatch) can contain "/", which breaks file paths.
|
||||||
|
$tag = $tag -replace '[\\/]', '-'
|
||||||
|
|
||||||
# Rename Executables
|
# Rename Rust executable
|
||||||
Move-Item ./publish-standalone/SshHandler.exe "./SshHandler-$tag-Standalone.exe"
|
Move-Item SshHandlerRs/target/x86_64-pc-windows-msvc/release/ssh-handler.exe "./ssh-handler-rs-$tag.exe"
|
||||||
Move-Item ./publish-dotnet8/SshHandler.exe "./SshHandler-$tag-dotnet8.exe"
|
|
||||||
|
|
||||||
# Rename Scripts
|
# Rename Scripts
|
||||||
Copy-Item "ssh-handler.sh" "./ssh-handler-$tag.sh"
|
Copy-Item "ssh-handler.sh" "./ssh-handler-$tag.sh"
|
||||||
@@ -78,8 +55,7 @@ jobs:
|
|||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
SshHandler-*-Standalone.exe
|
ssh-handler-rs-*.exe
|
||||||
SshHandler-*-dotnet8.exe
|
|
||||||
ssh-handler-*.sh
|
ssh-handler-*.sh
|
||||||
openssh_protocol_handler-*.bat
|
openssh_protocol_handler-*.bat
|
||||||
generate_release_notes: true
|
generate_release_notes: true
|
||||||
|
|||||||
+2
-19
@@ -1,21 +1,4 @@
|
|||||||
# .gitignore
|
# .gitignore
|
||||||
|
|
||||||
# User-specific files
|
# Rust
|
||||||
*.rsuser
|
/SshHandlerRs/target/
|
||||||
*.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/
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
Generated
-113
@@ -1,113 +0,0 @@
|
|||||||
partial class Form1
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Required designer variable.
|
|
||||||
/// </summary>
|
|
||||||
private System.ComponentModel.IContainer components = null;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Clean up any resources being used.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
|
||||||
protected override void Dispose(bool disposing)
|
|
||||||
{
|
|
||||||
if (disposing && (components != null))
|
|
||||||
{
|
|
||||||
components.Dispose();
|
|
||||||
}
|
|
||||||
base.Dispose(disposing);
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Windows Form Designer generated code
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Required method for Designer support - do not modify
|
|
||||||
/// the contents of this method with the code editor.
|
|
||||||
/// </summary>
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<root>
|
|
||||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
|
||||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
|
||||||
<xsd:element name="root" msdata:IsDataSet="true">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:choice maxOccurs="unbounded">
|
|
||||||
<xsd:element name="metadata">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:sequence>
|
|
||||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
|
||||||
</xsd:sequence>
|
|
||||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
|
||||||
<xsd:attribute name="type" type="xsd:string" />
|
|
||||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
|
||||||
<xsd:attribute ref="xml:space" />
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
<xsd:element name="assembly">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:attribute name="alias" type="xsd:string" />
|
|
||||||
<xsd:attribute name="name" type="xsd:string" />
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
<xsd:element name="data">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:sequence>
|
|
||||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
|
||||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
|
||||||
</xsd:sequence>
|
|
||||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
|
||||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
|
||||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
|
||||||
<xsd:attribute ref="xml:space" />
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
<xsd:element name="resheader">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:sequence>
|
|
||||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
|
||||||
</xsd:sequence>
|
|
||||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
</xsd:choice>
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
</xsd:schema>
|
|
||||||
<resheader name="resmimetype">
|
|
||||||
<value>text/microsoft-resx</value>
|
|
||||||
</resheader>
|
|
||||||
<resheader name="version">
|
|
||||||
<value>2.0</value>
|
|
||||||
</resheader>
|
|
||||||
<resheader name="reader">
|
|
||||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
|
||||||
</resheader>
|
|
||||||
<resheader name="writer">
|
|
||||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
|
||||||
</resheader>
|
|
||||||
<data name="$this.ClientSize" type="System.Drawing.Size, System.Drawing">
|
|
||||||
<value>384, 141</value>
|
|
||||||
</data>
|
|
||||||
<data name="$this.Text" xml:space="preserve">
|
|
||||||
<value>SSH Connection</value>
|
|
||||||
</data>
|
|
||||||
</root>
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<OutputType>WinExe</OutputType>
|
|
||||||
<TargetFramework>net8.0-windows</TargetFramework>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<UseWindowsForms>true</UseWindowsForms>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<ApplicationIcon>ssh_icon.ico</ApplicationIcon>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
Generated
+460
@@ -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",
|
||||||
|
]
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# ssh-handler (Rust)
|
||||||
|
|
||||||
|
The Windows `ssh://` protocol handler, in Rust — raw Win32 (via `windows-rs`)
|
||||||
|
instead of a managed GUI framework. This started as a rewrite of an earlier
|
||||||
|
.NET/WinForms version (since removed) whose self-contained publish was
|
||||||
|
~150MB and paid 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
|
||||||
|
|
||||||
|
CI (`.github/workflows/build.yml`) builds natively on `windows-latest` with
|
||||||
|
the MSVC toolchain:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo build --release --target x86_64-pc-windows-msvc
|
||||||
|
```
|
||||||
|
|
||||||
|
Cross-compiling from Linux also works, e.g. for local iteration:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
rustup target add x86_64-pc-windows-gnu
|
||||||
|
sudo apt install mingw-w64
|
||||||
|
cargo build --release --target x86_64-pc-windows-gnu
|
||||||
|
```
|
||||||
|
|
||||||
|
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 registers `ssh_handler_rs` as an `ssh://` handler using the "Default
|
||||||
|
Programs" capability model (a dedicated ProgID with
|
||||||
|
`Capabilities\UrlAssociations`, plus a
|
||||||
|
`HKLM\SOFTWARE\RegisteredApplications` entry) — the same pattern
|
||||||
|
`openssh_protocol_handler.bat` uses, and the one Windows 10/11 actually
|
||||||
|
honors in Settings > 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
|
||||||
|
KEX/HostKey/MAC/cipher algorithms for old SSH servers), then launches
|
||||||
|
`ssh.exe -A -C ... user@host` via `ShellExecuteW`.
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
embed_resource::compile("resources/ssh_handler.rc", embed_resource::NONE);
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
#define IDI_APPICON 102
|
||||||
|
#define IDD_CONNECT 101
|
||||||
|
#define IDC_EDIT_TARGET 1001
|
||||||
|
#define IDC_CHECK_LEGACY 1002
|
||||||
@@ -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
|
||||||
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.6 KiB |
@@ -0,0 +1,311 @@
|
|||||||
|
#![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, HKEY_LOCAL_MACHINE, 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";
|
||||||
|
|
||||||
|
/// 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> {
|
||||||
|
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 ({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(&exe_path, &command_str) {
|
||||||
|
Ok(()) => {
|
||||||
|
message_box(
|
||||||
|
&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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
message_box(
|
||||||
|
&format!("Failed to write to registry:\n{e:?}"),
|
||||||
|
"Error",
|
||||||
|
MB_OK | MB_ICONERROR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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(),
|
||||||
|
};
|
||||||
|
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(root: HKEY, subkey: &str) -> windows::core::Result<HKEY> {
|
||||||
|
let subkey_wide = to_wide(subkey);
|
||||||
|
let mut key = HKEY::default();
|
||||||
|
unsafe {
|
||||||
|
RegCreateKeyExW(
|
||||||
|
root,
|
||||||
|
PCWSTR(subkey_wide.as_ptr()),
|
||||||
|
0,
|
||||||
|
None,
|
||||||
|
REG_OPTION_NON_VOLATILE,
|
||||||
|
KEY_WRITE,
|
||||||
|
None,
|
||||||
|
&mut key,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.ok()?;
|
||||||
|
Ok(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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"),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user