view ServerMonitor/Objects/Server.cs @ 17:68d7834dc28e

More comments.
author Brad Greco <brad@bgreco.net>
date Sat, 25 May 2019 15:14:26 -0400
parents 052aa62cb42a
children 06ff59b59e70
line wrap: on
line source

using System;
using System.Linq;
using System.Text;
using System.Security.Cryptography;
using System.ComponentModel;
using Renci.SshNet;
using System.Xml.Serialization;

namespace ServerMonitorApp
{
    /// <summary>Types of SSH logins supported by the server monitor.</summary>
    public enum LoginType { PrivateKey = 0, Password = 1 };

    /// <summary>Remote server that checks can be run against.</summary>
    public class Server
    {
        private string _host;
        private int _port;
        private string _username;
        private LoginType _loginType;
        private string _keyFile;
        private SshClient _sshClient;
        private bool _enabled = true;
        private byte[] passwordHash;
        private PrivateKeyFile _privateKeyFile;

        /// <summary>Fires when a check belonging to this server is modifed.</summary>
        public event EventHandler CheckModified;
        /// <summary>Fires when the server enabled state changes.</summary>
        public event EventHandler EnabledChanged;

        /// <summary>The checks that belong to this server.</summary>
        public readonly BindingList<Check> Checks = new BindingList<Check>();

        /// <summary>Internal ID of the server.</summary>
        public int Id { get; set; }

        /// <summary>Name of the server.</summary>
        public string Name { get; set; }

        /// <summary>Hostname of the server.</summary>
        public string Host
        {
            get { return _host; }
            set { _host = value; InvalidateSshConnection(); }
        }

        /// <summary>Port to use when connecting using SSH.</summary>
        public int Port
        {
            get { return _port; }
            set { _port = value; InvalidateSshConnection(); }
        }

        /// <summary>Username to use when connecting using SSH.</summary>
        public string Username
        {
            get { return _username; }
            set { _username = value; InvalidateSshConnection(); }
        }

        /// <summary>Login type to use when connecting using SSH.</summary>
        public LoginType LoginType
        {
            get { return _loginType; }
            set { _loginType = value; InvalidateSshConnection(); }
        }

        /// <summary>Path to the private key file to use when connecting using SSH.</summary>
        public string KeyFile
        {
            get { return _keyFile; }
            set { _keyFile = value; InvalidateSshConnection(); }
        }

        /// <summary>Password to use when connecting using SSH.</summary>
        /// <remarks>The password is encrypted using the current Windows user account.</remarks>
        public string Password
        {
            get {
                return passwordHash == null ? null :
                    Encoding.UTF8.GetString(ProtectedData.Unprotect(passwordHash
                    , Encoding.UTF8.GetBytes("Server".Reverse().ToString()) // Super-secure obfuscation of additional entropy
                    , DataProtectionScope.CurrentUser));
            }
            set
            {
                passwordHash = ProtectedData.Protect(Encoding.UTF8.GetBytes(value),
                                    Encoding.UTF8.GetBytes("Server".Reverse().ToString()), // Super-secure obfuscation of additional entropy
                                    DataProtectionScope.CurrentUser);
            }
        }

        /// <summary>Private key file to use when connecting using SSH.</summary>
        /// <remarks>
        /// If the private key file is encrypted, will be null until the user enters
        /// the decryption key.
        /// </remarks>
        [XmlIgnore]
        public PrivateKeyFile PrivateKeyFile
        {
            get { return _privateKeyFile; }
            set
            {
                _privateKeyFile = value;
                if (LoginType == LoginType.PrivateKey)
                {
                    if (_privateKeyFile == null)
                    {
                        // The private key has not been opened yet.
                        // Disable the server until the user enters the decryption key,
                        // and set the KeyStatus to indicate why the server was disabled.
                        KeyStatus = KeyStatus.Closed;
                        Enabled = false;
                    }
                    else
                    {
                        // The private key is open and accessible.
                        // Automatically re-enable the server if it was previously disabled
                        // due to a locked or inaccessible private key (i.e. disabled
                        // programatically and not by user request).
                        if (!KeyStatus.In(KeyStatus.Open, KeyStatus.Closed))
                            Enabled = true;
                        KeyStatus = KeyStatus.Open;
                    }
                }
            }
        }

