Mercurial > servermonitor
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 } |