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