Creating Class 2 WebDAV Server

In addition to features provided by Class 1 server, Class 2 server supports hierarchy items locking, that prevents concurrent modifications of items by several users.

Most WebDAV clients, such as Microsoft Miniredirector/Web Folders, Mac OS X Finder and Microsoft Office require Class 2 server. If they discover a Class 1 server they will treat it as read-only.

To create a Class 2 server, you must implement ILockAsync interface on your folder and file items. When ILockAsync interface is implemented on a folder item your server will report a Class 2 compliance.

The class diagram for ILockAsync interface

Note that locking has nothing to do with authentication and authorization and does not protect from unauthorized access.

How WebDAV Clients Discovers Class 2 Compliance

When discovering your WebDAV server compliance, WebDAV clients rely on DAV header returned with OPTIONS request that is usually sent to a folder item. After you implement ILockAsync interface on an item, the server will respond with DAV: 1, 2, 3 header, meaning the item supports locking. It will also return LOCK and UNLOCK verbs in Allow and Public headers. If you need to verify if your server reports Class 2 compliance, you can check the DAV header of OPTIONS request in your WebDAV server log or you can examine the traffic using HTTP debugging proxy (for example with Fiddler tool).

As you can see the server also reports Class 3 compliance. The Class 3 is reported both for Class 1 and Class 2 server, regardless of ILock implementation.

How Locking Works

When a WebDAV client requires to protect an item from modifications, that could be made by other users, it locks the item (usually file), submitting LOCK request to the server. The server generates the new lock token, marks the item as locked and returns the lock token to the client. The WebDAV client application keeps the lock token and when it requires to perform any updates, it supplies the lock token with the request. When the server receives the update request, it verifies that the lock token belongs to the item that is being updated and performs modifications. The diagram below illustrates the whole process:

In response to WebDAV lock request the server generates and returns lock-token to client. This lock token is then used when file is being updated

The actual sequence of requests depends on the operation performed and varies between different WebDAV clients. But to give you an idea of what WebDAV requests are being sent, below is a typical sequence of requests for average WebDAV client for file upload:

  1. Verify if a file with such name exists. 
  2. Create a zero-length file. 
  3. Lock the newly created file, get the lock token generated by the server and save it on a client. 
  4. Upload content, specifying lock-token. 
  5. Unlock the file.

In addition, the client could submit more requests, for getting and updating file properties, creating temporary files, verifying if a file was actually created, listing folder contents, etc.

Note that while you have to add ILockAsync interface on folder items to report server compliance, most WebDAV clients, including Microsoft Web Folders/mini redirector, Mac OS X Finder, Microsoft Office and OpenOffice, never lock folder items, so on folders you can leave the implementation blank.

Using the ASP.NET WebDAV Application Wizard to Create a Class 2 Server

Now, let's create a new WebDAV server using ASP.NET WebDAV Server Application wizard. Select 'Store files and metadata in Microsoft SQL Database' on the first step. For the sake of simplicity, we recommend Microsoft SQL Express to be installed on your machine, otherwise you will need to create the database from SQL script generated by Wizard and update the connection string in web.config.

You can go through the Wizard leaving all other options in a default state.

Important! We recommend developing your project using IIS 7.x or later version, or IIS Express. Due to limitations of Visual Studio Development Server we do not recommend it for development purposes.

Now switch your project to IIS Express selecting 'Use IIS Express...' in project context menu. If you do not see this option, probably the IIS Express is not installed on your machine. Finally, run the project from Visual Studio to access and test it with WebDAV clients.

To get an idea of what information is submitted with each lock request, let's examine the database schema created by the Wizard. The Microsoft SQL Express database file, WebDav.mdf, is located in \App_Data\WebDav\DB\ folder (to see it in Visual Studio Solution Explorer, you should check the 'Show All Files' button in a Solution Explorer toolbar). You can open this file in Visual Studio Server Explorer double clicking on it and generate the database diagram.

When the lock is requested, the WebDAV client specifies several parameters with the lock, you can see them in the Lock table:

  1. Shared. Specifies if the lock is shared or exclusive.
  2. Deep. Specifies if the lock applied only to this item or to the entire subtree.
  3. Expires. Stores time in UTC when lock expires.
  4. Owner. Name of the user that applies lock.

We will describe each parameter below with more details.

In your project folder, you can also find the DB.sql, that contains all commands that were used to create the database and populate it with the sample data. You can use this SQL to deploy / move the database to another Microsoft SQL Server instance.

Locking the Item

The ILockAsync interface provides the means for locking the hierarchy item, updating lock timeout and getting the list of applied locks. When WebDAV client issues lock request WebDAV Engine calls ILockAsync.LockAsync method passing information about requested lock. In your ILockAsync.LockAsync method implementation you must do the following:

  1. Generate the new lock token, usually GUID.
  2. Save information about the lock in a storage.
  3. Associate the lock with the item in the repository on which ILock.Lock() is called.
  4. Return the lock token to the Engine.

