Sending WebDAV Requests in .NET Revisited

I recently spoke with a great customer in India, and he was experimenting with the code from my Sending WebDAV Requests in .NET blog post. He had a need to send the WebDAV LOCK/UNLOCK commands, so I wrote a quick addition to the code in my original blog post to send those commands, and I thought that I'd share that code in an updated blog post.

Using WebDAV Locks

First of all, you may need to enable WebDAV locks on your server. To do so, follow the instructions in the following walkthrough:

How to Use WebDAV Locks
http://learn.iis.net/page.aspx/596/how-to-use-webdav-locks/

If you were writing a WebDAV client, sending the LOCK/UNLOCK commands would help to avoid two clients attempting to author the same resource. So if your WebDAV client was editing a file named "foo.txt", the flow of events would be something like the following:

  1. LOCK foo.txt
  2. GET foo.txt
  3. [make some changes to foo.txt]
  4. PUT foo.txt
  5. UNLOCK foo.txt

Description for the Sample Application

The updated code sample in this blog post shows how to send most of the common WebDAV requests using C# and common .NET libraries. In addition to adding the LOCK/UNLOCK commands to this version, I also changed the sample files to upload/download Classic ASP pages instead of text files; I did this so you can see that the WebDAV requests are correctly accessing the source code of the ASP pages instead of the translated output.

Having said that, I need to mention once again that I create more objects than are necessary for each section of the sample, which creates several intentional redundancies; I did this because I wanted to make each section somewhat self-sufficient, which helps you to copy and paste a little easier. I present the WebDAV methods the in the following order:

WebDAV MethodNotes
PUT This section of the sample writes a string as a text file to the destination server as "foobar1.asp". Sending a raw string is only one way of writing data to the server, in a more common scenario you would probably open a file using a steam object and write it to the destination. One thing to note in this section of the sample is the addition of the "Overwrite" header, which specifies that the destination file can be overwritten.
LOCK This section of the sample sends a WebDAV request to lock the "foobar1.asp" before downloading it with a GET request.
GET This section of the sample sends a WebDAV-specific form of the HTTP GET method to retrieve the source code for the destination URL. This is accomplished by sending the "Translate: F" header and value, which instructs IIS to send the source code instead of the processed URL. In this specific sample I am using Classic ASP, but if the requests were for ASP.NET or PHP files you would also need to specify the "Translate: F" header/value pair.
PUT This section of the sample sends an updated version of the "foobar1.asp" script to the server, which overwrites the original file. The purpose of this PUT command is to simulate creating a WebDAV client that can update files on the server.
GET This section of the sample retrieves the updated version of the "foobar1.asp" script from the server, just to show that the updated version was saved successfully.
UNLOCK This section of the sample uses the lock token from the earlier LOCK request to unlock the "foobar1.asp"
COPY This section of the sample copies the file from "foobar1.asp" to "foobar2.asp", and uses the "Overwrite" header to specify that the destination file can be overwritten. One thing to note in this section of the sample is the addition of the "Destination" header, which obviously specifies the destination URL. The value for this header can be a relative path or an FQDN, but it may not be an FQDN to a different server.
MOVE This section of the sample moves the file from "foobar2.asp" to "foobar1.asp", thereby replacing the original uploaded file. As with the previous two sections of the sample, this section of the sample uses the "Overwrite" and "Destination" headers.
DELETE This section of the sample deletes the original file, thereby removing the sample file from the destination server.
MKCOL This section of the sample creates a folder named "foobar3" on the destination server; as far as WebDAV on IIS is concerned, the MKCOL method is a lot like the old DOS MKDIR command.
DELETE This section of the sample deletes the folder from the destination server.

Source Code for the Sample Application

Here is the source code for the updated sample application:

using System;
using System.Net;
using System.IO;
using System.Text;

