xref: /lighttpd1.4/src/mod_webdav.c (revision fb7e5a78)
1 /*
2  * mod_webdav - WEBDAV support for lighttpd
3  *
4  * Fully-rewritten from original
5  * Copyright(c) 2019 Glenn Strauss gstrauss()gluelogic.com  All rights reserved
6  * License: BSD 3-clause (same as lighttpd)
7  */
8 
9 /*
10  * Note: This plugin is a basic implementation of [RFC4918] WebDAV
11  *
12  *       Version Control System (VCS) backing WebDAV is recommended instead
13  *       and Subversion (svn) is one such VCS supporting WebDAV.
14  *
15  * status: *** EXPERIMENTAL *** (and likely insecure encoding/decoding)
16  *
17  * future:
18  *
19  * TODO: moving props should delete any existing props instead of
20  *       preserving any that are not overwritten with UPDATE OR REPLACE
21  *       (and, if merging directories, be careful when doing so)
22  * TODO: add proper support for locks with "shared" lockscope
23  *       (instead of treating match of any shared lock as sufficient,
24  *        even when there are different lockroots)
25  * TODO: does libxml2 xml-decode (html-decode),
26  *       or must I do so to normalize input?
27  * TODO: add strict enforcement of property names to be valid XML tags
28  *       (or encode as such before putting into database)
29  *       & " < >
30  * TODO: should we be using xmlNodeListGetString() or xmlBufNodeDump()
31  *       and how does it handle encoding and entity-inlining of external refs?
32  *       Must xml decode/encode (normalize) before storing user data in database
33  *       if going to add that data verbatim to xml doc returned in queries
34  * TODO: when walking xml nodes, should add checks for "DAV:" namespace
35  * TODO: consider where it might be useful/informative to check
36  *       SQLITE_OK != sqlite3_reset() or SQLITE_OK != sqlite3_bind_...() or ...
37  *       (in some cases no rows returned is ok, while in other cases it is not)
38  * TODO: Unsupported: !r->conf.follow_symlink is not currently honored;
39  *       symlinks are followed.  Supporting !r->conf.follow_symlinks would
40  *       require operating system support for POSIX.1-2008 *at() commands,
41  *       and reworking the mod_webdav code to exclusively operate with *at()
42  *       commands, for example, replacing unlink() with unlinkat().
43  *
44  * RFE: add config option whether or not to include locktoken and ownerinfo
45  *      in PROPFIND lockdiscovery
46  *
47  * deficiencies
48  * - incomplete "shared" lock support
49  * - review code for proper decoding/encoding of elements from/to XML and db
50  * - preserve XML info in scope on dead properties, e.g. xml:lang
51  *
52  * [RFC4918] 4.3 Property Values
53  *   Servers MUST preserve the following XML Information Items (using the
54  *   terminology from [REC-XML-INFOSET]) in storage and transmission of dead
55  *   properties: ...
56  * [RFC4918] 14.26 set XML Element
57  *   The 'set' element MUST contain only a 'prop' element. The elements
58  *   contained by the 'prop' element inside the 'set' element MUST specify the
59  *   name and value of properties that are set on the resource identified by
60  *   Request-URI. If a property already exists, then its value is replaced.
61  *   Language tagging information appearing in the scope of the 'prop' element
62  *   (in the "xml:lang" attribute, if present) MUST be persistently stored along
63  *   with the property, and MUST be subsequently retrievable using PROPFIND.
64  * [RFC4918] F.2 Changes for Server Implementations
65  *   Strengthened server requirements for storage of property values, in
66  *   particular persistence of language information (xml:lang), whitespace, and
67  *   XML namespace information (see Section 4.3).
68  *
69  * resource usage containment
70  * - filesystem I/O operations might take a non-trivial amount of time,
71  *   blocking the server from responding to other requests during this time.
72  *   Potential solution: have a thread dedicated to handling webdav requests
73  *   and serialize such requests in each thread dedicated to handling webdav.
74  *   (Limit number of such dedicated threads.)  Remove write interest from
75  *   connection during this period so that server will not trigger any timeout
76  *   on the connection.
77  * - recursive directory operations are depth-first and may consume a large
78  *   number of file descriptors if the directory hierarchy is deep.
79  *   Potential solution: serialize webdav requests into dedicated thread (above)
80  *   Potential solution: perform breadth-first directory traversal and pwrite()
81  *   directory paths into a temporary file.  After reading each directory,
82  *   close() the dirhandle and pread() next directory from temporary file.
83  *   (Keeping list of directories in memory might result in large memory usage)
84  * - flush response to client (or to intermediate temporary file) at regular
85  *   intervals or triggers to avoid response consume large amount of memory
86  *   during operations on large collection hierarchies (with lots of nested
87  *   directories)
88  *
89  * beware of security concerns involved in enabling WebDAV
90  * on publicly accessible servers
91  * - (general)      [RFC4918] 20 Security Considersations
92  * - (specifically) [RFC4918] 20.6 Implications of XML Entities
93  * - TODO review usage of xml libs for security, resource usage, containment
94  *   libxml2 vs expat vs ...
95  *     http://xmlbench.sourceforge.net/
96  *     http://stackoverflow.com/questions/399704/xml-parser-for-c
97  *     http://tibleiz.net/asm-xml/index.html
98  *     http://dev.yorhel.nl/yxml
99  * - how might mod_webdav be affected by mod_openssl setting REMOTE_USER?
100  * - when encoding href in responses, also ensure proper XML encoding
101  *     (do we need to ENCODING_REL_URI and then ENCODING_MINIMAL_XML?)
102  * - TODO: any (non-numeric) data that goes into database should be encoded
103  *     before being sent back to user, not just href.  Perhaps anything that
104  *     is not an href should be stored in database in XML-encoded form.
105  *
106  * consider implementing a set of (reasonable) limits on such things as
107  * - max number of collections
108  * - max number of objects in a collection
109  * - max number of properties per object
110  * - max length of property name
111  * - max length of property value
112  * - max length of locktoken, lockroot, ownerinfo
113  * - max number of locks held by a client, or by an owner
114  * - max number of locks on a resource (shared locks)
115  * - ...
116  *
117  * robustness
118  * - should check return value from sqlite3_reset(stmt) for REPLACE, UPDATE,
119  *   DELETE statements (which is when commit occurs and locks are obtained/fail)
120  * - handle SQLITE_BUSY (e.g. other processes modifying db and hold locks)
121  *   https://www.sqlite.org/lang_transaction.html
122  *   https://www.sqlite.org/rescode.html#busy
123  *   https://www.sqlite.org/c3ref/busy_handler.html
124  *   https://www.sqlite.org/c3ref/busy_timeout.html
125  * - periodically execute query to delete expired locks
126  *   (MOD_WEBDAV_SQLITE_LOCKS_DELETE_EXPIRED)
127  *   (should defend against removing locks protecting long-running operations
128  *    that are in progress on the server)
129  * - having all requests go through database, including GET and HEAD would allow
130  *   for better transactional semantics, instead of the current race conditions
131  *   inherent in multiple (and many) filesystem operations.  All queries would
132  *   go through database, which would map to objects on disk, and copy and move
133  *   would simply be database entries to objects with reference counts and
134  *   copy-on-write semantics (instead of potential hard-links on disk).
135  *   lstat() information could also be stored in database.  Right now, if a file
136  *   is copied or moved or deleted, the status of the property update in the db
137  *   is discarded, whether it succeeds or not, since file operation succeeded.
138  *   (Then again, it might also be okay if props do not exist on a given file.)
139  *   On the other hand, if everything went through database, then etag could be
140  *   stored in database and could be updated upon PUT (or MOVE/COPY/DELETE).
141  *   There would also need to be a way to trigger a rescan of filesystem to
142  *   bring the database into sync with any out-of-band changes.
143  *
144  *
145  * notes:
146  *
147  * - lstat() used instead of stat_cache_*() since the stat_cache might have
148  *   expired data, as stat_cache is not invalidated by outside modifications,
149  *   such as WebDAV PUT method (unless FAM is used)
150  *
151  * - SQLite database can be run in WAL mode (https://sqlite.org/wal.html)
152  *   though mod_webdav does not provide a mechanism to configure WAL.
153  *   Instead, once lighttpd starts up mod_webdav and creates the database,
154  *   set WAL mode on the database from the command and then restart lighttpd.
155  */
156 
157 
158 /* linkat() fstatat() unlinkat() fdopendir() NAME_MAX */
159 #if !defined(_XOPEN_SOURCE) || _XOPEN_SOURCE-0 < 700
160 #undef  _XOPEN_SOURCE
161 #define _XOPEN_SOURCE 700
162 #endif
163 /* DT_UNKNOWN DTTOIF() */
164 #ifndef _GNU_SOURCE
165 #define _GNU_SOURCE
166 #endif
167 
168 #include "first.h"      /* first */
169 #include "sys-mmap.h"
170 #include <sys/types.h>
171 #include <sys/stat.h>
172 #include <dirent.h>
173 #include <errno.h>
174 #include <fcntl.h>
175 #include <stdio.h>      /* rename() */
176 #include <stdlib.h>     /* strtol() */
177 #include <string.h>
178 #include <unistd.h>     /* getpid() linkat() rmdir() unlinkat() */
179 
180 #ifndef _D_EXACT_NAMLEN
181 #ifdef _DIRENT_HAVE_D_NAMLEN
182 #define _D_EXACT_NAMLEN(d) ((d)->d_namlen)
183 #else
184 #define _D_EXACT_NAMLEN(d) (strlen ((d)->d_name))
185 #endif
186 #endif
187 
188 #ifndef PATH_MAX
189 #define PATH_MAX 4096
190 #endif
191 
192 #if defined(HAVE_LIBXML_H) && defined(HAVE_SQLITE3_H)
193 
194 #define USE_PROPPATCH
195 /* minor: libxml2 includes stdlib.h in headers, too */
196 #include <libxml/tree.h>
197 #include <libxml/parser.h>
198 #include <sqlite3.h>
199 
200 #if defined(HAVE_LIBUUID) && defined(HAVE_UUID_UUID_H)
201 #define USE_LOCKS
202 #include <uuid/uuid.h>
203 #endif
204 
205 #endif /* defined(HAVE_LIBXML_H) && defined(HAVE_SQLITE3_H) */
206 
207 #include "base.h"
208 #include "buffer.h"
209 #include "chunk.h"
210 #include "fdevent.h"
211 #include "http_header.h"
212 #include "etag.h"
213 #include "log.h"
214 #include "request.h"
215 #include "response.h"   /* http_response_redirect_to_directory() */
216 #include "stat_cache.h" /* stat_cache_mimetype_by_ext() */
217 
218 #include "plugin.h"
219 
220 #if (defined(__linux__) || defined(__CYGWIN__)) && defined(O_TMPFILE)
221 static int has_proc_self_fd;
222 #endif
223 
224 #define http_status_get(r)           ((r)->http_status)
225 #define http_status_set_fin(r, code) ((r)->resp_body_finished = 1,\
226                                       (r)->handler_module = NULL, \
227                                       (r)->http_status = (code))
228 #define http_status_set(r, code)     ((r)->http_status = (code))
229 #define http_status_unset(r)         ((r)->http_status = 0)
230 #define http_status_is_set(r)        (0 != (r)->http_status)
231 __attribute_cold__
232 __attribute_noinline__
233 static int http_status_set_error (request_st * const r, int status) {
234     return http_status_set_fin(r, status);
235 }
236 
237 typedef physical physical_st;
238 
239 INIT_FUNC(mod_webdav_init);
240 FREE_FUNC(mod_webdav_free);
241 SETDEFAULTS_FUNC(mod_webdav_set_defaults);
242 SERVER_FUNC(mod_webdav_worker_init);
243 URIHANDLER_FUNC(mod_webdav_uri_handler);
244 PHYSICALPATH_FUNC(mod_webdav_physical_handler);
245 SUBREQUEST_FUNC(mod_webdav_subrequest_handler);
246 REQUEST_FUNC(mod_webdav_handle_reset);
247 
248 int mod_webdav_plugin_init(plugin *p);
249 int mod_webdav_plugin_init(plugin *p) {
250     p->version           = LIGHTTPD_VERSION_ID;
251     p->name              = "webdav";
252 
253     p->init              = mod_webdav_init;
254     p->cleanup           = mod_webdav_free;
255     p->set_defaults      = mod_webdav_set_defaults;
256     p->worker_init       = mod_webdav_worker_init;
257     p->handle_uri_clean  = mod_webdav_uri_handler;
258     p->handle_physical   = mod_webdav_physical_handler;
259     p->handle_subrequest = mod_webdav_subrequest_handler;
260     p->handle_request_reset = mod_webdav_handle_reset;
261 
262     return 0;
263 }
264 
265 
266 #define WEBDAV_FILE_MODE  S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH
267 #define WEBDAV_DIR_MODE   S_IRWXU|S_IRWXG|S_IRWXO
268 
269 #define WEBDAV_FLAG_LC_NAMES     0x01
270 #define WEBDAV_FLAG_OVERWRITE    0x02
271 #define WEBDAV_FLAG_MOVE_RENAME  0x04
272 #define WEBDAV_FLAG_COPY_LINK    0x08
273 #define WEBDAV_FLAG_MOVE_XDEV    0x10
274 #define WEBDAV_FLAG_COPY_XDEV    0x20
275 
276 #define webdav_xmlstrcmp_fixed(s, fixed) \
277         strncmp((const char *)(s), (fixed), sizeof(fixed))
278 
279 #include <ctype.h>      /* isupper() tolower() */
280 __attribute_noinline__
281 static void
282 webdav_str_len_to_lower (char * const ss, const uint32_t len)
283 {
284     /*(caller must ensure that len not truncated to (int);
285      * for current intended use, NAME_MAX typically <= 255)*/
286     unsigned char * const restrict s = (unsigned char *)ss;
287     for (int i = 0; i < (int)len; ++i) {
288         if (isupper(s[i]))
289             s[i] = tolower(s[i]);
290     }
291 }
292 
293 typedef struct {
294   #ifdef USE_PROPPATCH
295     sqlite3 *sqlh;
296     sqlite3_stmt *stmt_props_select_propnames;
297     sqlite3_stmt *stmt_props_select_props;
298     sqlite3_stmt *stmt_props_select_prop;
299     sqlite3_stmt *stmt_props_update_prop;
300     sqlite3_stmt *stmt_props_delete_prop;
301 
302     sqlite3_stmt *stmt_props_copy;
303     sqlite3_stmt *stmt_props_move;
304     sqlite3_stmt *stmt_props_move_col;
305     sqlite3_stmt *stmt_props_delete;
306 
307     sqlite3_stmt *stmt_locks_acquire;
308     sqlite3_stmt *stmt_locks_refresh;
309     sqlite3_stmt *stmt_locks_release;
310     sqlite3_stmt *stmt_locks_read;
311     sqlite3_stmt *stmt_locks_read_uri;
312     sqlite3_stmt *stmt_locks_read_uri_infinity;
313     sqlite3_stmt *stmt_locks_read_uri_members;
314     sqlite3_stmt *stmt_locks_delete_uri;
315     sqlite3_stmt *stmt_locks_delete_uri_col;
316   #else
317     int dummy;
318   #endif
319 } sql_config;
320 
321 enum { /* opts bitflags */
322   MOD_WEBDAV_UNSAFE_PARTIAL_PUT_COMPAT      = 0x1
323  ,MOD_WEBDAV_UNSAFE_PROPFIND_FOLLOW_SYMLINK = 0x2
324  ,MOD_WEBDAV_PROPFIND_DEPTH_INFINITY        = 0x4
325 };
326 
327 typedef struct {
328     unsigned short enabled;
329     unsigned short is_readonly;
330     unsigned short log_xml;
331     unsigned short opts;
332 
333     sql_config *sql;
334     buffer *tmpb;
335     buffer *sqlite_db_name; /* not used after worker init */
336 } plugin_config;
337 
338 typedef struct {
339     PLUGIN_DATA;
340     plugin_config defaults;
341 } plugin_data;
342 
343 
344 INIT_FUNC(mod_webdav_init) {
345     return calloc(1, sizeof(plugin_data));
346 }
347 
348 
349 FREE_FUNC(mod_webdav_free) {
350     plugin_data * const p = (plugin_data *)p_d;
351     if (NULL == p->cvlist) return;
352     /* (init i to 0 if global context; to 1 to skip empty global context) */
353     for (int i = !p->cvlist[0].v.u2[1], used = p->nconfig; i < used; ++i) {
354         config_plugin_value_t *cpv = p->cvlist + p->cvlist[i].v.u2[0];
355         for (; -1 != cpv->k_id; ++cpv) {
356             switch (cpv->k_id) {
357              #ifdef USE_PROPPATCH
358               case 0: /* webdav.sqlite-db-name */
359                 if (cpv->vtype == T_CONFIG_LOCAL) {
360                     sql_config * const sql = cpv->v.v;
361                     if (!sql->sqlh) {
362                         free(sql);
363                         continue;
364                     }
365 
366                     sqlite3_finalize(sql->stmt_props_select_propnames);
367                     sqlite3_finalize(sql->stmt_props_select_props);
368                     sqlite3_finalize(sql->stmt_props_select_prop);
369                     sqlite3_finalize(sql->stmt_props_update_prop);
370                     sqlite3_finalize(sql->stmt_props_delete_prop);
371                     sqlite3_finalize(sql->stmt_props_copy);
372                     sqlite3_finalize(sql->stmt_props_move);
373                     sqlite3_finalize(sql->stmt_props_move_col);
374                     sqlite3_finalize(sql->stmt_props_delete);
375 
376                     sqlite3_finalize(sql->stmt_locks_acquire);
377                     sqlite3_finalize(sql->stmt_locks_refresh);
378                     sqlite3_finalize(sql->stmt_locks_release);
379                     sqlite3_finalize(sql->stmt_locks_read);
380                     sqlite3_finalize(sql->stmt_locks_read_uri);
381                     sqlite3_finalize(sql->stmt_locks_read_uri_infinity);
382                     sqlite3_finalize(sql->stmt_locks_read_uri_members);
383                     sqlite3_finalize(sql->stmt_locks_delete_uri);
384                     sqlite3_finalize(sql->stmt_locks_delete_uri_col);
385                     sqlite3_close(sql->sqlh);
386                     free(sql);
387                 }
388                 break;
389              #endif
390               default:
391                 break;
392             }
393         }
394     }
395 }
396 
397 
398 static void mod_webdav_merge_config_cpv(plugin_config * const pconf, const config_plugin_value_t * const cpv) {
399     switch (cpv->k_id) { /* index into static config_plugin_keys_t cpk[] */
400       case 0: /* webdav.sqlite-db-name */
401         if (cpv->vtype == T_CONFIG_LOCAL)
402             pconf->sql = cpv->v.v;
403         break;
404       case 1: /* webdav.activate */
405         pconf->enabled = (unsigned short)cpv->v.u;
406         break;
407       case 2: /* webdav.is-readonly */
408         pconf->is_readonly = (unsigned short)cpv->v.u;
409         break;
410       case 3: /* webdav.log-xml */
411         pconf->log_xml = (unsigned short)cpv->v.u;
412         break;
413       case 4: /* webdav.opts */
414         if (cpv->vtype == T_CONFIG_LOCAL)
415             pconf->opts = (unsigned short)cpv->v.u;
416         break;
417       default:/* should not happen */
418         return;
419     }
420 }
421 
422 
423 static void mod_webdav_merge_config(plugin_config * const pconf, const config_plugin_value_t *cpv) {
424     do {
425         mod_webdav_merge_config_cpv(pconf, cpv);
426     } while ((++cpv)->k_id != -1);
427 }
428 
429 
430 static void mod_webdav_patch_config(request_st * const r, plugin_data * const p, plugin_config * const pconf) {
431     memcpy(pconf, &p->defaults, sizeof(plugin_config));
432     for (int i = 1, used = p->nconfig; i < used; ++i) {
433         if (config_check_cond(r, (uint32_t)p->cvlist[i].k_id))
434             mod_webdav_merge_config(pconf, p->cvlist + p->cvlist[i].v.u2[0]);
435     }
436 }
437 
438 
439 __attribute_cold__
440 static int mod_webdav_sqlite3_init (const char * restrict s, log_error_st *errh);
441 
442 SETDEFAULTS_FUNC(mod_webdav_set_defaults) {
443     static const config_plugin_keys_t cpk[] = {
444       { CONST_STR_LEN("webdav.sqlite-db-name"),
445         T_CONFIG_STRING,
446         T_CONFIG_SCOPE_CONNECTION }
447      ,{ CONST_STR_LEN("webdav.activate"),
448         T_CONFIG_BOOL,
449         T_CONFIG_SCOPE_CONNECTION }
450      ,{ CONST_STR_LEN("webdav.is-readonly"),
451         T_CONFIG_BOOL,
452         T_CONFIG_SCOPE_CONNECTION }
453      ,{ CONST_STR_LEN("webdav.log-xml"),
454         T_CONFIG_BOOL,
455         T_CONFIG_SCOPE_CONNECTION }
456      ,{ CONST_STR_LEN("webdav.opts"),
457         T_CONFIG_ARRAY_KVSTRING,
458         T_CONFIG_SCOPE_CONNECTION }
459      ,{ NULL, 0,
460         T_CONFIG_UNSET,
461         T_CONFIG_SCOPE_UNSET }
462     };
463 
464     plugin_data * const p = p_d;
465     if (!config_plugin_values_init(srv, p, cpk, "mod_webdav"))
466         return HANDLER_ERROR;
467 
468   #ifdef USE_PROPPATCH
469     int sqlrc = sqlite3_config(SQLITE_CONFIG_SINGLETHREAD);
470     if (sqlrc != SQLITE_OK) {
471         log_error(srv->errh, __FILE__, __LINE__, "sqlite3_config(): %s",
472                   sqlite3_errstr(sqlrc));
473         /*(performance option since our use is not threaded; not fatal)*/
474         /*return HANDLER_ERROR;*/
475     }
476   #endif
477 
478     /* process and validate config directives
479      * (init i to 0 if global context; to 1 to skip empty global context) */
480     for (int i = !p->cvlist[0].v.u2[1]; i < p->nconfig; ++i) {
481         config_plugin_value_t *cpv = p->cvlist + p->cvlist[i].v.u2[0];
482         for (; -1 != cpv->k_id; ++cpv) {
483             switch (cpv->k_id) {
484               case 0: /* webdav.sqlite-db-name */
485                 if (!buffer_string_is_empty(cpv->v.b)) {
486                     if (!mod_webdav_sqlite3_init(cpv->v.b->ptr, srv->errh))
487                         return HANDLER_ERROR;
488                 }
489                 break;
490               case 1: /* webdav.activate */
491               case 2: /* webdav.is-readonly */
492               case 3: /* webdav.log-xml */
493                 break;
494               case 4: /* webdav.opts */
495                 if (cpv->v.a->used) {
496                     unsigned short opts = 0;
497                     for (uint32_t j = 0, used = cpv->v.a->used; j < used; ++j) {
498                         data_string *ds = (data_string *)cpv->v.a->data[j];
499                         if (buffer_is_equal_string(&ds->key,
500                               CONST_STR_LEN("deprecated-unsafe-partial-put"))
501                             && buffer_eq_slen(&ds->value,
502                                               CONST_STR_LEN("enable"))) {
503                             opts |= MOD_WEBDAV_UNSAFE_PARTIAL_PUT_COMPAT;
504                             continue;
505                         }
506                         if (buffer_is_equal_string(&ds->key,
507                               CONST_STR_LEN("propfind-depth-infinity"))
508                             && buffer_eq_slen(&ds->value,
509                                               CONST_STR_LEN("enable"))) {
510                             opts |= MOD_WEBDAV_PROPFIND_DEPTH_INFINITY;
511                             continue;
512                         }
513                         if (buffer_is_equal_string(&ds->key,
514                               CONST_STR_LEN("unsafe-propfind-follow-symlink"))
515                             && buffer_eq_slen(&ds->value,
516                                               CONST_STR_LEN("enable"))) {
517                             opts |= MOD_WEBDAV_UNSAFE_PROPFIND_FOLLOW_SYMLINK;
518                             continue;
519                         }
520                         log_error(srv->errh, __FILE__, __LINE__,
521                                   "unrecognized webdav.opts: %.*s",
522                                   BUFFER_INTLEN_PTR(&ds->key));
523                         return HANDLER_ERROR;
524                     }
525                     cpv->v.u = opts;
526                     cpv->vtype = T_CONFIG_LOCAL;
527                 }
528                 break;
529               default:/* should not happen */
530                 break;
531             }
532         }
533     }
534 
535     p->defaults.tmpb = srv->tmp_buf;
536 
537     /* initialize p->defaults from global config context */
538     if (p->nconfig > 0 && p->cvlist->v.u2[1]) {
539         const config_plugin_value_t *cpv = p->cvlist + p->cvlist->v.u2[0];
540         if (-1 != cpv->k_id)
541             mod_webdav_merge_config(&p->defaults, cpv);
542     }
543 
544   #if (defined(__linux__) || defined(__CYGWIN__)) && defined(O_TMPFILE)
545     struct stat st;
546     has_proc_self_fd = (0 == stat("/proc/self/fd", &st));
547   #endif
548 
549     return HANDLER_GO_ON;
550 }
551 
552 
553 URIHANDLER_FUNC(mod_webdav_uri_handler)
554 {
555     if (r->http_method != HTTP_METHOD_OPTIONS)
556         return HANDLER_GO_ON;
557 
558     plugin_config pconf;
559     mod_webdav_patch_config(r, (plugin_data *)p_d, &pconf);
560     if (!pconf.enabled) return HANDLER_GO_ON;
561 
562     /* [RFC4918] 18 DAV Compliance Classes */
563     http_header_response_set(r, HTTP_HEADER_OTHER,
564       CONST_STR_LEN("DAV"),
565      #ifdef USE_LOCKS
566       CONST_STR_LEN("1,2,3")
567      #else
568       CONST_STR_LEN("1,3")
569      #endif
570     );
571 
572     /* instruct MS Office Web Folders to use DAV
573      * (instead of MS FrontPage Extensions)
574      * http://www.zorched.net/2006/03/01/more-webdav-tips-tricks-and-bugs/ */
575     http_header_response_set(r, HTTP_HEADER_OTHER,
576                              CONST_STR_LEN("MS-Author-Via"),
577                              CONST_STR_LEN("DAV"));
578 
579     if (pconf.is_readonly)
580         http_header_response_append(r, HTTP_HEADER_ALLOW,
581           CONST_STR_LEN("Allow"),
582           CONST_STR_LEN("PROPFIND"));
583     else
584         http_header_response_append(r, HTTP_HEADER_ALLOW,
585           CONST_STR_LEN("Allow"),
586       #ifdef USE_PROPPATCH
587        #ifdef USE_LOCKS
588           CONST_STR_LEN(
589             "PROPFIND, DELETE, MKCOL, PUT, MOVE, COPY, PROPPATCH, LOCK, UNLOCK")
590        #else
591           CONST_STR_LEN(
592             "PROPFIND, DELETE, MKCOL, PUT, MOVE, COPY, PROPPATCH")
593        #endif
594       #else
595           CONST_STR_LEN(
596             "PROPFIND, DELETE, MKCOL, PUT, MOVE, COPY")
597       #endif
598         );
599 
600     return HANDLER_GO_ON;
601 }
602 
603 
604 #ifdef USE_LOCKS
605 
606 typedef struct webdav_lockdata_wr {
607   buffer locktoken;
608   buffer lockroot;
609   buffer ownerinfo;
610   buffer *owner;           /* NB: caller must provide writable storage */
611   const buffer *lockscope; /* future: might use enum, store int in db */
612   const buffer *locktype;  /* future: might use enum, store int in db */
613   int depth;
614   int timeout;           /* offset from now, not absolute time_t */
615 } webdav_lockdata_wr;
616 
617 typedef struct webdav_lockdata {
618   buffer locktoken;
619   buffer lockroot;
620   buffer ownerinfo;
621   const buffer *owner;
622   const buffer *lockscope; /* future: might use enum, store int in db */
623   const buffer *locktype;  /* future: might use enum, store int in db */
624   int depth;
625   int timeout;           /* offset from now, not absolute time_t */
626 } webdav_lockdata;
627 
628 typedef struct { const char *ptr; uint32_t used; uint32_t size; } tagb;
629 
630 static const tagb lockscope_exclusive =
631   { "exclusive", sizeof("exclusive"), 0 };
632 static const tagb lockscope_shared =
633   { "shared",    sizeof("shared"),    0 };
634 static const tagb locktype_write =
635   { "write",     sizeof("write"),     0 };
636 
637 #endif
638 
639 typedef struct {
640     const char *ns;
641     const char *name;
642     uint32_t nslen;
643     uint32_t namelen;
644 } webdav_property_name;
645 
646 typedef struct {
647     webdav_property_name *ptr;
648     int used;
649     int size;
650 } webdav_property_names;
651 
652 /*
653  * http://www.w3.org/TR/1998/NOTE-XML-data-0105/
654  *   The datatype attribute "dt" is defined in the namespace named
655  *   "urn:uuid:C2F41010-65B3-11d1-A29F-00AA00C14882/".
656  *   (See the XML Namespaces Note at the W3C site for details of namespaces.)
657  *   The full URN of the attribute is
658  *   "urn:uuid:C2F41010-65B3-11d1-A29F-00AA00C14882/dt".
659  * http://www.w3.org/TR/1998/NOTE-xml-names-0119
660  * http://www.w3.org/TR/1998/WD-xml-names-19980327
661  * http://lists.xml.org/archives/xml-dev/200101/msg00924.html
662  * http://lists.xml.org/archives/xml-dev/200101/msg00929.html
663  * http://lists.xml.org/archives/xml-dev/200101/msg00930.html
664  * (Microsoft) Namespace Guidelines
665  *   https://msdn.microsoft.com/en-us/library/ms879470%28v=exchg.65%29.aspx
666  * (Microsoft) XML Persistence Format
667  *   https://msdn.microsoft.com/en-us/library/ms676547%28v=vs.85%29.aspx
668  * http://www.xml.com/pub/a/2002/06/26/vocabularies.html
669  *   The "Uuid" namespaces is the namespace
670  *   "uuid:C2F41010-65B3-11d1-A29F-00AA00C14882",
671  *   mainly found in association with the MS Office
672  *   namespace on the http://www.omg.org website.
673  * http://www.data2type.de/en/xml-xslt-xslfo/wordml/wordml-introduction/the-root-element/
674  *   xmlns:dt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882"
675  *   By using the prefix dt, the namespace declares an attribute which
676  *   determines the data type of a value. The name of the underlying schema
677  *   is dt.xsd and it can be found in the folder for Excel schemas.
678  */
679 #define MOD_WEBDAV_XMLNS_NS0 "xmlns:ns0=\"urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/\""
680 
681 
682 static void
683 webdav_xml_doctype (buffer * const b, request_st * const r)
684 {
685     http_header_response_set(r, HTTP_HEADER_CONTENT_TYPE,
686       CONST_STR_LEN("Content-Type"),
687       CONST_STR_LEN("application/xml; charset=\"utf-8\""));
688 
689     buffer_copy_string_len(b, CONST_STR_LEN(
690       "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"));
691 }
692 
693 
694 static void
695 webdav_xml_prop (buffer * const b,
696                  const webdav_property_name * const prop,
697                  const char * const value, const uint32_t vlen)
698 {
699     buffer_append_string_len(b, CONST_STR_LEN("<"));
700     buffer_append_string_len(b, prop->name, prop->namelen);
701     buffer_append_string_len(b, CONST_STR_LEN(" xmlns=\""));
702     buffer_append_string_len(b, prop->ns, prop->nslen);
703     if (0 == vlen) {
704         buffer_append_string_len(b, CONST_STR_LEN("\"/>"));
705     }
706     else {
707         buffer_append_string_len(b, CONST_STR_LEN("\">"));
708         buffer_append_string_len(b, value, vlen);
709         buffer_append_string_len(b, CONST_STR_LEN("</"));
710         buffer_append_string_len(b, prop->name, prop->namelen);
711         buffer_append_string_len(b, CONST_STR_LEN(">"));
712     }
713 }
714 
715 
716 #ifdef USE_LOCKS
717 static void
718 webdav_xml_href_raw (buffer * const b, const buffer * const href)
719 {
720     buffer_append_string_len(b, CONST_STR_LEN(
721       "<D:href>"));
722     buffer_append_string_len(b, CONST_BUF_LEN(href));
723     buffer_append_string_len(b, CONST_STR_LEN(
724       "</D:href>\n"));
725 }
726 #endif
727 
728 
729 static void
730 webdav_xml_href (buffer * const b, const buffer * const href)
731 {
732     buffer_append_string_len(b, CONST_STR_LEN(
733       "<D:href>"));
734     buffer_append_string_encoded(b, CONST_BUF_LEN(href), ENCODING_REL_URI);
735     buffer_append_string_len(b, CONST_STR_LEN(
736       "</D:href>\n"));
737 }
738 
739 
740 static void
741 webdav_xml_status (buffer * const b, const int status)
742 {
743     buffer_append_string_len(b, CONST_STR_LEN(
744       "<D:status>HTTP/1.1 "));
745     http_status_append(b, status);
746     buffer_append_string_len(b, CONST_STR_LEN(
747       "</D:status>\n"));
748 }
749 
750 
751 #ifdef USE_PROPPATCH
752 __attribute_cold__
753 static void
754 webdav_xml_propstat_protected (buffer * const b, const char * const propname,
755                                const uint32_t len, const int status)
756 {
757     buffer_append_string_len(b, CONST_STR_LEN(
758       "<D:propstat>\n"
759       "<D:prop><DAV:"));
760     buffer_append_string_len(b, propname, len);
761     buffer_append_string_len(b, CONST_STR_LEN(
762       "/></D:prop>\n"
763       "<D:error><DAV:cannot-modify-protected-property/></D:error>\n"));
764     webdav_xml_status(b, status); /* 403 */
765     buffer_append_string_len(b, CONST_STR_LEN(
766       "</D:propstat>\n"));
767 }
768 #endif
769 
770 
771 #ifdef USE_PROPPATCH
772 __attribute_cold__
773 static void
774 webdav_xml_propstat_status (buffer * const b, const char * const ns,
775                             const char * const name, const int status)
776 {
777     buffer_append_string_len(b, CONST_STR_LEN(
778       "<D:propstat>\n"
779       "<D:prop><"));
780     buffer_append_string(b, ns);
781     buffer_append_string(b, name);
782     buffer_append_string_len(b, CONST_STR_LEN(
783       "/></D:prop>\n"));
784     webdav_xml_status(b, status);
785     buffer_append_string_len(b, CONST_STR_LEN(
786       "</D:propstat>\n"));
787 }
788 #endif
789 
790 
791 static void
792 webdav_xml_propstat (buffer * const b, buffer * const value, const int status)
793 {
794     buffer_append_string_len(b, CONST_STR_LEN(
795       "<D:propstat>\n"
796       "<D:prop>\n"));
797     buffer_append_string_buffer(b, value);
798     buffer_append_string_len(b, CONST_STR_LEN(
799       "</D:prop>\n"));
800     webdav_xml_status(b, status);
801     buffer_append_string_len(b, CONST_STR_LEN(
802       "</D:propstat>\n"));
803 }
804 
805 
806 __attribute_cold__
807 static void
808 webdav_xml_response_status (buffer * const b,
809                             const buffer * const href,
810                             const int status)
811 {
812     buffer_append_string_len(b, CONST_STR_LEN(
813       "<D:response>\n"));
814     webdav_xml_href(b, href);
815     webdav_xml_status(b, status);
816     buffer_append_string_len(b, CONST_STR_LEN(
817       "</D:response>\n"));
818 }
819 
820 
821 #ifdef USE_LOCKS
822 static void
823 webdav_xml_activelock (buffer * const b,
824                        const webdav_lockdata * const lockdata,
825                        const char * const tbuf, uint32_t tbuf_len)
826 {
827     buffer_append_string_len(b, CONST_STR_LEN(
828       "<D:activelock>\n"
829       "<D:lockscope>"
830       "<D:"));
831     buffer_append_string_buffer(b, lockdata->lockscope);
832     buffer_append_string_len(b, CONST_STR_LEN(
833       "/>"
834       "</D:lockscope>\n"
835       "<D:locktype>"
836       "<D:"));
837     buffer_append_string_buffer(b, lockdata->locktype);
838     buffer_append_string_len(b, CONST_STR_LEN(
839       "/>"
840       "</D:locktype>\n"
841       "<D:depth>"));
842     if (0 == lockdata->depth)
843         buffer_append_string_len(b, CONST_STR_LEN("0"));
844     else
845         buffer_append_string_len(b, CONST_STR_LEN("infinity"));
846     buffer_append_string_len(b, CONST_STR_LEN(
847       "</D:depth>\n"
848       "<D:timeout>"));
849     if (0 != tbuf_len)
850         buffer_append_string_len(b, tbuf, tbuf_len); /* "Second-..." */
851     else {
852         buffer_append_string_len(b, CONST_STR_LEN("Second-"));
853         buffer_append_int(b, lockdata->timeout);
854     }
855     buffer_append_string_len(b, CONST_STR_LEN(
856       "</D:timeout>\n"
857       "<D:owner>"));
858     if (!buffer_string_is_empty(&lockdata->ownerinfo))
859         buffer_append_string_buffer(b, &lockdata->ownerinfo);
860     buffer_append_string_len(b, CONST_STR_LEN(
861       "</D:owner>\n"
862       "<D:locktoken>\n"));
863     webdav_xml_href_raw(b, &lockdata->locktoken); /*(as-is; not URL-encoded)*/
864     buffer_append_string_len(b, CONST_STR_LEN(
865       "</D:locktoken>\n"
866       "<D:lockroot>\n"));
867     webdav_xml_href(b, &lockdata->lockroot);
868     buffer_append_string_len(b, CONST_STR_LEN(
869       "</D:lockroot>\n"
870       "</D:activelock>\n"));
871 }
872 #endif
873 
874 
875 static void
876 webdav_xml_doc_multistatus (request_st * const r,
877                             const plugin_config * const pconf,
878                             buffer * const ms)
879 {
880     http_status_set_fin(r, 207); /* Multi-status */
881 
882     buffer * const b = /*(optimization; buf extended as needed)*/
883       chunkqueue_append_buffer_open_sz(&r->write_queue, 128 + ms->used);
884 
885     webdav_xml_doctype(b, r);
886     buffer_append_string_len(b, CONST_STR_LEN(
887       "<D:multistatus xmlns:D=\"DAV:\">\n"));
888     buffer_append_string_buffer(b, ms);
889     buffer_append_string_len(b, CONST_STR_LEN(
890       "</D:multistatus>\n"));
891 
892     if (pconf->log_xml)
893         log_error(r->conf.errh, __FILE__, __LINE__,
894                   "XML-response-body: %.*s", BUFFER_INTLEN_PTR(b));
895 
896     chunkqueue_append_buffer_commit(&r->write_queue);
897 }
898 
899 
900 #ifdef USE_PROPPATCH
901 static void
902 webdav_xml_doc_multistatus_response (request_st * const r,
903                                      const plugin_config * const pconf,
904                                      buffer * const ms)
905 {
906     http_status_set_fin(r, 207); /* Multi-status */
907 
908     buffer * const b = /*(optimization; buf extended as needed)*/
909       chunkqueue_append_buffer_open_sz(&r->write_queue, 128 + ms->used);
910 
911     webdav_xml_doctype(b, r);
912     buffer_append_string_len(b, CONST_STR_LEN(
913       "<D:multistatus xmlns:D=\"DAV:\">\n"
914       "<D:response>\n"));
915     webdav_xml_href(b, &r->physical.rel_path);
916     buffer_append_string_buffer(b, ms);
917     buffer_append_string_len(b, CONST_STR_LEN(
918       "</D:response>\n"
919       "</D:multistatus>\n"));
920 
921     if (pconf->log_xml)
922         log_error(r->conf.errh, __FILE__, __LINE__,
923                   "XML-response-body: %.*s", BUFFER_INTLEN_PTR(b));
924 
925     chunkqueue_append_buffer_commit(&r->write_queue);
926 }
927 #endif
928 
929 
930 #ifdef USE_LOCKS
931 static void
932 webdav_xml_doc_lock_acquired (request_st * const r,
933                               const plugin_config * const pconf,
934                               const webdav_lockdata * const lockdata)
935 {
936     /*(http_status is set by caller to 200 OK or 201 Created)*/
937 
938     char tbuf[32] = "Second-";
939     const uint32_t tbuf_len = sizeof("Second-")-1 +
940       li_itostrn(tbuf+sizeof("Second-")-1, sizeof(tbuf)-(sizeof("Second-")-1),
941                  lockdata->timeout);
942     http_header_response_set(r, HTTP_HEADER_OTHER,
943       CONST_STR_LEN("Timeout"),
944       tbuf, tbuf_len);
945 
946     buffer * const b =
947       chunkqueue_append_buffer_open_sz(&r->write_queue, 1024);
948 
949     webdav_xml_doctype(b, r);
950     buffer_append_string_len(b, CONST_STR_LEN(
951       "<D:prop xmlns:D=\"DAV:\">\n"
952       "<D:lockdiscovery>\n"));
953     webdav_xml_activelock(b, lockdata, tbuf, tbuf_len);
954     buffer_append_string_len(b, CONST_STR_LEN(
955       "</D:lockdiscovery>\n"
956       "</D:prop>\n"));
957 
958     if (pconf->log_xml)
959         log_error(r->conf.errh, __FILE__, __LINE__,
960                   "XML-response-body: %.*s", BUFFER_INTLEN_PTR(b));
961 
962     chunkqueue_append_buffer_commit(&r->write_queue);
963 }
964 #endif
965 
966 
967 /*
968  * [RFC4918] 16 Precondition/Postcondition XML Elements
969  */
970 
971 
972 /*
973  * 403 Forbidden
974  * "<D:error><DAV:cannot-modify-protected-property/></D:error>"
975  *
976  * 403 Forbidden
977  * "<D:error><DAV:no-external-entities/></D:error>"
978  *
979  * 409 Conflict
980  * "<D:error><DAV:preserved-live-properties/></D:error>"
981  */
982 
983 
984 __attribute_cold__
985 static void
986 webdav_xml_doc_error_propfind_finite_depth (request_st * const r)
987 {
988     http_status_set(r, 403); /* Forbidden */
989     r->resp_body_finished = 1;
990 
991     buffer * const b =
992       chunkqueue_append_buffer_open_sz(&r->write_queue, 256);
993     webdav_xml_doctype(b, r);
994     buffer_append_string_len(b, CONST_STR_LEN(
995       "<D:error><DAV:propfind-finite-depth/></D:error>\n"));
996     chunkqueue_append_buffer_commit(&r->write_queue);
997 }
998 
999 
1000 #ifdef USE_LOCKS
1001 __attribute_cold__
1002 static void
1003 webdav_xml_doc_error_lock_token_matches_request_uri (request_st * const r)
1004 {
1005     http_status_set(r, 409); /* Conflict */
1006     r->resp_body_finished = 1;
1007 
1008     buffer * const b =
1009       chunkqueue_append_buffer_open_sz(&r->write_queue, 256);
1010     webdav_xml_doctype(b, r);
1011     buffer_append_string_len(b, CONST_STR_LEN(
1012       "<D:error><DAV:lock-token-matches-request-uri/></D:error>\n"));
1013     chunkqueue_append_buffer_commit(&r->write_queue);
1014 }
1015 #endif
1016 
1017 
1018 #ifdef USE_LOCKS
1019 __attribute_cold__
1020 static void
1021 webdav_xml_doc_423_locked (request_st * const r, buffer * const hrefs,
1022                            const char * const errtag, const uint32_t errtaglen)
1023 {
1024     http_status_set(r, 423); /* Locked */
1025     r->resp_body_finished = 1;
1026 
1027     buffer * const b = /*(optimization; buf extended as needed)*/
1028       chunkqueue_append_buffer_open_sz(&r->write_queue, 256 + hrefs->used);
1029 
1030     webdav_xml_doctype(b, r);
1031     buffer_append_string_len(b, CONST_STR_LEN(
1032       "<D:error xmlns:D=\"DAV:\">\n"
1033       "<D:"));
1034     buffer_append_string_len(b, errtag, errtaglen);
1035     buffer_append_string_len(b, CONST_STR_LEN(
1036       ">\n"));
1037     buffer_append_string_buffer(b, hrefs);
1038     buffer_append_string_len(b, CONST_STR_LEN(
1039       "</D:"));
1040     buffer_append_string_len(b, errtag, errtaglen);
1041     buffer_append_string_len(b, CONST_STR_LEN(
1042       ">\n"
1043       "</D:error>\n"));
1044 
1045     chunkqueue_append_buffer_commit(&r->write_queue);
1046 }
1047 #endif
1048 
1049 
1050 #ifdef USE_LOCKS
1051 __attribute_cold__
1052 static void
1053 webdav_xml_doc_error_lock_token_submitted (request_st * const r,
1054                                            buffer * const hrefs)
1055 {
1056     webdav_xml_doc_423_locked(r, hrefs,
1057                               CONST_STR_LEN("lock-token-submitted"));
1058 }
1059 #endif
1060 
1061 
1062 #ifdef USE_LOCKS
1063 __attribute_cold__
1064 static void
1065 webdav_xml_doc_error_no_conflicting_lock (request_st * const r,
1066                                           buffer * const hrefs)
1067 {
1068     webdav_xml_doc_423_locked(r, hrefs,
1069                               CONST_STR_LEN("no-conflicting-lock"));
1070 }
1071 #endif
1072 
1073 
1074 #ifdef USE_PROPPATCH
1075 
1076   #define MOD_WEBDAV_SQLITE_CREATE_TABLE_PROPERTIES \
1077     "CREATE TABLE IF NOT EXISTS properties ("       \
1078     "  resource TEXT NOT NULL,"                     \
1079     "  prop TEXT NOT NULL,"                         \
1080     "  ns TEXT NOT NULL,"                           \
1081     "  value TEXT NOT NULL,"                        \
1082     "  PRIMARY KEY(resource, prop, ns))"
1083 
1084   #define MOD_WEBDAV_SQLITE_CREATE_TABLE_LOCKS \
1085     "CREATE TABLE IF NOT EXISTS locks ("       \
1086     "  locktoken TEXT NOT NULL,"               \
1087     "  resource TEXT NOT NULL,"                \
1088     "  lockscope TEXT NOT NULL,"               \
1089     "  locktype TEXT NOT NULL,"                \
1090     "  owner TEXT NOT NULL,"                   \
1091     "  ownerinfo TEXT NOT NULL,"               \
1092     "  depth INT NOT NULL,"                    \
1093     "  timeout TIMESTAMP NOT NULL,"            \
1094     "  PRIMARY KEY(locktoken))"
1095 
1096   #define MOD_WEBDAV_SQLITE_PROPS_SELECT_PROPNAMES \
1097     "SELECT prop, ns FROM properties WHERE resource = ?"
1098 
1099   #define MOD_WEBDAV_SQLITE_PROPS_SELECT_PROP \
1100     "SELECT value FROM properties WHERE resource = ? AND prop = ? AND ns = ?"
1101 
1102   #define MOD_WEBDAV_SQLITE_PROPS_SELECT_PROPS \
1103     "SELECT prop, ns, value FROM properties WHERE resource = ?"
1104 
1105   #define MOD_WEBDAV_SQLITE_PROPS_UPDATE_PROP \
1106     "REPLACE INTO properties (resource, prop, ns, value) VALUES (?, ?, ?, ?)"
1107 
1108   #define MOD_WEBDAV_SQLITE_PROPS_DELETE_PROP \
1109     "DELETE FROM properties WHERE resource = ? AND prop = ? AND ns = ?"
1110 
1111   #define MOD_WEBDAV_SQLITE_PROPS_DELETE \
1112     "DELETE FROM properties WHERE resource = ?"
1113 
1114   #define MOD_WEBDAV_SQLITE_PROPS_COPY \
1115     "INSERT INTO properties"           \
1116     "  SELECT ?, prop, ns, value FROM properties WHERE resource = ?"
1117 
1118   #define MOD_WEBDAV_SQLITE_PROPS_MOVE \
1119     "UPDATE OR REPLACE properties SET resource = ? WHERE resource = ?"
1120 
1121   #define MOD_WEBDAV_SQLITE_PROPS_MOVE_COL                                 \
1122     "UPDATE OR REPLACE properties SET resource = ? || SUBSTR(resource, ?)" \
1123     "  WHERE SUBSTR(resource, 1, ?) = ?"
1124 
1125   #define MOD_WEBDAV_SQLITE_LOCKS_ACQUIRE                                     \
1126     "INSERT INTO locks"                                                       \
1127     "  (locktoken,resource,lockscope,locktype,owner,ownerinfo,depth,timeout)" \
1128     "  VALUES (?,?,?,?,?,?,?, CURRENT_TIME + ?)"
1129 
1130   #define MOD_WEBDAV_SQLITE_LOCKS_REFRESH \
1131     "UPDATE locks SET timeout = CURRENT_TIME + ? WHERE locktoken = ?"
1132 
1133   #define MOD_WEBDAV_SQLITE_LOCKS_RELEASE \
1134     "DELETE FROM locks WHERE locktoken = ?"
1135 
1136   #define MOD_WEBDAV_SQLITE_LOCKS_READ \
1137     "SELECT resource, owner, depth"    \
1138     "  FROM locks WHERE locktoken = ?"
1139 
1140   #define MOD_WEBDAV_SQLITE_LOCKS_READ_URI                           \
1141     "SELECT"                                                         \
1142     "  locktoken,resource,lockscope,locktype,owner,ownerinfo,depth," \
1143         "timeout - CURRENT_TIME"                                     \
1144     "  FROM locks WHERE resource = ?"
1145 
1146   #define MOD_WEBDAV_SQLITE_LOCKS_READ_URI_INFINITY                  \
1147     "SELECT"                                                         \
1148     "  locktoken,resource,lockscope,locktype,owner,ownerinfo,depth," \
1149         "timeout - CURRENT_TIME"                                     \
1150     "  FROM locks"                                                   \
1151     "  WHERE depth = -1 AND resource = SUBSTR(?, 1, LENGTH(resource))"
1152 
1153   #define MOD_WEBDAV_SQLITE_LOCKS_READ_URI_MEMBERS                   \
1154     "SELECT"                                                         \
1155     "  locktoken,resource,lockscope,locktype,owner,ownerinfo,depth," \
1156         "timeout - CURRENT_TIME"                                     \
1157     "  FROM locks WHERE SUBSTR(resource, 1, ?) = ?"
1158 
1159   #define MOD_WEBDAV_SQLITE_LOCKS_DELETE_URI \
1160     "DELETE FROM locks WHERE resource = ?"
1161 
1162   #define MOD_WEBDAV_SQLITE_LOCKS_DELETE_URI_COL \
1163     "DELETE FROM locks WHERE SUBSTR(resource, 1, ?) = ?"
1164     /*"DELETE FROM locks WHERE locktoken LIKE ? || '%'"*/
1165 
1166   /*(not currently used)*/
1167   #define MOD_WEBDAV_SQLITE_LOCKS_DELETE_EXPIRED \
1168     "DELETE FROM locks WHERE timeout < CURRENT_TIME"
1169 
1170 #endif /* USE_PROPPATCH */
1171 
1172 
1173 __attribute_cold__
1174 static int
1175 mod_webdav_sqlite3_init (const char * const restrict dbname,
1176                          log_error_st * const errh)
1177 {
1178   #ifndef USE_PROPPATCH
1179 
1180     log_error(errh, __FILE__, __LINE__,
1181               "Sorry, no sqlite3 and libxml2 support include, "
1182               "compile with --with-webdav-props");
1183     UNUSED(dbname);
1184     return 0;
1185 
1186   #else /* USE_PROPPATCH */
1187 
1188   /*(expects (plugin_config *s) (log_error_st *errh) (char *err))*/
1189   #define MOD_WEBDAV_SQLITE_CREATE_TABLE(query, label)                   \
1190     if (sqlite3_exec(sqlh, query, NULL, NULL, &err) != SQLITE_OK) {      \
1191         if (0 != strcmp(err, "table " label " already exists")) {        \
1192             log_error(errh, __FILE__, __LINE__,                          \
1193                       "create table " label ": %s", err);                \
1194             sqlite3_free(err);                                           \
1195             sqlite3_close(sqlh);                                         \
1196             return 0;                                                    \
1197         }                                                                \
1198         sqlite3_free(err);                                               \
1199     }
1200 
1201     sqlite3 *sqlh;
1202     int sqlrc = sqlite3_open_v2(dbname, &sqlh,
1203                                 SQLITE_OPEN_READWRITE|SQLITE_OPEN_CREATE, NULL);
1204     if (sqlrc != SQLITE_OK) {
1205         log_error(errh, __FILE__, __LINE__, "sqlite3_open() '%s': %s",
1206                   dbname, sqlh ? sqlite3_errmsg(sqlh) : sqlite3_errstr(sqlrc));
1207         if (sqlh) sqlite3_close(sqlh);
1208         return 0;
1209     }
1210 
1211     char *err = NULL;
1212     MOD_WEBDAV_SQLITE_CREATE_TABLE( MOD_WEBDAV_SQLITE_CREATE_TABLE_PROPERTIES,
1213                                     "properties");
1214     MOD_WEBDAV_SQLITE_CREATE_TABLE( MOD_WEBDAV_SQLITE_CREATE_TABLE_LOCKS,
1215                                     "locks");
1216 
1217     /* add ownerinfo column to locks table (update older mod_webdav sqlite db)
1218      * (could check if 'PRAGMA user_version;' is 0, add column, and increment)*/
1219   #define MOD_WEBDAV_SQLITE_SELECT_LOCKS_OWNERINFO_TEST \
1220     "SELECT COUNT(*) FROM locks WHERE ownerinfo = \"\""
1221   #define MOD_WEBDAV_SQLITE_ALTER_TABLE_LOCKS \
1222     "ALTER TABLE locks ADD COLUMN ownerinfo TEXT NOT NULL DEFAULT \"\""
1223     if (sqlite3_exec(sqlh, MOD_WEBDAV_SQLITE_SELECT_LOCKS_OWNERINFO_TEST,
1224                      NULL, NULL, &err) != SQLITE_OK) {
1225         sqlite3_free(err); /* "no such column: ownerinfo" */
1226         if (sqlite3_exec(sqlh, MOD_WEBDAV_SQLITE_ALTER_TABLE_LOCKS,
1227                          NULL, NULL, &err) != SQLITE_OK) {
1228             log_error(errh, __FILE__, __LINE__, "alter table locks: %s", err);
1229             sqlite3_free(err);
1230             sqlite3_close(sqlh);
1231             return 0;
1232         }
1233     }
1234 
1235     sqlite3_close(sqlh);
1236     return 1;
1237 
1238   #endif /* USE_PROPPATCH */
1239 }
1240 
1241 
1242 #ifdef USE_PROPPATCH
1243 __attribute_cold__
1244 static int
1245 mod_webdav_sqlite3_prep (sql_config * const restrict sql,
1246                          const char * const sqlite_db_name,
1247                          log_error_st * const errh)
1248 {
1249   /*(expects (plugin_config *s) (log_error_st *errh))*/
1250   #define MOD_WEBDAV_SQLITE_PREPARE_STMT(query, stmt)                      \
1251     if (sqlite3_prepare_v2(sql->sqlh, query, sizeof(query)-1, &stmt, NULL) \
1252         != SQLITE_OK) {                                                    \
1253         log_error(errh, __FILE__, __LINE__, "sqlite3_prepare_v2(): %s",    \
1254                   sqlite3_errmsg(sql->sqlh));                              \
1255         return 0;                                                          \
1256     }
1257 
1258     int sqlrc = sqlite3_open_v2(sqlite_db_name, &sql->sqlh,
1259                                 SQLITE_OPEN_READWRITE, NULL);
1260     if (sqlrc != SQLITE_OK) {
1261         log_error(errh, __FILE__, __LINE__, "sqlite3_open() '%s': %s",
1262                   sqlite_db_name,
1263                   sql->sqlh
1264                     ? sqlite3_errmsg(sql->sqlh)
1265                     : sqlite3_errstr(sqlrc));
1266         return 0;
1267     }
1268 
1269     /* future: perhaps not all statements should be prepared;
1270      * infrequently executed statements could be run with sqlite3_exec(),
1271      * or prepared and finalized on each use, as needed */
1272 
1273     MOD_WEBDAV_SQLITE_PREPARE_STMT( MOD_WEBDAV_SQLITE_PROPS_SELECT_PROPNAMES,
1274                                     sql->stmt_props_select_propnames);
1275     MOD_WEBDAV_SQLITE_PREPARE_STMT( MOD_WEBDAV_SQLITE_PROPS_SELECT_PROPS,
1276                                     sql->stmt_props_select_props);
1277     MOD_WEBDAV_SQLITE_PREPARE_STMT( MOD_WEBDAV_SQLITE_PROPS_SELECT_PROP,
1278                                     sql->stmt_props_select_prop);
1279     MOD_WEBDAV_SQLITE_PREPARE_STMT( MOD_WEBDAV_SQLITE_PROPS_UPDATE_PROP,
1280                                     sql->stmt_props_update_prop);
1281     MOD_WEBDAV_SQLITE_PREPARE_STMT( MOD_WEBDAV_SQLITE_PROPS_DELETE_PROP,
1282                                     sql->stmt_props_delete_prop);
1283     MOD_WEBDAV_SQLITE_PREPARE_STMT( MOD_WEBDAV_SQLITE_PROPS_COPY,
1284                                     sql->stmt_props_copy);
1285     MOD_WEBDAV_SQLITE_PREPARE_STMT( MOD_WEBDAV_SQLITE_PROPS_MOVE,
1286                                     sql->stmt_props_move);
1287     MOD_WEBDAV_SQLITE_PREPARE_STMT( MOD_WEBDAV_SQLITE_PROPS_MOVE_COL,
1288                                     sql->stmt_props_move_col);
1289     MOD_WEBDAV_SQLITE_PREPARE_STMT( MOD_WEBDAV_SQLITE_PROPS_DELETE,
1290                                     sql->stmt_props_delete);
1291     MOD_WEBDAV_SQLITE_PREPARE_STMT( MOD_WEBDAV_SQLITE_LOCKS_ACQUIRE,
1292                                     sql->stmt_locks_acquire);
1293     MOD_WEBDAV_SQLITE_PREPARE_STMT( MOD_WEBDAV_SQLITE_LOCKS_REFRESH,
1294                                     sql->stmt_locks_refresh);
1295     MOD_WEBDAV_SQLITE_PREPARE_STMT( MOD_WEBDAV_SQLITE_LOCKS_RELEASE,
1296                                     sql->stmt_locks_release);
1297     MOD_WEBDAV_SQLITE_PREPARE_STMT( MOD_WEBDAV_SQLITE_LOCKS_READ,
1298                                     sql->stmt_locks_read);
1299     MOD_WEBDAV_SQLITE_PREPARE_STMT( MOD_WEBDAV_SQLITE_LOCKS_READ_URI,
1300                                     sql->stmt_locks_read_uri);
1301     MOD_WEBDAV_SQLITE_PREPARE_STMT( MOD_WEBDAV_SQLITE_LOCKS_READ_URI_INFINITY,
1302                                     sql->stmt_locks_read_uri_infinity);
1303     MOD_WEBDAV_SQLITE_PREPARE_STMT( MOD_WEBDAV_SQLITE_LOCKS_READ_URI_MEMBERS,
1304                                     sql->stmt_locks_read_uri_members);
1305     MOD_WEBDAV_SQLITE_PREPARE_STMT( MOD_WEBDAV_SQLITE_LOCKS_DELETE_URI,
1306                                     sql->stmt_locks_delete_uri);
1307     MOD_WEBDAV_SQLITE_PREPARE_STMT( MOD_WEBDAV_SQLITE_LOCKS_DELETE_URI_COL,
1308                                     sql->stmt_locks_delete_uri_col);
1309 
1310     return 1;
1311 
1312 }
1313 #endif /* USE_PROPPATCH */
1314 
1315 
1316 __attribute_cold__
1317 SERVER_FUNC(mod_webdav_worker_init)
1318 {
1319   #ifdef USE_PROPPATCH
1320     /* open sqlite databases and prepare SQL statements in each worker process
1321      *
1322      * https://www.sqlite.org/faq.html
1323      *   Under Unix, you should not carry an open SQLite database
1324      *   across a fork() system call into the child process.
1325      */
1326     plugin_data * const p = (plugin_data *)p_d;
1327     /* (init i to 0 if global context; to 1 to skip empty global context) */
1328     for (int i = !p->cvlist[0].v.u2[1], used = p->nconfig; i < used; ++i) {
1329         config_plugin_value_t *cpv = p->cvlist + p->cvlist[i].v.u2[0];
1330         for (; -1 != cpv->k_id; ++cpv) {
1331             switch (cpv->k_id) {
1332              #ifdef USE_PROPPATCH
1333               case 0: /* webdav.sqlite-db-name */
1334                 if (!buffer_is_empty(cpv->v.b)) {
1335                     const char * const dbname = cpv->v.b->ptr;
1336                     cpv->v.v = calloc(1, sizeof(sql_config));
1337                     cpv->vtype = T_CONFIG_LOCAL;
1338                     if (!mod_webdav_sqlite3_prep(cpv->v.v, dbname, srv->errh))
1339                         return HANDLER_ERROR;
1340                     /*(update p->defaults after init)*/
1341                     if (0 == i) p->defaults.sql = cpv->v.v;
1342                 }
1343                 break;
1344              #endif
1345               default:
1346                 break;
1347             }
1348         }
1349     }
1350   #else
1351     UNUSED(srv);
1352     UNUSED(p_d);
1353   #endif /* USE_PROPPATCH */
1354     return HANDLER_GO_ON;
1355 }
1356 
1357 
1358 #ifdef USE_PROPPATCH
1359 static int
1360 webdav_db_transaction (const plugin_config * const pconf,
1361                        const char * const action)
1362 {
1363     if (!pconf->sql)
1364         return 1;
1365     char *err = NULL;
1366     if (SQLITE_OK == sqlite3_exec(pconf->sql->sqlh, action, NULL, NULL, &err))
1367         return 1;
1368     else {
1369       #if 0
1370         fprintf(stderr, "%s: %s: %s\n", __func__, action, err);
1371         log_error(pconf->errh, __FILE__, __LINE__,
1372                   "%s: %s: %s\n", __func__, action, err);
1373       #endif
1374         sqlite3_free(err);
1375         return 0;
1376     }
1377 }
1378 
1379 #define webdav_db_transaction_begin(pconf) \
1380         webdav_db_transaction(pconf, "BEGIN;")
1381 
1382 #define webdav_db_transaction_begin_immediate(pconf) \
1383         webdav_db_transaction(pconf, "BEGIN IMMEDIATE;")
1384 
1385 #define webdav_db_transaction_commit(pconf) \
1386         webdav_db_transaction(pconf, "COMMIT;")
1387 
1388 #define webdav_db_transaction_rollback(pconf) \
1389         webdav_db_transaction(pconf, "ROLLBACK;")
1390 
1391 #else
1392 
1393 #define webdav_db_transaction_begin(pconf)            1
1394 #define webdav_db_transaction_begin_immediate(pconf)  1
1395 #define webdav_db_transaction_commit(pconf)           1
1396 #define webdav_db_transaction_rollback(pconf)         1
1397 
1398 #endif
1399 
1400 
1401 #ifdef USE_LOCKS
1402 static int
1403 webdav_lock_match (const plugin_config * const pconf,
1404                    const webdav_lockdata * const lockdata)
1405 {
1406     if (!pconf->sql)
1407         return 0;
1408     sqlite3_stmt * const stmt = pconf->sql->stmt_locks_read;
1409     if (!stmt)
1410         return 0;
1411 
1412     sqlite3_bind_text(
1413       stmt, 1, CONST_BUF_LEN(&lockdata->locktoken), SQLITE_STATIC);
1414 
1415     int status = -1; /* if lock does not exist */
1416     if (SQLITE_ROW == sqlite3_step(stmt)) {
1417         const char *text = (char *)sqlite3_column_text(stmt, 0); /* resource */
1418         uint32_t text_len = (uint32_t) sqlite3_column_bytes(stmt, 0);
1419         if (text_len < lockdata->lockroot.used
1420             && 0 == memcmp(lockdata->lockroot.ptr, text, text_len)
1421             && (text_len == lockdata->lockroot.used-1
1422                 || -1 == sqlite3_column_int(stmt, 2))) { /* depth */
1423             text = (char *)sqlite3_column_text(stmt, 1); /* owner */
1424             text_len = (uint32_t)sqlite3_column_bytes(stmt, 1);
1425             if (0 == text_len /*(if no auth required to lock; not recommended)*/
1426                 || buffer_is_equal_string(lockdata->owner, text, text_len))
1427                 status = 0; /* success; lock match */
1428             else {
1429                 /*(future: might check if owner is a privileged admin user)*/
1430                 status = -3; /* not lock owner; not authorized */
1431             }
1432         }
1433         else
1434             status = -2; /* URI is not in scope of lock */
1435     }
1436 
1437     sqlite3_reset(stmt);
1438 
1439     /* status
1440      *    0 lock exists and uri in scope and owner is privileged/owns lock
1441      *   -1 lock does not exist
1442      *   -2 URI is not in scope of lock
1443      *   -3 owner does not own lock/is not privileged
1444      */
1445     return status;
1446 }
1447 #endif
1448 
1449 
1450 #ifdef USE_LOCKS
1451 static void
1452 webdav_lock_activelocks_lockdata (sqlite3_stmt * const stmt,
1453                                   webdav_lockdata_wr * const lockdata)
1454 {
1455     lockdata->locktoken.ptr  = (char *)sqlite3_column_text(stmt, 0);
1456     lockdata->locktoken.used = sqlite3_column_bytes(stmt, 0);
1457     lockdata->lockroot.ptr   = (char *)sqlite3_column_text(stmt, 1);
1458     lockdata->lockroot.used  = sqlite3_column_bytes(stmt, 1);
1459     lockdata->lockscope      =
1460       (sqlite3_column_bytes(stmt, 2) == (int)sizeof("exclusive")-1)
1461         ? (const buffer *)&lockscope_exclusive
1462         : (const buffer *)&lockscope_shared;
1463     lockdata->locktype       = (const buffer *)&locktype_write;
1464     lockdata->owner->ptr     = (char *)sqlite3_column_text(stmt, 4);
1465     lockdata->owner->used    = sqlite3_column_bytes(stmt, 4);
1466     lockdata->ownerinfo.ptr  = (char *)sqlite3_column_text(stmt, 5);
1467     lockdata->ownerinfo.used = sqlite3_column_bytes(stmt, 5);
1468     lockdata->depth          = sqlite3_column_int(stmt, 6);
1469     lockdata->timeout        = sqlite3_column_int(stmt, 7);
1470 
1471     if (lockdata->locktoken.used) ++lockdata->locktoken.used;
1472     if (lockdata->lockroot.used)  ++lockdata->lockroot.used;
1473     if (lockdata->owner->used)    ++lockdata->owner->used;
1474     if (lockdata->ownerinfo.used) ++lockdata->ownerinfo.used;
1475 }
1476 
1477 
1478 typedef
1479   void webdav_lock_activelocks_cb(void * const vdata,
1480                                   const webdav_lockdata * const lockdata);
1481 
1482 static void
1483 webdav_lock_activelocks (const plugin_config * const pconf,
1484                          const buffer * const uri,
1485                          const int expand_checks,
1486                          webdav_lock_activelocks_cb * const lock_cb,
1487                          void * const vdata)
1488 {
1489     webdav_lockdata lockdata;
1490     buffer owner = { NULL, 0, 0 };
1491     lockdata.locktoken.size = 0;
1492     lockdata.lockroot.size  = 0;
1493     lockdata.ownerinfo.size = 0;
1494     lockdata.owner = &owner;
1495 
1496     if (!pconf->sql)
1497         return;
1498 
1499     /* check for locks with Depth: 0 (and Depth: infinity if 0==expand_checks)*/
1500     sqlite3_stmt *stmt = pconf->sql->stmt_locks_read_uri;
1501     if (!stmt || !pconf->sql->stmt_locks_read_uri_infinity
1502               || !pconf->sql->stmt_locks_read_uri_members)
1503         return;
1504 
1505     sqlite3_bind_text(stmt, 1, CONST_BUF_LEN(uri), SQLITE_STATIC);
1506 
1507     while (SQLITE_ROW == sqlite3_step(stmt)) {
1508         /* (avoid duplication with query below if infinity lock on collection)
1509          * (infinity locks are rejected on non-collections elsewhere) */
1510         if (0 != expand_checks && -1 == sqlite3_column_int(stmt, 6) /*depth*/)
1511             continue;
1512 
1513         webdav_lock_activelocks_lockdata(stmt, (webdav_lockdata_wr *)&lockdata);
1514         if (lockdata.timeout > 0)
1515             lock_cb(vdata, &lockdata);
1516     }
1517 
1518     sqlite3_reset(stmt);
1519 
1520     if (0 == expand_checks)
1521         return;
1522 
1523     /* check for locks with Depth: infinity
1524      * (i.e. collections: self (if collection) or containing collections) */
1525     stmt = pconf->sql->stmt_locks_read_uri_infinity;
1526 
1527     sqlite3_bind_text(stmt, 1, CONST_BUF_LEN(uri), SQLITE_STATIC);
1528 
1529     while (SQLITE_ROW == sqlite3_step(stmt)) {
1530         webdav_lock_activelocks_lockdata(stmt, (webdav_lockdata_wr *)&lockdata);
1531         if (lockdata.timeout > 0)
1532             lock_cb(vdata, &lockdata);
1533     }
1534 
1535     sqlite3_reset(stmt);
1536 
1537     if (1 == expand_checks)
1538         return;
1539 
1540   #ifdef __COVERITY__
1541     force_assert(0 != uri->used);
1542   #endif
1543 
1544     /* check for locks on members within (internal to) collection */
1545     stmt = pconf->sql->stmt_locks_read_uri_members;
1546 
1547     sqlite3_bind_int( stmt, 1, (int)uri->used-1);
1548     sqlite3_bind_text(stmt, 2, CONST_BUF_LEN(uri), SQLITE_STATIC);
1549 
1550     while (SQLITE_ROW == sqlite3_step(stmt)) {
1551         /* (avoid duplication with query above for exact resource match) */
1552         if (uri->used-1 == (uint32_t)sqlite3_column_bytes(stmt, 1) /*resource*/)
1553             continue;
1554 
1555         webdav_lock_activelocks_lockdata(stmt, (webdav_lockdata_wr *)&lockdata);
1556         if (lockdata.timeout > 0)
1557             lock_cb(vdata, &lockdata);
1558     }
1559 
1560     sqlite3_reset(stmt);
1561 }
1562 #endif
1563 
1564 
1565 static int
1566 webdav_lock_delete_uri (const plugin_config * const pconf,
1567                         const buffer * const uri)
1568 {
1569   #ifdef USE_LOCKS
1570 
1571     if (!pconf->sql)
1572         return 0;
1573     sqlite3_stmt * const stmt = pconf->sql->stmt_locks_delete_uri;
1574     if (!stmt)
1575         return 0;
1576 
1577     sqlite3_bind_text(stmt, 1, CONST_BUF_LEN(uri), SQLITE_STATIC);
1578 
1579     int status = 1;
1580     while (SQLITE_DONE != sqlite3_step(stmt)) {
1581         status = 0;
1582       #if 0
1583         fprintf(stderr, "%s: %s\n", __func__, sqlite3_errmsg(pconf->sql->sqlh));
1584         log_error(pconf->errh, __FILE__, __LINE__,
1585                   "%s: %s", __func__, sqlite3_errmsg(pconf->sql->sqlh));
1586       #endif
1587     }
1588 
1589     sqlite3_reset(stmt);
1590 
1591     return status;
1592 
1593   #else
1594     UNUSED(pconf);
1595     UNUSED(uri);
1596     return 1;
1597   #endif
1598 }
1599 
1600 
1601 static int
1602 webdav_lock_delete_uri_col (const plugin_config * const pconf,
1603                             const buffer * const uri)
1604 {
1605   #ifdef USE_LOCKS
1606 
1607     if (!pconf->sql)
1608         return 0;
1609     sqlite3_stmt * const stmt = pconf->sql->stmt_locks_delete_uri_col;
1610     if (!stmt)
1611         return 0;
1612 
1613   #ifdef __COVERITY__
1614     force_assert(0 != uri->used);
1615   #endif
1616 
1617     sqlite3_bind_int( stmt, 1, (int)uri->used-1);
1618     sqlite3_bind_text(stmt, 2, CONST_BUF_LEN(uri), SQLITE_STATIC);
1619 
1620     int status = 1;
1621     while (SQLITE_DONE != sqlite3_step(stmt)) {
1622         status = 0;
1623       #if 0
1624         fprintf(stderr, "%s: %s\n", __func__, sqlite3_errmsg(pconf->sql->sqlh));
1625         log_error(pconf->errh, __FILE__, __LINE__,
1626                   "%s: %s", __func__, sqlite3_errmsg(pconf->sql->sqlh));
1627       #endif
1628     }
1629 
1630     sqlite3_reset(stmt);
1631 
1632     return status;
1633 
1634   #else
1635     UNUSED(pconf);
1636     UNUSED(uri);
1637     return 1;
1638   #endif
1639 }
1640 
1641 
1642 #ifdef USE_LOCKS
1643 static int
1644 webdav_lock_acquire (const plugin_config * const pconf,
1645                      const webdav_lockdata * const lockdata)
1646 {
1647     /*
1648      * future:
1649      * only lockscope:"exclusive" and locktype:"write" currently supported,
1650      * so inserting strings into database is extraneous, and anyway should
1651      * be enums instead of strings, since there are limited supported values
1652      */
1653 
1654     if (!pconf->sql)
1655         return 0;
1656     sqlite3_stmt * const stmt = pconf->sql->stmt_locks_acquire;
1657     if (!stmt)
1658         return 0;
1659 
1660     sqlite3_bind_text(
1661       stmt, 1, CONST_BUF_LEN(&lockdata->locktoken),     SQLITE_STATIC);
1662     sqlite3_bind_text(
1663       stmt, 2, CONST_BUF_LEN(&lockdata->lockroot),      SQLITE_STATIC);
1664     sqlite3_bind_text(
1665       stmt, 3, CONST_BUF_LEN(lockdata->lockscope),      SQLITE_STATIC);
1666     sqlite3_bind_text(
1667       stmt, 4, CONST_BUF_LEN(lockdata->locktype),       SQLITE_STATIC);
1668     if (lockdata->owner->used)
1669         sqlite3_bind_text(
1670           stmt, 5, CONST_BUF_LEN(lockdata->owner),      SQLITE_STATIC);
1671     else
1672         sqlite3_bind_text(
1673           stmt, 5, CONST_STR_LEN(""),                   SQLITE_STATIC);
1674     if (lockdata->ownerinfo.used)
1675         sqlite3_bind_text(
1676           stmt, 6, CONST_BUF_LEN(&lockdata->ownerinfo), SQLITE_STATIC);
1677     else
1678         sqlite3_bind_text(
1679           stmt, 6, CONST_STR_LEN(""),                   SQLITE_STATIC);
1680     sqlite3_bind_int(
1681       stmt, 7, lockdata->depth);
1682     sqlite3_bind_int(
1683       stmt, 8, lockdata->timeout);
1684 
1685     int status = 1;
1686     if (SQLITE_DONE != sqlite3_step(stmt)) {
1687         status = 0;
1688       #if 0
1689         fprintf(stderr, "%s: %s\n", __func__, sqlite3_errmsg(pconf->sql->sqlh));
1690         log_error(pconf->errh, __FILE__, __LINE__,
1691                   "%s: %s", __func__, sqlite3_errmsg(pconf->sql->sqlh));
1692       #endif
1693     }
1694 
1695     sqlite3_reset(stmt);
1696 
1697     return status;
1698 }
1699 #endif
1700 
1701 
1702 #ifdef USE_LOCKS
1703 static int
1704 webdav_lock_refresh (const plugin_config * const pconf,
1705                      webdav_lockdata * const lockdata)
1706 {
1707     if (!pconf->sql)
1708         return 0;
1709     sqlite3_stmt * const stmt = pconf->sql->stmt_locks_refresh;
1710     if (!stmt)
1711         return 0;
1712 
1713     const buffer * const locktoken = &lockdata->locktoken;
1714     sqlite3_bind_text(stmt, 1, CONST_BUF_LEN(locktoken),  SQLITE_STATIC);
1715     sqlite3_bind_int( stmt, 2, lockdata->timeout);
1716 
1717     int status = 1;
1718     if (SQLITE_DONE != sqlite3_step(stmt)) {
1719         status = 0;
1720       #if 0
1721         fprintf(stderr, "%s: %s\n", __func__, sqlite3_errmsg(pconf->sql->sqlh));
1722         log_error(pconf->errh, __FILE__, __LINE__,
1723                   "%s: %s", __func__, sqlite3_errmsg(pconf->sql->sqlh));
1724       #endif
1725     }
1726 
1727     sqlite3_reset(stmt);
1728 
1729     /*(future: fill in lockscope, locktype, depth from database)*/
1730 
1731     return status;
1732 }
1733 #endif
1734 
1735 
1736 #ifdef USE_LOCKS
1737 static int
1738 webdav_lock_release (const plugin_config * const pconf,
1739                      const webdav_lockdata * const lockdata)
1740 {
1741     if (!pconf->sql)
1742         return 0;
1743     sqlite3_stmt * const stmt = pconf->sql->stmt_locks_release;
1744     if (!stmt)
1745         return 0;
1746 
1747     sqlite3_bind_text(
1748       stmt, 1, CONST_BUF_LEN(&lockdata->locktoken), SQLITE_STATIC);
1749 
1750     int status = 0;
1751     if (SQLITE_DONE == sqlite3_step(stmt))
1752         status = (0 != sqlite3_changes(pconf->sql->sqlh));
1753     else {
1754       #if 0
1755         fprintf(stderr, "%s: %s\n", __func__, sqlite3_errmsg(pconf->sql->sqlh));
1756         log_error(pconf->errh, __FILE__, __LINE__,
1757                   "%s: %s", __func__, sqlite3_errmsg(pconf->sql->sqlh));
1758       #endif
1759     }
1760 
1761     sqlite3_reset(stmt);
1762 
1763     return status;
1764 }
1765 #endif
1766 
1767 
1768 static int
1769 webdav_prop_move_uri (const plugin_config * const pconf,
1770                       const buffer * const src,
1771                       const buffer * const dst)
1772 {
1773   #ifdef USE_PROPPATCH
1774     if (!pconf->sql)
1775         return 0;
1776     sqlite3_stmt * const stmt = pconf->sql->stmt_props_move;
1777     if (!stmt)
1778         return 0;
1779 
1780     sqlite3_bind_text(stmt, 1, CONST_BUF_LEN(dst), SQLITE_STATIC);
1781     sqlite3_bind_text(stmt, 2, CONST_BUF_LEN(src), SQLITE_STATIC);
1782 
1783     if (SQLITE_DONE != sqlite3_step(stmt)) {
1784       #if 0
1785         fprintf(stderr, "%s: %s\n", __func__, sqlite3_errmsg(pconf->sql->sqlh));
1786         log_error(pconf->errh, __FILE__, __LINE__,
1787                   "%s: %s", __func__, sqlite3_errmsg(pconf->sql->sqlh));
1788       #endif
1789     }
1790 
1791     sqlite3_reset(stmt);
1792 
1793   #else
1794     UNUSED(pconf);
1795     UNUSED(src);
1796     UNUSED(dst);
1797   #endif
1798 
1799     return 0;
1800 }
1801 
1802 
1803 static int
1804 webdav_prop_move_uri_col (const plugin_config * const pconf,
1805                           const buffer * const src,
1806                           const buffer * const dst)
1807 {
1808   #ifdef USE_PROPPATCH
1809     if (!pconf->sql)
1810         return 0;
1811     sqlite3_stmt * const stmt = pconf->sql->stmt_props_move_col;
1812     if (!stmt)
1813         return 0;
1814 
1815   #ifdef __COVERITY__
1816     force_assert(0 != src->used);
1817   #endif
1818 
1819     sqlite3_bind_text(stmt, 1, CONST_BUF_LEN(dst), SQLITE_STATIC);
1820     sqlite3_bind_int( stmt, 2, (int)src->used);
1821     sqlite3_bind_int( stmt, 3, (int)src->used-1);
1822     sqlite3_bind_text(stmt, 4, CONST_BUF_LEN(src), SQLITE_STATIC);
1823 
1824     if (SQLITE_DONE != sqlite3_step(stmt)) {
1825       #if 0
1826         fprintf(stderr, "%s: %s\n", __func__, sqlite3_errmsg(pconf->sql->sqlh));
1827         log_error(pconf->errh, __FILE__, __LINE__,
1828                   "%s: %s", __func__, sqlite3_errmsg(pconf->sql->sqlh));
1829       #endif
1830     }
1831 
1832     sqlite3_reset(stmt);
1833 
1834   #else
1835     UNUSED(pconf);
1836     UNUSED(src);
1837     UNUSED(dst);
1838   #endif
1839 
1840     return 0;
1841 }
1842 
1843 
1844 static int
1845 webdav_prop_delete_uri (const plugin_config * const pconf,
1846                         const buffer * const uri)
1847 {
1848   #ifdef USE_PROPPATCH
1849     if (!pconf->sql)
1850         return 0;
1851     sqlite3_stmt * const stmt = pconf->sql->stmt_props_delete;
1852     if (!stmt)
1853         return 0;
1854 
1855     sqlite3_bind_text(stmt, 1, CONST_BUF_LEN(uri), SQLITE_STATIC);
1856 
1857     if (SQLITE_DONE != sqlite3_step(stmt)) {
1858       #if 0
1859         fprintf(stderr, "%s: %s\n", __func__, sqlite3_errmsg(pconf->sql->sqlh));
1860         log_error(pconf->errh, __FILE__, __LINE__,
1861                   "%s: %s", __func__, sqlite3_errmsg(pconf->sql->sqlh));
1862       #endif
1863     }
1864 
1865     sqlite3_reset(stmt);
1866 
1867   #else
1868     UNUSED(pconf);
1869     UNUSED(uri);
1870   #endif
1871 
1872     return 0;
1873 }
1874 
1875 
1876 static int
1877 webdav_prop_copy_uri (const plugin_config * const pconf,
1878                       const buffer * const src,
1879                       const buffer * const dst)
1880 {
1881   #ifdef USE_PROPPATCH
1882     if (!pconf->sql)
1883         return 0;
1884     sqlite3_stmt * const stmt = pconf->sql->stmt_props_copy;
1885     if (!stmt)
1886         return 0;
1887 
1888     sqlite3_bind_text(stmt, 1, CONST_BUF_LEN(dst), SQLITE_STATIC);
1889     sqlite3_bind_text(stmt, 2, CONST_BUF_LEN(src), SQLITE_STATIC);
1890 
1891     if (SQLITE_DONE != sqlite3_step(stmt)) {
1892       #if 0
1893         fprintf(stderr, "%s: %s\n", __func__, sqlite3_errmsg(pconf->sql->sqlh));
1894         log_error(pconf->errh, __FILE__, __LINE__,
1895                   "%s: %s", __func__, sqlite3_errmsg(pconf->sql->sqlh));
1896       #endif
1897     }
1898 
1899     sqlite3_reset(stmt);
1900 
1901   #else
1902     UNUSED(pconf);
1903     UNUSED(dst);
1904     UNUSED(src);
1905   #endif
1906 
1907     return 0;
1908 }
1909 
1910 
1911 #ifdef USE_PROPPATCH
1912 static int
1913 webdav_prop_delete (const plugin_config * const pconf,
1914                     const buffer * const uri,
1915                     const char * const prop_name,
1916                     const char * const prop_ns)
1917 {
1918     if (!pconf->sql)
1919         return 0;
1920     sqlite3_stmt * const stmt = pconf->sql->stmt_props_delete_prop;
1921     if (!stmt)
1922         return 0;
1923 
1924     sqlite3_bind_text(stmt, 1, CONST_BUF_LEN(uri),           SQLITE_STATIC);
1925     sqlite3_bind_text(stmt, 2, prop_name, strlen(prop_name), SQLITE_STATIC);
1926     sqlite3_bind_text(stmt, 3, prop_ns,   strlen(prop_ns),   SQLITE_STATIC);
1927 
1928     if (SQLITE_DONE != sqlite3_step(stmt)) {
1929       #if 0
1930         fprintf(stderr, "%s: %s\n", __func__, sqlite3_errmsg(pconf->sql->sqlh));
1931         log_error(pconf->errh, __FILE__, __LINE__,
1932                   "%s: %s", __func__, sqlite3_errmsg(pconf->sql->sqlh));
1933       #endif
1934     }
1935 
1936     sqlite3_reset(stmt);
1937 
1938     return 0;
1939 }
1940 #endif
1941 
1942 
1943 #ifdef USE_PROPPATCH
1944 static int
1945 webdav_prop_update (const plugin_config * const pconf,
1946                     const buffer * const uri,
1947                     const char * const prop_name,
1948                     const char * const prop_ns,
1949                     const char * const prop_value)
1950 {
1951     if (!pconf->sql)
1952         return 0;
1953     sqlite3_stmt * const stmt = pconf->sql->stmt_props_update_prop;
1954     if (!stmt)
1955         return 0;
1956 
1957     sqlite3_bind_text(stmt, 1, CONST_BUF_LEN(uri),             SQLITE_STATIC);
1958     sqlite3_bind_text(stmt, 2, prop_name,  strlen(prop_name),  SQLITE_STATIC);
1959     sqlite3_bind_text(stmt, 3, prop_ns,    strlen(prop_ns),    SQLITE_STATIC);
1960     sqlite3_bind_text(stmt, 4, prop_value, strlen(prop_value), SQLITE_STATIC);
1961 
1962     if (SQLITE_DONE != sqlite3_step(stmt)) {
1963       #if 0
1964         fprintf(stderr, "%s: %s\n", __func__, sqlite3_errmsg(pconf->sql->sqlh));
1965         log_error(pconf->errh, __FILE__, __LINE__,
1966                   "%s: %s", __func__, sqlite3_errmsg(pconf->sql->sqlh));
1967       #endif
1968     }
1969 
1970     sqlite3_reset(stmt);
1971 
1972     return 0;
1973 }
1974 #endif
1975 
1976 
1977 static int
1978 webdav_prop_select_prop (const plugin_config * const pconf,
1979                          const buffer * const uri,
1980                          const webdav_property_name * const prop,
1981                          buffer * const b)
1982 {
1983   #ifdef USE_PROPPATCH
1984     if (!pconf->sql)
1985         return -1;
1986     sqlite3_stmt * const stmt = pconf->sql->stmt_props_select_prop;
1987     if (!stmt)
1988         return -1; /* not found */
1989 
1990     sqlite3_bind_text(stmt, 1, CONST_BUF_LEN(uri),        SQLITE_STATIC);
1991     sqlite3_bind_text(stmt, 2, prop->name, prop->namelen, SQLITE_STATIC);
1992     sqlite3_bind_text(stmt, 3, prop->ns,   prop->nslen,   SQLITE_STATIC);
1993 
1994     if (SQLITE_ROW == sqlite3_step(stmt)) {
1995         webdav_xml_prop(b, prop, (char *)sqlite3_column_text(stmt, 0),
1996                                  (uint32_t)sqlite3_column_bytes(stmt, 0));
1997         sqlite3_reset(stmt);
1998         return 0; /* found */
1999     }
2000     sqlite3_reset(stmt);
2001   #else
2002     UNUSED(pconf);
2003     UNUSED(uri);
2004     UNUSED(prop);
2005     UNUSED(b);
2006   #endif
2007     return -1; /* not found */
2008 }
2009 
2010 
2011 static void
2012 webdav_prop_select_props (const plugin_config * const pconf,
2013                           const buffer * const uri,
2014                           buffer * const b)
2015 {
2016   #ifdef USE_PROPPATCH
2017     if (!pconf->sql)
2018         return;
2019     sqlite3_stmt * const stmt = pconf->sql->stmt_props_select_props;
2020     if (!stmt)
2021         return;
2022 
2023     sqlite3_bind_text(stmt, 1, CONST_BUF_LEN(uri), SQLITE_STATIC);
2024 
2025     while (SQLITE_ROW == sqlite3_step(stmt)) {
2026         webdav_property_name prop;
2027         prop.ns      = (char *)sqlite3_column_text(stmt, 1);
2028         prop.name    = (char *)sqlite3_column_text(stmt, 0);
2029         prop.nslen   = (uint32_t)sqlite3_column_bytes(stmt, 1);
2030         prop.namelen = (uint32_t)sqlite3_column_bytes(stmt, 0);
2031         webdav_xml_prop(b, &prop, (char *)sqlite3_column_text(stmt, 2),
2032                                   (uint32_t)sqlite3_column_bytes(stmt, 2));
2033     }
2034 
2035     sqlite3_reset(stmt);
2036   #else
2037     UNUSED(pconf);
2038     UNUSED(uri);
2039     UNUSED(b);
2040   #endif
2041 }
2042 
2043 
2044 static int
2045 webdav_prop_select_propnames (const plugin_config * const pconf,
2046                               const buffer * const uri,
2047                               buffer * const b)
2048 {
2049   #ifdef USE_PROPPATCH
2050     if (!pconf->sql)
2051         return 0;
2052     sqlite3_stmt * const stmt = pconf->sql->stmt_props_select_propnames;
2053     if (!stmt)
2054         return 0;
2055 
2056     /* get all property names (EMPTY) */
2057     sqlite3_bind_text(stmt, 1, CONST_BUF_LEN(uri), SQLITE_STATIC);
2058 
2059     while (SQLITE_ROW == sqlite3_step(stmt)) {
2060         webdav_property_name prop;
2061         prop.ns      = (char *)sqlite3_column_text(stmt, 1);
2062         prop.name    = (char *)sqlite3_column_text(stmt, 0);
2063         prop.nslen   = (uint32_t)sqlite3_column_bytes(stmt, 1);
2064         prop.namelen = (uint32_t)sqlite3_column_bytes(stmt, 0);
2065         webdav_xml_prop(b, &prop, NULL, 0);
2066     }
2067 
2068     sqlite3_reset(stmt);
2069 
2070   #else
2071     UNUSED(pconf);
2072     UNUSED(uri);
2073     UNUSED(b);
2074   #endif
2075 
2076     return 0;
2077 }
2078 
2079 
2080 #if defined(__APPLE__) && defined(__MACH__)
2081 #if __ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ >= 1050
2082 #include <copyfile.h>     /* fcopyfile() *//* OS X 10.5+ */
2083 #endif
2084 #endif
2085 #ifdef HAVE_ELFTC_COPYFILE/* __FreeBSD__ */
2086 #include <libelftc.h>     /* elftc_copyfile() */
2087 #endif
2088 #ifdef __linux__
2089 #include <sys/sendfile.h> /* sendfile() */
2090 #endif
2091 
2092 /* file copy (blocking)
2093  * fds should point to regular files (S_ISREG()) (not dir, symlink, or other)
2094  * fds should not have O_NONBLOCK flag set
2095  *   (unless O_NONBLOCK not relevant for files on a given operating system)
2096  * isz should be size of input file, and is a param to avoid extra fstat()
2097  *   since size is needed for Linux sendfile(), as well as posix_fadvise().
2098  * caller should handler fchmod() and copying extended attribute, if desired
2099  */
2100 __attribute_noinline__
2101 static int
2102 webdav_fcopyfile_sz (int ifd, int ofd, off_t isz)
2103 {
2104     if (0 == isz)
2105         return 0;
2106 
2107   #ifdef _WIN32
2108     /* Windows CopyFile() not usable here; operates on filenames, not fds */
2109   #else
2110     /*(file descriptors to *regular files* on most OS ignore O_NONBLOCK)*/
2111     /*fcntl(ifd, F_SETFL, fcntl(ifd, F_GETFL, 0) & ~O_NONBLOCK);*/
2112     /*fcntl(ofd, F_SETFL, fcntl(ofd, F_GETFL, 0) & ~O_NONBLOCK);*/
2113   #endif
2114 
2115   #if defined(__APPLE__) && defined(__MACH__)
2116   #if __ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ >= 1050
2117     if (0 == fcopyfile(ifd, ofd, NULL, COPYFILE_ALL))
2118         return 0;
2119 
2120     if (0 != lseek(ifd, 0, SEEK_SET)) return -1;
2121     if (0 != lseek(ofd, 0, SEEK_SET)) return -1;
2122   #endif
2123   #endif
2124 
2125   #ifdef HAVE_ELFTC_COPYFILE /* __FreeBSD__ */
2126     if (0 == elftc_copyfile(ifd, ofd))
2127         return 0;
2128 
2129     if (0 != lseek(ifd, 0, SEEK_SET)) return -1;
2130     if (0 != lseek(ofd, 0, SEEK_SET)) return -1;
2131   #endif
2132 
2133   #ifdef __linux__ /* Linux 2.6.33+ sendfile() supports file-to-file copy */
2134     off_t offset = 0;
2135     while (offset < isz && sendfile(ifd,ofd,&offset,(size_t)(isz-offset)) >= 0);
2136     if (offset == isz)
2137         return 0;
2138 
2139     /*lseek(ifd, 0, SEEK_SET);*/ /*(ifd offset not modified due to &offset arg)*/
2140     if (0 != lseek(ofd, 0, SEEK_SET)) return -1;
2141   #endif
2142 
2143     ssize_t rd, wr, off;
2144     char buf[16384];
2145     do {
2146         do {
2147             rd = read(ifd, buf, sizeof(buf));
2148         } while (-1 == rd && errno == EINTR);
2149         if (rd < 0) return rd;
2150 
2151         off = 0;
2152         do {
2153             wr = write(ofd, buf+off, (size_t)(rd-off));
2154         } while (wr >= 0 ? (off += wr) != rd : errno == EINTR);
2155         if (wr < 0) return -1;
2156     } while (rd > 0);
2157     return rd;
2158 }
2159 
2160 
2161 static int
2162 webdav_if_match_or_unmodified_since (request_st * const r, struct stat *st)
2163 {
2164     const buffer *im = (0 != r->conf.etag_flags)
2165       ? http_header_request_get(r, HTTP_HEADER_IF_MATCH,
2166                                 CONST_STR_LEN("If-Match"))
2167       : NULL;
2168 
2169     const buffer *inm = (0 != r->conf.etag_flags)
2170       ? http_header_request_get(r, HTTP_HEADER_IF_NONE_MATCH,
2171                                 CONST_STR_LEN("If-None-Match"))
2172       : NULL;
2173 
2174     const buffer *ius =
2175       http_header_request_get(r, HTTP_HEADER_IF_UNMODIFIED_SINCE,
2176                               CONST_STR_LEN("If-Unmodified-Since"));
2177 
2178     if (NULL == im && NULL == inm && NULL == ius) return 0;
2179 
2180     struct stat stp;
2181     if (NULL == st)
2182         st = (0 == lstat(r->physical.path.ptr, &stp)) ? &stp : NULL;
2183 
2184     buffer *etagb = &r->physical.etag;
2185     if (NULL != st && (NULL != im || NULL != inm)) {
2186         etag_create(etagb, st, r->conf.etag_flags);
2187         etag_mutate(etagb, etagb);
2188     }
2189 
2190     if (NULL != im) {
2191         if (NULL == st || !etag_is_equal(etagb, im->ptr, 0))
2192             return 412; /* Precondition Failed */
2193     }
2194 
2195     if (NULL != inm) {
2196         if (NULL == st
2197             ? !buffer_is_equal_string(inm,CONST_STR_LEN("*"))
2198               || (errno != ENOENT && errno != ENOTDIR)
2199             : etag_is_equal(etagb, inm->ptr, 1))
2200             return 412; /* Precondition Failed */
2201     }
2202 
2203     if (NULL != ius) {
2204         if (NULL == st)
2205             return 412; /* Precondition Failed */
2206         struct tm itm, *ftm = gmtime(&st->st_mtime);
2207         if (NULL == strptime(ius->ptr, "%a, %d %b %Y %H:%M:%S GMT", &itm)
2208             || mktime(ftm) > mktime(&itm)) { /* timegm() not standard */
2209             return 412; /* Precondition Failed */
2210         }
2211     }
2212 
2213     return 0;
2214 }
2215 
2216 
2217 static void
2218 webdav_response_etag (request_st * const r, struct stat *st)
2219 {
2220     if (0 != r->conf.etag_flags) {
2221         buffer *etagb = &r->physical.etag;
2222         etag_create(etagb, st, r->conf.etag_flags);
2223         stat_cache_update_entry(CONST_BUF_LEN(&r->physical.path), st, etagb);
2224         etag_mutate(etagb, etagb);
2225         http_header_response_set(r, HTTP_HEADER_ETAG,
2226                                  CONST_STR_LEN("ETag"),
2227                                  CONST_BUF_LEN(etagb));
2228     }
2229     else {
2230         stat_cache_update_entry(CONST_BUF_LEN(&r->physical.path), st, NULL);
2231     }
2232 }
2233 
2234 
2235 static void
2236 webdav_parent_modified (const buffer *path)
2237 {
2238     uint32_t dirlen = buffer_string_length(path);
2239     const char *fn = path->ptr;
2240     /*force_assert(0 != dirlen);*/
2241     /*force_assert(fn[0] == '/');*/
2242     if (fn[dirlen-1] == '/') --dirlen;
2243     if (0 != dirlen) while (fn[--dirlen] != '/') ;
2244     if (0 == dirlen) dirlen = 1; /* root dir ("/") */
2245     stat_cache_invalidate_entry(fn, dirlen);
2246 }
2247 
2248 
2249 __attribute_pure__
2250 static int
2251 webdav_parse_Depth (const request_st * const r)
2252 {
2253     /* Depth = "Depth" ":" ("0" | "1" | "infinity") */
2254     const buffer * const h =
2255       http_header_request_get(r, HTTP_HEADER_OTHER, CONST_STR_LEN("Depth"));
2256     if (NULL != h) {
2257         /* (leading LWS is removed during header parsing in request.c) */
2258         switch (*h->ptr) {
2259           case  '0': return 0;
2260           case  '1': return 1;
2261           /*case 'i':*/ /* e.g. "infinity" */
2262           /*case 'I':*/ /* e.g. "Infinity" */
2263           default:   return -1;/* treat not-'0' and not-'1' as "infinity" */
2264         }
2265     }
2266 
2267     return -1; /* default value is -1 to represent "infinity" */
2268 }
2269 
2270 
2271 static int
2272 webdav_unlinkat (const plugin_config * const pconf, const buffer * const uri,
2273                  const int dfd, const char * const d_name, uint32_t len)
2274 {
2275     if (0 == unlinkat(dfd, d_name, 0)) {
2276         stat_cache_delete_entry(d_name, len);
2277         return webdav_prop_delete_uri(pconf, uri);
2278     }
2279 
2280     switch(errno) {
2281       case EACCES: case EPERM: return 403; /* Forbidden */
2282       case ENOENT:             return 404; /* Not Found */
2283       default:                 return 501; /* Not Implemented */
2284     }
2285 }
2286 
2287 
2288 static int
2289 webdav_delete_file (const plugin_config * const pconf,
2290                     const physical_st * const dst)
2291 {
2292     if (0 == unlink(dst->path.ptr)) {
2293         stat_cache_delete_entry(CONST_BUF_LEN(&dst->path));
2294         return webdav_prop_delete_uri(pconf, &dst->rel_path);
2295     }
2296 
2297     switch(errno) {
2298       case EACCES: case EPERM: return 403; /* Forbidden */
2299       case ENOENT:             return 404; /* Not Found */
2300       default:                 return 501; /* Not Implemented */
2301     }
2302 }
2303 
2304 
2305 static int
2306 webdav_delete_dir (const plugin_config * const pconf,
2307                    physical_st * const dst,
2308                    buffer * const b,
2309                    const int flags)
2310 {
2311     int multi_status = 0;
2312     const int dfd = fdevent_open_dirname(dst->path.ptr, 0);
2313     DIR * const dir = (dfd >= 0) ? fdopendir(dfd) : NULL;
2314     if (NULL == dir) {
2315         if (dfd >= 0) close(dfd);
2316         webdav_xml_response_status(b, &dst->rel_path, 403);
2317         return 1;
2318     }
2319 
2320     /* dst is modified in place to extend path,
2321      * so be sure to restore to base each loop iter */
2322     const uint32_t dst_path_used     = dst->path.used;
2323     const uint32_t dst_rel_path_used = dst->rel_path.used;
2324     int s_isdir;
2325     struct dirent *de;
2326     while (NULL != (de = readdir(dir))) {
2327         if (de->d_name[0] == '.'
2328             && (de->d_name[1] == '\0'
2329                 || (de->d_name[1] == '.' && de->d_name[2] == '\0')))
2330             continue; /* ignore "." and ".." */
2331 
2332       #ifdef _DIRENT_HAVE_D_TYPE
2333         if (de->d_type != DT_UNKNOWN)
2334             s_isdir = (de->d_type == DT_DIR);
2335         else
2336       #endif
2337         {
2338             struct stat st;
2339             if (0 != fstatat(dfd, de->d_name, &st, AT_SYMLINK_NOFOLLOW))
2340                 continue; /* file *just* disappeared? */
2341                 /* parent rmdir() will fail later if file still exists
2342                  * and fstatat() failed for other reasons */
2343             s_isdir = S_ISDIR(st.st_mode);
2344         }
2345 
2346         const uint32_t len = (uint32_t) _D_EXACT_NAMLEN(de);
2347         if (flags & WEBDAV_FLAG_LC_NAMES) /*(needed at least for rel_path)*/
2348             webdav_str_len_to_lower(de->d_name, len);
2349         buffer_append_string_len(&dst->path, de->d_name, len);
2350         buffer_append_string_len(&dst->rel_path, de->d_name, len);
2351 
2352         if (s_isdir) {
2353             buffer_append_string_len(&dst->path, CONST_STR_LEN("/"));
2354             buffer_append_string_len(&dst->rel_path, CONST_STR_LEN("/"));
2355             multi_status |= webdav_delete_dir(pconf, dst, b, flags);
2356         }
2357         else {
2358             int status =
2359               webdav_unlinkat(pconf, &dst->rel_path, dfd, de->d_name, len);
2360             if (0 != status) {
2361                 webdav_xml_response_status(b, &dst->rel_path, status);
2362                 multi_status = 1;
2363             }
2364         }
2365 
2366         dst->path.ptr[    (dst->path.used     = dst_path_used)    -1] = '\0';
2367         dst->rel_path.ptr[(dst->rel_path.used = dst_rel_path_used)-1] = '\0';
2368     }
2369     closedir(dir);
2370 
2371     if (0 == multi_status) {
2372         int rmdir_status;
2373         if (0 == rmdir(dst->path.ptr))
2374             rmdir_status = webdav_prop_delete_uri(pconf, &dst->rel_path);
2375         else {
2376             switch(errno) {
2377               case EACCES:
2378               case EPERM:  rmdir_status = 403; break; /* Forbidden */
2379               case ENOENT: rmdir_status = 404; break; /* Not Found */
2380               default:     rmdir_status = 501; break; /* Not Implemented */
2381             }
2382         }
2383         if (0 != rmdir_status) {
2384             webdav_xml_response_status(b, &dst->rel_path, rmdir_status);
2385             multi_status = 1;
2386         }
2387     }
2388 
2389     return multi_status;
2390 }
2391 
2392 
2393 static int
2394 webdav_linktmp_rename (const plugin_config * const pconf,
2395                        const buffer * const src,
2396                        const buffer * const dst)
2397 {
2398     buffer * const tmpb = pconf->tmpb;
2399     int rc = -1; /*(not zero)*/
2400 
2401     buffer_copy_buffer(tmpb, dst);
2402     buffer_append_string_len(tmpb, CONST_STR_LEN("."));
2403     buffer_append_int(tmpb, (long)getpid());
2404     buffer_append_string_len(tmpb, CONST_STR_LEN("."));
2405     buffer_append_uint_hex_lc(tmpb, (uintptr_t)pconf); /*(stack/heap addr)*/
2406     buffer_append_string_len(tmpb, CONST_STR_LEN("~"));
2407     if (buffer_string_length(tmpb) < PATH_MAX
2408         && 0 == linkat(AT_FDCWD, src->ptr, AT_FDCWD, tmpb->ptr, 0)) {
2409 
2410         rc = rename(tmpb->ptr, dst->ptr);
2411 
2412         /* unconditionally unlink() src if rename() succeeds, just in case
2413          * dst previously existed and was already hard-linked to src.  From
2414          * 'man -s 2 rename':
2415          *   If oldpath and newpath are existing hard links referring to the
2416          *   same file, then rename() does nothing, and returns a success
2417          *   status.
2418          * This introduces a small race condition between the rename() and
2419          * unlink() should new file have been created at src in the middle,
2420          * though unlikely if locks are used since locks have not yet been
2421          * released. */
2422         unlink(tmpb->ptr);
2423     }
2424     return rc;
2425 }
2426 
2427 
2428 static int
2429 webdav_copytmp_rename (const plugin_config * const pconf,
2430                        const physical_st * const src,
2431                        const physical_st * const dst,
2432                        const int overwrite)
2433 {
2434     buffer * const tmpb = pconf->tmpb;
2435     buffer_copy_buffer(tmpb, &dst->path);
2436     buffer_append_string_len(tmpb, CONST_STR_LEN("."));
2437     buffer_append_int(tmpb, (long)getpid());
2438     buffer_append_string_len(tmpb, CONST_STR_LEN("."));
2439     buffer_append_uint_hex_lc(tmpb, (uintptr_t)pconf); /*(stack/heap addr)*/
2440     buffer_append_string_len(tmpb, CONST_STR_LEN("~"));
2441     if (buffer_string_length(tmpb) >= PATH_MAX)
2442         return 414; /* URI Too Long */
2443 
2444     /* code does not currently support symlinks in webdav collections;
2445      * disallow symlinks as target when opening src and dst */
2446     struct stat st;
2447     const int ifd = fdevent_open_cloexec(src->path.ptr, 0, O_RDONLY, 0);
2448     if (ifd < 0)
2449         return 403; /* Forbidden */
2450     if (0 != fstat(ifd, &st) || !S_ISREG(st.st_mode)) {
2451         close(ifd);
2452         return 403; /* Forbidden */
2453     }
2454     const int ofd = fdevent_open_cloexec(tmpb->ptr, 0,
2455                                          O_WRONLY | O_CREAT | O_EXCL | O_TRUNC,
2456                                          WEBDAV_FILE_MODE);
2457     if (ofd < 0) {
2458         close(ifd);
2459         return 403; /* Forbidden */
2460     }
2461 
2462     /* perform *blocking* copy (not O_NONBLOCK);
2463      * blocks server from doing any other work until after copy completes
2464      * (should reach here only if unable to use link() and rename()
2465      *  due to copy/move crossing device boundaries within the workspace) */
2466     int rc = webdav_fcopyfile_sz(ifd, ofd, st.st_size);
2467 
2468     close(ifd);
2469     const int wc = close(ofd);
2470 
2471     if (0 != rc || 0 != wc) {
2472         /* error reading or writing files */
2473         rc = (0 != wc && wc == ENOSPC) ? 507 : 403;
2474         unlink(tmpb->ptr);
2475         return rc;
2476     }
2477 
2478   #ifndef RENAME_NOREPLACE /*(renameat2() not well-supported yet)*/
2479     if (!overwrite) {
2480         struct stat stb;
2481         if (0 == lstat(dst->path.ptr, &stb) || errno != ENOENT)
2482             return 412; /* Precondition Failed */
2483         /* TOC-TOU race between lstat() and rename(),
2484          * but this is reasonable attempt to not overwrite existing entity */
2485     }
2486     if (0 == rename(tmpb->ptr, dst->path.ptr))
2487   #else
2488     if (0 == renameat2(AT_FDCWD, tmpb->ptr,
2489                        AT_FDCWD, dst->path.ptr,
2490                        overwrite ? 0 : RENAME_NOREPLACE))
2491   #endif
2492     {
2493         /* unconditional stat cache deletion
2494          * (not worth extra syscall/race to detect overwritten or not) */
2495         stat_cache_delete_entry(CONST_BUF_LEN(&dst->path));
2496         return 0;
2497     }
2498     else {
2499         const int errnum = errno;
2500         unlink(tmpb->ptr);
2501         switch (errnum) {
2502           case ENOENT:
2503           case ENOTDIR:
2504           case EISDIR: return 409; /* Conflict */
2505           case EEXIST: return 412; /* Precondition Failed */
2506           default:     return 403; /* Forbidden */
2507         }
2508     }
2509 }
2510 
2511 
2512 static int
2513 webdav_copymove_file (const plugin_config * const pconf,
2514                       const physical_st * const src,
2515                       const physical_st * const dst,
2516                       int * const flags)
2517 {
2518     const int overwrite = (*flags & WEBDAV_FLAG_OVERWRITE);
2519     if (*flags & WEBDAV_FLAG_MOVE_RENAME) {
2520       #ifndef RENAME_NOREPLACE /*(renameat2() not well-supported yet)*/
2521         if (!overwrite) {
2522             struct stat st;
2523             if (0 == lstat(dst->path.ptr, &st) || errno != ENOENT)
2524                 return 412; /* Precondition Failed */
2525             /* TOC-TOU race between lstat() and rename(),
2526              * but this is reasonable attempt to not overwrite existing entity*/
2527         }
2528         if (0 == rename(src->path.ptr, dst->path.ptr))
2529       #else
2530         if (0 == renameat2(AT_FDCWD, src->path.ptr,
2531                            AT_FDCWD, dst->path.ptr,
2532                            overwrite ? 0 : RENAME_NOREPLACE))
2533       #endif
2534         {
2535             /* unconditionally unlink() src if rename() succeeds, just in case
2536              * dst previously existed and was already hard-linked to src.  From
2537              * 'man -s 2 rename':
2538              *   If oldpath and newpath are existing hard links referring to the
2539              *   same file, then rename() does nothing, and returns a success
2540              *   status.
2541              * This introduces a small race condition between the rename() and
2542              * unlink() should new file have been created at src in the middle,
2543              * though unlikely if locks are used since locks have not yet been
2544              * released. */
2545             if (overwrite) unlink(src->path.ptr);
2546             /* unconditional stat cache deletion
2547              * (not worth extra syscall/race to detect overwritten or not) */
2548             stat_cache_delete_entry(CONST_BUF_LEN(&dst->path));
2549             stat_cache_delete_entry(CONST_BUF_LEN(&src->path));
2550             webdav_prop_move_uri(pconf, &src->rel_path, &dst->rel_path);
2551             return 0;
2552         }
2553         else if (errno == EEXIST)
2554             return 412; /* Precondition Failed */
2555     }
2556     else if (*flags & WEBDAV_FLAG_COPY_LINK) {
2557         if (0 == linkat(AT_FDCWD, src->path.ptr, AT_FDCWD, dst->path.ptr, 0)){
2558             webdav_prop_copy_uri(pconf, &src->rel_path, &dst->rel_path);
2559             return 0;
2560         }
2561         else if (errno == EEXIST) {
2562             if (!overwrite)
2563                 return 412; /* Precondition Failed */
2564             if (0 == webdav_linktmp_rename(pconf, &src->path, &dst->path)) {
2565                 webdav_prop_copy_uri(pconf, &src->rel_path, &dst->rel_path);
2566                 return 0;
2567             }
2568         }
2569         else if (errno == EXDEV) {
2570             *flags &= ~WEBDAV_FLAG_COPY_LINK;
2571             *flags |= WEBDAV_FLAG_COPY_XDEV;
2572         }
2573     }
2574 
2575     /* link() or rename() failed; fall back to copy to tempfile and rename() */
2576     int status = webdav_copytmp_rename(pconf, src, dst, overwrite);
2577     if (0 == status) {
2578         webdav_prop_copy_uri(pconf, &src->rel_path, &dst->rel_path);
2579         if (*flags & (WEBDAV_FLAG_MOVE_RENAME|WEBDAV_FLAG_MOVE_XDEV))
2580             webdav_delete_file(pconf, src);
2581             /*(copy successful, but how should we report if delete fails?)*/
2582     }
2583     return status;
2584 }
2585 
2586 
2587 static int
2588 webdav_mkdir (const plugin_config * const pconf,
2589               const physical_st * const dst,
2590               const int overwrite)
2591 {
2592     if (0 == mkdir(dst->path.ptr, WEBDAV_DIR_MODE)) {
2593         webdav_parent_modified(&dst->path);
2594         return 0;
2595     }
2596 
2597     switch (errno) {
2598       case EEXIST:
2599       case ENOTDIR: break;
2600       case ENOENT:  return 409; /* Conflict */
2601       case EPERM:
2602       default:      return 403; /* Forbidden */
2603     }
2604 
2605     /* [RFC4918] 9.3.1 MKCOL Status Codes
2606      *   405 (Method Not Allowed) -
2607      *     MKCOL can only be executed on an unmapped URL.
2608      */
2609     if (overwrite < 0)  /*(mod_webdav_mkcol() passes overwrite = -1)*/
2610         return (errno != ENOTDIR)
2611           ? 405  /* Method Not Allowed */
2612           : 409; /* Conflict */
2613 
2614   #ifdef __COVERITY__
2615     force_assert(2 <= dst->path.used);
2616     force_assert(2 <= dst->rel_path.used);
2617   #endif
2618 
2619     struct stat st;
2620     int status;
2621     dst->path.ptr[dst->path.used-2] = '\0'; /*(trailing slash)*/
2622     status = lstat(dst->path.ptr, &st);
2623     dst->path.ptr[dst->path.used-2] = '/';  /*(restore slash)*/
2624     if (0 != status) /* still ENOTDIR or *just* disappeared */
2625         return 409; /* Conflict */
2626 
2627     if (!overwrite) /* copying into a non-dir ? */
2628         return 409; /* Conflict */
2629 
2630     if (S_ISDIR(st.st_mode))
2631         return 0;
2632 
2633     dst->path.ptr[dst->path.used-2] = '\0'; /*(trailing slash)*/
2634     dst->rel_path.ptr[dst->rel_path.used-2] = '\0';
2635     status = webdav_delete_file(pconf, dst);
2636     dst->path.ptr[dst->path.used-2] = '/';  /*(restore slash)*/
2637     dst->rel_path.ptr[dst->rel_path.used-2] = '/';
2638     if (0 != status)
2639         return status;
2640 
2641     webdav_parent_modified(&dst->path);
2642     return (0 == mkdir(dst->path.ptr, WEBDAV_DIR_MODE))
2643       ? 0
2644       : 409; /* Conflict */
2645 }
2646 
2647 
2648 static int
2649 webdav_copymove_dir (const plugin_config * const pconf,
2650                      physical_st * const src,
2651                      physical_st * const dst,
2652                      buffer * const b,
2653                      int flags)
2654 {
2655     /* NOTE: merging collections is NON-CONFORMANT behavior
2656      *       (specified in [RFC4918])
2657      *
2658      * However, merging collections during COPY/MOVE might be expected behavior
2659      * by client, as merging is the behavior of unix cp -r (recursive copy) as
2660      * well as how Microsoft Windows Explorer performs folder copies.
2661      *
2662      * [RFC4918] 9.8.4 COPY and Overwriting Destination Resources
2663      *   When a collection is overwritten, the membership of the destination
2664      *   collection after the successful COPY request MUST be the same
2665      *   membership as the source collection immediately before the COPY. Thus,
2666      *   merging the membership of the source and destination collections
2667      *   together in the destination is not a compliant behavior.
2668      * [Ed: strange how non-compliance statement is immediately followed by:]
2669      *   In general, if clients require the state of the destination URL to be
2670      *   wiped out prior to a COPY (e.g., to force live properties to be reset),
2671      *   then the client could send a DELETE to the destination before the COPY
2672      *   request to ensure this reset.
2673      * [Ed: if non-compliant merge behavior is the default here, and were it to
2674      *  not be desired by client, client could send a DELETE to the destination
2675      *  before issuing COPY.  There is no easy way to obtain merge behavior
2676      *  (were it not the non-compliant default here) unless the client recurses
2677      *  into the source and destination, and creates a list of objects that need
2678      *  to be copied.  This could fail or miss files due to racing with other
2679      *  clients.  All of this might forget to emphasize that wiping out an
2680      *  existing destination collection (a recursive operation) is dangerous and
2681      *  would happen if the client set Overwrite: T or omitted setting Overwrite
2682      *  since Overwrite: T is default (client must explicitly set Overwrite: F)]
2683      * [RFC4918] 9.9.3 MOVE and the Overwrite Header
2684      *   If a resource exists at the destination and the Overwrite header is
2685      *   "T", then prior to performing the move, the server MUST perform a
2686      *   DELETE with "Depth: infinity" on the destination resource. If the
2687      *   Overwrite header is set to "F", then the operation will fail.
2688      */
2689 
2690     /* NOTE: aborting if 507 Insufficient Storage is NON-CONFORMANT behavior
2691      *       [RFC4918] specifies that as much as possible of COPY or MOVE
2692      *       should be completed.
2693      */
2694 
2695     /* ??? upon encountering errors, should src->rel_path or dst->rel_path
2696      *     be used in XML error ??? */
2697 
2698     struct stat st;
2699     int status;
2700     int dfd;
2701 
2702     int make_destdir = 1;
2703     const int overwrite = (flags & WEBDAV_FLAG_OVERWRITE);
2704     if (flags & WEBDAV_FLAG_MOVE_RENAME) {
2705       #ifndef RENAME_NOREPLACE /*(renameat2() not well-supported yet)*/
2706         if (!overwrite) {
2707             if (0 == lstat(dst->path.ptr, &st) || errno != ENOENT) {
2708                 webdav_xml_response_status(b, &src->rel_path, 412);
2709                 return 412; /* Precondition Failed */
2710             }
2711             /* TOC-TOU race between lstat() and rename(),
2712              * but this is reasonable attempt to not overwrite existing entity*/
2713         }
2714         if (0 == rename(src->path.ptr, dst->path.ptr))
2715       #else
2716         if (0 == renameat2(AT_FDCWD, src->path.ptr,
2717                            AT_FDCWD, dst->path.ptr,
2718                            overwrite ? 0 : RENAME_NOREPLACE))
2719       #endif
2720         {
2721             webdav_prop_move_uri_col(pconf, &src->rel_path, &dst->rel_path);
2722             return 0;
2723         }
2724         else {
2725             switch (errno) {
2726               case EEXIST:
2727              #if EEXIST != ENOTEMPTY
2728               case ENOTEMPTY:
2729              #endif
2730                 if (!overwrite) {
2731                         webdav_xml_response_status(b, &src->rel_path, 412);
2732                         return 412; /* Precondition Failed */
2733                 }
2734                 make_destdir = 0;
2735                 break;
2736               case ENOTDIR:
2737                 if (!overwrite) {
2738                         webdav_xml_response_status(b, &src->rel_path, 409);
2739                         return 409; /* Conflict */
2740                 }
2741 
2742               #ifdef __COVERITY__
2743                 force_assert(2 <= dst->path.used);
2744               #endif
2745 
2746                 dst->path.ptr[dst->path.used-2] = '\0'; /*(trailing slash)*/
2747                 status = lstat(dst->path.ptr, &st);
2748                 dst->path.ptr[dst->path.used-2] = '/';  /*(restore slash)*/
2749                 if (0 == status) {
2750                     if (S_ISDIR(st.st_mode)) {
2751                         make_destdir = 0;
2752                         break;
2753                     }
2754 
2755                   #ifdef __COVERITY__
2756                     force_assert(2 <= dst->rel_path.used);
2757                   #endif
2758 
2759                     dst->path.ptr[dst->path.used-2] = '\0'; /*(remove slash)*/
2760                     dst->rel_path.ptr[dst->rel_path.used-2] = '\0';
2761                     status = webdav_delete_file(pconf, dst);
2762                     dst->path.ptr[dst->path.used-2] = '/'; /*(restore slash)*/
2763                     dst->rel_path.ptr[dst->rel_path.used-2] = '/';
2764                     if (0 != status) {
2765                         webdav_xml_response_status(b, &src->rel_path, status);
2766                         return status;
2767                     }
2768 
2769                     if (0 == rename(src->path.ptr, dst->path.ptr)) {
2770                         webdav_prop_move_uri_col(pconf, &src->rel_path,
2771                                                         &dst->rel_path);
2772                         return 0;
2773                     }
2774                 }
2775                 break;
2776               case EXDEV:
2777                 flags &= ~WEBDAV_FLAG_MOVE_RENAME;
2778                 flags |= WEBDAV_FLAG_MOVE_XDEV;
2779                 /* (if overwrite, then could switch to WEBDAV_FLAG_COPY_XDEV
2780                  *  and set a flag so that before returning from this routine,
2781                  *  directory is deleted recursively, instead of deleting each
2782                  *  file after each copy.  Only reliable if overwrite is set
2783                  *  since if it is not set, an error would leave file copies in
2784                  *  two places and would be difficult to recover if !overwrite)
2785                  * (collections typically do not cross devices, so this is not
2786                  *  expected to be a common case) */
2787                 break;
2788               default:
2789                 break;
2790             }
2791         }
2792     }
2793 
2794     if (make_destdir) {
2795         if (0 != (status = webdav_mkdir(pconf, dst, overwrite))) {
2796             webdav_xml_response_status(b, &src->rel_path, status);
2797             return status;
2798         }
2799     }
2800 
2801     webdav_prop_copy_uri(pconf, &src->rel_path, &dst->rel_path);
2802 
2803     /* copy from src to dst (and, if move, then delete src)
2804      * src and dst are modified in place to extend path,
2805      * so be sure to restore to base each loop iter */
2806 
2807     const uint32_t src_path_used     = src->path.used;
2808     const uint32_t src_rel_path_used = src->rel_path.used;
2809     const uint32_t dst_path_used     = dst->path.used;
2810     const uint32_t dst_rel_path_used = dst->rel_path.used;
2811 
2812     dfd = fdevent_open_dirname(src->path.ptr, 0);
2813     DIR * const srcdir = (dfd >= 0) ? fdopendir(dfd) : NULL;
2814     if (NULL == srcdir) {
2815         if (dfd >= 0) close(dfd);
2816         webdav_xml_response_status(b, &src->rel_path, 403);
2817         return 403; /* Forbidden */
2818     }
2819     mode_t d_type;
2820     int multi_status = 0;
2821     struct dirent *de;
2822     while (NULL != (de = readdir(srcdir))) {
2823         if (de->d_name[0] == '.'
2824             && (de->d_name[1] == '\0'
2825                 || (de->d_name[1] == '.' && de->d_name[2] == '\0')))
2826             continue; /* ignore "." and ".." */
2827 
2828       #ifdef _DIRENT_HAVE_D_TYPE
2829         if (de->d_type != DT_UNKNOWN)
2830             d_type = DTTOIF(de->d_type);
2831         else
2832       #endif
2833         {
2834             if (0 != fstatat(dfd, de->d_name, &st, AT_SYMLINK_NOFOLLOW))
2835                 continue; /* file *just* disappeared? */
2836             d_type = st.st_mode;
2837         }
2838 
2839         const uint32_t len = (uint32_t) _D_EXACT_NAMLEN(de);
2840         if (flags & WEBDAV_FLAG_LC_NAMES) /*(needed at least for rel_path)*/
2841             webdav_str_len_to_lower(de->d_name, len);
2842 
2843         buffer_append_string_len(&src->path,     de->d_name, len);
2844         buffer_append_string_len(&dst->path,     de->d_name, len);
2845         buffer_append_string_len(&src->rel_path, de->d_name, len);
2846         buffer_append_string_len(&dst->rel_path, de->d_name, len);
2847 
2848         if (S_ISDIR(d_type)) { /* recursive call; depth first */
2849             buffer_append_string_len(&src->path,     CONST_STR_LEN("/"));
2850             buffer_append_string_len(&dst->path,     CONST_STR_LEN("/"));
2851             buffer_append_string_len(&src->rel_path, CONST_STR_LEN("/"));
2852             buffer_append_string_len(&dst->rel_path, CONST_STR_LEN("/"));
2853             status = webdav_copymove_dir(pconf, src, dst, b, flags);
2854             if (0 != status)
2855                multi_status = 1;
2856         }
2857         else if (S_ISREG(d_type)) {
2858             status = webdav_copymove_file(pconf, src, dst, &flags);
2859             if (0 != status)
2860                 webdav_xml_response_status(b, &src->rel_path, status);
2861         }
2862       #if 0
2863         else if (S_ISLNK(d_type)) {
2864             /*(might entertain support in future, including readlink()
2865              * and changing dst symlink to be relative to new location.
2866              * (or, if absolute to the old location, then absolute to new)
2867              * Be sure to hard-link using linkat() w/o AT_SYMLINK_FOLLOW)*/
2868         }
2869       #endif
2870         else {
2871             status = 0;
2872         }
2873 
2874         src->path.ptr[    (src->path.used     = src_path_used)    -1] = '\0';
2875         src->rel_path.ptr[(src->rel_path.used = src_rel_path_used)-1] = '\0';
2876         dst->path.ptr[    (dst->path.used     = dst_path_used)    -1] = '\0';
2877         dst->rel_path.ptr[(dst->rel_path.used = dst_rel_path_used)-1] = '\0';
2878 
2879         if (507 == status) {
2880             multi_status = 507; /* Insufficient Storage */
2881             break;
2882         }
2883     }
2884     closedir(srcdir);
2885 
2886     if (0 == multi_status) {
2887         if (flags & (WEBDAV_FLAG_MOVE_RENAME|WEBDAV_FLAG_MOVE_XDEV)) {
2888             status = webdav_delete_dir(pconf, src, b, flags); /* content */
2889             if (0 != status) {
2890                 webdav_xml_response_status(b, &src->rel_path, status);
2891                 multi_status = 1;
2892             }
2893         }
2894     }
2895 
2896     return multi_status;
2897 }
2898 
2899 
2900 typedef struct webdav_propfind_bufs {
2901   request_st * restrict r;
2902   const plugin_config * restrict pconf;
2903   physical_st * restrict dst;
2904   buffer * restrict b;
2905   buffer * restrict b_200;
2906   buffer * restrict b_404;
2907   webdav_property_names proplist;
2908   int allprop;
2909   int propname;
2910   int lockdiscovery;
2911   int depth;
2912   int recursed;
2913   int atflags;
2914   struct stat st;
2915 } webdav_propfind_bufs;
2916 
2917 
2918 enum webdav_live_props_e {
2919   WEBDAV_PROP_UNSET = -1             /* (enum value to avoid compiler warning)*/
2920  ,WEBDAV_PROP_ALL = 0                /* (ALL not really a prop; internal use) */
2921  /*,WEBDAV_PROP_CREATIONDATE*/       /* (located in database, if present) */
2922  /*,WEBDAV_PROP_DISPLAYNAME*/        /* (located in database, if present) */
2923  /*,WEBDAV_PROP_GETCONTENTLANGUAGE*/ /* (located in database, if present) */
2924  ,WEBDAV_PROP_GETCONTENTLENGTH
2925  ,WEBDAV_PROP_GETCONTENTTYPE
2926  ,WEBDAV_PROP_GETETAG
2927  ,WEBDAV_PROP_GETLASTMODIFIED
2928  /*,WEBDAV_PROP_LOCKDISCOVERY*/      /* (located in database, if present) */
2929  ,WEBDAV_PROP_RESOURCETYPE
2930  /*,WEBDAV_PROP_SOURCE*/             /* not implemented; removed in RFC4918 */
2931  ,WEBDAV_PROP_SUPPORTEDLOCK
2932 };
2933 
2934 
2935 #ifdef USE_PROPPATCH
2936 
2937 struct live_prop_list {
2938   const char *prop;
2939   const uint32_t len;
2940   enum webdav_live_props_e pnum;
2941 };
2942 
2943 static const struct live_prop_list live_properties[] = { /*(namespace "DAV:")*/
2944  /* { CONST_STR_LEN("creationdate"),       WEBDAV_PROP_CREATIONDATE }*/
2945  /*,{ CONST_STR_LEN("displayname"),        WEBDAV_PROP_DISPLAYNAME }*/
2946  /*,{ CONST_STR_LEN("getcontentlanguage"), WEBDAV_PROP_GETCONTENTLANGUAGE}*/
2947   { CONST_STR_LEN("getcontentlength"),     WEBDAV_PROP_GETCONTENTLENGTH }
2948  ,{ CONST_STR_LEN("getcontenttype"),       WEBDAV_PROP_GETCONTENTTYPE }
2949  ,{ CONST_STR_LEN("getetag"),              WEBDAV_PROP_GETETAG }
2950  ,{ CONST_STR_LEN("getlastmodified"),      WEBDAV_PROP_GETLASTMODIFIED }
2951  #ifdef USE_LOCKS
2952  /*,{ CONST_STR_LEN("lockdiscovery"),      WEBDAV_PROP_LOCKDISCOVERY }*/
2953  #endif
2954  ,{ CONST_STR_LEN("resourcetype"),         WEBDAV_PROP_RESOURCETYPE }
2955  /*,{ CONST_STR_LEN("source"),             WEBDAV_PROP_SOURCE }*/
2956  #ifdef USE_LOCKS
2957  ,{ CONST_STR_LEN("supportedlock"),        WEBDAV_PROP_SUPPORTEDLOCK }
2958  #endif
2959 
2960  ,{ NULL, 0, WEBDAV_PROP_UNSET }
2961 };
2962 
2963 /* protected live properties
2964  * (must also protect creationdate and lockdiscovery in database) */
2965 static const struct live_prop_list protected_props[] = { /*(namespace "DAV:")*/
2966   { CONST_STR_LEN("creationdate"),         WEBDAV_PROP_UNSET
2967                                              /*WEBDAV_PROP_CREATIONDATE*/ }
2968  /*,{ CONST_STR_LEN("displayname"),        WEBDAV_PROP_DISPLAYNAME }*/
2969  /*,{ CONST_STR_LEN("getcontentlanguage"), WEBDAV_PROP_GETCONTENTLANGUAGE}*/
2970  ,{ CONST_STR_LEN("getcontentlength"),     WEBDAV_PROP_GETCONTENTLENGTH }
2971  ,{ CONST_STR_LEN("getcontenttype"),       WEBDAV_PROP_GETCONTENTTYPE }
2972  ,{ CONST_STR_LEN("getetag"),              WEBDAV_PROP_GETETAG }
2973  ,{ CONST_STR_LEN("getlastmodified"),      WEBDAV_PROP_GETLASTMODIFIED }
2974  ,{ CONST_STR_LEN("lockdiscovery"),        WEBDAV_PROP_UNSET
2975                                              /*WEBDAV_PROP_LOCKDISCOVERY*/ }
2976  ,{ CONST_STR_LEN("resourcetype"),         WEBDAV_PROP_RESOURCETYPE }
2977  /*,{ CONST_STR_LEN("source"),             WEBDAV_PROP_SOURCE }*/
2978  ,{ CONST_STR_LEN("supportedlock"),        WEBDAV_PROP_SUPPORTEDLOCK }
2979 
2980  ,{ NULL, 0, WEBDAV_PROP_UNSET }
2981 };
2982 
2983 #endif
2984 
2985 
2986 static int
2987 webdav_propfind_live_props (const webdav_propfind_bufs * const restrict pb,
2988                            const enum webdav_live_props_e pnum)
2989 {
2990     buffer * const restrict b = pb->b_200;
2991     switch (pnum) {
2992       case WEBDAV_PROP_ALL:
2993         /*(fall through)*/
2994       /*case WEBDAV_PROP_CREATIONDATE:*/  /* (located in database, if present)*/
2995       #if 0
2996       case WEBDAV_PROP_CREATIONDATE: {
2997         /* st->st_ctim
2998          * defined by POSIX.1-2008 as last file status change timestamp
2999          * and is no long create-time (as it may have been on older filesystems)
3000          * Therefore, this should return Not Found.
3001          * [RFC4918] 15.1 creationdate Property
3002          *   The DAV:creationdate property SHOULD be defined on all DAV
3003          *   compliant resources. If present, it contains a timestamp of the
3004          *   moment when the resource was created. Servers that are incapable
3005          *   of persistently recording the creation date SHOULD instead leave
3006          *   it undefined (i.e. report "Not Found").
3007          * (future: might store creationdate in database when PUT creates file
3008          *  or LOCK creates empty file, or MKCOL creates collection (dir),
3009          *  i.e. wherever the status is 201 Created)
3010          */
3011         struct tm tm;
3012         char ctime_buf[sizeof("2005-08-18T07:27:16Z")];
3013         if (__builtin_expect( (NULL != gmtime_r(&pb->st.st_ctime, &tm)), 1)) {
3014             buffer_append_string_len(b, CONST_STR_LEN(
3015               "<D:creationdate ns0:dt=\"dateTime.tz\">"));
3016             buffer_append_string_len(b, ctime_buf,
3017                                      strftime(ctime_buf, sizeof(ctime_buf),
3018                                               "%Y-%m-%dT%TZ", &tm));
3019             buffer_append_string_len(b, CONST_STR_LEN(
3020               "</D:creationdate>"));
3021         }
3022         else if (pnum != WEBDAV_PROP_ALL)
3023             return -1; /* invalid; report 'not found' */
3024         if (pnum != WEBDAV_PROP_ALL) return 0;/* found *//*(else fall through)*/
3025         __attribute_fallthrough__
3026       }
3027       #endif
3028       /*case WEBDAV_PROP_DISPLAYNAME:*/   /* (located in database, if present)*/
3029       /*case WEBDAV_PROP_GETCONTENTLANGUAGE:*/  /* (located in db, if present)*/
3030       #if 0
3031       case WEBDAV_PROP_GETCONTENTLANGUAGE:
3032         /* [RFC4918] 15.3 getcontentlanguage Property
3033          *   SHOULD NOT be protected, so that clients can reset the language.
3034          *   [...]
3035          *   The DAV:getcontentlanguage property MUST be defined on any
3036          *   DAV-compliant resource that returns the Content-Language header on
3037          *   a GET.
3038          * (future: server does not currently set Content-Language and this
3039          *  module would need to somehow find out if another module set it)
3040          */
3041         buffer_append_string_len(b, CONST_STR_LEN(
3042           "<D:getcontentlanguage>en</D:getcontentlanguage>"));
3043         if (pnum != WEBDAV_PROP_ALL) return 0;/* found *//*(else fall through)*/
3044         __attribute_fallthrough__
3045       #endif
3046       case WEBDAV_PROP_GETCONTENTLENGTH:
3047         buffer_append_string_len(b, CONST_STR_LEN(
3048           "<D:getcontentlength>"));
3049         buffer_append_int(b, pb->st.st_size);
3050         buffer_append_string_len(b, CONST_STR_LEN(
3051           "</D:getcontentlength>"));
3052         if (pnum != WEBDAV_PROP_ALL) return 0;/* found *//*(else fall through)*/
3053         __attribute_fallthrough__
3054       case WEBDAV_PROP_GETCONTENTTYPE:
3055         /* [RFC4918] 15.5 getcontenttype Property
3056          *   Potentially protected if the server prefers to assign content types
3057          *   on its own (see also discussion in Section 9.7.1).
3058          * (server currently assigns content types)
3059          *
3060          * [RFC4918] 15 DAV Properties
3061          *   For properties defined based on HTTP GET response headers
3062          *   (DAV:get*), the header value could include LWS as defined
3063          *   in [RFC2616], Section 4.2. Server implementors SHOULD strip
3064          *   LWS from these values before using as WebDAV property
3065          *   values.
3066          * e.g. application/xml;charset="utf-8"
3067          *      instead of: application/xml; charset="utf-8"
3068          * (documentation-only; no check is done here to remove LWS)
3069          */
3070         if (S_ISDIR(pb->st.st_mode)) {
3071             buffer_append_string_len(b, CONST_STR_LEN(
3072               "<D:getcontenttype>httpd/unix-directory</D:getcontenttype>"));
3073         }
3074         else {
3075             /* provide content type by extension
3076              * Note: not currently supporting filesystem xattr */
3077             const array * const mtypes = pb->r->conf.mimetypes;
3078             const buffer *ct =
3079               stat_cache_mimetype_by_ext(mtypes, CONST_BUF_LEN(&pb->dst->path));
3080             if (NULL != ct) {
3081                 buffer_append_string_len(b, CONST_STR_LEN(
3082                   "<D:getcontenttype>"));
3083                 buffer_append_string_buffer(b, ct);
3084                 buffer_append_string_len(b, CONST_STR_LEN(
3085                   "</D:getcontenttype>"));
3086             }
3087             else {
3088                 if (pnum != WEBDAV_PROP_ALL)
3089                     return -1; /* invalid; report 'not found' */
3090             }
3091         }
3092         if (pnum != WEBDAV_PROP_ALL) return 0;/* found *//*(else fall through)*/
3093         __attribute_fallthrough__
3094       case WEBDAV_PROP_GETETAG:
3095         if (0 != pb->r->conf.etag_flags) {
3096             buffer *etagb = &pb->r->physical.etag;
3097             etag_create(etagb, &pb->st, pb->r->conf.etag_flags);
3098             etag_mutate(etagb, etagb);
3099             buffer_append_string_len(b, CONST_STR_LEN(
3100               "<D:getetag>"));
3101             buffer_append_string_buffer(b, etagb);
3102             buffer_append_string_len(b, CONST_STR_LEN(
3103               "</D:getetag>"));
3104             buffer_clear(etagb);
3105         }
3106         else if (pnum != WEBDAV_PROP_ALL)
3107             return -1; /* invalid; report 'not found' */
3108         if (pnum != WEBDAV_PROP_ALL) return 0;/* found *//*(else fall through)*/
3109         __attribute_fallthrough__
3110       case WEBDAV_PROP_GETLASTMODIFIED:
3111         {
3112             buffer_append_string_len(b, CONST_STR_LEN(
3113               "<D:getlastmodified ns0:dt=\"dateTime.rfc1123\">"));
3114             buffer_append_strftime(b, "%a, %d %b %Y %H:%M:%S GMT",
3115                                    gmtime(&pb->st.st_mtime));
3116             buffer_append_string_len(b, CONST_STR_LEN(
3117               "</D:getlastmodified>"));
3118         }
3119         if (pnum != WEBDAV_PROP_ALL) return 0;/* found *//*(else fall through)*/
3120         __attribute_fallthrough__
3121       #if 0
3122       #ifdef USE_LOCKS
3123       case WEBDAV_PROP_LOCKDISCOVERY:
3124         /* database query for locks occurs in webdav_propfind_resource_props()*/
3125         if (pnum != WEBDAV_PROP_ALL) return 0;/* found *//*(else fall through)*/
3126         __attribute_fallthrough__
3127       #endif
3128       #endif
3129       case WEBDAV_PROP_RESOURCETYPE:
3130         if (S_ISDIR(pb->st.st_mode))
3131             buffer_append_string_len(b, CONST_STR_LEN(
3132               "<D:resourcetype><D:collection/></D:resourcetype>"));
3133         else
3134             buffer_append_string_len(b, CONST_STR_LEN(
3135               "<D:resourcetype/>"));
3136         if (pnum != WEBDAV_PROP_ALL) return 0;/* found *//*(else fall through)*/
3137         __attribute_fallthrough__
3138       /*case WEBDAV_PROP_SOURCE:*/        /* not impl; removed in RFC4918 */
3139       #ifdef USE_LOCKS
3140       case WEBDAV_PROP_SUPPORTEDLOCK:
3141         buffer_append_string_len(b, CONST_STR_LEN(
3142               "<D:supportedlock>"
3143               "<D:lockentry>"
3144               "<D:lockscope><D:exclusive/></D:lockscope>"
3145               "<D:locktype><D:write/></D:locktype>"
3146               "</D:lockentry>"
3147               "<D:lockentry>"
3148               "<D:lockscope><D:shared/></D:lockscope>"
3149               "<D:locktype><D:write/></D:locktype>"
3150               "</D:lockentry>"
3151               "</D:supportedlock>"));
3152         if (pnum != WEBDAV_PROP_ALL) return 0;/* found *//*(else fall through)*/
3153         __attribute_fallthrough__
3154       #endif
3155       default: /* WEBDAV_PROP_UNSET */
3156         if (pnum == WEBDAV_PROP_ALL) break;
3157         return -1; /* not found */
3158     }
3159     return 0; /* found (WEBDAV_PROP_ALL) */
3160 }
3161 
3162 
3163 #ifdef USE_LOCKS
3164 static void
3165 webdav_propfind_lockdiscovery_cb (void * const vdata,
3166                                   const webdav_lockdata * const lockdata)
3167 {
3168     webdav_xml_activelock((buffer *)vdata, lockdata, NULL, 0);
3169 }
3170 #endif
3171 
3172 
3173 static void
3174 webdav_propfind_resource_props (const webdav_propfind_bufs * const restrict pb)
3175 {
3176     const webdav_property_names * const props = &pb->proplist;
3177     if (props->used) { /* "props" or "allprop"+"include" */
3178         const webdav_property_name *prop = props->ptr;
3179         for (int i = 0; i < props->used; ++i, ++prop) {
3180             if (NULL == prop->name  /*(flag indicating prop is live prop enum)*/
3181                 ? 0 == webdav_propfind_live_props(pb, (enum webdav_live_props_e)
3182                                                       prop->namelen)
3183                 : 0 == webdav_prop_select_prop(pb->pconf, &pb->dst->rel_path,
3184                                                prop, pb->b_200))
3185                 continue;
3186 
3187             /*(error obtaining prop if reached)*/
3188             if (prop->name)
3189                 webdav_xml_prop(pb->b_404, prop, NULL, 0);
3190             else {
3191               #ifdef USE_PROPPATCH
3192                 const struct live_prop_list *list = live_properties;
3193                 while (0 != list->len && (uint32_t)list->pnum != prop->namelen)
3194                     ++list;
3195                 if (0 != list->len) { /*(list->pnum == prop->namelen)*/
3196                     webdav_property_name lprop =
3197                       { prop->ns, list->prop, prop->nslen, list->len };
3198                     webdav_xml_prop(pb->b_404, &lprop, NULL, 0);
3199                 }
3200               #endif
3201             }
3202         }
3203     }
3204 
3205     if (pb->allprop) {
3206         webdav_propfind_live_props(pb, WEBDAV_PROP_ALL);
3207         webdav_prop_select_props(pb->pconf, &pb->dst->rel_path, pb->b_200);
3208     }
3209 
3210   #ifdef USE_LOCKS
3211     if (pb->lockdiscovery) {
3212         /* pb->lockdiscovery > 0:
3213          *   report locks resource or containing (parent) collections
3214          * pb->lockdiscovery < 0:
3215          *   report only those locks on specific resource
3216          * While this is not compliant with RFC, it may reduces quite a bit of
3217          * redundancy for propfind on Depth: 1 and Depth: infinity when there
3218          * are locks on parent collections.  The client receiving this propfind
3219          * XML response should easily know that locks on collections apply to
3220          * the members of those collections and to further nested collections
3221          *
3222          * future: might be many, many fewer database queries if make a single
3223          * query for the locks in the collection directory tree and parse the
3224          * results, rather than querying the database for each resource */
3225         buffer_append_string_len(pb->b_200, CONST_STR_LEN(
3226           "<D:lockdiscovery>"));
3227         webdav_lock_activelocks(pb->pconf, &pb->dst->rel_path,
3228                                 (pb->lockdiscovery > 0),
3229                                 webdav_propfind_lockdiscovery_cb, pb->b_200);
3230         buffer_append_string_len(pb->b_200, CONST_STR_LEN(
3231           "</D:lockdiscovery>"));
3232     }
3233   #endif
3234 }
3235 
3236 
3237 static void
3238 webdav_propfind_resource_propnames (const webdav_propfind_bufs *
3239                                       const restrict pb)
3240 {
3241     static const char live_propnames[] =
3242       "<getcontentlength/>\n"
3243       "<getcontenttype/>\n"
3244       "<getetag/>\n"
3245       "<getlastmodified/>\n"
3246       "<resourcetype/>\n"
3247      #ifdef USE_LOCKS
3248       "<supportedlock/>\n"
3249       "<lockdiscovery/>\n"
3250      #endif
3251       ;
3252     /* list live_properties which are not in database, plus "lockdiscovery" */
3253     buffer_append_string_len(pb->b_200,live_propnames,sizeof(live_propnames)-1);
3254 
3255     /* list properties in database 'properties' table for resource */
3256     webdav_prop_select_propnames(pb->pconf, &pb->dst->rel_path, pb->b_200);
3257 }
3258 
3259 
3260 __attribute_cold__
3261 static void
3262 webdav_propfind_resource_403 (const webdav_propfind_bufs * const restrict pb)
3263 {
3264     buffer * const restrict b = pb->b;
3265     buffer_append_string_len(b, CONST_STR_LEN(
3266       "<D:response>\n"));
3267     webdav_xml_href(b, &pb->dst->rel_path);
3268     buffer_append_string_len(b, CONST_STR_LEN(
3269       "<D:propstat>\n"));
3270     webdav_xml_status(b, 403); /* Forbidden */
3271     buffer_append_string_len(b, CONST_STR_LEN(
3272       "</D:propstat>\n"
3273       "</D:response>\n"));
3274 }
3275 
3276 
3277 static void
3278 webdav_propfind_resource (const webdav_propfind_bufs * const restrict pb)
3279 {
3280     buffer_clear(pb->b_200);
3281     buffer_clear(pb->b_404);
3282 
3283     if (!pb->propname)
3284         webdav_propfind_resource_props(pb);
3285     else
3286         webdav_propfind_resource_propnames(pb);
3287 
3288     /* buffer could get very large for large directory (or Depth: infinity)
3289      * attempt to allocate in 8K chunks, rather than default realloc in
3290      * 64-byte chunks (see buffer.h) which will lead to exponentially more
3291      * expensive copy behavior as buffer is resized over and over and over
3292      *
3293      * future: avoid (potential) excessive memory usage by accumulating output
3294      *         in temporary file
3295      */
3296     buffer * const restrict b     = pb->b;
3297     buffer * const restrict b_200 = pb->b_200;
3298     buffer * const restrict b_404 = pb->b_404;
3299     if (b->size - b->used < b_200->used + b_404->used + 1024) {
3300         size_t sz = b->used + 8192-1 + b_200->used + b_404->used + 1024 - 1;
3301         /*(optimization; buffer is extended as needed)*/
3302         buffer_string_prepare_append(b, sz & (8192-1));
3303     }
3304 
3305     buffer_append_string_len(b, CONST_STR_LEN(
3306       "<D:response>\n"));
3307     webdav_xml_href(b, &pb->dst->rel_path);
3308     if (b_200->used > 1)  /* !unset and !blank */
3309         webdav_xml_propstat(b, b_200, 200);
3310     if (b_404->used > 1)  /* !unset and !blank */
3311         webdav_xml_propstat(b, b_404, 404);
3312     buffer_append_string_len(b, CONST_STR_LEN(
3313       "</D:response>\n"));
3314 }
3315 
3316 
3317 static void
3318 webdav_propfind_dir (webdav_propfind_bufs * const restrict pb)
3319 {
3320     /* arbitrary recursion limit to prevent infinite loops,
3321      * e.g. due to symlink loops, or excessive resource usage */
3322     if (++pb->recursed > 100) return;
3323 
3324     physical_st * const dst = pb->dst;
3325     const int dfd = fdevent_open_dirname(dst->path.ptr, 0);
3326     DIR * const dir = (dfd >= 0) ? fdopendir(dfd) : NULL;
3327     if (NULL == dir) {
3328         int errnum = errno;
3329         if (dfd >= 0) close(dfd);
3330         if (errnum != ENOENT)
3331             webdav_propfind_resource_403(pb); /* Forbidden */
3332         return;
3333     }
3334 
3335     webdav_propfind_resource(pb);
3336 
3337     if (pb->lockdiscovery > 0)
3338         pb->lockdiscovery = -pb->lockdiscovery; /*(check locks on node only)*/
3339 
3340     /* dst is modified in place to extend path,
3341      * so be sure to restore to base each loop iter */
3342     const uint32_t dst_path_used     = dst->path.used;
3343     const uint32_t dst_rel_path_used = dst->rel_path.used;
3344     const int flags =
3345       (pb->r->conf.force_lowercase_filenames ? WEBDAV_FLAG_LC_NAMES : 0);
3346     struct dirent *de;
3347     while (NULL != (de = readdir(dir))) {
3348         if (de->d_name[0] == '.'
3349             && (de->d_name[1] == '\0'
3350                 || (de->d_name[1] == '.' && de->d_name[2] == '\0')))
3351             continue; /* ignore "." and ".." */
3352 
3353         if (0 != fstatat(dfd, de->d_name, &pb->st, pb->atflags))
3354             continue; /* file *just* disappeared? */
3355 
3356         const uint32_t len = (uint32_t) _D_EXACT_NAMLEN(de);
3357         if (flags & WEBDAV_FLAG_LC_NAMES) /*(needed by rel_path)*/
3358             webdav_str_len_to_lower(de->d_name, len);
3359         buffer_append_string_len(&dst->path, de->d_name, len);
3360         buffer_append_string_len(&dst->rel_path, de->d_name, len);
3361         if (S_ISDIR(pb->st.st_mode)) {
3362             buffer_append_string_len(&dst->path,     CONST_STR_LEN("/"));
3363             buffer_append_string_len(&dst->rel_path, CONST_STR_LEN("/"));
3364         }
3365 
3366         if (S_ISDIR(pb->st.st_mode) && -1 == pb->depth)
3367             webdav_propfind_dir(pb); /* recurse */
3368         else
3369             webdav_propfind_resource(pb);
3370 
3371         dst->path.ptr[    (dst->path.used     = dst_path_used)    -1] = '\0';
3372         dst->rel_path.ptr[(dst->rel_path.used = dst_rel_path_used)-1] = '\0';
3373     }
3374     closedir(dir);
3375 }
3376 
3377 
3378 static int
3379 webdav_open_chunk_file_rd (chunk * const c)
3380 {
3381     if (c->file.fd < 0) /* open file if not already open *//*permit symlink*/
3382         c->file.fd = fdevent_open_cloexec(c->mem->ptr, 1, O_RDONLY, 0);
3383     return c->file.fd;
3384 }
3385 
3386 
3387 static int
3388 webdav_mmap_file_rd (void ** const addr, const size_t length,
3389                    const int fd, const off_t offset)
3390 {
3391     /*(caller must ensure offset is properly aligned to mmap requirements)*/
3392 
3393     if (0 == length) {
3394         *addr = NULL; /*(something other than MAP_FAILED)*/
3395         return 0;
3396     }
3397 
3398   #ifdef HAVE_MMAP
3399 
3400     *addr = mmap(NULL, length, PROT_READ, MAP_SHARED, fd, offset);
3401     if (*addr == MAP_FAILED && errno == EINVAL)
3402         *addr = mmap(NULL, length, PROT_READ, MAP_PRIVATE, fd, offset);
3403     return (*addr != MAP_FAILED ? 0 : -1);
3404 
3405   #else
3406 
3407     return -1;
3408 
3409   #endif
3410 }
3411 
3412 
3413 static char *
3414 webdav_mmap_file_chunk (chunk * const c)
3415 {
3416     /*(request body provided in temporary file, so ok to mmap().
3417      * Otherwise, must check defined(ENABLE_MMAP)) */
3418     /* chunk_reset() or chunk_free() will clean up mmap'd chunk */
3419     /* close c->file.fd only after mmap() succeeds, since it will not
3420      * be able to be re-opened if it was a tmpfile that was unlinked */
3421     /*assert(c->type == FILE_CHUNK);*/
3422     if (MAP_FAILED != c->file.mmap.start)
3423         return c->file.mmap.start + c->offset - c->file.mmap.offset;
3424 
3425     if (webdav_open_chunk_file_rd(c) < 0)
3426         return NULL;
3427 
3428     webdav_mmap_file_rd((void **)&c->file.mmap.start, (size_t)c->file.length,
3429                         c->file.fd, 0);
3430 
3431     if (MAP_FAILED == c->file.mmap.start)
3432         return NULL;
3433 
3434     close(c->file.fd);
3435     c->file.fd = -1;
3436     c->file.mmap.length = c->file.length;
3437     return c->file.mmap.start + c->offset - c->file.mmap.offset;
3438 }
3439 
3440 
3441 #if defined(USE_PROPPATCH) || defined(USE_LOCKS)
3442 __attribute_noinline__
3443 static xmlDoc *
3444 webdav_parse_chunkqueue (request_st * const r,
3445                          const plugin_config * const pconf)
3446 {
3447     /* read the chunks in to the XML document */
3448     xmlParserCtxtPtr ctxt = xmlCreatePushParserCtxt(NULL, NULL, NULL, 0, NULL);
3449     /* XXX: evaluate adding more xmlParserOptions */
3450     xmlCtxtUseOptions(ctxt, XML_PARSE_NOERROR | XML_PARSE_NOWARNING
3451                           | XML_PARSE_PEDANTIC| XML_PARSE_NONET);
3452     char *xmlstr;
3453     chunkqueue * const cq = &r->reqbody_queue;
3454     size_t weWant = chunkqueue_length(cq);
3455     int err = XML_ERR_OK;
3456 
3457     while (weWant) {
3458         size_t weHave = 0;
3459         chunk *c = cq->first;
3460         char buf[16384];
3461       #ifdef __COVERITY__
3462         force_assert(0 == weWant || c != NULL);
3463       #endif
3464 
3465         if (c->type == MEM_CHUNK) {
3466             xmlstr = c->mem->ptr + c->offset;
3467             weHave = buffer_string_length(c->mem) - c->offset;
3468         }
3469         else if (c->type == FILE_CHUNK) {
3470             xmlstr = webdav_mmap_file_chunk(c);
3471             /*xmlstr = c->file.mmap.start + c->offset - c->file.mmap.offset;*/
3472             if (NULL != xmlstr) {
3473                 weHave = c->file.length - c->offset;
3474             }
3475             else {
3476                 switch (errno) {
3477                   case ENOSYS: case ENODEV: case EINVAL: break;
3478                   default:
3479                     log_perror(r->conf.errh, __FILE__, __LINE__,
3480                                "open() or mmap() '%*.s'",
3481                                BUFFER_INTLEN_PTR(c->mem));
3482                 }
3483                 if (webdav_open_chunk_file_rd(c) < 0) {
3484                     log_perror(r->conf.errh, __FILE__, __LINE__,
3485                                "open() '%*.s'",
3486                                BUFFER_INTLEN_PTR(c->mem));
3487                     err = XML_IO_UNKNOWN;
3488                     break;
3489                 }
3490                 ssize_t rd = -1;
3491                 do {
3492                     if (-1 == lseek(c->file.fd, c->offset, SEEK_SET))
3493                         break;
3494                     off_t len = c->file.length - c->offset;
3495                     if (len > (off_t)sizeof(buf)) len = (off_t)sizeof(buf);
3496                     rd = read(c->file.fd, buf, (size_t)len);
3497                 } while (-1 == rd && errno == EINTR);
3498                 if (rd >= 0) {
3499                     xmlstr = buf;
3500                     weHave = (size_t)rd;
3501                 }
3502                 else {
3503                     log_perror(r->conf.errh, __FILE__, __LINE__,
3504                                "read() '%*.s'",
3505                                BUFFER_INTLEN_PTR(c->mem));
3506                     err = XML_IO_UNKNOWN;
3507                     break;
3508                 }
3509             }
3510         }
3511         else {
3512             log_error(r->conf.errh, __FILE__, __LINE__,
3513                       "unrecognized chunk type: %d", c->type);
3514             err = XML_IO_UNKNOWN;
3515             break;
3516         }
3517 
3518         if (weHave > weWant) weHave = weWant;
3519 
3520         if (pconf->log_xml)
3521             log_error(r->conf.errh, __FILE__, __LINE__,
3522                       "XML-request-body: %.*s", (int)weHave, xmlstr);
3523 
3524         if (XML_ERR_OK != (err = xmlParseChunk(ctxt, xmlstr, weHave, 0))) {
3525             log_error(r->conf.errh, __FILE__, __LINE__,
3526                       "xmlParseChunk failed at: %lld %zu %d",
3527                       (long long int)cq->bytes_out, weHave, err);
3528             break;
3529         }
3530 
3531         weWant -= weHave;
3532         chunkqueue_mark_written(cq, weHave);
3533     }
3534 
3535     if (XML_ERR_OK == err) {
3536         switch ((err = xmlParseChunk(ctxt, 0, 0, 1))) {
3537           case XML_ERR_DOCUMENT_END:
3538           case XML_ERR_OK:
3539             if (ctxt->wellFormed) {
3540                 xmlDoc * const xml = ctxt->myDoc;
3541                 xmlFreeParserCtxt(ctxt);
3542                 return xml;
3543             }
3544             break;
3545           default:
3546             log_error(r->conf.errh, __FILE__, __LINE__,
3547                       "xmlParseChunk failed at final packet: %d", err);
3548             break;
3549         }
3550     }
3551 
3552     xmlFreeDoc(ctxt->myDoc);
3553     xmlFreeParserCtxt(ctxt);
3554     return NULL;
3555 }
3556 #endif
3557 
3558 
3559 #ifdef USE_LOCKS
3560 
3561 struct webdav_lock_token_submitted_st {
3562   buffer *tokens;
3563   int used;
3564   int size;
3565   const buffer *authn_user;
3566   buffer *b;
3567   int nlocks;
3568   int slocks;
3569   int smatch;
3570 };
3571 
3572 
3573 static void
3574 webdav_lock_token_submitted_cb (void * const vdata,
3575                                 const webdav_lockdata * const lockdata)
3576 {
3577     /* RFE: improve support for shared locks
3578      * (instead of treating match of any shared lock as sufficient,
3579      *  even when there are different lockroots)
3580      * keep track of matched shared locks and unmatched shared locks and
3581      * ensure that each lockroot with shared locks has at least one match
3582      * (Will need to allocate strings for each URI with shared lock and keep
3583      *  track whether or not a shared lock has been matched for that URI.
3584      *  After walking all locks, must walk looking for unmatched URIs,
3585      *  and must free these strings) */
3586 
3587     /* [RFC4918] 6.4 Lock Creator and Privileges
3588      *   When a locked resource is modified, a server MUST check that the
3589      *   authenticated principal matches the lock creator (in addition to
3590      *   checking for valid lock token submission).
3591      */
3592 
3593     struct webdav_lock_token_submitted_st * const cbdata =
3594       (struct webdav_lock_token_submitted_st *)vdata;
3595     const buffer * const locktoken = &lockdata->locktoken;
3596     const int shared = (lockdata->lockscope->used != sizeof("exclusive"));
3597 
3598     ++cbdata->nlocks;
3599     if (shared) ++cbdata->slocks;
3600 
3601     for (int i = 0; i < cbdata->used; ++i) {
3602         const buffer * const token = &cbdata->tokens[i];
3603         /* locktoken match (locktoken not '\0' terminated) */
3604         if (buffer_is_equal_string(token, CONST_BUF_LEN(locktoken))) {
3605             /*(0 length owner if no auth required to lock; not recommended)*/
3606             if (buffer_string_is_empty(lockdata->owner)/*no lock owner;match*/
3607                 || buffer_is_equal_string(cbdata->authn_user,
3608                                           CONST_BUF_LEN(lockdata->owner))) {
3609                 if (shared) ++cbdata->smatch;
3610                 return; /* authenticated lock owner match */
3611             }
3612         }
3613     }
3614 
3615     /* no match with lock tokens in request */
3616     if (!shared)
3617         webdav_xml_href(cbdata->b, &lockdata->lockroot);
3618 }
3619 
3620 
3621 /**
3622  * check if request provides necessary locks to access the resource
3623  */
3624 static int
3625 webdav_has_lock (request_st * const r,
3626                  const plugin_config * const pconf,
3627                  const buffer * const uri)
3628 {
3629     /* Note with regard to exclusive locks on collections: client should not be
3630      * able to obtain an exclusive lock on a collection if there are existing
3631      * locks on resource members inside the collection.  Therefore, there is no
3632      * need to check here for locks on resource members inside collections.
3633      * (This ignores the possibility that an admin or some other privileged
3634      * or out-of-band process has added locks in spite of lock on collection.)
3635      * Revisit to properly support shared locks. */
3636 
3637     struct webdav_lock_token_submitted_st cbdata;
3638     cbdata.b = chunk_buffer_acquire();
3639     cbdata.tokens = NULL;
3640     cbdata.used  = 0;
3641     cbdata.size  = 0;
3642     cbdata.nlocks = 0;
3643     cbdata.slocks = 0;
3644     cbdata.smatch = 0;
3645 
3646     /* XXX: maybe add config switch to require that authentication occurred? */
3647     buffer owner = { NULL, 0, 0 };/*owner (not authenticated)(auth_user unset)*/
3648     const data_string * const authn_user = (const data_string *)
3649       array_get_element_klen(&r->env, CONST_STR_LEN("REMOTE_USER"));
3650     cbdata.authn_user = authn_user ? &authn_user->value : &owner;
3651 
3652     const buffer * const h =
3653       http_header_request_get(r, HTTP_HEADER_OTHER, CONST_STR_LEN("If"));
3654 
3655     if (!buffer_is_empty(h)) {
3656         /* parse "If" request header for submitted lock tokens
3657          * While the below not a pedantic, validating parse, if the header
3658          * is non-conformant or contains unencoded characters, the result
3659          * will be misidentified or ignored lock tokens, which will result
3660          * in fail closed -- secure default behavior -- if those lock
3661          * tokens are required.  It is highly unlikely that misparsing the "If"
3662          * request header will result in a valid lock token since lock tokens
3663          * should be unique, and opaquelocktoken should be globally unique */
3664         char *p = h->ptr;
3665         do {
3666           #if 0
3667             while (*p == ' ' || *p == '\t') ++p;
3668             if (*p == '<') { /* Resource-Tag */
3669                 do { ++p; } while (*p != '>' && *p != '\0');
3670                 if (*p == '\0') break;
3671                 do { ++p; } while (*p == ' ' || *p == '\t');
3672             }
3673           #endif
3674 
3675             while (*p != '(' && *p != '\0') ++p;
3676 
3677             /* begin List in No-tag-list or Tagged-list
3678              *   List = "(" 1*Condition ")"
3679              *   Condition = ["Not"] (State-token | "[" entity-tag "]")
3680              */
3681             int notflag = 0;
3682             while (*p != '\0' && *++p != ')') {
3683                 while (*p == ' ' || *p == '\t') ++p;
3684                 if (   (p[0] & 0xdf) == 'N'
3685                     && (p[1] & 0xdf) == 'O'
3686                     && (p[2] & 0xdf) == 'T') {
3687                     notflag = 1;
3688                     p += 3;
3689                     while (*p == ' ' || *p == '\t') ++p;
3690                 }
3691                 if (*p != '<') { /* '<' begins State-token (Coded-URL) */
3692                     if (*p != '[') break; /* invalid syntax */
3693                     /* '[' and ']' wrap entity-tag */
3694                     char *etag = p+1;
3695                     do { ++p; } while (*p != ']' && *p != '\0');
3696                     if (*p != ']') break; /* invalid syntax */
3697                     if (p == etag) continue; /* ignore entity-tag if empty */
3698                     if (notflag) continue;/* ignore entity-tag in NOT context */
3699                     if (0 == r->conf.etag_flags) continue; /*ignore entity-tag*/
3700                     struct stat st;
3701                     if (0 != lstat(r->physical.path.ptr, &st)) {
3702                         http_status_set_error(r, 412); /* Precondition Failed */
3703                         chunk_buffer_release(cbdata.b);
3704                         return 0;
3705                     }
3706                     if (S_ISDIR(st.st_mode)) continue;/*we ignore etag if dir*/
3707                     buffer *etagb = &r->physical.etag;
3708                     etag_create(etagb, &st, r->conf.etag_flags);
3709                     etag_mutate(etagb, etagb);
3710                     *p = '\0';
3711                     int ematch = etag_is_equal(etagb, etag, 0);
3712                     *p = ']';
3713                     if (!ematch) {
3714                         http_status_set_error(r, 412); /* Precondition Failed */
3715                         chunk_buffer_release(cbdata.b);
3716                         return 0;
3717                     }
3718                     continue;
3719                 }
3720 
3721                 if (p[1] == 'D'
3722                     && 0 == strncmp(p, "<DAV:no-lock>",
3723                                     sizeof("<DAV:no-lock>")-1)) {
3724                     if (0 == notflag) {
3725                         http_status_set_error(r, 412); /* Precondition Failed */
3726                         chunk_buffer_release(cbdata.b);
3727                         return 0;
3728                     }
3729                     p += sizeof("<DAV:no-lock>")-2; /* point p to '>' */
3730                     continue;
3731                 }
3732 
3733                 /* State-token (Coded-URL)
3734                  *   Coded-URL = "<" absolute-URI ">"
3735                  *   ; No linear whitespace (LWS) allowed in Coded-URL
3736                  *   ; absolute-URI defined in RFC 3986, Section 4.3
3737                  */
3738                 if (cbdata.size == cbdata.used) {
3739                     if (cbdata.size == 16) { /* arbitrary limit */
3740                         http_status_set_error(r, 400); /* Bad Request */
3741                         chunk_buffer_release(cbdata.b);
3742                         return 0;
3743                     }
3744                     cbdata.tokens =
3745                       realloc(cbdata.tokens, sizeof(*(cbdata.tokens)) * 16);
3746                     force_assert(cbdata.tokens); /*(see above limit)*/
3747                 }
3748                 cbdata.tokens[cbdata.used].ptr = p+1;
3749 
3750                 do { ++p; } while (*p != '>' && *p != '\0');
3751                 if (*p == '\0') break; /* (*p != '>') */
3752 
3753                 cbdata.tokens[cbdata.used].used =
3754                   (uint32_t)(p - cbdata.tokens[cbdata.used].ptr + 1);
3755                 ++cbdata.used;
3756             }
3757         } while (*p++ == ')'); /* end of List in No-tag-list or Tagged-list */
3758     }
3759 
3760     webdav_lock_activelocks(pconf, uri, 1,
3761                             webdav_lock_token_submitted_cb, &cbdata);
3762 
3763     if (NULL != cbdata.tokens)
3764         free(cbdata.tokens);
3765 
3766     int has_lock = 1;
3767 
3768     if (0 != cbdata.b->used)
3769         has_lock = 0;
3770     else if (0 == cbdata.nlocks) { /* resource is not locked at all */
3771         /* error if lock provided on source and no locks present on source;
3772          * not error if no locks on Destination, but "If" provided for source */
3773         if (cbdata.used && uri == &r->physical.rel_path) {
3774             has_lock = -1;
3775             http_status_set_error(r, 412); /* Precondition Failed */
3776         }
3777       #if 0  /*(treat no locks as if caller is holding an appropriate lock)*/
3778         else {
3779             has_lock = 0;
3780             webdav_xml_href(cbdata.b, uri);
3781         }
3782       #endif
3783     }
3784 
3785     /*(XXX: overly simplistic shared lock matching allows any match of shared
3786      * locks even when there are shared locks on multiple different lockroots.
3787      * Failure is misreported since unmatched shared locks are not added to
3788      * cbdata.b) */
3789     if (cbdata.slocks && !cbdata.smatch)
3790         has_lock = 0;
3791 
3792     if (!has_lock)
3793         webdav_xml_doc_error_lock_token_submitted(r, cbdata.b);
3794 
3795     chunk_buffer_release(cbdata.b);
3796 
3797     return (has_lock > 0);
3798 }
3799 
3800 #else  /* ! defined(USE_LOCKS) */
3801 
3802 #define webdav_has_lock(r, pconf, uri) 1
3803 
3804 #endif /* ! defined(USE_LOCKS) */
3805 
3806 
3807 static handler_t
3808 mod_webdav_propfind (request_st * const r, const plugin_config * const pconf)
3809 {
3810     if (r->reqbody_length) {
3811       #ifdef USE_PROPPATCH
3812         if (r->state == CON_STATE_READ_POST) {
3813             handler_t rc = r->con->reqbody_read(r);
3814             if (rc != HANDLER_GO_ON) return rc;
3815         }
3816       #else
3817         /* PROPFIND is idempotent and safe, so even if parsing XML input is not
3818          * supported, live properties can still be produced, so treat as allprop
3819          * request.  NOTE: this behavior is NOT RFC CONFORMANT (and, well, if
3820          * compiled without XML support, this WebDAV implementation is already
3821          * non-compliant since it is missing support for XML request body).
3822          * RFC-compliant behavior would reject an ignored request body with
3823          *   415 Unsupported Media Type */
3824        #if 0
3825         http_status_set_error(r, 415); /* Unsupported Media Type */
3826         return HANDLER_FINISHED;
3827        #endif
3828       #endif
3829     }
3830 
3831     webdav_propfind_bufs pb;
3832 
3833     /* [RFC4918] 9.1 PROPFIND Method
3834      *   Servers MUST support "0" and "1" depth requests on WebDAV-compliant
3835      *   resources and SHOULD support "infinity" requests. In practice, support
3836      *   for infinite-depth requests MAY be disabled, due to the performance and
3837      *   security concerns associated with this behavior. Servers SHOULD treat
3838      *   a request without a Depth header as if a "Depth: infinity" header was
3839      *   included.
3840      */
3841     pb.allprop      = 0;
3842     pb.propname     = 0;
3843     pb.lockdiscovery= 0;
3844     pb.recursed     = 0;
3845     pb.depth        = webdav_parse_Depth(r);
3846 
3847     if (-1 == pb.depth && !(pconf->opts & MOD_WEBDAV_PROPFIND_DEPTH_INFINITY)) {
3848         webdav_xml_doc_error_propfind_finite_depth(r);
3849         return HANDLER_FINISHED;
3850     }
3851 
3852     pb.atflags =
3853       ((pconf->opts & MOD_WEBDAV_UNSAFE_PROPFIND_FOLLOW_SYMLINK)
3854        && pconf->is_readonly)
3855         ? 0 /* non-standard */
3856         : AT_SYMLINK_NOFOLLOW; /* WebDAV does not have symlink concept */
3857 
3858     if (pb.atflags == AT_SYMLINK_NOFOLLOW
3859         ? 0 != lstat(r->physical.path.ptr, &pb.st)
3860         : 0 != stat(r->physical.path.ptr, &pb.st)) { /* non-standard */
3861         http_status_set_error(r, (errno == ENOENT) ? 404 : 403);
3862         return HANDLER_FINISHED;
3863     }
3864     else if (S_ISDIR(pb.st.st_mode)) {
3865         if (r->physical.path.ptr[r->physical.path.used - 2] != '/') {
3866             const buffer *vb =
3867               http_header_request_get(r, HTTP_HEADER_USER_AGENT,
3868                                       CONST_STR_LEN("User-Agent"));
3869             if (vb && 0 == strncmp(vb->ptr, "Microsoft-WebDAV-MiniRedir/",
3870                                    sizeof("Microsoft-WebDAV-MiniRedir/")-1)) {
3871                 /* workaround Microsoft-WebDAV-MiniRedir bug */
3872                 /* (MS File Explorer unable to open folder if not redirected) */
3873                 http_response_redirect_to_directory(r, 308);
3874                 return HANDLER_FINISHED;
3875             }
3876             /* set "Content-Location" instead of sending 308 redirect to dir */
3877             if (!http_response_redirect_to_directory(r, 0))
3878                 return HANDLER_FINISHED;
3879             buffer_append_string_len(&r->physical.path,    CONST_STR_LEN("/"));
3880             buffer_append_string_len(&r->physical.rel_path,CONST_STR_LEN("/"));
3881         }
3882     }
3883     else if (r->physical.path.ptr[r->physical.path.used - 2] == '/') {
3884         http_status_set_error(r, 403);
3885         return HANDLER_FINISHED;
3886     }
3887     else if (0 != pb.depth) {
3888         http_status_set_error(r, 403);
3889         return HANDLER_FINISHED;
3890     }
3891 
3892     pb.proplist.ptr  = NULL;
3893     pb.proplist.used = 0;
3894     pb.proplist.size = 0;
3895 
3896   #ifdef USE_PROPPATCH
3897     xmlDocPtr xml = NULL;
3898     const xmlNode *rootnode = NULL;
3899     if (r->reqbody_length) {
3900         if (NULL == (xml = webdav_parse_chunkqueue(r, pconf))) {
3901             http_status_set_error(r, 400); /* Bad Request */
3902             return HANDLER_FINISHED;
3903         }
3904         rootnode = xmlDocGetRootElement(xml);
3905     }
3906 
3907     if (NULL != rootnode
3908         && 0 == webdav_xmlstrcmp_fixed(rootnode->name, "propfind")) {
3909         for (const xmlNode *cmd = rootnode->children; cmd; cmd = cmd->next) {
3910             if (0 == webdav_xmlstrcmp_fixed(cmd->name, "allprop"))
3911                 pb.allprop = pb.lockdiscovery = 1;
3912             else if (0 == webdav_xmlstrcmp_fixed(cmd->name, "propname"))
3913                 pb.propname = 1;
3914             else if (0 != webdav_xmlstrcmp_fixed(cmd->name, "prop")
3915                      && 0 != webdav_xmlstrcmp_fixed(cmd->name, "include"))
3916                 continue;
3917 
3918             /* "prop" or "include": get prop by name */
3919             for (const xmlNode *prop = cmd->children; prop; prop = prop->next) {
3920                 if (prop->type == XML_TEXT_NODE)
3921                     continue; /* ignore WS */
3922 
3923                 if (prop->ns && '\0' == *(char *)prop->ns->href
3924                              && '\0' != *(char *)prop->ns->prefix) {
3925                     log_error(r->conf.errh, __FILE__, __LINE__,
3926                               "no name space for: %s", prop->name);
3927                     /* 422 Unprocessable Entity */
3928                     http_status_set_error(r, 422);
3929                     free(pb.proplist.ptr);
3930                     xmlFreeDoc(xml);
3931                     return HANDLER_FINISHED;
3932                 }
3933 
3934                 /* add property to requested list */
3935                 if (pb.proplist.size == pb.proplist.used) {
3936                     if (pb.proplist.size == 32) {
3937                         /* arbitrarily chosen limit of 32 */
3938                         log_error(r->conf.errh, __FILE__, __LINE__,
3939                                   "too many properties in request (> 32)");
3940                         http_status_set_error(r, 400); /* Bad Request */
3941                         free(pb.proplist.ptr);
3942                         xmlFreeDoc(xml);
3943                         return HANDLER_FINISHED;
3944                     }
3945                     pb.proplist.ptr =
3946                       realloc(pb.proplist.ptr, sizeof(*(pb.proplist.ptr)) * 32);
3947                     force_assert(pb.proplist.ptr); /*(see above limit)*/
3948                 }
3949 
3950                 const size_t namelen = strlen((char *)prop->name);
3951                 if (prop->ns && 0 == strcmp((char *)prop->ns->href, "DAV:")) {
3952                     if (namelen == sizeof("lockdiscovery")-1
3953                         && 0 == memcmp(prop->name,
3954                                        CONST_STR_LEN("lockdiscovery"))) {
3955                         pb.lockdiscovery = 1;
3956                         continue;
3957                     }
3958                     const struct live_prop_list *list = live_properties;
3959                     while (0 != list->len
3960                            && (list->len != namelen
3961                                || 0 != memcmp(prop->name,list->prop,list->len)))
3962                         ++list;
3963                     if (NULL != list->prop) {
3964                         if (cmd->name[0] == 'p') { /* "prop", not "include" */
3965                             pb.proplist.ptr[pb.proplist.used].name = NULL;
3966                             pb.proplist.ptr[pb.proplist.used].namelen =
3967                               list->pnum;
3968                             pb.proplist.used++;
3969                         } /* (else skip; will already be part of allprop) */
3970                         continue;
3971                     }
3972                     if (cmd->name[0] == 'i') /* allprop "include", not "prop" */
3973                         continue; /*(all props in db returned with allprop)*/
3974                     /* dead props or props in "DAV:" ns not handed above */
3975                 }
3976 
3977                 /* save pointers directly into parsed xmlDoc
3978                  * Therefore, MUST NOT call xmlFreeDoc(xml)
3979                  * until also done with pb.proplist */
3980                 webdav_property_name * const propname =
3981                   pb.proplist.ptr + pb.proplist.used++;
3982                 if (prop->ns) {
3983                     propname->ns = (char *)prop->ns->href;
3984                     propname->nslen = strlen(propname->ns);
3985                 }
3986                 else {
3987                     propname->ns = "";
3988                     propname->nslen = 0;
3989                 }
3990                 propname->name = (char *)prop->name;
3991                 propname->namelen = namelen;
3992             }
3993         }
3994     }
3995   #endif
3996 
3997     if (NULL == pb.proplist.ptr && !pb.propname)
3998         pb.allprop = pb.lockdiscovery = 1;
3999 
4000     pb.r     = r;
4001     pb.pconf = pconf;
4002     pb.dst   = &r->physical;
4003     pb.b     = /*(optimization; buf extended as needed)*/
4004       chunkqueue_append_buffer_open_sz(&r->write_queue, 8192);
4005     pb.b_200 = chunk_buffer_acquire();
4006     pb.b_404 = chunk_buffer_acquire();
4007 
4008     webdav_xml_doctype(pb.b, r);
4009     buffer_append_string_len(pb.b, CONST_STR_LEN(
4010       "<D:multistatus xmlns:D=\"DAV:\" " MOD_WEBDAV_XMLNS_NS0 ">\n"));
4011 
4012     if (0 != pb.depth) /*(must be collection or else error returned above)*/
4013         webdav_propfind_dir(&pb);
4014     else
4015         webdav_propfind_resource(&pb);
4016 
4017     buffer_append_string_len(pb.b, CONST_STR_LEN(
4018       "</D:multistatus>\n"));
4019 
4020     if (pconf->log_xml)
4021         log_error(r->conf.errh, __FILE__, __LINE__, "XML-response-body: %.*s",
4022                   BUFFER_INTLEN_PTR(pb.b));
4023 
4024     chunkqueue_append_buffer_commit(&r->write_queue);
4025     http_status_set_fin(r, 207); /* Multi-status */
4026 
4027     chunk_buffer_release(pb.b_404);
4028     chunk_buffer_release(pb.b_200);
4029   #ifdef USE_PROPPATCH
4030     if (pb.proplist.ptr)
4031         free(pb.proplist.ptr);
4032     if (NULL != xml)
4033         xmlFreeDoc(xml);
4034   #endif
4035 
4036     return HANDLER_FINISHED;
4037 }
4038 
4039 
4040 static handler_t
4041 mod_webdav_mkcol (request_st * const r, const plugin_config * const pconf)
4042 {
4043     const int status = webdav_mkdir(pconf, &r->physical, -1);
4044     if (0 == status)
4045         http_status_set_fin(r, 201); /* Created */
4046     else
4047         http_status_set_error(r, status);
4048 
4049     return HANDLER_FINISHED;
4050 }
4051 
4052 
4053 static handler_t
4054 mod_webdav_delete (request_st * const r, const plugin_config * const pconf)
4055 {
4056     /* reject DELETE if original URI sent with fragment ('litmus' warning) */
4057     if (NULL != strchr(r->target_orig.ptr, '#')) {
4058         http_status_set_error(r, 403);
4059         return HANDLER_FINISHED;
4060     }
4061 
4062     struct stat st;
4063     if (-1 == lstat(r->physical.path.ptr, &st)) {
4064         http_status_set_error(r, (errno == ENOENT) ? 404 : 403);
4065         return HANDLER_FINISHED;
4066     }
4067 
4068     if (0 != webdav_if_match_or_unmodified_since(r, &st)) {
4069         http_status_set_error(r, 412); /* Precondition Failed */
4070         return HANDLER_FINISHED;
4071     }
4072 
4073     if (S_ISDIR(st.st_mode)) {
4074         if (r->physical.path.ptr[r->physical.path.used - 2] != '/') {
4075           #if 0 /*(issues warning for /usr/bin/litmus copymove test)*/
4076             http_response_redirect_to_directory(r, 308);
4077             return HANDLER_FINISHED; /* 308 Permanent Redirect */
4078             /* Alternatively, could append '/' to r->physical.path
4079              * and r->physical.rel_path, set Content-Location in
4080              * response headers, and continue to serve the request */
4081           #else
4082             buffer_append_string_len(&r->physical.path,    CONST_STR_LEN("/"));
4083             buffer_append_string_len(&r->physical.rel_path,CONST_STR_LEN("/"));
4084            #if 0 /*(Content-Location not very useful to client after DELETE)*/
4085             /*(? should it be target or target_orig ?)*/
4086             /*(should be url-encoded path)*/
4087             buffer_append_string_len(&r->target, CONST_STR_LEN("/"));
4088             http_header_response_set(r, HTTP_HEADER_CONTENT_LOCATION,
4089                                      CONST_STR_LEN("Content-Location"),
4090                                      CONST_BUF_LEN(&r->target));
4091            #endif
4092           #endif
4093         }
4094         /* require "infinity" if Depth request header provided */
4095         if (-1 != webdav_parse_Depth(r)) {
4096             /* [RFC4918] 9.6.1 DELETE for Collections
4097              *   The DELETE method on a collection MUST act as if a
4098              *   "Depth: infinity" header was used on it. A client MUST NOT
4099              *   submit a Depth header with a DELETE on a collection with any
4100              *   value but infinity.
4101              */
4102             http_status_set_error(r, 400); /* Bad Request */
4103             return HANDLER_FINISHED;
4104         }
4105 
4106         buffer * const ms = chunk_buffer_acquire(); /* multi-status */
4107 
4108         const int flags = (r->conf.force_lowercase_filenames)
4109           ? WEBDAV_FLAG_LC_NAMES
4110           : 0;
4111         if (0 == webdav_delete_dir(pconf, &r->physical, ms, flags)) {
4112             /* Note: this does not destroy locks if an error occurs,
4113              * which is not a problem if lock is only on the collection
4114              * being moved, but might need finer updates if there are
4115              * locks on internal elements that are successfully deleted */
4116             webdav_lock_delete_uri_col(pconf, &r->physical.rel_path);
4117             http_status_set_fin(r, 204); /* No Content */
4118         }
4119         else {
4120             webdav_xml_doc_multistatus(r, pconf, ms); /* 207 Multi-status */
4121         }
4122 
4123         chunk_buffer_release(ms);
4124         /* invalidate stat cache of src if DELETE, whether or not successful */
4125         stat_cache_delete_dir(CONST_BUF_LEN(&r->physical.path));
4126     }
4127     else if (r->physical.path.ptr[r->physical.path.used - 2] == '/')
4128         http_status_set_error(r, 403);
4129     else {
4130         const int status = webdav_delete_file(pconf, &r->physical);
4131         if (0 == status) {
4132             webdav_lock_delete_uri(pconf, &r->physical.rel_path);
4133             http_status_set_fin(r, 204); /* No Content */
4134         }
4135         else
4136             http_status_set_error(r, status);
4137     }
4138 
4139     return HANDLER_FINISHED;
4140 }
4141 
4142 
4143 static ssize_t
4144 mod_webdav_write_cq_first_chunk (request_st * const r, chunkqueue * const cq,
4145                                  const int fd)
4146 {
4147     /* (Note: copying might take some time, temporarily pausing server) */
4148     chunk *c = cq->first;
4149     ssize_t wr = 0;
4150 
4151     switch(c->type) {
4152     case FILE_CHUNK:
4153         if (NULL != webdav_mmap_file_chunk(c)) {
4154             do {
4155                 wr = write(fd,
4156                            c->file.mmap.start + c->offset - c->file.mmap.offset,
4157                            c->file.length - c->offset);
4158             } while (-1 == wr && errno == EINTR);
4159             break;
4160         }
4161         else {
4162             switch (errno) {
4163               case ENOSYS: case ENODEV: case EINVAL: break;
4164               default:
4165                 log_perror(r->conf.errh, __FILE__, __LINE__,
4166                            "open() or mmap() '%*.s'",
4167                            BUFFER_INTLEN_PTR(c->mem));
4168             }
4169 
4170             if (webdav_open_chunk_file_rd(c) < 0) {
4171                 http_status_set_error(r, 500); /* Internal Server Error */
4172                 return -1;
4173             }
4174             ssize_t rd = -1;
4175             char buf[16384];
4176             do {
4177                 if (-1 == lseek(c->file.fd, c->offset, SEEK_SET))
4178                     break;
4179                 off_t len = c->file.length - c->offset;
4180                 if (len > (off_t)sizeof(buf)) len = (off_t)sizeof(buf);
4181                 rd = read(c->file.fd, buf, (size_t)len);
4182             } while (-1 == rd && errno == EINTR);
4183             if (rd >= 0) {
4184                 do {
4185                     wr = write(fd, buf, (size_t)rd);
4186                 } while (-1 == wr && errno == EINTR);
4187                 break;
4188             }
4189             else {
4190                 log_perror(r->conf.errh, __FILE__, __LINE__,
4191                            "read() '%*.s'",
4192                            BUFFER_INTLEN_PTR(c->mem));
4193                 http_status_set_error(r, 500); /* Internal Server Error */
4194                 return -1;
4195             }
4196         }
4197     case MEM_CHUNK:
4198         do {
4199             wr = write(fd, c->mem->ptr + c->offset,
4200                        buffer_string_length(c->mem) - c->offset);
4201         } while (-1 == wr && errno == EINTR);
4202         break;
4203     }
4204 
4205     if (wr > 0) {
4206         chunkqueue_mark_written(cq, wr);
4207     }
4208     else if (wr < 0)
4209         http_status_set_error(r, (errno == ENOSPC) ? 507 : 403);
4210 
4211     return wr;
4212 }
4213 
4214 
4215 __attribute_noinline__
4216 static int
4217 mod_webdav_write_cq (request_st * const r, chunkqueue * const cq, const int fd)
4218 {
4219     chunkqueue_remove_finished_chunks(cq);
4220     while (!chunkqueue_is_empty(cq)) {
4221         if (mod_webdav_write_cq_first_chunk(r, cq, fd) < 0) return 0;
4222     }
4223     return 1;
4224 }
4225 
4226 
4227 #if (defined(__linux__) || defined(__CYGWIN__)) && defined(O_TMPFILE)
4228 static int
4229 mod_webdav_write_single_file_chunk (request_st * const r, chunkqueue * const cq)
4230 {
4231     /* cq might have mem chunks after initial tempfile chunk
4232      * due to chunkqueue_steal() if request body is small */
4233     /*assert(cq->first->type == FILE_CHUNK);*/
4234     /*assert(cq->first->next != NULL);*/
4235     chunk * const c = cq->first;
4236     cq->first = c->next;
4237     const off_t len = chunkqueue_length(cq);
4238     const off_t bytes_out = cq->bytes_out;
4239     if (mod_webdav_write_cq(r, cq, c->file.fd)) {
4240         /*assert(cq->first == NULL);*/
4241         /* chunks merged; chunkqueue length did not change,
4242          * so restore cq->bytes_out instead of chunkqueue_file_update() */
4243         cq->bytes_out = bytes_out;
4244         c->file.length = len;
4245         c->next = NULL;
4246         cq->first = cq->last = c;
4247         return 1;
4248     }
4249     else {
4250         /*assert(cq->first != NULL);*/
4251         c->next = cq->first;
4252         cq->first = c;
4253         return 0;
4254     }
4255 }
4256 #endif
4257 
4258 
4259 static handler_t
4260 mod_webdav_put_0 (request_st * const r, const plugin_config * const pconf)
4261 {
4262     if (0 != webdav_if_match_or_unmodified_since(r, NULL)) {
4263         http_status_set_error(r, 412); /* Precondition Failed */
4264         return HANDLER_FINISHED;
4265     }
4266 
4267     /* special-case PUT 0-length file */
4268     int fd;
4269     fd = fdevent_open_cloexec(r->physical.path.ptr, 0,
4270                               O_WRONLY | O_CREAT | O_EXCL | O_TRUNC,
4271                               WEBDAV_FILE_MODE);
4272     if (fd >= 0) {
4273         if (0 != r->conf.etag_flags) {
4274             /*(skip sending etag if fstat() error; not expected)*/
4275             struct stat st;
4276             if (0 == fstat(fd, &st)) webdav_response_etag(r, &st);
4277         }
4278         close(fd);
4279         webdav_parent_modified(&r->physical.path);
4280         http_status_set_fin(r, 201); /* Created */
4281         return HANDLER_FINISHED;
4282     }
4283     else if (errno == EISDIR) {
4284         http_status_set_error(r, 405); /* Method Not Allowed */
4285         return HANDLER_FINISHED;
4286     }
4287 
4288     if (errno == ELOOP)
4289         webdav_delete_file(pconf, &r->physical); /*(ignore result)*/
4290         /*(attempt unlink(); target might be symlink
4291          * and above O_NOFOLLOW resulted in ELOOP)*/
4292 
4293     fd = fdevent_open_cloexec(r->physical.path.ptr, 0,
4294                               O_WRONLY | O_CREAT | O_TRUNC,
4295                               WEBDAV_FILE_MODE);
4296     if (fd >= 0) {
4297         close(fd);
4298         http_status_set_fin(r, 204); /* No Content */
4299         return HANDLER_FINISHED;
4300     }
4301 
4302     http_status_set_error(r, 500); /* Internal Server Error */
4303     return HANDLER_FINISHED;
4304 }
4305 
4306 
4307 static handler_t
4308 mod_webdav_put_prep (request_st * const r, const plugin_config * const pconf)
4309 {
4310     if (light_btst(r->rqst_htags, HTTP_HEADER_CONTENT_RANGE)) {
4311         if (pconf->opts & MOD_WEBDAV_UNSAFE_PARTIAL_PUT_COMPAT)
4312             return HANDLER_GO_ON;
4313         /* [RFC7231] 4.3.4 PUT
4314          *   An origin server that allows PUT on a given target resource MUST
4315          *   send a 400 (Bad Request) response to a PUT request that contains a
4316          *   Content-Range header field (Section 4.2 of [RFC7233]), since the
4317          *   payload is likely to be partial content that has been mistakenly
4318          *   PUT as a full representation.
4319          */
4320         http_status_set_error(r, 400); /* Bad Request */
4321         return HANDLER_FINISHED;
4322     }
4323 
4324     const uint32_t used = r->physical.path.used;
4325     char *slash = r->physical.path.ptr + used - 2;
4326     if (*slash == '/') { /* disallow PUT on a collection (path ends in '/') */
4327         http_status_set_error(r, 400); /* Bad Request */
4328         return HANDLER_FINISHED;
4329     }
4330 
4331     /* special-case PUT 0-length file */
4332     if (0 == r->reqbody_length)
4333         return mod_webdav_put_0(r, pconf);
4334 
4335     /* Create temporary file in target directory (to store reqbody as received)
4336      * Temporary file is unlinked so that if receiving reqbody fails,
4337      * temp file is automatically cleaned up when fd is closed.
4338      * While being received, temporary file is not part of directory listings.
4339      * While this might result in extra copying, it is simple and robust. */
4340     int fd;
4341     size_t len = buffer_string_length(&r->physical.path);
4342   #if (defined(__linux__) || defined(__CYGWIN__)) && defined(O_TMPFILE)
4343     slash = memrchr(r->physical.path.ptr, '/', len);
4344     if (slash == r->physical.path.ptr) slash = NULL;
4345     if (slash) *slash = '\0';
4346     fd = fdevent_open_cloexec(r->physical.path.ptr, 1,
4347                               O_RDWR | O_TMPFILE | O_APPEND, WEBDAV_FILE_MODE);
4348     if (slash) *slash = '/';
4349     if (fd < 0)
4350   #endif
4351     {
4352         buffer_append_string_len(&r->physical.path, CONST_STR_LEN("-XXXXXX"));
4353         fd = fdevent_mkstemp_append(r->physical.path.ptr);
4354         if (fd >= 0) unlink(r->physical.path.ptr);
4355         buffer_string_set_length(&r->physical.path, len);
4356     }
4357     if (fd < 0) {
4358         http_status_set_error(r, 500); /* Internal Server Error */
4359         return HANDLER_FINISHED;
4360     }
4361 
4362     /* copy all chunks even though expecting (at most) single MEM_CHUNK chunk
4363      * (still, loop on partial writes)
4364      * (Note: copying might take some time, temporarily pausing server)
4365      * (error status is set if error occurs) */
4366     chunkqueue * const cq = &r->reqbody_queue;
4367     off_t cqlen = chunkqueue_length(cq);
4368     if (!mod_webdav_write_cq(r, cq, fd)) {
4369         close(fd);
4370         return HANDLER_FINISHED;
4371     }
4372 
4373     chunkqueue_reset(cq);
4374     if (0 != cqlen)  /*(r->physical.path copied, then c->mem cleared below)*/
4375         chunkqueue_append_file_fd(cq, &r->physical.path, fd, 0, cqlen);
4376     else {
4377         /*(must be non-zero for fd to be appended, then reset to 0-length)*/
4378         chunkqueue_append_file_fd(cq, &r->physical.path, fd, 0, 1);
4379         cq->last->file.length = 0;
4380         cq->bytes_in = 0;
4381     }
4382   #ifdef __COVERITY__
4383     /* chunkqueue_append_file_fd() does not update cq->last when 0 == cqlen,
4384      * and that is handled above, so cq->last is never NULL here */
4385     force_assert(cq->last);
4386   #endif
4387     buffer_clear(cq->last->mem); /* file already unlink()ed */
4388     chunkqueue_set_tempdirs(cq, cq->tempdirs, INTMAX_MAX);
4389     /* force huge cq->upload_temp_file_size since chunkqueue_set_tempdirs()
4390      * might truncate upload_temp_file_size to chunk.c:MAX_TEMPFILE_SIZE */
4391     cq->upload_temp_file_size = INTMAX_MAX;
4392     cq->last->file.is_temp = 1;
4393 
4394     return HANDLER_GO_ON;
4395 }
4396 
4397 
4398 #if (defined(__linux__) || defined(__CYGWIN__)) && defined(O_TMPFILE)
4399 static int
4400 mod_webdav_put_linkat_rename (request_st * const r,
4401                               const char * const pathtemp)
4402 {
4403     if (!has_proc_self_fd) return 0;
4404     chunkqueue * const cq = &r->reqbody_queue;
4405     chunk *c = cq->first;
4406 
4407     char pathproc[32] = "/proc/self/fd/";
4408     size_t plen =
4409       li_itostrn(pathproc+sizeof("/proc/self/fd/")-1,
4410                  sizeof(pathproc)-(sizeof("/proc/self/fd/")-1), c->file.fd);
4411     pathproc[sizeof("/proc/self/fd/")-1+plen] = '\0';
4412     if (0 == linkat(AT_FDCWD, pathproc, AT_FDCWD, pathtemp, AT_SYMLINK_FOLLOW)){
4413         struct stat st;
4414       #ifdef RENAME_NOREPLACE /*(renameat2() not well-supported yet)*/
4415         if (0 == renameat2(AT_FDCWD, pathtemp,
4416                            AT_FDCWD, r->physical.path.ptr, RENAME_NOREPLACE))
4417             http_status_set_fin(r, 201); /* Created */
4418         else if (0 == rename(pathtemp, r->physical.path.ptr))
4419             http_status_set_fin(r, 204); /* No Content */ /*(replaced)*/
4420         else
4421       #else
4422         http_status_set_fin(r, 0 == lstat(r->physical.path.ptr, &st)
4423                                ? 204   /* No Content */
4424                                : 201); /* Created */
4425         if (201 == http_status_get(r))
4426             webdav_parent_modified(&r->physical.path);
4427         if (0 != rename(pathtemp, r->physical.path.ptr))
4428       #endif
4429         {
4430             if (errno == EISDIR)
4431                 http_status_set_error(r, 405); /* Method Not Allowed */
4432             else
4433                 http_status_set_error(r, 403); /* Forbidden */
4434             unlink(pathtemp);
4435         }
4436 
4437         if (0 != r->conf.etag_flags
4438             && http_status_get(r) < 300) { /*(201, 204)*/
4439             /*(skip sending etag if fstat() error; not expected)*/
4440             if (0 == fstat(c->file.fd, &st))
4441                 webdav_response_etag(r, &st);
4442         }
4443 
4444         chunkqueue_mark_written(cq, c->file.length); /*(c->offset == 0)*/
4445         return 1;
4446     }
4447 
4448     return 0;
4449 }
4450 #endif
4451 
4452 
4453 __attribute_cold__
4454 static handler_t
4455 mod_webdav_put_deprecated_unsafe_partial_put_compat (request_st * const r,
4456                                                      const buffer * const h)
4457 {
4458     /* historical code performed very limited range parse (repeated here) */
4459     /* we only support <num>- ... */
4460     const char *num = h->ptr;
4461     off_t offset;
4462     char *err;
4463     if (0 != strncmp(num, "bytes ", sizeof("bytes ")-1)) {
4464         http_status_set_error(r, 501); /* Not Implemented */
4465         return HANDLER_FINISHED;
4466     }
4467     num += sizeof("bytes ")-1; /* +6 for "bytes " */
4468     offset = strtoll(num, &err, 10); /*(strtoll() ignores leading whitespace)*/
4469     if (num == err || *err != '-' || offset < 0) {
4470         http_status_set_error(r, 501); /* Not Implemented */
4471         return HANDLER_FINISHED;
4472     }
4473 
4474     const int fd = fdevent_open_cloexec(r->physical.path.ptr, 0,
4475                                         O_WRONLY, WEBDAV_FILE_MODE);
4476     if (fd < 0) {
4477         http_status_set_error(r, (errno == ENOENT) ? 404 : 403);
4478         return HANDLER_FINISHED;
4479     }
4480 
4481   #ifdef HAVE_COPY_FILE_RANGE
4482     /* use Linux copy_file_range() if available
4483      * (Linux 4.5, but glibc 2.27 provides a user-space emulation)
4484      * fd_in and fd_out must be on same mount (handled in mod_webdav_put_prep())
4485      * check that reqbody is contained in single tempfile and open fd (expected)
4486      * (Note: copying might take some time, temporarily pausing server)
4487      */
4488     chunkqueue * const cq = &r->reqbody_queue;
4489     chunk *c = cq->first;
4490     off_t cqlen = chunkqueue_length(cq);
4491     if (c->type == FILE_CHUNK && NULL == c->next && c->file.fd >= 0) {
4492         loff_t zoff = 0;
4493         loff_t ooff = offset;
4494         ssize_t wr;
4495         do {
4496             wr = copy_file_range(c->file.fd,&zoff,fd,&ooff,(size_t)cqlen, 0);
4497         } while (wr >= 0 && (cqlen -= wr));
4498     }
4499     if (0 != cqlen) /* fallback, retry if copy_file_range() did not finish */
4500   #endif
4501   {
4502     if (-1 == lseek(fd, offset, SEEK_SET)) {
4503         close(fd);
4504         http_status_set_error(r, 500); /* Internal Server Error */
4505         return HANDLER_FINISHED;
4506     }
4507 
4508     /* copy all chunks even though expecting single chunk
4509      * (still, loop on partial writes)
4510      * (Note: copying might take some time, temporarily pausing server)
4511      * (error status is set if error occurs) */
4512     mod_webdav_write_cq(r, &r->reqbody_queue, fd);
4513   }
4514 
4515     struct stat st;
4516     if (0 != r->conf.etag_flags && !http_status_is_set(r)) {
4517         /*(skip sending etag if fstat() error; not expected)*/
4518         if (0 != fstat(fd, &st)) r->conf.etag_flags = 0;
4519     }
4520 
4521     const int wc = close(fd);
4522     if (0 != wc && !http_status_is_set(r))
4523         http_status_set_error(r, (errno == ENOSPC) ? 507 : 403);
4524 
4525     if (!http_status_is_set(r)) {
4526         http_status_set_fin(r, 204); /* No Content */
4527         if (0 != r->conf.etag_flags) webdav_response_etag(r, &st);
4528     }
4529 
4530     return HANDLER_FINISHED;
4531 }
4532 
4533 
4534 static handler_t
4535 mod_webdav_put (request_st * const r, const plugin_config * const pconf)
4536 {
4537     if (r->state == CON_STATE_READ_POST) {
4538         int first_read = chunkqueue_is_empty(&r->reqbody_queue);
4539         handler_t rc = r->con->reqbody_read(r);
4540         if (rc != HANDLER_GO_ON) {
4541             if (first_read && rc == HANDLER_WAIT_FOR_EVENT
4542                 && 0 != webdav_if_match_or_unmodified_since(r, NULL)) {
4543                 http_status_set_error(r, 412); /* Precondition Failed */
4544                 return HANDLER_FINISHED;
4545             }
4546             return rc;
4547         }
4548     }
4549 
4550     if (0 != webdav_if_match_or_unmodified_since(r, NULL)) {
4551         http_status_set_error(r, 412); /* Precondition Failed */
4552         return HANDLER_FINISHED;
4553     }
4554 
4555     if (pconf->opts & MOD_WEBDAV_UNSAFE_PARTIAL_PUT_COMPAT) {
4556         const buffer * const h =
4557           http_header_request_get(r, HTTP_HEADER_CONTENT_RANGE,
4558                                   CONST_STR_LEN("Content-Range"));
4559         if (NULL != h)
4560             return
4561               mod_webdav_put_deprecated_unsafe_partial_put_compat(r, h);
4562     }
4563 
4564     /* construct temporary filename in same directory as target
4565      * (expect cq contains exactly one chunk:
4566      *    the temporary FILE_CHUNK created in mod_webdav_put_prep())
4567      * (do not reuse c->mem buffer; if tmpfile was unlinked, c->mem is blank)
4568      * (since temporary file was unlinked, no guarantee of unique name,
4569      *  so add pid and fd to avoid conflict with an unlikely parallel
4570      *  PUT request being handled by same server pid (presumably by
4571      *  same client using same lock token)) */
4572     chunkqueue * const cq = &r->reqbody_queue;
4573     chunk *c = cq->first;
4574 
4575     /* future: might support client specifying getcontenttype property
4576      * using Content-Type request header.  However, [RFC4918] 9.7.1 notes:
4577      *   Many servers do not allow configuring the Content-Type on a
4578      *   per-resource basis in the first place. Thus, clients can't always
4579      *   rely on the ability to directly influence the content type by
4580      *   including a Content-Type request header
4581      */
4582 
4583     /*(similar to beginning of webdav_linktmp_rename())*/
4584     buffer * const tmpb = pconf->tmpb;
4585     buffer_copy_buffer(tmpb, &r->physical.path);
4586     buffer_append_string_len(tmpb, CONST_STR_LEN("."));
4587     buffer_append_int(tmpb, (long)getpid());
4588     buffer_append_string_len(tmpb, CONST_STR_LEN("."));
4589     if (c->type == MEM_CHUNK)
4590         buffer_append_uint_hex_lc(tmpb, (uintptr_t)pconf); /*(stack/heap addr)*/
4591     else
4592         buffer_append_int(tmpb, (long)c->file.fd);
4593     buffer_append_string_len(tmpb, CONST_STR_LEN("~"));
4594 
4595     if (buffer_string_length(tmpb) >= PATH_MAX) { /*(temp file path too long)*/
4596         http_status_set_error(r, 500); /* Internal Server Error */
4597         return HANDLER_FINISHED;
4598     }
4599 
4600     const char *pathtemp = tmpb->ptr;
4601 
4602   #if (defined(__linux__) || defined(__CYGWIN__)) && defined(O_TMPFILE)
4603     if (c->type == FILE_CHUNK) { /*(reqbody contained in single tempfile)*/
4604         if (NULL != c->next) {
4605             /* if request body <= 64k, in-memory chunks might have been
4606              * moved to cq instead of appended to first chunk FILE_CHUNK */
4607             if (!mod_webdav_write_single_file_chunk(r, cq))
4608                 return HANDLER_FINISHED;
4609         }
4610         if (mod_webdav_put_linkat_rename(r, pathtemp))
4611             return HANDLER_FINISHED;
4612         /* attempt traditional copy (below) if linkat() failed for any reason */
4613     }
4614   #endif
4615 
4616     const int fd = fdevent_open_cloexec(pathtemp, 0,
4617                                         O_WRONLY | O_CREAT | O_EXCL | O_TRUNC,
4618                                         WEBDAV_FILE_MODE);
4619     if (fd < 0) {
4620         http_status_set_error(r, 500); /* Internal Server Error */
4621         return HANDLER_FINISHED;
4622     }
4623 
4624     /* copy all chunks even though expecting single chunk
4625      * (still, loop on partial writes)
4626      * (Note: copying might take some time, temporarily pausing server)
4627      * (error status is set if error occurs) */
4628     mod_webdav_write_cq(r, cq, fd);
4629 
4630     struct stat st;
4631     if (0 != r->conf.etag_flags && !http_status_is_set(r)) {
4632         /*(skip sending etag if fstat() error; not expected)*/
4633         if (0 != fstat(fd, &st)) r->conf.etag_flags = 0;
4634     }
4635 
4636     const int wc = close(fd);
4637     if (0 != wc && !http_status_is_set(r))
4638         http_status_set_error(r, (errno == ENOSPC) ? 507 : 403);
4639 
4640     if (!http_status_is_set(r)) {
4641         struct stat ste;
4642         http_status_set_fin(r, 0 == lstat(r->physical.path.ptr, &ste)
4643                                ? 204   /* No Content */
4644                                : 201); /* Created */
4645         if (201 == http_status_get(r))
4646             webdav_parent_modified(&r->physical.path);
4647         if (0 == rename(pathtemp, r->physical.path.ptr)) {
4648             if (0 != r->conf.etag_flags) webdav_response_etag(r, &st);
4649         }
4650         else {
4651             if (errno == EISDIR)
4652                 http_status_set_error(r, 405); /* Method Not Allowed */
4653             else
4654                 http_status_set_error(r, 500); /* Internal Server Error */
4655             unlink(pathtemp);
4656         }
4657     }
4658     else
4659         unlink(pathtemp);
4660 
4661     return HANDLER_FINISHED;
4662 }
4663 
4664 
4665 static handler_t
4666 mod_webdav_copymove_b (request_st * const r, const plugin_config * const pconf, physical_st * const dst)
4667 {
4668     buffer * const dst_path = &dst->path;
4669     buffer * const dst_rel_path = &dst->rel_path;
4670 
4671     int flags = WEBDAV_FLAG_OVERWRITE /*(default)*/
4672               | (r->conf.force_lowercase_filenames
4673                   ? WEBDAV_FLAG_LC_NAMES
4674                   : 0)
4675               | (r->http_method == HTTP_METHOD_MOVE
4676                   ? WEBDAV_FLAG_MOVE_RENAME
4677                   : WEBDAV_FLAG_COPY_LINK);
4678 
4679     const buffer * const h =
4680       http_header_request_get(r,HTTP_HEADER_OTHER,CONST_STR_LEN("Overwrite"));
4681     if (NULL != h) {
4682         if (h->used != 2
4683             || ((h->ptr[0] & 0xdf) != 'F' && (h->ptr[0] & 0xdf) != 'T'))  {
4684             http_status_set_error(r, 400); /* Bad Request */
4685             return HANDLER_FINISHED;
4686         }
4687         if ((h->ptr[0] & 0xdf) == 'F')
4688             flags &= ~WEBDAV_FLAG_OVERWRITE;
4689     }
4690 
4691     /* parse Destination
4692      *
4693      * http://127.0.0.1:1025/dav/litmus/copydest
4694      *
4695      * - host has to match Host: header in request
4696      *   (or else would need to check that Destination is reachable from server
4697      *    and authentication credentials grant privileges on Destination)
4698      * - query string on Destination, if present, is discarded
4699      *
4700      * NOTE: Destination path is relative to document root and IS NOT re-run
4701      * through other modules on server (such as aliasing or rewrite or userdir)
4702      */
4703     const buffer * const destination =
4704       http_header_request_get(r,HTTP_HEADER_OTHER,CONST_STR_LEN("Destination"));
4705     if (NULL == destination) {
4706         http_status_set_error(r, 400); /* Bad Request */
4707         return HANDLER_FINISHED;
4708     }
4709   #ifdef __COVERITY__
4710     force_assert(2 <= destination->used);
4711   #endif
4712 
4713     const char *sep = destination->ptr, *start;
4714     if (*sep != '/') { /* path-absolute or absolute-URI form */
4715         start = sep;
4716         sep = start + buffer_string_length(&r->uri.scheme);
4717         if (0 != strncmp(start, r->uri.scheme.ptr, sep - start)
4718             || sep[0] != ':' || sep[1] != '/' || sep[2] != '/') {
4719             http_status_set_error(r, 400); /* Bad Request */
4720             return HANDLER_FINISHED;
4721         }
4722         start = sep + 3;
4723 
4724         if (NULL == (sep = strchr(start, '/'))) {
4725             http_status_set_error(r, 400); /* Bad Request */
4726             return HANDLER_FINISHED;
4727         }
4728         if (!buffer_is_equal_string(&r->uri.authority, start, sep - start)
4729                /* skip login info (even though it should not be present) */
4730             && (NULL == (start = (char *)memchr(start, '@', sep - start))
4731                 || (++start, !buffer_is_equal_string(&r->uri.authority,
4732                                                      start, sep - start)))) {
4733             /* not the same host */
4734             http_status_set_error(r, 502); /* Bad Gateway */
4735             return HANDLER_FINISHED;
4736         }
4737     }
4738     start = sep; /* starts with '/' */
4739 
4740     /* destination: remove query string, urldecode, path_simplify
4741      * and (maybe) lowercase for consistent destination URI path */
4742     buffer_copy_string_len(dst_rel_path, start,
4743                            NULL == (sep = strchr(start, '?'))
4744                              ? destination->ptr + destination->used-1 - start
4745                              : sep - start);
4746     if (buffer_string_length(dst_rel_path) >= PATH_MAX) {
4747         http_status_set_error(r, 403); /* Forbidden */
4748         return HANDLER_FINISHED;
4749     }
4750     buffer_urldecode_path(dst_rel_path);
4751     if (!buffer_is_valid_UTF8(dst_rel_path)) {
4752         /* invalid UTF-8 after url-decode */
4753         http_status_set_error(r, 400);
4754         return HANDLER_FINISHED;
4755     }
4756     buffer_path_simplify(dst_rel_path, dst_rel_path);
4757     if (buffer_string_is_empty(dst_rel_path) || dst_rel_path->ptr[0] != '/') {
4758         http_status_set_error(r, 400);
4759         return HANDLER_FINISHED;
4760     }
4761 
4762     if (flags & WEBDAV_FLAG_LC_NAMES)
4763         buffer_to_lower(dst_rel_path);
4764 
4765     /* Destination physical path
4766      * src r->physical.path might have been remapped with mod_alias.
4767      *   (but mod_alias does not modify r->physical.rel_path)
4768      * Find matching prefix to support use of mod_alias to remap webdav root.
4769      * Aliasing of paths underneath the webdav root might not work.
4770      * Likewise, mod_rewrite URL rewriting might thwart this comparison.
4771      * Use mod_redirect instead of mod_alias to remap paths *under* webdav root.
4772      * Use mod_redirect instead of mod_rewrite on *any* parts of path to webdav.
4773      * (Related, use mod_auth to protect webdav root, but avoid attempting to
4774      *  use mod_auth on paths underneath webdav root, as Destination is not
4775      *  validated with mod_auth)
4776      *
4777      * tl;dr: webdav paths and webdav properties are managed by mod_webdav,
4778      *        so do not modify paths externally or else undefined behavior
4779      *        or corruption may occur
4780      *
4781      * find matching URI prefix (lowercased if WEBDAV_FLAG_LC_NAMES)
4782      * (r->physical.rel_path and dst_rel_path will always match leading '/')
4783      * check if remaining r->physical.rel_path matches suffix of
4784      *   r->physical.path so that we can use the prefix to remap
4785      *   Destination physical path */
4786   #ifdef __COVERITY__
4787     force_assert(0 != r->physical.rel_path.used);
4788   #endif
4789     uint32_t i, remain;
4790     {
4791         const char * const p1 = r->physical.rel_path.ptr;
4792         const char * const p2 = dst_rel_path->ptr;
4793         for (i = 0; p1[i] && p1[i] == p2[i]; ++i) ;
4794         while (i != 0 && p1[--i] != '/') ; /* find matching directory path */
4795     }
4796     remain = r->physical.rel_path.used - 1 - i;
4797     if (r->physical.path.used - 1 <= remain) { /*(should not happen)*/
4798         http_status_set_error(r, 403); /* Forbidden */
4799         return HANDLER_FINISHED;
4800     }
4801     if (0 == memcmp(r->physical.rel_path.ptr+i, /*(suffix match)*/
4802                     r->physical.path.ptr + r->physical.path.used-1-remain,
4803                     remain)) { /*(suffix match)*/
4804       #ifdef __COVERITY__
4805         force_assert(2 <= dst_rel_path->used);
4806       #endif
4807         buffer_copy_string_len(dst_path, r->physical.path.ptr,
4808                                r->physical.path.used - 1 - remain);
4809         buffer_append_string_len(dst_path,
4810                                  dst_rel_path->ptr+i,
4811                                  dst_rel_path->used - 1 - i);
4812         if (buffer_string_length(dst_path) >= PATH_MAX) {
4813             http_status_set_error(r, 403); /* Forbidden */
4814             return HANDLER_FINISHED;
4815         }
4816     }
4817     else { /*(not expected; some other module mucked with path or rel_path)*/
4818         /* unable to perform physical path remap here;
4819          * assume doc_root/rel_path and no remapping */
4820       #ifdef __COVERITY__
4821         force_assert(2 <= r->physical.doc_root.used);
4822         force_assert(2 <= dst_path->used);
4823       #endif
4824         buffer_copy_buffer(dst_path, &r->physical.doc_root);
4825         if (dst_path->ptr[dst_path->used-2] == '/')
4826             --dst_path->used; /* since dst_rel_path begins with '/' */
4827         buffer_append_string_buffer(dst_path, dst_rel_path);
4828         if (buffer_string_length(dst_rel_path) >= PATH_MAX) {
4829             http_status_set_error(r, 403); /* Forbidden */
4830             return HANDLER_FINISHED;
4831         }
4832     }
4833 
4834     if (r->physical.path.used <= dst_path->used
4835         && 0 == memcmp(r->physical.path.ptr, dst_path->ptr,
4836                        r->physical.path.used-1)
4837         && (r->physical.path.ptr[r->physical.path.used-2] == '/'
4838             || dst_path->ptr[r->physical.path.used-1] == '/'
4839             || dst_path->ptr[r->physical.path.used-1] == '\0')) {
4840         /* dst must not be nested under (or same as) src */
4841         http_status_set_error(r, 403); /* Forbidden */
4842         return HANDLER_FINISHED;
4843     }
4844 
4845     struct stat st;
4846     if (-1 == lstat(r->physical.path.ptr, &st)) {
4847         /* don't known about it yet, unlink will fail too */
4848         http_status_set_error(r, (errno == ENOENT) ? 404 : 403);
4849         return HANDLER_FINISHED;
4850     }
4851 
4852     if (0 != webdav_if_match_or_unmodified_since(r, &st)) {
4853         http_status_set_error(r, 412); /* Precondition Failed */
4854         return HANDLER_FINISHED;
4855     }
4856 
4857     if (S_ISDIR(st.st_mode)) {
4858         if (r->physical.path.ptr[r->physical.path.used - 2] != '/') {
4859             http_response_redirect_to_directory(r, 308);
4860             return HANDLER_FINISHED; /* 308 Permanent Redirect */
4861             /* Alternatively, could append '/' to r->physical.path
4862              * and r->physical.rel_path, set Content-Location in
4863              * response headers, and continue to serve the request. */
4864         }
4865 
4866         /* ensure Destination paths end with '/' since dst is a collection */
4867       #ifdef __COVERITY__
4868         force_assert(2 <= dst_rel_path->used);
4869       #endif
4870         if (dst_rel_path->ptr[dst_rel_path->used - 2] != '/') {
4871             buffer_append_slash(dst_rel_path);
4872             buffer_append_slash(dst_path);
4873         }
4874 
4875         /* check for lock on destination (after ensuring dst ends in '/') */
4876         if (!webdav_has_lock(r, pconf, dst_rel_path))
4877             return HANDLER_FINISHED; /* 423 Locked */
4878 
4879         const int depth = webdav_parse_Depth(r);
4880         if (1 == depth) {
4881             http_status_set_error(r, 400); /* Bad Request */
4882             return HANDLER_FINISHED;
4883         }
4884         if (0 == depth) {
4885             if (r->http_method == HTTP_METHOD_MOVE) {
4886                 http_status_set_error(r, 400); /* Bad Request */
4887                 return HANDLER_FINISHED;
4888             }
4889             /* optionally create collection, then copy properties */
4890             int status;
4891             if (0 == lstat(dst_path->ptr, &st)) {
4892                 if (S_ISDIR(st.st_mode))
4893                     status = 204; /* No Content */
4894                 else if (flags & WEBDAV_FLAG_OVERWRITE) {
4895                     status = webdav_mkdir(pconf, dst, 1);
4896                     if (0 == status) status = 204; /* No Content */
4897                 }
4898                 else
4899                     status = 412; /* Precondition Failed */
4900             }
4901             else if (errno == ENOENT) {
4902                 status = webdav_mkdir(pconf, dst,
4903                                       !!(flags & WEBDAV_FLAG_OVERWRITE));
4904                 if (0 == status) status = 201; /* Created */
4905             }
4906             else
4907                 status = 403; /* Forbidden */
4908             if (status < 300) {
4909                 http_status_set_fin(r, status);
4910                 webdav_prop_copy_uri(pconf, &r->physical.rel_path,
4911                                             dst_rel_path);
4912             }
4913             else
4914                 http_status_set_error(r, status);
4915             return HANDLER_FINISHED;
4916         }
4917 
4918         /* ensure destination is not nested in source */
4919         if (dst_rel_path->ptr[dst_rel_path->used - 2] != '/') {
4920             buffer_append_slash(dst_rel_path);
4921             buffer_append_slash(dst_path);
4922         }
4923 
4924         buffer * const ms = chunk_buffer_acquire(); /* multi-status */
4925         if (0 == webdav_copymove_dir(pconf, &r->physical, dst, ms, flags)) {
4926             if (r->http_method == HTTP_METHOD_MOVE)
4927                 webdav_lock_delete_uri_col(pconf, &r->physical.rel_path);
4928             /*(requiring lock on destination requires MKCOL create dst first)
4929              *(if no lock support, return 200 OK unconditionally
4930              * instead of 200 OK or 201 Created; not fully RFC-conformant)*/
4931             http_status_set_fin(r, 200); /* OK */
4932         }
4933         else {
4934             /* Note: this does not destroy any locks if any error occurs,
4935              * which is not a problem if lock is only on the collection
4936              * being moved, but might need finer updates if there are
4937              * locks on internal elements that are successfully moved */
4938             webdav_xml_doc_multistatus(r, pconf, ms); /* 207 Multi-status */
4939         }
4940         chunk_buffer_release(ms);
4941         /* invalidate stat cache of src if MOVE, whether or not successful */
4942         if (r->http_method == HTTP_METHOD_MOVE)
4943             stat_cache_delete_dir(CONST_BUF_LEN(&r->physical.path));
4944         return HANDLER_FINISHED;
4945     }
4946     else if (r->physical.path.ptr[r->physical.path.used - 2] == '/') {
4947         http_status_set_error(r, 403); /* Forbidden */
4948         return HANDLER_FINISHED;
4949     }
4950     else {
4951         /* check if client has lock for destination
4952          * Note: requiring a lock on non-collection means that destination
4953          * should always exist since the issuance of the lock creates the
4954          * resource, so client will always have to provide Overwrite: T
4955          * for direct operations on non-collections (files) */
4956         if (!webdav_has_lock(r, pconf, dst_rel_path))
4957             return HANDLER_FINISHED; /* 423 Locked */
4958 
4959         /* check if destination exists
4960          * (Destination should exist since lock is required,
4961          *  and obtaining a lock will create the resource) */
4962         int rc = lstat(dst_path->ptr, &st);
4963         if (0 == rc && S_ISDIR(st.st_mode)) {
4964             /* file to dir/
4965              * append basename to physical path
4966              * future: might set Content-Location if dst_path does not end '/'*/
4967             if (NULL != (sep = strrchr(r->physical.path.ptr, '/'))) {
4968               #ifdef __COVERITY__
4969                 force_assert(0 != dst_path->used);
4970               #endif
4971                 size_t len = r->physical.path.used - 1
4972                            - (sep - r->physical.path.ptr);
4973                 if (dst_path->ptr[dst_path->used-1] == '/') {
4974                     ++sep; /*(avoid double-slash in path)*/
4975                     --len;
4976                 }
4977                 buffer_append_string_len(dst_path, sep, len);
4978                 buffer_append_string_len(dst_rel_path, sep, len);
4979                 if (buffer_string_length(dst_path) >= PATH_MAX) {
4980                     http_status_set_error(r, 403); /* Forbidden */
4981                     return HANDLER_FINISHED;
4982                 }
4983                 rc = lstat(dst_path->ptr, &st);
4984                 /* target (parent collection) already exists */
4985                 http_status_set_fin(r, 204); /* No Content */
4986             }
4987         }
4988 
4989         if (-1 == rc) {
4990             char *slash;
4991             switch (errno) {
4992               case ENOENT:
4993                 if (http_status_is_set(r)) break;
4994                 /* check that parent collection exists */
4995                 if ((slash = strrchr(dst_path->ptr, '/'))) {
4996                     *slash = '\0';
4997                     if (0 == lstat(dst_path->ptr, &st) && S_ISDIR(st.st_mode)) {
4998                         *slash = '/';
4999                         /* new entity will be created */
5000                         if (!http_status_is_set(r)) {
5001                             webdav_parent_modified(dst_path);
5002                             http_status_set_fin(r, 201); /* Created */
5003                         }
5004                         break;
5005                     }
5006                 }
5007                 /* fall through */
5008               /*case ENOTDIR:*/
5009               default:
5010                 http_status_set_error(r, 409); /* Conflict */
5011                 return HANDLER_FINISHED;
5012             }
5013         }
5014         else if (!(flags & WEBDAV_FLAG_OVERWRITE)) {
5015             /* destination exists, but overwrite is not set */
5016             http_status_set_error(r, 412); /* Precondition Failed */
5017             return HANDLER_FINISHED;
5018         }
5019         else if (S_ISDIR(st.st_mode)) {
5020             /* destination exists, but is a dir, not a file */
5021             http_status_set_error(r, 409); /* Conflict */
5022             return HANDLER_FINISHED;
5023         }
5024         else { /* resource already exists */
5025             http_status_set_fin(r, 204); /* No Content */
5026         }
5027 
5028         rc = webdav_copymove_file(pconf, &r->physical, dst, &flags);
5029         if (0 == rc) {
5030             if (r->http_method == HTTP_METHOD_MOVE)
5031                 webdav_lock_delete_uri(pconf, &r->physical.rel_path);
5032         }
5033         else
5034             http_status_set_error(r, rc);
5035 
5036         return HANDLER_FINISHED;
5037     }
5038 }
5039 
5040 
5041 static handler_t
5042 mod_webdav_copymove (request_st * const r, const plugin_config * const pconf)
5043 {
5044     buffer *dst_path = chunk_buffer_acquire();
5045     buffer *dst_rel_path = chunk_buffer_acquire();
5046     physical_st dst;
5047     dst.path = *dst_path;
5048     dst.rel_path = *dst_rel_path;
5049     handler_t rc = mod_webdav_copymove_b(r, pconf, &dst);
5050     *dst_path = dst.path;
5051     *dst_rel_path = dst.rel_path;
5052     chunk_buffer_release(dst_rel_path);
5053     chunk_buffer_release(dst_path);
5054     return rc;
5055 }
5056 
5057 
5058 #ifdef USE_PROPPATCH
5059 static handler_t
5060 mod_webdav_proppatch (request_st * const r, const plugin_config * const pconf)
5061 {
5062     if (!pconf->sql) {
5063         http_header_response_set(r, HTTP_HEADER_ALLOW,
5064                                  CONST_STR_LEN("Allow"),
5065                                  CONST_STR_LEN("GET, HEAD, PROPFIND, DELETE, "
5066                                                "MKCOL, PUT, MOVE, COPY"));
5067         http_status_set_error(r, 405); /* Method Not Allowed */
5068         return HANDLER_FINISHED;
5069     }
5070 
5071     if (0 == r->reqbody_length) {
5072         http_status_set_error(r, 400); /* Bad Request */
5073         return HANDLER_FINISHED;
5074     }
5075 
5076     if (r->state == CON_STATE_READ_POST) {
5077         handler_t rc = r->con->reqbody_read(r);
5078         if (rc != HANDLER_GO_ON) return rc;
5079     }
5080 
5081     struct stat st;
5082     if (0 != lstat(r->physical.path.ptr, &st)) {
5083         http_status_set_error(r, (errno == ENOENT) ? 404 : 403);
5084         return HANDLER_FINISHED;
5085     }
5086 
5087     if (0 != webdav_if_match_or_unmodified_since(r, &st)) {
5088         http_status_set_error(r, 412); /* Precondition Failed */
5089         return HANDLER_FINISHED;
5090     }
5091 
5092     if (S_ISDIR(st.st_mode)) {
5093         if (r->physical.path.ptr[r->physical.path.used - 2] != '/') {
5094             const buffer *vb =
5095               http_header_request_get(r, HTTP_HEADER_USER_AGENT,
5096                                       CONST_STR_LEN("User-Agent"));
5097             if (vb && 0 == strncmp(vb->ptr, "Microsoft-WebDAV-MiniRedir/",
5098                                    sizeof("Microsoft-WebDAV-MiniRedir/")-1)) {
5099                 /* workaround Microsoft-WebDAV-MiniRedir bug */
5100                 /* (might not be necessary for PROPPATCH here,
5101                  *  but match behavior in mod_webdav_propfind() for PROPFIND) */
5102                 http_response_redirect_to_directory(r, 308);
5103                 return HANDLER_FINISHED;
5104             }
5105             /* set "Content-Location" instead of sending 308 redirect to dir */
5106             if (!http_response_redirect_to_directory(r, 0))
5107                 return HANDLER_FINISHED;
5108             buffer_append_string_len(&r->physical.path,    CONST_STR_LEN("/"));
5109             buffer_append_string_len(&r->physical.rel_path,CONST_STR_LEN("/"));
5110         }
5111     }
5112     else if (r->physical.path.ptr[r->physical.path.used - 2] == '/') {
5113         http_status_set_error(r, 403);
5114         return HANDLER_FINISHED;
5115     }
5116 
5117     xmlDocPtr const xml = webdav_parse_chunkqueue(r, pconf);
5118     if (NULL == xml) {
5119         http_status_set_error(r, 400); /* Bad Request */
5120         return HANDLER_FINISHED;
5121     }
5122 
5123     const xmlNode * const rootnode = xmlDocGetRootElement(xml);
5124     if (NULL == rootnode
5125         || 0 != webdav_xmlstrcmp_fixed(rootnode->name, "propertyupdate")) {
5126         http_status_set_error(r, 422); /* Unprocessable Entity */
5127         xmlFreeDoc(xml);
5128         return HANDLER_FINISHED;
5129     }
5130 
5131     if (!webdav_db_transaction_begin_immediate(pconf)) {
5132         http_status_set_error(r, 500); /* Internal Server Error */
5133         xmlFreeDoc(xml);
5134         return HANDLER_FINISHED;
5135     }
5136 
5137     /* NOTE: selectively providing multi-status response is NON-CONFORMANT
5138      *       (specified in [RFC4918])
5139      * However, PROPPATCH is all-or-nothing, so client should be able to
5140      * unequivocably know that all items in PROPPATCH succeeded if it receives
5141      * 204 No Content, or that items that are not listed with a failure status
5142      * in a multi-status response have the status of 424 Failed Dependency,
5143      * without the server having to be explicit. */
5144 
5145     /* UPDATE request, we know 'set' and 'remove' */
5146     buffer *ms = NULL; /*(multi-status)*/
5147     int update;
5148     for (const xmlNode *cmd = rootnode->children; cmd; cmd = cmd->next) {
5149         if (!(update = (0 == webdav_xmlstrcmp_fixed(cmd->name, "set")))) {
5150             if (0 != webdav_xmlstrcmp_fixed(cmd->name, "remove"))
5151                 continue; /* skip; not "set" or "remove" */
5152         }
5153 
5154         for (const xmlNode *props = cmd->children; props; props = props->next) {
5155             if (0 != webdav_xmlstrcmp_fixed(props->name, "prop"))
5156                 continue;
5157 
5158             const xmlNode *prop = props->children;
5159             /* libxml2 will keep those blank (whitespace only) nodes */
5160             while (NULL != prop && xmlIsBlankNode(prop))
5161                 prop = prop->next;
5162             if (NULL == prop)
5163                 continue;
5164             if (prop->ns && '\0' == *(char *)prop->ns->href
5165                          && '\0' != *(char *)prop->ns->prefix) {
5166                 /* error: missing namespace for property */
5167                 log_error(r->conf.errh, __FILE__, __LINE__,
5168                           "no namespace for: %s", prop->name);
5169                 if (!ms) ms = chunk_buffer_acquire(); /* Unprocessable Entity */
5170                 webdav_xml_propstat_status(ms, "", (char *)prop->name, 422);
5171                 continue;
5172             }
5173 
5174             /* XXX: ??? should blank namespace be normalized to "DAV:" ???
5175              *      ??? should this also be done in propfind requests ??? */
5176 
5177             if (prop->ns && 0 == strcmp((char *)prop->ns->href, "DAV:")) {
5178                 const size_t namelen = strlen((char *)prop->name);
5179                 const struct live_prop_list *list = protected_props;
5180                 while (0 != list->len
5181                        && (list->len != namelen
5182                            || 0 != memcmp(prop->name, list->prop, list->len)))
5183                     ++list;
5184                 if (NULL != list->prop) {
5185                     /* error <DAV:cannot-modify-protected-property/> */
5186                     if (!ms) ms = chunk_buffer_acquire();
5187                     webdav_xml_propstat_protected(ms, (char *)prop->name,
5188                                                   namelen, 403); /* Forbidden */
5189                     continue;
5190                 }
5191             }
5192 
5193             if (update) {
5194                 if (!prop->children) continue;
5195                 char * const propval = prop->children
5196                   ? (char *)xmlNodeListGetString(xml, prop->children, 0)
5197                   : NULL;
5198                 webdav_prop_update(pconf, &r->physical.rel_path,
5199                                    (char *)prop->name,
5200                                    prop->ns ? (char *)prop->ns->href : "",
5201                                    propval ? propval : "");
5202                 xmlFree(propval);
5203             }
5204             else
5205                 webdav_prop_delete(pconf, &r->physical.rel_path,
5206                                    (char *)prop->name,
5207                                    prop->ns ? (char *)prop->ns->href : "");
5208         }
5209     }
5210 
5211     if (NULL == ms
5212           ? webdav_db_transaction_commit(pconf)
5213           : webdav_db_transaction_rollback(pconf)) {
5214         if (NULL == ms) {
5215             const buffer *vb =
5216               http_header_request_get(r, HTTP_HEADER_USER_AGENT,
5217                                       CONST_STR_LEN("User-Agent"));
5218             if (vb && 0 == strncmp(vb->ptr, "Microsoft-WebDAV-MiniRedir/",
5219                                    sizeof("Microsoft-WebDAV-MiniRedir/")-1)) {
5220                 /* workaround Microsoft-WebDAV-MiniRedir bug; 204 not handled */
5221                 /* 200 without response body or 204 both incorrectly interpreted
5222                  * as 507 Insufficient Storage by Microsoft-WebDAV-MiniRedir. */
5223                 ms = chunk_buffer_acquire(); /* 207 Multi-status */
5224                 webdav_xml_response_status(ms, &r->physical.path, 200);
5225             }
5226         }
5227         if (NULL == ms)
5228             http_status_set_fin(r, 204); /* No Content */
5229         else /* 207 Multi-status */
5230             webdav_xml_doc_multistatus_response(r, pconf, ms);
5231     }
5232     else
5233         http_status_set_error(r, 500); /* Internal Server Error */
5234 
5235     if (NULL != ms)
5236         chunk_buffer_release(ms);
5237 
5238     xmlFreeDoc(xml);
5239     return HANDLER_FINISHED;
5240 }
5241 #endif
5242 
5243 
5244 #ifdef USE_LOCKS
5245 struct webdav_conflicting_lock_st {
5246   webdav_lockdata *lockdata;
5247   buffer *b;
5248 };
5249 
5250 
5251 static void
5252 webdav_conflicting_lock_cb (void * const vdata,
5253                             const webdav_lockdata * const lockdata)
5254 {
5255     /* lock is not available if someone else has exclusive lock or if
5256      * client requested exclusive lock and others have shared locks */
5257     struct webdav_conflicting_lock_st * const cbdata =
5258       (struct webdav_conflicting_lock_st *)vdata;
5259     if (lockdata->lockscope->used == sizeof("exclusive")
5260         || cbdata->lockdata->lockscope->used == sizeof("exclusive"))
5261         webdav_xml_href(cbdata->b, &lockdata->lockroot);
5262 }
5263 
5264 
5265 static handler_t
5266 mod_webdav_lock (request_st * const r, const plugin_config * const pconf)
5267 {
5268     /**
5269      * a mac wants to write
5270      *
5271      * LOCK /dav/expire.txt HTTP/1.1\r\n
5272      * User-Agent: WebDAVFS/1.3 (01308000) Darwin/8.1.0 (Power Macintosh)\r\n
5273      * Accept: * / *\r\n
5274      * Depth: 0\r\n
5275      * Timeout: Second-600\r\n
5276      * Content-Type: text/xml; charset=\"utf-8\"\r\n
5277      * Content-Length: 229\r\n
5278      * Connection: keep-alive\r\n
5279      * Host: 192.168.178.23:1025\r\n
5280      * \r\n
5281      * <?xml version=\"1.0\" encoding=\"utf-8\"?>\n
5282      * <D:lockinfo xmlns:D=\"DAV:\">\n
5283      *  <D:lockscope><D:exclusive/></D:lockscope>\n
5284      *  <D:locktype><D:write/></D:locktype>\n
5285      *  <D:owner>\n
5286      *   <D:href>http://www.apple.com/webdav_fs/</D:href>\n
5287      *  </D:owner>\n
5288      * </D:lockinfo>\n
5289      */
5290 
5291     if (r->reqbody_length) {
5292         if (r->state == CON_STATE_READ_POST) {
5293             handler_t rc = r->con->reqbody_read(r);
5294             if (rc != HANDLER_GO_ON) return rc;
5295         }
5296     }
5297 
5298     /* XXX: maybe add config switch to require that authentication occurred? */
5299     buffer owner = { NULL, 0, 0 };/*owner (not authenticated)(auth_user unset)*/
5300     const data_string * const authn_user = (const data_string *)
5301       array_get_element_klen(&r->env, CONST_STR_LEN("REMOTE_USER"));
5302 
5303     /* future: make max timeout configurable (e.g. pconf->lock_timeout_max)
5304      *
5305      * [RFC4918] 10.7 Timeout Request Header
5306      *   The "Second" TimeType specifies the number of seconds that will elapse
5307      *   between granting of the lock at the server, and the automatic removal
5308      *   of the lock. The timeout value for TimeType "Second" MUST NOT be
5309      *   greater than 2^32-1.
5310      */
5311 
5312     webdav_lockdata lockdata = {
5313       { NULL, 0, 0 }, /* locktoken */
5314       { r->physical.rel_path.ptr, r->physical.rel_path.used, 0}, /*lockroot*/
5315       { NULL, 0, 0 }, /* ownerinfo */
5316       (authn_user ? &authn_user->value : &owner), /* owner */
5317       NULL, /* lockscope */
5318       NULL, /* locktype  */
5319       -1,   /* depth */
5320       600   /* timeout (arbitrary default lock timeout: 10 minutes) */
5321     };
5322 
5323     const buffer *h =
5324       http_header_request_get(r, HTTP_HEADER_OTHER, CONST_STR_LEN("Timeout"));
5325     if (!buffer_is_empty(h)) {
5326         /* loosely parse Timeout request header and ignore "infinity" timeout */
5327         /* future: might implement config param for upper limit for timeout */
5328         const char *p = h->ptr;
5329         do {
5330             if ((*p | 0x20) == 's'
5331                 && buffer_eq_icase_ssn(p, CONST_STR_LEN("second-"))) {
5332                 long t = strtol(p+sizeof("second-")-1, NULL, 10);
5333                 if (0 < t && t < lockdata.timeout)
5334                     lockdata.timeout = t > 5 ? t : 5;
5335                     /*(arbitrary min timeout: 5 secs)*/
5336                 else if (sizeof(long) != sizeof(int) && t > INT32_MAX)
5337                     lockdata.timeout = INT32_MAX;
5338                     /* while UINT32_MAX is actual limit in RFC4918,
5339                      * sqlite more easily supports int, though could be
5340                      * changed to use int64 to for the timeout param.
5341                      * The "limitation" between timeouts that are many
5342                      * *years* long does not really matter in reality. */
5343                 break;
5344             }
5345           #if 0
5346             else if ((*p | 0x20) == 'i'
5347                      && buffer_eq_icase_ssn(p, CONST_STR_LEN("infinity"))) {
5348                 lockdata.timeout = INT32_MAX;
5349                 break;
5350             }
5351           #endif
5352             while (*p != ',' && *p != '\0') ++p;
5353             while (*p == ' ' || *p == '\t') ++p;
5354         } while (*p != '\0');
5355     }
5356 
5357     if (r->reqbody_length) {
5358         lockdata.depth = webdav_parse_Depth(r);
5359         if (1 == lockdata.depth) {
5360             /* [RFC4918] 9.10.3 Depth and Locking
5361              *   Values other than 0 or infinity MUST NOT be used
5362              *   with the Depth header on a LOCK method.
5363              */
5364             http_status_set_error(r, 400); /* Bad Request */
5365             return HANDLER_FINISHED;
5366         }
5367 
5368         xmlDocPtr const xml = webdav_parse_chunkqueue(r, pconf);
5369         if (NULL == xml) {
5370             http_status_set_error(r, 400); /* Bad Request */
5371             return HANDLER_FINISHED;
5372         }
5373 
5374         const xmlNode * const rootnode = xmlDocGetRootElement(xml);
5375         if (NULL == rootnode
5376             || 0 != webdav_xmlstrcmp_fixed(rootnode->name, "lockinfo")) {
5377             http_status_set_error(r, 422); /* Unprocessable Entity */
5378             xmlFreeDoc(xml);
5379             return HANDLER_FINISHED;
5380         }
5381 
5382         const xmlNode *lockinfo = rootnode->children;
5383         for (; lockinfo; lockinfo = lockinfo->next) {
5384             if (0 == webdav_xmlstrcmp_fixed(lockinfo->name, "lockscope")) {
5385                 const xmlNode *value = lockinfo->children;
5386                 for (; value; value = value->next) {
5387                     if (0 == webdav_xmlstrcmp_fixed(value->name, "exclusive"))
5388                         lockdata.lockscope=(const buffer *)&lockscope_exclusive;
5389                     else if (0 == webdav_xmlstrcmp_fixed(value->name, "shared"))
5390                         lockdata.lockscope=(const buffer *)&lockscope_shared;
5391                     else {
5392                         lockdata.lockscope=NULL; /* trigger error below loop */
5393                         break;
5394                     }
5395                 }
5396             }
5397             else if (0 == webdav_xmlstrcmp_fixed(lockinfo->name, "locktype")) {
5398                 const xmlNode *value = lockinfo->children;
5399                 for (; value; value = value->next) {
5400                     if (0 == webdav_xmlstrcmp_fixed(value->name, "write"))
5401                         lockdata.locktype = (const buffer *)&locktype_write;
5402                     else {
5403                         lockdata.locktype = NULL;/* trigger error below loop */
5404                         break;
5405                     }
5406                 }
5407             }
5408             else if (0 == webdav_xmlstrcmp_fixed(lockinfo->name, "owner")) {
5409                 if (lockinfo->children)
5410                     lockdata.ownerinfo.ptr =
5411                       (char *)xmlNodeListGetString(xml, lockinfo->children, 0);
5412                 if (lockdata.ownerinfo.ptr)
5413                     lockdata.ownerinfo.used = strlen(lockdata.ownerinfo.ptr)+1;
5414             }
5415         }
5416 
5417       do { /*(resources are cleaned up after code block)*/
5418 
5419         if (NULL == lockdata.lockscope || NULL == lockdata.locktype) {
5420             /*(missing lockscope and locktype in lock request)*/
5421             http_status_set_error(r, 422); /* Unprocessable Entity */
5422             break; /* clean up resources and return HANDLER_FINISHED */
5423         }
5424 
5425         /* check lock prior to potentially creating new resource,
5426          * and prior to using entropy to create uuid */
5427         struct webdav_conflicting_lock_st cbdata;
5428         cbdata.lockdata = &lockdata;
5429         cbdata.b = chunk_buffer_acquire();
5430         webdav_lock_activelocks(pconf, &lockdata.lockroot,
5431                                 (0 == lockdata.depth ? 1 : -1),
5432                                 webdav_conflicting_lock_cb, &cbdata);
5433         if (0 != cbdata.b->used) {
5434             /* 423 Locked */
5435             webdav_xml_doc_error_no_conflicting_lock(r, cbdata.b);
5436             chunk_buffer_release(cbdata.b);
5437             break; /* clean up resources and return HANDLER_FINISHED */
5438         }
5439         chunk_buffer_release(cbdata.b);
5440 
5441         int created = 0;
5442         struct stat st;
5443         if (0 != lstat(r->physical.path.ptr, &st)) {
5444             /* [RFC4918] 7.3 Write Locks and Unmapped URLs
5445              *   A successful lock request to an unmapped URL MUST result in
5446              *   the creation of a locked (non-collection) resource with empty
5447              *   content.
5448              *   [...]
5449              *   The response MUST indicate that a resource was created, by
5450              *   use of the "201 Created" response code (a LOCK request to an
5451              *   existing resource instead will result in 200 OK).
5452              * [RFC4918] 9.10.4 Locking Unmapped URLs
5453              *   A successful LOCK method MUST result in the creation of an
5454              *   empty resource that is locked (and that is not a collection)
5455              *   when a resource did not previously exist at that URL. Later on,
5456              *   the lock may go away but the empty resource remains. Empty
5457              *   resources MUST then appear in PROPFIND responses including that
5458              *   URL in the response scope. A server MUST respond successfully
5459              *   to a GET request to an empty resource, either by using a 204
5460              *   No Content response, or by using 200 OK with a Content-Length
5461              *   header indicating zero length
5462              *
5463              * unmapped resource; create empty file
5464              * (open() should fail if path ends in '/', but does not on some OS.
5465              *  This is desired behavior since collection should be created
5466              *  with MKCOL, and not via LOCK on an unmapped resource) */
5467             const int fd =
5468               (errno == ENOENT
5469                && r->physical.path.ptr[r->physical.path.used-2] != '/')
5470               ? fdevent_open_cloexec(r->physical.path.ptr, 0,
5471                                      O_WRONLY | O_CREAT | O_EXCL | O_TRUNC,
5472                                      WEBDAV_FILE_MODE)
5473               : -1;
5474             if (fd >= 0) {
5475                 /*(skip sending etag if fstat() error; not expected)*/
5476                 if (0 != fstat(fd, &st)) r->conf.etag_flags = 0;
5477                 close(fd);
5478                 created = 1;
5479                 webdav_parent_modified(&r->physical.path);
5480             }
5481             else if (errno != EEXIST) {
5482                 http_status_set_error(r, 403); /* Forbidden */
5483                 break; /* clean up resources and return HANDLER_FINISHED */
5484             }
5485             else if (0 != lstat(r->physical.path.ptr, &st)) {
5486                 http_status_set_error(r, 403); /* Forbidden */
5487                 break; /* clean up resources and return HANDLER_FINISHED */
5488             }
5489             lockdata.depth = 0; /* force Depth: 0 on non-collections */
5490         }
5491 
5492         if (!created) {
5493             if (0 != webdav_if_match_or_unmodified_since(r, &st)) {
5494                 http_status_set_error(r, 412); /* Precondition Failed */
5495                 break; /* clean up resources and return HANDLER_FINISHED */
5496             }
5497         }
5498 
5499         if (created) {
5500         }
5501         else if (S_ISDIR(st.st_mode)) {
5502             if (r->physical.path.ptr[r->physical.path.used - 2] != '/') {
5503                 /* 308 Permanent Redirect */
5504                 http_response_redirect_to_directory(r, 308);
5505                 break; /* clean up resources and return HANDLER_FINISHED */
5506                 /* Alternatively, could append '/' to r->physical.path
5507                  * and r->physical.rel_path, set Content-Location in
5508                  * response headers, and continue to serve the request */
5509             }
5510         }
5511         else if (r->physical.path.ptr[r->physical.path.used - 2] == '/') {
5512             http_status_set_error(r, 403); /* Forbidden */
5513             break; /* clean up resources and return HANDLER_FINISHED */
5514         }
5515         else if (0 != lockdata.depth)
5516             lockdata.depth = 0; /* force Depth: 0 on non-collections */
5517 
5518         /* create locktoken
5519          * (uuid_unparse() output is 36 chars + '\0') */
5520         uuid_t id;
5521         char lockstr[sizeof("<urn:uuid:>") + 36] = "<urn:uuid:";
5522         lockdata.locktoken.ptr = lockstr+1;         /*(without surrounding <>)*/
5523         lockdata.locktoken.used = sizeof(lockstr)-2;/*(without surrounding <>)*/
5524         uuid_generate(id);
5525         uuid_unparse(id, lockstr+sizeof("<urn:uuid:")-1);
5526 
5527         /* XXX: consider fix TOC-TOU race condition by starting transaction
5528          * and re-running webdav_lock_activelocks() check before running
5529          * webdav_lock_acquire() (but both routines would need to be modified
5530          * to defer calling sqlite3_reset(stmt) to be part of transaction) */
5531         if (webdav_lock_acquire(pconf, &lockdata)) {
5532             lockstr[sizeof(lockstr)-2] = '>';
5533             http_header_response_set(r, HTTP_HEADER_OTHER,
5534                                      CONST_STR_LEN("Lock-Token"),
5535                                      lockstr, sizeof(lockstr)-1);
5536             webdav_xml_doc_lock_acquired(r, pconf, &lockdata);
5537             if (0 != r->conf.etag_flags && !S_ISDIR(st.st_mode))
5538                 webdav_response_etag(r, &st);
5539             http_status_set_fin(r, created ? 201 : 200); /* Created | OK */
5540         }
5541         else /*(database error obtaining lock)*/
5542             http_status_set_error(r, 500); /* Internal Server Error */
5543 
5544       } while (0); /*(resources are cleaned up after code block)*/
5545 
5546         xmlFree(lockdata.ownerinfo.ptr);
5547         xmlFreeDoc(xml);
5548         return HANDLER_FINISHED;
5549     }
5550     else {
5551         h = http_header_request_get(r, HTTP_HEADER_OTHER, CONST_STR_LEN("If"));
5552         if (NULL == h
5553             || h->used < 6 || h->ptr[1] != '<' || h->ptr[h->used-3] != '>') {
5554             /*(rejects value with trailing LWS, even though RFC-permitted)*/
5555             http_status_set_error(r, 400); /* Bad Request */
5556             return HANDLER_FINISHED;
5557         }
5558         /* remove (< >) around token */
5559         lockdata.locktoken.ptr = h->ptr+2;
5560         lockdata.locktoken.used = h->used-4;
5561         /*(future: fill in from database, though exclusive write lock is the
5562          * only lock supported at the moment)*/
5563         lockdata.lockscope = (const buffer *)&lockscope_exclusive;
5564         lockdata.locktype  = (const buffer *)&locktype_write;
5565         lockdata.depth     = 0;
5566 
5567         if (webdav_lock_refresh(pconf, &lockdata)) {
5568             webdav_xml_doc_lock_acquired(r, pconf, &lockdata);
5569             http_status_set_fin(r, 200); /* OK */
5570         }
5571         else
5572             http_status_set_error(r, 412); /* Precondition Failed */
5573 
5574         return HANDLER_FINISHED;
5575     }
5576 }
5577 #endif
5578 
5579 
5580 #ifdef USE_LOCKS
5581 static handler_t
5582 mod_webdav_unlock (request_st * const r, const plugin_config * const pconf)
5583 {
5584     const buffer * const h =
5585       http_header_request_get(r, HTTP_HEADER_OTHER,
5586                               CONST_STR_LEN("Lock-Token"));
5587     if (NULL == h
5588         || h->used < 4 || h->ptr[0] != '<' || h->ptr[h->used-2] != '>') {
5589         /*(rejects value with trailing LWS, even though RFC-permitted)*/
5590         http_status_set_error(r, 400); /* Bad Request */
5591         return HANDLER_FINISHED;
5592     }
5593 
5594     buffer owner = { NULL, 0, 0 };/*owner (not authenticated)(auth_user unset)*/
5595     const data_string * const authn_user = (const data_string *)
5596       array_get_element_klen(&r->env, CONST_STR_LEN("REMOTE_USER"));
5597 
5598     webdav_lockdata lockdata = {
5599       { h->ptr+1, h->used-2, 0 }, /* locktoken (remove < > around token) */
5600       { r->physical.rel_path.ptr, r->physical.rel_path.used, 0}, /*lockroot*/
5601       { NULL, 0, 0 }, /* ownerinfo (unused for unlock) */
5602       (authn_user ? &authn_user->value : &owner), /* owner */
5603       NULL, /* lockscope (unused for unlock) */
5604       NULL, /* locktype  (unused for unlock) */
5605       0,    /* depth     (unused for unlock) */
5606       0     /* timeout   (unused for unlock) */
5607     };
5608 
5609     /* check URI (lockroot) and depth in scope for locktoken and authorized */
5610     switch (webdav_lock_match(pconf, &lockdata)) {
5611       case  0:
5612         if (webdav_lock_release(pconf, &lockdata)) {
5613             http_status_set_fin(r, 204); /* No Content */
5614             return HANDLER_FINISHED;
5615         }
5616         /* fall through */
5617       default:
5618       case -1: /* lock does not exist */
5619       case -2: /* URI not in scope of locktoken and depth */
5620         /* 409 Conflict */
5621         webdav_xml_doc_error_lock_token_matches_request_uri(r);
5622         return HANDLER_FINISHED;
5623       case -3: /* not owner/not authorized to remove lock */
5624         http_status_set_error(r, 403); /* Forbidden */
5625         return HANDLER_FINISHED;
5626     }
5627 }
5628 #endif
5629 
5630 
5631 SUBREQUEST_FUNC(mod_webdav_subrequest_handler)
5632 {
5633     const plugin_config * const pconf =
5634       (plugin_config *)r->plugin_ctx[((plugin_data *)p_d)->id];
5635     if (NULL == pconf) return HANDLER_GO_ON; /*(should not happen)*/
5636 
5637     switch (r->http_method) {
5638     case HTTP_METHOD_PROPFIND:
5639         return mod_webdav_propfind(r, pconf);
5640     case HTTP_METHOD_MKCOL:
5641         return mod_webdav_mkcol(r, pconf);
5642     case HTTP_METHOD_DELETE:
5643         return mod_webdav_delete(r, pconf);
5644     case HTTP_METHOD_PUT:
5645         return mod_webdav_put(r, pconf);
5646     case HTTP_METHOD_MOVE:
5647     case HTTP_METHOD_COPY:
5648         return mod_webdav_copymove(r, pconf);
5649    #ifdef USE_PROPPATCH
5650     case HTTP_METHOD_PROPPATCH:
5651         return mod_webdav_proppatch(r, pconf);
5652    #endif
5653    #ifdef USE_LOCKS
5654     case HTTP_METHOD_LOCK:
5655         return mod_webdav_lock(r, pconf);
5656     case HTTP_METHOD_UNLOCK:
5657         return mod_webdav_unlock(r, pconf);
5658    #endif
5659     default:
5660         http_status_set_error(r, 501); /* Not Implemented */
5661         return HANDLER_FINISHED;
5662     }
5663 }
5664 
5665 
5666 PHYSICALPATH_FUNC(mod_webdav_physical_handler)
5667 {
5668     /* physical path is set up */
5669     /*assert(0 != r->physical.path.used);*/
5670   #ifdef __COVERITY__
5671     force_assert(2 <= r->physical.path.used);
5672   #endif
5673 
5674     int check_readonly = 0;
5675     int check_lock_src = 0;
5676     int reject_reqbody = 0;
5677 
5678     /* check for WebDAV request methods handled by this module */
5679     switch (r->http_method) {
5680       case HTTP_METHOD_GET:
5681       case HTTP_METHOD_HEAD:
5682       case HTTP_METHOD_POST:
5683       default:
5684         return HANDLER_GO_ON;
5685       case HTTP_METHOD_PROPFIND:
5686       case HTTP_METHOD_LOCK:
5687         break;
5688       case HTTP_METHOD_UNLOCK:
5689         reject_reqbody = 1;
5690         break;
5691       case HTTP_METHOD_DELETE:
5692       case HTTP_METHOD_MOVE:
5693         reject_reqbody = 1; /*(fall through)*/ __attribute_fallthrough__
5694       case HTTP_METHOD_PROPPATCH:
5695       case HTTP_METHOD_PUT:
5696         check_readonly = check_lock_src = 1;
5697         break;
5698       case HTTP_METHOD_COPY:
5699       case HTTP_METHOD_MKCOL:
5700         check_readonly = reject_reqbody = 1;
5701         break;
5702     }
5703 
5704     plugin_config pconf;
5705     mod_webdav_patch_config(r, (plugin_data *)p_d, &pconf);
5706     if (!pconf.enabled) return HANDLER_GO_ON;
5707 
5708     if (check_readonly && pconf.is_readonly) {
5709         http_status_set_error(r, 403); /* Forbidden */
5710         return HANDLER_FINISHED;
5711     }
5712 
5713     if (reject_reqbody && r->reqbody_length) {
5714         /* [RFC4918] 8.4 Required Bodies in Requests
5715          *   Servers MUST examine all requests for a body, even when a
5716          *   body was not expected. In cases where a request body is
5717          *   present but would be ignored by a server, the server MUST
5718          *   reject the request with 415 (Unsupported Media Type).
5719          */
5720         http_status_set_error(r, 415); /* Unsupported Media Type */
5721         return HANDLER_FINISHED;
5722     }
5723 
5724     if (check_lock_src && !webdav_has_lock(r, &pconf, &r->physical.rel_path))
5725         return HANDLER_FINISHED; /* 423 Locked */
5726 
5727     /* initial setup for methods */
5728     switch (r->http_method) {
5729       case HTTP_METHOD_PUT:
5730         if (mod_webdav_put_prep(r, &pconf) == HANDLER_FINISHED)
5731             return HANDLER_FINISHED;
5732         break;
5733       default:
5734         break;
5735     }
5736 
5737     r->handler_module = ((plugin_data *)p_d)->self;
5738     r->conf.stream_request_body &=
5739       ~(FDEVENT_STREAM_REQUEST | FDEVENT_STREAM_REQUEST_BUFMIN);
5740     r->plugin_ctx[((plugin_data *)p_d)->id] = &pconf;
5741     const handler_t rc =
5742       mod_webdav_subrequest_handler(r, p_d); /*p->handle_subrequest()*/
5743     if (rc == HANDLER_FINISHED || rc == HANDLER_ERROR)
5744         r->plugin_ctx[((plugin_data *)p_d)->id] = NULL;
5745     else {  /* e.g. HANDLER_WAIT_FOR_RD */
5746         plugin_config * const save_pconf =
5747           (plugin_config *)malloc(sizeof(pconf));
5748         force_assert(save_pconf);
5749         memcpy(save_pconf, &pconf, sizeof(pconf));
5750         r->plugin_ctx[((plugin_data *)p_d)->id] = save_pconf;
5751     }
5752     return rc;
5753 }
5754 
5755 
5756 REQUEST_FUNC(mod_webdav_handle_reset) {
5757     /* free plugin_config if allocated and saved to per-request storage */
5758     void ** const restrict dptr =
5759       &r->plugin_ctx[((plugin_data *)p_d)->id];
5760     if (*dptr) {
5761         free(*dptr);
5762         *dptr = NULL;
5763         chunkqueue_set_tempdirs(&r->reqbody_queue, /* reset sz */
5764                                 r->reqbody_queue.tempdirs, 0);
5765     }
5766     return HANDLER_GO_ON;
5767 }
5768