Optionally in your ILockAsync.LockAsync implementation you can modify the lock timeout requested by the client. For example instead of infinity lock you can set lock for some limited time. You must return both lock token and lock timeout via LockResult return parameter. The engine then sends the lock token and timeout values back to WebDAV client.

public LockResult Lock(LockLevel level, bool isDeep, TimeSpan? timeout, string owner)
{
    if (ItemHasLock(level == LockLevel.Shared))
    {
        throw new LockedException();
    }
 
    if (isDeep)
    {
        // check if no items are locked in this subtree
        FindLocksDown(this, level == LockLevel.Shared);
    }
 
    if (!timeout.HasValue || timeout == TimeSpan.MaxValue)
    {
        // If timeout is absent or infinite timeout requested,
        // grant 5 minute lock.
        timeout = TimeSpan.FromMinutes(5);
    }
 
    // We store an expiration time in UTC. If server/database is moved 
    // to another time zone the locks expiration time is always correct.
    DateTime expires = DateTime.UtcNow + timeout.Value;
 
    string token = Guid.NewGuid().ToString();
    string insertLockCommand = 
        @"INSERT INTO Lock (ItemID, Token, Shared, Deep, Expires, Owner)
            VALUES(@ItemID, @Token, @Shared, @Deep, @Expires, @Owner)";
 
    Context.ExecuteNonQuery(       
      insertLockCommand,         
      "@ItemID", ItemId,         
      "@Token", token,        
      "@Shared", level == LockLevel.Shared,       
      "@Deep", isDeep,   
      "@Expires", expires,   
      "@Owner", owner);    
   
    return new LockResult(token, timeout.Value); 
}

Lock Parameters

The level argument specifies if client is requesting an exclusive or shared lock. If a user sets an exclusive lock other users will not be able to set any locks. If a user sets shared lock other users will be able to set only shared lock on the item. There could be only 1 exclusive lock set on an item or it can have 1 or more shared locks. If the item is locked and the new lock cannot be applied you can throw LockedException, this will inform the client about lock conflict on the server.

The isDeep argument specifies if the lock should be applied only to this item or to the entire subtree. In case of deep lock (no matter shared or exclusive) you must verify if no exclusive lock is applied to any child item. In case of deep exclusive lock you must verify if no shared lock is applied to any child item. If any lock conflicts occurs, to provide WebDAV client with information about locked children, you can throw MultistatusException with information about each locked child item.

The requestedTimeOut argument specifies the time when lock expires. The lock must be automatically removed by the server after amount of time specified in this parameter are elapsed. TimeSpan.MaxValue value means infinite lock, that should not be removed automatically. You can set the timeout that is different from the one requested by the client. The actual timeout that you apply must be returned to the framework via LockResult return value together with the generated lock-token. The WebDAV framework will then return the updated timeout to WebDAV client.

The owner argument specifies the name of the user applying the lock. Note that this is only an informational sting provided by WebDAV client and it cannot be used for security purposes. To get the name of authenticated user you must use other mechanisms.

Refreshing Lock

The WebDAV client can change the lock timeout at later times. In this case framework will call ILockAsync.RefreshLockAsync method providing the lock-token and new timeout value. Again in this method you can set the timeout that is different from the one requested by client.

Listing Locks

The WebDAV client application can request the list of locks applied to the item. In this case framework calls ILockAsync.GetActiveLocksAsync method:

public IEnumerable<LockInfo> GetActiveLocks()
{
    Guid itemID = ItemId;
    List<LockInfo> l = new List<LockInfo>();
 
    l.AddRange(GetLocks(itemID, false)); // get all locks
    while (true)
    {
        itemID = Context.ExecuteScalar<Guid>(
            "SELECT ParentItemId FROM Item WHERE ItemId = @ItemId",
            "@ItemId", itemID);
 
        if (itemID == Guid.Empty)
        {
            break;
        }
 
        l.AddRange(GetLocks(itemID, true)); // get only deep locks
    }
 
    return l;
}

Updating the Locked Item

When a WebDAV client is updating a locked item it sends to the server the list of lock tokens. You can access these tokens on the server side via DavRequest.ClientLockTokens property. In your WebDAV server Class 2 implementation, before modifying any locked items, you must check if WebDAV client provided necessary lock-token for this item. If no valid lock-token was provided you must throw LockedException.

public void CreateFolder(string name)
{
    if (!ClientHasToken)
    {
        throw new LockedException();
    }
 
    createChild(name, ItemType.Folder);
}

...

internal bool ClientHasToken
{
    get
    {
        List<LockInfo> itemLocks = GetActiveLocks().ToList();
        if (itemLocks.Count == 0)
        {
            return true;
        }
 
        IList<string> clientLockTokens = Context.Request.ClientLockTokens;
        return itemLocks.Where(il => clientLockTokens.Contains(il.Token)).Any();
    }
}