class WebDavTest
{
  static void Main(string[] args)
  {
    try
    {
      // Define the URLs.
      string szURL1 = @"http://localhost/foobar1.asp";
      string szURL2 = @"http://localhost/foobar2.asp";
      string szURL3 = @"http://localhost/foobar3";

      // Some sample code to put in an ASP file.
      string szAspCode1 = @"<%=Year()%>";
      string szAspCode2 = @"<%=Time()%>";

      // Some XML to put in a lock request.
      string szLockXml = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>" +
        "<D:lockinfo xmlns:D='DAV:'>" +
        "<D:lockscope><D:exclusive/></D:lockscope>" +
        "<D:locktype><D:write/></D:locktype>" +
        "<D:owner><D:href>mailto:someone@example.com</D:href></D:owner>" +
        "</D:lockinfo>";

      // Define username, password, and lock token strings.
      string szUsername  = @"username";
      string szPassword  = @"password";
      string szLockToken = null;

      // --------------- PUT REQUEST #1 --------------- //

      // Create an HTTP request for the URL.
      HttpWebRequest httpPutRequest1 =
         (HttpWebRequest)WebRequest.Create(szURL1);

      // Set up new credentials.
      httpPutRequest1.Credentials =
         new NetworkCredential(szUsername, szPassword);

      // Pre-authenticate the request.
      httpPutRequest1.PreAuthenticate = true;

      // Define the HTTP method.
      httpPutRequest1.Method = @"PUT";

      // Specify that overwriting the destination is allowed.
      httpPutRequest1.Headers.Add(@"Overwrite", @"T");

      // Specify the content length.
      httpPutRequest1.ContentLength = szAspCode1.Length;

      // Optional, but allows for larger files.
      httpPutRequest1.SendChunked = true;

      // Retrieve the request stream.
      Stream putRequestStream1 =
         httpPutRequest1.GetRequestStream();

      // Write the string to the destination as text bytes.
      putRequestStream1.Write(
         Encoding.UTF8.GetBytes((string)szAspCode1),
         0, szAspCode1.Length);

      // Close the request stream.
      putRequestStream1.Close();

      // Retrieve the response.
      HttpWebResponse httpPutResponse1 =
         (HttpWebResponse)httpPutRequest1.GetResponse();

      // Write the response status to the console.
      Console.WriteLine(@"PUT Response #1: {0}",
         httpPutResponse1.StatusDescription);

      // --------------- LOCK REQUEST --------------- //

      // Create an HTTP request for the URL.
      HttpWebRequest httpLockRequest =
         (HttpWebRequest)WebRequest.Create(szURL1);

      // Set up new credentials.
      httpLockRequest.Credentials =
         new NetworkCredential(szUsername, szPassword);

      // Pre-authenticate the request.
      httpLockRequest.PreAuthenticate = true;

      // Define the HTTP method.
      httpLockRequest.Method = @"LOCK";

      // Specify the request timeout.
      httpLockRequest.Headers.Add(@"Timeout", "Infinite");

      // Specify the request content type.
      httpLockRequest.ContentType = "text/xml; charset=\"utf-8\"";

      // Retrieve the request stream.
      Stream lockRequestStream =
         httpLockRequest.GetRequestStream();

      // Write the lock XML to the destination.
      lockRequestStream.Write(
         Encoding.UTF8.GetBytes((string)szLockXml),
         0, szLockXml.Length);

      // Close the request stream.
      lockRequestStream.Close();

      // Retrieve the response.
      HttpWebResponse httpLockResponse =
         (HttpWebResponse)httpLockRequest.GetResponse();

      // Retrieve the lock token for the request.
      szLockToken = httpLockResponse.GetResponseHeader("Lock-Token");

      // Write the response status to the console.
      Console.WriteLine(
         @"LOCK Response: {0}",
         httpLockResponse.StatusDescription);
      Console.WriteLine(
         @" LOCK Token: {0}",
         szLockToken);

      // --------------- GET REQUEST #1 --------------- //

      // Create an HTTP request for the URL.
      HttpWebRequest httpGetRequest1 =
         (HttpWebRequest)WebRequest.Create(szURL1);

      // Set up new credentials.
      httpGetRequest1.Credentials =
         new NetworkCredential(szUsername, szPassword);

      // Pre-authenticate the request.
      httpGetRequest1.PreAuthenticate = true;

      // Define the HTTP method.
      httpGetRequest1.Method = @"GET";

      // Specify the request for source code.
      httpGetRequest1.Headers.Add(@"Translate", "F");

      // Retrieve the response.
      HttpWebResponse httpGetResponse1 =
         (HttpWebResponse)httpGetRequest1.GetResponse();

      // Retrieve the response stream.
      Stream getResponseStream1 =
         httpGetResponse1.GetResponseStream();

      // Create a stream reader for the response.
      StreamReader getStreamReader1 =
         new StreamReader(getResponseStream1, Encoding.UTF8);

      // Write the response status to the console.
      Console.WriteLine(
         @"GET Response #1: {0}",
         httpGetResponse1.StatusDescription);
      Console.WriteLine(
         @" Response Length: {0}",
         httpGetResponse1.ContentLength);
      Console.WriteLine(
         @" Response Text: {0}",
         getStreamReader1.ReadToEnd());

      // Close the response streams.
      getStreamReader1.Close();
      getResponseStream1.Close();

      // --------------- PUT REQUEST #2 --------------- //

      // Create an HTTP request for the URL.
      HttpWebRequest httpPutRequest2 =
         (HttpWebRequest)WebRequest.Create(szURL1);

      // Set up new credentials.
      httpPutRequest2.Credentials =
         new NetworkCredential(szUsername, szPassword);

      // Pre-authenticate the request.
      httpPutRequest2.PreAuthenticate = true;

      // Define the HTTP method.
      httpPutRequest2.Method = @"PUT";

      // Specify that overwriting the destination is allowed.
      httpPutRequest2.Headers.Add(@"Overwrite", @"T");

      // Specify the lock token.
      httpPutRequest2.Headers.Add(@"If",
        String.Format(@"({0})",szLockToken));

      // Specify the content length.
      httpPutRequest2.ContentLength = szAspCode1.Length;

      // Optional, but allows for larger files.
      httpPutRequest2.SendChunked = true;

      // Retrieve the request stream.
      Stream putRequestStream2 =
         httpPutRequest2.GetRequestStream();

      // Write the string to the destination as a text file.
      putRequestStream2.Write(
         Encoding.UTF8.GetBytes((string)szAspCode2),
         0, szAspCode1.Length);

      // Close the request stream.
      putRequestStream2.Close();

      // Retrieve the response.
      HttpWebResponse httpPutResponse2 =
         (HttpWebResponse)httpPutRequest2.GetResponse();

      // Write the response status to the console.
      Console.WriteLine(@"PUT Response #2: {0}",
         httpPutResponse2.StatusDescription);

      // --------------- GET REQUEST #2 --------------- //

      // Create an HTTP request for the URL.
      HttpWebRequest httpGetRequest2 =
         (HttpWebRequest)WebRequest.Create(szURL1);

      // Set up new credentials.
      httpGetRequest2.Credentials =
         new NetworkCredential(szUsername, szPassword);

      // Pre-authenticate the request.
      httpGetRequest2.PreAuthenticate = true;

      // Define the HTTP method.
      httpGetRequest2.Method = @"GET";

      // Specify the request for source code.
      httpGetRequest2.Headers.Add(@"Translate", "F");

      // Retrieve the response.
      HttpWebResponse httpGetResponse2 =
         (HttpWebResponse)httpGetRequest2.GetResponse();

      // Retrieve the response stream.
      Stream getResponseStream2 =
         httpGetResponse2.GetResponseStream();

      // Create a stream reader for the response.
      StreamReader getStreamReader2 =
         new StreamReader(getResponseStream2, Encoding.UTF8);

      // Write the response status to the console.
      Console.WriteLine(
         @"GET Response #2: {0}",
         httpGetResponse2.StatusDescription);
      Console.WriteLine(
         @" Response Length: {0}",
         httpGetResponse2.ContentLength);
      Console.WriteLine(
         @" Response Text: {0}",
         getStreamReader2.ReadToEnd());

      // Close the response streams.
      getStreamReader2.Close();
      getResponseStream2.Close();
      
      // --------------- UNLOCK REQUEST --------------- //

      // Create an HTTP request for the URL.
      HttpWebRequest httpUnlockRequest =
         (HttpWebRequest)WebRequest.Create(szURL1);

      // Set up new credentials.
      httpUnlockRequest.Credentials = 
         new NetworkCredential(szUsername, szPassword);

      // Pre-authenticate the request.
      httpUnlockRequest.PreAuthenticate = true;

      // Define the HTTP method.
      httpUnlockRequest.Method = @"UNLOCK";

      // Specify the lock token.
      httpUnlockRequest.Headers.Add(@"Lock-Token", szLockToken);

      // Retrieve the response.
      HttpWebResponse httpUnlockResponse =
         (HttpWebResponse)httpUnlockRequest.GetResponse();

      // Write the response status to the console.
      Console.WriteLine(
         @"UNLOCK Response: {0}",
         httpUnlockResponse.StatusDescription);

      // --------------- COPY REQUEST --------------- //

      // Create an HTTP request for the URL.
      HttpWebRequest httpCopyRequest =
         (HttpWebRequest)WebRequest.Create(szURL1);

      // Set up new credentials.
      httpCopyRequest.Credentials =
         new NetworkCredential(szUsername, szPassword);

      // Pre-authenticate the request.
      httpCopyRequest.PreAuthenticate = true;

      // Define the HTTP method.
      httpCopyRequest.Method = @"COPY";

      // Specify the destination URL.
      httpCopyRequest.Headers.Add(@"Destination", szURL2);

      // Specify that overwriting the destination is allowed.
      httpCopyRequest.Headers.Add(@"Overwrite", @"T");

      // Retrieve the response.
      HttpWebResponse httpCopyResponse =
         (HttpWebResponse)httpCopyRequest.GetResponse();

      // Write the response status to the console.
      Console.WriteLine(@"COPY Response: {0}",
         httpCopyResponse.StatusDescription);

      // --------------- MOVE REQUEST --------------- //

      // Create an HTTP request for the URL.
      HttpWebRequest httpMoveRequest =
         (HttpWebRequest)WebRequest.Create(szURL2);

      // Set up new credentials.
      httpMoveRequest.Credentials =
         new NetworkCredential(szUsername, szPassword);

      // Pre-authenticate the request.
      httpMoveRequest.PreAuthenticate = true;

      // Define the HTTP method.
      httpMoveRequest.Method = @"MOVE";

      // Specify the destination URL.
      httpMoveRequest.Headers.Add(@"Destination", szURL1);

      // Specify that overwriting the destination is allowed.
      httpMoveRequest.Headers.Add(@"Overwrite", @"T");

      // Retrieve the response.
      HttpWebResponse httpMoveResponse =
         (HttpWebResponse)httpMoveRequest.GetResponse();

      // Write the response status to the console.
      Console.WriteLine(@"MOVE Response: {0}",
         httpMoveResponse.StatusDescription);

      // --------------- DELETE FILE REQUEST --------------- //

      // Create an HTTP request for the URL.
      HttpWebRequest httpDeleteFileRequest =
         (HttpWebRequest)WebRequest.Create(szURL1);

      // Set up new credentials.
      httpDeleteFileRequest.Credentials =
         new NetworkCredential(szUsername, szPassword);

      // Pre-authenticate the request.
      httpDeleteFileRequest.PreAuthenticate = true;

      // Define the HTTP method.
      httpDeleteFileRequest.Method = @"DELETE";

      // Retrieve the response.
      HttpWebResponse httpDeleteFileResponse =
         (HttpWebResponse)httpDeleteFileRequest.GetResponse();

      // Write the response status to the console.
      Console.WriteLine(@"DELETE File Response: {0}",
         httpDeleteFileResponse.StatusDescription);

      // --------------- MKCOL REQUEST --------------- //

      // Create an HTTP request for the URL.
      HttpWebRequest httpMkColRequest =
         (HttpWebRequest)WebRequest.Create(szURL3);

      // Set up new credentials.
      httpMkColRequest.Credentials =
         new NetworkCredential(szUsername, szPassword);

      // Pre-authenticate the request.
      httpMkColRequest.PreAuthenticate = true;

      // Define the HTTP method.
      httpMkColRequest.Method = @"MKCOL";

      // Retrieve the response.
      HttpWebResponse httpMkColResponse =
         (HttpWebResponse)httpMkColRequest.GetResponse();

      // Write the response status to the console.
      Console.WriteLine(@"MKCOL Response: {0}",
         httpMkColResponse.StatusDescription);

      // --------------- DELETE FOLDER REQUEST --------------- //

      // Create an HTTP request for the URL.
      HttpWebRequest httpDeleteFolderRequest =
         (HttpWebRequest)WebRequest.Create(szURL3);

      // Set up new credentials.
      httpDeleteFolderRequest.Credentials =
         new NetworkCredential(szUsername, szPassword);

      // Pre-authenticate the request.
      httpDeleteFolderRequest.PreAuthenticate = true;

      // Define the HTTP method.
      httpDeleteFolderRequest.Method = @"DELETE";

      // Retrieve the response.
      HttpWebResponse httpDeleteFolderResponse =
         (HttpWebResponse)httpDeleteFolderRequest.GetResponse();

      // Write the response status to the console.
      Console.WriteLine(@"DELETE Folder Response: {0}",
         httpDeleteFolderResponse.StatusDescription);
    }
    catch (Exception ex)
    {
      Console.WriteLine(ex.Message);
    }
  }
}

