Automating the Creation of FTP User Isolation Folders

A customer asked me a question a little while ago that provided me the opportunity to recycle some code that I had written many years ago. In so doing, I also made a bunch of updates to the code to make it considerably more useful, and I thought that it would make a great blog.

Here's the scenario: a customer had hundreds of user accounts created, and he wanted to use the FTP service's User Isolation features to restrict each user to a specific folder on his FTP site. Since it would take a long time to manually create a folder for each user account, the customer wanted to know if there was a way to automate the process. As it turns out, I had posted a very simple script in the IIS.net forums several years ago that did something like what he wanted; and that script was based off an earlier script that I had written for someone else back in the IIS 6.0 days.

One quick reminder - FTP User Isolation uses a specific set of folders for user accounts, which are listed in the table below.

User Account TypesHome Directory Syntax
Anonymous users %FtpRoot%\LocalUser\Public
Local Windows user accounts

(Requires Basic authentication.)

%FtpRoot%\LocalUser\%UserName%
Windows domain accounts

(Requires Basic authentication.)

%FtpRoot%\%UserDomain%\%UserName%

Note: %FtpRoot% is the root directory for your FTP site: for example, C:\Inetpub\Ftproot.

That being said, I'm a big believer in recycling code, so I found the last version of that script that I gave to someone and I made a bunch of changes to it so it would be more useful for the customer. What that in mind, here's the resulting script, and I'll explain a little more about what it does after the code sample.

Option Explicit

' Define the root path for the user isolation folders.
' This should be the root directory for your FTP site.
Dim strRootPath : strRootPath = "C:\Inetpub\wwwroot\"

' Define the name of the domain or the computer to use.
' Leave this blank for the local computer.
Dim strComputerOrDomain : strComputerOrDomain = ""

' Define the remaining script variables.
Dim objFSO, objCollection, objUser, objNetwork, strContainerName

' Create a network object; used to query the computer name.
Set objNetwork = WScript.CreateObject("WScript.Network")

' Create a file system object; used to creat folders.
Set objFSO = CreateObject("Scripting.FileSystemObject")

' Test if the computer name is null.
If Len(strComputerOrDomain)=0 Or strComputerOrDomain="." Then
  ' If so, define the local computer name as the account repository.
  strComputerOrDomain = objNetwork.ComputerName
End If

