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