Running the Sample Application

When you run the code sample, if there are no errors you should see something like the following output:

PUT Response #1: Created
LOCK Response: OK
   LOCK Token: <opaquelocktoken:4e616d65-6f6e-6d65-6973-526f62657274.426f62526f636b73>
GET Response #1: OK
   Response Length: 11
   Response Text: <%=Year()%>
PUT Response #2: No Content
GET Response #2: OK
   Response Length: 11
   Response Text: <%=Time()%>
UNLOCK Response: No Content
COPY Response: Created
MOVE Response: No Content
DELETE File Response: OK
MKCOL Response: Created
DELETE Folder Response: OK

Press any key to continue . . .

If you looked at the IIS logs after running the sample application, you should see entries like the following example:

#Software: Microsoft Internet Information Services 7.5
#Version: 1.0
#Date: 2011-10-18 06:49:07
#Fields: date time s-ip cs-method cs-uri-stem cs-uri-query s-port cs-username c-ip sc-status sc-substatus sc-win32-status
2011-10-18 06:49:07 ::1 PUT /foobar1.asp - 80 - ::1 401 2 5
2011-10-18 06:49:07 ::1 PUT /foobar1.asp - 80 username ::1 201 0 0
2011-10-18 06:49:07 ::1 LOCK /foobar1.asp - 80 username ::1 200 0 0
2011-10-18 06:49:07 ::1 GET /foobar1.asp - 80 username ::1 200 0 0
2011-10-18 06:49:07 ::1 PUT /foobar1.asp - 80 username ::1 204 0 0
2011-10-18 06:49:07 ::1 GET /foobar1.asp - 80 username ::1 200 0 0
2011-10-18 06:49:07 ::1 UNLOCK /foobar1.asp - 80 username ::1 204 0 0
2011-10-18 06:49:07 ::1 COPY /foobar1.asp http://localhost/foobar2.asp 80 username ::1 201 0 0
2011-10-18 06:49:07 ::1 MOVE /foobar2.asp http://localhost/foobar1.asp 80 username ::1 204 0 0
2011-10-18 06:49:07 ::1 DELETE /foobar1.asp - 80 username ::1 200 0 0
2011-10-18 06:49:07 ::1 MKCOL /foobar3 - 80 username ::1 201 0 0
2011-10-18 06:49:07 ::1 DELETE /foobar3 - 80 username ::1 200 0 0