' Verify that the root path exists.
If objFSO.FolderExists(strRootPath) Then

  ' Test if the script is using local users.
  If StrComp(strComputerOrDomain,objNetwork.ComputerName,vbTextCompare)=0 Then
    ' If so, define the local users container path.
    strContainerName = "LocalUser"
    ' And define the users collection as local.
    Set objCollection = GetObject("WinNT://.")
  Else
    ' Otherwise, use the source name as the path.
    strContainerName = strComputerOrDomain
    ' And define the users collection as remote.
    Set objCollection = GetObject("WinNT://" & strComputerOrDomain & "")
  End If

  ' Append trailing backslash if necessary.
  If Right(strRootPath,1)<>"\" Then strRootPath = strRootPath & "\"
  ' Define the adjusted root path for the container folder.
  strRootPath = strRootPath & strContainerName & "\"

  ' Test if the container folder already exists.
  If objFSO.FolderExists(strRootPath)=False Then
    ' Create the container folder if necessary.
    objFSO.CreateFolder(strRootPath)
  End If

  ' Specify the collection filter for user objects only.
  objCollection.Filter = Array("user")

  ' Loop through the users collection.
  For Each objUser In objCollection
    ' Test if the user's account is enabled.
    If objUser.AccountDisabled = False Then
      ' Test if the user's folder already exists.
      If objFSO.FolderExists(strRootPath & "\" & objUser.Name)=False Then
        ' Create the user's folder if necessary.
           objFSO.CreateFolder(strRootPath & "\" & objUser.Name)
         End If
       End If
     Next

End If

I documented this script in great detail, so it should be self-explanatory for the most part. But just to be on the safe side, here's an explanation of what this script is doing when you run it on your FTP server:

  • Defines two user-updatable variables:
    • strRootPath - which specifies the physical path to the root of your FTP site.
    • strComputerOrDomain - which specifies the computer name or the domain name where your user accounts are located. (Note: You can leave this blank if you are using local user accounts on your FTP server.)
  • Creates a few helper objects and determines the local computer name if necessary.
  • Checks to see if the physical path to the root of your FTP site actually exists before continuing.
  • Creates a connection to the user account store (local or domain).
  • Determines the container folder name that be the parent directory of user account folders, and creates it if necessary. (See my earlier note about the folder names.)
  • Defines a filter for user objects in the specifies account repository. (This removes computer accounts and such from the operation.)
  • Loops through the collection of user accounts, checks each account to see if it is enabled, and creates a folder for each user account if it does not already exist.

That's all for now. ;-]


Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/

Programmatically Starting and Stopping FTP Sites in IIS 7 and IIS 8

I was recently contacted by someone who was trying to use Windows Management Instrumentation (WMI) code to stop and restart FTP websites by using code that he had written for IIS 6.0; his code was something similar to the following:

Option Explicit
On Error Resume Next

Dim objWMIService, colItems, objItem

' Attach to the IIS service.
Set objWMIService = GetObject("winmgmts:\root\microsoftiisv2")
' Retrieve the collection of FTP sites.
Set colItems = objWMIService.ExecQuery("Select * from IIsFtpServer")
' Loop through the sites collection.
For Each objItem in colItems
    ' Restart one single website.
    If (objItem.Name = "MSFTPSVC/1") Then
        Err.Clear
        objItem.Stop
        If (Err.Number <> 0) Then WScript.Echo Err.Number
        objItem.Start
        If (Err.Number <> 0) Then WScript.Echo Err.Number
    End If
Next

The problem that the customer was seeing is that this query did not return the list of FTP-based websites for IIS 7.0 or IIS 7.5 (called IIS7 henceforth), although changing the class in the query from IIsFtpServer to IIsWebServer would make the script work with HTTP-based websites those versions of IIS7.

The problem with the customer's code was that he is using WMI to manage IIS7; this relies on our old management APIs that have been deprecated, although part of that model is partially available through the metabase compatibility feature in IIS7. Here's what I mean by "partially": only a portion of the old ADSI/WMI objects are available, and unfortunately FTP is not part of the objects that can be scripted through the metabase compatibility feature in IIS7.

That being said, what the customer wants to do is still possible through scripting in both IIS7 and IIS8, and the following sample shows how to loop through all of the sites, determine which sites have FTP bindings, and then stop/start FTP for each site. To use this script, copy the code into a text editor like Windows Notepad and save it with a name like "RestartAllFtpSites.vbs" to your system, then double-click the file to run it.

' Temporarily disable breaking on runtime errors.
On Error Resume Next

' Create an Admin Manager object.
Set adminManager = CreateObject("Microsoft.ApplicationHost.AdminManager")
adminManager.CommitPath = "MACHINE/WEBROOT/APPHOST"

' Test for commit path support.
If Err.Number <> 0 Then
    Err.Clear
    ' Create a Writable Admin Manager object.
    Set adminManager = CreateObject("Microsoft.ApplicationHost.WritableAdminManager")
    adminManager.CommitPath = "MACHINE/WEBROOT/APPHOST"
    If Err.Number <> 0 Then WScript.Quit
End If

' Resume breaking on runtime errors.
On Error Goto 0

' Retrieve the sites collection.
Set sitesSection = adminManager.GetAdminSection("system.applicationHost/sites", "MACHINE/WEBROOT/APPHOST")
Set sitesCollection = sitesSection.Collection

' Loop through the sites collection.
For siteCount = 0 To CInt(sitesCollection.Count)-1
    isFtpSite = False
    ' Determine if the current site is an FTP site by checking the bindings.
    Set siteElement = sitesCollection(siteCount)
    Set bindingsCollection = siteElement.ChildElements.Item("bindings").Collection
    For bindingsCount = 0 To CInt(bindingsCollection.Count)-1
        Set bindingElement = bindingsCollection(bindingsCount)
        If StrComp(CStr(bindingElement.Properties.Item("protocol").Value),"ftp",vbTextCompare)=0 Then
            isFtpSite = True
            Exit For
        End If
    Next
    ' If it's an FTP site, start and stop the site.
    If isFtpSite = True Then
        Set ftpServerElement = siteElement.ChildElements.Item("ftpServer")
        ' Create an instance of the Stop method.
        Set stopFtpSite = ftpServerElement.Methods.Item("Stop").CreateInstance()
        ' Execute the method to stop the FTP site.
        stopFtpSite.Execute()
        ' Create an instance of the Start method.
        Set startFtpSite = ftpServerElement.Methods.Item("Start").CreateInstance()
        ' Execute the method to start the FTP site.
        startFtpSite.Execute()
    End If
Next

And the following code sample shows how to stop/start a single FTP site. To use this script, copy the code into a text editor like Windows Notepad, rename the site name appropriately for one of your FTP sites, save it with a name like "RestartContosoFtpSite.vbs" to your system, then double-click the file to run it.

' Temporarily disable breaking on runtime errors.
On Error Resume Next

' Create an Admin Manager object.
Set adminManager = CreateObject("Microsoft.ApplicationHost.AdminManager")
adminManager.CommitPath = "MACHINE/WEBROOT/APPHOST"

' Test for commit path support.
If Err.Number <> 0 Then
    Err.Clear
    ' Create a Writable Admin Manager object.
    Set adminManager = CreateObject("Microsoft.ApplicationHost.WritableAdminManager")
    adminManager.CommitPath = "MACHINE/WEBROOT/APPHOST"
    If Err.Number <> 0 Then WScript.Quit
End If

' Resume breaking on runtime errors.
On Error Goto 0

' Retrieve the sites collection.
Set sitesSection = adminManager.GetAdminSection("system.applicationHost/sites", "MACHINE/WEBROOT/APPHOST")
Set sitesCollection = sitesSection.Collection

' Locate a specific site.
siteElementPos = FindElement(sitesCollection, "site", Array("name", "ftp.contoso.com"))
If siteElementPos = -1 Then
    WScript.Echo "Site was not found!"
    WScript.Quit
End If

' Determine if the selected site is an FTP site by checking the bindings.
Set siteElement = sitesCollection(siteElementPos)
Set bindingsCollection = siteElement.ChildElements.Item("bindings").Collection
For bindingsCount = 0 To CInt(bindingsCollection.Count)-1
    Set bindingElement = bindingsCollection(bindingsCount)
    If StrComp(CStr(bindingElement.Properties.Item("protocol").Value),"ftp",vbTextCompare)=0 Then
        isFtpSite = True
        Exit For
    End If
Next

' If it's an FTP site, start and stop the site.
If isFtpSite = True Then
    Set ftpServerElement = siteElement.ChildElements.Item("ftpServer")
    ' Create an instance of the Stop method.
    Set stopFtpSite = ftpServerElement.Methods.Item("Stop").CreateInstance()
    ' Execute the method to stop the FTP site.
    stopFtpSite.Execute()
    ' Create an instance of the Start method.
    Set startFtpSite = ftpServerElement.Methods.Item("Start").CreateInstance()
    ' Execute the method to start the FTP site.
    startFtpSite.Execute()
End If

' Locate and return the index for a specific element in a collection.
Function FindElement(collection, elementTagName, valuesToMatch)
   For i = 0 To CInt(collection.Count) - 1
      Set elem = collection.Item(i)
      If elem.Name = elementTagName Then
         matches = True
         For iVal = 0 To UBound(valuesToMatch) Step 2
            Set prop = elem.GetPropertyByName(valuesToMatch(iVal))
            value = prop.Value
            If Not IsNull(value) Then
               value = CStr(value)
            End If
            If Not value = CStr(valuesToMatch(iVal + 1)) Then
               matches = False
               Exit For
            End If
         Next
         If matches Then
            Exit For
         End If
      End If
   Next
   If matches Then
      FindElement = i
   Else
      FindElement = -1
   End If
End Function

I hope this helps!


Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/

Programmatically Enumerating Installations of the FrontPage Server Extensions

I had a great question from a customer the other day: "How do you programmatically enumerate how many web sites on a server have the FrontPage Server Extensions installed?" Of course, that's one of those questions that sounds so simple at first, and then you start to think about how to actually go about it and it gets a little more complicated.

The first thought that came to mind was to just look for all the "W3SVCnnnn" subfolders that are located in the "%ALLUSERSPROFILE%\Application Data\Microsoft\Web Server Extensions\50" folder. (These folders contain the "ROLES.INI" files for each installation.) The trouble with this solution is that some folders and files do not get cleaned up when the server extensions are uninstalled, so you'd get erroneous results.

The next thought that came to mind was to check the registry, because each installation of the server extensions will create a string value and subkey named "Port /LM/W3SVC/nnnn:" under the "[HKLM\SOFTWARE\Microsoft\Shared Tools\Web Server Extensions\Ports]" key. Enumerating these keys will give you the list of web sites that have the server extensions or SharePoint installed. The string values that are located under the subkey contain some additional useful information, so I thought that as long as I was enumerating the keys, I might as well enumerate those values.

The resulting script is listed below, and when run it will create a log file that lists all of the web sites that have the server extensions or SharePoint installed on the server that is specified by the "strComputer" constant.

Option Explicit

Const strComputer = "localhost"

Dim objFSO, objFile
Dim objRegistry
Dim strRootKeyPath, strSubKeyPath, strValue
Dim arrRootValueTypes, arrRootValueNames
Dim arrSubValueTypes, arrSubValueNames
Dim intLoopA, intLoopB

Const HKEY_LOCAL_MACHINE = &H80000002
Const REG_SZ = 1

strRootKeyPath = "Software\Microsoft\" & _
  "Shared Tools\Web Server Extensions\Ports"

Set objFSO = WScript.CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.CreateTextFile("ServerExtensions.Log")

objFile.WriteLine String(40,"-")
objFile.WriteLine "Report for server: " & UCase(strComputer)
objFile.WriteLine String(40,"-")

Set objRegistry = GetObject(_
  "winmgmts:{impersonationLevel=impersonate}!\\" & _
  strComputer & "\root\default:StdRegProv")
objRegistry.EnumValues HKEY_LOCAL_MACHINE, strRootKeyPath, _
  arrRootValueNames, arrRootValueTypes

For intLoopA = 0 To UBound(arrRootValueTypes)
  If arrRootValueTypes(intLoopA) = REG_SZ Then
    objFile.WriteLine arrRootValueNames(intLoopA)
    strSubKeyPath = strRootKeyPath & _
      "\" & arrRootValueNames(intLoopA)
    objRegistry.EnumValues HKEY_LOCAL_MACHINE, _
      strSubKeyPath, arrSubValueNames, arrSubValueTypes
    For intLoopB = 0 To UBound(arrSubValueTypes)
      If arrSubValueTypes(intLoopB) = REG_SZ Then
        objRegistry.GetStringValue HKEY_LOCAL_MACHINE, _
          strSubKeyPath, arrSubValueNames(intLoopB), strValue
        objFile.WriteLine vbTab & _
          arrSubValueNames(intLoopB) & "=" & strValue
      End If
    Next
    objFile.WriteLine String(40,"-")
  End If
Next

objFile.Close

The script should be fairly easy to understand, and you can customize it to suit your needs. For example, you could change the "strComputer" constant to a string array and loop through an array of servers.

Note: More information about the WMI objects used in the script can be found on the following pages:

Hope this helps!