Some Thoughts About My 20th Anniversary At Microsoft

At the risk of gratuitous self-aggrandizement - today is my actual 20th anniversary. My 20th anniversary crystal arrived the other day, so I added to the collection on a bookcase in my office. (Although to be honest, they didn't hand out anniversary crystals when I reached my 5th and 10th anniversaries, so I had to buy those after the fact.)

20thAnniversaryCrystal

Things I'm Most-Proud Of...

Disclaimer: Yeah, I realize that I'm definitely bragging here; hence why I labeled this section "Things I'm Most-Proud Of."

  • IIS Express - I came up with the idea for IIS Express one afternoon after writing an SDK sample where I showed that it was possible to create a mini version of IIS that could be used for self-hosting applications and debugging. After I proved that it could work, I faced a huge uphill battle trying to sell the idea for a miniature version of IIS to my bosses, (e.g. ThomasD, VijayS, Mai-lanB, BillS, etc.) Once I had convinced all of them, then I had to convince the Visual Studio team to dump Cassini in favor of IIS Express. (That was no small task, believe me.) Shortly after I had everyone on board, Vijay took over the project while I changed teams for the next year, and Vijay grew my rather simple idea into a full-fledged application. (Kudos to Vijay - I had more modest plans originally.) A few years later I changed teams again and I took over IIS Express, where I added some more cool features to it. Now IIS Express ships everywhere, so it's pretty cool to see how far you can take an SDK sample. Smile
  • Being a PM on the IIS team - This was a dream come true for me. Oh sure, that dream was more of a nightmare in the days before shipping a product, but being a PM for IIS was pretty much everything I thought it would be. By way of nostalgia, here's what the IIS Product team looked like in 2009. (Note: I took the picture, so I'm not in the picture. I prefer it that way.)
    IISTEAM2009
  • RFC 7151 - It took me eight years to get this Internet Standard published, and I wrote most of it as part of my IIS PM work. As I have mentioned before, I get to honestly say that I helped create part of the Internet. (It's a very small part of the Internet, but I still get bragging rights.)
  • Technical Writing - Between my ten years in product support and my years as a writer in this organization, I have written thousands of how-to articles, samples, walkthroughs, blogs, reference topics, and KB articles. It has never ceased to amaze me how much of what I have written has been used by customers; I love seeing code that I wrote in an article show up in customers' blogs or forum posts. It's especially amusing to me when they're passing it off as their code even though it's 100% copied from my samples. (Hey, I always can tell.) If I combined every article that I have written during my tenure at Microsoft, I could create a whole series of books. (Although that would be a really boring books series.)
  • FTP Service for IIS 7 & IIS 8 - There's lots of good stuff in there; our feature team discarded the old FTP service (which no one wanted to use) and we created a first-class service from scratch, for which we received a lot of positive feedback.
  • Being a Customer Advocate - I started in technical support, where I spent 10 years speaking with customers, and I have always hosted my own websites and run my own servers so that I could face many of their same frustrations. For instance, I run a full Windows domain in my house, where family members have individual accounts, and all Internet access goes through my proxy server, and I could use group policy to set restrictions for my kids when they were younger, etc. By using Microsoft's products as a customer, I made countless discoveries from a customer-point-of-view. But that being said, some of the people with whom I have had to work still don't understand their customers; I ran into that situation a lot when I was a PM. For example, when you make something easier for you that is harder for the customer, you have just failed your customer. (Sigh.Sad smile)
  • WebDAV for IIS 7 & IIS 8 - I loved this technology and I was a huge advocate for it. I hated the old version of WebDAV on IIS 6 and earlier, so when I was a PM on the IIS team I helped create a version from scratch that customers would like. Most people don't realize that Windows ships both a WebDAV service and WebDAV client, so you can map dives and share files over HTTP thanks to WebDAV.
  • MetaEdit - This utility was like Regedit for IIS settings and shipped with the Windows Resource Kits. This was a very popular tool in its day, and it was cool to see classes being taught at ATECs for something that I had created. Eventually MetaEdit was replaced by another tool, and IIS 7 made both tools obsolete, but in its day MetaEdit reigned supreme.
  • FrontPage Server Extensions (FPSE) - Customers both loved and hated FPSE, and I knew FPSE inside and out from my days in technical support; for years I was arguably the most-knowledgeable person about FPSE the company. (No exaggeration.) So when I became a PM for IIS, I was able to get them to work in all sorts of scenarios where they weren't designed to work, which helped to create new versions of FPSE that resolved thousands of customer problems for Visual Studio and other Enterprise-level customers. (Seriously - the lack of FPSE was an adoption blocker for thousands of Windows Server 2008 customers.) Then a few years later I got to be the PM who finally killed FPSE as far as Microsoft was concerned. (Or as one customer gratefully remarked as he reached to shake my hand, "So you're the guy who's driving the stake through the heart of FPSE!")

Embarrassing Microsoft Moments...

Here is a collection of amusing stories with which I have been involved over the years:

  • Due to a simple programming error, (which wasn't my error - I swear), one of my servers managed to register "localhost" in the company's name servers, and it started responding to all TCP/IP requests for "localhost" across the entire network. Within a few hours, this mishap had taken down several mission-critical services across the company, so my office was kicked off the network and I received a call from a corporate president asking me to never turn my server back on. (Note - I found the error and fixed it. But I swear it was in someone else's code. Really. Why would I lie about something like that? Smile)
  • I mentioned MetaEdit earlier, and one of my greatest embarrassments was when I famously messed up the version checking dialog box.
  • When I designed the extensibility APIs for the IIS 7 FTP service, I realized later that should have added an extra parameter to one of the method prototypes. I couldn't get approval to change the method in subsequent versions of the FTP service, so I have to live with that for the rest of my life. (Darn. Darn. Darn.)
  • I didn't cause this error, but I helped diagnose it back in my technical support days: Windows has several built-in reserved filenames that all go back to the DOS days, such as PRN, CON, NUL, LPT1, COM2, etc. Back in the earliest days of IIS, most companies only had one server for everything - domain, web server, proxy server, file shares, print sharing, etc. So you could connect to the FTP service and start uploading a text file filled with nothing but page feed commands while using a destination filename of LPT1; if the company had a printer attached to their server, it would spit out all the paper and then pop up a dialog message asking the user to reload the printer. However, this was a modal dialog box, so IIS would hang until the user had completed the task. We called this the "Denial of Paper" attack. Smile

Weird Microsoft Traditions...

We have done some silly things over the years at Microsoft; some of these traditions are still in practice, while others are long-gone. For example:

  • Togas - Believe it or not, employees used to wear togas to work whenever our stock split. (Yeah, it's been years since that happened. Sad smile)
  • "Fire Me" Emails - Locking one's workstation used to be policed by the community, so if you found someone's desktop computer unlocked, you were supposed to send an email to the team that contained the words "FIRE MY LAME @$$!!!" (Eventually this practice was heavily frowned upon.)
  • April Fool's Websites - Okay, this is mostly just me, but over the years I have created several internal-only websites for April Fool's Day - for example:
    • People for the Ethical Treatment of Software (PETS) - this was a take off on PETA that was advocating for the abolition of the cruel conditions under which software is tested
    • Buugle - It looked and acted like Google, but it wasn't...
    • Technical Support Trading Cards - this website looked like baseball cards, but listed off all the stats for technical support employees, (e.g. SDFC = "Solution Delivered First Contact," MPI = "Minutes Per Incident," etc.)
    • Virtual Tech Lead - I created Virtual Tech Lead back when I was a Tech Lead in technical support, and I populated it with a bunch of innocuous statements that most Tech Leads often said. I sent it out to my team on April Fool's Day with the caveat that it was "90% as effective as a real Tech Lead," and it took off across the company's tech support divisions like wildfire. Pretty soon my poor internal-only webserver couldn't keep up with the web traffic so it rolled over and died; I had caused a denial-of-service on myself.
  • Office Pranks - We used to trash people's offices when they went on vacation. I realize that open space offices have put a dent in certain gags like filling an office with balloons, but how hard is it to wrap someone's desk in saran wrap? Smile
  • Anniversary Goodies - Usually people bring in one pound of M&M's for each year on their anniversary. Once I hit my 10-year anniversary I decided that 10 (or more) pounds of M&M's were simply too many for any team to eat, so I started bringing in rum cakes from the Caribbean. Since I work remotely I don't do that now, but my wife surprised me with 20 rum cakes on my desk this morning:
    20thAnniversaryRumCakes

A Few of the Hardest Things for Me at 20 Years...

  • The Demise of IIS - It has been difficult for me to watch IIS slowly get killed off as a product in an effort to encourage customers to host their websites on Azure. I started working with IIS in early 1996, so it's sad for me to see a product wither away and perish ignominiously after I have I spent thousands of hours designing, developing, and documenting it. But time and tide wait for no man, so the wheels of progress soldier on. However, if you want to see what IIS looked like way back in 1996, see http://www.geekybob.com/iis-legacy-screenshots/.
  • Kenny King - I was hired at Microsoft on the same day as Kenny; we were hired within the same hour, to be exact. We quickly became friends and we worked together for years; we bought houses down the street from each other in Texas where our kids grew up together and our wives were like sisters. Kenny was always a rock star with everything he did at Microsoft, and he should be here celebrating his 20th anniversary along with me, but sadly he passed away from cancer shortly before our 19th anniversary with the company.

A Few Manager Anecdotes...

I worked with some great first-level managers at Microsoft - too many to recall - and I worked with some great skip-level managers, too. That being said, here is a list with some of my all-time favorite first-level managers (many of whom are no longer with the company): NancyL, SibhonO, MicheleP, JayV, StaceyC, TerryM, SharonM, BillS, Mai-lanB, EricD, ThomasD, AndrewL, TomW, WonY, and of course - WadeP.

Here are a few small managerial anecdotes - I don't have stories about everyone, so anyone left off the list is not meant as a slight:

  • NancyL was my first manager at Microsoft, and she was on my interview loop when I was originally hired in Arizona. There had been 8,000 job applicants for 100 jobs, so after I was hired I naively asked Nancy why she wanted to hire me when there were so many great people to choose from, to which she replied, "You scared me; I was afraid that someone else would hire you and we would have to compete against you."
  • JayV was one of the best "Big Picture" managers with whom I ever worked; Jay could take a scenario and see how it applied to the company at every level. Yet Jay never lost touch with his individual team members; he would always grab two or three team members and head out to lunch where we'd talk about how things were going. Whenever I have been in a position of leadership during my time at Microsoft, I have actively tried to emulate several of Jay's great qualities.
  • StaceyC was one of my all-time favorite managers; I worked for Stacey on two different teams for a total of six years. Stacey was one of the best "people-persons" with whom I have worked. Even in the hardest of time she had a great attitude, and I watched her successfully manage several teams through some extremely traumatic times.
  • EricD had not had many direct reports when he became my manager, so he called me into his office one day and said, "Look, I'm the guy who makes sure that IIS ships, so I'm usually pretty difficult to work with. So I want you to be my guinea pig - I want to try not to be jerk for you." Despite the ominous sound of that proposal, Eric turned out to be a great manager.
  • JeffH was a really good guy who was in a really difficult position when I worked for him in Technical Support; I had pitched an idea for creating a specialty team of escalation engineers to several managers, and they liked my idea. However, once the team was created my original idea was quickly transformed into something that was entirely different than what I had proposed, so I no longer wanted to work on the team that I had just helped to create. Nevertheless, Jeff really needed the headcount, so he asked me to please stay on the team for six months in order to help get the team started - so I reluctantly agreed. In the end I worked on the team for a year and a half; I hated every minute of it, but I stayed because of Jeff. That was some good leadership on his part.
  • Mai-lanB doubled as my mentor at times; she gave me a lot of great advice on how to manage my career as a PM during our 1:1 meetings, which were often over lunch somewhere off campus. However, Mai-lan's primary weakness was that she was a workaholic like me, so she worried some of her direct reports because she would email them at any time during the day or night with a question. Many people thought they had to have an answer right then, so a lot of her reports spent some restless nights until they figured out that it was okay to answer the next day. But that being said, I also had an unpredictable schedule, so Mai-lan was amazed that I seemed to answer all of her emails 24 hours a day. She eventually asked if I ever slept, to which I replied, "Only when you're on vacation." (Perhaps it takes a workaholic to understand a workaholic.Smile) When Mai-lan's position was unexpectedly cut from the IIS team, she and I had found out immediately before we were scheduled to attend a team meeting, and when I sat next to her, I risked an HR violation by hugging her and saying, "I'm so sorry - I really liked working for you, and you will be sorely missed." Mai-lan simply cried as a response. Sad smile
  • I reported to WonY when I was an IIS PM, and he sat down the hall from me. Won had a laugh that was infectious; you could always tell whenever he had a meeting with someone because sooner or later something would make him laugh, and you could hear it everywhere in the building.
  • It would seem inappropriate to expound upon WadeP's great qualities since I currently report to him and we have a review meeting coming up soon. Smile So I'll just say that I have probably worked for Wade longer than any other manager, and the same can be said about most of Wade's reports. Any manager with a team of faithful, long-term employees and a zero attrition rate must be doing something right.

Windows Versions during my Tenure...

I thought it would be fun to take a look back at all the version of Windows that have existed during my time at Microsoft, with a few of my thoughts about each version. (Note: you should also see Windows turns 30: a visual history, which is a pretty amusing look back at the history of Windows.)

  • Windows 3.1 - Believe it or not, this OS was still widely-used when I was hired, and I had to take tech support calls for it. (How many of you ever knew that there was a 32-bit version of Win31? I did - and I talked customers through getting it up and working.) Win31 wasn't bad, and I knew all sorts of really cool tweaks for customizing ProgMan that were soon to be obsolete. But in its day it was great.
  • Windows NT 3.51 - This was the only viable Windows client and server product at the time that I was hired. Before I started at Microsoft, I was working in the IT department of a small college, and we had used WinNT351 for one of our servers. After I was hired by Microsoft I had to take tech support calls for WinNT351, which were an adventure. In terms of productivity, WinNT351 beat the crap out of Novell, which was far-too-cumbersome for most people, and that is why WinNT eventually took over the server market from Novell.
  • Windows 95 - This was released shortly before I was hired, and Win95 quickly dominated the home PC market. That said, I had a love/hate relationship with Win95; some things were great, others weren't quite where they needed to be. But the built-in networking was great, and it eventually supplanted every other PC-based peer-to-peer networking technology. (Anyone remember LANtastic? ARCnet? Of course not.)
  • Windows NT 4.0 - This was released shortly after I was hired - it was a great OS, and it had lots of stability (unlike its predecessor); both the client and server versions were fantastic. I earned my first MCSE certification on this OS.
  • Windows 98 - I loved this OS, and believe it or not I still run the Win98 core OS (non-GUI) on a legacy machine where I need pure DOS-level functionality for some old MIDI stuff that needs unrestricted physical access to drive hardware.
  • Windows NT Option Pack - This wasn't really a separate OS, but NTOP added amazing Internet-hosting features to Windows NT 4.0.
  • Windows ME - A lot of people hated this OS; I only put it on one computer, and I didn't have any problems with it, so I never understood what the griping was all about.
  • Windows 2000 - This was a good OS, both the client and server versions. That said, I never liked using Win2000 Pro for a desktop machine because it had crappy support for audio/video/MIDI processing, which was an intentional decision from the Windows product team. (Their argument was, "This is a business machine; watch videos and create music on your home PC." Of course, that decision alienated anyone who wanted to use Win2000 Pro as the OS on their home PC. It also helped Apple to take over the recording industry. I earned my second MCSE on this OS.
  • Windows XP - As everyone is well-aware, WinXP was a fantastic product; in many ways it was too amazing for its own good so millions of people still refuse to upgrade from it. WinXP was clearly one of the greatest operating systems ever created, although there is some funny irony here: Microsoft received a lot of bad press about WinXP when it was first released - some people didn't like having to upgrade their systems for a few of the new product requirements. (Of course, Apple users had always forced to do that for years.) In any event, when Vista and Win7 came along a few years later, they also received bad press from people who didn't like having to upgrade, which made me start ignoring early reviews of operating systems.
  • Windows Server 2003/R2 - This OS had some amazing security features, and certainly the best "Old School" version of IIS.
  • Windows Vista - This wasn't a bad OS, although it got lots of bad press because it required more video/memory resources than were typical for many legacy systems, so millions of users were forced to upgrade their systems if they wanted to use it. It never worked correctly on my wife's laptop, which was cause for many unhappy conversations in our house. (e.g. My Wife: "Why did you upgrade my computer to this OS? I hate Vista!")
  • Windows Server 2008/R2 - These were great server operating systems; lots of killer features, and amazing versions of a completely-redesigned IIS.
  • Windows 7 - This is still my favorite OS of all time; it was so amazing that during the beta time frame I reformatted every machine that I had (including my home PCs) and installed the Win7 beta. It was pretty, it was fast, and it did everything right (or so it seemed). Microsoft hit an amazing home run with this OS. One of the least-actualized features of Windows 7 was Windows Media Center (WMC), which should have taken off and dominated the home theater and HTPC markets, but whoever was in charge of that product did not follow through with the right people in the right places to make that happen. WMC on a Windows 7 computer is still the centerpiece of our home entertainment system; it's our DVR, video library, music player, etc.
  • Windows Server 2012/R2 - Once again, these were two great server operating systems, with amazing versions of IIS.
  • Windows 8 - I am almost embarrassed to admit this publicly, but I really wasn't fond of this OS, even though I tried very, very hard to like it. I could wax poetic about my reasons for this decision, but I'll just leave this as it stands.
  • Windows 8.1 - This OS fixed a few of the clunkers in Win8, but unfortunately Microsoft showed off additional fixes (like a replacement Start Menu) that were promised for a Win8.1 update, and that update never happened. The Windows product team decided to hold off and ship those features in Windows 10, and this was a seriously bad decision, for which I heard a great deal of end-user grumbling at Microsoft's expense. (Which may have been was well-deserved.)
  • Windows 10 - I upgraded a couple of computers to Win10 during the beta: one physical computer (which stayed on a particular stable build) and one virtual computer (which I updated when new customer previews were available). I've continued to run Win10 on a few computers since its release, and so far it's much better than the Windows 8 versions. Of course, we completely killed Windows Media Center in Win10, so I'm unhappy about that. (Time to look into running XBMC on a Raspberry Pi? Smile)

That about wraps it up for me. So if anyone is still reading by now, here is my statement that I have always used to summarize how I feel about working at Microsoft: "To me, working for Microsoft is like working for Santa Claus; we make all the coolest toys, and we get to play with them before anyone else."

With that in mind, who wouldn't want to spend 20 years working for Microsoft?

What I Do For A Living

It seems that I have always had a difficult time explaining to people what I do at Microsoft. It's not that I'm unsure about what I do - the details of my job have always been crystal-clear to me and I love what I am doing. It's just that I can't find a way to explain things in a way that doesn't result in blank stares from anyone who isn't a geek. (This problem isn't limited to me, though; my non-technical wife simply responds "I have no idea what he does" when someone asks her what I do for a living.)

Here's a perfect example: when I was a Program Manager on the Internet Information Services (IIS) team, people would often ask me what I did for Microsoft, and I would reply with something like, "I help design and implement the web publishing protocols for Microsoft's web server."

Other Person: [Blank Stare]

I would attempt to remedy the situation by adding, "You know, I design Microsoft's implementation of Internet technologies like the File Transfer Protocol, WebDAV, and the FrontPage Server Extensions."

Other Person: [Blank Stare]

In a sometimes-futile effort to salvage the conversation from complete disaster, I would interject, "You like to use the Internet, right? Well, your computer is on one side of the Internet, and my team helps build the other side of the Internet. That's kind of what I do."

That comment would usually be met with a slight spark of recognition, which was sometimes followed by a half-muttered, "That's nice."

At one time or other during my tenure as a Program Manager on the IIS team I was responsible for a smattering of disparate technologies; things like FTP, WebDAV, FPSE, FastCGI, PHP, URL Rewrite, IIS Express, Log Parser, etc. Most of those technologies garnered little to no interest for the average person, and many of my coworkers found them pretty boring as well. Just the same, I personally found every one of those technologies completely fascinating. (Why else would I spend eight years trying to get just one new command added to FTP?)

A couple of years ago I left the IIS program management team and I joined the writing team which is responsible for documenting Microsoft's ASP.NET framework; and if you have to ask what that means, then you are probably not interested in the answer.

Still, people would ask me what I do for Microsoft, and I would try to explain my job with statements like, "I document the Application Programming Interfaces (or APIs) for Microsoft's ASP.NET."

Other Person: [Blank Stare]

I would try to nudge the conversation along by saying things like, "I help people write web code."

Other Person: [Blank Stare]

Skipping ahead in the conversation, I would usually make a last-ditch attempt by stating, "Let's say you wanted to create a website; if so, you might read something that I wrote in order to help you get started."

Sometimes this remark would illicit a hint of acknowledgment, but usually I just got another blank stare.

This leads me to a few days ago. My wife and I were at dinner, and a waiter asked me what I did for a living. In the back of my mind I started to say something like, "Well, these days I'm documenting a set of APIs that Java programmers will use with Microsoft Azure technologies [blah blah blah]..."

But what actually came out of my mouth was, "I could explain it to you, but I'm pretty sure you wouldn't want me to. Trust me."

I like that answer. I think I'll stick with it in the future. :-)

FTP ETW Tracing and IIS 8 - Part 2

Shortly after I published my FTP ETW Tracing and IIS 8 blog post, I was using the batch file from that blog to troubleshoot an issue that I was having with a custom FTP provider. One of the columns which I display in my results is Clock-Time, which is obviously a sequential timestamp that is used to indicate the time and order in which the events occurred.

(Click the following image to view it full-size.)

At first glance the Clock-Time values might appear to be a range of useless numbers, but I use Clock-Time values quite often when I import the data from my ETW traces into something like Excel and I need to sort the data by the various columns.

That being said, apart from keeping the trace events in order, Clock-Time isn't a very user-friendly value. However, LogParser has some great built-in functions for crunching date/time values, so I decided to update the script to take advantage of some LogParser coolness and reformat the Clock-Time value into a human-readable Date/Time value.

My first order of business was to figure out how to decode the Clock-Time value; since Clock-Time increases for each event, it is obviously an offset from some constant, and after a bit of searching I found that the Clock-Time value is the offset in 100-nanosecond intervals since midnight on January 1, 1601. (Windows uses that value in a lot of places, not just ETW.) Once I had that information, it was pretty easy to come up with a LogParser formula to convert the Clock-Time value into the local time for my system, which is much easier to read.

With that in mind, here is the modified batch file:

@echo off

rem ======================================================================

rem Clean up old log files
for %%a in (ETL CSV) do if exist "%~n0.%%a" del "%~n0.%%a"

echo Starting the ETW session for full FTP tracing...
LogMan.exe start "%~n0" -p "IIS: Ftp Server" 255 5 -ets
echo.
echo Now reproduce your problem.
echo.
echo After you have reproduced your issue, hit any key to close the FTP
echo tracing session. Your trace events will be displayed automatically.
echo.
pause>nul

rem ======================================================================

echo.
echo Closing the ETW session for full FTP tracing...
LogMan.exe stop "%~n0" -ets

rem ======================================================================

echo.
echo Parsing the results - this may take a long time depending on the size of the trace...
if exist "%~n0.etl" (
   TraceRpt.exe "%~n0.etl" -o "%~n0.csv" -of CSV
   LogParser.exe "SELECT [Clock-Time], TO_LOCALTIME(ADD(TO_TIMESTAMP('1601-01-01 00:00:00', 'yyyy-MM-dd hh:mm:ss'), TO_TIMESTAMP(DIV([Clock-Time],10000000)))) AS [Date/Time], [Event Name], Type, [User Data] FROM '%~n0.csv'" -i:csv -e 2 -o:DATAGRID -rtp 20
)

When you run this new batch file, it will display an additional "Date/Time" column with a more-informative value in local time for the sever where you captured the trace.

(Click the following image to view it full-size.)

The new Date/Time column is considerably more practical, so I'll probably keep it in the batch file that I use when I am troubleshooting. You will also notice that I kept the original Clock-Time column; I chose to do so because I will undoubtedly continue to use that column for sorting when I import the data into something else, but you can safely remove that column if you would prefer to use only the new Date/Time value.

That wraps it up for today's post. :-)


Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/

FTP ETW Tracing and IIS 8

In the past I have written a couple of blogs about using the FTP service's Event Tracing for Windows (ETW) features to troubleshoot issues; see FTP and ETW Tracing and Troubleshooting Custom FTP Providers with ETW for details. Those blog posts contain batch files which use the built-in Windows LogMan utility to capture an ETW trace, and they use downloadable LogParser utility to parse the results into human-readable form. I use the batch files from those blogs quite often, and I tend to use them a lot when I am developing custom FTP providers which add new functionality to my FTP servers.

Unfortunately, sometime around the release of Windows 8 and Windows Server 2012 I discovered that the ETW format had changed, and the current version of LogParser (version 2.2) cannot read the new ETW files. When you try to use the batch files from my blog with IIS 8, you see the following errors:

Verifying that LogParser.exe is in the path...
Done.

Starting the ETW session for full FTP tracing...
The command completed successfully.

Now reproduce your problem.

After you have reproduced your issue, hit any key to close the FTP tracing session. Your trace events will be displayed automatically.

Closing the ETW session for full FTP tracing...
The command completed successfully.

Parsing the results - this may take a long time depending on the size of the trace...
Task aborted.
Cannot open <from-entity>: Trace file "C:\temp\ftp.etl" has been created on a OS version (6.3) that is not compatible with the current OS version


Statistics:
-----------
Elements processed: 0
Elements output: 0
Execution time: 0.06 seconds

I meant to research a workaround at the time, but one thing led to another and I simply forgot about doing so. But I needed to use ETW the other day when I was developing something, so that seemed like a good time to quit slacking and come up with an answer. :-)

With that in mind, I came up with a very easy workaround, which I will present here. Once again, this batch file has a requirement on LogParser being installed on your system, but for the sake of brevity I have removed the lines from this version of the batch file which check for LogParser. (You can copy those lines from my previous blog posts if you want that functionality restored.)

Here's the way that this workaround is implemented: instead of creating an ETW log and then parsing it directly with LogParser, this new batch file invokes the built-in Windows TraceRpt command to parse the ETW file and save the results as a CSV file, which is then read by LogParser to view the results in a datagrid like the batch files in my previous blogs:

@echo off

rem ======================================================================

rem Clean up old log files
for %%a in (ETL CSV) do if exist "%~n0.%%a" del "%~n0.%%a"

echo Starting the ETW session for full FTP tracing...
LogMan.exe start "%~n0" -p "IIS: Ftp Server" 255 5 -ets
echo.
echo Now reproduce your problem.
echo.
echo After you have reproduced your issue, hit any key to close the FTP
echo tracing session. Your trace events will be displayed automatically.
echo.
pause>nul

rem ======================================================================

echo.
echo Closing the ETW session for full FTP tracing...
LogMan.exe stop "%~n0" -ets

rem ======================================================================

echo.
echo Parsing the results - this may take a long time depending on the size of the trace...
if exist "%~n0.etl" (
   TraceRpt.exe "%~n0.etl" -o "%~n0.csv" -of CSV
   LogParser.exe "SELECT [Clock-Time], [Event Name], Type, [User Data] FROM '%~n0.csv'" -i:csv -e 2 -o:DATAGRID -rtp 20
)

Here's another great thing about this new batch file - it will also work down-level on Windows 7 and Windows Server 2008; so if you have been using my previous batch files with IIS 7 - you can simply replace your old batch file with this new version. You will see a few differences between the results from my old batch files and this new version, namely that I included a couple of extra columns that I like to use for troubleshooting.

(Click the following image to view it full-size.)

There is one last thing which I would like to mention in closing: I realize that it would be much easier on everyone if Microsoft simply released a new version of LogParser which works with the new ETW format, but unfortunately there are no plans at the moment to release a new version of LogParser. And trust me - I'm just as depressed about that fact as anyone else. :-(


Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/

Adding Additional Content Types to my Classic ASP and URL Rewrite Samples for Dynamic SEO Functionality

In December of 2012 I wrote a blog titled "Using Classic ASP and URL Rewrite for Dynamic SEO Functionality", in which I described how to use Classic ASP and URL Rewrite to dynamically-generate Robots.txt and Sitemap.xml or Sitemap.txt files. I received a bunch of requests for additional information, so I wrote a follow-up blog this past November titled "Revisiting My Classic ASP and URL Rewrite for Dynamic SEO Functionality Examples" which illustrated how to limit the output for the Robots.asp and Sitemap.asp files to specific sections of your website.

That being said, I continue to receive requests for additional ways to stretch those samples, so I thought that I would write at least a couple of blogs on the subject. With that in mind, for today I wanted to show how you can add additional content types to the samples.

Overview

Here is a common question that I have been asked by several people:

"The example only works with *.html files; how do I include my other files?"

That's a great question, and additional content types are really easy to implement, and the majority of the code from my original blog will remain unchanged. Here's the file by file breakdown for the changes that need made:

FilenameChanges
Robots.asp None
Sitemap.asp See the sample later in this blog
Web.config None

If you are already using the files from my original blog, no changes need to be made to your Robots.asp file or the URL Rewrite rules in your Web.config file because the updates in this blog will only impact the output from the Sitemap.asp file.

Updating the Sitemap.asp File

My original sample contained a line of code which read "If StrComp(strExt,"html",vbTextCompare)=0 Then" and this line was used to restrict the sitemap output to static *.html files. For this new sample I need to make two changes:

  1. I am adding a string constant which contains a list of file extensions from several content types to use for the output.
  2. I replace the line of code which performs the comparison.

Note: I define the constant near the beginning of the file so it's easier for other people to find; I would normally define that constant elsewhere in the code.

<%
    Option Explicit
    On Error Resume Next
    
    Const strContentTypes = "htm|html|asp|aspx|txt"
    
    Response.Clear
    Response.Buffer = True
    Response.AddHeader "Connection", "Keep-Alive"
    Response.CacheControl = "public"
    
    Dim strFolderArray, lngFolderArray
    Dim strUrlRoot, strPhysicalRoot, strFormat
    Dim strUrlRelative, strExt

    Dim objFSO, objFolder, objFile

    strPhysicalRoot = Server.MapPath("/")
    Set objFSO = Server.CreateObject("Scripting.Filesystemobject")
    
    strUrlRoot = "http://" & Request.ServerVariables("HTTP_HOST")
    
    ' Check for XML or TXT format.
    If UCase(Trim(Request("format")))="XML" Then
        strFormat = "XML"
        Response.ContentType = "text/xml"
    Else
        strFormat = "TXT"
        Response.ContentType = "text/plain"
    End If

    ' Add the UTF-8 Byte Order Mark.
    Response.Write Chr(CByte("&hEF"))
    Response.Write Chr(CByte("&hBB"))
    Response.Write Chr(CByte("&hBF"))
    
    If strFormat = "XML" Then
        Response.Write "<?xml version=""1.0"" encoding=""UTF-8""?>" & vbCrLf
        Response.Write "<urlset xmlns=""http://www.sitemaps.org/schemas/sitemap/0.9"">" & vbCrLf
    End if
    
    ' Always output the root of the website.
    Call WriteUrl(strUrlRoot,Now,"weekly",strFormat)

    ' --------------------------------------------------
    ' This following section contains the logic to parse
    ' the directory tree and return URLs based on the
    ' files that it locates.
    ' -------------------------------------------------- 
    strFolderArray = GetFolderTree(strPhysicalRoot)

    For lngFolderArray = 1 to UBound(strFolderArray)
        strUrlRelative = Replace(Mid(strFolderArray(lngFolderArray),Len(strPhysicalRoot)+1),"\","/")
        Set objFolder = objFSO.GetFolder(Server.MapPath("." & strUrlRelative))
        For Each objFile in objFolder.Files
            strExt = objFSO.GetExtensionName(objFile.Name)
            If InStr(1,strContentTypes,strExt,vbTextCompare) Then
                If StrComp(Left(objFile.Name,6),"google",vbTextCompare)<>0 Then
                    Call WriteUrl(strUrlRoot & strUrlRelative & "/" & objFile.Name, objFile.DateLastModified, "weekly", strFormat)
                End If
            End If
        Next
    Next

    ' --------------------------------------------------
    ' End of file system loop.
    ' -------------------------------------------------- 
    If strFormat = "XML" Then
        Response.Write "</urlset>"
    End If
    
    Response.End

    ' ======================================================================
    '
    ' Outputs a sitemap URL to the client in XML or TXT format.
    ' 
    ' tmpStrFreq = always|hourly|daily|weekly|monthly|yearly|never 
    ' tmpStrFormat = TXT|XML
    '
    ' ======================================================================

    Sub WriteUrl(tmpStrUrl,tmpLastModified,tmpStrFreq,tmpStrFormat)
        On Error Resume Next
        Dim tmpDate : tmpDate = CDate(tmpLastModified)
        ' Check if the request is for XML or TXT and return the appropriate syntax.
        If tmpStrFormat = "XML" Then
            Response.Write " <url>" & vbCrLf
            Response.Write " <loc>" & Server.HtmlEncode(tmpStrUrl) & "</loc>" & vbCrLf
            Response.Write " <lastmod>" & Year(tmpLastModified) & "-" & Right("0" & Month(tmpLastModified),2) & "-" & Right("0" & Day(tmpLastModified),2) & "</lastmod>" & vbCrLf
            Response.Write " <changefreq>" & tmpStrFreq & "</changefreq>" & vbCrLf
            Response.Write " </url>" & vbCrLf
        Else
            Response.Write tmpStrUrl & vbCrLf
        End If
    End Sub

    ' ======================================================================
    '
    ' Returns a string array of folders under a root path
    '
    ' ======================================================================

    Function GetFolderTree(strBaseFolder)
        Dim tmpFolderCount,tmpBaseCount
        Dim tmpFolders()
        Dim tmpFSO,tmpFolder,tmpSubFolder
        ' Define the initial values for the folder counters.
        tmpFolderCount = 1
        tmpBaseCount = 0
        ' Dimension an array to hold the folder names.
        ReDim tmpFolders(1)
        ' Store the root folder in the array.
        tmpFolders(tmpFolderCount) = strBaseFolder
        ' Create file system object.
        Set tmpFSO = Server.CreateObject("Scripting.Filesystemobject")
        ' Loop while we still have folders to process.
        While tmpFolderCount <> tmpBaseCount
            ' Set up a folder object to a base folder.
            Set tmpFolder = tmpFSO.GetFolder(tmpFolders(tmpBaseCount+1))
              ' Loop through the collection of subfolders for the base folder.
            For Each tmpSubFolder In tmpFolder.SubFolders
                ' Increment the folder count.
                tmpFolderCount = tmpFolderCount + 1
                ' Increase the array size
                ReDim Preserve tmpFolders(tmpFolderCount)
                ' Store the folder name in the array.
                tmpFolders(tmpFolderCount) = tmpSubFolder.Path
            Next
            ' Increment the base folder counter.
            tmpBaseCount = tmpBaseCount + 1
        Wend
        GetFolderTree = tmpFolders
    End Function
%>

That's it. Pretty easy, eh?

I have also received several requests about creating a sitemap which contains URLs with query strings, but I'll cover that scenario in a later blog.


Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/

Rapid PHP Deployment for IIS using a Batch File

Whenever I am delivering a presentation where I need to use PHP, I typically use a batch file that I wrote in order to rapidly deploy PHP on the system that I am using for my demos. The batch file usually takes less than a second to run, which always seems to amaze people in the audience. As a result, I usually have several people ask me for my batch file after each presentation, so I thought that it would make a good subject for today's blog.

I should mention that I have used this batch file in order to demonstrate PHP with IIS in a variety of scenarios, and one of my favorite demos is when I would borrow someone's laptop and plug in a flash drive where I had IIS Express pre-installed, and then I would run the batch file in this blog to deploy PHP. Next I would launch IIS Express, open a web browser on their system, and then browse to http://localhost/ in order to show that IIS Express was working correctly. Lastly I would write a simple PHP "Hello World" page to show that PHP was up-and-running on their system in a matter of seconds.

That being said, I have to point out that there is a very important prerequisite that you must have in order to follow the steps in the blog: you need to start with a known-good installation of PHP from one of your systems, and I'll explain what I mean by that.

My batch file expects to find a folder containing ready-to-run files for PHP in order to deploy PHP on a new system. I originally obtained my PHP files by using the Web Platform Installer (WebPI) to install PHP, and then I copied the files to my flash drive or some other repository. (Note that WebPI usually installs PHP in the "%ProgramFiles(x86)%\PHP" folder.) If you don't want to use WebPI, you can also download PHP from http://windows.php.net/, but you're on your own for configuration.

Once I have the files from a known-good installation of PHP, I create the following folder structure in the location where I will be storing the files that I use to deploy PHP on other systems:

  • <root folder>
    • SETUP_PHP.cmd (the batch file from this blog)
    • PHP (the folder containing the PHP files)
      • PHP.INI
      • PHP-CGI.EXE
      • etc. (all of the remaining PHP files and folders)

One thing to note is that the PHP.INI file you use may contain paths which refer to specific directories on the system from which you are copying your PHP files, so you need to make sure that those paths will exist on the system where you deploy PHP.

Here is an example: when I used WebPI to install PHP 5.5 on a system with IIS, it installed PHP into my "%ProgramFiles(x86)%\PHP\v5.5" folder. During the installation process, WebPI updated the PHP file to reflect any paths that need to be defined. At the time that I put together my notes for this blog, those updates mainly applied to the path where PHP expects to find it's extensions:

extension_dir="C:\Program Files (x86)\PHP\v5.5\ext\"

What this means is - if you want to deploy PHP to some other path on subsequent systems, you will need to update at least that line in the PHP.INI file that you are using to deploy PHP. In my particular case, I prefer to deploy PHP to the "%SystemDrive%\PHP" path, but it can be anywhere as long as you update everything accordingly.

The following batch file will deploy the PHP files in the "%SystemDrive%\PHP" folder on your system, and then it will update IIS with the necessary settings for this PHP deployment to work:

@echo off

REM Change to the installation folder
pushd "%~dp0"

REM Cheap test to see if IIS is installed
if exist "%SystemRoot%\System32\inetsrv" (
  REM Check for the PHP installation files in a subfolder
  if exist "%~dp0PHP" (
    REM Check for an existing installation of PHP
    if not exist "%SystemDrive%\PHP" (
      REM Create the folder for PHP
      md "%SystemDrive%\PHP"
      REM Deploy the PHP files
      xcopy /erhky "%~dp0PHP\*" "%SystemDrive%\PHP"
    )
    pushd "%SystemRoot%\System32\inetsrv"
    REM Configure the IIS settings for PHP
    appcmd.exe set config -section:system.webServer/fastCgi /+"[fullPath='%SystemDrive%\PHP\php-cgi.exe',monitorChangesTo='php.ini',activityTimeout='600',requestTimeout='600',instanceMaxRequests='10000']" /commit:apphost
    appcmd.exe set config -section:system.webServer/fastCgi /+"[fullPath='%SystemDrive%\PHP\php-cgi.exe',monitorChangesTo='php.ini',activityTimeout='600',requestTimeout='600',instanceMaxRequests='10000'].environmentVariables.[name='PHP_FCGI_MAX_REQUESTS',value='10000']" /commit:apphost
    appcmd.exe set config -section:system.webServer/fastCgi /+"[fullPath='%SystemDrive%\PHP\php-cgi.exe',monitorChangesTo='php.ini',activityTimeout='600',requestTimeout='600',instanceMaxRequests='10000'].environmentVariables.[name='PHPRC',value='%SystemDrive%\PHP']" /commit:apphost
    appcmd.exe set config -section:system.webServer/handlers /+"[name='PHP_via_FastCGI',path='*.php',verb='GET,HEAD,POST',modules='FastCgiModule',scriptProcessor='%SystemDrive%\PHP\php-cgi.exe',resourceType='Either']" /commit:apphost
    popd
  )
)
popd

Once you have all of that in place, it usually takes less than a second to deploy PHP, which is why so many people seem interested during my presentations.

Note that you can deploy PHP for IIS Express just as easily by updating the "%SystemRoot%\System32\inetsrv" paths in the batch file to "%ProgramFiles%\IIS Express" or "%ProgramFiles(x86)%\IIS Express" paths. You can also use this batch file as part of a deployment process for PHP within a web farm; in which case, you will need to pay attention to the paths inside your PHP.INI file which I mentioned earlier.


Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/

Revisiting My Classic ASP and URL Rewrite for Dynamic SEO Functionality Examples

Last year I wrote a blog titled Using Classic ASP and URL Rewrite for Dynamic SEO Functionality, in which I described how you could combine Classic ASP and the URL Rewrite module for IIS to dynamically create Robots.txt and Sitemap.xml files for your website, thereby helping with your Search Engine Optimization (SEO) results. A few weeks ago I had a follow-up question which I thought was worth answering in a blog post.

Overview

Here is the question that I was asked:

"What if I don't want to include all dynamic pages in sitemap.xml but only a select few or some in certain directories because I don't want bots to crawl all of them. What can I do?"

That's a great question, and it wasn't tremendously difficult for me to update my original code samples to address this request. First of all, the majority of the code from my last blog will remain unchanged - here's the file by file breakdown for the changes that need made:

FilenameChanges
Robots.asp None
Sitemap.asp See the sample later in this blog
Web.config None

So if you are already using the files from my original blog, no changes need to be made to your Robot.asp file or the URL Rewrite rules in your Web.config file because the question only concerns the files that are returned in the the output for Sitemap.xml.

Updating the Necessary Files

The good news it, I wrote most of the heavy duty code in my last blog - there were only a few changes that needed to made in order to accommodate the requested functionality. The main difference is that the original Sitemap.asp file used to have a section that recursively parsed the entire website and listed all of the files in the website, whereas this new version moves that section of code into a separate function to which you pass the unique folder name to parse recursively. This allows you to specify only those folders within your website that you want in the resultant sitemap output.

With that being said, here's the new code for the Sitemap.asp file:

<%
    Option Explicit
    On Error Resume Next
    
    Response.Clear
    Response.Buffer = True
    Response.AddHeader "Connection", "Keep-Alive"
    Response.CacheControl = "public"
    
    Dim strUrlRoot, strPhysicalRoot, strFormat
    Dim objFSO, objFolder, objFile

    strPhysicalRoot = Server.MapPath("/")
    Set objFSO = Server.CreateObject("Scripting.Filesystemobject")
    
    strUrlRoot = "http://" & Request.ServerVariables("HTTP_HOST")
    
    ' Check for XML or TXT format.
    If UCase(Trim(Request("format")))="XML" Then
        strFormat = "XML"
        Response.ContentType = "text/xml"
    Else
        strFormat = "TXT"
        Response.ContentType = "text/plain"
    End If

    ' Add the UTF-8 Byte Order Mark.
    Response.Write Chr(CByte("&hEF"))
    Response.Write Chr(CByte("&hBB"))
    Response.Write Chr(CByte("&hBF"))
    
    If strFormat = "XML" Then
        Response.Write "<?xml version=""1.0"" encoding=""UTF-8""?>" & vbCrLf
        Response.Write "<urlset xmlns=""http://www.sitemaps.org/schemas/sitemap/0.9"">" & vbCrLf
    End if
    
    ' Always output the root of the website.
    Call WriteUrl(strUrlRoot,Now,"weekly",strFormat)

    ' Output only specific folders.
    Call ParseFolder("/marketing")
    Call ParseFolder("/sales")
    Call ParseFolder("/hr/jobs")

    ' --------------------------------------------------
    ' End of file system loop.
    ' -------------------------------------------------- 
    If strFormat = "XML" Then
        Response.Write "</urlset>"
    End If
    
    Response.End

    ' ======================================================================
    '
    ' Recursively walks a folder path and return URLs based on the
    ' static *.html files that it locates.
    ' 
    ' strRootFolder = The base path for recursion
    '
    ' ======================================================================

    Sub ParseFolder(strParentFolder)
        On Error Resume Next

        Dim strChildFolders, lngChildFolders
        Dim strUrlRelative, strExt

        ' Get the list of child folders under a parent folder.
        strChildFolders = GetFolderTree(Server.MapPath(strParentFolder))

        ' Loop through the collection of folders.
        For lngChildFolders = 1 to UBound(strChildFolders)
            strUrlRelative = Replace(Mid(strChildFolders(lngChildFolders),Len(strPhysicalRoot)+1),"\","/")
            Set objFolder = objFSO.GetFolder(Server.MapPath("." & strUrlRelative))
            ' Loop through the collection of files.
            For Each objFile in objFolder.Files
                strExt = objFSO.GetExtensionName(objFile.Name)
                If StrComp(strExt,"html",vbTextCompare)=0 Then
                    If StrComp(Left(objFile.Name,6),"google",vbTextCompare)<>0 Then
                        Call WriteUrl(strUrlRoot & strUrlRelative & "/" & objFile.Name, objFile.DateLastModified, "weekly", strFormat)
                    End If
                End If
            Next
        Next

    End Sub

    ' ======================================================================
    '
    ' Outputs a sitemap URL to the client in XML or TXT format.
    ' 
    ' tmpStrFreq = always|hourly|daily|weekly|monthly|yearly|never 
    ' tmpStrFormat = TXT|XML
    '
    ' ======================================================================

    Sub WriteUrl(tmpStrUrl,tmpLastModified,tmpStrFreq,tmpStrFormat)
        On Error Resume Next
        Dim tmpDate : tmpDate = CDate(tmpLastModified)
        ' Check if the request is for XML or TXT and return the appropriate syntax.
        If tmpStrFormat = "XML" Then
            Response.Write " <url>" & vbCrLf
            Response.Write " <loc>" & Server.HtmlEncode(tmpStrUrl) & "</loc>" & vbCrLf
            Response.Write " <lastmod>" & Year(tmpLastModified) & "-" & Right("0" & Month(tmpLastModified),2) & "-" & Right("0" & Day(tmpLastModified),2) & "</lastmod>" & vbCrLf
            Response.Write " <changefreq>" & tmpStrFreq & "</changefreq>" & vbCrLf
            Response.Write " </url>" & vbCrLf
        Else
            Response.Write tmpStrUrl & vbCrLf
        End If
    End Sub

    ' ======================================================================
    '
    ' Returns a string array of folders under a root path
    '
    ' ======================================================================

    Function GetFolderTree(strBaseFolder)
        Dim tmpFolderCount,tmpBaseCount
        Dim tmpFolders()
        Dim tmpFSO,tmpFolder,tmpSubFolder
        ' Define the initial values for the folder counters.
        tmpFolderCount = 1
        tmpBaseCount = 0
        ' Dimension an array to hold the folder names.
        ReDim tmpFolders(1)
        ' Store the root folder in the array.
        tmpFolders(tmpFolderCount) = strBaseFolder
        ' Create file system object.
        Set tmpFSO = Server.CreateObject("Scripting.Filesystemobject")
        ' Loop while we still have folders to process.
        While tmpFolderCount <> tmpBaseCount
            ' Set up a folder object to a base folder.
            Set tmpFolder = tmpFSO.GetFolder(tmpFolders(tmpBaseCount+1))
              ' Loop through the collection of subfolders for the base folder.
            For Each tmpSubFolder In tmpFolder.SubFolders
                ' Increment the folder count.
                tmpFolderCount = tmpFolderCount + 1
                ' Increase the array size
                ReDim Preserve tmpFolders(tmpFolderCount)
                ' Store the folder name in the array.
                tmpFolders(tmpFolderCount) = tmpSubFolder.Path
            Next
            ' Increment the base folder counter.
            tmpBaseCount = tmpBaseCount + 1
        Wend
        GetFolderTree = tmpFolders
    End Function
%>

It should be easily seen that the code is largely unchanged from my previous blog.

In Closing...

One last thing to consider, I didn't make any changes to the Robots.asp file in this blog. But that being said, when you do not want specific paths crawled, you should add rules to your Robots.txt file to disallow those paths. For example, here is a simple Robots.txt file which allows your entire website:

# Robots.txt
# For more information on this file see:
# http://www.robotstxt.org/

# Define the sitemap path
Sitemap: http://localhost:53644/sitemap.xml

# Make changes for all web spiders
User-agent: *
Allow: /
Disallow:

If you were going to deny crawling on certain paths, you would need to add the specific paths that you do not want crawled to your Robots.txt file like the following example:

# Robots.txt
# For more information on this file see:
# http://www.robotstxt.org/

# Define the sitemap path
Sitemap: http://localhost:53644/sitemap.xml

# Make changes for all web spiders
User-agent: *
Disallow: /foo
Disallow: /bar

With that being said, if you are using my Robots.asp file from my last blog, you would need to update the section of code that defines the paths like my previous example:

Response.Write "# Make changes for all web spiders" & vbCrLf
Response.Write "User-agent: *" & vbCrLf
Response.Write "Disallow: /foo" & vbCrLf
Response.Write "Disallow: /bar" & vbCrLf

I hope this helps. ;-]


Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/

Updating my HTML Application for Configuring your WebDAV Redirector Settings

A couple of years ago I wrote a blog that was titled "How to create an HTML Application to configure your WebDAV Redirector settings", where I showed how to use HTMLA to create a simple editor for most of the WebDAV Redirector settings. These settings have no other user interface, so prior to my blog post users had to manually edit the registry in order to modify their WebDAV Redirector settings.

Click image to expand

In the past two years since I wrote that blog, I have found myself using that simple application so often that I now keep it in my personal utilities folder on my SkyDrive so I can have it with me no matter where I am travelling. But that being said, I ran into an interesting situation the other day that made me want to update the application, so I thought that it was time to write a new blog with the updated changes.

Here's what happened - I had opened my application for modifying my WebDAV Redirector settings, but then something happened which distracted me, and then I headed off to lunch before I committed my changes to the registry. When I came back to my office, I noticed that my WebDAV Redirector settings application was still open and I clicked the Exit Application button. The application popped up a dialog which informed me that I had changes that hadn't been saved to the registry, but I forgot what they were. This put me in a quandary - I could simply click Yes and hope for the best, or I could click No and lose whatever changes that I had made and re-open the application to start over.

It was at that time that I thought to myself, "If only I had a Reset Values button..."

By now you can probably see where this blog is going, and here's what the new application looks like - it's pretty much the same as the last application, with the additional button that allows you to reset your values without exiting the application. (Note - the application will prompt you for confirmation if you attempt to reset the values and you have unsaved changes.)

Click image to expand

Creating the Updated HTML Application

To create this HTML Application, you need to use the same steps as my last blog: save the following HTMLA code as "WebDAV Redirector Settings.hta" to your computer, and then double-click its icon to run the application.

<html>

<head>
<title>WebDAV Redirector Settings</title>
<HTA:APPLICATION
  APPLICATIONNAME="WebDAV Redirector Settings"
  ID="WebDAV Redirector Settings"
  VERSION="1.0"
  BORDER="dialog"
  BORDERSTYLE="static"
  INNERBORDER="no"
  SYSMENU="no"
  MAXIMIZEBUTTON="no"
  MINIMIZEBUTTON="no"
  SCROLL="no"
  SCROLLFLAT="yes"
  SINGLEINSTANCE="yes"
  CONTEXTMENU="no"
  SELECTION="no"/>

<script language="vbscript">

' ----------------------------------------
' Start of main code section.
' ----------------------------------------

Option Explicit

Const intDialogWidth = 700
Const intDialogHeight = 620
Const HKEY_LOCAL_MACHINE = &H80000002
Const strWebClientKeyPath = "SYSTEM\CurrentControlSet\Services\WebClient\Parameters"
Const strLuaKeyPath = "Software\Microsoft\Windows\CurrentVersion\Policies\System"
Dim objRegistry
Dim blnHasChanges

' ----------------------------------------
' Start the application.
' ----------------------------------------

Sub Window_OnLoad
  On Error Resume Next
  ' Set up the UI dimensions.
  Self.resizeTo intDialogWidth,intDialogHeight
  Self.moveTo (Screen.AvailWidth - intDialogWidth) / 2, _
    (Screen.AvailHeight - intDialogHeight) / 2
  ' Retrieve the current settings.
  Document.all.TheBody.ClassName = "hide"
  Set objRegistry = GetObject( _
    "winmgmts:{impersonationLevel=impersonate}!\\.\root\default:StdRegProv")
  Call CheckForLUA()
  Call GetValues()
  Document.All.TheBody.ClassName = "show"
End Sub

' ----------------------------------------
' Check for User Access Control
' ----------------------------------------

Sub CheckForLUA()
  If GetRegistryDWORD(strLuaKeyPath,"EnableLUA",1)<> 0 Then
    MsgBox "User Access Control (UAC) is enabled on this computer." & _
      vbCrLf & vbCrLf & "UAC must be disabled in order to edit " & _
      "the registry and restart the service for the WebDAV Redirector. " & _
      "Please disable UAC before running this application again. " & _
      "This application will now exit.", _
      vbCritical, "User Access Control"
    Self.close
  End If 
End Sub

' ----------------------------------------
' Exit the application.
' ----------------------------------------

Sub ExitApplication()
  If blnHasChanges = False Then
    If MsgBox("Are you sure you want to exit?", _
      vbQuestion Or vbYesNo Or vbDefaultButton2, _
      "Exit Application") = vbNo Then
      Exit Sub
    End If
  Else
    Dim intRetVal
    intRetVal = MsgBox("You have unsaved changes. " & _
      "Do you want to save them before you exit?", _
      vbQuestion Or vbYesNoCancel Or vbDefaultButton1, _
      "Reset Application")
    If intRetVal = vbYes Then
      Call SetValues()
    ElseIf intRetVal = vbCancel Then
      Exit Sub
    End If
  End If
  Self.close
End Sub

' ----------------------------------------
' Reset the application.
' ----------------------------------------

Sub ResetApplication()
  If blnHasChanges = True Then
    Dim intRetVal
    intRetVal = MsgBox("You have unsaved changes. " & _
      "Do you want to save them before you reset the values?", _
      vbQuestion Or vbYesNoCancel Or vbDefaultButton1, _
      "Reset Application")
    If intRetVal = vbYes Then
      Call SetValues()
    ElseIf intRetVal = vbCancel Then
      Exit Sub
    End If
  End If
  Call GetValues()
End Sub

' ----------------------------------------
' Flag the application as having changes.
' ----------------------------------------

Sub FlagChanges()
  blnHasChanges = True
End Sub

' ----------------------------------------
' Retrieve the settings from the registry.
' ----------------------------------------

Sub GetValues()
  On Error Resume Next
  Dim tmpCount,tmpArray,tmpString
  ' Get the radio button values
  Call SetRadioValue(Document.all.BasicAuthLevel, _
    GetRegistryDWORD(strWebClientKeyPath, _
    "BasicAuthLevel",1))
  Call SetRadioValue(Document.all.SupportLocking, _
    GetRegistryDWORD(strWebClientKeyPath, _
    "SupportLocking",1))
  ' Get the text box values
  Document.all.InternetServerTimeoutInSec.Value = _
    GetRegistryDWORD(strWebClientKeyPath, _
    "InternetServerTimeoutInSec",30)
  Document.all.FileAttributesLimitInBytes.Value = _
    GetRegistryDWORD(strWebClientKeyPath, _
    "FileAttributesLimitInBytes",1000000)
  Document.all.FileSizeLimitInBytes.Value = _
    GetRegistryDWORD(strWebClientKeyPath, _
    "FileSizeLimitInBytes",50000000)
  Document.all.LocalServerTimeoutInSec.Value = _
    GetRegistryDWORD(strWebClientKeyPath, _
    "LocalServerTimeoutInSec",15)
  Document.all.SendReceiveTimeoutInSec.Value = _
    GetRegistryDWORD(strWebClientKeyPath, _
    "SendReceiveTimeoutInSec",60)
  Document.all.ServerNotFoundCacheLifeTimeInSec.Value = _
    GetRegistryDWORD(strWebClientKeyPath, _
    "ServerNotFoundCacheLifeTimeInSec",60)
  ' Get the text area values
  tmpArray = GetRegistryMULTISZ( _
    strWebClientKeyPath,"AuthForwardServerList")
  For tmpCount = 0 To UBound(tmpArray)
    tmpString = tmpString & tmpArray(tmpCount) & vbTab
  Next
  If Len(tmpString)>0 Then
    Document.all.AuthForwardServerList.Value = _
      Replace(Left(tmpString,Len(tmpString)-1),vbTab,vbCrLf)
  End If
  blnHasChanges = False
End Sub

' ----------------------------------------
' Save the settings in the registry.
' ----------------------------------------

Sub SetValues()
  On Error Resume Next
  ' Set the radio button values
  Call SetRegistryDWORD( _
    strWebClientKeyPath, _
    "BasicAuthLevel", _
    GetRadioValue(Document.all.BasicAuthLevel))
  Call SetRegistryDWORD( _
    strWebClientKeyPath, _
    "SupportLocking", _
    GetRadioValue(Document.all.SupportLocking))
  ' Set the text box values
  Call SetRegistryDWORD( _
    strWebClientKeyPath, _
    "InternetServerTimeoutInSec", _
    Document.all.InternetServerTimeoutInSec.Value)
  Call SetRegistryDWORD( _
    strWebClientKeyPath, _
    "FileAttributesLimitInBytes", _
    Document.all.FileAttributesLimitInBytes.Value)
  Call SetRegistryDWORD( _
    strWebClientKeyPath, _
    "FileSizeLimitInBytes", _
    Document.all.FileSizeLimitInBytes.Value)
  Call SetRegistryDWORD( _
    strWebClientKeyPath, _
    "LocalServerTimeoutInSec", _
    Document.all.LocalServerTimeoutInSec.Value)
  Call SetRegistryDWORD( _
    strWebClientKeyPath, _
    "SendReceiveTimeoutInSec", _
    Document.all.SendReceiveTimeoutInSec.Value)
  Call SetRegistryDWORD( _
    strWebClientKeyPath, _
    "ServerNotFoundCacheLifeTimeInSec", _
    Document.all.ServerNotFoundCacheLifeTimeInSec.Value)
  ' Set the text area values
  Call SetRegistryMULTISZ( _
    strWebClientKeyPath, _
    "AuthForwardServerList", _
    Split(Document.all.AuthForwardServerList.Value,vbCrLf))
  ' Prompt to restart the WebClient service
  If MsgBox("Do you want to restart the WebDAV Redirector " & _
    "service so your settings will take effect?", _
    vbQuestion Or vbYesNo Or vbDefaultButton2, _
    "Restart WebDAV Redirector") = vbYes Then
    ' Restart the WebClient service.
    Call RestartWebClient()
  End If
  Call GetValues()
End Sub

' ----------------------------------------
' Start the WebClient service.
' ----------------------------------------

Sub RestartWebClient()
  On Error Resume Next
  Dim objWMIService,colServices,objService
  Document.All.TheBody.ClassName = "hide"
  Set objWMIService = GetObject( _
    "winmgmts:{impersonationLevel=impersonate}!\\.\root\cimv2")
  Set colServices = objWMIService.ExecQuery( _
    "Select * from Win32_Service Where Name='WebClient'")
  For Each objService in colServices
    objService.StopService()
    objService.StartService()
  Next
  Document.All.TheBody.ClassName = "show"
End Sub

' ----------------------------------------
' Retrieve a DWORD value from the registry.
' ----------------------------------------

Function GetRegistryDWORD( _
    ByVal tmpKeyPath, _
    ByVal tmpValueName, _
    ByVal tmpDefaultValue)
  On Error Resume Next
  Dim tmpDwordValue
  If objRegistry.GetDWORDValue( _
      HKEY_LOCAL_MACHINE, _
      tmpKeyPath, _
      tmpValueName, _
      tmpDwordValue)=0 Then
    GetRegistryDWORD = CLng(tmpDwordValue)
  Else
    GetRegistryDWORD = CLng(tmpDefaultValue)
  End If
End Function

' ----------------------------------------
' Set a DWORD value in the registry.
' ----------------------------------------

Sub SetRegistryDWORD( _
    ByVal tmpKeyPath, _
    ByVal tmpValueName, _
    ByVal tmpDwordValue)
  On Error Resume Next
  Call objRegistry.SetDWORDValue( _
    HKEY_LOCAL_MACHINE, _
    tmpKeyPath, _
    tmpValueName, _
    CLng(tmpDwordValue))
End Sub

' ----------------------------------------
' Retrieve a MULTISZ value from the registry.
' ----------------------------------------

Function GetRegistryMULTISZ( _
    ByVal tmpKeyPath, _
    ByVal tmpValueName)
  On Error Resume Next
  Dim tmpMultiSzValue
  If objRegistry.GetMultiStringValue( _
      HKEY_LOCAL_MACHINE, _
      tmpKeyPath, _
      tmpValueName, _
      tmpMultiSzValue)=0 Then
    GetRegistryMULTISZ = tmpMultiSzValue
  Else
    GetRegistryMULTISZ = Array()
  End If
End Function

' ----------------------------------------
' Set a MULTISZ value in the registry.
' ----------------------------------------

Sub SetRegistryMULTISZ( _
    ByVal tmpKeyPath, _
    ByVal tmpValueName, _
    ByVal tmpMultiSzValue)
  On Error Resume Next
  Call objRegistry.SetMultiStringValue( _
    HKEY_LOCAL_MACHINE, _
    tmpKeyPath, _
    tmpValueName, _
    tmpMultiSzValue)
End Sub

' ----------------------------------------
' Retrieve the value of a radio button group.
' ----------------------------------------

Function GetRadioValue(ByVal tmpRadio)
  On Error Resume Next
  Dim tmpCount
  For tmpCount = 0 To (tmpRadio.Length-1)
    If tmpRadio(tmpCount).Checked Then
      GetRadioValue = CLng(tmpRadio(tmpCount).Value)
      Exit For
    End If
  Next
End Function

' ----------------------------------------
' Set the value for a radio button group.
' ----------------------------------------

Sub SetRadioValue(ByVal tmpRadio, ByVal tmpValue)
  On Error Resume Next
  Dim tmpCount
  For tmpCount = 0 To (tmpRadio.Length-1)
    If CLng(tmpRadio(tmpCount).Value) = CLng(tmpValue) Then
      tmpRadio(tmpCount).Checked = True
      Exit For
    End If
  Next
End Sub

' ----------------------------------------
'
' ----------------------------------------

Sub Validate(tmpField)
  Dim tmpRegEx, tmpMatches
  Set tmpRegEx = New RegExp
  tmpRegEx.Pattern = "[0-9]"
  tmpRegEx.IgnoreCase = True
  tmpRegEx.Global = True
  Set tmpMatches = tmpRegEx.Execute(tmpField.Value)
  If tmpMatches.Count = Len(CStr(tmpField.Value)) Then
    If CDbl(tmpField.Value) => 0 And _
      CDbl(tmpField.Value) =< 4294967295 Then
      Exit Sub
    End If
  End If
  MsgBox "Please enter a whole number between 0 and 4294967295.", _
    vbCritical, "Validation Error"
  tmpField.Focus
End Sub

' ----------------------------------------
'
' ----------------------------------------

Sub BasicAuthWarning()
  MsgBox "WARNING:" & vbCrLf  & vbCrLf & _
    "Using Basic Authentication over non-SSL connections can cause " & _
    "serious security issues. Usernames and passwords are transmitted " & _
    "in clear text, therefore the use of Basic Authentication with " & _
    "WebDAV is disabled by default for non-SSL connections. That " & _
    "being said, this setting can override the default behavior for " & _
    "Basic Authentication, but it is strongly discouraged.", _
    vbCritical, "Basic Authentication Warning"
End Sub

' ----------------------------------------
' End of main code section.
' ----------------------------------------

</script>
<style>
body { color:#000000; background-color:#cccccc;
  font-family:'Segoe UI',Tahoma,Verdana,Arial; font-size:9pt; }
fieldset { padding:10px; width:640px; }
.button { width:150px; }
.textbox { width:200px; height:22px; text-align:right; }
.textarea { width:300px; height:50px; text-align:left; }
.radio { margin-left:-5px; margin-top: -2px; }
.hide { display:none; }
.show { display:block; }
select { width:300px; text-align:left; }
table { border-collapse:collapse; empty-cells:hide; }
h1 { font-size:14pt; }
th { font-size:9pt; text-align:left; vertical-align:top; padding:2px; }
td { font-size:9pt; text-align:left; vertical-align:top; padding:2px; }
big { font-size:11pt; }
small { font-size:8pt; }
</style>
</head>

<body id="TheBody" class="hide">

<h1 align="center" id="TheTitle" style="margin-bottom:-20px;">WebDAV Redirector Settings</h1>
<div align="center">
<p style="margin-bottom:-20px;"><i><small><b>Note</b>: See <a target="_blank" href="http://go.microsoft.com/fwlink/?LinkId=324291">Using the WebDAV Redirector</a> for additional details.</small></i></p>
  <form>
    <center>
    <table border="0" cellpadding="2" cellspacing="2" style="width:600px;">
      <tr>
        <td style="width:600px;text-align:left"><fieldset title="Security Settings">
        <legend>&nbsp;<b>Security Settings</b>&nbsp;</legend>
        These values affect the security behavior for the WebDAV Redirector.<br>
        <table style="width:600px;">
          <tr title="Specifies whether the WebDAV Redirector can use Basic Authentication to communicate with a server.">
            <td style="width:300px">
            <table border="0">
              <tr>
                <td style="width:300px"><b>Basic Authentication Level</b></td>
              </tr>
              <tr>
                <td style="width:300px;"><span style="width:280px;padding-left:20px;"><small><i><b>Note</b>: Using basic authentication can cause <u>serious security issues</u> as the username and password are transmitted in clear text, therefore the use of basic authentication over WebDAV is disabled by default unless the connection is using SSL.</i></small></span></td>
              </tr>
            </table>
            </td>
            <td style="width:300px">
            <table style="width:300px">
              <tr>
                <td style="width:020px"><input class="radio" type="radio" value="0" name="BasicAuthLevel" onchange="VBScript:FlagChanges()" id="BasicAuthLevel0"></td>
                <td style="width:280px"><label for="BasicAuthLevel0">Basic Authentication is disabled</label></td>
              </tr>
              <tr>
                <td style="width:020px"><input class="radio" type="radio" value="1" checked name="BasicAuthLevel" onchange="VBScript:FlagChanges()" id="BasicAuthLevel1"></td>
                <td style="width:280px"><label for="BasicAuthLevel1">Basic Authentication is enabled for SSL web sites only</label></td>
              </tr>
              <tr>
                <td style="width:020px"><input class="radio" type="radio" value="2" name="BasicAuthLevel" onchange="VBScript:FlagChanges()" id="BasicAuthLevel2" onClick="VBScript:BasicAuthWarning()"></td>
                <td style="width:280px"><label for="BasicAuthLevel2">Basic Authentication is enabled for SSL and non-SSL web sites</label></td>
              </tr>
            </table>
            </td>
          </tr>
          <tr title="Specifies a list of local URLs for forwarding credentials that bypasses any proxy settings. (Note: This requires Windows Vista SP1 or later.)">
            <td style="width:300px">
            <table border="0">
              <tr>
                <td style="width:300px"><b>Authentication Forwarding Server List</b></td>
              </tr>
              <tr>
                <td style="width:300px;"><span style="width:280px;padding-left:20px;"><small><i><b>Note</b>: Include one server name per line.</i></small></span></td>
              </tr>
            </table>
            </td>
            <td style="width:300px"><textarea class="textarea" name="AuthForwardServerList" onchange="VBScript:FlagChanges()"></textarea></td>
          </tr>
          <tr title="Specifies whether the WebDAV Redirector supports locking.">
            <td style="width:300px"><b>Support for WebDAV Locking</b></td>
            <td style="width:300px">
            <table style="width:300px">
              <tr>
                <td style="width:020px"><input class="radio" type="radio" value="1" checked name="SupportLocking" onchange="VBScript:FlagChanges()" id="SupportLocking1"></td>
                <td style="width:280px"><label for="SupportLocking1">Enable Lock Support</label></td>
              </tr>
              <tr>
                <td style="width:020px"><input class="radio" type="radio" value="0" name="SupportLocking" onchange="VBScript:FlagChanges()" id="SupportLocking0"></td>
                <td style="width:280px"><label for="SupportLocking0">Disable Lock Support</label></td>
              </tr>
            </table>
            </td>
          </tr>
        </table>
        </fieldset> </td>
      </tr>
      <tr>
        <td style="width:600px;text-align:left"><fieldset title="Time-outs">
        <legend>&nbsp;<b>Time-outs and Maximum Sizes</b>&nbsp;</legend>
        These values affect the behavior for WebDAV Client/Server operations.<br>
        <table border="0" style="width:600px;">
          <tr title="Specifies the connection time-out for the WebDAV Redirector uses when communicating with non-local WebDAV servers.">
            <td style="width:300px"><b>Internet Server Time-out</b> <small>(In Seconds)</small></td>
            <td style="width:300px"><input class="textbox" type="text" name="InternetServerTimeoutInSec" onchange="VBScript:FlagChanges()" onblur="VBScript:Validate(Me)" value="30"></td>
          </tr>
          <tr title="Specifies the connection time-out for the WebDAV Redirector uses when communicating with a local WebDAV server.">
            <td style="width:300px"><b>Local Server Time-out</b> <small>(In Seconds)</small></td>
            <td style="width:300px"><input class="textbox" type="text" name="LocalServerTimeoutInSec" onchange="VBScript:FlagChanges()" onblur="VBScript:Validate(Me)" value="15"></td>
          </tr>
          <tr title="Specifies the time-out in seconds that the WebDAV Redirector uses after issuing a request.">
            <td style="width:300px"><b>Send/Receive Time-out</b> <small>(In Seconds)</small></td>
            <td style="width:300px"><input class="textbox" type="text" name="SendReceiveTimeoutInSec" onchange="VBScript:FlagChanges()" onblur="VBScript:Validate(Me)" value="60"></td>
          </tr>
          <tr title="Specifies the period of time that a server is cached as non-WebDAV by the WebDAV Redirector. If a server is found in this list, a fail is returned immediately without attempting to contact the server.">
            <td style="width:300px"><b>Server Not Found Cache Time-out</b> <small>(In Seconds)</small></td>
            <td style="width:300px"><input class="textbox" type="text" name="ServerNotFoundCacheLifeTimeInSec" onchange="VBScript:FlagChanges()" onblur="VBScript:Validate(Me)" value="60"></td>
          </tr>
          <tr title="Specifies the maximum size in bytes that the WebDAV Redirector allows for file transfers.">
            <td style="width:300px"><b>Maximum File Size</b> <small>(In Bytes)</small></td>
            <td style="width:300px"><input class="textbox" type="text" name="FileSizeLimitInBytes" onchange="VBScript:FlagChanges()" onblur="VBScript:Validate(Me)" value="50000000"></td>
          </tr>
          <tr title="Specifies the maximum size that is allowed by the WebDAV Redirector for all properties on a specific collection.">
            <td style="width:300px"><b>Maximum Attributes Size</b> <small>(In Bytes)</small></td>
            <td style="width:300px"><input class="textbox" type="text" name="FileAttributesLimitInBytes" onchange="VBScript:FlagChanges()" onblur="VBScript:Validate(Me)" value="1000000"></td>
          </tr>
        </table>
        </fieldset> </td>
      </tr>
      <tr>
        <td style="text-align:center">
        <table border="0">
          <tr>
            <td style="text-align:center"><input class="button" type="button" value="Apply Settings" onclick="VBScript:SetValues()">
            <td style="text-align:center"><input class="button" type="button" value="Reset Values" onclick="VBScript:ResetApplication()">
            <td style="text-align:center"><input class="button" type="button" value="Exit Application" onclick="VBScript:ExitApplication()">
          </tr>
        </table>
        </td>
      </tr>
    </table>
    </center>
  </form>
</div>

</body>

</html>
Additional Notes

As with the last version of this HTML Application, you will need to run this application as an administrator in order to save the settings to the registry and restart the WebDAV Redirector service.

Have fun! ;-]


Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/

How to Create a Blind Drop WebDAV Share

I had an interesting WebDAV question earlier today that I had not considered before: how can someone create a "Blind Drop Share" using WebDAV? By way of explanation, a Blind Drop Share is a path where users can copy files, but never see the files that are in the directory. You can setup something like this by using NTFS permissions, but that environment can be a little difficult to set up and maintain.

With that in mind, I decided to research a WebDAV-specific solution that didn't require mangling my NTFS permissions. In the end it was pretty easy to achieve, so I thought that it would make a good blog for anyone who wants to do this.

A Little Bit About WebDAV

NTFS permissions contain access controls that configure the directory-listing behavior for files and folders; if you modify those settings, you can control who can see files and folders when they connect to your shared resources. However, there are no built-in features for the WebDAV module which ships with IIS that will approximate the NTFS behavior. But that being said, there is an interesting WebDAV quirk that you can use that will allow you to restrict directory listings, and I will explain how that works.

WebDAV uses the PROPFIND command to retrieve the properties for files and folders, and the WebDAV Redirector will use the response from a PROPFIND command to display a directory listing. (Note: Official WebDAV terminology has no concept of files and folders, those physical objects are respectively called Resources and Collections in WebDAV jargon. But that being said, I will use files and folders throughout this blog post for ease of understanding.)

In any event, one of the HTTP headers that WebDAV uses with the PROPFIND command is the Depth header, which is used to specify how deep the folder/collection traversal should go:

  • If you sent a PROPFIND command for the root of your website with a Depth:0 header/value, you would get the properties for just the root directory - with no files listed; a Depth:0 header/value only retrieves properties for the single resource that you requested.
  • If you sent a PROPFIND command for the root of your website with a Depth:1 header/value, you would get the properties for every file and folder in the root of your website; a Depth:1 header/value retrieves properties for the resource that you requested and all siblings.
  • If you sent a PROPFIND command for the root of your website with a Depth:infinity header/value, you would get the properties for every file and folder in your entire website; a Depth:infinity header/value retrieves properties for every resource regardless of its depth in the hierarchy. (Note that retrieving directory listings with infinite depth are disabled by default in IIS 7 and IIS 8 because it can be CPU intensive.)

By analyzing the above information, it should be obvious that what you need to do is to restrict users to using a Depth:0 header/value. But that's where this scenario gets interesting: if your end-users are using the Windows WebDAV Redirector or other similar technology to map a drive to your HTTP website, you have no control over the value of the Depth header. So how can you restrict that?

In the past I would have written custom native-code HTTP module or ISAPI filter to modify the value of the Depth header; but once you understand how WebDAV works, you can use the URL Rewrite module to modify the headers of incoming HTTP requests to accomplish some pretty cool things - like modifying the values WebDAV-related HTTP headers.

Adding URL Rewrite Rules to Modify the WebDAV Depth Header

Here's how I configured URL Rewrite to set the value of the Depth header to 0, which allowed me to create a "Blind Drop" WebDAV site:

  1. Open the URL Rewrite feature in IIS Manager for your website.
    Click image to expand
  2. Click the Add Rules link in the Actionspane.
    Click image to expand
  3. When the Add Rules dialog box appears, highlight Blank rule and click OK.
    Click image to expand
  4. When the Edit Inbound Rulepage appears, configure the following settings:
    1. Name the rule "Modify Depth Header".
      Click image to expand
    2. In the Match URLsection:
      1. Choose Matches the Pattern in the Requested URL drop-down menu.
      2. Choose Wildcards in the Using drop-down menu.
      3. Type a single asterisk "*" in the Pattern text box.
      Click image to expand
    3. Expand the Server Variables collection and click Add.
      Click image to expand
    4. When the Set Server Variabledialog box appears:
      1. Type "HTTP_DEPTH" in the Server variable name text box.
      2. Type "0" in the Value text box.
      3. Make sure that Replace the existing value checkbox is checked.
      4. Click OK.
    5. In the Action group, choose None in the Action typedrop-down menu.
      Click image to expand
    6. Click Apply in the Actions pane, and then click Back to Rules.
      Click image to expand
  5. Click View Server Variables in the Actionspane.
    Click image to expand
  6. When the Allowed Server Variablespage appears, configure the following settings:
    1. Click Add in the Actionspane.
      Click image to expand
    2. When the Add Server Variabledialog box appears:
      1. Type "HTTP_DEPTH" in the Server variable name text box.
      2. Click OK.
    3. Click Back to Rules in the Actionspane.
      Click image to expand

If all of these changes were saved to your applicationHost.config file, the resulting XML might resemble the following example - with XML comments added by me to highlight some of the major sections:

<location path="Default Web Site">
    <system.webServer>
    
        <-- Start of Security Settings -->
        <security>
            <authentication>
                <anonymousAuthentication enabled="false" />
                <basicAuthentication enabled="true" />
            </authentication>
            <requestFiltering>
                <fileExtensions applyToWebDAV="false" />
                <verbs applyToWebDAV="false" />
                <hiddenSegments applyToWebDAV="false" />
            </requestFiltering>
        </security>
        
        <-- Start of WebDAV Settings -->
        <webdav>
            <authoringRules>
                <add roles="administrators" path="*" access="Read, Write, Source" />
            </authoringRules>
            <authoring enabled="true">
                <properties allowAnonymousPropfind="false" allowInfinitePropfindDepth="true">
                    <clear />
                    <add xmlNamespace="*" propertyStore="webdav_simple_prop" />
                </properties>
            </authoring>
        </webdav>
        
        <-- Start of URL Rewrite Settings -->
        <rewrite>
            <rules>
                <rule name="Modify Depth Header" enabled="true" patternSyntax="Wildcard">
                    <match url="*" />
                    <serverVariables>
                        <set name="HTTP_DEPTH" value="0" />
                    </serverVariables>
                    <action type="None" />
                </rule>
            </rules>
            <allowedServerVariables>
                <add name="HTTP_DEPTH" />
            </allowedServerVariables>
        </rewrite>
        
    </system.webServer>
</location>

In all likelihood, some of these settings will be stored in your applicationHost.config file, and the remaining settings will be stored in the web.config file of your website.

Testing the URL Rewrite Settings

If you did not have the URL Rewrite rule in place, or if you disabled the rule, then your web server might respond like the following example if you used the WebDAV Redirector to map a drive to your website from a command prompt:

C:\>net use z: http://www.contoso.com/
Enter the user name for 'www.contoso.com': www.contoso.com\robert
Enter the password for www.contoso.com:
The command completed successfully.

C:\>z:

Z:\>dir
Volume in drive Z has no label.
Volume Serial Number is 0000-0000

Directory of Z:\

09/16/2013 08:55 PM <DIR> .
09/16/2013 08:55 PM <DIR> ..
09/14/2013 12:39 AM <DIR> aspnet_client
09/16/2013 08:06 PM <DIR> scripts
09/16/2013 07:55 PM 66 default.aspx
09/14/2013 12:38 AM 98,757 iis-85.png
09/14/2013 12:38 AM 694 iisstart.htm
09/16/2013 08:55 PM 75 web.config
              4 File(s) 99,592 bytes
              8 Dir(s) 956,202,631,168 bytes free

Z:\>

However, when you have the URL Rewrite correctly configured and enabled, connecting to the same website will resemble the following example - notice how no files or folders are listed:

C:\>net use z: http://www.contoso.com/
Enter the user name for 'www.contoso.com': www.contoso.com\robert
Enter the password for www.contoso.com:
The command completed successfully.

C:\>z:

Z:\>dir
Volume in drive Z has no label.
Volume Serial Number is 0000-0000

Directory of Z:\

09/16/2013 08:55 PM <DIR> .
09/16/2013 08:55 PM <DIR> ..
              0 File(s) 0 bytes
              2 Dir(s) 956,202,803,200 bytes free

Z:\>

Despite the blank directory listing, you can still retrieve the properties for any file or folder if you know that it exists. So if you were to use the mapped drive from the preceding example, you could still use an explicit directory command for any object that you had uploaded or created:

Z:\>dir default.aspx
Volume in drive Z has no label.
Volume Serial Number is 0000-0000

Directory of Z:\

09/16/2013 07:55 PM 66 default.aspx
              1 File(s) 66 bytes
              0 Dir(s) 956,202,799,104 bytes free

Z:\>dir scripts
Volume in drive Z has no label.
Volume Serial Number is 0000-0000

Directory of Z:\scripts

09/16/2013 07:52 PM <DIR> .
09/16/2013 07:52 PM <DIR> ..
              0 File(s) 0 bytes
              2 Dir(s) 956,202,799,104 bytes free

Z:\>

The same is true for creating directories and files; you can create them, but they will not show up in the directory listings after you have created them unless you reference them explicitly:

Z:\>md foobar

Z:\>dir
Volume in drive Z has no label.
Volume Serial Number is 0000-0000

Directory of Z:\

09/16/2013 11:52 PM <DIR> .
09/16/2013 11:52 PM <DIR> ..
              0 File(s) 0 bytes
              2 Dir(s) 956,202,618,880 bytes free

Z:\>cd foobar

Z:\foobar>copy NUL foobar.txt
        1 file(s) copied.

Z:\foobar>dir
Volume in drive Z has no label.
Volume Serial Number is 0000-0000

Directory of Z:\foobar

09/16/2013 11:52 PM <DIR> .
09/16/2013 11:52 PM <DIR> ..
              0 File(s) 0 bytes
              2 Dir(s) 956,202,303,488 bytes free

Z:\foobar>dir foobar.txt
Volume in drive Z has no label.
Volume Serial Number is 0000-0000

Directory of Z:\foobar

09/16/2013 11:53 PM 0 foobar.txt
              1 File(s) 0 bytes
              0 Dir(s) 956,202,299,392 bytes free

Z:\foobar>

That wraps it up for today's post, although I should point out that if you see any errors when you are using the WebDAV Redirector, you should take a look at the Troubleshooting the WebDAV Redirector section of my Using the WebDAV Redirector article; I have done my best to list every error and resolution that I have discovered over the past several years.


Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/

What's New in IIS 8.5?

Microsoft released the Windows 8.1 Preview on June 26, but some of the articles that the IIS team was working on for IIS 8.5 needed a little more work before we published them. That being said, those articles were published on IIS.net earlier today, and here is a quick preview of those articles and the new features in IIS 8.5 which they describe:

  • Dynamic Website Activation in IIS 8.5
    This feature allows IIS to scale to an even larger set of websites than before by only loading websites after an HTTP request has been received; this significantly reduces the startup time and initial resource allocation for IIS, and it prevents resources from being unnecessarily consumed for websites that are accessed infrequently.
  • Enhanced Logging for IIS 8.5
    IIS now provides built-in functionality for adding custom logging fields to your W3C log files that are populated with values that are from request headers, response headers, or server variables. This functionality allows you to log a great deal of custom information about each HTTP request; for example, you could include the information an HTTP header like X-Forwarded-For, which would allow you to decode the request path through all firewalls/proxies, or you could insert information from a custom module into a server variable and have IIS add that information directly to your logs.
  • Idle Worker Process Page-Out in IIS 8.5
    In previous versions of IIS, inactive worker processes were terminated when they reached their recycle interval, but IIS 8.5 allows you to configure your application pools so that inactive worker processes are swapped out to the page file; since IIS can restore a suspended worker process into service much faster than starting up a new worker process, this functionality significantly improves the user experience when a request is received for a suspended application.
  • Logging to Event Tracing for Windows in IIS 8.5
    IIS 8.5 expands the logging capabilities for HTTP events to include Event Tracing for Windows (ETW); this functionality allows you monitor your IIS events in real-time, which can be extremely useful for troubleshooting and debugging/developing applications.

For examples of these technologies in action, you can watch one of the following presentations:

Special thanks go to Gary Ericson for putting together and publishing these articles on IIS.net, and a big round of thanks to the IIS team for adding all of these new features!

(One quick note in closing: functionality similar to IIS 8.5's Enhanced Logging was available in the past by installing separate modules, but IIS 8.5 now makes this functionality available out-of-the-box.)


Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/