I had some free time one night and decided to work on something that I’d had several people ask me about – extending a web application programmatically. Honestly I was surprised at how many people had specifically asked me to create this command. To accomplish this I created a new command: gl-extendwebapp. Note that I’m starting to prefix my commands (something I should always have been doing) and I will eventually set all commands to have this prefix so expect that breaking change to come soon. The code is actually not too bad – you basically create a new SPIisSettings object and add an SPServerBinding or SPSecureBinding object based on whether it’s an SSL site or not. The only odd piece of my code is that I use a little bit of reflection so that I can fire the timer job using the same code that Microsoft uses when executed via the browser:

  1using System;
  2using System.DirectoryServices;
  3using System.Globalization;
  4using System.IO;
  5using System.Reflection;
  6using System.Text;
  7using Lapointe.SharePoint.STSADM.Commands.OperationHelpers;
  8using Lapointe.SharePoint.STSADM.Commands.SPValidators;
  9using Microsoft.SharePoint;
 10using Microsoft.SharePoint.Administration;
 11 
 12namespace Lapointe.SharePoint.STSADM.Commands.WebApplications
 13{
 14    public class ExtendWebApplication : SPOperation
 15    {
 16        /// <summary>
 17        /// Initializes a new instance of the <see cref="ExtendWebApplication"/> class.
 18        /// </summary>
 19        public ExtendWebApplication()
 20        {
 21            SPParamCollection parameters = new SPParamCollection();
 22            parameters.Add(new SPParam("url", "url", true, null, new SPUrlValidator()));
 23            parameters.Add(new SPParam("vsname", "vsname", true, null, new SPNonEmptyValidator()));
 24            parameters.Add(new SPParam("allowanonymous", "anon"));
 25            parameters.Add(new SPParam("exclusivelyusentlm", "ntlm"));
 26            parameters.Add(new SPParam("usessl", "ssl"));
 27            parameters.Add(new SPParam("hostheader", "hostheader", false, null, new SPNonEmptyValidator()));
 28            parameters.Add(new SPParam("port", "p", false, "80", new SPIntRangeValidator(0, int.MaxValue)));
 29            parameters.Add(new SPParam("path", "path", true, null, new SPNonEmptyValidator()));
 30            SPEnumValidator zoneValidator = new SPEnumValidator(typeof (SPUrlZone));
 31            parameters.Add(new SPParam("zone", "zone", false, SPUrlZone.Custom.ToString(), zoneValidator));
 32            parameters.Add(new SPParam("loadbalancedurl", "lburl", true, null, new SPUrlValidator()));
 33 
 34            StringBuilder sb = new StringBuilder();
 35            sb.Append("\r\n\r\nExtends a web application onto another IIS web site.  This allows you to serve the same content on another port or to a different audience\r\n\r\nParameters:");
 36            sb.Append("\r\n\t-url <url of the web application to extend>");
 37            sb.Append("\r\n\t-vsname <web application name>");
 38            sb.Append("\r\n\t-path <path>");
 39            sb.Append("\r\n\t-loadbalancedurl <the load balanced URL is the domain name for all sites users will access in this SharePoint Web application>");
 40            sb.AppendFormat("\r\n\t[-zone <{0} (defaults to Custom)>]", zoneValidator.DisplayValue);
 41            sb.Append("\r\n\t[-port <port number (default is 80)>]");
 42            sb.Append("\r\n\t[-hostheader <host header>]");
 43            sb.Append("\r\n\t[-exclusivelyusentlm]");
 44            sb.Append("\r\n\t[-allowanonymous]");
 45            sb.Append("\r\n\t[-usessl]");
 46 
 47            Init(parameters, sb.ToString());
 48 
 49        }
 50 
 51        /// <summary>
 52        /// Gets the help message.
 53        /// </summary>
 54        /// <param name="command">The command.</param>
 55        /// <returns></returns>
 56        public override string GetHelpMessage(string command)
 57        {
 58            return HelpMessage;
 59        }
 60 
 61        /// <summary>
 62        /// Runs the specified command.
 63        /// </summary>
 64        /// <param name="command">The command.</param>
 65        /// <param name="keyValues">The key values.</param>
 66        /// <param name="output">The output.</param>
 67        /// <returns></returns>
 68        public override int Execute(string command, System.Collections.Specialized.StringDictionary keyValues, out string output)
 69        {
 70            output = string.Empty;
 71 
 72            SPWebApplication webApplication = SPWebApplication.Lookup(new Uri(Params["url"].Value.TrimEnd('/')));
 73            string description = Params["vsname"].Value;
 74            bool useSsl = Params["usessl"].UserTypedIn;
 75            string hostHeader = Params["hostheader"].Value;
 76            int port = int.Parse(Params["port"].Value);
 77            bool allowAnonymous = Params["allowanonymous"].UserTypedIn;
 78            bool useNtlm = Params["exclusivelyusentlm"].UserTypedIn;
 79            string path = Params["path"].Value;
 80            SPUrlZone zone = (SPUrlZone) Enum.Parse(typeof (SPUrlZone), Params["zone"].Value, true);
 81            string loadBalancedUrl = Params["loadbalancedurl"].Value;
 82 
 83            ExtendWebApp(webApplication, description, hostHeader, port, loadBalancedUrl, path, allowAnonymous, useNtlm, useSsl, zone);
 84 
 85            return OUTPUT_SUCCESS;
 86        }
 87 
 88        /// <summary>
 89        /// Extends the web app.
 90        /// </summary>
 91        /// <param name="webApplication">The web application.</param>
 92        /// <param name="description">The description.</param>
 93        /// <param name="hostHeader">The host header.</param>
 94        /// <param name="port">The port.</param>
 95        /// <param name="loadBalancedUrl">The load balanced URL.</param>
 96        /// <param name="path">The path.</param>
 97        /// <param name="allowAnonymous">if set to <c>true</c> [allow anonymous].</param>
 98        /// <param name="useNtlm">if set to <c>true</c> [use NTLM].</param>
 99        /// <param name="useSsl">if set to <c>true</c> [use SSL].</param>
100        /// <param name="zone">The zone.</param>
101        public static void ExtendWebApp(SPWebApplication webApplication, string description, string hostHeader, int port, string loadBalancedUrl, string path, bool allowAnonymous, bool useNtlm, bool useSsl, SPUrlZone zone)
102        {
103            SPServerBinding serverBinding = null;
104            SPSecureBinding secureBinding = null;
105            if (!useSsl)
106            {
107                serverBinding = new SPServerBinding();
108                serverBinding.Port = port;
109                serverBinding.HostHeader = hostHeader;
110            }
111            else
112            {
113                secureBinding = new SPSecureBinding();
114                secureBinding.Port = port;
115            }
116 
117            SPIisSettings settings = new SPIisSettings(description, allowAnonymous, useNtlm, serverBinding, secureBinding, new DirectoryInfo(path.Trim()));
118            settings.PreferredInstanceId = GetPreferredInstanceId(description);
119 
120            webApplication.IisSettings.Add(zone, settings);
121            webApplication.AlternateUrls.SetResponseUrl(new SPAlternateUrl(new Uri(loadBalancedUrl), zone));
122            webApplication.AlternateUrls.Update();
123            webApplication.Update();
124            webApplication.Provision();
125            if (SPFarm.Local.TimerService.Instances.Count > 1)
126            {
127                // SPWebApplicationProvisioningJobDefinition definition = new SPWebApplicationProvisioningJobDefinition(currentItem, false);
128                Type webAppProvisionJobDefType = Type.GetType("Microsoft.SharePoint.Administration.SPWebApplicationProvisioningJobDefinition, Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c");
129                ConstructorInfo webAppProvisionConstructor =
130                    webAppProvisionJobDefType.GetConstructor(Utilities.AllBindings, null, new Type[] { webApplication.GetType(), typeof(bool) }, null);
131                object definition = webAppProvisionConstructor.Invoke(new object[] { webApplication, false });
132 
133                //definition.Schedule = new SPOneTimeSchedule(DateTime.Now);
134                Utilities.SetPropertyValue(definition, "Schedule", new SPOneTimeSchedule(DateTime.Now));
135 
136                //definition.Update();
137                Utilities.ExecuteMethod(definition, "Update", new Type[] {}, new object[] {});
138            }
139        }
140 
141        /// <summary>
142        /// Gets the preferred instance id.
143        /// </summary>
144        /// <param name="iisServerComment">The IIS server comment.</param>
145        /// <returns></returns>
146        private static int GetPreferredInstanceId(string iisServerComment)
147        {
148            try
149            {
150                int num;
151                if (!LookupByServerComment(iisServerComment, out num))
152                {
153                    return GetUnusedInstanceId(0);
154                }
155                return num;
156            }
157            catch
158            {
159                return GetUnusedInstanceId(0);
160            }
161        }
162 
163        /// <summary>
164        /// Lookups the by server comment.
165        /// </summary>
166        /// <param name="serverComment">The server comment.</param>
167        /// <param name="instanceId">The instance id.</param>
168        /// <returns></returns>
169        private static bool LookupByServerComment(string serverComment, out int instanceId)
170        {
171            instanceId = -1;
172            using (DirectoryEntry entry = new DirectoryEntry("IIS://localhost/w3svc"))
173            {
174                foreach (DirectoryEntry entry2 in entry.Children)
175                {
176                    if (entry2.SchemaClassName != "IIsWebServer")
177                    {
178                        continue;
179                    }
180                    string str = (string) entry2.Properties["ServerComment"].Value;
181                    if (!Utilities.StsCompareStrings(str, serverComment))
182                        continue;
183 
184                    instanceId = int.Parse(entry2.Name, NumberFormatInfo.InvariantInfo);
185                    return true;
186                }
187            }
188            return false;
189        }
190 
191        /// <summary>
192        /// Gets the unused instance id.
193        /// </summary>
194        /// <param name="preferredInstanceId">The preferred instance id.</param>
195        /// <returns></returns>
196        private static int GetUnusedInstanceId(int preferredInstanceId)
197        {
198            Random random = new Random();
199            int num = 0;
200            int num2 = preferredInstanceId;
201            if (num2 < 1)
202            {
203                num2 = random.Next(1, 0x7fffffff);
204            }
205 
206            while (true)
207            {
208                if (++num >= 0x19)
209                {
210                    throw new InvalidOperationException(SPResource.GetString("CannotFindUnusedInstanceId", new object[0]));
211                }
212                if (DirectoryEntry.Exists("IIS://localhost/w3svc/" + num2))
213                {
214                    num2 = random.Next(1, 0x7fffffff);
215                }
216                else
217                    break;
218            }
219            return num2;
220        }
221    }
222}

