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