Mercurial > servermonitor
changeset 5:b6fe203af9d5
Private key passwords and validation
author | Brad Greco <brad@bgreco.net> |
---|---|
date | Thu, 28 Feb 2019 21:19:32 -0500 |
parents | 3142e52cbe69 |
children | c1dffaac66fa |
files | ServerMonitor/Forms/InputDialog.Designer.cs ServerMonitor/Forms/InputDialog.cs ServerMonitor/Forms/InputDialog.resx ServerMonitor/Forms/ServerForm.Designer.cs ServerMonitor/Forms/ServerForm.cs ServerMonitor/Forms/ServerSummaryForm.cs ServerMonitor/Helpers.cs ServerMonitor/Objects/Checks/SshCheck.cs ServerMonitor/Objects/Server.cs ServerMonitor/Objects/ServerMonitor.cs ServerMonitor/ServerMonitor.csproj |
diffstat | 11 files changed, 508 insertions(+), 23 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ServerMonitor/Forms/InputDialog.Designer.cs Thu Feb 28 21:19:32 2019 -0500 @@ -0,0 +1,137 @@ +namespace ServerMonitorApp +{ + partial class InputDialog + { + /// <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.OkButton = new System.Windows.Forms.Button(); + this.InputCancelButton = new System.Windows.Forms.Button(); + this.MessageLabel = new System.Windows.Forms.Label(); + this.panel1 = new System.Windows.Forms.Panel(); + this.MessageIconPictureBox = new System.Windows.Forms.PictureBox(); + this.InputTextBox = new System.Windows.Forms.TextBox(); + this.panel1.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)(this.MessageIconPictureBox)).BeginInit(); + this.SuspendLayout(); + // + // OkButton + // + this.OkButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.OkButton.DialogResult = System.Windows.Forms.DialogResult.OK; + this.OkButton.Location = new System.Drawing.Point(211, 13); + this.OkButton.Name = "OkButton"; + this.OkButton.Size = new System.Drawing.Size(75, 23); + this.OkButton.TabIndex = 1; + this.OkButton.Text = "&OK"; + this.OkButton.UseVisualStyleBackColor = true; + // + // InputCancelButton + // + this.InputCancelButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.InputCancelButton.DialogResult = System.Windows.Forms.DialogResult.Cancel; + this.InputCancelButton.Location = new System.Drawing.Point(292, 13); + this.InputCancelButton.Name = "InputCancelButton"; + this.InputCancelButton.Size = new System.Drawing.Size(75, 23); + this.InputCancelButton.TabIndex = 2; + this.InputCancelButton.Text = "&Cancel"; + this.InputCancelButton.UseVisualStyleBackColor = true; + // + // MessageLabel + // + this.MessageLabel.AutoSize = true; + this.MessageLabel.Location = new System.Drawing.Point(75, 31); + this.MessageLabel.Name = "MessageLabel"; + this.MessageLabel.Size = new System.Drawing.Size(34, 13); + this.MessageLabel.TabIndex = 3; + this.MessageLabel.Text = "Input:"; + // + // panel1 + // + this.panel1.BackColor = System.Drawing.SystemColors.Control; + this.panel1.Controls.Add(this.OkButton); + this.panel1.Controls.Add(this.InputCancelButton); + this.panel1.Dock = System.Windows.Forms.DockStyle.Bottom; + this.panel1.Location = new System.Drawing.Point(0, 84); + this.panel1.Name = "panel1"; + this.panel1.Size = new System.Drawing.Size(379, 49); + this.panel1.TabIndex = 5; + // + // MessageIconPictureBox + // + this.MessageIconPictureBox.Location = new System.Drawing.Point(25, 41); + this.MessageIconPictureBox.Name = "MessageIconPictureBox"; + this.MessageIconPictureBox.Size = new System.Drawing.Size(32, 32); + this.MessageIconPictureBox.TabIndex = 4; + this.MessageIconPictureBox.TabStop = false; + // + // InputTextBox + // + this.InputTextBox.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.InputTextBox.Location = new System.Drawing.Point(78, 55); + this.InputTextBox.Name = "InputTextBox"; + this.InputTextBox.PasswordChar = '●'; + this.InputTextBox.Size = new System.Drawing.Size(289, 20); + this.InputTextBox.TabIndex = 1; + this.InputTextBox.TextChanged += new System.EventHandler(this.InputTextBox_TextChanged); + // + // InputDialog + // + this.AcceptButton = this.OkButton; + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.BackColor = System.Drawing.SystemColors.Control; + this.CancelButton = this.InputCancelButton; + this.ClientSize = new System.Drawing.Size(379, 133); + this.Controls.Add(this.InputTextBox); + this.Controls.Add(this.panel1); + this.Controls.Add(this.MessageIconPictureBox); + this.Controls.Add(this.MessageLabel); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog; + this.MaximizeBox = false; + this.MinimizeBox = false; + this.Name = "InputDialog"; + this.ShowInTaskbar = false; + this.Text = "Unlock private key"; + this.Load += new System.EventHandler(this.InputDialog_Load); + this.panel1.ResumeLayout(false); + ((System.ComponentModel.ISupportInitialize)(this.MessageIconPictureBox)).EndInit(); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private System.Windows.Forms.Button OkButton; + private System.Windows.Forms.Button InputCancelButton; + private System.Windows.Forms.Label MessageLabel; + private System.Windows.Forms.PictureBox MessageIconPictureBox; + private System.Windows.Forms.Panel panel1; + private System.Windows.Forms.TextBox InputTextBox; + } +} \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ServerMonitor/Forms/InputDialog.cs Thu Feb 28 21:19:32 2019 -0500 @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace ServerMonitorApp +{ + public partial class InputDialog : Form + { + public string Message { get; set; } + + public Icon MessageIcon { get; set; } + + public string Input { get; private set; } + + public InputDialog() + { + InitializeComponent(); + } + + private void InputDialog_Load(object sender, EventArgs e) + { + MessageIconPictureBox.Image = (MessageIcon ?? SystemIcons.Question).ToBitmap(); + MessageLabel.Text = Message; + } + + public static string ShowDialog(string message, Icon icon = null, IWin32Window owner = null) + { + using (InputDialog dialog = new InputDialog() { Message = message, MessageIcon = icon }) + { + return dialog.ShowDialog(owner) == DialogResult.OK ? dialog.Input : null; + } + } + + private void InputTextBox_TextChanged(object sender, EventArgs e) + { + Input = InputTextBox.Text; + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ServerMonitor/Forms/InputDialog.resx Thu Feb 28 21:19:32 2019 -0500 @@ -0,0 +1,120 @@ +<?xml version="1.0" encoding="utf-8"?> +<root> + <!-- + Microsoft ResX Schema + + Version 2.0 + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes + associated with the data types. + + Example: + + ... ado.net/XML headers & schema ... + <resheader name="resmimetype">text/microsoft-resx</resheader> + <resheader name="version">2.0</resheader> + <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader> + <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader> + <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data> + <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data> + <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64"> + <value>[base64 mime encoded serialized .NET Framework object]</value> + </data> + <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> + <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> + <comment>This is a comment</comment> + </data> + + There are any number of "resheader" rows that contain simple + name/value pairs. + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the + mimetype set. + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not + extensible. For a given mimetype the value must be set accordingly: + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can + read any of the formats listed below. + + mimetype: application/x-microsoft.net.object.binary.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.soap.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Soap.SoapFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.bytearray.base64 + value : The object must be serialized into a byte array + : using a System.ComponentModel.TypeConverter + : and then encoded with base64 encoding. + --> + <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> +</root> \ No newline at end of file
--- a/ServerMonitor/Forms/ServerForm.Designer.cs Sun Feb 10 20:51:26 2019 -0500 +++ b/ServerMonitor/Forms/ServerForm.Designer.cs Thu Feb 28 21:19:32 2019 -0500 @@ -192,6 +192,7 @@ this.KeyTextBox.Name = "KeyTextBox"; this.KeyTextBox.Size = new System.Drawing.Size(202, 20); this.KeyTextBox.TabIndex = 13; + this.KeyTextBox.Leave += new System.EventHandler(this.KeyTextBox_Leave); // // LoginLabel //
--- a/ServerMonitor/Forms/ServerForm.cs Sun Feb 10 20:51:26 2019 -0500 +++ b/ServerMonitor/Forms/ServerForm.cs Thu Feb 28 21:19:32 2019 -0500 @@ -1,9 +1,12 @@ -using ServerMonitorApp.Properties; +using Renci.SshNet; +using Renci.SshNet.Common; +using ServerMonitorApp.Properties; using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; +using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -54,8 +57,7 @@ } else { - Text = Server.Name; - TitleLabel.Text = Server.Name; + SetTitle(); NameTextBox.Text = Server.Name; HostTextBox.Text = Server.Host; PortTextBox.Text = Server.Port.ToString(); @@ -114,10 +116,16 @@ } } + private void SetTitle(string title = null) + { + title = (title ?? Server.Name) + (Server.Enabled ? "" : " (disabled)"); + Text = title; + TitleLabel.Text = title; + } + private void NameTextBox_TextChanged(object sender, EventArgs e) { - Text = NameTextBox.Text; - TitleLabel.Text = NameTextBox.Text; + SetTitle(NameTextBox.Text); } private void LoginComboBox_SelectedIndexChanged(object sender, EventArgs e) @@ -231,6 +239,7 @@ private void BindChangeListeners() { + Server.EnabledChanged += Server_EnabledChanged; foreach (Control control in ServerInfoPanel.Controls.OfType<Control>().Where(c => c is TextBox)) control.TextChanged += (sender, e) => UpdateServer(false); foreach (Control control in ServerInfoPanel.Controls.OfType<Control>().Where(c => c is ComboBox)) @@ -383,5 +392,45 @@ { return filteredStatuses.Contains(result.CheckStatus) && (LogCheckComboBox.SelectedIndex == 0 || LogCheckComboBox.SelectedItem == result.Check); } + + private void KeyTextBox_Leave(object sender, EventArgs e) + { + OpenPrivateKey(monitor, Server, this); + } + + public static void OpenPrivateKey(ServerMonitor monitor, Server server, IWin32Window owner) + { + if (server.LoginType != LoginType.PrivateKey) + return; + + KeyStatus keyStatus = monitor.OpenPrivateKey(server.KeyFile); + if (keyStatus == KeyStatus.NeedPassword) + { + string message = "Private key password for " + server.Name + ":"; + Icon icon = SystemIcons.Question; + while (keyStatus != KeyStatus.Open) + { + string password = InputDialog.ShowDialog(message, icon, owner); + if (password == null) + return; + keyStatus = monitor.OpenPrivateKey(server.KeyFile, password); + if (keyStatus == KeyStatus.NeedPassword) + { + message = "Incorrect private key password for " + server.Name + ", please try again:"; + icon = SystemIcons.Error; + } + } + } + else if (keyStatus == KeyStatus.NotAccessible) + { + MessageBox.Show("The private key file " + server.KeyFile + " is not accessible or does not exist.", "Cannot open private key", MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } + } + + private void Server_EnabledChanged(object sender, EventArgs e) + { + SetTitle(); + } } }
--- a/ServerMonitor/Forms/ServerSummaryForm.cs Sun Feb 10 20:51:26 2019 -0500 +++ b/ServerMonitor/Forms/ServerSummaryForm.cs Thu Feb 28 21:19:32 2019 -0500 @@ -64,6 +64,7 @@ NotifyIcon.Icon = new Icon(Icon, 16, 16); monitor.CheckStatusChanged += Monitor_CheckStatusChanged; RefreshDisplay(); + CollectPrivateKeyPasswords(); } private ServerForm ShowServerForm(Server server, bool activate = true) @@ -95,6 +96,8 @@ ServerPanel.Controls.Clear(); foreach (Server server in monitor.Servers) { + server.EnabledChanged -= Server_EnabledChanged; + server.EnabledChanged += Server_EnabledChanged; ServerSummaryControl control = new ServerSummaryControl(server); control.ContextMenuStrip = ServerContextMenu; control.Click += ServerSummaryControl_Click; @@ -102,10 +105,28 @@ } } + private void RefreshServer(Server server) + { + ServerPanel.Controls.Cast<ServerSummaryControl>().FirstOrDefault(c => c.Server == server).Refresh(); + } + + private void CollectPrivateKeyPasswords() + { + foreach (Server server in monitor.Servers) + { + ServerForm.OpenPrivateKey(monitor, server, this); + } + } + + private void Server_EnabledChanged(object sender, EventArgs e) + { + RefreshServer((Server)sender); + } + private void Monitor_CheckStatusChanged(object sender, CheckStatusChangedEventArgs e) { if (e.CheckResult != null) - ServerPanel.Controls.Cast<ServerSummaryControl>().FirstOrDefault(c => c.Server == e.Check.Server).Refresh(); + RefreshServer(e.Check.Server); } private ToolTipIcon GetToolTipIcon(CheckStatus status) @@ -166,7 +187,7 @@ private void ServerContextMenu_ItemClicked(object sender, ToolStripItemClickedEventArgs e) { - Server server = getClickedServer((ContextMenuStrip)e.ClickedItem.Owner); + Server server = GetClickedServer((ContextMenuStrip)e.ClickedItem.Owner); if (e.ClickedItem == DeleteServerMenuItem) { ServerContextMenu.Close(); @@ -184,6 +205,11 @@ else if (e.ClickedItem == ToggleEnableServerMenuItem) { bool enable = ToggleEnableServerMenuItem.Text == "Enable"; + if (enable) + { + ServerContextMenu.Close(); + ServerForm.OpenPrivateKey(monitor, server, this); + } server.Enabled = enable; RefreshDisplay(); } @@ -191,11 +217,11 @@ private void ServerContextMenu_Opening(object sender, CancelEventArgs e) { - Server server = getClickedServer((ContextMenuStrip)sender); + Server server = GetClickedServer((ContextMenuStrip)sender); ToggleEnableServerMenuItem.Text = server.Enabled ? "Disable" : "Enable"; } - private Server getClickedServer(ContextMenuStrip menu) + private Server GetClickedServer(ContextMenuStrip menu) { return ((ServerSummaryControl)menu.SourceControl).Server; }
--- a/ServerMonitor/Helpers.cs Sun Feb 10 20:51:26 2019 -0500 +++ b/ServerMonitor/Helpers.cs Thu Feb 28 21:19:32 2019 -0500 @@ -1,4 +1,6 @@ -using ServerMonitorApp.Properties; +using Renci.SshNet; +using Renci.SshNet.Common; +using ServerMonitorApp.Properties; using System; using System.Collections.Generic; using System.ComponentModel;
--- a/ServerMonitor/Objects/Checks/SshCheck.cs Sun Feb 10 20:51:26 2019 -0500 +++ b/ServerMonitor/Objects/Checks/SshCheck.cs Thu Feb 28 21:19:32 2019 -0500 @@ -29,12 +29,19 @@ protected override Task<CheckResult> ExecuteCheckAsync(CancellationToken token) { + try + { + if (!Server.SshClient.IsConnected) + Server.SshClient.Connect(); + } + catch (Exception e) + { + return Task.FromResult(Fail(e)); + } return Task.Run(() => { try { - if (!Server.SshClient.IsConnected) - Server.SshClient.Connect(); token.ThrowIfCancellationRequested(); using (SshCommand command = Server.SshClient.CreateCommand(Command)) {
--- a/ServerMonitor/Objects/Server.cs Sun Feb 10 20:51:26 2019 -0500 +++ b/ServerMonitor/Objects/Server.cs Thu Feb 28 21:19:32 2019 -0500 @@ -6,6 +6,7 @@ using System.ComponentModel; using Renci.SshNet; using System.Runtime.Serialization; +using System.Xml.Serialization; namespace ServerMonitorApp { @@ -21,6 +22,7 @@ private SshClient _sshClient; private bool _enabled = true; private byte[] passwordHash; + private PrivateKeyFile _privateKeyFile; public event EventHandler CheckModified; public event EventHandler EnabledChanged; @@ -70,17 +72,51 @@ set { passwordHash = ProtectedData.Protect(Encoding.UTF8.GetBytes(value), - Encoding.UTF8.GetBytes("Server".Reverse().ToString()), // Minor obfuscation of additional entropy - DataProtectionScope.CurrentUser); + Encoding.UTF8.GetBytes("Server".Reverse().ToString()), // Minor obfuscation of additional entropy + DataProtectionScope.CurrentUser); } } + [XmlIgnore] + public PrivateKeyFile PrivateKeyFile + { + get { return _privateKeyFile; } + set + { + _privateKeyFile = value; + if (LoginType == LoginType.PrivateKey) + { + if (_privateKeyFile == null) + { + KeyStatus = KeyStatus.Closed; + Enabled = false; + } + else + { + if (!KeyStatus.In(KeyStatus.Open, KeyStatus.Closed)) + Enabled = true; + KeyStatus = KeyStatus.Open; + } + } + } + } + + public KeyStatus KeyStatus { get; set; } + public bool Enabled { get { return _enabled; } - set { _enabled = value; EnabledChanged?.Invoke(this, new EventArgs()); } + set + { + if (LoginType == LoginType.PrivateKey && PrivateKeyFile == null && value == true) + return; + _enabled = value; + EnabledChanged?.Invoke(this, new EventArgs()); + } } + //public bool WaitingForUser { get; set; } + public CheckStatus Status => !Enabled ? CheckStatus.Disabled : Checks .Where(c => c.Enabled) .Select(c => c.LastRunStatus) @@ -93,12 +129,7 @@ { if (_sshClient == null) { - AuthenticationMethod auth = null; - if (LoginType == LoginType.Password) - auth = new PasswordAuthenticationMethod(Username, Password); - else - auth = new PrivateKeyAuthenticationMethod(Username, new PrivateKeyFile(KeyFile)); - ConnectionInfo info = new ConnectionInfo(Host, Port, Username, auth); + ConnectionInfo info = new ConnectionInfo(Host, Port, Username, GetAuthentication()); _sshClient = new SshClient(info); } return _sshClient; @@ -156,10 +187,28 @@ && Checks.Count == 0; } + private AuthenticationMethod GetAuthentication() + { + if (LoginType == LoginType.Password) + return new PasswordAuthenticationMethod(Username, Password); + else + return new PrivateKeyAuthenticationMethod(Username, PrivateKeyFile); + } + private void InvalidateSshConnection() { _sshClient?.Dispose(); _sshClient = null; } } + + public enum KeyStatus + { + Closed, + Open, + NotAccessible, + NeedPassword, + } + + }
--- a/ServerMonitor/Objects/ServerMonitor.cs Sun Feb 10 20:51:26 2019 -0500 +++ b/ServerMonitor/Objects/ServerMonitor.cs Thu Feb 28 21:19:32 2019 -0500 @@ -1,4 +1,6 @@ -using ServerMonitorApp.Properties; +using Renci.SshNet; +using Renci.SshNet.Common; +using ServerMonitorApp.Properties; using System; using System.Collections.Generic; using System.Diagnostics; @@ -18,6 +20,7 @@ private readonly string configFileDir; private readonly Logger logger; private readonly Dictionary<int, CancellationTokenSource> tokens = new Dictionary<int, CancellationTokenSource>(); + private readonly Dictionary<string, PrivateKeyFile> privateKeys = new Dictionary<string, PrivateKeyFile>(); private readonly List<int> pausedChecks = new List<int>(); private bool running, networkAvailable; private Dictionary<Task<CheckResult>, int> tasks; @@ -33,6 +36,8 @@ public string ConfigFile { get; private set; } + public IEnumerable<string> LockedKeys { get { return privateKeys.Where(kvp => kvp.Value == null).Select(kvp => kvp.Key); } } + public ServerMonitor(ServerSummaryForm mainForm) { this.mainForm = mainForm; @@ -67,6 +72,8 @@ // that doesn't work when using the XML serializer for some reason. foreach (Server server in Servers) { + if (server.LoginType == LoginType.PrivateKey) + OpenPrivateKey(server.KeyFile); foreach (Check check in server.Checks) { check.Server = server; @@ -236,7 +243,8 @@ private void CancelCheck(Check check) { Task<CheckResult> task = tasks.FirstOrDefault(kvp => kvp.Value == check.Id).Key; - tasks.Remove(task); + if (task != null) + tasks.Remove(task); pausedChecks.RemoveAll(id => id == check.Id); if (tokens.TryGetValue(check.Id, out CancellationTokenSource cts)) cts.Cancel(); @@ -269,6 +277,38 @@ mainForm.Invoke((MethodInvoker)(() => Run())); } + public KeyStatus OpenPrivateKey(string path, string password = null) + { + KeyStatus keyStatus; + if (path == null) + return KeyStatus.NotAccessible; + if (privateKeys.TryGetValue(path, out PrivateKeyFile key) && key != null) + return KeyStatus.Open; + try + { + key = new PrivateKeyFile(path, password); + keyStatus = KeyStatus.Open; + } + catch (Exception e) when (e is SshPassPhraseNullOrEmptyException || e is InvalidOperationException) + { + keyStatus = KeyStatus.NeedPassword; + } + catch (Exception) + { + keyStatus = KeyStatus.NotAccessible; + } + foreach (Server server in Servers) + { + if (server.KeyFile == path) + { + server.PrivateKeyFile = key; + server.KeyStatus = keyStatus; + } + } + privateKeys[path] = key; + return keyStatus; + } + private void GenerateIds() { if (Servers.Any())
--- a/ServerMonitor/ServerMonitor.csproj Sun Feb 10 20:51:26 2019 -0500 +++ b/ServerMonitor/ServerMonitor.csproj Thu Feb 28 21:19:32 2019 -0500 @@ -95,6 +95,12 @@ <Compile Include="Controls\MatchComboBox.cs"> <SubType>Component</SubType> </Compile> + <Compile Include="Forms\InputDialog.cs"> + <SubType>Form</SubType> + </Compile> + <Compile Include="Forms\InputDialog.Designer.cs"> + <DependentUpon>InputDialog.cs</DependentUpon> + </Compile> <Compile Include="Forms\SettingsForm.cs"> <SubType>Form</SubType> </Compile> @@ -179,6 +185,9 @@ <EmbeddedResource Include="Forms\CheckForm.resx"> <DependentUpon>CheckForm.cs</DependentUpon> </EmbeddedResource> + <EmbeddedResource Include="Forms\InputDialog.resx"> + <DependentUpon>InputDialog.cs</DependentUpon> + </EmbeddedResource> <EmbeddedResource Include="Forms\QuickHelpForm.resx"> <DependentUpon>QuickHelpForm.cs</DependentUpon> </EmbeddedResource>