Just a short, simple blog for Bob to share his thoughts.
30 July 2009 • by Bob • FTP, Extensibility
I had a great question in the publishing forums on forums.iis.net, where someone was asking if FTP 7 supported the XCRC command. The short answer is that the XCRC command is not supported, but I came up with a way to create an FTP provider that supports something like it. Since it was a rather fun code sample to write, I thought that I'd turn it into a blog.
The sample FTP provider code in this blog post will automatically calculate an MD5 checksum from a file that is uploaded and store it in a file with a "*.MD5.TXT" file name extension. You can then compare the uploaded checksum with a local checksum on the client to verify the uploaded file's integrity.
There are a few points that I need to discuss before I present the code sample:
All of that being said, this provider follows the same development path as the provider in my How to Use Managed Code (C#) to Create a Simple FTP Logging Provider walkthrough, so if you follow the steps in that walkthrough and substitute "FtpUploadChecksumDemo" every place that you see "FtpLoggingDemo" and add a reference to Microsoft.Web.Administration, you should have all of the steps that you need in order to use this provider.
So without further discussion, here's the code for the provider:
using System; using System.Configuration.Provider; using System.IO; using System.Security.Cryptography; using System.Text; using Microsoft.Web.Administration; using Microsoft.Web.FtpServer; // NOTE: This code is provided "as-is" and comes with the following security // considerations. The FTP service will host the compiled assembly in the // "Microsoft FTP Service Extensibility Host" COM+ package (DLLHOST.EXE), // which runs by default as NETWORK SERVICE. By default, this account does not // have sufficient privileges to read the IIS configuration settings. As such, // you must either grant READ permissions to NETWORK SERVICE for the configuration // files, or configure the COM+ package to run as a user that has at least READ // access to the files in the InetSrv\config folder and READ/WRITE access to the // destination where the checksum file will be written. However, these are not // generally recommended practices. // // If you choose to grant NETWORK SERVICE permission to the configuration files, // the following three commands should accomplish the requisite permissions: // // cacls "%SystemRoot%\System32\inetsrv\config" /G "Network Service":R /E // cacls "%SystemRoot%\System32\inetsrv\config\redirection.config" /G "Network Service":R /E // cacls "%SystemRoot%\System32\inetsrv\config\applicationHost.config" /G "Network Service":R /E // // NOTE: You will need to do something similar for your content directory so that // the checksum files can be created. public sealed class FtpUploadChecksumDemo : BaseProvider, IFtpLogProvider { // Implement the logging method. void IFtpLogProvider.Log(FtpLogEntry loggingParameters) { // Test for a successful file upload operation. if ((loggingParameters.Command == "STOR") && (loggingParameters.FtpStatus == 226)) { try { // Define a 1GB maximum length - to prevent system hogging. const long maxLength = 0x3fffffff; // Map the path to the site root. string fullPath = MapSiteRootPath(loggingParameters.SiteName); // Append the relative path of the uploaded file. fullPath += loggingParameters.FullPath; // Expand any environment variables. fullPath = Environment.ExpandEnvironmentVariables(fullPath); // Convert forward slashes to back slashes fullPath = fullPath.Replace(@"/", @"\"); // Open the uploaded file to create a CRC. using (FileStream input = File.Open( fullPath, FileMode.Open, FileAccess.Read, FileShare.Read)) { // Test the input file length. if (input.Length > maxLength) { // Throw an execption if the file is too big. throw new ProviderException( String.Format("Input file is too large: {0}", input.Length.ToString())); } else { // Open the hash file for output. using (StreamWriter output = new StreamWriter( fullPath + ".MD5.txt", false)) { // Create an MD5 object. MD5 md5 = MD5.Create(); // Retrieve the hash byte array. byte[] byteArray = md5.ComputeHash(input); // Create a new string builder for the ASCII hash string. StringBuilder stringBuilder = new StringBuilder(byteArray.Length * 2); // Loop through the hash. foreach (byte byteMember in byteArray) { // Append each ASCII hex byte to the hash string. stringBuilder.AppendFormat("{0:x2}", byteMember); } // Write the hash string to the output file. output.Write(stringBuilder); } } } } catch(Exception ex) { throw new ProviderException(ex.Message); } } } // This method is almost 100% from scripts that were created // by the IIS Manager Configuration Editor admin pack tool. private static string MapSiteRootPath(string siteName) { try { using (ServerManager serverManager = new ServerManager()) { Configuration config = serverManager.GetApplicationHostConfiguration(); ConfigurationSection sitesSection = config.GetSection("system.applicationHost/sites"); ConfigurationElementCollection sitesCollection = sitesSection.GetCollection(); ConfigurationElement siteElement = FindElement(sitesCollection, "site", "name", siteName); if (siteElement == null) { throw new InvalidOperationException("Element not found!"); } else { ConfigurationElementCollection siteCollection = siteElement.GetCollection(); ConfigurationElement applicationElement = FindElement(siteCollection, "application", "path", @"/"); if (applicationElement == null) { throw new InvalidOperationException("Element not found!"); } else { ConfigurationElementCollection applicationCollection = applicationElement.GetCollection(); ConfigurationElement virtualDirectoryElement = FindElement(applicationCollection, "virtualDirectory", "path", @"/"); if (virtualDirectoryElement == null) { throw new InvalidOperationException("Element not found!"); } else { return virtualDirectoryElement["physicalPath"].ToString(); } } } } } catch (Exception ex) { throw new ProviderException(ex.Message); } } // This method is almost 100% from scripts that were created // by the IIS Manager Configuration Editor admin pack tool. private static ConfigurationElement FindElement( ConfigurationElementCollection collection, string elementTagName, params string[] keyValues) { foreach (ConfigurationElement element in collection) { if (String.Equals(element.ElementTagName, elementTagName, StringComparison.OrdinalIgnoreCase)) { bool matches = true; for (int i = 0; i < keyValues.Length; i += 2) { object o = element.GetAttributeValue(keyValues[i]); string value = null; if (o != null) { value = o.ToString(); } if (!String.Equals(value, keyValues[i + 1], StringComparison.OrdinalIgnoreCase)) { matches = false; break; } } if (matches) { return element; } } } return null; } }
That wraps it up for today's post.
Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/
Tags: