In this day and age, Web applications have become the norm. We've even come to the point where many development projects involve Web applications that must be installed on multiple servers.

But even if you don't build vertical Web applications, it's useful to have a configuration utility that can recreate a configuration via code. This might be for backup purposes, or for high volume environments like load balancing, where multiple servers need to be configured.

In order to accomplish this gracefully, you need to be able to configure the Web server programmatically, because the current crop of development tools do not provide that functionality. In this article, I'll demonstrate how you can configure Internet Information Server programmatically by using the Active Directory Service Interface (ADSI) and the IIS Admin objects to create an installation application. I'll show a Visual FoxPro class that provides several of the key elements needed for configuring a Web application, so you can build setup Wizards or preset setup scripts.

How do we configure a Web server?

When you build a Web application there are many components that make up that application. You have the data engine with its data files, whether SQL Server, another backend database, Visual FoxPro or Access. There are the actual application binary files - the EXE or DLL of the application, plus any support files. If you're building a purely script-based application, you won't have those binary application files. In a Web application, there are HTML or scripted HTML (plain HTML or ASP, JSP, Cold Fusion or Web Connection script) files that must be deployed in a Web directory.

Notice that in most Web applications, the HTML/script content in the Web directories will be separate from the binary and data content of the application. This means that if you distribute a Web application, you typically install into two locations: the application path and the Web path.

The Web path requires more work than a traditional application path, because it's sitting on the Web server and needs to be recognized as a Web path. It also needs certain permissions set so that the Web server can access the files correctly.

A typical Web application needs to perform the following configuration tasks:

Find the Web site the user wants to install onIIS allows you to install multiple Web sites on the same machine. In particular, ISPs will have a large number of sites on a single machine, so don't necessarily assume that the user wants to install on the default Web site.

Install and/or configure the Web siteIf you're building a vertical Web application, it's likely that you want to install the application on a completely separate Web site as opposed to just a directory under an existing site. You'd create the Web site and then also configure it with performance and security settings. In some cases, you may also change some of these settings for non-dedicated Web sites.

Create a Virtual Directory for your applicationRegardless of whether your application runs on a dedicated site or under an existing site, you'll need to set up a virtual directory for it. Virtual directories allow you to isolate your application from the rest of the site and configure it independently. Once a virtual directory is created, you need to configure its security settings.

Create script mapsIf your application runs a custom development tool such as Web Connection, you may also want to set up a custom script map that routes to a specific handler ISAPI DLL. The script map provides a custom extension to your application and, in some cases, lets you hide clues about the tool you're using behind the scenes (which can give you more security against hackers).

The IIS Admin Objects

The IIS Admin Objects make it possible to configure the above tasks and more relatively painlessly. The IIS Admin Objects are in a COM object that can connect you to any resource listed in the IIS metabase, which is used to store all the IIS configuration settings. The IIS Admin Objects contain an Active Directory Service Interface (ADSI) that's specific to IIS and uses the common ADSI interface syntax. This includes a handful of method calls and properties, extended by the properties that the actual service (in this case the IIS Admin provider) exposes.

This is best illustrated by an example. The following code accesses the default Web site and retrieves a few of the properties via the IIS Admin objects:

*** Connect to the Root directory of the first site
oVirtual = GETOBJECT("IIS://LOCALHOST/W3SVC/1/ROOT")

? oVirtual.Class && IIsWebVirtualDir

? oVirtual.Path && d:\inetpub\wwwroot
? oVirtual.AnonymousUserName && IUSR_<Machine>
? oVirtual.AuthBasic && Basic Authentication flag
? oVirtual.AccessExecute && Execute rights

*** Configure settings
oVirtual.Path = "d:\wwindweb"
oVirtual.AuthBasic = .T.
oVirtual.AccessExecute = .T.

oVirtual.SetInfo() && Save Settings

This short snippet connects to the root directory of the default Web site and reads a few settings. However, most configuration settings of value are found at the virtual directory level. The hierarchy of the IIS Admin Objects starts with:

IISWebService

GETOBJECT("IIS://LOCALHOST/W3SVC/")

Contains many settings similar to the virtual directory settings, but doesn't let you control them. This object contains many performance options, however. Typically you'll use this only for very specific things. The Web service is W3SVC in the GETOBJECT moniker string above.

IISWebServer


GETOBJECT("IIS://LOCALHOST/W3SVC/1")

