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