Closing Notes

Since the code sample cleans up after itself, you should not see any files or folders on the destination server when it has completed executing. To see the files and folders that are actually created and deleted on the destination server, you would need to step through the code in a debugger.

This updated version does not include examples of the WebDAV PROPPATCH/PROPFIND methods in this sample for the same reason that I did not do so in my previous blog - those commands require processing the XML responses, and that is outside the scope of what I wanted to do with this sample.

I hope this helps!


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

How to create an HTML Application to configure your WebDAV Redirector settings

I've mentioned in previous blog posts that I use the Windows WebDAV Redirector a lot. (And believe me, I use it a lot.) Having said that, there are a lot of registry settings that control how the Windows WebDAV Redirector operates, and I tend to tweak those settings fairly often.

I documented all of those registry settings in my Using the WebDAV Redirector walkthrough, but unfortunately there isn't a built-in interface for managing the settings. With that in mind, I decided to write my own user interface.

I knew that it would be pretty simple to create a basic Windows Form application that does everything, but my trouble is that I would want to share the code in a blog, and the steps create a Windows application are probably more than I would want to write in such a short space. So I decided to reach into my scripting past and create an HTML Application for Windows that configures all of the Windows WebDAV Redirector settings.

It should be noted, like everything else these days, that this code is provided as-is. ;-]

Using the HTML Application

When you run the application, it will present you with the following user interface, which allows you to configure most of the useful Windows WebDAV Redirector settings:

Creating the HTML Application

To create this HTML Application, 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, _
      "Exit Application")
    If intRetVal = vbYes Then
      Call SetValues()
    ElseIf intRetVal = vbCancel Then
      Exit Sub
    End If
  End If
  Self.close
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="https://docs.microsoft.com/iis/publish/using-webdav/using-the-webdav-redirector/">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="Exit Application" onclick="VBScript:ExitApplication()">
          </tr>
        </table>
        </td>
      </tr>
    </table>
    </center>
  </form>
</div>

</body>

</html>
Additional Notes

