Just a short, simple blog for Bob to share his thoughts.
21 October 2011 • by Bob • FTP
For this installment in my series about FTP Clients, I'd like to take a look at FTP Voyager from Rhino Software. For this blog I used FTP Voyager 15.2.0.17, and it is available from the following URL:
FTP Voyager is a great FTP client that supports a wide array of features and connection options, but I shouldn't get ahead of myself and talk about everything in my introduction. ;-]
At the time of this blog post, FTP Voyager is a for-retail product that is available in two different versions:
You should take a look at the FTP Voyager Versions page for a description of the features that are available in each version.
The FTP Voyager user interface is uncluttered, easy to understand, and allows you to customize which panes you want to see displayed.
If you want a really uncluttered display, FTP Voyager offers a Simple Mode, which narrows down the number of panes that are displayed. (Sometimes this is a handy feature to have.)
FTP Voyager doesn't have a command-line interface, but it has web browser integration; and it has a really cool scheduler, which allows you to configure FTP jobs to run at scheduled times.
FTP Voyager also supports sending custom FTP commands, and it has an extensibility interface for creating add-ons. I didn't experiment with creating any add-ons, but you can find details about creating your own add-ons through RhinoSoft's FTP Voyager Add-Ons page.
FTP connections are created and edited through FTP Voyager's Site Profile Manager, which is comparable to the site management features that I have found in many of the better GUI-based FTP clients.
That concludes my summary for some of the general features - so now we'll take a look at the FTP7-specific features that I've discussed in my other FTP client blog posts.
FTP Voyager supports both Implicit and Explicit FTPS, so the choice is up to you to decide which method to use. As I have mentioned in my previous blogs, the FTPS method in FTP7 is specified by the port number that you choose when you are creating your bindings. Once again, I realize that I have posted the following information in almost all of my posts in this FTP client series, but it needs to be mentioned that the following rules apply for FTP7 when determining whether you are using Implicit or Explicit FTPS:
To configure the security options for a connection in FTP Voyager, you need to open the Advanced Settings dialog for the connection in FTP Voyager's Site Profile Manager.
![]() |
Fig. 6 - FTP Voyager's Security Options |
The additional security options in FTP Voyager's Security Options allow you to configure the SSL environment to match FTP7's Advanced SSL Policy settings.
![]() |
Fig. 7 - FTP7's Advanced SSL Policy Settings |
Note: I was able to use FTP Voyager's FTPS features with FTP7's virtual host names, but I should mention that I had to configure a Global Listener FTP Site in order to get that to work.
FTP Voyager has built-in for the HOST command, so you can use true FTP host names when using FTP Voyager to connect to FTP7 sites that are configured with host names. This feature is enabled by default, but if you needed to disable it for some reason, that feature can be accessed through FTP Voyager's Advanced Settings dialog.
![]() |
Fig. 7 - FTP Voyager's Advanced Connection Settings |
The following excerpt from the Log Pane of an FTP Voyager session shows the HOST command in action:
STATUS:> |
Connecting to "ftp.contoso.com" on port 21. |
|
220 Microsoft FTP Service |
STATUS:> |
Connected. Logging into the server |
COMMAND:> |
HOST ftp.contoso.com |
|
220 Host accepted. |
COMMAND:> |
USER robert |
|
331 Password required for robert. |
COMMAND:> |
PASS ********** |
|
230 User logged in. |
STATUS:> |
Login successful |
FTP Voyager's login settings allow you to specify the virtual host name as part of the user credentials by using syntax like "ftp.example.com|username" or "ftp.example.com\username", but since FTP Voyager allows you to use true FTP hosts this is really a moot point. Just the same, there's nothing to stop you from disabling the HOST command for a connection and specifying an FTP virtual host as part of your username, although I'm not sure why you would want to do that.
This concludes our quick look at some of the FTP features that are available with FTP Voyager, and here are the scorecard results:
Client Name | Directory Browsing | Explicit FTPS | Implicit FTPS | Virtual Hosts | True HOSTs | Site Manager | Extensibility |
---|---|---|---|---|---|---|---|
Rich | Y | Y | Y | Y | Y | Y | |
As noted earlier, FTP Voyager supports the FTP HOST command, and is enabled by default for new connections. |
In closing, FTP Voyager is a great GUI-based FTP client that has first-class support for all of the features that I have been examining in detail throughout my FTP client blog series. But that being said, I included the following disclaimer in all of my preceding posts, so this post will be no exception: there are a great number of additional features that FTP Voyager provides - but once again I only focused on a few specific topic areas that apply to FTP7. ;-]
Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/
04 October 2011 • by Bob • FTP, LogParser
One of my colleagues here at Microsoft, Emmanuel Boersma, just reminded me of an email thread that we had several weeks ago, where a customer had asked him how they could tell if FTPS was being used on their FTP server. He had pointed out that when he looks at his FTP log files, the port number was always 21, so it wasn't as easy as looking at a website's log files and looking for port 80 for HTTP versus port 443 for HTTPS. I had sent him the following notes, and I thought that they might make a good blog. ;-)
As I mentioned earlier, we had discussed the control channel is typically over port 21 for both FTP and FTPS, so you can't rely on the port. But having said that, I mentioned that you will see certain verbs in your FTP logs that will let you know when FTPS is being used, and that’s a reliable way to check.
With that in mind, I suggested the following two methods that you can use to determine if FTPS is being used:
For example, see the highlighted data in following FTP log file excerpts:
Explicit FTPS over port 21:
#Fields: date time c-ip cs-username cs-host s-ip s-port cs-method cs-uri-stem sc-status sc-win32-status sc-substatus sc-bytes cs-bytes time-taken 2011-06-30 22:11:24 ::1 - - ::1 21 ControlChannelOpened - - 0 0 0 0 0 2011-06-30 22:11:24 ::1 - - ::1 21 AUTH TLS 234 0 0 31 10 16 2011-06-30 22:11:27 ::1 - - ::1 21 PBSZ 0 200 0 0 69 8 0 2011-06-30 22:11:27 ::1 - - ::1 21 PROT P 200 0 0 69 8 0 2011-06-30 22:11:36 ::1 - - ::1 21 USER robert 331 0 0 69 13 0 2011-06-30 22:11:42 ::1 robert - ::1 21 PASS *** 230 0 0 53 15 2808
Implicit FTPS over port 990:
#Fields: date time c-ip cs-username cs-host s-ip s-port cs-method cs-uri-stem sc-status sc-win32-status sc-substatus sc-bytes cs-bytes time-taken 2011-06-30 22:16:55 ::1 - - ::1 990 ControlChannelOpened - - 0 0 0 0 0 2011-06-30 22:16:58 ::1 - - ::1 990 USER robert 331 0 0 69 13 0 2011-06-30 22:16:58 ::1 robert - ::1 990 PASS *** 230 0 0 53 15 78 2011-06-30 22:16:58 ::1 robert - ::1 990 SYST - 500 5 51 1005 6 0 2011-06-30 22:16:58 ::1 robert - ::1 990 FEAT - 211 0 0 313 6 0 2011-06-30 22:16:58 ::1 robert - ::1 990 OPTS UTF8+ON 200 0 0 85 14 0 2011-06-30 22:16:58 ::1 robert - ::1 990 PBSZ 0 200 0 0 69 8 0 2011-06-30 22:16:58 ::1 robert - ::1 990 PROT P 200 0 0 69 8 0
FWIW – An explanation about Implicit FTPS and Explicit FTPS can be found in the following articles:
Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/
29 September 2011 • by Bob • FTP, Extensibility
I had a question from someone that had an interesting scenario: they had a series of reports that their manufacturing company generates on a daily basis, and they wanted to automate uploading those files over FTP from their factory to their headquarters. Their existing automation created report files with names like Widgets.log, Sprockets.log, Gadgets.log, etc.
But they had an additional request: they wanted the reports dropped into folders based on the day of the week. People in their headquarters could retrieve the reports from a share on their headquarters network where the FTP server would drop the files, and anyone could look at data from anytime within the past seven days.
This seemed like an extremely trivial script for me to write, so I threw together the following example batch file for them:
@echo off pushd "C:\Reports" for /f "usebackq delims= " %%a in (`date /t`) do ( echo open MyServerName>ftpscript.txt echo MyUsername>>ftpscript.txt echo MyPassword>>ftpscript.txt echo mkdir %%a>>ftpscript.txt echo cd %%a>>ftpscript.txt echo asc>>ftpscript.txt echo prompt>>ftpscript.txt echo mput *.log>>ftpscript.txt echo bye>>ftpscript.txt ) ftp.exe -s:ftpscript.txt del ftpscript.txt popd
This would have worked great for most scenarios, but they pointed out a few problems in their specific environment: manufacturing and headquarters were in different geographical regions of the world, therefore in different time zones, and they wanted the day of the week to be based on the day of the week where their headquarters was located. They also wanted to make sure that if anyone logged in over FTP, they would only see the reports for the current day, and they didn't want to take a chance that something might go wrong with the batch file and they might overwrite the logs from the wrong day.
With all of those requirements in mind, this was beginning to look like a problem for a custom home directory provider to tackle. Fortunately, this was a really easy home directory provider to write, and I thought that it might make a good blog.
Note: I wrote and tested the steps in this blog using both Visual Studio 2010 and Visual Studio 2008; if you use an different version of Visual Studio, some of the version-specific steps may need to be changed.
The following items are required to complete the procedures in this blog:
In this step, you will create a project in Microsoft Visual Studio for the demo provider.
net stop ftpsvc
call "%VS100COMNTOOLS%\vsvars32.bat">null
gacutil.exe /if "$(TargetPath)"
net start ftpsvc
net stop ftpsvc
call "%VS90COMNTOOLS%\vsvars32.bat">null
gacutil.exe /if "$(TargetPath)"
net start ftpsvc
In this step, you will implement the extensibility interfaces for the demo provider.
using System; using System.Collections.Generic; using System.Collections.Specialized; using Microsoft.Web.FtpServer; public class FtpDayOfWeekHomeDirectory : BaseProvider, IFtpHomeDirectoryProvider { // Store the path to the default FTP folder. private static string _defaultDirectory = string.Empty; // Override the default initialization method. protected override void Initialize(StringDictionary config) { // Retrieve the default directory path from configuration. _defaultDirectory = config["defaultDirectory"]; // Test for the default home directory (Required). if (string.IsNullOrEmpty(_defaultDirectory)) { throw new ArgumentException( "Missing default directory path in configuration."); } } // Define the home directory provider method. string IFtpHomeDirectoryProvider.GetUserHomeDirectoryData( string sessionId, string siteName, string userName) { // Return the path to the folder for the day of the week. return String.Format( @"{0}\{1}", _defaultDirectory, DateTime.Today.DayOfWeek); } }
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 your provider to the global list of custom providers for your FTP service, configure your provider's settings, and enable your provider for an FTP site.
Note: If you prefer, you could use the command line to add the provider to FTP by using syntax like the following example:
cd %SystemRoot%\System32\Inetsrv
appcmd.exe set config -section:system.ftpServer/providerDefinitions /+"[name='FtpDayOfWeekHomeDirectory',type='FtpDayOfWeekHomeDirectory,FtpDayOfWeekHomeDirectory,version=1.0.0.0,Culture=neutral,PublicKeyToken=426f62526f636b73']" /commit:apphost
At the moment there is no user interface that allows you to configure properties for a custom home directory provider, so you will have to use the following command line:
cd %SystemRoot%\System32\Inetsrv
appcmd.exe set config -section:system.ftpServer/providerDefinitions /+"activation.[name='FtpDayOfWeekHomeDirectory']" /commit:apphost
appcmd.exe set config -section:system.ftpServer/providerDefinitions /+"activation.[name='FtpDayOfWeekHomeDirectory'].[key='defaultDirectory',value='C:\Inetpub\ftproot']" /commit:apphost
Note: The highlighted area contains the value that you need to update with the root directory of your FTP site.
At the moment there is no user interface that allows you to enable a custom home directory provider for an FTP site, so you will have to use the following command line:
cd %SystemRoot%\System32\Inetsrv
appcmd.exe set config -section:system.applicationHost/sites /+"[name='My FTP Site'].ftpServer.customFeatures.providers.[name='FtpDayOfWeekHomeDirectory']" /commit:apphost
appcmd.exe set config -section:system.applicationHost/sites /"[name='My FTP Site'].ftpServer.userIsolation.mode:Custom" /commit:apphost
Note: The highlighted areas contain the name of the FTP site where you want to enable the custom home directory provider.
In this blog I showed you how to:
When users connect to your FTP site, the FTP service will drop their session in the corresponding folder for the day of the week under the home directory for your FTP site, and they will not be able to change to the root directory or a directory for a different day of the week.
Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/
16 September 2011 • by Bob • FTP, Extensibility
This blog is designed as a complement to my FTP and LDAP - Part 1: How to Use Managed Code (C#) to Create an FTP Authentication Provider that uses an LDAP Server blog post. In this second blog, I'll walk you through the steps to set up an Active Directory Lightweight Directory Services (AD LDS) server, which you can use with the custom FTP LDAP Authentication provider that I discussed in my last blog.
The following steps will walk you through installing Active Directory Lightweight Directory Services on a computer that is running Windows Server 2008.
Note: Before completing these steps I created a local user account named "LdapAdmin" that I would specify the administrative account for managing my LDAP instance. This user account was only a member of the local "Users" group, and not a member of the local "Administrators" group.
For additional information about working with AD LDS instances, see the following URLs:
While this is technically outside the scope of setting up the LDAP server, I'm reposting the notes from my last blog about adding the FTP LDAP Authentication provider and adding authorization rules for FTP users or groups.
Once these settings are configured and users connect to your FTP site, the FTP service will attempt to authenticate users from your LDAP server by using the custom FTP LDAP Authentication provider.
Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/
16 September 2011 • by Bob • FTP, Extensibility
Over the past few years I've created a series of authentication providers for the FTP 7.5 service that ships with Windows Server 2008 R2 and Windows 7, and is available for download for Windows Server 2008. Some of these authentication providers are available on the http://learn.iis.net/page.aspx/590/developing-for-ftp-75/ website, while others have been in my blog posts.
With that in mind, I had a question a little while ago about using an LDAP server to authenticate users for the FTP service, and it seemed like that would make a great subject for another custom FTP authentication provider blog post.
The steps in this blog will lead you through the steps to use managed code to create an FTP authentication provider that uses a server running Active Directory Lightweight Directory Services (AD LDS) that is located on your local network.
Note: I wrote and tested the steps in this blog using both Visual Studio 2010 and Visual Studio 2008; if you use an different version of Visual Studio, some of the version-specific steps may need to be changed.
The following items are required to complete the procedures in this blog:
Note: To test this blog, I used AD LDS on Windows Server 2008; if you use a different LDAP server, you may need to change some of the LDAP syntax in the code samples. To get started using AD LDS, see the following topics:
I tested this blog by using the user objects from both the MS-User.LDF and MS-InetOrgPerson.LDF Lightweight Directory interchange Format (LDIF) files.
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 the password in your AD LDS server, 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
In this step, you will create a project in Visual Studio 2008 for the demo provider.
net stop ftpsvc
call "%VS100COMNTOOLS%\vsvars32.bat">null
gacutil.exe /if "$(TargetPath)"
net start ftpsvc
net stop ftpsvc
call "%VS90COMNTOOLS%\vsvars32.bat">null
gacutil.exe /if "$(TargetPath)"
net start ftpsvc
In this step, you will implement the authentication and role extensibility interfaces for the demo provider.
using System; using System.Collections.Specialized; using System.Configuration.Provider; using System.DirectoryServices; using System.DirectoryServices.AccountManagement; using Microsoft.Web.FtpServer; public class FtpLdapAuthentication : BaseProvider, IFtpAuthenticationProvider, IFtpRoleProvider { private static string _ldapServer = string.Empty; private static string _ldapPartition = string.Empty; private static string _ldapAdminUsername = string.Empty; private static string _ldapAdminPassword = string.Empty; // Override the default initialization method. protected override void Initialize(StringDictionary config) { // Retrieve the provider settings from configuration. _ldapServer = config["ldapServer"]; _ldapPartition = config["ldapPartition"]; _ldapAdminUsername = config["ldapAdminUsername"]; _ldapAdminPassword = config["ldapAdminPassword"]; // Test for the LDAP server name (Required). if (string.IsNullOrEmpty(_ldapServer) || string.IsNullOrEmpty(_ldapPartition)) { throw new ArgumentException( "Missing LDAP server values in configuration."); } } public bool AuthenticateUser( string sessionId, string siteName, string userName, string userPassword, out string canonicalUserName) { canonicalUserName = userName; // Attempt to look up the user and password. return LookupUser(true, userName, string.Empty, userPassword); } public bool IsUserInRole( string sessionId, string siteName, string userName, string userRole) { // Attempt to look up the user and role. return LookupUser(false, userName, userRole, string.Empty); } private static bool LookupUser( bool isUserLookup, string userName, string userRole, string userPassword) { PrincipalContext _ldapPrincipalContext = null; DirectoryEntry _ldapDirectoryEntry = null; try { // Create the context object using the LDAP connection information. _ldapPrincipalContext = new PrincipalContext( ContextType.ApplicationDirectory, _ldapServer, _ldapPartition, ContextOptions.SimpleBind, _ldapAdminUsername, _ldapAdminPassword); // Test for LDAP credentials. if (string.IsNullOrEmpty(_ldapAdminUsername) || string.IsNullOrEmpty(_ldapAdminPassword)) { // If LDAP credentials do not exist, attempt to create an unauthenticated directory entry object. _ldapDirectoryEntry = new DirectoryEntry("LDAP://" + _ldapServer + "/" + _ldapPartition); } else { // If LDAP credentials exist, attempt to create an authenticated directory entry object. _ldapDirectoryEntry = new DirectoryEntry("LDAP://" + _ldapServer + "/" + _ldapPartition, _ldapAdminUsername, _ldapAdminPassword, AuthenticationTypes.Secure); } // Create a DirectorySearcher object from the cached DirectoryEntry object. DirectorySearcher userSearcher = new DirectorySearcher(_ldapDirectoryEntry); // Specify the the directory searcher to filter by the user name. userSearcher.Filter = String.Format("(&(objectClass=user)(cn={0}))", userName); // Specify the search scope. userSearcher.SearchScope = SearchScope.Subtree; // Specify the directory properties to load. userSearcher.PropertiesToLoad.Add("distinguishedName"); // Specify the search timeout. userSearcher.ServerTimeLimit = new TimeSpan(0, 1, 0); // Retrieve a single search result. SearchResult userResult = userSearcher.FindOne(); // Test if no result was found. if (userResult == null) { // Return false if no matching user was found. return false; } else { if (isUserLookup == true) { try { // Attempt to validate credentials using the username and password. return _ldapPrincipalContext.ValidateCredentials(userName, userPassword, ContextOptions.SimpleBind); } catch (Exception ex) { // Throw an exception if an error occurs. throw new ProviderException(ex.Message); } } else { // Retrieve the distinguishedName for the user account. string distinguishedName = userResult.Properties["distinguishedName"][0].ToString(); // Create a DirectorySearcher object from the cached DirectoryEntry object. DirectorySearcher groupSearcher = new DirectorySearcher(_ldapDirectoryEntry); // Specify the the directory searcher to filter by the group/role name. groupSearcher.Filter = String.Format("(&(objectClass=group)(cn={0}))", userRole); // Specify the search scope. groupSearcher.SearchScope = SearchScope.Subtree; // Specify the directory properties to load. groupSearcher.PropertiesToLoad.Add("member"); // Specify the search timeout. groupSearcher.ServerTimeLimit = new TimeSpan(0, 1, 0); // Retrieve a single search result. SearchResult groupResult = groupSearcher.FindOne(); // Loop through the member collection. for (int i = 0; i < groupResult.Properties["member"].Count; ++i) { string member = groupResult.Properties["member"][i].ToString(); // Test if the current member contains the user's distinguished name. if (member.IndexOf(distinguishedName, StringComparison.OrdinalIgnoreCase) > -1) { // Return true (role lookup succeeded) if the user is found. return true; } } // Return false (role lookup failed) if the user is not found for the role. return false; } } } catch (Exception ex) { // Throw an exception if an error occurs. throw new ProviderException(ex.Message); } } }
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 your provider to the list of providers for your FTP service, configure your provider for your LDAP server, and enable your provider to authenticate users for an FTP site.
cd %SystemRoot%\System32\Inetsrv
appcmd.exe set config -section:system.ftpServer/providerDefinitions /+"activation.[name='FtpLdapAuthentication']" /commit:apphost
appcmd.exe set config -section:system.ftpServer/providerDefinitions /+"activation.[name='FtpLdapAuthentication'].[key='ldapServer',value='MYSERVER:389']" /commit:apphost
appcmd.exe set config -section:system.ftpServer/providerDefinitions /+"activation.[name='FtpLdapAuthentication'].[key='ldapPartition',value='CN=MyServer,DC=MyDomain,DC=local']" /commit:apphost
appcmd.exe set config -section:system.ftpServer/providerDefinitions /+"activation.[name='FtpLdapAuthentication'].[key='ldapAdminUsername',encryptedValue='MyAdmin']" /commit:apphost
appcmd.exe set config -section:system.ftpServer/providerDefinitions /+"activation.[name='FtpLdapAuthentication'].[key='ldapAdminPassword',encryptedValue='MyPassword1']" /commit:apphost
In this blog I showed you how to:
When users connect to your FTP site, the FTP service will attempt to authenticate users from your LDAP server by using your custom authentication provider.
The PrincipalContext.ValidateCredentials() method will validate the user name in the userName parameter with the value of the userPrincipalName attribute of the user object in AD LDS. Because of this, the userPrincipalName attribute for a user object is expected to match the name of the user account that an FTP client will use to log in, which will should be the same value as the cn attribute for the user object. Therefore, when you create a user object in AD LDS, you will need to set the corresponding userPrincipalName attribute for the user object. In addition, when you create a user object in AD LDS, the msDS-UserAccountDisabled attribute is set to TRUE by default, so you will need to change the value of that attribute to FALSE before you attempt to log in.
For more information, see my follow-up blog that is titled FTP and LDAP - Part 2: How to Set Up an Active Directory Lightweight Directory Services (AD LDS) Server.
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/
17 April 2011 • by Bob • FTP
Following on the heels of Clive Webster's article that FTP is 40 years old, let me be the next person to wish FTP a wonderful 40th birthday!
Happy Birthday FTP!
Yeah, that was kind of silly, wasn't it...? ;-]
08 April 2011 • by Bob • FTP, IIS
I've seen a few situations where people that are using the FTP 7.0 and FTP 7.5 service have noticed that it takes a while for their password changes to be reflected by the FTP service. To put this another way, here are the typical symptoms that people describe to me:
Here's why this happens: to help improve the performance for authentication requests, the FTP service caches the credentials for successful logins. (The cache duration is set to 15 minutes by default.) This means that if you change your password, your changes may not be reflected for the cache duration.
The good news is, the FTP credential cache settings can be changed easily, and I have documented all of the settings for FTP caching in the IIS configuration reference at the following URLs:
Quoting and paraphrasing the above documentation, there are the two settings that you can configure on the <credentialsCache>
element:
Attribute | Description |
---|---|
enabled |
Optional Boolean attribute. true if credential caching is enabled; otherwise, false. The default value is true . |
flushInterval |
Optional uint attribute. Specifies the cache lifetime, in seconds, for credentials that are stored in the cache. Note: This value must be between 5 and 604,800 seconds. The default value is 900 . |
What this means to you is - you can completely disable credential caching, or you can specify a different timeout. For example, on several of my development servers I often disable credential caching; this allows me to change passwords whenever I want, which is very useful when I am creating custom authentication providers. For my production servers I tend to stick with the default values, although I might change those values when I'm troubleshooting a problem.
I usually configure the settings from a command line or a batch file, although the articles that I listed earlier have steps for using the IIS Manager to change the settings for FTP credential caching. Just the same, here are some examples for setting the values by using appcmd.exe:
How to Disable FTP Credential Caching
cd /d "%SystemRoot%\System32\Inetsrv" appcmd.exe set config -section:system.ftpServer/caching /credentialsCache.enabled:"False" /commit:apphost net stop FTPSVC net start FTPSVC
How to Specify a Custom Timeout for FTP Credential Caching
cd /d "%SystemRoot%\System32\Inetsrv" appcmd.exe set config -section:system.ftpServer/caching /credentialsCache.enabled:"True" /commit:apphost appcmd.exe set config -section:system.ftpServer/caching /credentialsCache.flushInterval:"300" /commit:apphost net stop FTPSVC net start FTPSVC
I hope this helps. ;-]
Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/
30 July 2010 • by Bob • FTP
For this installment in my series about FTP Clients I'm going to review the FTP features in Microsoft's Expression Web 4 (EW4). You can find out more about the Microsoft Expression series of products from the following URL:
Note: There are a lot of really cool features that are built into EW4, like Search Engine Optimization (SEO) tools, rich extensibility APIs, previewing content side-by-side in multiple browser windows through SuperPreview, built-in support for programming languages like ASP/ASP.NET/PHP/etc. But that being said, in keeping with the main theme of my FTP client series, this review is focusing on just the FTP aspects of EW4 - not the entire product.
At the time of this blog post, EW4 is a for-retail product that is available as part of the Expression Studio 4 Web Professional and Expression Studio 4 Ultimate suites.
The EW4 user interface follows the same design paradigm as earlier versions of Expression Web, albeit with the darker color scheme that Expression products have been using in recent versions. While EW4 contains many features that you would expect in a Microsoft Office application, it does not resemble the Office 2007/2010 user interface, so there is no ribbon-style toolbar. (This is a bad thing if you love the Office ribbon and a good thing if you hate the Office ribbon; but I'll leave that up to you to decide. <grin>)
One minor personal issue that I have with Expression Web is that VBA was deprecated a while ago, so EW4 doesn't have a macro language that I can use to automate tasks like I would do with previous versions. It's possible to create "add-ins" for Expression Web, but there's a lot of overhead associated with that. From my perspective, that's pretty much like saying to someone, "I know that you would like to get across town and you already have a really nice car, but we're going to take that away. If you take 17 different buses and then walk three or four blocks, you will eventually wind up where you want to go. Of course, it will take you several hours longer and it's a really big hassle, but sooner or later you'll get there." (No comments about carbon footprint - please. <grin>)
That being said, EW4 is a great web site editor and is a good FTP client, and EW4 is much better than its predecessors. (Note: By "predecessors" I mean earlier versions of Expression Web and FrontPage.) I'll explain more in the following sections of this post.
Opening a site is straight-forward, and for the most part the user interface is the same whether you are opening a site over FTP/FTPS or over HTTP using WebDAV or FPSE.
When you are opening an FTP or HTTP site for the first time, your list of managed sites will be empty. As you open sites, the list of sites will be populated for each site there you have the Add to managed sites check box selected.
Once you have entered your site information, EW4 will prompt you for your remote editing options. This allows you to choose between editing the live site over FTP or editing a local copy and publishing your changes at a later date & time.
The last dialog before opening the FTP site is the all-too-familiar prompt for your user credentials, albeit with a warning about FTP credentials being transmitted without encryption. (This is why you should use FTPS, but I'll discuss that later in this post.)
Once the credentials have been verified by the FTP server, EW4 will display your site and you can begin editing your content.
EW4 has some basic site management functionality, which is accessed through the Site -> Manage Site List menu. From there you can add or remove sites from the list. Unfortunately you cannot modify the settings for sites in the list; you have to remove and re-add sites with different settings.
That's it for the simple stuff - now we'll take a look at the specific FTP topics that I've discussed in my other FTP client blog posts.
EW4 supports both Implicit and Explicit FTPS, so the choice is up to you to decide which method to use. The FTPS method is specified by the port number that you choose when you are connecting.
I realize that I have posted the following information in almost all of my posts in this FTP client series, but in the interests of completeness it needs to be said again - the following rules apply for FTP7 when determining whether you are using Implicit or Explicit FTPS:
EW4 doesn't have a way of specifying Explicit or Implicit FTPS other than the port numbers listed above. That being said, more often than not you will probably be using Explicit FTPS on the default port (21) so you won't need to enter a port at all.
For example, if you are using EW4 with Explicit FTPS on the default port, you can skip adding a port number.
However, if you are using Implicit FTPS, you need make sure that you configure EW4 to connect on port 990.
Because EW4's login dialog allows you to specify the virtual host name as part of the user credentials, EW4 works great with FTP7's virtual host names. All that you need to do is use the "ftp.example.com|username" or "ftp.example.com\username" syntax when specifying your username, and when you connect to the FTP7 server it will route your requests to the correct FTP virtual host site.
Unfortunately EW4 does not have built-in for the HOST command, nor does it have support for entering commands that will be sent before the client has logged in, so you cannot use true FTP host names when using EW4 to connect to FTP7 sites that are configured with host names.
This concludes our quick look at some of the FTP features that are available with EW4, and here are the scorecard results:
Client Name | Directory Browsing | Explicit FTPS | Implicit FTPS | Virtual Hosts | True HOSTs |
---|---|---|---|---|---|
Y | Y | Y | Y | N1 | |
1 As noted earlier, EW4 has no way to send a HOST command, so true FTP HOSTs are not supported. |
Note: I've included the following disclaimer in all of my posts, and this post is no exception - there are a great number of additional features that EW4 provides - once again I'm just keeping the focus on those topic areas that apply to FTP7. ;-]
Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/
07 May 2010 • by Bob • FTP
For this installment in my series about FTP Clients I'm going to review the SmartFTP Client from SmartSoft Ltd. For this blog post I used the SmartFTP Client Ultimate Edition version 4.0.1105.0, and it is available from the following URL:
At the time of this blog post, SmartFTP is a for-retail product that is available in three editions: Home, Professional, and Ultimate. A description of the prices and features that are available in each edition is currently available from the following URL:
As for myself, I would use at least the Professional edition, and that statement is based on the features that I typically look for in an FTP client. (Although I would have loved to have had the Ultimate Edition several years ago when I used Telnet to connect to servers. <grin>)
The SmartFTP Client is pretty intuitive and it's easy to navigate within the application. If you are used to using typical Windows applications then you should find that the user interface follows most of the established paradigms that you would expect from a Microsoft application; it seemed to me that the design emulated the relevant parts of Windows Explorer, Visual Studio, and Office with an FTP focus.
For example, switching your directory listing views align with most Windows applications, and the site management functionality is managed through a hierarchical set of "Favorites."
Each "Favorite" has a variety of additional settings that you can edit by open the properties dialog for the favorite.
One of the great features in all editions of the SmartFTP client is a nicely-implemented Remote Edit functionality, which allows you to invoke your favorite editor from inside the client's GUI.
The SmartFTP Client does not have a built-in command-line interface, although there is a script-based command-line interface that you can download separately from the SmartFTP web site. That being said, that script is not created by the folks at SmartSoft, and it's functionality is extremely limited.
For me personally, the SmartFTP Client's extensibility model more than makes up for the lack of command-line functionality. More often than not I'm simply using the command-line in order to script FTP operations, and the SmartFTP extensibility features provide a great deal more capabilities than I would have available to me when automating a command-line FTP client.
I haven't spent a great deal of time working with the extensibility features, but so far I am pretty impressed. I was able to take one of the samples and retool it into a simple FTP client pretty easily. (I will include that as an example in a later blog post.) In the meantime, you can download the SDK for the SmartFTP Client from the following URL:
The SmartFTP Client supports both Implicit and Explicit FTPS, so the choice is up to you to decide which method to use. The FTPS method is stored as the Protocol in a favorite's properties, which is easily located in the General settings for a favorite.
Once again, the following rules apply for FTP7 when determining whether to specify Implicit or Explicit FTPS:
If you are using Implicit FTPS, make sure that you configure your FTP client to connect on port 990.
Because the SmartFTP Client's properties for favorites allow you to specify the virtual host name as part of the user credentials, the SmartFTP Client works great with FTP7's virtual host names. All that you need to do is use the "ftp.example.com|username" syntax when specifying your username, and when you connect to the FTP7 server it will route your requests to the correct FTP virtual host site.
The SmartFTP Client provides built-in support for the HOST command, which means that you can have real multi-homed FTP sites when using the SmartFTP Client to connect to FTP7 sites that are configured with host names. In order to use true HOSTs in the SmartFTP Client, you need to configure the client to send the FEAT command before logging in. This is configured in the Connection settings in the drop-down menu for "Send FEAT."
As an FYI - I had some discussions with Mat Berchtold from SmartSoft while I was writing this review, and Mat informed me that the SmartFTP Client doesn't automatically assume support for the HOST command; sending the FEAT command before logging in allows the client to discover if HOST is supported before continuing.
Mat Berchtold from SmartSoft also mentioned that some firewalls do not yet recognize the HOST command, and therefore those firewalls may not pass the HOST command through the firewall. That's something to think about if you start to see connection failures related to hostnames not being found - you can check your FTP server's logs to see if the HOST command is arriving at the server.
This concludes our quick look at some of the features that are available with the SmartFTP Client, and here's the scorecard results:
Client Name | Directory Browsing | Explicit FTPS | Implicit FTPS | Virtual Hosts | True HOSTs |
---|---|---|---|---|---|
SmartFTP Client Ultimate 4.0.1105.0 | Y | Y | Y | Y | Y 1 |
1 As noted earlier, true FTP HOSTs are fully supported, but you need to configure the SmartFTP Client to send the FEAT command before logging in. |
Note: I've included the following disclaimer in all of my posts, and this post is no exception. ;-] There are a great number of additional features that the SmartFTP Client provides - once again I'm just keeping the focus on those topic areas that apply to FTP7.
Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/