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