MOSS MVP

I've moved my blog to http://blog.falchionconsulting.com!. Please update your links. This blog is no longer in use--you can find all posts and comments at my new blog; I will no longer be posting to this site and comments have been disabled.

Monday, March 31, 2008

Extend Web Application

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:

   1: using System;
   2: using System.DirectoryServices;
   3: using System.Globalization;
   4: using System.IO;
   5: using System.Reflection;
   6: using System.Text;
   7: using Lapointe.SharePoint.STSADM.Commands.OperationHelpers;
   8: using Lapointe.SharePoint.STSADM.Commands.SPValidators;
   9: using Microsoft.SharePoint;
  10: using Microsoft.SharePoint.Administration;
  11:  
  12: namespace 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 Name Availability Build Date
gl-extendwebapp WSS v3, MOSS 2007 Released: 3/31/2008
Updated: 11/5/2008

Parameter Name Short Form Required Description Example Usage
url   Yes The URL of the existing web application to extend. -url http://portal
vsname   Yes The virtual server name to use - this is the name that will appear in the IIS manager. -vsname "New Portal - 80"
path   Yes The physical path to store the web files in. -path c:\moss\webs\newportal
loadbalancedurl lburl Yes The load balanced URL is the domain name for all sites users will access in this SharePoint web application. -loadbalancedurl http://newportal

-lburl http://newportal
zone   No The 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
port p N The port to bind the web application to.  If not specified defaults to 80. -port 80

-p 80
hostheader   N The host header to use. -hostheader newportal
exclusivelyusentlm ntlm N Specifies to exclusively use NTLM authentication instead of Negotiate (Kerberos). -exclusivelyusentlm

-ntlm
allowanonymous anon N

Specifies 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.

NoteNote:

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
usessl ssl No Use 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.