[Spice-devel] [WIP spice-gtk 4/4] WIP: webdav channel

Marc-André Lureau marcandre.lureau at gmail.com
Sun Jan 12 09:40:26 PST 2014


From: Marc-André Lureau <marcandre.lureau at redhat.com>

See spice-common for protocol details.

The webdav channel is reponsible for handling port events and
multiplexing the request streams. Extra care has been made to avoid
blocking and to enable some fairness between concurrent streams, however
this has been particularly tricky and is likely to have some issues
left.

The webdav server is run in a seperate thread, using libsoup. The client
communication is done via a local tcp socket (in the future, libsoup
could learn to use unix sockets or perhaps GIOStreams
directly). So currently, the webdav server will be "public" !

Only the current directory is exported atm. Suggestions on what to share
are welcome (~/Desktop ? multiple directories ? ~/Public ?).

There is a stand-alone server for testing, it can be run to export a
particualr directory: 'webdav-server -P ~/tmp'.

This implementation hasn't been tested on Windows and is likely to not
compile and be incomplete. Also, it relies on xattr which are not
provided equally by all filesystems.
---
 configure.ac               |    5 +
 gtk/Makefile.am            |   17 +-
 gtk/channel-webdav.c       |  582 +++++++++
 gtk/channel-webdav.h       |   82 ++
 gtk/map-file               |    1 +
 gtk/spice-channel.c        |    6 +
 gtk/spice-client.h         |    1 +
 gtk/spice-glib-sym-file    |    2 +
 gtk/spice-gtk-session.c    |    1 -
 gtk/spice-session-priv.h   |    2 +
 gtk/spice-session.c        |    4 +-
 gtk/webdav/Makefile.am     |   43 +
 gtk/webdav/guuid.c         |  471 ++++++++
 gtk/webdav/guuid.h         |  105 ++
 gtk/webdav/test-start      |  141 +++
 gtk/webdav/webdav-server.c |  197 +++
 gtk/webdav/webdav.c        | 2855 ++++++++++++++++++++++++++++++++++++++++++++
 gtk/webdav/webdav.h        |   44 +
 spice-common               |    2 +-
 19 files changed, 4554 insertions(+), 7 deletions(-)
 create mode 100644 gtk/channel-webdav.c
 create mode 100644 gtk/channel-webdav.h
 create mode 100644 gtk/webdav/Makefile.am
 create mode 100644 gtk/webdav/guuid.c
 create mode 100644 gtk/webdav/guuid.h
 create mode 100755 gtk/webdav/test-start
 create mode 100644 gtk/webdav/webdav-server.c
 create mode 100644 gtk/webdav/webdav.c
 create mode 100644 gtk/webdav/webdav.h

diff --git a/configure.ac b/configure.ac
index dccc231..5406f9c 100644
--- a/configure.ac
+++ b/configure.ac
@@ -247,6 +247,10 @@ PKG_CHECK_MODULES(GIO, gio-2.0 >= 2.10.0 $gio_os)
 AC_SUBST(GIO_CFLAGS)
 AC_SUBST(GIO_LIBS)
 
+PKG_CHECK_MODULES(SOUP, libsoup-2.4 libxml-2.0)
+AC_SUBST(SOUP_CFLAGS)
+AC_SUBST(SOUP_LIBS)
+
 PKG_CHECK_EXISTS([gio-2.0 >= 2.26], [have_gproxy=yes])
 AM_CONDITIONAL([WITH_GPROXY], [test "x$have_gproxy" = "xyes"])
 
@@ -703,6 +707,7 @@ data/spicy.desktop.in
 data/spicy.nsis
 po/Makefile.in
 gtk/Makefile
+gtk/webdav/Makefile
 gtk/controller/Makefile
 doc/Makefile
 doc/reference/Makefile
diff --git a/gtk/Makefile.am b/gtk/Makefile.am
index 7ceb22f..d8bfadc 100644
--- a/gtk/Makefile.am
+++ b/gtk/Makefile.am
@@ -1,6 +1,6 @@
 NULL =
 
-SUBDIRS =
+SUBDIRS = webdav
 
 if WITH_CONTROLLER
 SUBDIRS += controller
@@ -182,8 +182,9 @@ libspice_client_glib_2_0_la_LDFLAGS =	\
 libspice_client_glib_2_0_la_LIBADD =					\
 	$(top_builddir)/spice-common/common/libspice-common.la		\
 	$(top_builddir)/spice-common/common/libspice-common-client.la	\
+	webdav/libwebdav.la						\
 	$(GLIB2_LIBS)							\
-	$(GIO_LIBS)							\
+	$(SOUP_LIBS)							\
 	$(GOBJECT2_LIBS)						\
 	$(CELT051_LIBS)							\
 	$(OPUS_LIBS)							\
@@ -233,6 +234,7 @@ libspice_client_glib_2_0_la_SOURCES =			\
 	gio-coroutine.h					\
 							\
 	channel-base.c					\
+	channel-webdav.c				\
 	channel-cursor.c				\
 	channel-display.c				\
 	channel-display-priv.h				\
@@ -451,11 +453,19 @@ spice-marshal.c: spice-marshal.txt
 spice-marshal.h: spice-marshal.txt
 	$(AM_V_GEN)glib-genmarshal --header $< > $@ || (rm -f $@ && exit 1)
 
-spice-glib-enums.c: spice-channel.h channel-inputs.h spice-session.h
+SPICE_GLIB_ENUMS_FILES =			\
+	channel-webdav.h			\
+	channel-inputs.h			\
+	spice-channel.h				\
+	spice-session.h				\
+	$(NULL)
+
+spice-glib-enums.c: $(SPICE_GLIB_ENUMS_FILES)
 	$(AM_V_GEN)glib-mkenums --fhead "#include <glib-object.h>\n" \
 			--fhead "#include \"spice-glib-enums.h\"\n\n" \
 			--fprod "\n#include \"spice-session.h\"\n" \
 			--fprod "\n#include \"spice-channel.h\"\n" \
+			--fprod "\n#include \"channel-webdav.h\"\n" \
 			--fprod "\n#include \"channel-inputs.h\"\n" \
 			--vhead "static const G at Type@Value _ at enum_name@_values[] = {" \
 			--vprod "  { @VALUENAME@, \"@VALUENAME@\", \"@valuenick@\" }," \
@@ -593,6 +603,7 @@ glib_introspection_files =				\
 	spice-glib-enums.c				\
 	spice-option.c					\
 	spice-util.c					\
+	channel-webdav.c				\
 	channel-cursor.c				\
 	channel-display.c				\
 	channel-inputs.c				\
diff --git a/gtk/channel-webdav.c b/gtk/channel-webdav.c
new file mode 100644
index 0000000..664714d
--- /dev/null
+++ b/gtk/channel-webdav.c
@@ -0,0 +1,582 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+   Copyright (C) 2013 Red Hat, Inc.
+
+   This library is free software; you can redistribute it and/or
+   modify it under the terms of the GNU Lesser General Public
+   License as published by the Free Software Foundation; either
+   version 2.1 of the License, or (at your option) any later version.
+
+   This library is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   Lesser General Public License for more details.
+
+   You should have received a copy of the GNU Lesser General Public
+   License along with this library; if not, see <http://www.gnu.org/licenses/>.
+*/
+#include "spice-client.h"
+#include "spice-common.h"
+#include "spice-channel-priv.h"
+#include "spice-session-priv.h"
+#include "spice-marshal.h"
+#include "glib-compat.h"
+#include "vmcstream.h"
+
+/**
+ * SECTION:channel-webdav
+ * @short_description:
+ * @title: filesystem sharing
+ * @section_id:
+ * @see_also: #SpiceChannel
+ * @stability: Stable
+ * @include: channel-webdav.h
+ *
+ * The "webdav" channel exports
+ *
+ * Since: 0.20
+ */
+
+#define SPICE_WEBDAV_CHANNEL_GET_PRIVATE(obj)                                  \
+    (G_TYPE_INSTANCE_GET_PRIVATE((obj), SPICE_TYPE_WEBDAV_CHANNEL, SpiceWebdavChannelPrivate))
+
+typedef struct _OutputQueue OutputQueue;
+
+struct _SpiceWebdavChannelPrivate {
+    SpiceVmcStream *stream;
+    GCancellable *cancellable;
+    GHashTable *clients;
+    OutputQueue *queue;
+
+    struct _demux {
+        gint64 client;
+        guint16 size;
+        guint8 *buf;
+    } demux;
+};
+
+G_DEFINE_TYPE(SpiceWebdavChannel, spice_webdav_channel, SPICE_TYPE_PORT_CHANNEL)
+
+static void spice_webdav_handle_msg(SpiceChannel *channel, SpiceMsgIn *msg);
+
+struct _OutputQueue {
+    GOutputStream *output;
+    gboolean flushing;
+    guint idle_id;
+    GQueue *queue;
+};
+
+typedef struct _OutputQueueElem {
+    OutputQueue *queue;
+    const guint8 *buf;
+    gsize size;
+    GFunc cb;
+    gpointer user_data;
+} OutputQueueElem;
+
+static OutputQueue* output_queue_new(GOutputStream *output)
+{
+    OutputQueue *queue = g_new0(OutputQueue, 1);
+
+    queue->output = g_object_ref(output);
+    queue->queue = g_queue_new();
+
+    return queue;
+}
+
+static void output_queue_free(OutputQueue *queue)
+{
+    g_warn_if_fail(g_queue_get_length(queue->queue) == 0);
+    g_warn_if_fail(!queue->flushing);
+    g_warn_if_fail(!queue->idle_id);
+
+    g_queue_free_full(queue->queue, g_free);
+    g_clear_object(&queue->output);
+    g_free(queue);
+}
+
+static gboolean output_queue_idle(gpointer user_data);
+
+static void output_queue_flush_cb(GObject *source_object,
+                                  GAsyncResult *res,
+                                  gpointer user_data)
+{
+    GError *error = NULL;
+    OutputQueueElem *e = user_data;
+    OutputQueue *q = e->queue;
+
+    q->flushing = FALSE;
+    g_output_stream_flush_finish(G_OUTPUT_STREAM(source_object),
+                                 res, &error);
+    if (error)
+        g_warning("error: %s", error->message);
+
+    g_clear_error(&error);
+
+    if (!q->idle_id)
+        q->idle_id = g_idle_add(output_queue_idle, q);
+
+    g_free(e);
+}
+
+static gboolean output_queue_idle(gpointer user_data)
+{
+    OutputQueue *q = user_data;
+    OutputQueueElem *e;
+    GError *error = NULL;
+
+    if (q->flushing) {
+        q->idle_id = 0;
+        return FALSE;
+    }
+
+    e = g_queue_pop_head(q->queue);
+    if (!e) {
+        q->idle_id = 0;
+        return FALSE;
+    }
+
+    g_output_stream_write_all(q->output, e->buf, e->size, NULL, NULL, &error);
+    if (error)
+        goto err;
+    else if (e->cb)
+        e->cb(q, e->user_data);
+
+    q->flushing = TRUE;
+    g_output_stream_flush_async(q->output, G_PRIORITY_DEFAULT, NULL, output_queue_flush_cb, e);
+
+    return TRUE;
+
+err:
+    g_warning("error: %s", error->message);
+    g_clear_error(&error);
+
+    q->idle_id = 0;
+    return FALSE;
+}
+
+static void output_queue_push(OutputQueue *q, const guint8 *buf, gsize size,
+                              GFunc pushed_cb, gpointer user_data)
+{
+    OutputQueueElem *e = g_new(OutputQueueElem, 1);
+
+    e->buf = buf;
+    e->size = size;
+    e->cb = pushed_cb;
+    e->user_data = user_data;
+    e->queue = q;
+    g_queue_push_tail(q->queue, e);
+
+    if (!q->idle_id && !q->flushing)
+        q->idle_id = g_idle_add(output_queue_idle, q);
+}
+
+typedef struct Client
+{
+    guint refs;
+    SpiceWebdavChannel *self;
+    GSocketConnection *conn;
+    OutputQueue *output;
+    gint64 id;
+    GCancellable *cancellable;
+
+    struct _mux {
+        gint64 id;
+        guint16 size;
+        guint8 *buf;
+    } mux;
+} Client;
+
+static void
+client_unref(Client *client)
+{
+    if (--client->refs > 0)
+        return;
+
+    g_free(client->mux.buf);
+    output_queue_free(client->output);
+
+    g_object_unref(client->conn);
+    g_object_unref(client->cancellable);
+
+    g_free(client);
+}
+
+static Client *
+client_ref(Client *client)
+{
+    client->refs++;
+    return client;
+}
+
+static void client_start_read(SpiceWebdavChannel *self, Client *client);
+
+static void remove_client(SpiceWebdavChannel *self, Client *client)
+{
+    SpiceWebdavChannelPrivate *c;
+
+    if (g_cancellable_is_cancelled(client->cancellable))
+        return;
+
+    g_cancellable_cancel(client->cancellable);
+
+    c = self->priv;
+    g_hash_table_remove(c->clients, &client->id);
+}
+
+static void mux_pushed_cb(OutputQueue *q, gpointer user_data)
+{
+    Client *client = user_data;
+
+    if (client->mux.size == 0) {
+        remove_client(client->self, client);
+    } else {
+        client_start_read(client->self, client);
+    }
+
+    client_unref(client);
+}
+
+static void server_reply_cb(GObject *source_object,
+                            GAsyncResult *res,
+                            gpointer user_data)
+{
+    Client *client = user_data;
+    SpiceWebdavChannel *self = client->self;
+    SpiceWebdavChannelPrivate *c = self->priv;
+    GError *err = NULL;
+    gssize size;
+
+    size = g_input_stream_read_finish(G_INPUT_STREAM(source_object), res, &err);
+    if (err || g_cancellable_is_cancelled(client->cancellable))
+        goto end;
+
+    g_return_if_fail(size <= G_MAXUINT16);
+    g_return_if_fail(size >= 0);
+    client->mux.size = size;
+
+    output_queue_push(c->queue, (guint8 *)&client->mux.id, sizeof(gint64), NULL, NULL);
+    client->mux.size = GUINT16_TO_LE(client->mux.size);
+    output_queue_push(c->queue, (guint8 *)&client->mux.size, sizeof(guint16), NULL, NULL);
+    output_queue_push(c->queue, (guint8 *)client->mux.buf, size, (GFunc)mux_pushed_cb, client);
+
+    return;
+
+end:
+    if (err) {
+        if (!g_cancellable_is_cancelled(client->cancellable))
+            g_warning("read error: %s", err->message);
+        remove_client(self, client);
+        g_clear_error(&err);
+    }
+
+    client_unref(client);
+}
+
+static void client_start_read(SpiceWebdavChannel *self, Client *client)
+{
+    GInputStream *input;
+
+    input = g_io_stream_get_input_stream(G_IO_STREAM(client->conn));
+    g_input_stream_read_async(input, client->mux.buf, G_MAXUINT16,
+                              G_PRIORITY_DEFAULT, client->cancellable, server_reply_cb,
+                              client_ref(client));
+}
+
+static Client * start_client(SpiceWebdavChannel *self, gint64 clientid)
+{
+    SpiceWebdavChannelPrivate *c = self->priv;
+    GError *err = NULL;
+    GSocketClient *sclient;
+    GSocketConnection *conn;
+    GOutputStream *output;
+    gint davport;
+    Client *client;
+
+    spice_webdav_get(spice_channel_get_session(SPICE_CHANNEL(self)), &davport);
+    CHANNEL_DEBUG(self, "starting client %" G_GINT64_FORMAT, clientid);
+
+    sclient = g_socket_client_new();
+    conn = g_socket_client_connect_to_host(sclient, "localhost", davport, NULL, &err);
+    g_object_unref(sclient);
+    if (err)
+        goto error;
+
+    client = g_new0(Client, 1);
+    client->refs = 1;
+    client->id = clientid;
+    client->self = self;
+    client->conn = conn;
+    client->mux.id = GINT64_TO_LE(clientid);
+    client->mux.buf = g_malloc(G_MAXUINT16);
+    client->cancellable = g_cancellable_new();
+
+    output = g_buffered_output_stream_new(g_io_stream_get_output_stream(G_IO_STREAM(conn)));
+    client->output = output_queue_new(output);
+    g_object_unref(output);
+
+    client_start_read(self, client);
+
+    g_hash_table_insert(c->clients, &client->id, client);
+
+    return client;
+
+error:
+    if (err) {
+        g_critical("socket creation failed %s", err->message);
+        g_clear_error(&err);
+    }
+    return NULL;
+}
+
+static void start_demux(SpiceWebdavChannel *self);
+
+static void pushed_client_cb(OutputQueue *q, gpointer user_data)
+{
+    Client *client = user_data;
+    SpiceWebdavChannel *self = client->self;
+
+    start_demux(self);
+}
+
+static void data_read_cb(GObject *source_object,
+                         GAsyncResult *res,
+                         gpointer user_data)
+{
+    SpiceWebdavChannel *self = user_data;
+    SpiceWebdavChannelPrivate *c;
+    Client *client;
+    GError *error = NULL;
+    gssize size;
+
+    size = spice_vmc_input_stream_read_all_finish(G_INPUT_STREAM(source_object), res, &error);
+    if (error) {
+        g_warning("error: %s", error->message);
+        g_clear_error(&error);
+        return;
+    }
+
+    c = self->priv;
+    g_return_if_fail(size == c->demux.size);
+
+    client = g_hash_table_lookup(c->clients, &c->demux.client);
+
+    if (!client)
+        client = start_client(self, c->demux.client);
+
+    CHANNEL_DEBUG(self, "pushing %d to client %p", c->demux.size, client);
+    if (client) {
+        if (size != 0) {
+            output_queue_push(client->output, (guint8 *)c->demux.buf, c->demux.size,
+                              (GFunc)pushed_client_cb, client);
+            return;
+        } else {
+            remove_client(self, client);
+        }
+    }
+
+    start_demux(self);
+}
+
+
+static void size_read_cb(GObject *source_object,
+                         GAsyncResult *res,
+                         gpointer user_data)
+{
+    SpiceWebdavChannel *self = user_data;
+    SpiceWebdavChannelPrivate *c;
+    GInputStream *istream = G_INPUT_STREAM(source_object);
+    GError *error = NULL;
+    gssize size;
+
+    size = spice_vmc_input_stream_read_all_finish(G_INPUT_STREAM(source_object), res, &error);
+    if (error || size != sizeof(guint16))
+        goto end;
+
+    c = self->priv;
+    c->demux.size = GUINT16_FROM_LE(c->demux.size);
+    spice_vmc_input_stream_read_all_async(istream,
+        c->demux.buf, c->demux.size,
+        G_PRIORITY_DEFAULT, c->cancellable, data_read_cb, self);
+    return;
+
+end:
+    if (error) {
+        g_warning("error: %s", error->message);
+        g_clear_error(&error);
+    }
+}
+
+static void client_read_cb(GObject *source_object,
+                               GAsyncResult *res,
+                               gpointer user_data)
+{
+    SpiceWebdavChannel *self = user_data;
+    SpiceWebdavChannelPrivate *c = self->priv;
+    GInputStream *istream = G_INPUT_STREAM(source_object);
+    GError *error = NULL;
+    gssize size;
+
+    size = spice_vmc_input_stream_read_all_finish(G_INPUT_STREAM(source_object), res, &error);
+    if (error || size != sizeof(gint64))
+        goto end;
+
+    c->demux.client = GINT64_FROM_LE(c->demux.client);
+    spice_vmc_input_stream_read_all_async(istream,
+        &c->demux.size, sizeof(guint16),
+        G_PRIORITY_DEFAULT, c->cancellable, size_read_cb, self);
+    return;
+
+end:
+    if (error) {
+        g_warning("error: %s", error->message);
+        g_clear_error(&error);
+    }
+}
+
+static void start_demux(SpiceWebdavChannel *self)
+{
+    SpiceWebdavChannelPrivate *c = self->priv;
+    GInputStream *istream = g_io_stream_get_input_stream(G_IO_STREAM(c->stream));
+
+    CHANNEL_DEBUG(self, "start demux");
+    spice_vmc_input_stream_read_all_async(istream, &c->demux.client, sizeof(gint64),
+        G_PRIORITY_DEFAULT, c->cancellable, client_read_cb, self);
+
+}
+
+static void port_event(SpiceWebdavChannel *self, gint event)
+{
+    SpiceWebdavChannelPrivate *c = self->priv;
+
+    CHANNEL_DEBUG(self, "port event:%d", event);
+    if (event == SPICE_PORT_EVENT_OPENED) {
+        g_cancellable_reset(c->cancellable);
+        start_demux(self);
+    } else {
+        g_cancellable_cancel(c->cancellable);
+        g_hash_table_remove_all(c->clients);
+    }
+}
+
+static void client_remove_unref(gpointer data)
+{
+    Client *client = data;
+
+    g_cancellable_cancel(client->cancellable);
+    client_unref(client);
+}
+
+static void spice_webdav_channel_init(SpiceWebdavChannel *channel)
+{
+    SpiceWebdavChannelPrivate *c = SPICE_WEBDAV_CHANNEL_GET_PRIVATE(channel);
+
+    channel->priv = c;
+    c->stream = spice_vmc_stream_new(SPICE_CHANNEL(channel));
+    c->cancellable = g_cancellable_new();
+    c->clients = g_hash_table_new_full(g_int64_hash, g_int64_equal,
+                                       NULL, client_remove_unref);
+    c->demux.buf = g_malloc(G_MAXUINT16);
+
+    GOutputStream *ostream = g_io_stream_get_output_stream(G_IO_STREAM(c->stream));
+    c->queue = output_queue_new(ostream);
+}
+
+static void spice_webdav_channel_finalize(GObject *object)
+{
+    SpiceWebdavChannelPrivate *c = SPICE_WEBDAV_CHANNEL(object)->priv;
+
+    g_free(c->demux.buf);
+
+    G_OBJECT_CLASS(spice_webdav_channel_parent_class)->finalize(object);
+}
+
+static void spice_webdav_channel_dispose(GObject *object)
+{
+    SpiceWebdavChannelPrivate *c = SPICE_WEBDAV_CHANNEL(object)->priv;
+
+    g_cancellable_cancel(c->cancellable);
+    g_clear_object(&c->cancellable);
+    g_clear_pointer(&c->queue, output_queue_free);
+    g_clear_object(&c->stream);
+    g_hash_table_unref(c->clients);
+
+    G_OBJECT_CLASS(spice_webdav_channel_parent_class)->dispose(object);
+}
+
+static void spice_webdav_channel_up(SpiceChannel *channel)
+{
+    CHANNEL_DEBUG(channel, "up");
+}
+
+static void spice_webdav_channel_class_init(SpiceWebdavChannelClass *klass)
+{
+    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
+    SpiceChannelClass *channel_class = SPICE_CHANNEL_CLASS(klass);
+
+    gobject_class->dispose      = spice_webdav_channel_dispose;
+    gobject_class->finalize     = spice_webdav_channel_finalize;
+    channel_class->handle_msg   = spice_webdav_handle_msg;
+    channel_class->channel_up   = spice_webdav_channel_up;
+
+    g_signal_override_class_handler("port-event",
+                                    SPICE_TYPE_WEBDAV_CHANNEL,
+                                    G_CALLBACK(port_event));
+
+    g_type_class_add_private(klass, sizeof(SpiceWebdavChannelPrivate));
+}
+
+/* coroutine context */
+static void webdav_handle_msg(SpiceChannel *channel, SpiceMsgIn *in)
+{
+    SpiceWebdavChannel *self = SPICE_WEBDAV_CHANNEL(channel);
+    SpiceWebdavChannelPrivate *c = self->priv;
+    int size;
+    uint8_t *buf;
+
+    buf = spice_msg_in_raw(in, &size);
+    CHANNEL_DEBUG(channel, "len:%d buf:%p", size, buf);
+
+    spice_vmc_input_stream_co_data(
+        SPICE_VMC_INPUT_STREAM(g_io_stream_get_input_stream(G_IO_STREAM(c->stream))),
+        buf, size);
+}
+
+
+/* coroutine context */
+static void spice_webdav_handle_msg(SpiceChannel *channel, SpiceMsgIn *msg)
+{
+    int type = spice_msg_in_type(msg);
+    SpiceChannelClass *parent_class;
+
+    parent_class = SPICE_CHANNEL_CLASS(spice_webdav_channel_parent_class);
+
+    if (type == SPICE_MSG_SPICEVMC_DATA)
+        webdav_handle_msg(channel, msg);
+    else if (parent_class->handle_msg)
+        parent_class->handle_msg(channel, msg);
+    else
+        g_return_if_reached();
+}
+
+SpiceWebDAV* spice_webdav_get(SpiceSession *session, gint *port)
+{
+    g_return_val_if_fail(SPICE_IS_SESSION(session), NULL);
+
+    SpiceWebDAV *self;
+    static GStaticMutex mutex = G_STATIC_MUTEX_INIT;
+
+    g_static_mutex_lock(&mutex);
+    self = session->priv->webdav;
+    if (self == NULL) {
+        self = spice_webdav_new(0, ".");
+        session->priv->webdav = self;
+        spice_webdav_run(self);
+    }
+    g_static_mutex_unlock(&mutex);
+
+    if (port)
+        *port = spice_webdav_get_port(self);
+
+    return self;
+}
diff --git a/gtk/channel-webdav.h b/gtk/channel-webdav.h
new file mode 100644
index 0000000..7556a9f
--- /dev/null
+++ b/gtk/channel-webdav.h
@@ -0,0 +1,82 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+   Copyright (C) 2013 Red Hat, Inc.
+
+   This library is free software; you can redistribute it and/or
+   modify it under the terms of the GNU Lesser General Public
+   License as published by the Free Software Foundation; either
+   version 2.1 of the License, or (at your option) any later version.
+
+   This library is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   Lesser General Public License for more details.
+
+   You should have received a copy of the GNU Lesser General Public
+   License along with this library; if not, see <http://www.gnu.org/licenses/>.
+*/
+#ifndef __SPICE_CLIENT_WEBDAV_CHANNEL_H__
+#define __SPICE_CLIENT_WEBDAV_CHANNEL_H__
+
+#include <gio/gio.h>
+#include "spice-client.h"
+#include "channel-port.h"
+#include "webdav/webdav.h"
+
+G_BEGIN_DECLS
+
+#define SPICE_TYPE_WEBDAV_CHANNEL            (spice_webdav_channel_get_type())
+#define SPICE_WEBDAV_CHANNEL(obj)            (G_TYPE_CHECK_INSTANCE_CAST((obj), SPICE_TYPE_WEBDAV_CHANNEL, SpiceWebdavChannel))
+#define SPICE_WEBDAV_CHANNEL_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST((klass), SPICE_TYPE_WEBDAV_CHANNEL, SpiceWebdavChannelClass))
+#define SPICE_IS_WEBDAV_CHANNEL(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), SPICE_TYPE_WEBDAV_CHANNEL))
+#define SPICE_IS_WEBDAV_CHANNEL_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), SPICE_TYPE_WEBDAV_CHANNEL))
+#define SPICE_WEBDAV_CHANNEL_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS((obj), SPICE_TYPE_WEBDAV_CHANNEL, SpiceWebdavChannelClass))
+
+typedef struct _SpiceWebdavChannel SpiceWebdavChannel;
+typedef struct _SpiceWebdavChannelClass SpiceWebdavChannelClass;
+typedef struct _SpiceWebdavChannelPrivate SpiceWebdavChannelPrivate;
+
+/**
+ * SpiceWebdavOpenFlags:
+ *
+ **/
+typedef enum
+{
+    SPICE_WEBDAV_NONE = 0,
+
+    SPICE_WEBDAV_READWRITE = 1 << 0,
+} SpiceWebdavFlags;
+
+/**
+ * SpiceWebdavChannel:
+ *
+ * The #SpiceWebdavChannel struct is opaque and should not be accessed directly.
+ */
+struct _SpiceWebdavChannel {
+    SpicePortChannel parent;
+
+    /*< private >*/
+    SpiceWebdavChannelPrivate *priv;
+    /* Do not add fields to this struct */
+};
+
+/**
+ * SpiceWebdavChannelClass:
+ * @parent_class: Parent class.
+ *
+ * Class structure for #SpiceWebdavChannel.
+ */
+struct _SpiceWebdavChannelClass {
+    SpicePortChannelClass parent_class;
+
+    /*< private >*/
+    /* Do not add fields to this struct */
+};
+
+GType spice_webdav_channel_get_type(void);
+
+SpiceWebDAV* spice_webdav_get(SpiceSession *session, gint *port);
+
+G_END_DECLS
+
+#endif /* __SPICE_CLIENT_WEBDAV_CHANNEL_H__ */
diff --git a/gtk/map-file b/gtk/map-file
index 368b44f..afdc672 100644
--- a/gtk/map-file
+++ b/gtk/map-file
@@ -118,6 +118,7 @@ spice_util_get_debug;
 spice_util_get_version_string;
 spice_util_set_debug;
 spice_uuid_to_string;
