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