        /// <summary>The current status of the private key file.</summary>
        public KeyStatus KeyStatus { get; set; }

        /// <summary>Whether this server's checks will be automatically executed on their schedules.</summary>
        public bool Enabled
        {
            get { return _enabled; }
            set
            {
                // Do not allow enabling the server if the private key is not accessible.
                // Do not fire the EnabledChanged event if the Enabled state is not actually changing
                // from its existing value.
                if ((LoginType == LoginType.PrivateKey && PrivateKeyFile == null && value == true) || value == _enabled)
                    return;
                _enabled = value;
                EnabledChanged?.Invoke(this, new EventArgs());
            }
        }

        /// <summary>The status of the server.</summary>
        /// <remarks>
        /// The status of the server is the most severe status of all its enabled checks.
        /// The integer value of the CheckStatus enum increases with the severity,
        /// so the maximum value of all checks gives the most severe status.
        /// </remarks>
        public CheckStatus Status => !Enabled ? CheckStatus.Disabled : Checks
            .Where(c => c.Enabled)
            .Select(c => c.LastRunStatus)
            .DefaultIfEmpty(CheckStatus.Success)
            .Max();

        /// <summary>The SSH client to use when running checks on the server.</summary>
        /// <remarks>
        /// The connection is stored and kept open at the server level so it can be reused
        /// by all SSH checks.
        /// </remarks>
        public SshClient SshClient
        {
            get
            {
                if (_sshClient == null)
                {
                    ConnectionInfo info = new ConnectionInfo(Host, Port, Username, GetAuthentication());
                    _sshClient = new SshClient(info);
                }
                return _sshClient;
            }
        }

        /// <summary>Deletes a check from the server.</summary>
        /// <param name="check">The check to delete.</param>
        public void DeleteCheck(Check check)
        {
            Checks.Remove(check);
            check.Server = null;
            CheckModified?.Invoke(check, new EventArgs());
        }

        /// <summary>Validates server settings.</summary>
        /// <returns>An empty string if the server is valid, or the reason the server is invalid.</returns>
        public string Validate()
        {
            string message = string.Empty;
            if (Name.Length == 0)
                message += "\"Name\" must not be empty" + Environment.NewLine;
            if (Host.Length == 0)
                message += "\"Host\" must not be empty" + Environment.NewLine;
            return message.Length > 0 ? message : null;
        }

        /// <summary>Updates a check.</summary>
        public void UpdateCheck(Check check)
        {
            // See if there is already a check with this ID.
            Check oldCheck = Checks.FirstOrDefault(c => c.Id == check.Id);
            if (!ReferenceEquals(check, oldCheck))
            {
                // If there is already a check, but it is a different object instance,
                // replace the old check with the new one (or add it if it is new).
                int index = Checks.IndexOf(oldCheck);
                if (index == -1)
                    Checks.Add(check);
                else
                    Checks[index] = check;
            }
            CheckModified?.Invoke(check, new EventArgs());
        }

        /// <summary>Returns true if the server looks empty (no user data has been entered).</summary>
        public bool IsEmpty()
        {
            return Name.IsNullOrEmpty()
                && Host.IsNullOrEmpty()
                && Checks.Count == 0;
        }

        /// <summary>Generates the authentication method based on user preferences.</summary>
        private AuthenticationMethod GetAuthentication()
        {
            if (LoginType == LoginType.Password)
                return new PasswordAuthenticationMethod(Username, Password);
            else
                return new PrivateKeyAuthenticationMethod(Username, PrivateKeyFile);
        }

        /// <summary>Releases the open SSH connection.</summary>
        private void InvalidateSshConnection()
        {
            _sshClient?.Dispose();
            _sshClient = null;
        }

        public override string ToString()
        {
            return Name.IsNullOrEmpty() ? Host : Name;
        }
    }

    /// <summary>Possible statuses of the private key file.</summary>
    public enum KeyStatus
    {
        /// <summary>The private key file is closed for an unspecified reason.</summary>
        Closed,
        /// <summary>The private key file is accessible and open.</summary>
        Open,
        /// <summary>The private key file is not accessible (missing, access denied, etc).</summary>
        NotAccessible,
        /// <summary>The private key file is encrypted and the user has not entered the password yet.</summary>
        NeedPassword,
    }

}