+spice_webdav_flags_get_type;
 local:
 *;
 };
diff --git a/gtk/spice-channel.c b/gtk/spice-channel.c
index f101c3a..adcf50e 100644
--- a/gtk/spice-channel.c
+++ b/gtk/spice-channel.c
@@ -1862,6 +1862,7 @@ static const char *to_string[] = {
     [ SPICE_CHANNEL_SMARTCARD ] = "smartcard",
     [ SPICE_CHANNEL_USBREDIR ] = "usbredir",
     [ SPICE_CHANNEL_PORT ] = "port",
+    [ SPICE_CHANNEL_WEBDAV ] = "webdav",
 };
 
 /**
@@ -1922,6 +1923,7 @@ gchar *spice_channel_supported_string(void)
 #ifdef USE_USBREDIR
                      spice_channel_type_to_string(SPICE_CHANNEL_USBREDIR),
 #endif
+                     spice_channel_type_to_string(SPICE_CHANNEL_WEBDAV),
                      NULL);
 }
 
@@ -1986,6 +1988,10 @@ SpiceChannel *spice_channel_new(SpiceSession *s, int type, int id)
         break;
     }
 #endif
+    case SPICE_CHANNEL_WEBDAV: {
+        gtype = SPICE_TYPE_WEBDAV_CHANNEL;
+        break;
+    }
     case SPICE_CHANNEL_PORT:
         gtype = SPICE_TYPE_PORT_CHANNEL;
         break;
diff --git a/gtk/spice-client.h b/gtk/spice-client.h
index 730d11a..f5c8aa7 100644
--- a/gtk/spice-client.h
+++ b/gtk/spice-client.h
@@ -41,6 +41,7 @@
 #include "channel-smartcard.h"
 #include "channel-usbredir.h"
 #include "channel-port.h"
+#include "channel-webdav.h"
 
 #include "smartcard-manager.h"
 #include "usb-device-manager.h"
diff --git a/gtk/spice-glib-sym-file b/gtk/spice-glib-sym-file
index 4fc8643..7b18149 100644
--- a/gtk/spice-glib-sym-file
+++ b/gtk/spice-glib-sym-file
@@ -92,3 +92,5 @@ spice_util_get_debug
 spice_util_get_version_string
 spice_util_set_debug
 spice_uuid_to_string
+spice_webdav_channel_get_type
+spice_webdav_get
diff --git a/gtk/spice-gtk-session.c b/gtk/spice-gtk-session.c
index eab7e2f..44a8844 100644
--- a/gtk/spice-gtk-session.c
+++ b/gtk/spice-gtk-session.c
@@ -163,7 +163,6 @@ static void spice_gtk_session_dispose(GObject *gobject)
                 G_CALLBACK(clipboard_owner_change), self);
         s->clipboard_primary = NULL;
     }
-
     if (s->session) {
         g_signal_handlers_disconnect_by_func(s->session,
                                              G_CALLBACK(channel_new),
diff --git a/gtk/spice-session-priv.h b/gtk/spice-session-priv.h
index 55fee47..cf1e6d1 100644
--- a/gtk/spice-session-priv.h
+++ b/gtk/spice-session-priv.h
@@ -24,6 +24,7 @@
 #include "spice-session.h"
 #include "spice-proxy.h"
 #include "spice-gtk-session.h"
+#include "webdav/webdav.h"
 #include "spice-channel-cache.h"
 #include "decode.h"
 
@@ -108,6 +109,7 @@ struct _SpiceSessionPrivate {
     SpiceGtkSession   *gtk_session;
     SpiceUsbDeviceManager *usb_manager;
     SpicePlaybackChannel *playback_channel;
+    SpiceWebDAV       *webdav;
 };
 
 SpiceSession *spice_session_new_from_session(SpiceSession *session);
diff --git a/gtk/spice-session.c b/gtk/spice-session.c
index bcbba27..b4242c1 100644
--- a/gtk/spice-session.c
+++ b/gtk/spice-session.c
@@ -210,6 +210,7 @@ spice_session_dispose(GObject *gobject)
     g_clear_object(&s->audio_manager);
     g_clear_object(&s->desktop_integration);
     g_clear_object(&s->gtk_session);
+    g_clear_object(&s->webdav);
     g_clear_object(&s->usb_manager);
     g_clear_object(&s->proxy);
 
@@ -1825,10 +1826,9 @@ GSocketConnection* spice_session_channel_open_host(SpiceSession *session, SpiceC
         g_timeout_add_seconds(SOCKET_TIMEOUT, connect_timeout, &open_host);
 #endif
 
-    guint id = g_idle_add(open_host_idle_cb, &open_host);
+    g_idle_add(open_host_idle_cb, &open_host);
     /* switch to main loop and wait for connection */
     coroutine_yield(NULL);
-    g_source_remove(id);
 
 #if !GLIB_CHECK_VERSION(2,26,0)
     if (open_host.timeout_id == 0)