You will need to run this HTML Application as an administrator in order to save the settings and restart the Windows WebDAV Redirector. (Which is listed as the "WebClient" service in your Administrative Tools.)

This HTML Application performs basic validation for the numeric fields, and it prevents you from exiting the application when you have unsaved changes, but apart from that there's not much functionality other than setting and retrieving the registry values. How else can you get away with posting an application in a blog with only 500 lines of code and no compilation required? ;-]


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

Life after FPSE (Part 6)

In this latest installment on my series about configuring your server for hosting without the FrontPage Server Extensions (FPSE), I'd like to discuss a couple of WebDAV best practices that I like to use.

Blocking FPSE-related Folders with Request Filtering

In my How to Migrate FPSE Sites to WebDAV walkthough, I discuss the following FPSE-related folders:

Folder Notes
_fpclass Should contain publicly-available FrontPage code - but should be secured.
_private The FrontPage Server Extensions often keep sensitive data files in this folder, so it should be secured to prevent browsing.
_vti_bin This is the virtual directory for the FrontPage Server Extensions executables. This path is configured to allow executables to function, and since we are migrating sites to WebDAV it should be secured to prevent browsing.
_vti_cnf The FrontPage Server Extensions keep sensitive metadata files in this folder, so it should be deleted or secured to prevent browsing.
_vti_log The FrontPage Server Extensions keep author logs in this folder, so it should be deleted or secured to prevent browsing.
_vti_pvt This folder holds several files that contain various metadata for your website, and should be secured.
_vti_txt This folder contains the text indices and catalogs for the older FrontPage WAIS search. Since later versions of FrontPage only used Index Server, it is safe to delete this folder, but at the very least it should be secured to prevent browsing.
fpdb FrontPage keeps databases in this folder, so it should be secured to prevent browsing.

One of the actions that I usually take on my servers is to lock down all of these folders for my entire server using Request Filtering. To do so, open a command prompt and enter the following commands:

cd %WinDir%\System32\inetsrv

appcmd.exe set config -section:system.webServer/security/requestFiltering /+"hiddenSegments.[segment='_vti_cnf']" /commit:apphost

appcmd.exe set config -section:system.webServer/security/requestFiltering /+"hiddenSegments.[segment='_fpclass']" /commit:apphost

appcmd.exe set config -section:system.webServer/security/requestFiltering /+"hiddenSegments.[segment='_private']" /commit:apphost

appcmd.exe set config -section:system.webServer/security/requestFiltering /+"hiddenSegments.[segment='_vti_log']" /commit:apphost

appcmd.exe set config -section:system.webServer/security/requestFiltering /+"hiddenSegments.[segment='_vti_pvt']" /commit:apphost

appcmd.exe set config -section:system.webServer/security/requestFiltering /+"hiddenSegments.[segment='_vti_txt']" /commit:apphost

appcmd.exe set config -section:system.webServer/security/requestFiltering /+"hiddenSegments.[segment='fpdb']" /commit:apphost

Note: You should only enter the following commands if you are sure that you will not be using FPSE anywhere on your server!

cd %WinDir%\System32\inetsrv

appcmd.exe set config -section:system.webServer/security/requestFiltering /+"hiddenSegments.[segment='_vti_bin']" /commit:apphost

These settings will prevent any of the FPSE-related paths from being viewed over HTTP from a web browser; web clients will receive an HTTP Error 404.8 - Not Found message when they attempt to access those paths. But that being said - when you enable WebDAV for a website by using the Internet Information Services (IIS) Manager, it will configure the Request Filtering settings that enable WebDAV clients to access those paths through WebDAV requests, even though access from a web browser is still blocked. (All of this is made possible through the built-in integration between WebDAV and Request Filtering. ;-]) Enabling access to these folders over WebDAV is necessary if you are opening your website over a WebDAV-mapped drive while you are using authoring clients that do not have native WebDAV support, such as FrontPage or Visual Studio.

Two Sites are Better Than One

In part 4 of this blog series I discussed why I like to set up two websites when using WebDAV; as a quick review, here is the general idea for that environment:

  • The first website (e.g. www.example.com) is used for normal HTTP web browsing
  • The second website (e.g. authoring.example.com) is used for for WebDAV authoring

There is a list of several reasons in that blog post why using two sites that point to the same content can be beneficial, and I won't bother quoting that list in this blog post - you can view that information by looking at that post.

But that being said, one of the items that I mentioned in that list was using separate application pools for each website. For example:

  • The first application pool (e.g. www.example.com) is configured to use delegated configuration
  • The second application pool (e.g. authoring.example.com) is configured to ignore delegated configuration

