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