comparison 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
comparison
equal deleted inserted replaced
16:7626b099aefd 17:68d7834dc28e
1 using System; 1 using System;
2 using System.Collections.Generic;
3 using System.Linq; 2 using System.Linq;
4 using System.Text; 3 using System.Text;
5 using System.Security.Cryptography; 4 using System.Security.Cryptography;
6 using System.ComponentModel; 5 using System.ComponentModel;
7 using Renci.SshNet; 6 using Renci.SshNet;
8 using System.Runtime.Serialization;
9 using System.Xml.Serialization; 7 using System.Xml.Serialization;
10 8
11 namespace ServerMonitorApp 9 namespace ServerMonitorApp
12 { 10 {
11 /// <summary>Types of SSH logins supported by the server monitor.</summary>
13 public enum LoginType { PrivateKey = 0, Password = 1 }; 12 public enum LoginType { PrivateKey = 0, Password = 1 };
14 13
14 /// <summary>Remote server that checks can be run against.</summary>
15 public class Server 15 public class Server
16 { 16 {
17 private string _host; 17 private string _host;
18 private int _port; 18 private int _port;
19 private string _username; 19 private string _username;
22 private SshClient _sshClient; 22 private SshClient _sshClient;
23 private bool _enabled = true; 23 private bool _enabled = true;
24 private byte[] passwordHash; 24 private byte[] passwordHash;
25 private PrivateKeyFile _privateKeyFile; 25 private PrivateKeyFile _privateKeyFile;
26 26
27 /// <summary>Fires when a check belonging to this server is modifed.</summary>
27 public event EventHandler CheckModified; 28 public event EventHandler CheckModified;
29 /// <summary>Fires when the server enabled state changes.</summary>
28 public event EventHandler EnabledChanged; 30 public event EventHandler EnabledChanged;
29 31
32 /// <summary>The checks that belong to this server.</summary>
30 public readonly BindingList<Check> Checks = new BindingList<Check>(); 33 public readonly BindingList<Check> Checks = new BindingList<Check>();
31 34
35 /// <summary>Internal ID of the server.</summary>
32 public int Id { get; set; } 36 public int Id { get; set; }
33 37
38 /// <summary>Name of the server.</summary>
34 public string Name { get; set; } 39 public string Name { get; set; }
35 40
41 /// <summary>Hostname of the server.</summary>
36 public string Host 42 public string Host
37 { 43 {
38 get { return _host; } 44 get { return _host; }
39 set { _host = value; InvalidateSshConnection(); } 45 set { _host = value; InvalidateSshConnection(); }
40 } 46 }
41 47
48 /// <summary>Port to use when connecting using SSH.</summary>
42 public int Port 49 public int Port
43 { 50 {
44 get { return _port; } 51 get { return _port; }
45 set { _port = value; InvalidateSshConnection(); } 52 set { _port = value; InvalidateSshConnection(); }
46 } 53 }
47 54
55 /// <summary>Username to use when connecting using SSH.</summary>
48 public string Username 56 public string Username
49 { 57 {
50 get { return _username; } 58 get { return _username; }
51 set { _username = value; InvalidateSshConnection(); } 59 set { _username = value; InvalidateSshConnection(); }
52 } 60 }
53 61
62 /// <summary>Login type to use when connecting using SSH.</summary>
54 public LoginType LoginType 63 public LoginType LoginType
55 { 64 {
56 get { return _loginType; } 65 get { return _loginType; }
57 set { _loginType = value; InvalidateSshConnection(); } 66 set { _loginType = value; InvalidateSshConnection(); }
58 } 67 }
59 68
69 /// <summary>Path to the private key file to use when connecting using SSH.</summary>
60 public string KeyFile 70 public string KeyFile
61 { 71 {
62 get { return _keyFile; } 72 get { return _keyFile; }
63 set { _keyFile = value; InvalidateSshConnection(); } 73 set { _keyFile = value; InvalidateSshConnection(); }
64 } 74 }
65 75
76 /// <summary>Password to use when connecting using SSH.</summary>
77 /// <remarks>The password is encrypted using the current Windows user account.</remarks>
66 public string Password 78 public string Password
67 { 79 {
68 get { 80 get {
69 return passwordHash == null ? null : 81 return passwordHash == null ? null :
70 Encoding.UTF8.GetString(ProtectedData.Unprotect(passwordHash, Encoding.UTF8.GetBytes("Server".Reverse().ToString()), DataProtectionScope.CurrentUser)); 82 Encoding.UTF8.GetString(ProtectedData.Unprotect(passwordHash
83 , Encoding.UTF8.GetBytes("Server".Reverse().ToString()) // Super-secure obfuscation of additional entropy
84 , DataProtectionScope.CurrentUser));
71 } 85 }
72 set 86 set
73 { 87 {
74 passwordHash = ProtectedData.Protect(Encoding.UTF8.GetBytes(value), 88 passwordHash = ProtectedData.Protect(Encoding.UTF8.GetBytes(value),
75 Encoding.UTF8.GetBytes("Server".Reverse().ToString()), // Minor obfuscation of additional entropy 89 Encoding.UTF8.GetBytes("Server".Reverse().ToString()), // Super-secure obfuscation of additional entropy
76 DataProtectionScope.CurrentUser); 90 DataProtectionScope.CurrentUser);
77 } 91 }
78 } 92 }
79 93
94 /// <summary>Private key file to use when connecting using SSH.</summary>
95 /// <remarks>
96 /// If the private key file is encrypted, will be null until the user enters
97 /// the decryption key.
98 /// </remarks>
80 [XmlIgnore] 99 [XmlIgnore]
81 public PrivateKeyFile PrivateKeyFile 100 public PrivateKeyFile PrivateKeyFile
82 { 101 {
83 get { return _privateKeyFile; } 102 get { return _privateKeyFile; }
84 set 103 set
86 _privateKeyFile = value; 105 _privateKeyFile = value;
87 if (LoginType == LoginType.PrivateKey) 106 if (LoginType == LoginType.PrivateKey)
88 { 107 {
89 if (_privateKeyFile == null) 108 if (_privateKeyFile == null)
90 { 109 {
110 // The private key has not been opened yet.
111 // Disable the server until the user enters the decryption key,
112 // and set the KeyStatus to indicate why the server was disabled.
91 KeyStatus = KeyStatus.Closed; 113 KeyStatus = KeyStatus.Closed;
92 Enabled = false; 114 Enabled = false;
93 } 115 }
94 else 116 else
95 { 117 {
118 // The private key is open and accessible.
119 // Automatically re-enable the server if it was previously disabled
120 // due to a locked or inaccessible private key (i.e. disabled
121 // programatically and not by user request).
96 if (!KeyStatus.In(KeyStatus.Open, KeyStatus.Closed)) 122 if (!KeyStatus.In(KeyStatus.Open, KeyStatus.Closed))
97 Enabled = true; 123 Enabled = true;
98 KeyStatus = KeyStatus.Open; 124 KeyStatus = KeyStatus.Open;
99 } 125 }
100 } 126 }
101 } 127 }
102 } 128 }
103 129
130 /// <summary>The current status of the private key file.</summary>
104 public KeyStatus KeyStatus { get; set; } 131 public KeyStatus KeyStatus { get; set; }
105 132
133 /// <summary>Whether this server's checks will be automatically executed on their schedules.</summary>
106 public bool Enabled 134 public bool Enabled
107 { 135 {
108 get { return _enabled; } 136 get { return _enabled; }
109 set 137 set
110 { 138 {
139 // Do not allow enabling the server if the private key is not accessible.
140 // Do not fire the EnabledChanged event if the Enabled state is not actually changing
141 // from its existing value.
111 if ((LoginType == LoginType.PrivateKey && PrivateKeyFile == null && value == true) || value == _enabled) 142 if ((LoginType == LoginType.PrivateKey && PrivateKeyFile == null && value == true) || value == _enabled)
112 return; 143 return;
113 _enabled = value; 144 _enabled = value;
114 EnabledChanged?.Invoke(this, new EventArgs()); 145 EnabledChanged?.Invoke(this, new EventArgs());
115 } 146 }
116 } 147 }
117 148
118 //public bool WaitingForUser { get; set; } 149 /// <summary>The status of the server.</summary>
119 150 /// <remarks>
151 /// The status of the server is the most severe status of all its enabled checks.
152 /// The integer value of the CheckStatus enum increases with the severity,
153 /// so the maximum value of all checks gives the most severe status.
154 /// </remarks>
120 public CheckStatus Status => !Enabled ? CheckStatus.Disabled : Checks 155 public CheckStatus Status => !Enabled ? CheckStatus.Disabled : Checks
121 .Where(c => c.Enabled) 156 .Where(c => c.Enabled)
122 .Select(c => c.LastRunStatus) 157 .Select(c => c.LastRunStatus)
123 .DefaultIfEmpty(CheckStatus.Success) 158 .DefaultIfEmpty(CheckStatus.Success)
124 .Max(); 159 .Max();
125 160
161 /// <summary>The SSH client to use when running checks on the server.</summary>
162 /// <remarks>
163 /// The connection is stored and kept open at the server level so it can be reused
164 /// by all SSH checks.
165 /// </remarks>
126 public SshClient SshClient 166 public SshClient SshClient
127 { 167 {
128 get 168 get
129 { 169 {
130 if (_sshClient == null) 170 if (_sshClient == null)
134 } 174 }
135 return _sshClient; 175 return _sshClient;
136 } 176 }
137 } 177 }
138 178
139 /*public Server() { } 179 /// <summary>Deletes a check from the server.</summary>
140 180 /// <param name="check">The check to delete.</param>
141 public Server(Server server)
142 {
143 Name = server.Name;
144 Host = server.Host;
145 Port = server.Port;
146 Username = server.Username;
147 LoginType = server.LoginType;
148 KeyFile = server.KeyFile;
149 Enabled = server.Enabled;
150 }*/
151
152 public void DeleteCheck(Check check) 181 public void DeleteCheck(Check check)
153 { 182 {
154 Checks.Remove(check); 183 Checks.Remove(check);
155 check.Server = null; 184 check.Server = null;
156 CheckModified?.Invoke(check, new EventArgs()); 185 CheckModified?.Invoke(check, new EventArgs());
157 } 186 }
158 187
188 /// <summary>Validates server settings.</summary>
189 /// <returns>An empty string if the server is valid, or the reason the server is invalid.</returns>
159 public string Validate() 190 public string Validate()
160 { 191 {
161 string message = string.Empty; 192 string message = string.Empty;
162 if (Name.Length == 0) 193 if (Name.Length == 0)
163 message += "\"Name\" must not be empty" + Environment.NewLine; 194 message += "\"Name\" must not be empty" + Environment.NewLine;
164 if (Host.Length == 0) 195 if (Host.Length == 0)
165 message += "\"Host\" must not be empty" + Environment.NewLine; 196 message += "\"Host\" must not be empty" + Environment.NewLine;
166 return message.Length > 0 ? message : null; 197 return message.Length > 0 ? message : null;
167 } 198 }
168 199
200 /// <summary>Updates a check.</summary>
169 public void UpdateCheck(Check check) 201 public void UpdateCheck(Check check)
170 { 202 {
203 // See if there is already a check with this ID.
171 Check oldCheck = Checks.FirstOrDefault(c => c.Id == check.Id); 204 Check oldCheck = Checks.FirstOrDefault(c => c.Id == check.Id);
172 if (!ReferenceEquals(check, oldCheck)) 205 if (!ReferenceEquals(check, oldCheck))
173 { 206 {
207 // If there is already a check, but it is a different object instance,
208 // replace the old check with the new one (or add it if it is new).
174 int index = Checks.IndexOf(oldCheck); 209 int index = Checks.IndexOf(oldCheck);
175 if (index == -1) 210 if (index == -1)
176 Checks.Add(check); 211 Checks.Add(check);
177 else 212 else
178 Checks[index] = check; 213 Checks[index] = check;
179 } 214 }
180 CheckModified?.Invoke(check, new EventArgs()); 215 CheckModified?.Invoke(check, new EventArgs());
181 } 216 }
182 217
218 /// <summary>Returns true if the server looks empty (no user data has been entered).</summary>
183 public bool IsEmpty() 219 public bool IsEmpty()
184 { 220 {
185 return Name.IsNullOrEmpty() 221 return Name.IsNullOrEmpty()
186 && Host.IsNullOrEmpty() 222 && Host.IsNullOrEmpty()
187 && Checks.Count == 0; 223 && Checks.Count == 0;
188 } 224 }
189 225
226 /// <summary>Generates the authentication method based on user preferences.</summary>
190 private AuthenticationMethod GetAuthentication() 227 private AuthenticationMethod GetAuthentication()
191 { 228 {
192 if (LoginType == LoginType.Password) 229 if (LoginType == LoginType.Password)
193 return new PasswordAuthenticationMethod(Username, Password); 230 return new PasswordAuthenticationMethod(Username, Password);
194 else 231 else
195 return new PrivateKeyAuthenticationMethod(Username, PrivateKeyFile); 232 return new PrivateKeyAuthenticationMethod(Username, PrivateKeyFile);
196 } 233 }
197 234
235 /// <summary>Releases the open SSH connection.</summary>
198 private void InvalidateSshConnection() 236 private void InvalidateSshConnection()
199 { 237 {
200 _sshClient?.Dispose(); 238 _sshClient?.Dispose();
201 _sshClient = null; 239 _sshClient = null;
202 } 240 }
205 { 243 {
206 return Name.IsNullOrEmpty() ? Host : Name; 244 return Name.IsNullOrEmpty() ? Host : Name;
207 } 245 }
208 } 246 }
209 247
248 /// <summary>Possible statuses of the private key file.</summary>
210 public enum KeyStatus 249 public enum KeyStatus
211 { 250 {
251 /// <summary>The private key file is closed for an unspecified reason.</summary>
212 Closed, 252 Closed,
253 /// <summary>The private key file is accessible and open.</summary>
213 Open, 254 Open,
255 /// <summary>The private key file is not accessible (missing, access denied, etc).</summary>
214 NotAccessible, 256 NotAccessible,
257 /// <summary>The private key file is encrypted and the user has not entered the password yet.</summary>
215 NeedPassword, 258 NeedPassword,
216 } 259 }
217 260
218
219 } 261 }