This configuration helps alleviate problems from uploading invalid Web.config files that might otherwise prevent HTTP access to your website. By way of explanation, the WebDAV module attempts to validate Web.config files when they are uploaded over WebDAV - this is done to try and prevent crashing your HTTP functionality for a website and being unable to fix it. Here's what I mean by that: IIS 7 allows configuration settings to be delegated to Web.config files, but if there is a mistake in a Web.config file, IIS 7 will return an HTTP Error 500.19 - Internal Server Error message for all HTTP requests. Since WebDAV is HTTP-based, that means that you won't be able to fix the problem over WebDAV. (If the WebDAV module didn't perform any validation, that means that your website would become unusable and unrepairable if you had uploaded the bad Web.config file over WebDAV.) To help alleviate this, the WebDAV module performs a simple validation check to prevent uploading invalid Web.config files. But if you save an invalid Web.config file through some other means, like the local file system or over FTP, then you will have no way to repair the situation through WebDAV.

This leads us back to the idea that you can implement when you are using two websites - you can configure the application pool for the WebDAV-enabled website to ignore delegated configuration settings; so it doesn't matter if you have an invalid Web.config file - you will always be able to fix the problem over WebDAV. To configure an application pool to ignore delegated configuration settings, open a command prompt and enter the following commands:

cd %WinDir%\System32\inetsrv

appcmd.exe set config -section:system.applicationHost/applicationPools /[name='authoring.example.com'].enableConfigurationOverride:"False" /commit:apphost

Note: you need to update the highlighted section of that example with the name of your website, such as "Default Web Site," etc.

When you have two websites configured in this example and you have an invalid Web.config file that is causing the HTTP 500 error for the www.example.com website, you can still connect to authoring.example.com via WebDAV and fix the problem.

More Information

For additional information on the concepts that were discussing in this blog, see the following topics:

  • Life after FPSE (Part 4) - This blog post discusses a couple of WebDAV-related topics, and includes my list of additional reasons why configuring two websites when you are using WebDAV can be advantageous.
  • Adding Application Pools - This topic contains the detailed information about the enableConfigurationOverride setting for an application pool.
  • Using the WebDAV Redirector - This walkthrough discusses mapping drives to WebDAV-enabled websites.

I hope this helps. ;-]


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

IIS 6.0 WebDAV and Compound Document Format Files

We recently ran into a situation where a customer thought that they were seeing file corruption when they were transferring files from a Windows 7 client to their IIS 6.0 server using WebDAV. More specifically, the file sizes were increasing for several specific file types, and for obvious reasons the checksums for these files would not match for verification. Needless to say this situation caused a great deal of alarm on the WebDAV team when we heard about it - file corruption issues are simply unacceptable.

To alleviate any fears, I should tell you right up front that no corruption was actually taking place, and the increase in file size was easily explained once we discovered what was really going on. All of that being said, I thought that a detailed explanation of the scenario would make a great blog entry in case anyone else runs into the situation.

The Customer Scenario

First of all, the customer was copying installation files using a batch file over WebDAV; more specifically the batch file was copying a collection of MSI and MST files. After the batch file copied the files to the destination server it would call the command-line comp utility to compare the files. Each MSI and MST file that was copied would increase by a small number of bytes so the comparison would fail. The customer computed checksums for the files to troubleshoot the issue and found that the checksums for the files on the source and destination did not match. Armed with this knowledge the customer contacted Microsoft for support, and eventually I got involved and I explained what the situation was.

The Architecture Explanation

Windows has a type of file format called a Compound Document, and many Windows applications make use of this file format. For example, several Microsoft Office file formats prior to Office 2007 used a compound document format to store information.

A compound document file is somewhat analogous to a file-based database, or in some situations like a mini file system that is hosted inside another file system. In the case of an MST or MSI file these are both true: MST and MSI files store information in various database-style tables with rows and columns, and they also store files for installation.

With that in mind, here's a behind-the-scenes view of WebDAV in IIS 6.0:

The WebDAV protocol extension allows you to store information in "properties", and copying files over the WebDAV redirector stores several properties about a file when it sends the file to the server. If you were to examine a protocol trace for the WebDAV traffic between a Windows 7 client and an IIS server, you will see the PUT command for the document followed by several PROPPATCH commands for the properties.

IIS needs a way to store the properties for a file in a way where they will remain associated with the file in question, so the big question is - where do you store properties?

In IIS 7 we have a simple property provider that stores the properties in a file named "properties.dav," but for IIS 5.0 and IIS 6.0 WebDAV code we chose to write the properties in the compound document file format because there are lots of APIs for doing so. Here's the way that it works in IIS 5 and IIS 6.0:

  • If the file is already in the compound document file format, IIS simply adds the WebDAV properties to the existing file. This data will not be used by the application that created the file - it will only be used by WebDAV. This is exactly what the customer was seeing - the file size was increasing because the WebDAV properties were being added to the compound document.
  • For other files, WebDAV stores a compound document in an NTFS alternate data stream that is attached to the file. You will never see this additional data from any directory listing, and the file size doesn't change because it's in an alternate data stream.

So believe it or not, no harm is done by modifying a compound document file to store the WebDAV properties. Each application that wants to pull information from a compound document file simply asks for the data that it wants, so adding additional data to a compound document file in this scenario was essentially expected behavior. I know that this may seem counter-intuitive, but it's actually by design. ;-]

The Resolution

Once I was able to explain what was actually taking place, the customer was able to verify that their MST and MSI files still worked exactly as expected. Once again, no harm was done by adding the WebDAV properties to the compound document files.

You needn't take my word for this, you can easily verify this yourself. Here's a simple test: Word 2003 documents (*.DOC not *.DOCX) are in the compound document file format. So if you were to create a Word 2003 document and then copy that document to an IIS 6.0 server over WebDAV, you'll notice that the file size increases by several bytes. That being said, if you open the document in Word, you will see no corruption - the file contains the same data that you had originally entered.

I hope this helps. ;-]

Life after FPSE (Part 2)

Following up on my last blog post, today's blog post will discuss some of the highlights and pitfalls that I have seen while transitioning from using the FrontPage Server Extensions to publish web sites to WebDAV. It should be noted, of course, that FTP still works everywhere - e.g. Expression Web, FrontPage, Visual Studio, etc. As the Program Manager for both WebDAV and FTP in IIS I can honestly say that I love both technologies, but I'm understandably biased. <grin> That said, I'm quite partial to publishing over HTTP whenever possible, and Windows makes it easy to do because Windows ships with a built-in WebDAV redirector that enables you to map a drive to a web site that is using WebDAV.

