Just a short, simple blog for Bob to share his thoughts.
31 January 2015 • by Bob • BlogEngine.NET
I ran into an interesting predicament the other day, and I thought that both the situation and my solution were worth sharing. Here's the scenario: I host websites for several family members and friends, and one of my family member's uses BlogEngine.NET for her blog. (As you may have seen in my previous blogs, I'm a big fan of BlogEngine.NET.) In any event, she forgot her password, so I logged into the admin section of her website, only to discover that there was no way for me to reset her password – I could only reset my password. Since it's my webserver, I have access to the physical files, so I decided to write a simple utility that can create the requisite SHA256/BASE64 password hashes that BlogEngine.NET uses, and then I can manually update the Users.xml file with new password hashes as I create them.
With that in mind, here is the code for the command-line utility:
using System; using System.Collections.Generic; using System.Linq; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; namespace BlogEnginePasswordHash { class Program { static void Main(string[] args) { // Verify that a single argument was passed to the application... if (args.Length != 1) { // ...if not, reply with generic help message. Console.WriteLine("\nUSAGE: BlogEnginePasswordHash <password>\n"); } // ...otherwise... else { // Retrieve a sequence of bytes for the password argument. var passwordBytes = Encoding.UTF8.GetBytes(args[0]); // Retrieve a SHA256 object. using (HashAlgorithm sha256 = new SHA256Managed()) { // Hash the password. sha256.TransformFinalBlock(passwordBytes, 0, passwordBytes.Length); // Convert the hashed password to a Base64 string. string passwordHash = Convert.ToBase64String(sha256.Hash); // Display the password and it's hash. Console.WriteLine("\nPassword: {0}\nHash: {1}\n", args[0], passwordHash); } } } } }
That code snippet should be pretty self-explanatory; the application takes a single argument, which is the password to hash. Once you enter a password and hit enter, the password and it's respective hash will be displayed.
Here are a few examples:
C:\>BlogEnginePasswordHash.exe "This is my password" Password: This is my password Hash: 6tV+IGzvN4gaQ0vmCWNHSQ0UQ0WgW4+ThJuhpXR6Z3c= C:\>BlogEnginePasswordHash.exe Password1 Password: Password1 Hash: GVE/3J2k+3KkoF62aRdUjTyQ/5TVQZ4fI2PuqJ3+4d0= C:\>BlogEnginePasswordHash.exe Password2 Password: Password2 Hash: G+AiJ1Cq84iauVtdWTuhLk/xBGR0cC1rR3n0tScwWyM= C:\>
Once you have created password hashes, you can paste those into the Users.xml file for your website:
<Users> <User> <UserName>Alice</UserName> <Password>GVE/3J2k+3KkoF62aRdUjTyQ/5TVQZ4fI2PuqJ3+4d0=</Password> <Email>alice@fabrikam.com</Email> <LastLoginTime>2015-01-31 01:52:00</LastLoginTime> </User> <User> <UserName>Bob</UserName> <Password>G+AiJ1Cq84iauVtdWTuhLk/xBGR0cC1rR3n0tScwWyM=</Password> <Email>bob@fabrikam.com</Email> <LastLoginTime>2015-01-31 01:53:00</LastLoginTime> </User> </Users>
That's all there is to do. Pretty simple stuff.
Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/
30 June 2011 • by Bob • FTP, BlogEngine.NET, Extensibility
I ran into an interesting situation recently with BlogEngine.NET that I thought would make a good blog post.
Here's the background for the environment: I host several blog sites for friends of mine, and they BlogEngine.NET for their blogging engine. From a security perspective this works great for me, because I can give them accounts for blogging that are kept in the XML files for each of their respective blogs that aren't real user accounts on my Windows servers.
The problem that I ran into: BlogEngine.NET has great support for uploading files to your blog, but it doesn't provide a real way to manage the files that have been uploaded. So when one of my friends mentioned that they wanted to update one of their files, I was left in a momentary quandary.
My solution: I realized that I could write a custom FTP provider that would solve all of my needs. For my situation the provider needed to do three things:
Here's why item #3 was so important - my users have no idea about the underlying functionality for their blog, so I didn't want to simply enable FTP publishing for their website and give them access to their ASP.NET files - there's no telling what might happen. Since all of their files are kept in the path ~/App_Data/files, it made sense to have the custom FTP provider return home directories for each of their websites that point to their files instead of the root folders of their websites.
The following items are required to complete the steps in this blog:
Note: I used Visual Studio 2008 when I created my custom provider and wrote the steps that appear in this blog, although since then I have upgraded to Visual Studio 2010, and I have successfully recompiled my provider using that version. In any event, the steps should be similar whether you are using Visual Studio 2008 or Visual Studio 2010.;-]
In this step, you will create a project inVisual Studio 2008for the demo provider.
net stop ftpsvc call "%VS90COMNTOOLS%\vsvars32.bat">nul gacutil.exe /if "$(TargetPath)" net start ftpsvc
net stop ftpsvc call "%VS100COMNTOOLS%\vsvars32.bat">nul gacutil.exe /if "$(TargetPath)" net start ftpsvc
In this step, you will implement the logging extensibility interface for the demo provider.
using System; using System.Collections.Specialized; using System.Collections.Generic; using System.Configuration.Provider; using System.IO; using System.Security.Cryptography; using System.Text; using System.Xml; using System.Xml.XPath; using Microsoft.Web.FtpServer; public class FtpBlogEngineNetAuthentication : BaseProvider, IFtpAuthenticationProvider, IFtpRoleProvider, IFtpHomeDirectoryProvider { // Create strings to store the paths to the XML files that store the user and role data. private string _xmlUsersFileName; private string _xmlRolesFileName; // Create a string to store the FTP home directory path. private string _ftpHomeDirectory; // Create a file system watcher object for change notifications. private FileSystemWatcher _xmlFileWatch; // Create a dictionary to hold user data. private Dictionary<string, XmlUserData> _XmlUserData = new Dictionary<string, XmlUserData>( StringComparer.InvariantCultureIgnoreCase); // Override the Initialize method to retrieve the configuration settings. protected override void Initialize(StringDictionary config) { // Retrieve the paths from the configuration dictionary. _xmlUsersFileName = config[@"xmlUsersFileName"]; _xmlRolesFileName = config[@"xmlRolesFileName"]; _ftpHomeDirectory = config[@"ftpHomeDirectory"]; // Test if the path to the users or roles XML file is empty. if ((string.IsNullOrEmpty(_xmlUsersFileName)) || (string.IsNullOrEmpty(_xmlRolesFileName))) { // Throw an exception if the path is missing or empty. throw new ArgumentException(@"Missing xmlUsersFileName or xmlRolesFileName value in configuration."); } else { // Test if the XML files exist. if ((File.Exists(_xmlUsersFileName) == false) || (File.Exists(_xmlRolesFileName) == false)) { // Throw an exception if the file does not exist. throw new ArgumentException(@"The specified XML file does not exist."); } } try { // Create a file system watcher object for the XML file. _xmlFileWatch = new FileSystemWatcher(); // Specify the folder that contains the XML file to watch. _xmlFileWatch.Path = _xmlUsersFileName.Substring(0, _xmlUsersFileName.LastIndexOf(@"\")); // Filter events based on the XML file name. _xmlFileWatch.Filter = @"*.xml"; // Filter change notifications based on last write time and file size. _xmlFileWatch.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size; // Add the event handler. _xmlFileWatch.Changed += new FileSystemEventHandler(this.XmlFileChanged); // Enable change notification events. _xmlFileWatch.EnableRaisingEvents = true; } catch (Exception ex) { // Raise an exception if an error occurs. throw new ProviderException(ex.Message,ex.InnerException); } } // Define the event handler for changes to the XML files. public void XmlFileChanged(object sender, FileSystemEventArgs e) { // Verify that the changed file is one of the XML data files. if ((e.FullPath.Equals(_xmlUsersFileName, StringComparison.OrdinalIgnoreCase)) || (e.FullPath.Equals(_xmlRolesFileName, StringComparison.OrdinalIgnoreCase))) { // Clear the contents of the existing user dictionary. _XmlUserData.Clear(); // Repopulate the user dictionary. ReadXmlDataStore(); } } // Override the Dispose method to dispose of objects. protected override void Dispose(bool IsDisposing) { if (IsDisposing) { _xmlFileWatch.Dispose(); _XmlUserData.Clear(); } } // Define the AuthenticateUser method. bool IFtpAuthenticationProvider.AuthenticateUser( string sessionId, string siteName, string userName, string userPassword, out string canonicalUserName) { // Define the canonical user name. canonicalUserName = userName; // Validate that the user name and password are not empty. if (String.IsNullOrEmpty(userName) || String.IsNullOrEmpty(userPassword)) { // Return false (authentication failed) if either are empty. return false; } else { try { // Retrieve the user/role data from the XML file. ReadXmlDataStore(); // Create a user object. XmlUserData user = null; // Test if the user name is in the dictionary of users. if (_XmlUserData.TryGetValue(userName, out user)) { // Retrieve a sequence of bytes for the password. var passwordBytes = Encoding.UTF8.GetBytes(userPassword); // Retrieve a SHA256 object. using (HashAlgorithm sha256 = new SHA256Managed()) { // Hash the password. sha256.TransformFinalBlock(passwordBytes, 0, passwordBytes.Length); // Convert the hashed password to a Base64 string. string passwordHash = Convert.ToBase64String(sha256.Hash); // Perform a case-insensitive comparison on the password hashes. if (String.Compare(user.Password, passwordHash, true) == 0) { // Return true (authentication succeeded) if the hashed passwords match. return true; } } } } catch (Exception ex) { // Raise an exception if an error occurs. throw new ProviderException(ex.Message,ex.InnerException); } } // Return false (authentication failed) if authentication fails to this point. return false; } // Define the IsUserInRole method. bool IFtpRoleProvider.IsUserInRole( string sessionId, string siteName, string userName, string userRole) { // Validate that the user and role names are not empty. if (String.IsNullOrEmpty(userName) || String.IsNullOrEmpty(userRole)) { // Return false (role lookup failed) if either are empty. return false; } else { try { // Retrieve the user/role data from the XML file. ReadXmlDataStore(); // Create a user object. XmlUserData user = null; // Test if the user name is in the dictionary of users. if (_XmlUserData.TryGetValue(userName, out user)) { // Search for the role in the list. string roleFound = user.Roles.Find(item => item == userRole); // Return true (role lookup succeeded) if the role lookup was successful. if (!String.IsNullOrEmpty(roleFound)) return true; } } catch (Exception ex) { // Raise an exception if an error occurs. throw new ProviderException(ex.Message,ex.InnerException); } } // Return false (role lookup failed) if role lookup fails to this point. return false; } // Define the GetUserHomeDirectoryData method. public string GetUserHomeDirectoryData(string sessionId, string siteName, string userName) { // Test if the path to the home directory is empty. if (string.IsNullOrEmpty(_ftpHomeDirectory)) { // Throw an exception if the path is missing or empty. throw new ArgumentException(@"Missing ftpHomeDirectory value in configuration."); } // Return the path to the home directory. return _ftpHomeDirectory; } // Retrieve the user/role data from the XML files. private void ReadXmlDataStore() { // Lock the provider while the data is retrieved. lock (this) { try { // Test if the dictionary already has data. if (_XmlUserData.Count == 0) { // Create an XML document object and load the user data XML file XPathDocument xmlUsersDocument = GetXPathDocument(_xmlUsersFileName); // Create a navigator object to navigate through the XML file. XPathNavigator xmlNavigator = xmlUsersDocument.CreateNavigator(); // Loop through the users in the XML file. foreach (XPathNavigator userNode in xmlNavigator.Select("/Users/User")) { // Retrieve a user name. string userName = GetInnerText(userNode, @"UserName"); // Retrieve the user's password. string password = GetInnerText(userNode, @"Password"); // Test if the data is empty. if ((String.IsNullOrEmpty(userName) == false) && (String.IsNullOrEmpty(password) == false)) { // Create a user data class. XmlUserData userData = new XmlUserData(password); // Store the user data in the dictionary. _XmlUserData.Add(userName, userData); } } // Create an XML document object and load the role data XML file XPathDocument xmlRolesDocument = GetXPathDocument(_xmlRolesFileName); // Create a navigator object to navigate through the XML file. xmlNavigator = xmlRolesDocument.CreateNavigator(); // Loop through the roles in the XML file. foreach (XPathNavigator roleNode in xmlNavigator.Select(@"/roles/role")) { // Retrieve a role name. string roleName = GetInnerText(roleNode, @"name"); // Loop through the users for the role. foreach (XPathNavigator userNode in roleNode.Select(@"users/user")) { // Retrieve a user name. string userName = userNode.Value; // Create a user object. XmlUserData user = null; // Test if the user name is in the dictionary of users. if (_XmlUserData.TryGetValue(userName, out user)) { // Add the role name for the user. user.Roles.Add(roleName); } } } } } catch (Exception ex) { // Raise an exception if an error occurs. throw new ProviderException(ex.Message,ex.InnerException); } } } // Retrieve an XPathDocument object from a file path. private static XPathDocument GetXPathDocument(string path) { Exception _ex = null; // Specify number of attempts to create an XPathDocument. for (int i = 0; i < 8; ++i) { try { // Create an XPathDocument object and load the user data XML file XPathDocument xPathDocument = new XPathDocument(path); // Return the XPathDocument if successful. return xPathDocument; } catch (Exception ex) { // Save the exception for later. _ex = ex; // Pause for a brief interval. System.Threading.Thread.Sleep(250); } } // Throw the last exception if the function fails to this point. throw new ProviderException(_ex.Message,_ex.InnerException); } // Retrieve data from an XML element. private static string GetInnerText(XPathNavigator xmlNode, string xmlElement) { string xmlText = string.Empty; try { // Test if the XML element exists. if (xmlNode.SelectSingleNode(xmlElement) != null) { // Retrieve the text in the XML element. xmlText = xmlNode.SelectSingleNode(xmlElement).Value.ToString(); } } catch (Exception ex) { // Raise an exception if an error occurs. throw new ProviderException(ex.Message,ex.InnerException); } // Return the element text. return xmlText; } } // Define the user data class. internal class XmlUserData { // Create a private string to hold a user's password. private string _password = string.Empty; // Create a private string array to hold a user's roles. private List<String> _roles = null; // Define the class constructor requiring a user's password. public XmlUserData(string Password) { this.Password = Password; this.Roles = new List<String>(); } // Define the password property. public string Password { get { return _password; } set { try { _password = value; } catch (Exception ex) { throw new ProviderException(ex.Message,ex.InnerException); } } } // Define the roles property. public List<String> Roles { get { return _roles; } set { try { _roles = value; } catch (Exception ex) { throw new ProviderException(ex.Message,ex.InnerException); } } } }
Note: If you did not use the optional steps to register the assemblies in the GAC, you will need to manually copy the assemblies to your IIS 7 computer and add the assemblies to the GAC using the Gacutil.exe tool. For more information, see the following topic on the Microsoft MSDN Web site:
In this step, you will add the provider to your FTP service. These steps obviously assume that you are using BlogEngine.NET on your Default Web Site, but these steps can be easily amended for any other website where BlogEngine.NET is installed.
cd %SystemRoot%\System32\Inetsrv
appcmd.exe set config -section:system.ftpServer/providerDefinitions /+"[name='FtpBlogEngineNetAuthentication',type='FtpBlogEngineNetAuthentication,FtpBlogEngineNetAuthentication,version=1.0.0.0,Culture=neutral,PublicKeyToken=426f62526f636b73']" /commit:apphost
appcmd.exe set config -section:system.ftpServer/providerDefinitions /+"activation.[name='FtpBlogEngineNetAuthentication']" /commit:apphost
appcmd.exe set config -section:system.ftpServer/providerDefinitions /+"activation.[name='FtpBlogEngineNetAuthentication'].[key='xmlUsersFileName',value='C:\inetpub\wwwroot\App_Data\Users.xml']" /commit:apphost
appcmd.exe set config -section:system.ftpServer/providerDefinitions /+"activation.[name='FtpBlogEngineNetAuthentication'].[key='xmlRolesFileName',value='C:\inetpub\wwwroot\App_Data\Roles.xml']" /commit:apphost
appcmd.exe set config -section:system.ftpServer/providerDefinitions /+"activation.[name='FtpBlogEngineNetAuthentication'].[key='ftpHomeDirectory',value='C:\inetpub\wwwroot\App_Data\files']" /commit:apphost
Just like the steps that I listed earlier, these steps assume that you are using BlogEngine.NET on your Default Web Site, but these steps can be easily amended for any other website where BlogEngine.NET is installed.
At the moment there is no user interface that enables you to add custom home directory providers, so you will have to use the following command line:
cd %SystemRoot%\System32\Inetsrv
appcmd.exe set config -section:system.applicationHost/sites /+"[name='Default Web Site'].ftpServer.customFeatures.providers.[name='FtpBlogEngineNetAuthentication']" /commit:apphost
appcmd.exe set config -section:system.applicationHost/sites /"[name='Default Web Site'].ftpServer.userIsolation.mode:Custom" /commit:apphost
To help improve the performance for authentication requests, the FTP service caches the credentials for successful logins for 15 minutes by default. This means that if you change your passwords, this change may not be reflected for the cache duration. To alleviate this, you can disable credential caching for the FTP service. To do so, use the following steps:
cd /d "%SystemRoot%\System32\Inetsrv" Appcmd.exe set config -section:system.ftpServer/caching /credentialsCache.enabled:"False" /commit:apphost Net stop FTPSVC Net start FTPSVC
Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/
08 December 2010 • by Bob • BlogEngine.NET, IIS, Windows Phone 7
I love BlogEngine.NET, and I love my Windows Phone 7 mobile phone, so it goes without saying that I would want the two technologies to work together. I'm currently using BlogEngine.NET 1.6.1, but Windows Phone 7 is not supported by default. That being said, it's really easy to add support for Windows Phone 7 by modifying your BlogEngine.NET settings. To do so, open your Web.config file and locate the following section:
<appSettings>
<add key="BlogEngine.FileExtension" value=".aspx" />
<!-- You can e.g. use "~/blog/" if BlogEngine.NET is not located in the root of the application -->
<add key="BlogEngine.VirtualPath" value="~/" />
<!-- The regex used to identify mobile devices so a different theme can be shown -->
<add key="BlogEngine.MobileDevices" value="(nokia|sonyericsson|blackberry|samsung|sec\-|windows ce|motorola|mot\-|up.b|midp\-)" />
<!-- The name of the role with administrator permissions -->
<add key="BlogEngine.AdminRole" value="Administrators" />
<!--This value is to provide an alterantive location for storing data.-->
<add key="StorageLocation" value="~/App_Data/" />
<!--A comma separated list of script names to hard minify. It's case-sensitive. -->
<add key="BlogEngine.HardMinify" value="blog.js,widget.js,WebResource.axd" />
</appSettings>
The line that you need to modify is the BlogEngine.MobileDevices
line, and all that you need to do is add iemobile
to the list. When you finish, it should look like the following:
<add key="BlogEngine.MobileDevices" value="(iemobile|nokia|sonyericsson|blackberry|samsung|sec\-|windows ce|motorola|mot\-|up.b|midp\-)" />
You could also add support for Apple products by using the following syntax:
<add key="BlogEngine.MobileDevices" value="(iemobile|iphone|ipod|nokia|sonyericsson|blackberry|samsung|sec\-|windows ce|motorola|mot\-|up.b|midp\-)" />
That's all there is to it. ;-]
Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/