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