# HG changeset patch # User Brad Greco # Date 1551406772 18000 # Node ID b6fe203af9d5409d1d61a40bb73b2d7aa91e419c # Parent 3142e52cbe69c9799d0e9eecd1a2ef59f9a4a459 Private key passwords and validation diff -r 3142e52cbe69 -r b6fe203af9d5 ServerMonitor/Forms/InputDialog.Designer.cs --- /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 + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.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 diff -r 3142e52cbe69 -r b6fe203af9d5 ServerMonitor/Forms/InputDialog.cs --- /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; + } + } +} diff -r 3142e52cbe69 -r b6fe203af9d5 ServerMonitor/Forms/InputDialog.resx --- /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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff -r 3142e52cbe69 -r b6fe203af9d5 ServerMonitor/Forms/ServerForm.Designer.cs --- 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 // diff -r 3142e52cbe69 -r b6fe203af9d5 ServerMonitor/Forms/ServerForm.cs --- 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().Where(c => c is TextBox)) control.TextChanged += (sender, e) => UpdateServer(false); foreach (Control control in ServerInfoPanel.Controls.OfType().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(); + } } } diff -r 3142e52cbe69 -r b6fe203af9d5 ServerMonitor/Forms/ServerSummaryForm.cs --- 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().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().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; } diff -r 3142e52cbe69 -r b6fe203af9d5 ServerMonitor/Helpers.cs --- 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; diff -r 3142e52cbe69 -r b6fe203af9d5 ServerMonitor/Objects/Checks/SshCheck.cs --- 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 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)) { diff -r 3142e52cbe69 -r b6fe203af9d5 ServerMonitor/Objects/Server.cs --- 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, + } + + } diff -r 3142e52cbe69 -r b6fe203af9d5 ServerMonitor/Objects/ServerMonitor.cs --- 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 tokens = new Dictionary(); + private readonly Dictionary privateKeys = new Dictionary(); private readonly List pausedChecks = new List(); private bool running, networkAvailable; private Dictionary, int> tasks; @@ -33,6 +36,8 @@ public string ConfigFile { get; private set; } + public IEnumerable 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 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()) diff -r 3142e52cbe69 -r b6fe203af9d5 ServerMonitor/ServerMonitor.csproj --- 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 @@ Component + + Form + + + InputDialog.cs + Form @@ -179,6 +185,9 @@ CheckForm.cs + + InputDialog.cs + QuickHelpForm.cs