The individual Web sites which can be enumerated. The 1 in the GETOBJECT moniker above identifies the site in question. Sites are numbered sequentially and must be enumerated in order to retrieve a friendly name (I'll show an example of that shortly). This object also serves mainly as a performance and high level settings place holder. Although it has many of the same settings as IISWebVirtualDir, many of these settings don't do anything. The performance options do, however.

IISWebVirtualDir


GETOBJECT("IIS://LOCALHOST/W3SVC/1/ROOT") && Root directory
GETOBJECT("IIS://LOCALHOST/W3SVC/1/WCONNECT") && Specific Virtual

The virtual directories, including the ROOT directory, contain most of the important settings that you need to configure a Web site.

As a general rule, you want to set any inherited settings at the lowest possible level. So, setting execute rights should be done at the virtual level and not at the Web server level. Performance settings that are available both in the WebService and WebServer objects should be made on the WebServer object. For a detailed list of properties available for each of these objects, see the MSDN documentation for the IIS Admin Objects. A Web link to this topic is provided at the end of the article.

Looking at the example above, you can see that you can read and write settings simply by assigning values to the appropriate property. But, make sure that you call SetInfo() to actually write the settings into the IIS metabase. Until you do, the settings are cached and don't change the operation of IIS. Once SetInfo() has been called, the settings are written and take effect immediately.

We've got some configuring to do

OK, now that we have the basic idea for getting a reference to our Web service, let's set up a class and show some specific tasks that you'll want to accomplish. Here are a few examples of how you might use this class, called wwIISAdmin:

SET CLASSLIB TO WEBSERVER ADDITIVE

lcPath = "d:\westwind\CodeDemo\"

*** Some code to create a dummy directory and
*** copy an ISAPI DLL there
IF !ISDIR(lcPath)
   MD (lcPath)
   COPY FILE scripts\wc.dll TO (lcPath) + "wc.dll"
ENDIF

oIIS = CREATEOBJECT("wwIISAdmin")

*** Retrieve a list of all Web sites into laVirtuals
DIMENSION laVirtuals[1,3]

*** 1 - Site ID Number
*** 2 - Site Name
*** 3 - Site ADSI Path
lnCount = oIIS.aGetWebSites(@laVirtuals, "IIS://LOCALHOST/W3SVC")

*** Get the Web path to our site
lcWebPath = ""
FOR x=1 to lnCount
   IF UPPER(laVirtuals[x,2]) = "WEST WIND"
      lcWebPath = laVirtuals[x,3]
   ENDIF
ENDFOR

IF EMPTY(lcWebPath)
   RETURN
ENDIF

*** Assign the ROOT path of the Web site
oIIS.cPath = lcWebPath + "/ROOT"

*** Create the new virtual directory under the root
oIIS.CreateVirtual("CodeDemoVirtual",lcPath)

*** Set additional options
oIIS.oRef.AuthBasic = .F.
oIIS.oRef.AccessExecute = .T.

*** Save the setting changes on the oRef object
oIIS.Save()

*** Create a mapping for wwwc to wc.dll ISAPI DLL
oIIS.CreateScriptMap("wwwc",lcPath + "wc.dll")

This simple code demonstrates the basics of what happens in common Web installs: You create a new directory, copy some files to it, then create a Web virtual directory and configure it as needed. In this example, I'm also creating a script map that maps an ISAPI DLL to the WWWC extension so that any requests against this extension are routed to the specified DLL.

Figure 1 - Configuring a Web Server usually involves creating a virtual directory, setting configuration options on that directory, and setting up a script map. The directory above was configured entirely from our sample code.

There are a couple of important things to remember in this process. First, note that I go out and get a list of all the Web sites defined on this server. In a typical front end application, you'd probably give the user a choice of the Web site and then prompt for a file location. Then, you could copy files and create the virtual directory or set rights on an existing path.

Next, notice that the code above relies on two properties in the wwIISAdmin object:

cPath

A path that will be used with GETOBJECT() and refers to the parent object that we'll be working on for the next operation. So, if we create a virtual directory, it'll be the parent of the virtual. You set this path before a call to CreateVirtual or aGetWebSites.

oRef

This object contains a reference of the last operation. For example, a reference to the created directory will be returned so you can do further configuration on it. In the code above, note that the oRef object is used to override default settings made in the creation of the virtual directory.

Let's take a look and see how this code works behind the scenes, by starting with the aGetWebSites method:

FUNCTION aGetWebSites
LPARAMETER aWebSites, lcPath
LOCAL x, oSite

IF EMPTY(lcPath)
   lcpath = "IIS://LOCALHOST/W3SVC"
ENDIF

*** Get a reference to the Web Service
loRef=GETOBJECT(lcPath)
IF ISNULL(loRef)
   RETURN 0
ENDIF

x=0

*** Loop through all of the containers
*** inside of the Service item
FOR EACH oSite IN loRef
   *** Sites are identified by numeric values
   IF VAL(oSite.name) # 0
      x=x+1
      DIMENSION aWebSites[x,3]
      aWebSites[x,1] = oSite.Name
      aWebSites[x,2] = oSite.ServerComment
      aWebSites[x,3] = oSite.ADsPath
   ENDIF
ENDFOR

RETURN x

This very simple code goes out and uses GETOBJECT() to connect to a specific Web service via ADSI, then iterates through each of the Web sites using a FOR EACH loop. The code then retrieves the site's settings and stores them into the array that is passed in by reference. Remember that a good Web install program should ask the user which Web site he is targeting.

Note that you can also ask for an ADSI path on another machine, as long as your user account has admin rights there. For example, I can do:

lnCount=oIIS.aGetWebSites(@laVirtuals, "IIS://www.west-wind.com/W3SVC")

on my online Web site to retrieve the list of Web sites there. The same goes for the cPath setting. All operations can be performed against remote machines. Keep in mind, though, that all paths that are referenced will be paths on the remote machine and not the local one, so d:\westwind\CodeDemo will refer to the remote box. A good way to present this interface is with a Wizard, as shown in Figure 2.

Figure 2 - A Wizard interface can be perfect to let the user choose the Web server and Web site.

The next step (once you've figured out which site to pick) is to create a virtual directory or update settings on the ROOT directory of the server. If you're working on an existing directory, you can simply use GETOBJECT() to manipulate that object, as shown in the first example earlier.

If you need to create the directory, you can use the wwIISAdmin object. Since I have a pretty standard procedure and group of settings for virtuals created in my installs, I also provide common settings in the CreateVirtual method of the wwIISAdmin class. Take a look:

FUNCTION CreateVirtual
LPARAMETERS lcVirtual, lcPhysical, llNoExecute, llNoAuthBasic
LOCAL lcPath, loVirtual

THIS.lError = .F.

THIS.oRef = GETOBJECT(THIS.cpath)
IF ISNULL(THIS.oRef)
   THIS.cerrormsg = "Unable to connect to server root."
   RETURN .F.
ENDIF

IF EMPTY(lcPhysical)
   *** Delete the Virtual
   THIS.oRef.DELETE("IIsWebVirtualDir",lcVirtual)
   THIS.SAVE()
   IF THIS.lError
      RETURN .F.
   ENDIF
   RETURN .T.
ELSE
   *** Try to create it
   loVirtual = THIS.oRef.CREATE("IIsWebVirtualDir",lcVirtual)

   *** If an error occurred it might exist already
   IF THIS.lError OR TYPE("loVirtual") # "O"
      THIS.lError=.F.
      lcPath = THIS.cpath && Our current relative path

      *** ADd the virtual path to it and try to connect
      loVirtual = GETOBJECT(lcPath + "/" + lcVirtual)
      IF THIS.lError
         *** Still an error - reconnect to the original path
         *** and exit
         THIS.oRef = GETOBJECT(lcPath)
         RETURN .F.
      ENDIF
   ENDIF

   loVirtual.PATH = lcPhysical
   loVirtual.AppCreate(.T.) && Make sure our app is In Process
   loVirtual.AppFriendlyName = lcVirtual

   loVirtual.AccessRead = .T.
   loVirtual.AccessExecute = !llNoExecute
   loVirtual.AuthBasic = !llNoAuthBasic
   loVirtual.AuthNTLM = .T.

   THIS.OnCreateVirtual(loVirtual)

   loVirtual.SetInfo()

   *** Pass out the reference for the directory
   THIS.oRef=loVirtual
ENDIF

RETURN .T.

This code goes out to the directory on the Web server under which you want to create the virtual, then tries to create it. If the virtual exists already, an error will occur internally, which is captured and sets the lError flag. This flag is checked and, if .T., the assumption is that the directory already exists. In that case, the code simply tries to access the directory. Once the directory is created or accessed, we have a reference to the object, and several default settings are applied. The most important of these is the physical directory to which the virtual points.

Notice that the OnCreateVirtual method is called after this process is complete, to allow you to customize the virtual creation process with your own post-processing behavior by overriding that method. Alternatively, as shown in the example code above, you can simply exit the method and use the oRef member to change any settings after the fact. The bottom line is that you don't give up any flexibility, since you still get a reference to the base ADSI object and can do as you will with it. While the basic concept of creating a virtual is easy, you can see that a fair amount of code is required to do it right. This wrapper class provides the error handling and additional functionality of presetting defaults.

In an application, it's a good idea to let the user pick the location for the files and name of the virtual. A typical interface is shown in Figure 3.

Figure 3 - It's a good idea to let the end-user or administrator choose the directory and name of the virtual where your files will go.

Dealing with ADSI Collections in VFP

Looking at the ADSI code here, you can see that manipulation of the IIS Admin objects is simple. However, one aspect that has always been tricky in VFP is dealing with collections, such as those used for scriptmap extensions for the Web root (or individual web directory). The following code demonstrates how to manipulate the scriptmaps collection to create a new script map:

FUNCTION CreateScriptMap
LPARAMETERS lcScriptMap, lcPath
LOCAL lnResult, x, cScriptMap, loScriptmaps

*** Fix up script map entry
IF lcScriptMap <> "."
   lcScriptMap = "."+lcScriptMap
ENDIF

lcScriptMap = LOWER(lcScriptMap)
lcPath = LOWER(lcPath)

THIS.lerror = .F.

*** Get a reference to the Web path to work on
THIS.oRef=GETOBJECT(THIS.cPath)
IF ISNULL(THIS.oRef)
   THIS.ErrorMsg = "Unable to access the default web root"
   RETURN .F.
ENDIF

*** Make sure we're using 0 based arrays by Value
COMARRAY(THIS.oRef,0)
loScriptmaps = THIS.oRef.scriptmaps

x=0
FOR EACH cScriptMap IN loScriptmaps
   x=x+1

   *** Check if we need to UPDATE an existing script map
   IF LOWER(LEFT(cScriptMap,LEN(lcScriptMap))) = lcScriptMap
      loScriptmaps[x] = lcScriptMap + "," + lcPath + ",1"
      THIS.oRef.PutEx(2,"ScriptMaps",@loScriptmaps)
      THIS.oRef.SetInfo()

      loScriptmaps = .NULL.
      THIS.oRef = .NULL.
      RETURN .T.
   ENDIF

   *** Just in case there's a problem
   IF x > 200
      EXIT
   ENDIF

ENDFOR

*** Add another item
x=x+1
DIMENSION loScriptmaps[x]
loScriptmaps[x] = lcScriptMap + "," + lcPath + ",1"

THIS.oRef.PutEx(2,"ScriptMaps",@loScriptmaps)
THIS.oRef.SetInfo()

loScriptmaps = .NULL.
THIS.oRef = .NULL.
RETURN .T..

Note the use of the COMARRAY() function to set the array as 0-based, which is necessary for VFP to access and write the array data. This makes the array 0-based but lets VFP continue to treat it as an array starting with item [1].

The code runs through all the existing scriptmaps to see whether our specific scriptmap already exists. If it does, it's overwritten rather than added again. ADSI would (invalidly) allow duplicate scriptmapts, so this code is necessary. Note that in order to save any changes to the script map array, we need to save with the PutEx() ADSI method, which is passed a value of 2 (array), the key, and the actual value. In this case, our scriptmap array is passed by reference (actually, because of the COMARRAY setting, the array is passed by value over the COM boundary).

You can use this same type of code with any of the collections in the IIS Metabase, such as the list of ISAPI filters.

Summary

I hope this article has shown the basics you need to start building your own configuration programs for Web applications. It's highly useful to build an install script to quickly recreate a setup or build a full-featured install for a Web application to be installed in multiple locations.

The classes I provided let you easily do most of the work with IIS. Provided in the zip file of this class is also a more high-level class called wwWebServer, which provides functionality for IIS4 and 5, IIS3 (registry based), Personal Web Server on Win9x, Apache, and O'Reilly's Web site, so you can build installs for any of these Web servers providing most of the functionality I've described here. IIS is by far the most flexible platform, as far as installs are concerned, but if you have a product that can run on other servers, this can come in handy.