The syntax of the command can be seen below. Note that the vsname parameter is just the display name within IIS (also known as the server comment). The other fields are pretty self explanatory and match the fields seen via the browser:

C:\>stsadm -help gl-extendwebapp

stsadm -o gl-extendwebapp

Extends a web application onto another IIS web site.  This allows you to serve the same content on another port or to a different audience

Parameters:
        -url <url of the web application to extend>
        -vsname <web application name>
        -path <path>
        -loadbalancedurl <the load balanced URL is the domain name for all sites users will access in this SharePoint Web application>
        [-zone <default | intranet | internet | custom | extranet (defaults to Custom)>]
        [-port <port number (default is 80)>]
        [-hostheader <host header>]
        [-exclusivelyusentlm]
        [-allowanonymous]
        [-usessl]

The following table summarizes the command and its various parameters:

Command NameAvailabilityBuild Date
gl-extendwebappWSS v3, MOSS 2007Released: 3/31/2008, Updated: 11/5/2008
Parameter NameShort FormRequiredDescriptionExample Usage
urlYesThe URL of the existing web application to extend.-url http://portal
vsnameYesThe virtual server name to use – this is the name that will appear in the IIS manager.-vsname "New Portal - 80"
pathYesThe physical path to store the web files in.-path c:\moss\webs\newportal`
loadbalancedurllburlYesThe load balanced URL is the domain name for all sites users will access in this SharePoint web application.-loadbalancedurl http://newportal, -lburl http://newportal
zoneNoThe zone to use. Valid values are: default, intranet, internet, custom, extranet. If omitted defaults to custom. If a value is already in use then the following error will be returned: “An item with the same key has already been added.”-zone intranet
portpNThe port to bind the web application to. If not specified defaults to 80.-port 80, -p 80
hostheaderNThe host header to use.-hostheader newportal
exclusivelyusentlmntlmNSpecifies to exclusively use NTLM authentication instead of Negotiate (Kerberos).-exclusivelyusentlm, -ntlm
allowanonymousanonNSpecifies the default state for anonymous access during virtual server provisioning. The default setting is off, regardless of the current IIS setting. The administrator needs to explicitly turn on anonymous access. IIS anonymous access must be on for pluggable authentication. Anonymous requests must make it through IIS to get to the ASP.NET authentication system. There is no anonymous access choice when provisioning with forms-based authentication. Note: Allowing anonymous access in IIS does not automatically make all Microsoft Office SharePoint Server 2007 sites anonymously accessible. There is Web-level anonymous access control as well, which is also off by default. However, disabling anonymous access in IIS does disable anonymous access to all Office SharePoint Server 2007 sites on the Web application because IIS rejects the request before code even runs.-allowanonymous, -anon
usesslsslNoUse Secure Sockets Layer (SSL). If you choose to use SSL, you must add the certificate on each server using the IIS administration tools. Until this is done, the web application will be inaccessible from this IIS Web Site.-usessl, -ssl

Here’s an example of how to extend an existing web application:

stsadm -o gl-extendwebapp -url http://portal -vsname "New Portal – 80" -path c:\moss\webs\newportal -loadbalancedurl http://newportal -zone custom -port 80 -hostheader newportal

Update 11/5/2008: I fixed an issue where the PreferredInstanceId was not being set. Thanks to Michael (see comments) for pointing out the issue.