I have been a little bit dry on coding inspiration lately, until the other day when my collegue Karl Ahlin gave me an interesting idea concerning monitoring of configuration files. While I enjoyed coding it, I will leave it unsaid if and where it could actually create value. There may still be a few what ifs with the approach, and some of it may be refactored if I actually get around using it, but the code is available at GitHub nonetheless.
Calculating MD5 hash checksums from input strings
The idea is to have the website automatically send a notification e-mail as soon as a configuration file is changed; for this, we first need a way of comparing two potentially different file versions. Luckily it is not hard to compute MD5 hashes from strings using the build-in System.Security.Cryptography functionality.
ChecksumService.cs
private static string CalculateChecksumFor(string input) { var inputBytes = Encoding.ASCII.GetBytes(input); var hash = MD5.Create().ComputeHash(inputBytes); var stringBuilder = new StringBuilder(); foreach (var h in hash) { stringBuilder.Append(h.ToString("x2")); } return stringBuilder.ToString(); }
The code is pretty self explaining; first, all the characters in the input string are encoded into a sequence of bytes (172), after which a hash is computed from the obtained byte array (173). Each byte is then converted (178) into its equivalent string representation using the x2 format (for other formats, please see the Byte.ToString documentation over at MSDN).
For this functionality to work, the content of the configuration files is not really interesting after the point where we get hold of the corresponding checksums. Of course, this could be altered if the need to find out exactly what has changed in the files should arise, but for the moment just knowing that someone has been changing them is enough.
private static IEnumerable<KeyValuePair<string, string>> RecalculateChecksums() { var configfiles = Directory .GetFiles(HttpRuntime.AppDomainAppPath, "*.config", SearchOption.AllDirectories) .Where(n => !string.IsNullOrEmpty(n)); return from file in configfiles let checksum = CalculateChecksumFor(File.ReadAllText(file)) select new KeyValuePair<string, string>(file, checksum); }
In order to calculate, or rather recalculate, the MD5 checksum hashes we must first get hold of the configuration files from the file system. The GetFiles method (51) used above will retrieve all filenames matching the *.config pattern from the root of the web application and all of its subdirectories (due to the SearchOption.AllDirectories argument). I did not add anything other than the content of the configuration file (54) in the hash, but one might argue that the filename itself ought to be included. Currently the configuration file can be renamed without any notification being done if the same name change is first done in the hash storage.
Having retrieved the MD5 checksums for all of the currently present configuration files, all we need are the hashes for the old ones. These are stored in a text file located in the App_Data directory.
App_Data\Checksums.txt
D:\EPiServer\connectionStrings.config#dab873e0c0746d2d12c7484f87608ae2 D:\EPiServer\EPiServer.config#53a70adbd6de35b89be8586e2f66402a D:\EPiServer\EPiServerFramework.config#10bb080a8deafd53fc41a33adba9b1ea D:\EPiServer\EPiServerLog.config#b7260752f31b3343583ad85fe428cad7 D:\EPiServer\FileSummary.config#f1fb2139cf384450cfc8864bad76fc4f D:\EPiServer\web.config#81f70ffa90697b9a6659d1f113b9b9b5 D:\EPiServer\Licenses\License.config#cb61bc9f1000dd5483a6c46a440d63d1
The ValidateChecksums method (see ChecksumService.cs at Github) is mostly about comparing config paths, filenames and MD5 checksum hashes creating a discrepancy report if something differs. As it is mostly a bunch of if-statements I will leave it to the code to explain itself. What is more interesting is how to avoid using the web hosting’s own SMTP server for sending the notification e-mail (Yes, but just because you’re paranoid doesn’t mean they aren’t messing with your config files).
How to use Google’s Gmail SMTP server to send your MailMessage
If there is a discrepancy in the website’s configuration, the ChecksumService passes the report as the mail’s body to the SendNotificationMail method setting up a new SMTP client. If you want to use Google’s Gmail SMTP servers to send your e-mails, you will have to create a Gmail account to use as sender. If the sender name in the fromAddress object would not match the one specified in the account, Google automatically sorts it out for you; for better or for worse.
ChecksumService.cs
private static void SendNotificationMail(string mailBody) { var toAddress = new MailAddress("[email protected]", "Mathias Kunto"); var fromAddress = new MailAddress("[email protected]", "Prod Servers"); const string fromPassword = "R4c?.p4inEg5#34_Ds^^b4!!"; var subject = string.Concat("Config(s) changed on ", Environment.MachineName); var smtpClient = new SmtpClient { Host = "smtp.gmail.com", Port = 587, EnableSsl = true, DeliveryMethod = SmtpDeliveryMethod.Network, UseDefaultCredentials = false, Credentials = new NetworkCredential(fromAddress.Address, fromPassword) }; var mailMessage = new MailMessage(fromAddress, toAddress) { Subject = subject, Body = mailBody }; try { smtpClient.Send(mailMessage);
You will probably want to catch, for instance, the SmtpException exception and attempt some rescue operations should it occur. It may be thrown for various reasons; like you not being able to connect to the SMTP server, failing authentication, getting timeouts, and so on.
After the mail is sent, the ChecksumService will write the new checksums to the storage file as not to report the same changes twice.
Using EPiServer InitializableModule functionality to avoid web.config dependencies
One might question the EPiServerness of this functionality, and truth is that it would work just as fine in any ordernary ASP.NET application using for instance the IHttpModule interface. However, since I developed this for an EPiServer site, there is a benefit to be gained from built-in functionality. EPiServer provides us with the IInitializableModule interface allowing for the checksum validator to run during initialization without having to add your own module configuration to web.config; thus making disabling it a whole lot more tiresome.
ChecksumVerificationModule.cs
[InitializableModule] public class ChecksumVerificationModule : IInitializableModule { public void Initialize(InitializationEngine context) { ChecksumService.VerifyConfigs(); } public void Uninitialize(InitializationEngine context) { } public void Preload(string[] parameters) { } }
Just add the text book InitializableModule attribute to the class and implement the members of the IInitializableModule interface and you’re there.
This functionality could of course be altered to monitor other files as well, but changes made to files (including some of the config files mentioned in this post) that do not cause a website restart would not be reported until such time that the site restarts; for instance if the application pool decides to recycle. You could probably write some scheduled job to monitor the files, but if you need to do that the real problem is most likely elsewhere.
Or you could just use a FileSystemWatcher instead of reinventing the wheel, as my collegue Mikael Lundin pointed out. Thanks for making me aware of this round shaped thingy, Mikael :)
Because his solution does something FileSystemWatcher cannot.
FileSystemWatcher watches for changes that happen “at runtime” (and not changes that occur if you stop the application, change the files and then start the application again), this solution checks if the config files have been changed since “last known good” version.
But combining this with FileSystemWatcher would be ideal, that way you can make a “alert on config changes” solution.
Thanks for the information Aanund. I have not worked with the FileSystemWatcher before, but I’ll make sure to have a look at it implementing this functionality on the site where I intended to use it.