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