diff --git a/gtk/webdav/Makefile.am b/gtk/webdav/Makefile.am
new file mode 100644
index 0000000..39d7024
--- /dev/null
+++ b/gtk/webdav/Makefile.am
@@ -0,0 +1,43 @@
+NULL =
+
+noinst_LTLIBRARIES = libwebdav.la
+noinst_PROGRAMS = webdav-server
+
+# FIXME: -I.. for glib-compat atm
+AM_CPPFLAGS =					\
+	-DG_LOG_DOMAIN=\"webdav\"		\
+	$(COMMON_CFLAGS)			\
+	$(SPICE_CFLAGS)				\
+	$(GIO_CFLAGS)				\
+	$(SOUP_CFLAGS)				\
+	-I..					\
+	`pkg-config --cflags avahi-gobject` \
+	$(NULL)
+
+libwebdav_la_SOURCES =				\
+	guuid.c					\
+	guuid.h					\
+	webdav.c				\
+	webdav.h				\
+	$(NULL)
+
+libwebdav_la_LIBADD =				\
+	$(SOUP_LIBS)				\
+	$(NULL)
+
+
+webdav_server_SOURCES = webdav-server.c
+webdav_server_LDFLAGS =				\
+	`pkg-config --libs avahi-gobject avahi-client` \
+	$(NULL)
+webdav_server_LDADD =				\
+	libwebdav.la				\
+	$(GLIB_LIBS)				\
+	$(GIO_LIBS)				\
+	$(NULL)
+
+
+TESTS = \
+	test-start
+
+-include $(top_srcdir)/git.mk
diff --git a/gtk/webdav/guuid.c b/gtk/webdav/guuid.c
new file mode 100644
index 0000000..9579086
--- /dev/null
+++ b/gtk/webdav/guuid.c
@@ -0,0 +1,471 @@
+/* guuid.c - UUID functions
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * This library is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of the
+ * licence, or (at your option) any later version.
+ *
+ * This is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301
+ * USA.
+ *
+ * Authors: Marc-André Lureau <marcandre.lureau at redhat.com>
+ */
+
+#include "config.h"
+
+#include <string.h>
+#include <glib.h>
+
+#include "guuid.h"
+
+/**
+ * SECTION:uuid
+ * @title: GUuid
+ * @short_description: a universal unique identifier
+ *
+ * A UUID, or Universally unique identifier, is intended to uniquely
+ * identify information in a distributed environment. For the
+ * definition of UUID, see <ulink
+ * url="tools.ietf.org/html/rfc4122.html">RFC 4122</ulink>.
+ *
+ * The creation of UUIDs does not require a centralized authority.
+ *
+ * UUIDs are of relatively small size (128 bits, or 16 bytes). The
+ * common string representation (ex:
+ * 1d6c0810-2bd6-45f3-9890-0268422a6f14) needs 37 bytes.
+ *
+ * There are different mechanisms to generate UUIDs. The UUID
+ * specification defines 5 versions. If all you want is a unique ID,
+ * you should probably call g_uuid_random() or g_uuid_generate4(),
+ * which is the version 4.
+ *
+ * If you want to generate UUID based on a name within a namespace
+ * (%G_UUID_NAMESPACE_DNS for fully-qualified domain name for
+ * example), you may want to use version 5, g_uuid_generate5() using a
+ * SHA-1 hash, or its alternative based on a MD5 hash, version 3
+ * g_uuid_generate3().
+ *
+ * You can look up well-known namespaces with g_uuid_get_namespace().
+ *
+ * Since: 2.40
+ **/
+
+/**
+ * GUuid:
+ *
+ * A structure that holds a UUID.
+ *
+ * Since: 2.40
+ */
+
+/**
+ * G_UUID_DEFINE_STATIC:
+ * @name: the read-only variable name to define.
+ *
+ * A convenience macro to define a #GUuid.
+ *
+ * Since: 2.40
+ */
+
+/**
+ * G_UUID_NIL:
+ *
+ * A macro that can be used to initialize a #GUuid to the nil value.
+ * It can be used as initializer when declaring a variable, but it
+ * cannot be assigned to a variable.
+ *
+ * |[
+ *   GUuid uuid = G_UUID_NIL;
+ * ]|
+ *
+ * Since: 2.40
+ */
+
+G_UUID_DEFINE_STATIC (uuid_nil, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
+
+/**
+ * Common namespaces as defined in rfc4122:
+ * http://tools.ietf.org/html/rfc4122.html#appendix-C
+ */
+G_UUID_DEFINE_STATIC (uuid_dns,
+                      0x6b, 0xa7, 0xb8, 0x10,
+                      0x9d, 0xad,
+                      0x11, 0xd1,
+                      0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8)
+
+G_UUID_DEFINE_STATIC (uuid_url,
+                      0x6b, 0xa7, 0xb8, 0x11,
+                      0x9d, 0xad,
+                      0x11, 0xd1,
+                      0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8)
+
+G_UUID_DEFINE_STATIC (uuid_oid,
+                      0x6b, 0xa7, 0xb8, 0x12,
+                      0x9d, 0xad,
+                      0x11, 0xd1,
+                      0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8)
+
+G_UUID_DEFINE_STATIC (uuid_x500,
+                      0x6b, 0xa7, 0xb8, 0x14,
+                      0x9d, 0xad,
+                      0x11, 0xd1,
+                      0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8)
+
+/**
+ * g_uuid_equal:
+ * @uuid1: pointer to the first #GUuid
+ * @uuid2: pointer to the second #GUuid
+ *
+ * Checks if two UUIDs are equal.
+ *
+ * Returns: %TRUE if @uuid1 is equal to @uuid2, %FALSE otherwise
+ * Since: 2.40
+ **/
+gboolean
+g_uuid_equal (gconstpointer uuid1,
+              gconstpointer uuid2)
+{
+  g_return_val_if_fail (uuid1 != NULL, FALSE);
+  g_return_val_if_fail (uuid2 != NULL, FALSE);
+
+  return memcmp (uuid1, uuid2, sizeof (GUuid)) == 0;
+}
+
+/**
+ * g_uuid_is_nil:
+ * @uuid: a #GUuid
+ *
+ * Checks whether @uuid is the nil UUID (all the 128 bits are zero)
+ *
+ * Returns: %TRUE if @uuid is nil, %FALSE otherwise
+ * Since: 2.40
+ **/
+gboolean
+g_uuid_is_nil (const GUuid *uuid)
+{
+  g_return_val_if_fail (uuid != NULL, FALSE);
+
+  return g_uuid_equal (uuid, &uuid_nil);
+}
+
+/**
+ * g_uuid_to_string:
+ * @uuid: a #GUuid
+ *
+ * Creates a string representation of @uuid, of the form
+ * 06e023d5-86d8-420e-8103-383e4566087a (no braces nor urn:uuid:
+ * prefix).
+ *
+ * Returns: A string that should be freed with g_free().
+ * Since: 2.40
+ **/
+gchar *
+g_uuid_to_string (const GUuid *uuid)
+{
+  const guint8 *bytes;
+
+  g_return_val_if_fail (uuid != NULL, NULL);
+
+  bytes = uuid->bytes;
+
+  return g_strdup_printf ("%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x"
+                          "-%02x%02x%02x%02x%02x%02x",
+                          bytes[0], bytes[1], bytes[2], bytes[3],
+                          bytes[4], bytes[5], bytes[6], bytes[7],
+                          bytes[8], bytes[9], bytes[10], bytes[11],
+                          bytes[12], bytes[13], bytes[14], bytes[15]);
+}
+
+static gboolean
+uuid_parse_string (const gchar *str,
+                   gssize       len,
+                   GUuid       *uuid)
+{
+  GUuid tmp;
+  guint8 *bytes = tmp.bytes;
+  gint i, j, hi, lo;
+  guint expected_len = 36;
+
+  if (g_str_has_prefix (str, "urn:uuid:"))
+    str += 9;
+  else if (str[0] == '{')
+    expected_len += 2;
+
+  if (len == -1)
+    len = strlen (str);
+
+  if (len != expected_len)
+    return FALSE;
+
+  /* only if str[0] == '{' above */
+  if (expected_len == 38)
+    {
+      if (str[37] != '}')
+        return FALSE;
+
+      str++;
+    }
+
+  for (i = 0, j = 0; i < 16;)
+    {
+      if (j == 8 || j == 13 || j == 18 || j == 23)
+        {
+          if (str[j++] != '-')
+            return FALSE;
+
+          continue;
+        }
+
+      hi = g_ascii_xdigit_value (str[j++]);
+      lo = g_ascii_xdigit_value (str[j++]);
+
+      if (hi == -1 || lo == -1)
+        return FALSE;
+
+      bytes[i++] = hi << 8 | lo;
+    }
+
+  if (uuid != NULL)
+    *uuid = tmp;
+
+  return TRUE;
+}
+
+/**
+ * g_uuid_from_string:
+ * @str: a string representing a UUID
+ * @len: the length of @str (may be -1 if is nul-terminated)
+ * @uuid: (out) (caller-allocates): the #GUuid to store the parsed UUID value
+ *
+ * Reads a UUID from its string representation and set the value in
+ * @uuid. See g_uuid_string_is_valid() for examples of accepted string
+ * representations.
+ *
+ *
+ * Returns: %TRUE if the @str string is successfully parsed, %FALSE
+ * otherwise.
+ * Since: 2.40
+ **/
+gboolean
+g_uuid_from_string (const gchar *str,
+                    gssize       len,
+                    GUuid       *uuid)
+{
+  g_return_val_if_fail (str != NULL, FALSE);
+  g_return_val_if_fail (uuid != NULL, FALSE);
+  g_return_val_if_fail (len >= -1, FALSE);
+
+  return uuid_parse_string (str, len, uuid);
+}
+
+/**
+ * g_uuid_string_is_valid:
+ * @str: a string representing a UUID
+ * @len: the length of @str (may be -1 if is nul-terminated)
+ *
+ * Parses the string @str and verify if it is a UUID.
+ *
+ * The function accepts the following syntaxes:
+ *
+ * - simple forms (e.g. f81d4fae-7dec-11d0-a765-00a0c91e6bf6)
+ * - simple forms with curly braces (e.g.
+ *   {urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6})
+ * - URN (e.g. urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6)
+ *
+ * Returns: %TRUE if @str is a valid UUID, %FALSE otherwise.
+ * Since: 2.40
+ **/
+gboolean
+g_uuid_string_is_valid (const gchar *str, gssize len)
+{
+  g_return_val_if_fail (str != NULL, FALSE);
+  g_return_val_if_fail (len >= -1, FALSE);
+
+  return uuid_parse_string (str, len, NULL);
+}
+
+static void
+uuid_set_version (GUuid *uuid, int version)
+{
+  guint8 *bytes = uuid->bytes;
+
+  /*
+   * Set the four most significant bits (bits 12 through 15) of the
+   * time_hi_and_version field to the 4-bit version number from
+   * Section 4.1.3.
+   */
+  bytes[6] &= 0x0f;
+  bytes[6] |= version << 4;
+  /*
+   * Set the two most significant bits (bits 6 and 7) of the
+   * clock_seq_hi_and_reserved to zero and one, respectively.
+   */
+  bytes[8] &= 0x3f;
+  bytes[8] |= 0x80;
+}
+
+/**
+ * g_uuid_generate4:
+ * @uuid: a #GUuid
+ *
+ * Generates a random UUID (RFC 4122 version 4).
+ * Since: 2.40
+ **/
+void
+g_uuid_generate4 (GUuid *uuid)
+{
+  int i;
+  guint8 *bytes;
+  guint32 *ints;
+
+  g_return_if_fail (uuid != NULL);
+
+  bytes = uuid->bytes;
+  ints = (guint32*) bytes;
+  for (i = 0; i < 4; i++)
+    ints[i] = g_random_int ();
+
+  uuid_set_version (uuid, 4);
+}
+
+/**
+ * g_uuid_random:
+ *
+ * Generates a random UUID (RFC 4122 version 4) as a string.
+ *
+ * Returns: A string that should be freed with g_free().
+ * Since: 2.40
+ **/
+gchar *
+g_uuid_random (void)
+{
+  GUuid uuid;
+
+  g_uuid_generate4 (&uuid);
+
+  return g_uuid_to_string (&uuid);
+}
+
+/**
+ * g_uuid_get_namespace:
+ * @namespace: a #GUuidNamespace namespace
+ *
+ * Look up one of the well-known namespace UUIDs.
+ *
+ * Returns: a UUID, or %NULL on failure.
+ * Since: 2.40
+ **/
+const GUuid *
+g_uuid_get_namespace (GUuidNamespace namespace)
+{
+  switch (namespace)
+    {
+    case G_UUID_NAMESPACE_DNS:
+      return &uuid_dns;
+    case G_UUID_NAMESPACE_URL:
+      return &uuid_url;
+    case G_UUID_NAMESPACE_OID:
+      return &uuid_oid;
+    case G_UUID_NAMESPACE_X500:
+      return &uuid_x500;
+    default:
+      g_return_val_if_reached (NULL);
+    }
+}
+
+static void
+uuid_generate3or5 (GUuid         *uuid,
+                   gint           version,
+                   GChecksumType  checksum_type,
+                   const GUuid   *namespace,
+                   const guchar  *name,
+                   gssize         length)
+{
+  GChecksum *checksum;
+  gssize digest_len;
+  guint8 *digest;
+
+  if (length < 0)
+      length = strlen ((const gchar *)name);
+
+  digest_len = g_checksum_type_get_length (checksum_type);
+  g_assert (digest_len != -1);
+
+  checksum = g_checksum_new (checksum_type);
+  g_return_if_fail (checksum != NULL);
+
+  g_checksum_update (checksum, namespace->bytes, sizeof (namespace->bytes));
+  g_checksum_update (checksum, name, length);
+
+  digest = g_malloc (digest_len);
+  g_checksum_get_digest (checksum, digest, (gsize*) &digest_len);
+  g_assert (digest_len >= 16);
+
+  memcpy (uuid->bytes, digest, 16);
+  uuid_set_version (uuid, version);
+
+  g_checksum_free (checksum);
+  g_free (digest);
+}
+
+/**
+ * g_uuid_generate3:
+ * @uuid: a #GUuid
+ * @namespace: a namespace #GUuid
+ * @name: a string
+ * @length: size of the name, or -1 if @name is a null-terminated string
+ *
+ * Generates a UUID based on the MD5 hash of a namespace UUID and a
+ * string (RFC 4122 version 3). MD5 is <emphasis>no longer considered
+ * secure</emphasis>, and you should only use this if you need
+ * interoperability with existing systems that use version 3 UUIDs.
+ * For new code, you should use g_uuid_generate5().
+ *
+ * Since: 2.40
+ **/
+void
+g_uuid_generate3 (GUuid         *uuid,
+                  const GUuid   *namespace,
+                  const guchar  *name,
+                  gssize         length)
+{
+  g_return_if_fail (uuid != NULL);
+  g_return_if_fail (namespace != NULL);
+  g_return_if_fail (name != NULL);
+
+  uuid_generate3or5 (uuid, 3, G_CHECKSUM_MD5, namespace, name, length);
+}
+
+/**
+ * g_uuid_generate5:
+ * @uuid: a #GUuid
+ * @namespace: a namespace #GUuid
+ * @name: a string
+ * @length: size of the name, or -1 if @name is a null-terminated string
+ *
+ * Generates a UUID based on the SHA-1 hash of a namespace UUID and a
+ * string (RFC 4122 version 5).
+ *
+ * Since: 2.40
+ **/
+void
+g_uuid_generate5 (GUuid         *uuid,
+                  const GUuid   *namespace,
+                  const guchar  *name,
+                  gssize         length)
+{
+  g_return_if_fail (uuid != NULL);
+  g_return_if_fail (namespace != NULL);
+  g_return_if_fail (name != NULL);
+
+  uuid_generate3or5 (uuid, 5, G_CHECKSUM_SHA1, namespace, name, length);
+}
diff --git a/gtk/webdav/guuid.h b/gtk/webdav/guuid.h
new file mode 100644
index 0000000..a80d4ab
--- /dev/null
+++ b/gtk/webdav/guuid.h
@@ -0,0 +1,105 @@
+/* guuid.h - UUID functions
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * This library is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of the
+ * licence, or (at your option) any later version.
+ *
+ * This is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301
+ * USA.
+ *
+ * Authors: Marc-André Lureau <marcandre.lureau at redhat.com>
+ */
+
+#ifndef __G_UUID_H__
+#define __G_UUID_H__
+
+#include <glib/gtypes.h>
+
+G_BEGIN_DECLS
+
+/**
+ * GUuidNamespace:
+ * @G_UUID_NAMESPACE_DNS: for fully-qualified domain name
+ * @G_UUID_NAMESPACE_URL: for URLs
+ * @G_UUID_NAMESPACE_OID: for ISO Object IDs (OIDs)
+ * @G_UUID_NAMESPACE_X500: for X.500 istinguished Names (DNs)
+ *
+ * The well-known UUID namespace to look up with
+ * g_uuid_get_namespace().
+ *
+ * Note that the #GUuidNamespace enumeration may be extended at a
+ * later date to include new namespaces.
+ *
+ * Since: 2.40
+ */
+typedef enum
+{
+  G_UUID_NAMESPACE_DNS,
+  G_UUID_NAMESPACE_URL,
+  G_UUID_NAMESPACE_OID,
+  G_UUID_NAMESPACE_X500
+} GUuidNamespace;
+
+typedef struct _GUuid GUuid;
+
+struct _GUuid {
+  guint8 bytes[16];
+};
+
+
+#define G_UUID_INIT(u0,u1,u2,u3,u4,u5,u6,u7,u8,u9,u10,u11,u12,u13,u14,u15) \
+  { { u0,u1,u2,u3,u4,u5,u6,u7,u8,u9,u10,u11,u12,u13,u14,u15 } }
+
+#define G_UUID_INIT_NIL G_UUID_INIT(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0)
+
+#define G_UUID_DEFINE_STATIC(name,u0,u1,u2,u3,u4,u5,u6,u7,u8,u9,u10,u11,u12,u13,u14,u15) \
+  static const GUuid name =                                             \
+    G_UUID_INIT(u0,u1,u2,u3,u4,u5,u6,u7,u8,u9,u10,u11,u12,u13,u14,u15);
+
+GLIB_AVAILABLE_IN_2_40
+gboolean      g_uuid_is_nil                (const GUuid   *uuid);
+GLIB_AVAILABLE_IN_2_40
+gboolean      g_uuid_equal                 (gconstpointer uuid1,
+                                            gconstpointer uuid2);
+
+GLIB_AVAILABLE_IN_2_40
+gchar *       g_uuid_to_string             (const GUuid   *uuid);
+GLIB_AVAILABLE_IN_2_40
+gboolean      g_uuid_string_is_valid       (const gchar   *str,
+                                            gssize         len);
+GLIB_AVAILABLE_IN_2_40
+gboolean      g_uuid_from_string           (const gchar   *str,
+                                            gssize         len,
+                                            GUuid         *uuid);
+
+GLIB_AVAILABLE_IN_2_40
+gchar *       g_uuid_random                (void);
+GLIB_AVAILABLE_IN_2_40
+void          g_uuid_generate4             (GUuid         *uuid);
+
+GLIB_AVAILABLE_IN_2_40
+const GUuid * g_uuid_get_namespace         (GUuidNamespace namespace);
+GLIB_AVAILABLE_IN_2_40
+void          g_uuid_generate3             (GUuid         *uuid,
+                                            const GUuid   *namespace,
+                                            const guchar  *name,
+                                            gssize         length);
+GLIB_AVAILABLE_IN_2_40
+void          g_uuid_generate5             (GUuid         *uuid,
+                                            const GUuid   *namespace,
+                                            const guchar  *name,
+                                            gssize         length);
+
+G_END_DECLS
+
+#endif  /* __G_UUID_H__ */
diff --git a/gtk/webdav/test-start b/gtk/webdav/test-start
new file mode 100755
index 0000000..6341b6b
--- /dev/null
+++ b/gtk/webdav/test-start
@@ -0,0 +1,141 @@
+#!/bin/bash
+
+set -e
+
+export LC_ALL=C
+tmpdir=$(mktemp -d --tmpdir webdav.XXXXXXXX)
+server=
+
+setup () {
+    rm -rf $tmpdir
+    mkdir -p $tmpdir/root/subdir
+    echo "test" > $tmpdir/root/subdir/file
+    echo "prog" > $tmpdir/root/exec
+    chmod +x $tmpdir/root/exec
+}
+
+while [[ "$1" == -* ]]; do
+  case $1 in
+    --dir)
+      shift
+      tmpdir="$1"
+      ;;
+    --server)
+      shift
+      server="$1"
+      ;;
+    --setup)
+      setup
+      exit 0
+      ;;
+  esac
+  shift
+done
+
+setup
+
+if [ -e $server ]; then
+  server="localhost:9876"
+  trap 'kill $(jobs -p)' EXIT
+  ./webdav-server -p 9876 -P $tmpdir/root &
+fi
+
+update_result () {
+  cat $tmpdir/stdout | grep -v 'dav:/>' \
+      | tail -n +2 | head -n -1 | cut -b -40 | tee -a $tmpdir/result
+}
+
+clean_result () {
+  rm $tmpdir/result
+}
+
+diff_result () {
+  diff -Zu - $tmpdir/result
+}
+
+do_cmd () {
+  echo "> $1"
+  echo "$1" | cadaver "localhost:9876" >$tmpdir/stdout
+  update_result
+}
+
+## PROP ###################################
+
+do_cmd "ls"
+diff_result <<EOF
+Coll:   subdir
+       *exec
+EOF
+clean_result
+
+
+do_cmd "ls subdir"
+diff_result <<EOF
+        file
+EOF
+clean_result
+
+## COLLECTION #############################
+
+do_cmd "mkcol collection"
+do_cmd "ls"
+diff_result <<EOF
+Coll:   collection
+Coll:   subdir
+       *exec
+EOF
+clean_result
+
+do_cmd "mkcol collection"
+do_cmd "mkcol a/b/c"
+diff_result <<EOF
+405 Method Not Allowed
+409 Conflict
+EOF
+clean_result
+
+do_cmd "rmcol collection"
+do_cmd "ls"
+diff_result <<EOF
+Coll:   subdir
+       *exec
+EOF
+clean_result
+
+## GET ##################################
+
+do_cmd "cat subdir/file"
+diff_result <<EOF
+test
+EOF
+clean_result
+
+## COPY ##################################
+
+do_cmd "cp subdir/file subdir/file2"
+do_cmd "cp subdir/file subdir/file2"
+do_cmd "ls subdir"
+do_cmd "cat subdir/file2"
+diff_result <<EOF
+        file
+        file2
+test
+EOF
+clean_result
+
+
+## DELETE ###############################
+
+do_cmd "rm subdir/file2"
+do_cmd "ls subdir"
+diff_result <<EOF
+        file
+EOF
+clean_result
+
+do_cmd "rm subdir/file2"
+diff_result <<EOF
+http://localhost:9876/subdir/file2: 403
+
+EOF
+clean_result
diff --git a/gtk/webdav/webdav-server.c b/gtk/webdav/webdav-server.c
new file mode 100644
index 0000000..5611a4f
--- /dev/null
+++ b/gtk/webdav/webdav-server.c
@@ -0,0 +1,197 @@
+#include "config.h"
+
+#include <stdlib.h>
+#include <locale.h>
+#include <glib/gi18n.h>
+#include <glib.h>
+#include <glib/gprintf.h>
+#ifdef G_OS_UNIX
+#include <glib-unix.h>
+#endif
+
+#include <avahi-gobject/ga-client.h>
+#include <avahi-gobject/ga-entry-group.h>
+
+#include "webdav.h"
+
+static SpiceWebDAV *dav;
+static GaClient *mdns_client;
+static GaEntryGroup *mdns_group;
+static GaEntryGroupService *mdns_service;
+
+static gint verbose = 0;
+
+G_GNUC_PRINTF (1, 2) static void
+my_error (const gchar *format, ...)
+{
+  va_list args;
+
+  g_fprintf (stderr, PACKAGE_NAME ": ");
+  va_start (args, format);
+  g_vfprintf (stderr, format, args);
+  va_end (args);
+  g_fprintf (stderr, "\n");
+
+  exit (1);
+}
+
+#ifdef G_OS_UNIX
+static gboolean
+sighup_received (gpointer user_data)
+{
+  GMainLoop *mainloop = user_data;
+
+  g_message ("Signal received, leaving");
+  g_main_loop_quit (mainloop);
+
+  return G_SOURCE_CONTINUE;
+}
+#endif
+
+static void
+mdns_register_service (void)
+{
+  GError *error = NULL;
+
+  if (!mdns_group)
+    {
+      mdns_group = ga_entry_group_new ();
+
+      if (!ga_entry_group_attach (mdns_group, mdns_client, &error))
+        {
+          g_warning ("Could not attach MDNS group to client: %s", error->message);
+          g_error_free (error);
+          return;
+        }
+    }
+
+  gchar *name = g_strdup_printf ("%s\'s public share", g_get_user_name ());
+  guint port = spice_webdav_get_port (dav);
+  mdns_service = ga_entry_group_add_service (mdns_group,
+                                             name, "_webdav._tcp",
+                                             port, &error,
+                                             NULL);
+  g_free (name);
+  if (!mdns_service)
+    {
+      g_warning ("Could not create service: %s", error->message);
+      g_error_free (error);
+      return;
+    }
+
+  gchar *record = g_strdup_printf ("u=,p=,path=/");
+  if (!ga_entry_group_service_set (mdns_service, name, record, &error))
+    {
+      g_warning ("Could not update TXT record: %s", error->message);
+      g_error_free (error);
+    }
+  g_free (record);
+
+  if (!ga_entry_group_commit (mdns_group, &error))
+    {
+      g_warning ("Could not announce MDNS service: %s", error->message);
+      g_error_free (error);
+      return;
+    }
+}
+
+static void
+mdns_state_changed (GaClient *client, GaClientState state, gpointer user_data)
+{
+  switch (state)
+    {
+    case GA_CLIENT_STATE_FAILURE:
+      g_warning ("MDNS client state failure");
+      break;
+
+    case GA_CLIENT_STATE_S_RUNNING:
+      g_debug ("MDNS client found server running");
+      mdns_register_service ();
+      break;
+
+    case GA_CLIENT_STATE_S_COLLISION:
+    case GA_CLIENT_STATE_S_REGISTERING:
+      g_message ("MDNS collision");
+      if (mdns_group)
+        {
+          ga_entry_group_reset (mdns_group, NULL);
+          mdns_service = 0;
+        }
+      break;
+
+    default:
+      // Do nothing
+      break;
+    }
+}
+
+int
+main (int argc, char *argv[])
+{
+  GError *error = NULL;
+  GOptionContext *context;
+  gint port = 8080;
+  gchar *path = NULL;
+  GMainLoop *mainloop = NULL;
+
+  int version = 0;
+  GOptionEntry entries[] = {
+    { "version", 0, 0, G_OPTION_ARG_NONE, &version, N_ ("Print program version"), NULL },
+    { "verbose", 'v', 0, G_OPTION_ARG_NONE, &verbose, N_ ("Be verbose"), NULL },
+    { "port", 'p', 0, G_OPTION_ARG_INT, &port, N_ ("Port to listen to"), NULL },
+    { "path", 'P', 0, G_OPTION_ARG_STRING, &path, N_ ("Path to export"), NULL },
+    { NULL }
+  };
+
+  setlocale (LC_ALL, "");
+  textdomain (GETTEXT_PACKAGE);
+  /* FIXME bindtextdomain (GETTEXT_PACKAGE, SPICE_GTK_LOCALEDIR); */
+  bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
+
+#if !GLIB_CHECK_VERSION (2, 35, 1)
+  g_type_init ();
+#endif
+  g_set_prgname (PACKAGE_NAME);
+
+  context = g_option_context_new (_ ("- simple webdav server"));
+  gchar *s = g_strdup_printf (_ ("Report bugs to <%s>"), PACKAGE_BUGREPORT);
+  g_option_context_set_description (context, s);
+  g_free (s);
+
+  g_option_context_add_main_entries (context, entries, GETTEXT_PACKAGE);
+  g_option_context_set_translation_domain (context, GETTEXT_PACKAGE);
+  if (!g_option_context_parse (context, &argc, &argv, &error))
+    my_error (_ ("option parsing failed: %s\n"), error->message);
+  g_option_context_free (context);
+
+  if (version)
+    {
+      g_printf (PACKAGE_STRING "\n");
+      return 0;
+    }
+
+  mainloop = g_main_loop_new (NULL, FALSE);
+
+#ifdef G_OS_UNIX
+  g_unix_signal_add (SIGINT, sighup_received, mainloop);
+#endif
+
+  dav = spice_webdav_new (port, path);
+
+  mdns_client = ga_client_new (GA_CLIENT_FLAG_NO_FLAGS);
+  g_signal_connect (mdns_client, "state-changed", G_CALLBACK (mdns_state_changed), NULL);
+  if (!ga_client_start (mdns_client, &error))
+    my_error (_ ("mdns failed: %s\n"), error->message);
+
+  spice_webdav_run (dav);
+  g_main_loop_run (mainloop);
+  spice_webdav_quit (dav);
+  g_main_loop_unref (mainloop);
+
+  g_object_unref (mdns_client);
+  g_object_unref (dav);
+
+  g_message ("Bye");
+
+  return 0;
+}
diff --git a/gtk/webdav/webdav.c b/gtk/webdav/webdav.c
new file mode 100644
index 0000000..54474d7
--- /dev/null
+++ b/gtk/webdav/webdav.c
@@ -0,0 +1,2855 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/*
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include <libsoup/soup.h>
+#include <libxml/parser.h>
+#include <libxml/tree.h>
+#include <libxml/xpath.h>
+#include <libxml/xpathInternals.h>
+
+#include <sys/types.h>
+#include <attr/xattr.h>
+
+#include "guuid.h"
+#include "webdav.h"
+
+struct _SpiceWebDAV
+{
+  GObject       parent;
+  GMainContext *context;
+  GMainLoop    *loop;
+  GThread      *thread;
+  SoupServer   *server;
+  GCancellable *cancellable;
+  gchar        *path;
+  gint          port;
+  GHashTable   *paths;
+};
+
+struct _SpiceWebDAVClass
+{
+  GObjectClass parent_class;
+};
+
+typedef struct _DAVLock DAVLock;
+typedef struct _Path    Path;
+
+G_DEFINE_TYPE (SpiceWebDAV, spice_webdav, G_TYPE_OBJECT)
+
+/* Properties */
+enum {
+  PROP_0,
+  PROP_PORT,
+  PROP_PATH,
+};
+
+static void server_callback (SoupServer        *server,
+                             SoupMessage       *msg,
+                             const char        *path,
+                             GHashTable        *query,
+                             SoupClientContext *context,
+                             gpointer           user_data);
+
+static void dav_lock_free (DAVLock *lock);
+
+struct _Path
+{
+  gchar  *path;
+  GList  *locks;
+  guint32 refs;
+};
+
+static Path *
+path_ref (Path *path)
+{
+  path->refs++;
+
+  return path;
+}
+
+static void
+path_unref (Path *path)
+{
+  path->refs--;
+
+  if (path->refs == 0)
+    {
+      g_list_free_full (path->locks, (GDestroyNotify) dav_lock_free);
+      g_free (path->path);
+      g_slice_free (Path, path);
+    }
+}
+
+static void
+path_remove_lock (Path *path, DAVLock *lock)
+{
+  g_return_if_fail (path);
+  g_return_if_fail (lock);
+
+  path->locks =  g_list_remove (path->locks, lock);
+}
+
+static void
+path_add_lock (Path *path, DAVLock *lock)
+{
+  g_return_if_fail (path);
+  g_return_if_fail (lock);
+
+  path->locks = g_list_append (path->locks, lock);
+}
+
+static void
+remove_trailing (gchar *str, gchar c)
+{
+  gsize len = strlen (str);
+
+  while (len > 0 && str[len - 1] == c)
+    len--;
+
+  str[len] = '\0';
+}
+
+static Path *
+get_path (SpiceWebDAV *self, const gchar *_path)
+{
+  Path *p;
+  gchar *path = g_strdup (_path);
+
+  remove_trailing (path, '/');
+  p = g_hash_table_lookup (self->paths, path);
+  if (!p)
+    {
+      p = g_slice_new0 (Path);
+      p->path = path;
+      g_hash_table_insert (self->paths, p->path, path_ref (p));
+    }
+
+  return p;
+}
+
+typedef enum _LockScopeType {
+  LOCK_SCOPE_NONE,
+  LOCK_SCOPE_EXCLUSIVE,
+  LOCK_SCOPE_SHARED,
+} LockScopeType;
+
+typedef enum _LockType {
+  LOCK_NONE,
+  LOCK_WRITE,
+} LockType;
+
+typedef enum _DepthType {
+  DEPTH_ZERO,
+  DEPTH_ONE,
+  DEPTH_INFINITY
+} DepthType;
+
+struct _DAVLock
+{
+  Path         *path;
+  gchar         token[45];
+  LockScopeType scope;
+  LockType      type;
+  DepthType     depth;
+  xmlNodePtr    owner;
+  guint64       timeout;
+};
+
+static void
+dav_lock_refresh_timeout (DAVLock *lock, guint timeout)
+{
+  if (timeout)
+    lock->timeout = g_get_monotonic_time () / G_USEC_PER_SEC + timeout;
+  else
+    lock->timeout = 0;
+}
+
+static DAVLock *
+dav_lock_new (Path *path, const gchar *token,
+              LockScopeType scope, LockType type,
+              DepthType depth, const xmlNodePtr owner,
+              guint timeout)
+{
+  DAVLock *lock;
+
+  g_return_val_if_fail (token, NULL);
+  g_return_val_if_fail (strlen (token) == sizeof (lock->token), NULL);
+
+  lock = g_slice_new0 (DAVLock);
+  lock->path = path_ref (path);
+  memcpy (lock->token, token, sizeof (lock->token));
+  lock->scope = scope;
+  lock->type = type;
+  lock->depth = depth;
+  if (owner)
+    lock->owner = xmlCopyNode (owner, 1);
+
+  dav_lock_refresh_timeout (lock, timeout);
+
+  return lock;
+}
+
+static void
+dav_lock_free (DAVLock *lock)
+{
+  g_return_if_fail (lock);
+
+  path_remove_lock (lock->path, lock);
+  path_unref (lock->path);
+
+  if (lock->owner)
+    xmlFreeNode (lock->owner);
+
+  g_slice_free (DAVLock, lock);
+}
+
+typedef struct _PathHandler
+{
+  SpiceWebDAV *self;
+  GFile       *file;
+} PathHandler;
+
+static PathHandler*
+path_handler_new (SpiceWebDAV *self, GFile *file)
+{
+  PathHandler *h = g_slice_new0 (PathHandler);
+
+  h->self = self;
+  h->file = file;
+  return h;
+}
+
+static void
+path_handler_free (PathHandler *h)
+{
+  g_object_unref (h->file);
+  g_slice_free (PathHandler, h);
+}
+
+typedef struct _Response
+{
+  GList *props;
+  gint   status;
+} Response;
+
+static Response*
+response_new (GList *props, gint status)
+{
+  Response *r;
+
+  g_return_val_if_fail (props != NULL || status > 0, NULL);
+
+  r = g_slice_new0 (Response);
+  r->status = status;
+  r->props = props;
+
+  return r;
+}
+
+static void
+response_free (Response *h)
+{
+  g_list_free_full (h->props, (GDestroyNotify) xmlFreeNode);
+  g_slice_free (Response, h);
+}
+
+static void
+spice_webdav_init (SpiceWebDAV *self)
+{
+  self->cancellable = g_cancellable_new ();
+  self->context = g_main_context_new ();
+  self->loop = g_main_loop_new (self->context, FALSE);
+
+  self->paths = g_hash_table_new_full (g_str_hash, g_str_equal,
+                                       NULL, (GDestroyNotify) path_unref);
+}
+
+static void
+spice_webdav_constructed (GObject *gobject)
+{
+  SpiceWebDAV *self = SPICE_WEBDAV (gobject);
+
+  self->server = soup_server_new (SOUP_SERVER_PORT, self->port,
+                                  SOUP_SERVER_SERVER_HEADER, "SpiceWebDAV ",
+                                  SOUP_SERVER_ASYNC_CONTEXT, self->context,
+                                  NULL);
+
+  soup_server_add_handler (self->server, NULL,
+                           server_callback,
+                           path_handler_new (self, g_file_new_for_path (self->path)),
+                           (GDestroyNotify) path_handler_free);
+
+  /* Chain up to the parent class */
+  if (G_OBJECT_CLASS (spice_webdav_parent_class)->constructed)
+    G_OBJECT_CLASS (spice_webdav_parent_class)->constructed (gobject);
+}
+
+static void
+spice_webdav_dispose (GObject *gobject)
+{
+  SpiceWebDAV *self = SPICE_WEBDAV (gobject);
+
+  g_clear_pointer (&self->path, g_free);
+  g_clear_pointer (&self->context, g_main_context_unref);
+  g_clear_pointer (&self->thread, g_thread_unref);
+  g_clear_pointer (&self->path, g_hash_table_unref);
+
+  /* Chain up to the parent class */
+  if (G_OBJECT_CLASS (spice_webdav_parent_class)->dispose)
+    G_OBJECT_CLASS (spice_webdav_parent_class)->dispose (gobject);
+}
+
+static void
+spice_webdav_get_property (GObject    *gobject,
+                           guint       prop_id,
+                           GValue     *value,
+                           GParamSpec *pspec)
+{
+  SpiceWebDAV *self = SPICE_WEBDAV (gobject);
+
+  switch (prop_id)
+    {
+    case PROP_PORT:
+      g_value_set_int (value, spice_webdav_get_port (self));
+      break;
+
+    case PROP_PATH:
+      g_value_set_string (value, self->path);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
+      break;
+    }
+}
+
+static void
+spice_webdav_set_property (GObject      *gobject,
+                           guint         prop_id,
+                           const GValue *value,
+                           GParamSpec   *pspec)
+{
+  SpiceWebDAV *self = SPICE_WEBDAV (gobject);
+
+  switch (prop_id)
+    {
+    case PROP_PORT:
+      self->port = g_value_get_int (value);
+      break;
+
+    case PROP_PATH:
+      g_clear_pointer (&self->path, g_free);
+      self->path = g_value_dup_string (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
+      break;
+    }
+}
+
+static void
+spice_webdav_class_init (SpiceWebDAVClass *klass)
+{
+  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+  gobject_class->dispose      = spice_webdav_dispose;
+  gobject_class->constructed  = spice_webdav_constructed;
+  gobject_class->get_property = spice_webdav_get_property;
+  gobject_class->set_property = spice_webdav_set_property;
+
+  g_object_class_install_property
+    (gobject_class, PROP_PORT,
+    g_param_spec_int ("port",
+                      "Port",
+                      "Port",
+                      0, G_MAXINT16, 0,
+                      G_PARAM_CONSTRUCT_ONLY |
+                      G_PARAM_READWRITE |
+                      G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_property
+    (gobject_class, PROP_PATH,
+    g_param_spec_string ("path",
+                         "Path",
+                         "Path",
+                         ".",
+                         G_PARAM_CONSTRUCT |
+                         G_PARAM_READWRITE |
+                         G_PARAM_STATIC_STRINGS));
+}
+
+static void
+node_debug (xmlNodePtr node)
+{
+  g_debug ("%s ns:%s", node->name, node->ns ? (gchar *) node->ns->href : "");
+}
+
+static gboolean
+node_has_ns (xmlNodePtr node, const char *ns_href)
+{
+  return node->ns && node->ns->href &&
+         !g_strcmp0 ((gchar *) node->ns->href, ns_href);
+
+}
+
+static gboolean
+node_has_name_ns (xmlNodePtr node, const char *name, const char *ns_href)
+{
+  gboolean has_name;
+  gboolean has_ns;
+
+  g_return_val_if_fail (node != NULL, FALSE);
+
+  has_name = has_ns = TRUE;
+
+  if (name)
+    has_name = !g_strcmp0 ((gchar *) node->name, name);
+
+  if (ns_href)
+    has_ns = node_has_ns (node, ns_href);
+
+  return has_name && has_ns;
+}
+
+static gboolean
+node_has_name (xmlNodePtr node, const char *name)
+{
+  g_return_val_if_fail (node != NULL, FALSE);
+
+  return node_has_name_ns (node, name, "DAV:");
+}
+
+static gboolean
+node_is_element (xmlNodePtr node)
+{
+  return node->type == XML_ELEMENT_NODE && node->name != NULL;
+}
+
+static LockScopeType
+parse_lockscope (xmlNodePtr rt)
+{
+  xmlNodePtr node;
+
+  for (node = rt->children; node; node = node->next)
+    if (node_is_element (node))
+      break;
+
+  if (node == NULL)
+    return LOCK_SCOPE_NONE;
+
+  if (!g_strcmp0 ((char *) node->name, "exclusive"))
+    return LOCK_SCOPE_EXCLUSIVE;
+  else if (!g_strcmp0 ((char *) node->name, "shared"))
+    return LOCK_SCOPE_SHARED;
+  else
+    return LOCK_SCOPE_NONE;
+}
+
+static const gchar *
+lockscope_to_string (LockScopeType type)
+{
+  if (type == LOCK_SCOPE_EXCLUSIVE)
+    return "exclusive";
+  else if (type == LOCK_SCOPE_SHARED)
+    return "shared";
+
+  g_return_val_if_reached (NULL);
+}
+
+static LockType
+parse_locktype (xmlNodePtr rt)
+{
+  xmlNodePtr node;
+
+  for (node = rt->children; node; node = node->next)
+    if (node_is_element (node))
+      break;
+
+  if (node == NULL)
+    return LOCK_NONE;
+
+  if (!g_strcmp0 ((char *) node->name, "write"))
+    return LOCK_WRITE;
+  else
+    return LOCK_NONE;
+}
+
+static const gchar *
+locktype_to_string (LockType type)
+{
+  if (type == LOCK_WRITE)
+    return "write";
+
+  g_return_val_if_reached (NULL);
+}
+
+static xmlDocPtr
+parse_xml (const gchar  *data,
+           const goffset len,
+           xmlNodePtr   *root,
+           const char   *name)
+{
+  xmlDocPtr doc;
+
+  doc = xmlReadMemory (data, len,
+                       "request.xml",
+                       NULL,
+                       XML_PARSE_NONET |
+                       XML_PARSE_NOWARNING |
+                       XML_PARSE_NOBLANKS |
+                       XML_PARSE_NSCLEAN |
+                       XML_PARSE_NOCDATA |
+                       XML_PARSE_COMPACT);
+  if (doc == NULL)
+    {
+      g_debug ("Could not parse request");
+      return NULL;
+    }
+  if (!(doc->properties & XML_DOC_NSVALID))
+    {
+      g_debug ("Could not parse request, NS errors");
+      xmlFreeDoc (doc);
+      return NULL;
+    }
+
+  *root = xmlDocGetRootElement (doc);
+
+  if (*root == NULL || (*root)->children == NULL)
+    {
+      g_debug ("Empty request");
+      xmlFreeDoc (doc);
+      return NULL;
+    }
+
+  if (g_strcmp0 ((char *) (*root)->name, name))
+    {
+      g_debug ("Unexpected request");
+      xmlFreeDoc (doc);
+      return NULL;
+    }
+
+  return doc;
+}
+
+
+static int
+compare_strings (gconstpointer a, gconstpointer b)
+{
+  const char **sa = (const char * *) a;
+  const char **sb = (const char * *) b;
+
+  return g_strcmp0 (*sa, *sb);
+}
+
+static GString *
+get_directory_listing (GFile *file, GCancellable *cancellable, GError **err)
+{
+  GString *listing;
+  GPtrArray *entries;
+  GFileEnumerator *e;
+  gchar *escaped;
+  gchar *name;
+  gint i;
+
+  e = g_file_enumerate_children (file, "standard::*", G_FILE_QUERY_INFO_NONE,
+                                 cancellable, err);
+  g_return_val_if_fail (e != NULL, NULL);
+
+  entries = g_ptr_array_new ();
+  while (1)
+    {
+      GFileInfo *info = g_file_enumerator_next_file (e, cancellable, err);
+      if (!info)
+        break;
+
+      escaped = g_markup_escape_text (g_file_info_get_name (info), -1);
+      g_ptr_array_add (entries, escaped);
+      g_object_unref (info);
+    }
+  g_file_enumerator_close (e, cancellable, NULL);
+  g_clear_object (&e);
+
+  g_ptr_array_sort (entries, compare_strings);
+
+  listing = g_string_new ("<html>\r\n");
+  name = g_file_get_basename (file);
+  escaped = g_markup_escape_text (name, -1);
+  g_free (name);
+  g_string_append_printf (listing, "<head><title>Index of %s</title></head>\r\n", escaped);
+  g_string_append_printf (listing, "<body><h1>Index of %s</h1>\r\n<p>\r\n", escaped);
+  g_free (escaped);
+  for (i = 0; i < entries->len; i++)
+    {
+      g_string_append_printf (listing, "<a href=\"%s\">%s</a><br>\r\n",
+                              (gchar *) entries->pdata[i],
+                              (gchar *) entries->pdata[i]);
+      g_free (entries->pdata[i]);
+    }
+  g_string_append (listing, "</body>\r\n</html>\r\n");
+
+  g_ptr_array_free (entries, TRUE);
+  return listing;
+}
+
+static gint
+do_get_file (SoupMessage *msg, GFile *file,
+             GCancellable *cancellable, GError **err)
+{
+  GError *error = NULL;
+  gint status = SOUP_STATUS_NOT_FOUND;
+  GFileInfo *info;
+  const gchar *etag;
+
+  info = g_file_query_info (file, "standard::*,etag::*",
+                            G_FILE_QUERY_INFO_NONE, cancellable, &error);
+  if (!info)
+    goto end;
+
+  if (g_file_info_get_file_type (info) == G_FILE_TYPE_DIRECTORY)
+    {
+      GString *listing;
+
+      listing = get_directory_listing (file, cancellable, err);
+      soup_message_set_response (msg, "text/html",
+                                 SOUP_MEMORY_TAKE,
+                                 listing->str, listing->len);
+      g_string_free (listing, FALSE);
+      status = SOUP_STATUS_OK;
+      goto end;
+    }
+
+  etag = g_file_info_get_etag (info);
+  g_warn_if_fail (etag != NULL);
+
+  if (etag)
+    {
+      gchar *tmp = g_strdup_printf ("\"%s\"", etag);
+      soup_message_headers_append (msg->response_headers, "ETag", tmp);
+      g_free (tmp);
+    }
+
+  if (msg->method == SOUP_METHOD_GET)
+    {
+      GMappedFile *mapping;
+      SoupBuffer *buffer;
+      gchar *path = g_file_get_path (file);
+
+      mapping = g_mapped_file_new (path, FALSE, NULL);
+      g_free (path);
+      if (!mapping)
+        {
+          status = SOUP_STATUS_INTERNAL_SERVER_ERROR;
+          goto end;
+        }
+
+      buffer = soup_buffer_new_with_owner (g_mapped_file_get_contents (mapping),
+                                           g_mapped_file_get_length (mapping),
+                                           mapping, (GDestroyNotify) g_mapped_file_unref);
+      soup_message_body_append_buffer (msg->response_body, buffer);
+      soup_buffer_free (buffer);
+      status = SOUP_STATUS_OK;
+    }
+  else if (msg->method == SOUP_METHOD_HEAD)
+    {
+      gchar *length;
+
+      /* We could just use the same code for both GET and
+       * HEAD (soup-message-server-io.c will fix things up).
+       * But we'll optimize and avoid the extra I/O.
+       */
+      length = g_strdup_printf ("%" G_GUINT64_FORMAT, g_file_info_get_size (info));
+      soup_message_headers_append (msg->response_headers, "Content-Length", length);
+
+      g_free (length);
+      status = SOUP_STATUS_OK;
+    }
+  else
+    g_warn_if_reached ();
+
+end:
+  if (error)
+    {
+      if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
+        {
+          g_debug ("getfile: %s", error->message);
+          g_clear_error (&error);
+        }
+      else
+        g_propagate_error (err, error);
+    }
+
+  g_clear_object (&info);
+  return status;
+}
+
+typedef struct _DavDoc     DavDoc;
+typedef struct _DDPropfind DDPropfind;
+
+struct _DavDoc
+{
+  xmlDocPtr  doc;
+  xmlNodePtr root;
+
+  SoupURI   *target;
+  char      *path;
+};
+
+struct _DDPropfind
+{
+  DavDoc    *dd;
+
+  xmlNodePtr prop_node;
+};
+
+static gboolean
+davdoc_parse (DavDoc *dd, SoupMessage *msg, SoupMessageBody *body,
+              const gchar *name)
+{
+  xmlDocPtr doc;
+  xmlNodePtr root;
+  SoupURI *uri;
+
+  doc = parse_xml (body->data, body->length, &root, name);
+  if (!doc)
+    return FALSE;
+
+  uri = soup_message_get_uri (msg);
+
+  dd->doc = doc;
+  dd->root = root;
+  dd->target = uri;
+  dd->path = g_uri_unescape_string (uri->path, "/");
+
+  return TRUE;
+}
+
+static void
+davdoc_free (DavDoc *dd)
+{
+  if (dd->doc)
+    xmlFreeDoc (dd->doc);
+  g_free (dd->path);
+}
+
+typedef enum _PropFindType {
+  PROPFIND_ALLPROP,
+  PROPFIND_PROPNAME,
+  PROPFIND_PROP
+} PropFindType;
+
+typedef struct _PropFind
+{
+  PropFindType type;
+  GHashTable  *props;
+} PropFind;
+
+static PropFind*
+propfind_new (void)
+{
+  PropFind *pf = g_slice_new0 (PropFind);
+
+  // TODO: a better hash for Node (name, ns)
+  pf->props = g_hash_table_new (g_direct_hash, g_direct_equal);
+  return pf;
+}
+
+static void
+propfind_free (PropFind *pf)
+{
+  if (!pf)
+    return;
+
+  g_hash_table_unref (pf->props);
+  g_slice_free (PropFind, pf);
+}
+
+static gboolean
+parse_prop (xmlNodePtr node, GHashTable *props)
+{
+  for (node = node->children; node; node = node->next)
+    {
+      if (!node_is_element (node))
+        continue;
+
+      // only interested in ns&name
+      g_hash_table_add (props, node);
+    }
+
+  return TRUE;
+}
+
+static PropFind*
+parse_propfind (xmlNodePtr xml)
+{
+  PropFind *pf = propfind_new ();
+  xmlNodePtr node;
+
+  for (node = xml->children; node; node = node->next)
+    {
+      if (!node_is_element (node))
+        continue;
+
+      if (node_has_name (node, "allprop"))
+        {
+          pf->type = PROPFIND_ALLPROP;
+          goto end;
+        }
+      else if (node_has_name (node, "propname"))
+        {
+          pf->type = PROPFIND_PROPNAME;
+          goto end;
+        }
+      else if (node_has_name (node, "prop"))
+        {
+          pf->type = PROPFIND_PROP;
+          parse_prop (node, pf->props);
+          goto end;
+        }
+    }
+
+  g_warn_if_reached ();
+  g_clear_pointer (&pf, propfind_free);
+
+end:
+  return pf;
+}
+
+static void
+node_to_string (xmlNodePtr root, xmlChar **mem, int *size)
+{
+  xmlDocPtr doc;
+
+  doc = xmlNewDoc (BAD_CAST "1.0");
+  xmlDocSetRootElement (doc, root);
+  // xmlReconciliateNs
+  xmlDocDumpMemoryEnc (doc, mem, size, "utf-8");
+  /* FIXME: validate document? */
+  /*FIXME, pretty print?*/
+  xmlFreeDoc (doc);
+}
+
+#define PROP_SET_STATUS(Node, Status) G_STMT_START {     \
+    (Node)->_private = GINT_TO_POINTER (Status);        \
+} G_STMT_END
+
+static xmlNodePtr
+prop_resourcetype (GFileInfo *info, xmlNsPtr ns)
+{
+  gint status = SOUP_STATUS_OK;
+  xmlNodePtr node = xmlNewNode (ns, BAD_CAST "resourcetype");
+
+  if (g_file_info_get_file_type (info) == G_FILE_TYPE_DIRECTORY)
+    xmlNewChild (node, ns, BAD_CAST "collection", NULL);
+  else if (!g_file_info_get_file_type (info) == G_FILE_TYPE_REGULAR)
+    {
+      g_warn_if_reached ();
+      status = SOUP_STATUS_NOT_FOUND;
+    }
+
+  PROP_SET_STATUS (node, status);
+  return node;
+}
+
+static gchar*
+status_to_string (gint status)
+{
+  return g_strdup_printf ("HTTP/1.1 %d %s",
+                          status, soup_status_get_phrase (status));
+}
+
+static gint
+node_compare_int (xmlNodePtr a,
+                  xmlNodePtr b)
+{
+  return GPOINTER_TO_INT (a->_private) - GPOINTER_TO_INT (b->_private);
+}
+
+static void
+node_add_time (xmlNodePtr node, guint64 time, SoupDateFormat format)
+{
+  SoupDate *date;
+  gchar *text;
+
+  g_warn_if_fail (time != 0);
+  date = soup_date_new_from_time_t (time);
+  text = soup_date_to_string (date, format);
+  xmlAddChild (node, xmlNewText (BAD_CAST text));
+  g_free (text);
+  soup_date_free (date);
+}
+
+static DepthType
+depth_from_string (const gchar *depth)
+{
+  if (!depth)
+    return DEPTH_INFINITY;
+  else if (!g_strcmp0 (depth, "0"))
+    return DEPTH_ZERO;
+  else if (!g_strcmp0 (depth, "1"))
+    return DEPTH_ONE;
+  else if (!g_strcmp0 (depth, "infinity"))
+    return DEPTH_INFINITY;
+
+  g_warning ("Invalid depth: %s", depth);
+  return DEPTH_INFINITY;
+}
+
+static guint
+timeout_from_string (const gchar *timeout)
+{
+  if (!timeout ||
+      !g_strcmp0 (timeout, "Infinite"))
+    return 0;
+
+  if (!g_ascii_strncasecmp (timeout, "Second-", 7))
+    return g_ascii_strtoull (timeout + 7, NULL, 10);
+
+  g_return_val_if_reached (0);
+}
+
+static const gchar *
+depth_to_string (DepthType depth)
+{
+  if (depth == DEPTH_INFINITY)
+    return "infinity";
+  if (depth == DEPTH_ZERO)
+    return "0";
+  if (depth == DEPTH_ONE)
+    return "1";
+
+  g_return_val_if_reached (NULL);
+}
+
+static gchar *
+node_get_xattr_name (xmlNodePtr node, gchar *prefix)
+{
+  const gchar *ns = node->ns ? (gchar *) node->ns->href : NULL;
+  const gchar *name = (gchar *) node->name;
+
+  if (!name)
+    return NULL;
+
+  if (ns)
+    return g_strdup_printf ("%s%s#%s", prefix, ns, name);
+  else
+    return g_strdup_printf ("%s%s", prefix, name);
+}
+
+static xmlNodePtr
+get_activelock_node (const DAVLock *lock,
+                     xmlNsPtr       ns)
+{
+  xmlNodePtr node, active;
+
+  active = xmlNewNode (ns, BAD_CAST "activelock");
+
+  node = xmlNewChild (active, ns, BAD_CAST "locktype", NULL);
+  xmlNewChild (node, ns, BAD_CAST locktype_to_string (lock->type), NULL);
+  node = xmlNewChild (active, ns, BAD_CAST "lockscope", NULL);
+  xmlNewChild (node, ns, BAD_CAST lockscope_to_string (lock->scope), NULL);
+  node = xmlNewChild (active, ns, BAD_CAST "depth", NULL);
+  xmlAddChild (node, xmlNewText (BAD_CAST depth_to_string (lock->depth)));
+
+  if (lock->owner)
+    xmlAddChild (active, xmlCopyNode (lock->owner, 1));
+
+  node = xmlNewChild (active, ns, BAD_CAST "locktoken", NULL);
+  node = xmlNewChild (node, ns, BAD_CAST "href", NULL);
+  xmlAddChild (node, xmlNewText (BAD_CAST lock->token));
+
+  node = xmlNewChild (active, ns, BAD_CAST "lockroot", NULL);
+  node = xmlNewChild (node, ns, BAD_CAST "href", NULL);
+  xmlAddChild (node, xmlNewText (BAD_CAST lock->path->path));
+  if (lock->timeout)
+    {
+      gchar *tmp = g_strdup_printf ("Second-%" G_GINT64_FORMAT, lock->timeout -
+                                    g_get_monotonic_time () / G_USEC_PER_SEC);
+      node = xmlNewChild (active, ns, BAD_CAST "timeout", NULL);
+      xmlAddChild (node, xmlNewText (BAD_CAST tmp));
+      g_free (tmp);
+    }
+
+  return active;
+}
+
+typedef gboolean (* PathCb) (const gchar *key,
+                             Path        *path,
+                             gpointer     data);
+
+static gboolean
+foreach_parent_path (SpiceWebDAV *self, const gchar *path, PathCb cb, gpointer data)
+{
+  gchar **pathv, *partial = g_strdup ("/");
+  gboolean ret = FALSE;
+  gchar *key = NULL;
+  Path *p;
+  gint i;
+
+  pathv = g_strsplit (path, "/", -1);
+
+  for (i = 0; pathv[i]; i++)
+    {
+      if (!*pathv[i])
+        continue;
+
+      gchar *tmp = g_build_path ("/", partial, pathv[i], NULL);
+      g_free (partial);
+      partial = tmp;
+
+      if (g_hash_table_lookup_extended (self->paths, partial,
+                                        (gpointer *) &key, (gpointer *) &p))
+        {
+          if (!cb (key, p, data))
+            goto end;
+        }
+    }
+
+  ret = TRUE;
+
+end:
+  g_strfreev (pathv);
+  g_free (partial);
+
+  return ret;
+}
+
+typedef struct _PathGetLock
+{
+  const gchar *token;
+  DAVLock     *lock;
+} PathGetLock;
+
+static gboolean
+_path_get_lock (const gchar *key, Path *path, gpointer data)
+{
+  PathGetLock *d = data;
+  GList *l;
+
+  for (l = path->locks; l != NULL; l = l->next)
+    {
+      DAVLock *lock = l->data;
+
+      if (!g_strcmp0 (lock->token, d->token))
+        {
+          d->lock = lock;
+          return FALSE;
+        }
+    }
+
+  return TRUE;
+}
+
+static DAVLock *
+path_get_lock (SpiceWebDAV *self, const gchar *path, const gchar *token)
+{
+  PathGetLock p = { .token = token };
+  gboolean success = !foreach_parent_path (self, path,
+                                           _path_get_lock, (gpointer) &p);
+
+  if (!success)
+    g_message ("Invalid lock token %s for %s", token, path);
+
+  return p.lock;
+}
+
+static xmlNodePtr
+prop_supportedlock (xmlNsPtr ns)
+{
+  xmlNodePtr node = xmlNewNode (ns, BAD_CAST "supportedlock");
+
+  {
+    xmlNodePtr entry = xmlNewChild (node, NULL, BAD_CAST "lockentry", NULL);
+    xmlNodePtr scope = xmlNewChild (entry, NULL, BAD_CAST "lockscope", NULL);
+    xmlNewChild (scope, NULL, BAD_CAST "exclusive", NULL);
+    xmlNodePtr type = xmlNewChild (entry, NULL, BAD_CAST "locktype", NULL);
+    xmlNewChild (type, NULL, BAD_CAST "write", NULL);
+  }
+
+  {
+    xmlNodePtr entry = xmlNewChild (node, NULL, BAD_CAST "lockentry", NULL);
+    xmlNodePtr scope = xmlNewChild (entry, NULL, BAD_CAST "lockscope", NULL);
+    xmlNewChild (scope, NULL, BAD_CAST "shared", NULL);
+    xmlNodePtr type = xmlNewChild (entry, NULL, BAD_CAST "locktype", NULL);
+    xmlNewChild (type, NULL, BAD_CAST "write", NULL);
+  }
+
+  PROP_SET_STATUS (node, SOUP_STATUS_OK);
+  return node;
+}
+
+static gboolean
+lockdiscovery_cb (const gchar *key, Path *path, gpointer data)
+{
+  xmlNodePtr node = data;
+  GList *l;
+
+  g_return_val_if_fail (key, FALSE);
+  g_return_val_if_fail (path, FALSE);
+
+  for (l = path->locks; l != NULL; l = l->next)
+    xmlAddChild (node, get_activelock_node (l->data, NULL));
+
+  return TRUE;
+}
+
+static xmlNodePtr
+prop_lockdiscovery (SpiceWebDAV *self, const gchar *path, xmlNsPtr ns)
+{
+  xmlNodePtr node = xmlNewNode (ns, BAD_CAST "lockdiscovery");
+
+  foreach_parent_path (self, path, lockdiscovery_cb, node);
+
+  PROP_SET_STATUS (node, SOUP_STATUS_OK);
+  return node;
+}
+
+static xmlNodePtr
+prop_creationdate (GFileInfo *info, xmlNsPtr ns)
+{
+  gint status = SOUP_STATUS_OK;
+  xmlNodePtr node = xmlNewNode (ns, BAD_CAST "creationdate");
+  guint64 time;
+
+
+  time = g_file_info_get_attribute_uint64 (info,
+                                           G_FILE_ATTRIBUTE_TIME_CREATED);
+
+  /* windows seems to want this, even apache returns modified time */
+  if (time == 0)
+    time = g_file_info_get_attribute_uint64 (info,
+                                             G_FILE_ATTRIBUTE_TIME_MODIFIED);
+
+  if (time == 0)
+    status = SOUP_STATUS_NOT_FOUND;
+  else
+    node_add_time (node, time, SOUP_DATE_HTTP);
+
+  PROP_SET_STATUS (node, status);
+  return node;
+}
+
+static xmlNodePtr
+prop_getlastmodified (GFileInfo *info, xmlNsPtr ns)
+{
+  gint status = SOUP_STATUS_OK;
+  xmlNodePtr node = xmlNewNode (ns, BAD_CAST "getlastmodified");
+  guint64 time = g_file_info_get_attribute_uint64 (info,
+                                                   G_FILE_ATTRIBUTE_TIME_MODIFIED);
+
+  if (time == 0)
+    status = SOUP_STATUS_NOT_FOUND;
+  else
+    node_add_time (node, time, SOUP_DATE_ISO8601);
+
+  PROP_SET_STATUS (node, status);
+  return node;
+}
+
+static xmlNodePtr
+prop_getcontentlength (GFileInfo *info, xmlNsPtr ns)
+{
+  gint status = SOUP_STATUS_OK;
+  xmlNodePtr node = xmlNewNode (ns, BAD_CAST "getcontentlength");
+  guint64 size = g_file_info_get_attribute_uint64 (info,
+                                                   G_FILE_ATTRIBUTE_STANDARD_SIZE);
+  gchar *text = g_strdup_printf ("%" G_GUINT64_FORMAT, size);
+
+  xmlAddChild (node, xmlNewText (BAD_CAST text));
+  g_free (text);
+
+  PROP_SET_STATUS (node, status);
+  return node;
+}
+
+static xmlNodePtr
+prop_getcontenttype (GFileInfo *info, xmlNsPtr ns)
+{
+  gint status = SOUP_STATUS_OK;
+  xmlNodePtr node = xmlNewNode (ns, BAD_CAST "getcontenttype");
+  const gchar *type = g_file_info_get_attribute_string (info,
+                                                        G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE);
+
+  if (type == NULL)
+    status = SOUP_STATUS_NOT_FOUND;
+  else
+    xmlAddChild (node, xmlNewText (BAD_CAST type));
+
+  PROP_SET_STATUS (node, status);
+  return node;
+}
+
+static xmlNodePtr
+prop_displayname (GFileInfo *info, xmlNsPtr ns)
+{
+  gint status = SOUP_STATUS_OK;
+  xmlNodePtr node = xmlNewNode (ns, BAD_CAST "displayname");
+  const gchar *name = g_file_info_get_display_name (info);
+
+  if (name == NULL)
+    status = SOUP_STATUS_NOT_FOUND;
+  else
+    xmlAddChild (node, xmlNewText (BAD_CAST name));
+
+  PROP_SET_STATUS (node, status);
+  return node;
+}
+
+static xmlNodePtr
+prop_getetag (GFileInfo *info, xmlNsPtr ns)
+{
+  gint status = SOUP_STATUS_OK;
+  xmlNodePtr node = xmlNewNode (ns, BAD_CAST "getetag");
+  const gchar *etag = g_file_info_get_etag (info);
+
+  if (etag)
+    {
+      gchar *tmp = g_strdup_printf ("\"%s\"", etag);
+      xmlAddChild (node, xmlNewText (BAD_CAST tmp));
+      g_free (tmp);
+    }
+  else
+    {
+      status = SOUP_STATUS_NOT_FOUND;
+    }
+
+  PROP_SET_STATUS (node, status);
+  return node;
+}
+
+static xmlNodePtr
+prop_executable (GFileInfo *info)
+{
+  gint status = SOUP_STATUS_OK;
+  xmlNodePtr node = xmlNewNode (NULL, BAD_CAST "executable");
+
+  xmlNewNs (node, BAD_CAST "http://apache.org/dav/props/", NULL);
+
+  guint64 exec = g_file_info_get_attribute_boolean (info,
+                                                    G_FILE_ATTRIBUTE_ACCESS_CAN_EXECUTE);
+
+  if (g_file_info_get_file_type (info) == G_FILE_TYPE_DIRECTORY)
+    exec = FALSE;
+
+  xmlAddChild (node, xmlNewText (exec ?  BAD_CAST "T" : BAD_CAST "F"));
+
+  PROP_SET_STATUS (node, status);
+  return node;
+}
+
+static void
+prop_add (GList **stat, xmlNodePtr node)
+{
+  *stat = g_list_insert_sorted (*stat, node, (GCompareFunc) node_compare_int);
+}
+
+#define FILE_QUERY "standard::*,time::*,access::*,etag::*,xattr::*"
+
+static const struct _PropList
+{
+  const gchar *name;
+  const gchar *ns;
+} prop_list[] = {
+  { "resourcetype", "DAV:" },
+  { "supportedlock", "DAV:" },
+  { "lockdiscovery", "DAV:" },
+  { "supportedlock", "DAV:" },
+};
+
+static GList*
+propfind_populate (SpiceWebDAV *self, const gchar *path,
+                   PropFind *pf, GFileInfo *info)
+{
+  GHashTableIter iter;
+  xmlNodePtr node;
+  GList *stat = NULL;
+  xmlNsPtr ns = xmlNewNs (NULL, BAD_CAST "DAV:", BAD_CAST "D");
+
+  g_warn_if_fail (info != NULL); // FIXME
+
+  if (pf->type == PROPFIND_ALLPROP)
+    {
+      if (info)
+        {
+          /* perhaps not include the 404? */
+          prop_add (&stat, prop_resourcetype (info, ns));
+          prop_add (&stat, prop_creationdate (info, ns));
+          prop_add (&stat, prop_getlastmodified (info, ns));
+          prop_add (&stat, prop_getcontentlength (info, ns));
+          prop_add (&stat, prop_getcontenttype (info, ns));
+          prop_add (&stat, prop_displayname (info, ns));
+          prop_add (&stat, prop_getetag (info, ns));
+          prop_add (&stat, prop_executable (info));
+        }
+
+      prop_add (&stat, prop_supportedlock (ns));
+      prop_add (&stat, prop_lockdiscovery (self, path, ns));
+
+      return stat;
+    }
+
+  g_hash_table_iter_init (&iter, pf->props);
+  while (g_hash_table_iter_next (&iter, (void * *) &node, NULL))
+    {
+      if (node_has_name (node, "lockdiscovery"))
+        node = prop_lockdiscovery (self, path, ns);
+      else if (node_has_name (node, "supportedlock"))
+        node = prop_supportedlock (ns);
+      else if (node_has_name (node, "resourcetype"))
+        node = prop_resourcetype (info, ns);
+      else if (node_has_name (node, "creationdate"))
+        node = prop_creationdate (info, ns);
+      else if (node_has_name (node, "getlastmodified"))
+        node = prop_getlastmodified (info, ns);
+      else if (node_has_name (node, "getcontentlength"))
+        node = prop_getcontentlength (info, ns);
+      else if (node_has_name (node, "getcontenttype"))
+        node = prop_getcontenttype (info, ns);
+      else if (node_has_name (node, "displayname"))
+        node = prop_displayname (info, ns);
+      else if (node_has_name (node, "getetag"))
+        node = prop_getetag (info, ns);
+      else if (node_has_name_ns (node, "executable", "http://apache.org/dav/props/"))
+        node = prop_executable (info);
+      /* } else if (node_has_name (node, "quota-available-bytes")) { */
+      /* } else if (node_has_name (node, "quota-used-bytes")) { */
+      else
+        {
+          gchar *xattr = node_get_xattr_name (node, "xattr::");
+          node = xmlCopyNode (node, 2);
+          const gchar *val = NULL;
+
+          if (xattr)
+            {
+              val = g_file_info_get_attribute_string (info, xattr);
+              g_free (xattr);
+            }
+
+          if (val)
+            {
+              xmlAddChild (node, xmlNewText (BAD_CAST val));
+              PROP_SET_STATUS (node, SOUP_STATUS_OK);
+            }
+          else
+            {
+              node_debug (node);
+              PROP_SET_STATUS (node, SOUP_STATUS_NOT_FOUND);
+            }
+        }
+
+      prop_add (&stat, node);
+    }
+
+  return stat;
+}
+
+static gint
+propfind_query_zero (PathHandler *handler, PropFind *pf,
+                     const gchar *path, GHashTable *path_resp)
+{
+  SpiceWebDAV *self = handler->self;
+  GError *err = NULL;
+  GFileInfo *info = NULL;
+  GFile *file;
+  GList *stat = NULL;
+  gint status = SOUP_STATUS_OK;
+
+  file = g_file_get_child (handler->file, path + 1);
+  info = g_file_query_info (file, FILE_QUERY,
+                            G_FILE_QUERY_INFO_NONE, self->cancellable, &err);
+  g_object_unref (file);
+  if (err)
+    {
+      if (!g_error_matches (err, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
+        g_warning ("queryinfo: %s", err->message);
+      g_clear_error (&err);
+      return SOUP_STATUS_NOT_FOUND;
+    }
+
+  stat = propfind_populate (self, path, pf, info);
+  g_hash_table_insert (path_resp, g_strdup (path),
+                       response_new (stat, 0));
+  g_clear_object (&info);
+
+  return status;
+}
+
+static gint
+propfind_query_one (PathHandler *handler, PropFind *pf,
+                    const gchar *path, GHashTable *path_resp)
+{
+  SpiceWebDAV *self = handler->self;
+  GError *err = NULL;
+  GFile *file;
+  GFileEnumerator *e;
+  gint status;
+
+  status = propfind_query_zero (handler, pf, path, path_resp);
+  if (status != SOUP_STATUS_OK)
+    return status;
+
+  file = g_file_get_child (handler->file, path + 1);
+  e = g_file_enumerate_children (file, FILE_QUERY, G_FILE_QUERY_INFO_NONE,
+                                 self->cancellable, &err);
+  g_object_unref (file);
+  if (!e)
+    goto end;
+
+  while (1)
+    {
+      GList *stat;
+      GFileInfo *info = g_file_enumerator_next_file (e, self->cancellable, &err);
+      if (!info)
+        break;
+
+      gchar *escape = g_markup_escape_text (g_file_info_get_name (info), -1);
+      stat = propfind_populate (self, path, pf, info);
+      g_hash_table_insert (path_resp, g_build_path ("/", path, escape, NULL),
+                           response_new (stat, 0));
+      g_free (escape);
+      g_object_unref (info);
+    }
+
+  g_file_enumerator_close (e, self->cancellable, NULL);
+  g_clear_object (&e);
+
+end:
+  if (err)
+    {
+      if (!g_error_matches (err, G_IO_ERROR, G_IO_ERROR_NOT_DIRECTORY))
+        g_warning ("query: %s", err->message);
+      g_clear_error (&err);
+    }
+
+  return status;
+}
+
+static xmlNodePtr
+status_node_new (gint status)
+{
+  xmlNodePtr node;
+  gchar *text;
+
+  text = status_to_string (status);
+  node = xmlNewNode (NULL, BAD_CAST "D:status");
+  xmlAddChild (node, xmlNewText (BAD_CAST text));
+  g_free (text);
+
+  return node;
+}
+
+static void
+add_propstat (xmlNodePtr parent, SoupMessage *msg,
+              const gchar *path, GList *props)
+{
+  xmlNodePtr node, propstat, prop = NULL, stnode = NULL;
+  GList *s;
+  gint status = -1;
+
+  /* better if sorted by status */
+  for (s = props; s != NULL; s = s->next)
+    {
+      node = s->data;
+      if (GPOINTER_TO_INT (node->_private) != status)
+        {
+          status = GPOINTER_TO_INT (node->_private);
+          if (stnode)
+            xmlAddChild (propstat, stnode);
+
+          stnode = status_node_new (status);
+          propstat = xmlNewChild (parent, NULL, BAD_CAST "D:propstat", NULL);
+          prop = xmlNewChild (propstat, NULL, BAD_CAST "D:prop", NULL);
+        }
+      g_return_if_fail (prop != NULL);
+      xmlAddChild (prop, node);
+    }
+
+  if (stnode)
+    xmlAddChild (propstat, stnode);
+}
+
+static gint
+response_multistatus (SoupMessage *msg,
+                      GHashTable  *mstatus)
+{
+  xmlChar *mem = NULL;
+  int size;
+  xmlNodePtr root;
+  GHashTableIter iter;
+  Response *resp;
+  gchar *path, *text;
+
+  root = xmlNewNode (NULL, BAD_CAST "D:multistatus");
+  xmlNewNs (root, BAD_CAST "DAV:", BAD_CAST "D");
+
+  g_hash_table_iter_init (&iter, mstatus);
+  while (g_hash_table_iter_next (&iter, (gpointer *) &path, (gpointer *) &resp))
+    {
+      xmlNodePtr response;
+      SoupURI *new_uri;
+
+      response = xmlNewChild (root, NULL, BAD_CAST "D:response", NULL);
+      new_uri = soup_uri_new_with_base (soup_message_get_uri (msg), path);
+      text = soup_uri_to_string (new_uri, FALSE);
+      xmlNewChild (response, NULL, BAD_CAST "D:href", BAD_CAST text);
+      g_free (text);
+      soup_uri_free (new_uri);
+
+      if (resp->props)
+        {
+          add_propstat (response, msg, path, resp->props);
+          resp->props = NULL;
+        }
+      else if (resp->status)
+        xmlAddChild (response, status_node_new (resp->status));
+    }
+
+  node_to_string (root, &mem, &size);
+  soup_message_set_response (msg, "application/xml",
+                             SOUP_MEMORY_TAKE, (gchar *) mem, size);
+
+  return SOUP_STATUS_MULTI_STATUS;
+}
+
+static gint
+method_propfind (PathHandler *handler, SoupMessage *msg,
+                 const char *path, GError **err)
+{
+  PropFind *pf = NULL;
+  DepthType depth;
+  GHashTable *mstatus = NULL;   // path -> statlist
+  DavDoc doc = {0, };
+  gint status = SOUP_STATUS_NOT_FOUND;
+
+  depth = depth_from_string (soup_message_headers_get_one (msg->request_headers, "Depth"));
+  if (!msg->request_body || !msg->request_body->length)
+    {
+      /* Win kludge: http://code.google.com/p/sabredav/wiki/Windows */
+      pf = propfind_new ();
+      pf->type = PROPFIND_ALLPROP;
+    }
+  else
+    {
+      if (!davdoc_parse (&doc, msg, msg->request_body, "propfind"))
+        {
+          status = SOUP_STATUS_BAD_REQUEST;
+          goto end;
+        }
+
+      pf = parse_propfind (doc.root);
+      if (!pf)
+        goto end;
+    }
+
+  mstatus = g_hash_table_new_full (g_str_hash, g_str_equal, g_free,
+                                   (GDestroyNotify) response_free);
+  if (pf->type == PROPFIND_PROP || pf->type == PROPFIND_ALLPROP)
+    {
+      if (depth == DEPTH_ZERO)
+        {
+          soup_message_headers_append (msg->response_headers, "Depth", "0");
+          status = propfind_query_zero (handler, pf, path, mstatus);
+        }
+      else if (depth == DEPTH_ONE)
+        {
+          soup_message_headers_append (msg->response_headers, "Depth", "1");
+          status = propfind_query_one (handler, pf, path, mstatus);
+        }
+      else
+        {
+          /* soup_message_headers_append (msg->response_headers, "Depth", "infinity"); */
+          status = SOUP_STATUS_FORBIDDEN;
+          g_warn_if_reached ();
+        }
+    }
+  else
+    g_warn_if_reached ();
+
+  if (status != SOUP_STATUS_OK)
+    goto end;
+
+  status = response_multistatus (msg, mstatus);
+
+end:
+  davdoc_free (&doc);
+  propfind_free (pf);
+  if (mstatus)
+    g_hash_table_unref (mstatus);
+  return status;
+}
+
+static gint
+set_attr (SpiceWebDAV *self, GFile *file, xmlNodePtr attrnode,
+          GFileAttributeType type, gchar *mem)
+{
+  gchar *attrname;
+  gint status = SOUP_STATUS_OK;
+  GError *error = NULL;
+
+  if (type == G_FILE_ATTRIBUTE_TYPE_INVALID)
+    {
+      attrname = node_get_xattr_name (attrnode, "user.");
+      g_return_val_if_fail (attrname, SOUP_STATUS_BAD_REQUEST);
+
+      /* https://bugzilla.gnome.org/show_bug.cgi?id=720506 */
+      gchar *path = g_file_get_path (file);
+      removexattr (path, attrname);
+      g_free (path);
+    }
+  else
+    {
+      attrname = node_get_xattr_name (attrnode, "xattr::");
+      g_return_val_if_fail (attrname, SOUP_STATUS_BAD_REQUEST);
+
+      g_file_set_attribute (file, attrname, type, mem,
+                            G_FILE_QUERY_INFO_NONE, self->cancellable, &error);
+    }
+
+  g_free (attrname);
+
+  if (error)
+    {
+      g_warning ("failed to set property: %s", error->message);
+      g_clear_error (&error);
+      status = SOUP_STATUS_NOT_FOUND;
+    }
+
+  return status;
+}
+
+static xmlBufferPtr
+node_children_to_string (xmlNodePtr node)
+{
+  xmlBufferPtr buf = xmlBufferCreate ();
+
+  for (node = node->children; node; node = node->next)
+    xmlNodeDump (buf, node->doc, node, 0, 0);
+
+  return buf;
+}
+
+static gint
+prop_set (SpiceWebDAV *self, SoupMessage *msg,
+          GFile *file, xmlNodePtr parent, xmlNodePtr *attr,
+          gboolean remove)
+{
+  xmlNodePtr node, attrnode;
+  gint type = G_FILE_ATTRIBUTE_TYPE_INVALID;
+  gint status;
+
+  for (node = parent->children; node; node = node->next)
+    {
+      if (!node_is_element (node))
+        continue;
+
+      if (node_has_name (node, "prop"))
+        {
+          xmlBufferPtr buf = NULL;
+
+          attrnode = node->children;
+          if (!node_is_element (attrnode))
+            continue;
+
+          if (!remove)
+            {
+              *attr = xmlCopyNode (attrnode, 2);
+
+              buf = node_children_to_string (attrnode);
+              type = G_FILE_ATTRIBUTE_TYPE_STRING;
+            }
+
+          status = set_attr (self, file, attrnode, type, (gchar *) xmlBufferContent (buf));
+
+          if (buf)
+            xmlBufferFree (buf);
+
+          return status;
+        }
+    }
+
+  g_return_val_if_reached (SOUP_STATUS_BAD_REQUEST);
+}
+
+static gint
+method_get (PathHandler *handler, SoupMessage *msg, const char *path, GError **err)
+{
+  GFile *file;
+  SpiceWebDAV *self = handler->self;
+  gint status;
+
+  file = g_file_get_child (handler->file, path + 1);
+  status = do_get_file (msg, file, self->cancellable, err);
+  g_object_unref (file);
+
+  return status;
+}
+
+static gint
+do_put_file (SoupMessage *msg, GFile *file,
+             GCancellable *cancellable, GError **err)
+{
+  GFileOutputStream *s;
+  gchar *etag = NULL;
+  gsize bytes_written;
+  gboolean created = TRUE;
+  SoupMessageHeaders *headers = msg->request_headers;
+  gint status = SOUP_STATUS_INTERNAL_SERVER_ERROR;
+
+  if (g_file_query_exists (file, cancellable))
+    created = FALSE;
+
+  if (soup_message_headers_get_list (headers, "If-Match"))
+    g_warn_if_reached ();
+  else if (soup_message_headers_get_list (headers, "If-None-Match"))
+    g_warn_if_reached ();
+  else if (soup_message_headers_get_list (headers, "Expect"))
+    g_warn_if_reached ();
+
+  s = g_file_replace (file, etag, FALSE, G_FILE_CREATE_PRIVATE, cancellable, err);
+  if (!s)
+    goto end;
+
+  if (!g_output_stream_write_all (G_OUTPUT_STREAM (s),
+                                  msg->request_body->data, msg->request_body->length,
+                                  &bytes_written, cancellable, err))
+    goto end;
+
+  status = created ? SOUP_STATUS_CREATED : SOUP_STATUS_OK;
+
+end:
+  g_object_unref (s);
+  return status;
+}
+
+typedef struct _LockSubmitted
+{
+  gchar *path;
+  gchar *token;
+} LockSubmitted;
+
+static LockSubmitted *
+lock_submitted_new (const gchar *path, const gchar *token)
+{
+  LockSubmitted *l;
+
+  g_return_val_if_fail (path, NULL);
+  g_return_val_if_fail (token, NULL);
+
+  l = g_slice_new (LockSubmitted);
+
+  l->path = g_strdup (path);
+  l->token = g_strdup (token);
+
+  remove_trailing (l->path, '/');
+
+  return l;
+}
+
+static void
+lock_submitted_free (LockSubmitted *l)
+{
+  g_free (l->path);
+  g_free (l->token);
+  g_slice_free (LockSubmitted, l);
+}
+
+static gboolean
+locks_submitted_has (GList *locks, DAVLock *lock)
+{
+  GList *l;
+
+  for (l = locks; l != NULL; l = l->next)
+    {
+      LockSubmitted *sub = l->data;
+      if (!g_strcmp0 (sub->path, lock->path->path) &&
+          !g_strcmp0 (sub->token, lock->token))
+        return TRUE;
+    }
+
+  g_message ("missing lock: %s %s", lock->path->path, lock->token);
+
+  return FALSE;
+}
+
+static gboolean
+other_lock_exists (const gchar *key, Path *path, gpointer data)
+{
+  GList *locks = data;
+  GList *l;
+
+  for (l = path->locks; l != NULL; l = l->next)
+    {
+      DAVLock *lock = l->data;
+      if (!locks_submitted_has (locks, lock))
+        return FALSE;
+    }
+
+  return TRUE;
+}
+
+static gboolean
+path_has_other_locks (SpiceWebDAV *self, const gchar *path, GList *locks)
+{
+  return !foreach_parent_path (self, path, other_lock_exists, locks);
+}
+
+typedef struct _IfState
+{
+  gchar   *cur;
+  gchar   *path;
+  GList   *locks;
+  gboolean error;
+} IfState;
+
+static gboolean
+eat_whitespaces (IfState *state)
+{
+  while (*state->cur && strchr (" \f\n\r\t\v", *state->cur))
+    state->cur++;
+
+  return !*state->cur;
+}
+
+static gboolean
+next_token (IfState *state, const gchar *token)
+{
+  eat_whitespaces (state);
+
+  return g_str_has_prefix (state->cur, token);
+}
+
+static gboolean
+accept_token (IfState *state, const gchar *token)
+{
+  gboolean success = next_token (state, token);
+
+  if (success)
+    state->cur += strlen (token);
+
+  return success;
+}
+
+static const gchar*
+accept_ref (IfState *state)
+{
+  gchar *url, *end;
+
+  if (!accept_token (state, "<"))
+    return FALSE;
+
+  url = state->cur;
+  end = strchr (state->cur, '>');
+  if (end)
+    {
+      *end = '\0';
+      state->cur = end + 1;
+      return url;
+    }
+
+  return NULL;
+}
+
+static gchar*
+accept_etag (IfState *state)
+{
+  GString *str = NULL;
+  gboolean success = FALSE;
+
+  str = g_string_sized_new (strlen (state->cur));
+
+  if (!accept_token (state, "["))
+    goto end;
+
+  if (!accept_token (state, "\""))
+    goto end;
+
+  while (*state->cur)
+    {
+      if (*state->cur == '"')
+        break;
+      else if (*state->cur == '\\')
+        state->cur++;
+
+      g_string_append_c (str, *state->cur);
+      state->cur++;
+    }
+
+  if (!accept_token (state, "\""))
+    goto end;
+
+  if (!accept_token (state, "]"))
+    goto end;
+
+  success = TRUE;
+
+end:
+  return g_string_free (str, !success);
+}
+
+static gboolean
+check_token (PathHandler *handler, const gchar *path, const gchar *token)
+{
+  SpiceWebDAV *self = handler->self;
+
+  g_debug ("check %s for %s", token, path);
+
+  if (!g_strcmp0 (token, "DAV:no-lock"))
+    return FALSE;
+
+  return !!path_get_lock (self, path, token);
+}
+
+static gboolean
+check_etag (PathHandler *handler, const gchar *path, const gchar *etag)
+{
+  SpiceWebDAV *self = handler->self;
+  GFile *file = NULL;
+  GFileInfo *info = NULL;
+  GError *error = NULL;
+  const gchar *fetag;
+  gboolean success = FALSE;
+
+  g_debug ("check etag %s for %s", etag, path);
+
+  file = g_file_get_child (handler->file, path + 1);
+  info = g_file_query_info (file, "etag::*",
+                            G_FILE_QUERY_INFO_NONE, self->cancellable, &error);
+  if (!info)
+    goto end;
+
+  fetag = g_file_info_get_etag (info);
+  g_warn_if_fail (fetag != NULL);
+
+  success = !g_strcmp0 (etag, fetag);
+
+end:
+  if (error)
+    {
+      g_warning ("check_etag error: %s", error->message);
+      g_clear_error (&error);
+    }
+
+  g_clear_object (&info);
+  g_clear_object (&file);
+
+  return success;
+}
+
+static gboolean
+eval_if_condition (PathHandler *handler, IfState *state)
+{
+  gboolean success = FALSE;
+
+  if (next_token (state, "<"))
+    {
+      const gchar *token = accept_ref (state);
+      LockSubmitted *l = lock_submitted_new (state->path, token);
+
+      state->locks = g_list_append (state->locks, l);
+
+      success = check_token (handler, state->path, token);
+    }
+  else if (next_token (state, "["))
+    {
+      gchar *etag = accept_etag (state);
+
+      success = check_etag (handler, state->path, etag);
+      g_free (etag);
+    }
+  else
+    g_warn_if_reached ();
+
+  return success;
+}
+
+static gboolean
+eval_if_not_condition (PathHandler *handler, IfState *state)
+{
+  gboolean not = FALSE;
+  gboolean res;
+
+  if (accept_token (state, "Not"))
+    not = TRUE;
+
+  res = eval_if_condition (handler, state);
+
+  return not ? !res : res;
+}
+
+static gboolean
+eval_if_list (PathHandler *handler, IfState *state)
+{
+  gboolean success;
+
+  g_return_val_if_fail (accept_token (state, "("), FALSE);
+
+  success = eval_if_not_condition (handler, state);
+
+  while (!accept_token (state, ")"))
+    success &= eval_if_not_condition (handler, state);
+
+  return success;
+}
+
+static gboolean
+eval_if_lists (PathHandler *handler, IfState *state)
+{
+  gboolean success = FALSE;
+
+  g_return_val_if_fail (next_token (state, "("), FALSE);
+
+  while (next_token (state, "("))
+    success |= eval_if_list (handler, state);
+
+  return success;
+}
+
+static gboolean
+eval_if_tag (PathHandler *handler, IfState *state)
+{
+  SoupURI *uri;
+  const gchar *path;
+  const gchar *ref = accept_ref (state);
+
+  g_return_val_if_fail (ref != NULL, FALSE);
+
+  uri = soup_uri_new (ref);
+  path = soup_uri_get_path (uri);
+  g_free (state->path);
+  state->path = g_strdup (path);
+  soup_uri_free (uri);
+
+  return eval_if_lists (handler, state);
+}
+
+
+static gboolean
+eval_if (PathHandler *handler, IfState *state)
+{
+  gboolean success = FALSE;
+
+  if (next_token (state, "<")) {
+    while (!eat_whitespaces (state))
+      success |= eval_if_tag (handler, state);
+  } else {
+    while (!eat_whitespaces (state))
+      success |= eval_if_lists (handler, state);
+  }
+
+  return success;
+}
+
+static gint
+check_if (PathHandler *handler, SoupMessage *msg, const gchar *path, GList **locks)
+{
+  SpiceWebDAV *self = handler->self;
+  gboolean success = TRUE;
+  gint status;
+  gchar *str = g_strdup (soup_message_headers_get_one (msg->request_headers, "If"));
+  IfState state = { .cur = str, .path = g_strdup (path) };
+  gboolean copy = msg->method == SOUP_METHOD_COPY;
+
+  if (!str)
+    goto end;
+
+  if (eval_if (handler, &state))
+    {
+      *locks = state.locks;
+    }
+  else
+    {
+      g_list_free_full (state.locks, (GDestroyNotify) lock_submitted_free);
+      success = FALSE;
+    }
+
+end:
+  status = success ? SOUP_STATUS_OK
+           : str ? SOUP_STATUS_PRECONDITION_FAILED : SOUP_STATUS_LOCKED;
+
+  if (success && !copy && path_has_other_locks (self, path, *locks))
+    status = SOUP_STATUS_LOCKED;
+
+  g_free (str);
+  g_free (state.path);
+  return status;
+}
+
+static gint
+method_proppatch (PathHandler *handler, SoupMessage *msg,
+                  const char *path, GError **err)
+{
+  SpiceWebDAV *self = handler->self;
+  GFile *file = NULL;
+  GHashTable *mstatus = NULL;   // path -> statlist
+  DavDoc doc = {0, };
+  xmlNodePtr node = NULL, attr = NULL;
+  GList *props = NULL, *submitted = NULL;
+  gint status;
+
+  if (!davdoc_parse (&doc, msg, msg->request_body, "propertyupdate"))
+    {
+      status = SOUP_STATUS_BAD_REQUEST;
+      goto end;
+    }
+
+  status = check_if (handler, msg, path, &submitted);
+  if (status != SOUP_STATUS_OK)
+    goto end;
+
+  file = g_file_get_child (handler->file, path + 1);
+  mstatus = g_hash_table_new_full (g_str_hash, g_str_equal, g_free,
+                                   (GDestroyNotify) response_free);
+
+  node = doc.root;
+  for (node = node->children; node; node = node->next)
+    {
+      if (!node_is_element (node))
+        continue;
+
+      if (node_has_name (node, "set"))
+        status = prop_set (self, msg, file, node, &attr, FALSE);
+      else if (node_has_name (node, "remove"))
+        status = prop_set (self, msg, file, node, &attr, TRUE);
+      else
+        g_warn_if_reached ();
+
+      if (attr)
+        {
+          attr->_private = GINT_TO_POINTER (status);
+          props = g_list_append (props, attr);
+        }
+    }
+
+  g_hash_table_insert (mstatus, g_strdup (path),
+                       response_new (props, 0));
+
+  if (g_hash_table_size (mstatus) > 0)
+    status = response_multistatus (msg, mstatus);
+
+end:
+  davdoc_free (&doc);
+  if (mstatus)
+    g_hash_table_unref (mstatus);
+  g_clear_object (&file);
+
+  return status;
+}
+
+static gint
+method_put (PathHandler *handler, SoupMessage *msg,
+            const char *path, GError **err)
+{
+  GFile *file;
+  SpiceWebDAV *self = handler->self;
+  gint status;
+  GList *submitted = NULL;
+
+  status = check_if (handler, msg, path, &submitted);
+  if (status != SOUP_STATUS_OK)
+    goto end;
+
+  file = g_file_get_child (handler->file, path + 1);
+  status = do_put_file (msg, file, self->cancellable, err);
+
+end:
+  g_clear_object (&file);
+  return status;
+}
+
+static gint
+do_mkcol_file (SoupMessage *msg, GFile *file,
+               GCancellable *cancellable, GError **err)
+{
+  GError *error = NULL;
+  gint status = SOUP_STATUS_CREATED;
+
+  if (!g_file_make_directory (file, cancellable, &error))
+    {
+      if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
+        status = SOUP_STATUS_CONFLICT;
+      else if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_EXISTS))
+        status = SOUP_STATUS_METHOD_NOT_ALLOWED;
+      else
+        {
+          status = SOUP_STATUS_FORBIDDEN;
+          g_propagate_error (err, error);
+          error = NULL;
+        }
+
+      g_clear_error (&error);
+    }
+
+  return status;
+}
+
+static gint
+method_mkcol (PathHandler *handler, SoupMessage *msg,
+              const char *path, GError **err)
+{
+  GFile *file = NULL;
+  SpiceWebDAV *self = handler->self;
+  gint status;
+  GList *submitted = NULL;
+
+  if (msg->request_body && msg->request_body->length)
+    {
+      status = SOUP_STATUS_UNSUPPORTED_MEDIA_TYPE;
+      goto end;
+    }
+
+  status = check_if (handler, msg, path, &submitted);
+  if (status != SOUP_STATUS_OK)
+    goto end;
+
+  file = g_file_get_child (handler->file, path + 1);
+  status = do_mkcol_file (msg, file, self->cancellable, err);
+
+end:
+  g_clear_object (&file);
+  return status;
+}
+
+static gint
+error_to_status (GError *err)
+{
+  if (g_error_matches (err, G_FILE_ERROR, G_FILE_ERROR_NOENT))
+    return SOUP_STATUS_NOT_FOUND;
+  if (g_error_matches (err, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
+    return SOUP_STATUS_NOT_FOUND;
+
+  return SOUP_STATUS_FORBIDDEN;
+}
+
+static gint
+do_delete_file (const gchar *path, GFile *file,
+                GHashTable *mstatus,
+                GCancellable *cancellable)
+{
+  GError *error = NULL;
+  GFileEnumerator *e;
+  gint status = SOUP_STATUS_NO_CONTENT;
+
+  e = g_file_enumerate_children (file, "standard::*", G_FILE_QUERY_INFO_NONE,
+                                 cancellable, NULL);
+  if (e)
+    {
+      while (1)
+        {
+          GFileInfo *info = g_file_enumerator_next_file (e, cancellable, &error);
+          if (!info)
+            break;
+          GFile *del = g_file_get_child (file, g_file_info_get_name (info));
+          gchar *escape = g_markup_escape_text (g_file_info_get_name (info), -1);
+          gchar *del_path = g_build_path ("/", path, escape, NULL);
+          do_delete_file (del_path, del, mstatus, cancellable);
+          g_object_unref (del);
+          g_free (escape);
+          g_free (del_path);
+        }
+
+      g_file_enumerator_close (e, cancellable, NULL);
+      g_clear_object (&e);
+    }
+
+  if (!g_file_delete (file, cancellable, &error) && mstatus)
+    {
+      status = error_to_status (error);
+
+      g_hash_table_insert (mstatus, g_strdup (path),
+                           response_new (NULL, status));
+    }
+
+  if (error)
+    {
+      g_debug ("ignored del error: %s", error->message);
+      g_clear_error (&error);
+    }
+
+  return status;
+}
+
+static gint
+method_delete (PathHandler *handler, SoupMessage *msg,
+               const char *path, GError **err)
+{
+  SpiceWebDAV *self = handler->self;
+  GFile *file = NULL;
+  GHashTable *mstatus = NULL;
+  gint status;
+  GList *submitted = NULL;
+
+  /* depth = depth_from_string(soup_message_headers_get_one (msg->request_headers, "Depth")); */
+  /* must be == infinity with collection */
+
+  status = check_if (handler, msg, path, &submitted);
+  if (status != SOUP_STATUS_OK)
+    goto end;
+
+  file = g_file_get_child (handler->file, path + 1);
+  mstatus = g_hash_table_new_full (g_str_hash, g_str_equal, g_free,
+                                   (GDestroyNotify) response_free);
+
+  status = do_delete_file (path, file, mstatus, self->cancellable);
+  if (status == SOUP_STATUS_NO_CONTENT)
+    if (g_hash_table_size (mstatus) > 0)
+      status = response_multistatus (msg, mstatus);
+
+end:
+  if (mstatus)
+    g_hash_table_unref (mstatus);
+  g_clear_object (&file);
+
+  return status;
+}
+
+static gboolean
+do_copy_r (GFile *src, GFile *dest, GFileCopyFlags flags,
+           GCancellable *cancellable, GError **err)
+{
+  GFileEnumerator *e = NULL;
+  gboolean success = FALSE;
+  GFile *src_child = NULL;
+  GFile *dest_child = NULL;
+
+  if (!g_file_make_directory_with_parents (dest, cancellable, err))
+    goto end;
+
+  e = g_file_enumerate_children (src, "standard::*", G_FILE_QUERY_INFO_NONE,
+                                 cancellable, err);
+  if (!e)
+    goto end;
+
+  while (1)
+    {
+      GFileInfo *info = g_file_enumerator_next_file (e, cancellable, err);
+      if (!info)
+        break;
+
+      src_child = g_file_get_child (src, g_file_info_get_name (info));
+      dest_child = g_file_get_child (dest, g_file_info_get_name (info));
+
+      if (g_file_info_get_file_type (info) == G_FILE_TYPE_DIRECTORY)
+        {
+          if (!do_copy_r (src_child, dest_child, flags, cancellable, err))
+            goto end;
+        }
+      else if (!g_file_copy (src_child, dest_child, flags, cancellable, NULL, NULL, err))
+        goto end;
+
+      g_clear_object (&src_child);
+      g_clear_object (&dest_child);
+    }
+
+  success = TRUE;
+
+end:
+  g_clear_object (&e);
+  g_clear_object (&src_child);
+  g_clear_object (&dest_child);
+
+  return success;
+}
+
+static gint
+do_movecopy_file (SoupMessage *msg, GFile *file,
+                  GFile *dest, const gchar *dest_path,
+                  GCancellable *cancellable, GError **err)
+{
+  GError *error = NULL;
+  gboolean overwrite;
+  DepthType depth;
+  gint status = SOUP_STATUS_PRECONDITION_FAILED;
+  gboolean copy = msg->method == SOUP_METHOD_COPY;
+  GFileCopyFlags flags = G_FILE_COPY_ALL_METADATA;
+  gboolean retry = FALSE;
+  gboolean exists;
+
+  depth = depth_from_string (soup_message_headers_get_one (msg->request_headers, "Depth"));
+  overwrite = !!g_strcmp0 (
+    soup_message_headers_get_one (msg->request_headers, "Overwrite"), "F");
+  if (overwrite)
+    flags |= G_FILE_COPY_OVERWRITE;
+  exists = g_file_query_exists (dest, cancellable);
+
+again:
+  switch (depth)
+    {
+    case DEPTH_INFINITY:
+    case DEPTH_ZERO: {
+        copy
+        ? g_file_copy (file, dest, flags, cancellable, NULL, NULL, &error)
+        : g_file_move (file, dest, flags, cancellable, NULL, NULL, &error);
+
+        if (overwrite && !retry &&
+            (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_IS_DIRECTORY) ||
+             g_error_matches (error, G_IO_ERROR, G_IO_ERROR_WOULD_MERGE)) &&
+            do_delete_file (dest_path, dest, NULL, cancellable) == SOUP_STATUS_NO_CONTENT)
+          {
+            g_clear_error (&error);
+            retry = TRUE;
+            goto again;
+          }
+        else if (!overwrite &&
+                 g_error_matches (error, G_IO_ERROR, G_IO_ERROR_EXISTS))
+          {
+            g_clear_error (&error);
+            goto end;
+          }
+        else if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_WOULD_RECURSE))
+          {
+            g_clear_error (&error);
+            if (copy)
+              {
+                if (depth == DEPTH_INFINITY)
+                  do_copy_r (file, dest, flags, cancellable, &error);
+                else
+                  g_file_make_directory_with_parents (dest, cancellable, &error);
+              }
+          }
+        else if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
+          {
+            status = SOUP_STATUS_CONFLICT;
+            g_clear_error (&error);
+            goto end;
+          }
+
+        break;
+      }
+
+    default:
+      g_warn_if_reached ();
+    }
+
+  if (error)
+    g_propagate_error (err, error);
+  else
+    status = exists ? SOUP_STATUS_NO_CONTENT : SOUP_STATUS_CREATED;
+
+end:
+  return status;
+}
+
+static gint
+method_movecopy (PathHandler *handler, SoupMessage *msg,
+                 const char *path, GError **err)
+{
+  GFile *file = NULL, *dest_file = NULL;
+  SpiceWebDAV *self = handler->self;
+  SoupURI *dest_uri = NULL;
+  gint status = SOUP_STATUS_NOT_FOUND;
+  const gchar *dest;
+  gchar *udest;
+  GList *submitted = NULL;
+
+  dest = soup_message_headers_get_one (msg->request_headers, "Destination");
+  if (!dest)
+    goto end;
+  dest_uri = soup_uri_new (dest);
+  dest = soup_uri_get_path (dest_uri);
+  if (!dest || !*dest)
+    goto end;
+
+  status = check_if (handler, msg, path, &submitted);
+  if (status != SOUP_STATUS_OK)
+    goto end;
+
+  if (path_has_other_locks (self, dest, submitted))
+    {
+      status = SOUP_STATUS_LOCKED;
+      goto end;
+    }
+
+  udest = g_uri_unescape_string (dest + 1, NULL);
+  dest_file = g_file_get_child (handler->file, udest);
+  g_free (udest);
+
+  file = g_file_get_child (handler->file, path + 1);
+  status = do_movecopy_file (msg, file, dest_file, dest,
+                             self->cancellable, err);
+
+end:
+  if (dest_uri)
+    soup_uri_free (dest_uri);
+  g_clear_object (&file);
+  g_clear_object (&dest_file);
+  g_list_free_full (submitted, (GDestroyNotify) lock_submitted_free);
+  return status;
+}
+
+static gboolean
+check_lock (const gchar *key, Path *path, gpointer data)
+{
+  DAVLock *lock = data;
+  DAVLock *other = NULL;
+  GList *l;
+
+  for (l = path->locks; l; l = l->next)
+    {
+      other = l->data;
+      if (other->scope == LOCK_SCOPE_EXCLUSIVE)
+        return FALSE;
+    }
+
+  if (other && lock->scope == LOCK_SCOPE_EXCLUSIVE)
+    return FALSE;
+
+  return TRUE;
+}
+
+static gboolean
+try_add_lock (SpiceWebDAV *self, const gchar *path, DAVLock *lock)
+{
+  Path *p;
+
+  if (!foreach_parent_path (self, path, check_lock, lock))
+    return FALSE;
+
+  p = get_path (self, path);
+  path_add_lock (p, lock);
+
+  return TRUE;
+}
+
+static gboolean
+lock_ensure_file (PathHandler *handler, const char *path,
+                  GCancellable *cancellable, GError **err)
+{
+  GError *e = NULL;
+  GFileOutputStream *stream = NULL;
+  GFile *file = NULL;
+  gboolean created = FALSE;
+
+  file = g_file_get_child (handler->file, path + 1);
+  stream = g_file_create (file, G_FILE_CREATE_NONE, cancellable, &e);
+  created = !!stream;
+
+  if (e)
+    {
+      if (g_error_matches (e, G_IO_ERROR, G_IO_ERROR_EXISTS))
+        g_clear_error (&e);
+      else
+        g_propagate_error (err, e);
+    }
+
+  g_clear_object (&stream);
+  g_clear_object (&file);
+
+  return created;
+}
+
+static gint
+method_lock (PathHandler *handler, SoupMessage *msg,
+             const char *path, GError **err)
+{
+  SpiceWebDAV *self = handler->self;
+  Path *lpath = NULL;
+  xmlChar *mem = NULL;
+  int size = 0;
+  DavDoc doc = {0, };
+  xmlNodePtr node = NULL, owner = NULL, root = NULL;
+  xmlNsPtr ns = NULL;
+  LockScopeType scope = LOCK_SCOPE_SHARED;
+  LockType type;
+  DepthType depth;
+  guint timeout;
+  gchar *ltoken = NULL, *uuid = NULL, *token = NULL;
+  DAVLock *lock = NULL;
+  gint status = SOUP_STATUS_BAD_REQUEST;
+  gboolean created;
+
+  depth = depth_from_string (soup_message_headers_get_one (msg->request_headers, "Depth"));
+  timeout = timeout_from_string (soup_message_headers_get_one (msg->request_headers, "Timeout"));
+
+  if (depth != DEPTH_ZERO && depth != DEPTH_INFINITY)
+    goto end;
+
+  if (!msg->request_body->length)
+    {
+      const gchar *hif = soup_message_headers_get_one (msg->request_headers, "If");
+      gint len = strlen (hif);
+
+      if (hif[0] != '(' || hif[1] != '<' || hif[len - 2] != '>' || hif[len - 1] != ')')
+        goto end;
+
+      token = g_strndup (hif + 2, len - 4);
+
+      g_debug ("refresh token %s", token);
+      lock = path_get_lock (self, path, token);
+
+      if (!lock)
+        goto end;
+
+      dav_lock_refresh_timeout (lock, timeout);
+      status = SOUP_STATUS_OK;
+      goto body;
+    }
+
+  if (!davdoc_parse (&doc, msg, msg->request_body, "lockinfo"))
+    goto end;
+
+  node = doc.root;
+  for (node = node->children; node; node = node->next)
+    {
+      if (!node_is_element (node))
+        continue;
+
+      if (node_has_name (node, "lockscope"))
+        {
+          scope = parse_lockscope (node);
+          if (scope == LOCK_SCOPE_NONE)
+            break;
+        }
+      else if (node_has_name (node, "locktype"))
+        {
+          type = parse_locktype (node);
+          if (type == LOCK_NONE)
+            break;
+        }
+      else if (node_has_name (node, "owner"))
+        {
+          if (owner == NULL)
+            owner = xmlCopyNode (node, 1);
+          else
+            g_warn_if_reached ();
+        }
+    }
+
+  g_debug ("lock depth:%d scope:%d type:%d owner:%p, timeout: %u",
+           depth, scope, type, owner, timeout);
+
+  uuid = g_uuid_random ();
+  token = g_strdup_printf ("urn:uuid:%s", uuid);
+  ltoken = g_strdup_printf ("<%s>", token);
+  soup_message_headers_append (msg->response_headers, "Lock-Token", ltoken);
+
+  lpath = get_path (self, path);
+  lock = dav_lock_new (lpath, token, scope, type, depth, owner, timeout);
+  if (!try_add_lock (self, path, lock))
+    {
+      g_warning ("lock failed");
+      dav_lock_free (lock);
+      status = SOUP_STATUS_LOCKED;
+      goto end;
+    }
+
+  created = lock_ensure_file (handler, path, self->cancellable, err);
+  if (*err)
+    goto end;
+
+  status = created ? SOUP_STATUS_CREATED : SOUP_STATUS_OK;
+
+body:
+  root = xmlNewNode (NULL, BAD_CAST "D:prop");
+  ns = xmlNewNs (root, BAD_CAST "DAV:", BAD_CAST "D");
+
+  node = xmlNewChild (root, ns, BAD_CAST "lockdiscovery", NULL);
+  xmlAddChild (node, get_activelock_node (lock, ns));
+
+  node_to_string (root, &mem, &size);
+  soup_message_set_response (msg, "application/xml",
+                             SOUP_MEMORY_TAKE, (gchar *) mem, size);
+
+end:
+  g_free (ltoken);
+  g_free (token);
+  g_free (uuid);
+  davdoc_free (&doc);
+
+  return status;
+}
+
+static gchar *
+remove_brackets (const gchar *str)
+{
+  if (!str)
+    return NULL;
+
+  gint len = strlen (str);
+
+  if (str[0] != '<' || str[len - 1] != '>')
+    return NULL;
+
+  return g_strndup (str + 1, len - 2);
+}
+
+static gint
+method_unlock (PathHandler *handler, SoupMessage *msg,
+               const char *path, GError **err)
+{
+  SpiceWebDAV *self = handler->self;
+  DAVLock *lock;
+  gint status = SOUP_STATUS_BAD_REQUEST;
+
+  gchar *token = remove_brackets (
+    soup_message_headers_get_one (msg->request_headers, "Lock-Token"));
+
+  g_return_val_if_fail (token != NULL, SOUP_STATUS_BAD_REQUEST);
+
+  lock = path_get_lock (self, path, token);
+  if (!lock)
+    return SOUP_STATUS_CONFLICT;
+
+  dav_lock_free (lock);
+  status = SOUP_STATUS_NO_CONTENT;
+
+  g_free (token);
+  return status;
+}
+
+static void
+server_callback (SoupServer *server, SoupMessage *msg,
+                 const char *path, GHashTable *query,
+                 SoupClientContext *context, gpointer user_data)
+{
+  GError *err = NULL;
+  PathHandler *handler = user_data;
+  gint status = SOUP_STATUS_NOT_IMPLEMENTED;
+  SoupURI *uri = soup_message_get_uri (msg);
+  GHashTable *params;
+
+  g_debug ("%s %s HTTP/1.%d %s %s", msg->method, path, soup_message_get_http_version (msg),
+           soup_message_headers_get_one (msg->request_headers, "X-Litmus") ? : "",
+           soup_message_headers_get_one (msg->request_headers, "X-Litmus-Second") ? : "");
+
+  if (!(path && *path == '/'))
+    {
+      g_debug ("path must begin with /");
+      return;
+    }
+  if (!(uri && uri->fragment == NULL))
+    {
+      g_debug ("using fragments in query is not supported");
+      return;
+    }
+
+  if (msg->method == SOUP_METHOD_OPTIONS)
+    {
+      soup_message_headers_append (msg->response_headers, "DAV", "1,2");
+
+      /* according to http://code.google.com/p/sabredav/wiki/Windows */
+      soup_message_headers_append (msg->response_headers, "MS-Author-Via", "DAV");
+
+      soup_message_headers_append (msg->response_headers, "Allow",
+                                   "GET, HEAD, PUT, PROPFIND, PROPPATCH, MKCOL, DELETE, MOVE, COPY, LOCK, UNLOCK");
+      status = SOUP_STATUS_OK;
+    }
+  else if (msg->method == SOUP_METHOD_GET ||
+           msg->method == SOUP_METHOD_HEAD)
+    status = method_get (handler, msg, path, &err);
+  else if (msg->method == SOUP_METHOD_PUT)
+    status = method_put (handler, msg, path, &err);
+  else if (msg->method == SOUP_METHOD_PROPFIND)
+    status = method_propfind (handler, msg, path, &err);
+  else if (msg->method == SOUP_METHOD_PROPPATCH)
+    status = method_proppatch (handler, msg, path, &err);
+  else if (msg->method == SOUP_METHOD_MKCOL)
+    status = method_mkcol (handler, msg, path, &err);
+  else if (msg->method == SOUP_METHOD_DELETE)
+    status = method_delete (handler, msg, path, &err);
+  else if (msg->method == SOUP_METHOD_MOVE ||
+           msg->method == SOUP_METHOD_COPY)
+    status = method_movecopy (handler, msg, path, &err);
+  else if (msg->method == SOUP_METHOD_LOCK)
+    status = method_lock (handler, msg, path, &err);
+  else if (msg->method == SOUP_METHOD_UNLOCK)
+    status = method_unlock (handler, msg, path, &err);
+  else
+    {
+      g_warn_if_reached ();
+      goto end;
+    }
+
+  soup_message_set_status (msg, status);
+
+  params = g_hash_table_new (g_str_hash, g_str_equal);
+  g_hash_table_insert (params, "charset", "utf-8");
+  soup_message_headers_set_content_type (msg->response_headers,
+                                         "text/xml", params);
+  g_hash_table_destroy (params);
+
+end:
+  g_debug ("  -> %d %s\n", msg->status_code, msg->reason_phrase);
+  if (err)
+    {
+      g_warning ("error: %s", err->message);
+      g_clear_error (&err);
+    }
+}
+
+gint
+spice_webdav_get_port (SpiceWebDAV *self)
+{
+  g_return_val_if_fail (SPICE_IS_WEBDAV (self), -1);
+
+  return soup_server_get_port (self->server);
+}
+
+static gpointer
+thread_func (gpointer data)
+{
+  SpiceWebDAV *self = data;
+
+  g_debug ("Starting on port %d, serving %s", spice_webdav_get_port (self), self->path);
+
+  soup_server_run_async (self->server);
+
+  g_main_loop_run (self->loop);
+
+  return NULL;
+}
+
+void
+spice_webdav_run (SpiceWebDAV *self)
+{
+  g_return_if_fail (SPICE_IS_WEBDAV (self));
+
+  if (self->thread)
+    return;
+
+  g_object_ref (self);
+  self->thread = g_thread_new ("webdav-server", thread_func, self);
+}
+
+void
+spice_webdav_quit (SpiceWebDAV *self)
+{
+  g_return_if_fail (SPICE_IS_WEBDAV (self));
+
+  if (!self->thread)
+    return;
+
+  soup_server_quit (self->server);
+  g_main_loop_quit (self->loop);
+  g_thread_join (self->thread);
+  self->thread = NULL;
+  g_object_unref (self);
+}
+
+SpiceWebDAV *
+spice_webdav_new (gint port, const gchar *path)
+{
+  if (path == NULL)
+    path = ".";
+
+  return g_object_new (SPICE_TYPE_WEBDAV,
+                       "port", port, "path", path, NULL);
+}
diff --git a/gtk/webdav/webdav.h b/gtk/webdav/webdav.h
new file mode 100644
index 0000000..8b01fb7
--- /dev/null
+++ b/gtk/webdav/webdav.h
@@ -0,0 +1,44 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+  Copyright (C) 2013 Red Hat, Inc.
+
+  This library is free software; you can redistribute it and/or
+  modify it under the terms of the GNU Lesser General Public
+  License as published by the Free Software Foundation; either
+  version 2.1 of the License, or (at your option) any later version.
+
+  This library is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+  Lesser General Public License for more details.
+
+  You should have received a copy of the GNU Lesser General Public
+  License along with this library; if not, see <http://www.gnu.org/licenses/>.
+*/
+#ifndef __SPICE_WEBDAV_H__
+#define __SPICE_WEBDAV_H__
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define SPICE_TYPE_WEBDAV            (spice_webdav_get_type ())
+#define SPICE_WEBDAV(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), SPICE_TYPE_WEBDAV, SpiceWebDAV))
+#define SPICE_WEBDAV_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass), SPICE_TYPE_WEBDAV, SpiceWebDAVClass))
+#define SPICE_IS_WEBDAV(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), SPICE_TYPE_WEBDAV))
+#define SPICE_IS_WEBDAV_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), SPICE_TYPE_WEBDAV))
+#define SPICE_WEBDAV_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj), SPICE_TYPE_WEBDAV, SpiceWebDAVClass))
+
+typedef struct _SpiceWebDAV SpiceWebDAV;
+typedef struct _SpiceWebDAVClass SpiceWebDAVClass;
+
+GType         spice_webdav_get_type        (void);
+
+SpiceWebDAV*  spice_webdav_new             (gint port, const gchar *path);
+void          spice_webdav_run             (SpiceWebDAV *dav);
+void          spice_webdav_quit            (SpiceWebDAV *dav);
+gint          spice_webdav_get_port        (SpiceWebDAV *dav);
+
+G_END_DECLS
+
+#endif /* __SPICE_WEBDAV_H__ */
diff --git a/spice-common b/spice-common
index 57ce430..c1a98a3 160000
--- a/spice-common
+++ b/spice-common
@@ -1 +1 @@
-Subproject commit 57ce430ccd66bd1ca2447c14503234cfb88e2365
+Subproject commit c1a98a3e92488815cc90ea4d8148a17387302a7b
-- 
1.8.4.2



More information about the Spice-devel mailing list