To set the mood for today's blog, let's have a little fun at FPSE's expense...

No-FPSE

A Few Notes about Migrating FPSE Web Sites to WebDAV and Backwards Compatibility

To start things off, I wrote a detailed walkthrough with instructions regarding how to migrate a site that is using FPSE to WebDAV that is located at the following URL:

Migrating FPSE Sites to WebDAV
http://go.microsoft.com/fwlink/?LinkId=108347

I wrote that walkthrough from the point-of-view that you might want to preserve the FPSE-related metadata in order to open your web site using a tool like Visual Studio or FrontPage. Neither of these tools have native WebDAV support, so you have to map a drive to a WebDAV-enabled web site in order to use those tools, and the instructions in that walkthrough will lead you through the steps to make the FrontPage-related metadata available to those applications over WebDAV.

The part of that walkthrough that makes backwards compatibility work is where I discuss adding settings for the IIS 7 Request Filtering feature so that FPSE-related metadata files are blocked from normal HTTP requests, but still available to WebDAV. (These metadata settings are all kept in the folders with names like _vti_cnf, _vti_pvt, etc.)

It should be noted, however, that if you are not interested in backwards compatibility, the steps are much simpler. In Step 1 of the walkthrough, you would choose "Full Uninstall" as the removal option, and all of your _vti_nnn folders will be deleted. If you've already removed FPSE from a web site and you chose the "Uninstall" option, you can remove the _vti_nnn folders from your site by saving the following batch file as "_vti_rmv.cmd" in the root folder of you web site and then running it:

dir /ad /b /s _vti_???>_vti_rmv.txt

for /f "delims=;" %%i in (_vti_rmv.txt) do rd /q /s "%%i"

del _vti_rmv.txt

It's worth noting, of course, that this batch file can be pretty disastrous if run in the wrong web site, as FPSE will no longer be able to access any of the metadata that defined your web site. Any content stored in folders like _private, fpdb, _overlay, etc., will all be preserved.

Getting to Know the WebDAV Redirector

Windows Vista and Windows Server 2008 both ship a first-class director, making it easy to use WebDAV sites across the Internet as though they were local shares. Using the WebDAV director is as intuitive as mapping a drive to any UNC share, you just specify the drive letter and the destination URL:

MapDrive

If you prefer, you can also use the command-line to map a drive to a WebDAV site:

net use * http://www.example.com/

Enter the user name for 'www.example.com': msbob

Enter the password for www.example.com: ******

Drive Z: is now connected to http://www.example.com/.

The command completed successfully.

Rather than repeat myself any more than necessary, I wrote the following walkthrough for anyone that plans on using the WebDAV redirector:

That walkthrough discusses how to install the redirector if necessary, how to map drives to WebDAV sites, and how to troubleshoot any problems that you might see.

Microsoft Expression Web - Using a WebDAV-enabled Editor

One of my favorite publishing features in Expression Web is that it has native WebDAV support built-in, so it doesn't have a dependency on the WebDAV redirector in order to work with a WebDAV-enabled web site. If you're currently using Expression Web to open a web site using FPSE, the change to WebDAV should be fairly seamless. If you're currently using FrontPage, the Expression Web team has put together a whitepaper that describes the differences between FrontPage and Expression Web, which is available from the following link:

That being said, when opening a WebDAV web site in Expression Web, you simply enter the HTTP URL the same way that you would if you were opening a site using FPSE:

Expression-Web-Open-Site

When you first open a web site using WebDAV, Expression Web will prompt you whether to edit the web site live, or edit locally and publish your changes later:

Expression-Web-Edit-Live

Once your live web site is opened, the WebDAV editing experience is what you would have expected from using FPSE:

Expression-Web-Edit-Page

Summary

So in closing, I've presented a few things to consider when working with WebDAV instead of FPSE. Using the WebDAV redirector makes working with WebDAV sites as easy as working with network shares, and using Expression Web is by far the easiest way to edit WebDAV sites.

Life after FPSE (Part 1)

Today's blog post will be the first in a series of blog posts that I intend to write about my experiences with putting together a Windows Server 2008 machine without using the FrontPage Server Extensions (FPSE) for any web publishing. The main goal of this series is to describe some of the highlights and pitfalls that I have run into while transitioning away from FPSE.

Over the years I've seen the users of FPSE broken down into two groups: those that love FPSE and those that hate FPSE. So before anyone thinks that I fall into the category of people that hate FPSE, in this first part of the series I will explain a brief bit of my history with FPSE.

My Personal Background with FPSE

