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