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