In late 1995, Microsoft bought a little-known Massachusetts-based company named Vermeer Technologies, Inc., which really only had one product: FrontPage 1.0. (Incidentally, that's where all of those _vti_nnn folders that FPSE uses come from: the "vti" stands for Vermeer Technologies, Inc.)

frontpage-1.0

FrontPage was quickly transitioned into the Microsoft array of Office products, and Microsoft realized that they needed someone to support it. With that in mind, four of my coworkers and I started a FrontPage support team in Microsoft's Product Support Services (PSS) organization. The following photo shows what the five of us looked like "way back when..."

robmcm-frontpage-startup-team

Note: My hair is much shorter now. Open-mouthed smile

The First Microsoft FrontPage Version

Back then we supported both the FrontPage client and FPSE. Two coworkers specialized on what was then a small array of Windows-based servers (WebSite, Netscape, etc.), while another coworker and I specialized on the wider array of Unix-based servers, (NCSA, CERN, Netscape, Apache, etc.) At first FPSE was 100% CGI-based, but Microsoft soon released Internet Information Services (IIS) 1.0 for Windows NT Server 3.51 and we provided an ISAPI version of FPSE when FrontPage 1.1 was released in early 1996. In either case, FPSE was often somewhat difficult to configure, so a couple of my coworkers and I used to spend our free time searching the Internet looking for servers that were using FPSE incorrectly, then we would call them and offer to help fix their web sites for free. (Support was different back then, wasn't it? Open-mouthed smile)

frontpage-1.1Windows-NT-Server-v3.51-boxshot

FrontPage Gains Popularity

Industry acceptance for FrontPage and FPSE grew rapidly through the releases of FrontPage 97 and FrontPage 98. During that same time period Microsoft released IIS 2.0 through IIS 4.0 for Windows NT Server 4.0, where I switched from supporting the FrontPage client and refocused my career to work exclusively with FPSE and IIS. Over a short period of time a couple of coworkers and I became the escalation point for all the really difficult FPSE cases - so chances are good that if you were using FPSE back then and you had a problem then one us us probably helped you fix it. Sometime around this period Microsoft decided to scrap internal development for the Unix version of FPSE, so Microsoft contracted Ready-to-Run Software, Inc., to port FPSE to Unix.

frontpage-97frontpage-98Windows-NT-Server-v4.0-boxshot

FrontPage Grows Up

The next couple of years saw the releases of FrontPage 2000, FrontPage 2002, and FrontPage 2003, where FrontPage did its best to move away from a simple web authoring tool into more of a feature-rich developer tool. During that same time period Microsoft released IIS 5.0 for Windows Server 2000 and IIS 6.0 for Windows Server 2003. Through all of these releases I slowly transitioned from an escalation team member to writing content, where I wrote or edited hundreds of Knowledge Base articles about FPSE and IIS for Microsoft's support web site. I also worked extensively with several members of the IIS product team in order to get some much-needed changes into FTP and WebDAV.

frontpage-2000frontpage-2002frontpage-2003
Windows-2008-Server-Box-ArtWindows-Server-2003-Standard-Edition-angled-boxWindows-2008-Server-Box-Art

What was interesting about the release of FrontPage 2003 is that Microsoft did not release a version of FPSE to coincide with that release. This decision was based on the fact that the product team that was responsible for FPSE was also responsible for SharePoint, and they decided to drop FPSE as a separate product in favor of SharePoint. What this meant to FPSE end users was - FPSE was being slowly phased out in favor of SharePoint, or in favor of competing publishing technologies like WebDAV or FTP.

The End of FrontPage

After the release of IIS 6.0 I accepted a position as an SDK writer on the IIS User Education team, and a short time later I found out that the Product Unit Manager for IIS, Bill Staples, was looking for someone to take over web publishing in IIS 7.0 on Windows Server 2008. Bill and I had already had several discussions on the subject so I volunteered for the position, and for the last few years my life has been happily consumed with shipping FPSE, FTP, and WebDAV for Windows Server 2008.

During this same time period, however, Microsoft ended the line of FrontPage products; the team responsible for the FrontPage client splintered into the groups that now make the SharePoint Designer and Expression Web products, and the FPSE product team was now focusing exclusively on SharePoint. This situation meant that no one that worked on FPSE in the past was available to work on a new version for Windows Vista or Windows Server 2008, which left FPSE users in a predicament if they wanted to upgrade their operating systems. With this in mind, the IIS product team decided to contract Ready-to-Run Software, Inc. again in order to port the Windows version of FPSE to Windows Vista and Windows Server 2008. Even then, though, FPSE's days are numbered.

Summary

So, the short end to this long story is that I've been around the FrontPage Server Extensions in one way or another ever since the very beginning, and I've seen the good, the bad, and the ugly.

In my next post, I'll discuss using WebDAV instead of FPSE.


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

WebDAV Module for Windows Server 2008 GoLive Beta is released

Earlier today the IIS product team released the GoLive beta version of the new WebDAV extension module for IIS 7. (This version is currently available for Windows Server 2008 only.)

Listed below are the links for the download pages for each of the individual installation packages:

We've loaded this version with many great new features such as:

  • Integration with IIS 7.0: The new WebDAV extension module is fully integrated with the new IIS 7.0 administration interface and configuration store.
  • Per-site Configuration: WebDAV can be enabled at the site-level on IIS 7.0, which differed from IIS 6.0 where WebDAV was enabled at the server-level through a Web Service Extension.
  • Per-URL Security: WebDAV-specific security is implemented through WebDAV authoring rules that are configured on a per-URL basis.

Here are a couple of screenshots of the new WebDAV UI in action:

WebDAV UI WebDAV Authoring Rules

Additional documentation about installing and using this version of WebDAV can be found at the following URL:

Installing and Configuring WebDAV on IIS 7.0:
http://go.microsoft.com/fwlink/?LinkId=105146

While this release is a beta version and not technically supported, feedback about this release and requests for information can be posted to the following web forum:

IIS7 - Publishing:
http://forums.iis.net/1045.aspx

I would be remiss if I did not mention that special thanks go to:

  • Keith – for building it
  • Eok, Sriram, Ciprian – for testing it
  • Gurpreet, Brian, Reagan – for making it look pretty
  • Vijay, Will, Taylor – for helping keep everything on track ;-]

Thanks!


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