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