Just a short, simple blog for Bob to share his thoughts.
22 August 2011 • by Bob • Ponderings
My middle daughter turned 24 last week. This was a significant occasion by itself, but it was made even more significant because I had just walked her down the aisle only three weeks earlier when she married a great guy from Vancouver, BC.
It seems like only yesterday that I was teaching her how to brush her teeth, how to ride a bicycle, and how to write an English paper that didn't sound like she was talking to one of her friends on the telephone.
Momentous events like these will often motivate you sit back and wonder where the time went. It's been nearly thirty years since I became a "legal adult," but I still don't feel like I'm a "grown up." I still want to believe that my dad is the grown-up and I am just some long-haired kid from Arizona.
But it's easy for me to do the math - in a few short years my oldest daughter will turn thirty, so I must have grown up at some point; I just can't remember when.
03 August 2011 • by Bob • IIS, SSL
In this last appendix for my blog series about using SSL with IIS 6, I'll discuss processing a certificate request by using Windows 2003 Certificate Services. When you are running a certificate server for your network environment, you will need to physically issue the certificates that clients will request from your certificate server. There is a way that you can configure certificate services to automatically issue certificates, but I'd advise against that, unless you are only issuing certificates for testing purposes. If so, then you should read the Set the default action upon receipt of a certificate request topic on Microsoft's TechNet website.
That being said, the procedure to approve and issue a certificate is relatively easy; to do so, use the following steps:
That wraps up the last post in this blog series about using Secure Sockets Layer (SSL) with IIS 6.0, as well as some related information about using Windows 2003 Certificate Services. I hope this information helps administrators that have yet to upgrade to Windows Server 2008 or Windows Server 2008 R2. ;-]
Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/
29 July 2011 • by Bob • IIS, SSL
In this second appendix for my blog series about using SSL with IIS 6, I'm going to discuss obtaining the root certificate from Windows Server 2003 Certificate Services. By way of explanation, obtaining a root certificate is one of the most important steps for servers or clients that will use certificates that you issue. While this step is not necessary on the server where you installed Certificate Services, it is absolutely essential on your other servers or clients, because this step will allow those computers to trust your certificate server as a Certificate Authority (CA). Without that trust in place, you will either receive error messages or SSL simply won't work.
I've broken this process into two steps:
Note: If you were to bring up the properties for the root certificate, the certificate's icon should show an error; this is because the certificate has not been imported.
Before using any certificates that you issue on a computer, you need to install the Root Certificate. (This includes web servers and clients.)
Note: If you were to bring up the properties for the root certificate after you have installed it on your computer, you should see that the icon for the certificate no longer shows an error.
That's it for this post. In my next blog post, I'll discuss processing a certificate request.
Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/
28 July 2011 • by Bob • IIS, SSL
I needed to take a short break from my blog series about using SSL with IIS 6 in order to work on some other projects, but I wanted to finish the series by giving you a few appendices that give you some additional details that you might want to know if you are using SSL with IIS 6.
In this first appendix, I'll discuss how to install Certificate Services for Windows Server 2003. Installing Certificate Services will allow you to have your own Certificate Authority (CA), and thereby you will be able to issue certificates for your organization. It should be noted that Internet clients that are not part of your organization will not inherently trust your certificates - you will need to export your Root CA certificate, which I will describe in a later appendix for this blog series.
There are four different configurations that you can choose from when you are installing Certificate Services:
Enterprise root CA | Integrated with Active Directory Acts as the root CA for your organization |
---|---|
Enterprise subordinate CA | Integrated with Active Directory Child of your organization's root CA |
Stand-alone root CA | Not integrated with Active Directory Acts as the root CA for your certificate chain |
Stand-alone subordinate CA | Not integrated with Active Directory Child of your certificate chain's root CA |
Note: More information about these options is available at http://technet.microsoft.com/en-us/library/cc756989.aspx
For this blog, I will discuss setting up a Stand-alone root CA.
That wraps up this blog post. In my next post I'll discuss obtaining the root certificate for your certificate server so you can install it on a client computer or an IIS server; this will allow other computers to trust the certificates that you issue.
Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/
20 July 2011 • by Bob • Humor
I freely admit that I am a "Dog Person." What's more, I am blessed to have married another dog person - we both love dogs, and this is generally a good thing. My wife grew up surrounded by dogs, as did I.
My wife and I spent the first ten years of our marriage in poverty or in the military, and unfortunately being in the military is a lot like being in poverty. Just the same, we had been married ten years before the two of us were finally able to get a dog. Our first dog was a yellow Labrador Retriever named "Barney." Unfortunately, Barney had been mistreated by a previous owner and we were not able to keep him.
Our next dog was wonderful - we got a Bouvier des Flandres, who became a part of our family for the next eleven years. We named him "Ruff Waldo Emerson," which we shortened to Emerson. I had never owned a herding dog before, and it was a lot of fun to watch the way that he took care of our family: he would patiently wait by the door for the kids to arrive home safely from school, and he would try to push me out of my desk chair when he decided that it was time for me to go to bed.
Our most recent dog was a red-haired Golden Retriever, who our son named "Rook." (Our son, Peter, was heavily into chess at the time.) Rook was a great dog, and I now see why so many people love Golden Retrievers. Sadly, Rook died of a fast-acting bone cancer when he was just eight years old.
All of this is simply an introduction in order to offer proof that I am a dog lover. But that being said, I am decidedly not a "Cat Person." I am allergic to cats, which I think is God's way of saying that man isn't meant to coexist with cats. My daughter has a cat, and her cat seems to like me more than anyone else that comes to visit - which seems to be due to the fact that I ignore it.
Here are several of my thoughts on dogs versus cats:
The debate over which is better – dogs or cats - is ages old, and not likely to ever be resolved. But in my estimation, dogs will always be man's best friend, while cats will remain - at best - frenemies.
30 June 2011 • by Bob • FTP, BlogEngine.NET, Extensibility
I ran into an interesting situation recently with BlogEngine.NET that I thought would make a good blog post.
Here's the background for the environment: I host several blog sites for friends of mine, and they BlogEngine.NET for their blogging engine. From a security perspective this works great for me, because I can give them accounts for blogging that are kept in the XML files for each of their respective blogs that aren't real user accounts on my Windows servers.
The problem that I ran into: BlogEngine.NET has great support for uploading files to your blog, but it doesn't provide a real way to manage the files that have been uploaded. So when one of my friends mentioned that they wanted to update one of their files, I was left in a momentary quandary.
My solution: I realized that I could write a custom FTP provider that would solve all of my needs. For my situation the provider needed to do three things:
Here's why item #3 was so important - my users have no idea about the underlying functionality for their blog, so I didn't want to simply enable FTP publishing for their website and give them access to their ASP.NET files - there's no telling what might happen. Since all of their files are kept in the path ~/App_Data/files, it made sense to have the custom FTP provider return home directories for each of their websites that point to their files instead of the root folders of their websites.
The following items are required to complete the steps in this blog:
Note: I used Visual Studio 2008 when I created my custom provider and wrote the steps that appear in this blog, although since then I have upgraded to Visual Studio 2010, and I have successfully recompiled my provider using that version. In any event, the steps should be similar whether you are using Visual Studio 2008 or Visual Studio 2010.;-]
In this step, you will create a project inVisual Studio 2008for the demo provider.
net stop ftpsvc call "%VS90COMNTOOLS%\vsvars32.bat">nul gacutil.exe /if "$(TargetPath)" net start ftpsvc
net stop ftpsvc call "%VS100COMNTOOLS%\vsvars32.bat">nul gacutil.exe /if "$(TargetPath)" net start ftpsvc
In this step, you will implement the logging extensibility interface for the demo provider.
using System; using System.Collections.Specialized; using System.Collections.Generic; using System.Configuration.Provider; using System.IO; using System.Security.Cryptography; using System.Text; using System.Xml; using System.Xml.XPath; using Microsoft.Web.FtpServer; public class FtpBlogEngineNetAuthentication : BaseProvider, IFtpAuthenticationProvider, IFtpRoleProvider, IFtpHomeDirectoryProvider { // Create strings to store the paths to the XML files that store the user and role data. private string _xmlUsersFileName; private string _xmlRolesFileName; // Create a string to store the FTP home directory path. private string _ftpHomeDirectory; // Create a file system watcher object for change notifications. private FileSystemWatcher _xmlFileWatch; // Create a dictionary to hold user data. private Dictionary<string, XmlUserData> _XmlUserData = new Dictionary<string, XmlUserData>( StringComparer.InvariantCultureIgnoreCase); // Override the Initialize method to retrieve the configuration settings. protected override void Initialize(StringDictionary config) { // Retrieve the paths from the configuration dictionary. _xmlUsersFileName = config[@"xmlUsersFileName"]; _xmlRolesFileName = config[@"xmlRolesFileName"]; _ftpHomeDirectory = config[@"ftpHomeDirectory"]; // Test if the path to the users or roles XML file is empty. if ((string.IsNullOrEmpty(_xmlUsersFileName)) || (string.IsNullOrEmpty(_xmlRolesFileName))) { // Throw an exception if the path is missing or empty. throw new ArgumentException(@"Missing xmlUsersFileName or xmlRolesFileName value in configuration."); } else { // Test if the XML files exist. if ((File.Exists(_xmlUsersFileName) == false) || (File.Exists(_xmlRolesFileName) == false)) { // Throw an exception if the file does not exist. throw new ArgumentException(@"The specified XML file does not exist."); } } try { // Create a file system watcher object for the XML file. _xmlFileWatch = new FileSystemWatcher(); // Specify the folder that contains the XML file to watch. _xmlFileWatch.Path = _xmlUsersFileName.Substring(0, _xmlUsersFileName.LastIndexOf(@"\")); // Filter events based on the XML file name. _xmlFileWatch.Filter = @"*.xml"; // Filter change notifications based on last write time and file size. _xmlFileWatch.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size; // Add the event handler. _xmlFileWatch.Changed += new FileSystemEventHandler(this.XmlFileChanged); // Enable change notification events. _xmlFileWatch.EnableRaisingEvents = true; } catch (Exception ex) { // Raise an exception if an error occurs. throw new ProviderException(ex.Message,ex.InnerException); } } // Define the event handler for changes to the XML files. public void XmlFileChanged(object sender, FileSystemEventArgs e) { // Verify that the changed file is one of the XML data files. if ((e.FullPath.Equals(_xmlUsersFileName, StringComparison.OrdinalIgnoreCase)) || (e.FullPath.Equals(_xmlRolesFileName, StringComparison.OrdinalIgnoreCase))) { // Clear the contents of the existing user dictionary. _XmlUserData.Clear(); // Repopulate the user dictionary. ReadXmlDataStore(); } } // Override the Dispose method to dispose of objects. protected override void Dispose(bool IsDisposing) { if (IsDisposing) { _xmlFileWatch.Dispose(); _XmlUserData.Clear(); } } // Define the AuthenticateUser method. bool IFtpAuthenticationProvider.AuthenticateUser( string sessionId, string siteName, string userName, string userPassword, out string canonicalUserName) { // Define the canonical user name. canonicalUserName = userName; // Validate that the user name and password are not empty. if (String.IsNullOrEmpty(userName) || String.IsNullOrEmpty(userPassword)) { // Return false (authentication failed) if either are empty. return false; } else { try { // Retrieve the user/role data from the XML file. ReadXmlDataStore(); // Create a user object. XmlUserData user = null; // Test if the user name is in the dictionary of users. if (_XmlUserData.TryGetValue(userName, out user)) { // Retrieve a sequence of bytes for the password. var passwordBytes = Encoding.UTF8.GetBytes(userPassword); // Retrieve a SHA256 object. using (HashAlgorithm sha256 = new SHA256Managed()) { // Hash the password. sha256.TransformFinalBlock(passwordBytes, 0, passwordBytes.Length); // Convert the hashed password to a Base64 string. string passwordHash = Convert.ToBase64String(sha256.Hash); // Perform a case-insensitive comparison on the password hashes. if (String.Compare(user.Password, passwordHash, true) == 0) { // Return true (authentication succeeded) if the hashed passwords match. return true; } } } } catch (Exception ex) { // Raise an exception if an error occurs. throw new ProviderException(ex.Message,ex.InnerException); } } // Return false (authentication failed) if authentication fails to this point. return false; } // Define the IsUserInRole method. bool IFtpRoleProvider.IsUserInRole( string sessionId, string siteName, string userName, string userRole) { // Validate that the user and role names are not empty. if (String.IsNullOrEmpty(userName) || String.IsNullOrEmpty(userRole)) { // Return false (role lookup failed) if either are empty. return false; } else { try { // Retrieve the user/role data from the XML file. ReadXmlDataStore(); // Create a user object. XmlUserData user = null; // Test if the user name is in the dictionary of users. if (_XmlUserData.TryGetValue(userName, out user)) { // Search for the role in the list. string roleFound = user.Roles.Find(item => item == userRole); // Return true (role lookup succeeded) if the role lookup was successful. if (!String.IsNullOrEmpty(roleFound)) return true; } } catch (Exception ex) { // Raise an exception if an error occurs. throw new ProviderException(ex.Message,ex.InnerException); } } // Return false (role lookup failed) if role lookup fails to this point. return false; } // Define the GetUserHomeDirectoryData method. public string GetUserHomeDirectoryData(string sessionId, string siteName, string userName) { // Test if the path to the home directory is empty. if (string.IsNullOrEmpty(_ftpHomeDirectory)) { // Throw an exception if the path is missing or empty. throw new ArgumentException(@"Missing ftpHomeDirectory value in configuration."); } // Return the path to the home directory. return _ftpHomeDirectory; } // Retrieve the user/role data from the XML files. private void ReadXmlDataStore() { // Lock the provider while the data is retrieved. lock (this) { try { // Test if the dictionary already has data. if (_XmlUserData.Count == 0) { // Create an XML document object and load the user data XML file XPathDocument xmlUsersDocument = GetXPathDocument(_xmlUsersFileName); // Create a navigator object to navigate through the XML file. XPathNavigator xmlNavigator = xmlUsersDocument.CreateNavigator(); // Loop through the users in the XML file. foreach (XPathNavigator userNode in xmlNavigator.Select("/Users/User")) { // Retrieve a user name. string userName = GetInnerText(userNode, @"UserName"); // Retrieve the user's password. string password = GetInnerText(userNode, @"Password"); // Test if the data is empty. if ((String.IsNullOrEmpty(userName) == false) && (String.IsNullOrEmpty(password) == false)) { // Create a user data class. XmlUserData userData = new XmlUserData(password); // Store the user data in the dictionary. _XmlUserData.Add(userName, userData); } } // Create an XML document object and load the role data XML file XPathDocument xmlRolesDocument = GetXPathDocument(_xmlRolesFileName); // Create a navigator object to navigate through the XML file. xmlNavigator = xmlRolesDocument.CreateNavigator(); // Loop through the roles in the XML file. foreach (XPathNavigator roleNode in xmlNavigator.Select(@"/roles/role")) { // Retrieve a role name. string roleName = GetInnerText(roleNode, @"name"); // Loop through the users for the role. foreach (XPathNavigator userNode in roleNode.Select(@"users/user")) { // Retrieve a user name. string userName = userNode.Value; // Create a user object. XmlUserData user = null; // Test if the user name is in the dictionary of users. if (_XmlUserData.TryGetValue(userName, out user)) { // Add the role name for the user. user.Roles.Add(roleName); } } } } } catch (Exception ex) { // Raise an exception if an error occurs. throw new ProviderException(ex.Message,ex.InnerException); } } } // Retrieve an XPathDocument object from a file path. private static XPathDocument GetXPathDocument(string path) { Exception _ex = null; // Specify number of attempts to create an XPathDocument. for (int i = 0; i < 8; ++i) { try { // Create an XPathDocument object and load the user data XML file XPathDocument xPathDocument = new XPathDocument(path); // Return the XPathDocument if successful. return xPathDocument; } catch (Exception ex) { // Save the exception for later. _ex = ex; // Pause for a brief interval. System.Threading.Thread.Sleep(250); } } // Throw the last exception if the function fails to this point. throw new ProviderException(_ex.Message,_ex.InnerException); } // Retrieve data from an XML element. private static string GetInnerText(XPathNavigator xmlNode, string xmlElement) { string xmlText = string.Empty; try { // Test if the XML element exists. if (xmlNode.SelectSingleNode(xmlElement) != null) { // Retrieve the text in the XML element. xmlText = xmlNode.SelectSingleNode(xmlElement).Value.ToString(); } } catch (Exception ex) { // Raise an exception if an error occurs. throw new ProviderException(ex.Message,ex.InnerException); } // Return the element text. return xmlText; } } // Define the user data class. internal class XmlUserData { // Create a private string to hold a user's password. private string _password = string.Empty; // Create a private string array to hold a user's roles. private List<String> _roles = null; // Define the class constructor requiring a user's password. public XmlUserData(string Password) { this.Password = Password; this.Roles = new List<String>(); } // Define the password property. public string Password { get { return _password; } set { try { _password = value; } catch (Exception ex) { throw new ProviderException(ex.Message,ex.InnerException); } } } // Define the roles property. public List<String> Roles { get { return _roles; } set { try { _roles = value; } catch (Exception ex) { throw new ProviderException(ex.Message,ex.InnerException); } } } }
Note: If you did not use the optional steps to register the assemblies in the GAC, you will need to manually copy the assemblies to your IIS 7 computer and add the assemblies to the GAC using the Gacutil.exe tool. For more information, see the following topic on the Microsoft MSDN Web site:
In this step, you will add the provider to your FTP service. These steps obviously assume that you are using BlogEngine.NET on your Default Web Site, but these steps can be easily amended for any other website where BlogEngine.NET is installed.
cd %SystemRoot%\System32\Inetsrv
appcmd.exe set config -section:system.ftpServer/providerDefinitions /+"[name='FtpBlogEngineNetAuthentication',type='FtpBlogEngineNetAuthentication,FtpBlogEngineNetAuthentication,version=1.0.0.0,Culture=neutral,PublicKeyToken=426f62526f636b73']" /commit:apphost
appcmd.exe set config -section:system.ftpServer/providerDefinitions /+"activation.[name='FtpBlogEngineNetAuthentication']" /commit:apphost
appcmd.exe set config -section:system.ftpServer/providerDefinitions /+"activation.[name='FtpBlogEngineNetAuthentication'].[key='xmlUsersFileName',value='C:\inetpub\wwwroot\App_Data\Users.xml']" /commit:apphost
appcmd.exe set config -section:system.ftpServer/providerDefinitions /+"activation.[name='FtpBlogEngineNetAuthentication'].[key='xmlRolesFileName',value='C:\inetpub\wwwroot\App_Data\Roles.xml']" /commit:apphost
appcmd.exe set config -section:system.ftpServer/providerDefinitions /+"activation.[name='FtpBlogEngineNetAuthentication'].[key='ftpHomeDirectory',value='C:\inetpub\wwwroot\App_Data\files']" /commit:apphost
Just like the steps that I listed earlier, these steps assume that you are using BlogEngine.NET on your Default Web Site, but these steps can be easily amended for any other website where BlogEngine.NET is installed.
At the moment there is no user interface that enables you to add custom home directory providers, so you will have to use the following command line:
cd %SystemRoot%\System32\Inetsrv
appcmd.exe set config -section:system.applicationHost/sites /+"[name='Default Web Site'].ftpServer.customFeatures.providers.[name='FtpBlogEngineNetAuthentication']" /commit:apphost
appcmd.exe set config -section:system.applicationHost/sites /"[name='Default Web Site'].ftpServer.userIsolation.mode:Custom" /commit:apphost
To help improve the performance for authentication requests, the FTP service caches the credentials for successful logins for 15 minutes by default. This means that if you change your passwords, this change may not be reflected for the cache duration. To alleviate this, you can disable credential caching for the FTP service. To do so, use the following steps:
cd /d "%SystemRoot%\System32\Inetsrv" Appcmd.exe set config -section:system.ftpServer/caching /credentialsCache.enabled:"False" /commit:apphost Net stop FTPSVC Net start FTPSVC
Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/
01 June 2011 • by Bob • IIS, Scripting
I had a question recently where someone was trying to add <clear />
or <remove />
elements to a collection in their IIS 7 configuration settings. With that in mind, for today's blog I thought that I would discuss a couple of ways to add <clear />
and <remove />
elements by using two specific scripting methods: AppCmd and VBScript.
It should be noted that you can also use JavaScript or PowerShell, but I'm not covering those because the syntax for those is available elsewhere. (JavaScript syntax is available in the Configuration Editor in IIS Manager, and the PowerShell syntax is available through the Web Server (IIS) Administration Cmdlet Reference.) You can also use Managed-Code, and the syntax for that is also available in the Configuration Editor in IIS Manager; but compiled code isn't scripting, is it? :-)
Here's the scenario, IIS makes it possible to modify the contents of an inherited collection in two ways:
<configuration> <system.webServer> <defaultDocument enabled="true"> <files> <clear /> </files> </defaultDocument> </system.webServer> </configuration>
<configuration> <system.webServer> <defaultDocument enabled="true"> <files> <remove value="index.html" /> </files> </defaultDocument> </system.webServer> </configuration>
With that in mind, let's look at scripting those settings.
AppCmd.exe is a great utility that ships with IIS 7, which allows editing the configuration settings for IIS from a command line. This also allows you to create batch scripts that automate large numbers of configuration changes. For example, the following batch file enables ASP session state, sets the maximum number of ASP sessions to 1000, and then sets the session time-out to 10 minutes for the Default Web Site:
appcmd.exe set config "Default Web Site" -section:system.webServer/asp /session.allowSessionState:"True" /commit:apphost
appcmd.exe set config "Default Web Site" -section:system.webServer/asp /session.max:"1000" /commit:apphost
appcmd.exe set config "Default Web Site" -section:system.webServer/asp
I'm a big fan of IIS 7's AppCmd.exe, but unfortunately it has two rather large limitations:
These limitations have caused me some grief from time to time, because I often want to script the modification of collections, and I would love to remove items or clear a collection.
<clear />
element using AppCmd:Although it's kind of a hack, there is a way to force AppCmd.exe to add a <clear />
element.
Here's what you need to do in order to clear the list of default documents for the Default Web Site:
<?xml version="1.0" encoding="UTF-8"?> <appcmd> <CONFIG CONFIG.SECTION="system.webServer/defaultDocument" path="MACHINE/WEBROOT/APPHOST" overrideMode="Allow" locked="false"> <system.webServer-defaultDocument enabled="true"> <files> <clear /> </files> </system.webServer-defaultDocument> </CONFIG> </appcmd>
appcmd.exe set config /in "Default Web Site" < CLEAR.xml
Unfortunately this technique does not work for <remove />
elements. :-( But that being said, you can add a <remove />
element through VBScript; for more information, see the Using VBScript section.
Fortunately, VBScript doesn't have AppCmd.exe's limitations, so you can add both <clear />
and <remove />
elements.
<clear />
element in VBScript:The following steps will clear the list of default documents for the Default Web Site:
Set adminManager = WScript.CreateObject("Microsoft.ApplicationHost.WritableAdminManager") adminManager.CommitPath = "MACHINE/WEBROOT/APPHOST/Default Web Site" Set defaultDocumentSection = adminManager.GetAdminSection("system.webServer/defaultDocument", _ "MACHINE/WEBROOT/APPHOST/Default Web Site") Set filesCollection = defaultDocumentSection.ChildElements.Item("files").Collection filesCollection.Clear adminManager.CommitChanges
<remove />
element in VBScript:The following steps will remove a single item from the list of default documents for the Default Web Site:
Set adminManager = WScript.CreateObject("Microsoft.ApplicationHost.WritableAdminManager") adminManager.CommitPath = "MACHINE/WEBROOT/APPHOST/Default Web Site" Set defaultDocumentSection = adminManager.GetAdminSection("system.webServer/defaultDocument", _ "MACHINE/WEBROOT/APPHOST/Default Web Site") Set filesCollection = defaultDocumentSection.ChildElements.Item("files").Collection addElementPos = FindElement(filesCollection, "add", Array("value", "index.html")) If (addElementPos = -1) Then WScript.Echo "Element not found!" WScript.Quit End If filesCollection.DeleteElement(addElementPos) adminManager.CommitChanges Function FindElement(collection, elementTagName, valuesToMatch) For i = 0 To CInt(collection.Count) - 1 Set element = collection.Item(i) If element.Name = elementTagName Then matches = True For iVal = 0 To UBound(valuesToMatch) Step 2 Set property = element.GetPropertyByName(valuesToMatch(iVal)) value = property.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
For more information about scripting and IIS configuration settings, see the following:
Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/
31 May 2011 • by Bob • SharePoint
Sometime last year I wanted to set up a SharePoint 2007 website for my family members to exchange information. That being said, I was using a custom membership provider, and I ran into a few issues while I was setting things up. I had kept detailed notes while I was configuring my server and troubleshooting the problems that I encountered, and with that in mind, I thought that I would share my experiences. ;-]
My web server is only an older 32-bit Windows Server 2008 computer, so I couldn't install SharePoint 2010 (which is 64-bit only) and I had to install SharePoint 2007. Taking that into account, there were a few additional considerations that I had for my environment:
With my specific considerations in mind, I took a look at the following article to get started:
That being said, I did not use the following articles, even though they are related to my scenario and they looked interesting:
Here are the brief details on how I created my SharePoint website:
<Users> <User> <UserName>Alice</UserName> <Password>P@ssw0rd</Password> <EMail>alice@contoso.com</EMail> <Roles>Admins</Roles> </User> <User> <UserName>Bob</UserName> <Password>P@ssw0rd</Password> <EMail>bob@contoso.com</EMail> <Roles>Authors</Roles> </User> </Users>
There are a few additions that you have to make to your website's web.config file, as well as the SharePoint Central Administration web.config file for SharePoint 2007:
<system.web>
section of your website's web.config; in my example that file would be located at "C:\Inetpub\SharePointSite\wwwroot\web.config":
<!-- added on 05/31/2011 --> <membership defaultProvider="ReadOnlyXmlMembershipProvider"> <providers> <add name="ReadOnlyXmlMembershipProvider" type="ReadOnlyXmlMembershipProvider, ReadOnlyXmlMembershipProvider, Version=1.0.0.0, Culture=neutral, PublicKeyToken=426f62526f636b73" description="Read-only XML membership provider" xmlFileName="~/App_Data/MyUsers.xml" /> </providers> </membership> <roleManager enabled="true" defaultProvider="ReadOnlyXmlRoleProvider"> <providers> <add name="ReadOnlyXmlRoleProvider" type="ReadOnlyXmlRoleProvider, ReadOnlyXmlRoleProvider, Version=1.0.0.0, Culture=neutral, PublicKeyToken=426f62526f636b73" description="Read-only XML role provider" xmlFileName="~/App_Data/MyUsers.xml" /> </providers> </roleManager> <!--/added on 05/31/2011 -->
<system.web>
section of your SharePoint Central Administration web.config file; on my server that file is located at "C:\inetpub\wwwroot\wss\VirtualDirectories\6087\web.config":
<!-- added on 05/31/2011 --> <membership defaultProvider="ReadOnlyXmlMembershipProvider"> <providers> <add name="ReadOnlyXmlMembershipProvider" type="ReadOnlyXmlMembershipProvider, ReadOnlyXmlMembershipProvider, Version=1.0.0.0, Culture=neutral, PublicKeyToken=426f62526f636b73" description="Read-only XML membership provider" xmlFileName="~/App_Data/MyUsers.xml" /> </providers> </membership> <!--/added on 05/31/2011 -->
SharePoint Central Administration needs to be able to find the MyUsers.xml file, so I created an App_Data folder under physical path of the SharePoint Central Administration website, and I added a symbolic link in that folder that pointed to the physical MyUsers.xml file. Here's how I did that:
cd C:\inetpub\wwwroot\wss\VirtualDirectories\6087
mklink MyUsers.xml C:\Inetpub\SharePointSite\wwwroot\App_Data\MyUsers.xml
Note: I could have copied the XML file, but I preferred to use the symbolic link instead of having to manage two copies of the file.
If you were installing a membership provider that can perform lookups, you could add an additional entry to your SharePoint Central Administration web.config file:
<PeoplePickerWildcards> <clear /> <add key="AspNetSqlMembershipProvider" value="%" /> <!-- added on 05/31/2011 --> <add key="ReadOnlyXmlMembershipProvider" value="%" /> <!--/added on 05/31/2011 --> </PeoplePickerWildcards>
Okay - I admit that I everything that I did so far was probably making things harder that they needed to be, but I love a good challenge. ;-]
That being said, I ran into some problems that I thought would be worth mentioning, just in case someone else ran into them.
When browsing to my SharePoint website, I received several HTTP 403 errors. I used Process Monitor to troubleshoot the problem, and I discovered that IUSR could not access the "bin" folder in my website. (I'm still not quite sure why it was trying.) To resolve these errors, I used the following steps:
cd C:\Inetpub\SharePointSite\wwwroot
icacls bin /grant IIS_IUSRS:r
In my situation the problem was for IUSR, but if you are using a different anonymous identity or your application pool is running as a unique identity then it might be a different user. In any case, Process Monitor will let you know who needs permissions.
Later I discovered the following blog post by John Powell:
In that blog, John suggests adding the following permissions for the "bin" folder:
icacls bin /grant users:r
I'm not sure if that's necessary, but it's worth pointing out.
When browsing to my SharePoint website, I received several HTTP 404.8 errors. Those errors mean that the built-in IIS 7 Request Filtering feature was blocking something, so I did the following:
</configuration>
tag:
<system.webServer> <!-- added on 05/31/2011 --> <security> <requestFiltering> <hiddenSegments> <clear /> <add segment="web.config" /> <add segment="bin" /> <add segment="App_code" /> <add segment="App_GlobalResources" /> <add segment="App_LocalResources" /> <add segment="App_WebReferences" /> <add segment="App_Data" /> <add segment="App_Browsers" /> </hiddenSegments> </requestFiltering> </security> <!--/added on 05/31/2011 --> </system.webServer>
Note: This removes all of the hidden segments from the global IIS 7 Request Filtering settings, which may be overkill. I have a lot of custom global request filtering settings, and I didn't want to go through each individual setting to see which setting was blocking files that I needed, so I used settings for my website that cleared the inherited request filtering settings and added the default settings.
When browsing to my SharePoint website, the information bar in Internet Explorer kept prompting me with the following message:
The Web site wants to run the following add-on: 'Name ActiveX Control' from 'Microsoft Corporation'. If you trust the Web site and the add-on and want to allow it to run, click here...
This message was highly frustrating, so I did some digging around the Internet and discovered that I could hack the INIT.JS file for SharePoint to suppress this message. Here's how I did that:
ProcessImn()
and ProcessImnMarkers()
functions, and I remarked out the contents. Here's what this looked like when I was done:
function ProcessImn() { // if (EnsureIMNControl() && IMNControlObj.PresenceEnabled) // { // imnElems=document.getElementsByName("imnmark"); // imnElemsCount=imnElems.length; // ProcessImnMarkers(); // } } function ProcessImnMarkers() { // for (i=0;i<imnMarkerBatchSize;++i) // { // if (imnCount==imnElemsCount) // return; // IMNRC(imnElems[imnCount].sip,imnElems[imnCount]); // imnCount++; // } // setTimeout("ProcessImnMarkers()",imnMarkerBatchDelay); }
I should note that this solution is unsupported; and a few months I hacked my INIT.JS file, Microsoft published the following Knowledge Base article with a couple of different methods:
That being said, I like my solution better. ;-]
Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/
31 May 2011 • by Bob • IIS, Scripting, SharePoint
I ran into an interesting situation recently - I host a website for a friend of mine, and he was shopping around for a new website template. He found one that he liked, but he didn't like the colors. In fact, he wanted the exact opposite of the colors in the website template, so he asked what I could do about it.
I looked at the website template, and thankfully it was using linked Cascading Style Sheets (CSS) files for all of the color definitions, so I told him that changing the colors would probably be a pretty easy thing to do. However, once I cracked open the CSS files from the website template, I found that they had hundreds of color definitions. Changing every color definition by hand would have taken hours, so I decided to write some Windows Script Host (WSH) code to do the work for me.
With the above scenario in mind, here's the script that I wrote to negatize every color in a CSS file - all that you need to do is replace the paths to the input and output files and run the script to create the negatized version of the input CSS file.
Option Explicit Const strInputFile = "c:\inetpub\wwwroot\style-dark.css" Const strOutputFile = "c:\inetpub\wwwroot\style-light.css" ' ------------------------------------------------------------ Dim objFSO Dim objInputFile Dim objOutputFile Dim strInputLine Dim strLeft, strMid, strRight, strArray Dim blnFound ' ------------------------------------------------------------ Const strTempRGB = "[|[TMPRGBSTR1NG]|]" Const strTempHEX = "[|[TMPHEXSTR1NG]|]" ' ------------------------------------------------------------ Set objFSO = CreateObject("scripting.filesystemobject") Set objInputFile = objFSO.OpenTextFile(strInputFile) Set objOutputFile = objFSO.CreateTextFile(strOutputFile) Do While Not objInputFile.AtEndOfStream strInputLine = objInputFile.ReadLine blnFound = True Do While blnFound If InStr(1,strInputLine,"rgb(",vbTextCompare) Then strLeft = Left(strInputLine,InStr(1,strInputLine,"rgb(",vbTextCompare)-1) strMid = Mid(strInputLine,InStr(1,strInputLine,"rgb(",vbTextCompare)+4) strRight = Mid(strMid,InStr(strMid,")")+1) strMid = Left(strMid,InStr(strMid,")")-1) strArray = Split(strMid,",") strMid = InvertOctet(CInt(strArray(0))) & _ "," & InvertOctet(CInt(strArray(1))) & _ "," & InvertOctet(CInt(strArray(2))) strInputLine = strLeft & strTempRGB & "(" & strMid & ")" & strRight Else blnFound = False End If Loop strInputLine = Replace(strInputLine,strTempRGB,"rgb") blnFound = True Do While blnFound If InStr(strInputLine,"#") Then strLeft = Left(strInputLine,InStr(strInputLine,"#")-1) strMid = Mid(strInputLine,InStr(strInputLine,"#")+1) If Len(strMid)>6 Then strRight = Mid(strMid,7) strMid = Left(strMid,6) ElseIf Len(strMid)>3 Then strRight = Mid(strMid,4) strMid = Left(strMid,3) Else strRight = "" End If If IsHexString(strMid) Then If Len(strMid) = 6 Then strMid = Right("0" & Hex(InvertOctet(CInt("&h" & Left(strMid,2)))),2) & _ Right("0" & Hex(InvertOctet(CInt("&h" & Mid(strMid,3,2)))),2) & _ Right("0" & Hex(InvertOctet(CInt("&h" & Right(strMid,2)))),2) Else strMid = Hex(InvertByte(CInt("&h" & Left(strMid,2)))) & _ Hex(InvertByte(CInt("&h" & Mid(strMid,3,2)))) & _ Hex(InvertByte(CInt("&h" & Right(strMid,2)))) End If End If strInputLine = strLeft & strTempHEX & strMid & strRight Else blnFound = False End If Loop strInputLine = Replace(strInputLine,strTempHEX,"#") objOutputFile.WriteLine strInputLine Loop ' ------------------------------------------------------------ Function IsHexString(ByVal tmpString) Dim blnHexString, intHexCount, intHexByte blnHexString = True If Len(tmpString)<>3 and Len(tmpString)<>6 Then blnHexString = False Else tmpString = UCase(tmpString) For intHexCount = 1 To Len(tmpString) intHexByte = Asc(Mid(tmpString,intHexCount,1)) If (intHexByte < 48 Or intHexByte > 57) And (intHexByte < 65 Or intHexByte > 70) Then blnHexString = False End If Next End If IsHexString = blnHexString End Function ' ------------------------------------------------------------ Function InvertByte(ByVal tmpByte) tmpByte = tmpByte And 15 tmpByte = 15 - tmpByte InvertByte = tmpByte End Function ' ------------------------------------------------------------ Function InvertOctet(ByVal tmpOctet) tmpOctet = tmpOctet And 255 tmpOctet = 255 - tmpOctet InvertOctet = tmpOctet End Function ' ------------------------------------------------------------
After I wrote the above script, I found myself using it for a bunch of different websites that I manage for other people. One of the websites that I host is based on SharePoint 2007, so I wondered how difficult it would be negatize a SharePoint 2007 theme. As it turns out, it's pretty easy. The following steps will walk you through the steps that are required to create a negatized version of the built-in "Classic" SharePoint 2007 theme.
(NOTE: The steps in this section do not work with SharePoint 2010 or office 14; SharePoint 2010 and Office 14 store their themes in a different format, so these steps will not work.)
<
SPThemes
>
collection:
<Templates> <TemplateID>classicnegative</TemplateID> <DisplayName>Classic Negative</DisplayName> <Description>Classic Negative</Description> <Thumbnail>images/thclassic.gif</Thumbnail> <Preview>images/thclassic.gif</Preview> </Templates>
That's all it takes to negate the colors that are defined in the CSS files for a SharePoint 2007 theme. (NOTE: This does not modify the colors of the images in the SharePoint theme; you will need a graphics program to update the colors in the images.)
Before I receive any comments, I am perfectly aware that "negatize" is not an actual word in the English language, but it seemed appropriate, and new words have to start somewhere.
Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/
04 May 2011 • by Bob • Office
A friend of mine just sent me the following news article, along with the subtitle, "Can we just let this die, geez..."
Novell Antitrust Lawsuit Against Microsoft Revived by Court
Bloomberg Businessweek - May 03, 2011
By Tom Schoenberg
Personally, I find articles like this depressing - not just because they are frivolous lawsuits that do little more than wasting millions of dollars for everyone concerned, but because they send the wrong messages to the business world. Let me explain:
I love quotes that are worded like this: "WordPerfect's share of the word-processing market fell to less than 10 percent in 1996 from almost 50 percent in 1990." This statement is an excerpt from a section in that article which suggests that Microsoft is the bad guy in this situation.
Has anyone ever bothered to consider that whatever happened to WordPerfect occurred because the executive leadership at WordPerfect made a plethora of poor business choices and their applications ceased to be good products? This entire lawsuit reminds me of when Metallica sued Napster over the decline in their album sales - did it ever occur to them (Metallica) that maybe they had passed their prime and perhaps no one wanted to buy their albums anymore?
Here's another question: did anyone else actually try to use WordPerfect for Windows 3.x through Windows 98? Well, I did - because back in my DOS days I was an avid WordPerfect 4.x through 6.x user. So take my word for it, every version of WordPerfect starting from 5.x through 8.x on Windows platforms were simply awful, while at the same time the versions of Word for Windows got better and better.
I can give you several reasons behind this dichotomy, but the primary cause is simple - WordPerfect didn't have a clue how to make a Windows-based product. As the world transitioned from a DOS-based environment to a Windows realm, WordPerfect shipped products that were technologically inferior, way behind schedule, and badly engineered. By the time that the folks at WordPerfect quit wasting money and figured out what they were doing, it was way too late - they owned less than 10% of the market, and the damage was irrevocable.
Here's just one example: instead of leveraging Windows' built-in printing capabilities and investing in better application features and functionality, the people at WordPerfect continued to develop and ship their own printing subsystem, which bypassed the built-in Windows printing features. Even if WordPerfect's alternate printing subsystem had been better, (which I can honestly say from personal experience that it was not), that's not the way that you're supposed to do things in a Windows world, and WordPerfect threw away millions of dollars and countless thousands of man hours on this colossal failure.
Here's another oldie but goodie - WordPerfect bought into the fantasy from the now-defunct Sun Microsystems that Java was the up-and-coming, be-all/end-all of computer languages and the dawn of write-once/run-everywhere software. This was a wonderful theory, and I personally spent some time writing simple applications in Java back in the mid-to-late-1990s because I, too, bought into Sun's hype. (I still wear a Java baseball cap that I got from Sun back in 1996.) But it wasn't long before I, like many others, realized that Java was mostly hype, and writing software in Java was an experience that was more like rewrite-often/debug-everywhere. But I realized my mistake before I had wasted over $400 million on a failed word processing application in Java like WordPerfect did.
But the folks at WordPerfect continued to press on in their self-delusions - all the while falling behind Word, which was now integrating wonderfully with Windows, Microsoft Office, and a host of other applications through technologies like DDE, OLE, and ActiveX. By this point WordPerfect's losses were enormous, then along came Novell, who was already a sinking ship; this was due to the fact that their difficult-to-use and expensive flagship NetWare operating system was taking a serious beating from Windows NT's ease-of-use and significantly reduced barrier-to-entry pricing.
Novell realized that WordPerfect had once been a major cash cow, and I guess they hoped that they could turn around both of these massive sinking ships and get them headed back from the Red Sea into the Black Sea. But Novell's delusions proved to be worse than WordPerfect's, and eventually Novell had to sell WordPerfect to Corel for a pittance just to keep their ship from being dragged under as WordPerfect rocketed toward the bottom in a technology fate that was worse than the demise of the Titanic. And yet, very much like the sinking of the Titanic and the untimely deaths of technology giants like Netscape and Sun Microsystems, WordPerfect's downfall was ultimately caused by a series of gargantuan blunders and the terminal hubris of their leadership, and not by any action on Microsoft's part.
Not that any of this will matter in court - Microsoft will probably still have to shell out a few hundred million dollars in "damages," thereby rewarding former executives at WordPerfect for their incompetence, and reinforcing the message to the business world that just because you're a colossal failure and you ruined the lives of thousands of your loyal employees, that doesn't mean that you shouldn't be able to buy a large mansion and luxury yacht by cashing in on the profits of your successful competitors.
At the time of this writing, Wikipedia has a great write-up on the history of WordPerfect, including blunt analysis of WordPerfect's many failures. But pages on Wikipedia are subject to change, and they're not always accurate.
With that in mind, you might want to take a look at the book titled Almost Perfect by W. E. Peterson, who had been one of the senior executives at WordPerfect. Sometimes it's nice to have an insider's view of the breakdown and failure.