[core] chunkqueue perf: code reuse
[lighttpd.git] / src / mod_webdav.c
blob121e40333986782962a60c425a65cf471085d7ee
1 /*
2 * mod_webdav
3 */
5 /*
6 * Note: This plugin is a basic implementation of [RFC4918] WebDAV
8 * Version Control System (VCS) backing WebDAV is recommended instead
9 * and Subversion (svn) is one such VCS supporting WebDAV.
11 * status: *** EXPERIMENTAL *** (and likely insecure encoding/decoding)
13 * future:
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().
40 * RFE: add config option whether or not to include locktoken and ownerinfo
41 * in PROPFIND lockdiscovery
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
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).
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)
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.
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 * - ...
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.
141 * notes:
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)
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.
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
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() */
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
185 #if defined(HAVE_LIBXML_H) && defined(HAVE_SQLITE3_H)
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>
193 #if defined(HAVE_LIBUUID) && defined(HAVE_UUID_UUID_H)
194 #define USE_LOCKS
195 #include <uuid/uuid.h>
196 #endif
198 #endif /* defined(HAVE_LIBXML_H) && defined(HAVE_SQLITE3_H) */
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() */
212 #include "configfile.h"
213 #include "plugin.h"
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);
228 typedef physical physical_st;
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);
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");
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;
253 p->data = NULL;
255 return 0;
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
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
269 #define webdav_xmlstrcmp_fixed(s, fixed) \
270 strncmp((const char *)(s), (fixed), sizeof(fixed))
272 #include <ctype.h> /* isupper() tolower() */
273 static void
274 webdav_str_len_to_lower (char * const restrict s, const uint32_t len)
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]);
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;
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;
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;
312 /* plugin config for all request/connections */
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;
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;
329 typedef struct {
330 PLUGIN_DATA;
331 int nconfig;
332 plugin_config **config_storage;
333 } plugin_data;
336 /* init the plugin data */
337 INIT_FUNC(mod_webdav_init) {
338 return calloc(1, sizeof(plugin_data));
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;
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);
355 sql_config * const sql = s->sql;
356 if (!sql || !sql->sqlh) {
357 free(sql);
358 continue;
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);
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);
383 #endif
384 free(p->config_storage);
387 free(p);
389 UNUSED(srv);
390 return HANDLER_GO_ON;
394 __attribute_cold__
395 static handler_t mod_webdav_sqlite3_init (plugin_config * restrict s, log_error_st *errh);
397 /* handle plugin config and check values */
398 SETDEFAULTS_FUNC(mod_webdav_set_defaults) {
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;*/
408 #endif
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 },
417 { NULL, NULL, T_CONFIG_UNSET, T_CONFIG_SCOPE_UNSET }
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);
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();
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;
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;
444 if (!buffer_is_empty(s->sqlite_db_name)) {
445 if (mod_webdav_sqlite3_init(s, srv->errh) == HANDLER_ERROR)
446 return HANDLER_ERROR;
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;
457 log_error(srv->errh, __FILE__, __LINE__,
458 "unrecognized webdav.opts: %.*s",
459 BUFFER_INTLEN_PTR(ds->key));
460 return HANDLER_ERROR;
463 if (n_context) {
464 p->config_storage[0]->srv = srv;
465 p->config_storage[0]->tmpb = srv->tmp_buf;
468 return HANDLER_GO_ON;
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)
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;
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 */
489 s = p->config_storage[i];
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);
512 URIHANDLER_FUNC(mod_webdav_uri_handler)
514 UNUSED(srv);
515 if (con->request.http_method != HTTP_METHOD_OPTIONS)
516 return HANDLER_GO_ON;
518 plugin_config pconf;
519 mod_webdav_patch_connection(srv, con, (plugin_data *)p_d, &pconf);
520 if (!pconf.enabled) return HANDLER_GO_ON;
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
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"));
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
560 return HANDLER_GO_ON;
564 #ifdef USE_LOCKS
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;
577 typedef struct { const char *ptr; uint32_t used; uint32_t size; } tagb;
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 };
586 #endif
588 typedef struct {
589 const char *ns;
590 const char *name;
591 uint32_t nslen;
592 uint32_t namelen;
593 } webdav_property_name;
595 typedef struct {
596 webdav_property_name *ptr;
597 int used;
598 int size;
599 } webdav_property_names;
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.
628 #define MOD_WEBDAV_XMLNS_NS0 "xmlns:ns0=\"urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/\""
631 static void
632 webdav_xml_doctype (buffer * const b, connection * const con)
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\""));
638 buffer_copy_string_len(b, CONST_STR_LEN(
639 "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"));
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)
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("\"/>"));
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(">"));
665 #ifdef USE_LOCKS
666 static void
667 webdav_xml_href_raw (buffer * const b, const buffer * const href)
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"));
675 #endif
678 static void
679 webdav_xml_href (buffer * const b, const buffer * const href)
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"));
689 static void
690 webdav_xml_status (buffer * const b, const int status)
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"));
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)
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"));
717 #endif
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)
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"));
737 #endif
740 static void
741 webdav_xml_propstat (buffer * const b, buffer * const value, const int status)
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"));
755 __attribute_cold__
756 static void
757 webdav_xml_response_status (buffer * const b,
758 const buffer * const href,
759 const int status)
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"));
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)
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);
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"));
821 #endif
824 static void
825 webdav_xml_doc_multistatus (connection * const con,
826 const plugin_config * const pconf,
827 buffer * const ms)
829 http_status_set_fin(con, 207); /* Multi-status */
831 buffer * const b = /*(optimization; buf extended as needed)*/
832 chunkqueue_append_buffer_open_sz(con->write_queue, 128 + ms->used);
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"));
841 if (pconf->log_xml)
842 log_error(con->errh, __FILE__, __LINE__,
843 "XML-response-body: %.*s", BUFFER_INTLEN_PTR(b));
845 chunkqueue_append_buffer_commit(con->write_queue);
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)
855 http_status_set_fin(con, 207); /* Multi-status */
857 buffer * const b = /*(optimization; buf extended as needed)*/
858 chunkqueue_append_buffer_open_sz(con->write_queue, 128 + ms->used);
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"));
870 if (pconf->log_xml)
871 log_error(con->errh, __FILE__, __LINE__,
872 "XML-response-body: %.*s", BUFFER_INTLEN_PTR(b));
874 chunkqueue_append_buffer_commit(con->write_queue);
876 #endif
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)
885 /*(http_status is set by caller to 200 OK or 201 Created)*/
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);
895 buffer * const b =
896 chunkqueue_append_buffer_open_sz(con->write_queue, 1024);
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"));
907 if (pconf->log_xml)
908 log_error(con->errh, __FILE__, __LINE__,
909 "XML-response-body: %.*s", BUFFER_INTLEN_PTR(b));
911 chunkqueue_append_buffer_commit(con->write_queue);
913 #endif
917 * [RFC4918] 16 Precondition/Postcondition XML Elements
922 * 403 Forbidden
923 * "<D:error><DAV:cannot-modify-protected-property/></D:error>"
925 * 403 Forbidden
926 * "<D:error><DAV:no-external-entities/></D:error>"
928 * 409 Conflict
929 * "<D:error><DAV:preserved-live-properties/></D:error>"
933 __attribute_cold__
934 static void
935 webdav_xml_doc_error_propfind_finite_depth (connection * const con)
937 http_status_set(con, 403); /* Forbidden */
938 con->file_finished = 1;
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);
949 #ifdef USE_LOCKS
950 __attribute_cold__
951 static void
952 webdav_xml_doc_error_lock_token_matches_request_uri (connection * const con)
954 http_status_set(con, 409); /* Conflict */
955 con->file_finished = 1;
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);
964 #endif
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)
973 http_status_set(con, 423); /* Locked */
974 con->file_finished = 1;
976 buffer * const b = /*(optimization; buf extended as needed)*/
977 chunkqueue_append_buffer_open_sz(con->write_queue, 256 + hrefs->used);
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"));
994 chunkqueue_append_buffer_commit(con->write_queue);
996 #endif
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)
1005 webdav_xml_doc_423_locked(con, hrefs,
1006 CONST_STR_LEN("lock-token-submitted"));
1008 #endif
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)
1017 webdav_xml_doc_423_locked(con, hrefs,
1018 CONST_STR_LEN("no-conflicting-lock"));
1020 #endif
1023 #ifdef USE_PROPPATCH
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))"
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))"
1045 #define MOD_WEBDAV_SQLITE_PROPS_SELECT_PROPNAMES \
1046 "SELECT prop, ns FROM properties WHERE resource = ?"
1048 #define MOD_WEBDAV_SQLITE_PROPS_SELECT_PROP \
1049 "SELECT value FROM properties WHERE resource = ? AND prop = ? AND ns = ?"
1051 #define MOD_WEBDAV_SQLITE_PROPS_SELECT_PROPS \
1052 "SELECT prop, ns, value FROM properties WHERE resource = ?"
1054 #define MOD_WEBDAV_SQLITE_PROPS_UPDATE_PROP \
1055 "REPLACE INTO properties (resource, prop, ns, value) VALUES (?, ?, ?, ?)"
1057 #define MOD_WEBDAV_SQLITE_PROPS_DELETE_PROP \
1058 "DELETE FROM properties WHERE resource = ? AND prop = ? AND ns = ?"
1060 #define MOD_WEBDAV_SQLITE_PROPS_DELETE \
1061 "DELETE FROM properties WHERE resource = ?"
1063 #define MOD_WEBDAV_SQLITE_PROPS_COPY \
1064 "INSERT INTO properties" \
1065 " SELECT ?, prop, ns, value FROM properties WHERE resource = ?"
1067 #define MOD_WEBDAV_SQLITE_PROPS_MOVE \
1068 "UPDATE OR REPLACE properties SET resource = ? WHERE resource = ?"
1070 #define MOD_WEBDAV_SQLITE_PROPS_MOVE_COL \
1071 "UPDATE OR REPLACE properties SET resource = ? || SUBSTR(resource, ?)" \
1072 " WHERE SUBSTR(resource, 1, ?) = ?"
1074 #define MOD_WEBDAV_SQLITE_LOCKS_ACQUIRE \
1075 "INSERT INTO locks" \
1076 " (locktoken,resource,lockscope,locktype,owner,ownerinfo,depth,timeout)" \
1077 " VALUES (?,?,?,?,?,?,?, CURRENT_TIME + ?)"
1079 #define MOD_WEBDAV_SQLITE_LOCKS_REFRESH \
1080 "UPDATE locks SET timeout = CURRENT_TIME + ? WHERE locktoken = ?"
1082 #define MOD_WEBDAV_SQLITE_LOCKS_RELEASE \
1083 "DELETE FROM locks WHERE locktoken = ?"
1085 #define MOD_WEBDAV_SQLITE_LOCKS_READ \
1086 "SELECT resource, owner, depth" \
1087 " FROM locks WHERE locktoken = ?"
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 = ?"
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))"
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, ?) = ?"
1108 #define MOD_WEBDAV_SQLITE_LOCKS_DELETE_URI \
1109 "DELETE FROM locks WHERE resource = ?"
1111 #define MOD_WEBDAV_SQLITE_LOCKS_DELETE_URI_COL \
1112 "DELETE FROM locks WHERE SUBSTR(resource, 1, ?) = ?"
1113 /*"DELETE FROM locks WHERE locktoken LIKE ? || '%'"*/
1115 /*(not currently used)*/
1116 #define MOD_WEBDAV_SQLITE_LOCKS_DELETE_EXPIRED \
1117 "DELETE FROM locks WHERE timeout < CURRENT_TIME"
1119 #endif /* USE_PROPPATCH */
1122 __attribute_cold__
1123 static handler_t
1124 mod_webdav_sqlite3_init (plugin_config * const restrict s,
1125 log_error_st * const errh)
1127 #ifndef USE_PROPPATCH
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;
1135 #else /* USE_PROPPATCH */
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; \
1146 sqlite3_free(err); \
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;
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");
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;
1185 sqlite3_close(sql->sqlh);
1186 sql->sqlh = NULL;
1188 return HANDLER_GO_ON;
1190 #endif /* USE_PROPPATCH */
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)
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; \
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;
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 */
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);
1262 return HANDLER_GO_ON;
1265 #endif /* USE_PROPPATCH */
1268 SERVER_FUNC(mod_webdav_worker_init)
1270 #ifdef USE_PROPPATCH
1271 /* open sqlite databases and prepare SQL statements in each worker process
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.
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;
1285 #else
1286 UNUSED(srv);
1287 UNUSED(p_d);
1288 #endif /* USE_PROPPATCH */
1289 return HANDLER_GO_ON;
1293 #ifdef USE_PROPPATCH
1294 static int
1295 webdav_db_transaction (const plugin_config * const pconf,
1296 const char * const action)
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;
1314 #define webdav_db_transaction_begin(pconf) \
1315 webdav_db_transaction(pconf, "BEGIN;")
1317 #define webdav_db_transaction_begin_immediate(pconf) \
1318 webdav_db_transaction(pconf, "BEGIN IMMEDIATE;")
1320 #define webdav_db_transaction_commit(pconf) \
1321 webdav_db_transaction(pconf, "COMMIT;")
1323 #define webdav_db_transaction_rollback(pconf) \
1324 webdav_db_transaction(pconf, "ROLLBACK;")
1326 #else
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
1333 #endif
1336 #ifdef USE_LOCKS
1337 static int
1338 webdav_lock_match (const plugin_config * const pconf,
1339 const webdav_lockdata * const lockdata)
1341 if (!pconf->sql)
1342 return 0;
1343 sqlite3_stmt * const stmt = pconf->sql->stmt_locks_read;
1344 if (!stmt)
1345 return 0;
1347 sqlite3_bind_text(
1348 stmt, 1, CONST_BUF_LEN(&lockdata->locktoken), SQLITE_STATIC);
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 */
1368 else
1369 status = -2; /* URI is not in scope of lock */
1372 sqlite3_reset(stmt);
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
1380 return status;
1382 #endif
1385 #ifdef USE_LOCKS
1386 static void
1387 webdav_lock_activelocks_lockdata (sqlite3_stmt * const stmt,
1388 webdav_lockdata * const lockdata)
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);
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;
1413 typedef
1414 void webdav_lock_activelocks_cb(void * const vdata,
1415 const webdav_lockdata * const lockdata);
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)
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;
1431 if (!pconf->sql)
1432 return;
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;
1440 sqlite3_bind_text(stmt, 1, CONST_BUF_LEN(uri), SQLITE_STATIC);
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;
1448 webdav_lock_activelocks_lockdata(stmt, &lockdata);
1449 if (lockdata.timeout > 0)
1450 lock_cb(vdata, &lockdata);
1453 sqlite3_reset(stmt);
1455 if (0 == expand_checks)
1456 return;
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;
1462 sqlite3_bind_text(stmt, 1, CONST_BUF_LEN(uri), SQLITE_STATIC);
1464 while (SQLITE_ROW == sqlite3_step(stmt)) {
1465 webdav_lock_activelocks_lockdata(stmt, &lockdata);
1466 if (lockdata.timeout > 0)
1467 lock_cb(vdata, &lockdata);
1470 sqlite3_reset(stmt);
1472 if (1 == expand_checks)
1473 return;
1475 #ifdef __COVERITY__
1476 force_assert(0 != uri->used);
1477 #endif
1479 /* check for locks on members within (internal to) collection */
1480 stmt = pconf->sql->stmt_locks_read_uri_members;
1482 sqlite3_bind_int( stmt, 1, (int)uri->used-1);
1483 sqlite3_bind_text(stmt, 2, CONST_BUF_LEN(uri), SQLITE_STATIC);
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;
1490 webdav_lock_activelocks_lockdata(stmt, &lockdata);
1491 if (lockdata.timeout > 0)
1492 lock_cb(vdata, &lockdata);
1495 sqlite3_reset(stmt);
1497 #endif
1500 static int
1501 webdav_lock_delete_uri (const plugin_config * const pconf,
1502 const buffer * const uri)
1504 #ifdef USE_LOCKS
1506 if (!pconf->sql)
1507 return 0;
1508 sqlite3_stmt * const stmt = pconf->sql->stmt_locks_delete_uri;
1509 if (!stmt)
1510 return 0;
1512 sqlite3_bind_text(stmt, 1, CONST_BUF_LEN(uri), SQLITE_STATIC);
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
1524 sqlite3_reset(stmt);
1526 return status;
1528 #else
1529 UNUSED(pconf);
1530 UNUSED(uri);
1531 return 1;
1532 #endif
1536 static int
1537 webdav_lock_delete_uri_col (const plugin_config * const pconf,
1538 const buffer * const uri)
1540 #ifdef USE_LOCKS
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;
1548 #ifdef __COVERITY__
1549 force_assert(0 != uri->used);
1550 #endif
1552 sqlite3_bind_int( stmt, 1, (int)uri->used-1);
1553 sqlite3_bind_text(stmt, 2, CONST_BUF_LEN(uri), SQLITE_STATIC);
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
1565 sqlite3_reset(stmt);
1567 return status;
1569 #else
1570 UNUSED(pconf);
1571 UNUSED(uri);
1572 return 1;
1573 #endif
1577 #ifdef USE_LOCKS
1578 static int
1579 webdav_lock_acquire (const plugin_config * const pconf,
1580 const webdav_lockdata * const lockdata)
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
1589 if (!pconf->sql)
1590 return 0;
1591 sqlite3_stmt * const stmt = pconf->sql->stmt_locks_acquire;
1592 if (!stmt)
1593 return 0;
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);
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
1630 sqlite3_reset(stmt);
1632 return status;
1634 #endif
1637 #ifdef USE_LOCKS
1638 static int
1639 webdav_lock_refresh (const plugin_config * const pconf,
1640 webdav_lockdata * const lockdata)
1642 if (!pconf->sql)
1643 return 0;
1644 sqlite3_stmt * const stmt = pconf->sql->stmt_locks_refresh;
1645 if (!stmt)
1646 return 0;
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);
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
1662 sqlite3_reset(stmt);
1664 /*(future: fill in lockscope, locktype, depth from database)*/
1666 return status;
1668 #endif
1671 #ifdef USE_LOCKS
1672 static int
1673 webdav_lock_release (const plugin_config * const pconf,
1674 const webdav_lockdata * const lockdata)
1676 if (!pconf->sql)
1677 return 0;
1678 sqlite3_stmt * const stmt = pconf->sql->stmt_locks_release;
1679 if (!stmt)
1680 return 0;
1682 sqlite3_bind_text(
1683 stmt, 1, CONST_BUF_LEN(&lockdata->locktoken), SQLITE_STATIC);
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
1696 sqlite3_reset(stmt);
1698 return status;
1700 #endif
1703 static int
1704 webdav_prop_move_uri (const plugin_config * const pconf,
1705 const buffer * const src,
1706 const buffer * const dst)
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;
1715 sqlite3_bind_text(stmt, 1, CONST_BUF_LEN(dst), SQLITE_STATIC);
1716 sqlite3_bind_text(stmt, 2, CONST_BUF_LEN(src), SQLITE_STATIC);
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
1726 sqlite3_reset(stmt);
1728 #else
1729 UNUSED(pconf);
1730 UNUSED(src);
1731 UNUSED(dst);
1732 #endif
1734 return 0;
1738 static int
1739 webdav_prop_move_uri_col (const plugin_config * const pconf,
1740 const buffer * const src,
1741 const buffer * const dst)
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;
1750 #ifdef __COVERITY__
1751 force_assert(0 != src->used);
1752 #endif
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);
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
1767 sqlite3_reset(stmt);
1769 #else
1770 UNUSED(pconf);
1771 UNUSED(src);
1772 UNUSED(dst);
1773 #endif
1775 return 0;
1779 static int
1780 webdav_prop_delete_uri (const plugin_config * const pconf,
1781 const buffer * const uri)
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;
1790 sqlite3_bind_text(stmt, 1, CONST_BUF_LEN(uri), SQLITE_STATIC);
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
1800 sqlite3_reset(stmt);
1802 #else
1803 UNUSED(pconf);
1804 UNUSED(uri);
1805 #endif
1807 return 0;
1811 static int
1812 webdav_prop_copy_uri (const plugin_config * const pconf,
1813 const buffer * const src,
1814 const buffer * const dst)
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;
1823 sqlite3_bind_text(stmt, 1, CONST_BUF_LEN(dst), SQLITE_STATIC);
1824 sqlite3_bind_text(stmt, 2, CONST_BUF_LEN(src), SQLITE_STATIC);
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
1834 sqlite3_reset(stmt);
1836 #else
1837 UNUSED(pconf);
1838 UNUSED(dst);
1839 UNUSED(src);
1840 #endif
1842 return 0;
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)
1853 if (!pconf->sql)
1854 return 0;
1855 sqlite3_stmt * const stmt = pconf->sql->stmt_props_delete_prop;
1856 if (!stmt)
1857 return 0;
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);
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
1871 sqlite3_reset(stmt);
1873 return 0;
1875 #endif
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)
1886 if (!pconf->sql)
1887 return 0;
1888 sqlite3_stmt * const stmt = pconf->sql->stmt_props_update_prop;
1889 if (!stmt)
1890 return 0;
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);
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
1905 sqlite3_reset(stmt);
1907 return 0;
1909 #endif
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)
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 */
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);
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 */
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 */
1946 static void
1947 webdav_prop_select_props (const plugin_config * const pconf,
1948 const buffer * const uri,
1949 buffer * const b)
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;
1958 sqlite3_bind_text(stmt, 1, CONST_BUF_LEN(uri), SQLITE_STATIC);
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));
1970 sqlite3_reset(stmt);
1971 #else
1972 UNUSED(pconf);
1973 UNUSED(uri);
1974 UNUSED(b);
1975 #endif
1979 static int
1980 webdav_prop_select_propnames (const plugin_config * const pconf,
1981 const buffer * const uri,
1982 buffer * const b)
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;
1991 /* get all property names (EMPTY) */
1992 sqlite3_bind_text(stmt, 1, CONST_BUF_LEN(uri), SQLITE_STATIC);
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);
2003 sqlite3_reset(stmt);
2005 #else
2006 UNUSED(pconf);
2007 UNUSED(uri);
2008 UNUSED(b);
2009 #endif
2011 return 0;
2015 #if defined(__APPLE__) && defined(__MACH__)
2016 #include <copyfile.h> /* fcopyfile() *//* OS X 10.5+ */
2017 #endif
2018 #ifdef HAVE_ELFTC_COPYFILE/* __FreeBSD__ */
2019 #include <libelftc.h> /* elftc_copyfile() */
2020 #endif
2021 #ifdef __linux__
2022 #include <sys/sendfile.h> /* sendfile() */
2023 #endif
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
2033 __attribute_noinline__
2034 static int
2035 webdav_fcopyfile_sz (int ifd, int ofd, off_t isz)
2037 if (0 == isz)
2038 return 0;
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
2048 #if defined(__APPLE__) && defined(__MACH__)
2049 if (0 == fcopyfile(ifd, ofd, NULL, COPYFILE_ALL))
2050 return 0;
2052 if (0 != lseek(ifd, 0, SEEK_SET)) return -1;
2053 if (0 != lseek(ofd, 0, SEEK_SET)) return -1;
2054 #endif
2056 #ifdef HAVE_ELFTC_COPYFILE /* __FreeBSD__ */
2057 if (0 == elftc_copyfile(ifd, ofd))
2058 return 0;
2060 if (0 != lseek(ifd, 0, SEEK_SET)) return -1;
2061 if (0 != lseek(ofd, 0, SEEK_SET)) return -1;
2062 #endif
2064 #ifdef __linux__ /* Linux 2.6.33+ sendfile() supports file-to-file copy */
2065 off_t offset = 0;
2066 while (offset < isz && sendfile(ifd,ofd,&offset,(size_t)(isz-offset)) >= 0);
2067 if (offset == isz)
2068 return 0;
2070 /*lseek(ifd, 0, SEEK_SET);*/ /*(ifd offset not modified due to &offset arg)*/
2071 if (0 != lseek(ofd, 0, SEEK_SET)) return -1;
2072 #endif
2074 ssize_t rd, wr, off;
2075 char buf[16384];
2076 do {
2077 do {
2078 rd = read(ifd, buf, sizeof(buf));
2079 } while (-1 == rd && errno == EINTR);
2080 if (rd < 0) return rd;
2082 off = 0;
2083 do {
2084 wr = write(ofd, buf+off, (size_t)(rd-off));
2085 } while (wr >= 0 ? (off += wr) != rd : errno == EINTR);
2086 if (wr < 0) return -1;
2087 } while (rd > 0);
2088 return rd;
2092 static int
2093 webdav_if_match_or_unmodified_since (connection * const con, struct stat *st)
2095 buffer *im = (0 != con->etag_flags)
2096 ? http_header_request_get(con, HTTP_HEADER_OTHER,
2097 CONST_STR_LEN("If-Match"))
2098 : NULL;
2100 buffer *inm = (0 != con->etag_flags)
2101 ? http_header_request_get(con, HTTP_HEADER_IF_NONE_MATCH,
2102 CONST_STR_LEN("If-None-Match"))
2103 : NULL;
2105 buffer *ius =
2106 http_header_request_get(con, HTTP_HEADER_OTHER,
2107 CONST_STR_LEN("If-Unmodified-Since"));
2109 if (NULL == im && NULL == inm && NULL == ius) return 0;
2111 struct stat stp;
2112 if (NULL == st)
2113 st = (0 == lstat(con->physical.path->ptr, &stp)) ? &stp : NULL;
2115 buffer *etagb = con->physical.etag;
2116 if (NULL != st && (NULL != im || NULL != inm)) {
2117 etag_create(etagb, st, con->etag_flags);
2118 etag_mutate(etagb, etagb);
2121 if (NULL != im) {
2122 if (NULL == st || !etag_is_equal(etagb, im->ptr, 0))
2123 return 412; /* Precondition Failed */
2126 if (NULL != inm) {
2127 if (NULL == st
2128 ? !buffer_is_equal_string(inm,CONST_STR_LEN("*"))
2129 || (errno != ENOENT && errno != ENOTDIR)
2130 : etag_is_equal(etagb, inm->ptr, 1))
2131 return 412; /* Precondition Failed */
2134 if (NULL != ius) {
2135 if (NULL == st)
2136 return 412; /* Precondition Failed */
2137 struct tm itm, *ftm = gmtime(&st->st_mtime);
2138 if (NULL == strptime(ius->ptr, "%a, %d %b %Y %H:%M:%S GMT", &itm)
2139 || mktime(ftm) > mktime(&itm)) { /* timegm() not standard */
2140 return 412; /* Precondition Failed */
2144 return 0;
2148 static void
2149 webdav_response_etag (const plugin_config * const pconf,
2150 connection * const con, struct stat *st)
2152 server *srv = pconf->srv;
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));
2162 else {
2163 stat_cache_update_entry(srv,CONST_BUF_LEN(con->physical.path),st,NULL);
2168 static void
2169 webdav_parent_modified (const plugin_config * const pconf, const buffer *path)
2171 size_t dirlen = buffer_string_length(path);
2172 const char *fn = path->ptr;
2173 /*force_assert(0 != dirlen);*/
2174 /*force_assert(fn[0] == '/');*/
2175 if (fn[dirlen-1] == '/') --dirlen;
2176 if (0 != dirlen) while (fn[--dirlen] != '/') ;
2177 if (0 == dirlen) dirlen = 1; /* root dir ("/") */
2178 stat_cache_invalidate_entry(pconf->srv, fn, dirlen);
2182 static int
2183 webdav_parse_Depth (connection * const con)
2185 /* Depth = "Depth" ":" ("0" | "1" | "infinity") */
2186 const buffer * const h =
2187 http_header_request_get(con, HTTP_HEADER_OTHER, CONST_STR_LEN("Depth"));
2188 if (NULL != h) {
2189 /* (leading LWS is removed during header parsing in request.c) */
2190 switch (*h->ptr) {
2191 case '0': return 0;
2192 case '1': return 1;
2193 /*case 'i':*/ /* e.g. "infinity" */
2194 /*case 'I':*/ /* e.g. "Infinity" */
2195 default: return -1;/* treat not-'0' and not-'1' as "infinity" */
2199 return -1; /* default value is -1 to represent "infinity" */
2203 static int
2204 webdav_unlinkat (const plugin_config * const pconf, const buffer * const uri,
2205 const int dfd, const char * const d_name, size_t len)
2207 if (0 == unlinkat(dfd, d_name, 0)) {
2208 stat_cache_delete_entry(pconf->srv, d_name, len);
2209 return webdav_prop_delete_uri(pconf, uri);
2212 switch(errno) {
2213 case EACCES: case EPERM: return 403; /* Forbidden */
2214 case ENOENT: return 404; /* Not Found */
2215 default: return 501; /* Not Implemented */
2220 static int
2221 webdav_delete_file (const plugin_config * const pconf,
2222 const physical_st * const dst)
2224 if (0 == unlink(dst->path->ptr)) {
2225 stat_cache_delete_entry(pconf->srv, CONST_BUF_LEN(dst->path));
2226 return webdav_prop_delete_uri(pconf, dst->rel_path);
2229 switch(errno) {
2230 case EACCES: case EPERM: return 403; /* Forbidden */
2231 case ENOENT: return 404; /* Not Found */
2232 default: return 501; /* Not Implemented */
2237 static int
2238 webdav_delete_dir (const plugin_config * const pconf,
2239 physical_st * const dst,
2240 buffer * const b,
2241 const int flags)
2243 int multi_status = 0;
2244 const int dfd = fdevent_open_dirname(dst->path->ptr, 0);
2245 DIR * const dir = (dfd >= 0) ? fdopendir(dfd) : NULL;
2246 if (NULL == dir) {
2247 if (dfd >= 0) close(dfd);
2248 webdav_xml_response_status(b, dst->rel_path, 403);
2249 return 1;
2252 /* dst is modified in place to extend path,
2253 * so be sure to restore to base each loop iter */
2254 const uint32_t dst_path_used = dst->path->used;
2255 const uint32_t dst_rel_path_used = dst->rel_path->used;
2256 int s_isdir;
2257 struct dirent *de;
2258 while (NULL != (de = readdir(dir))) {
2259 if (de->d_name[0] == '.'
2260 && (de->d_name[1] == '\0'
2261 || (de->d_name[1] == '.' && de->d_name[2] == '\0')))
2262 continue; /* ignore "." and ".." */
2264 #ifdef _DIRENT_HAVE_D_TYPE
2265 if (de->d_type != DT_UNKNOWN)
2266 s_isdir = (de->d_type == DT_DIR);
2267 else
2268 #endif
2270 struct stat st;
2271 if (0 != fstatat(dfd, de->d_name, &st, AT_SYMLINK_NOFOLLOW))
2272 continue; /* file *just* disappeared? */
2273 /* parent rmdir() will fail later if file still exists
2274 * and fstatat() failed for other reasons */
2275 s_isdir = S_ISDIR(st.st_mode);
2278 const uint32_t len = (uint32_t) _D_EXACT_NAMLEN(de);
2279 if (flags & WEBDAV_FLAG_LC_NAMES) /*(needed at least for rel_path)*/
2280 webdav_str_len_to_lower(de->d_name, len);
2281 buffer_append_string_len(dst->path, de->d_name, len);
2282 buffer_append_string_len(dst->rel_path, de->d_name, len);
2284 if (s_isdir) {
2285 buffer_append_string_len(dst->path, CONST_STR_LEN("/"));
2286 buffer_append_string_len(dst->rel_path, CONST_STR_LEN("/"));
2287 multi_status |= webdav_delete_dir(pconf, dst, b, flags);
2289 else {
2290 int status =
2291 webdav_unlinkat(pconf, dst->rel_path, dfd, de->d_name, len);
2292 if (0 != status) {
2293 webdav_xml_response_status(b, dst->rel_path, status);
2294 multi_status = 1;
2298 dst->path->ptr[ (dst->path->used = dst_path_used) -1] = '\0';
2299 dst->rel_path->ptr[(dst->rel_path->used = dst_rel_path_used)-1] = '\0';
2301 closedir(dir);
2303 if (0 == multi_status) {
2304 int rmdir_status;
2305 if (0 == rmdir(dst->path->ptr))
2306 rmdir_status = webdav_prop_delete_uri(pconf, dst->rel_path);
2307 else {
2308 switch(errno) {
2309 case EACCES:
2310 case EPERM: rmdir_status = 403; break; /* Forbidden */
2311 case ENOENT: rmdir_status = 404; break; /* Not Found */
2312 default: rmdir_status = 501; break; /* Not Implemented */
2315 if (0 != rmdir_status) {
2316 webdav_xml_response_status(b, dst->rel_path, rmdir_status);
2317 multi_status = 1;
2321 return multi_status;
2325 static int
2326 webdav_linktmp_rename (const plugin_config * const pconf,
2327 const buffer * const src,
2328 const buffer * const dst)
2330 buffer * const tmpb = pconf->tmpb;
2331 int rc = -1; /*(not zero)*/
2333 buffer_copy_buffer(tmpb, dst);
2334 buffer_append_string_len(tmpb, CONST_STR_LEN("."));
2335 buffer_append_int(tmpb, (long)getpid());
2336 buffer_append_string_len(tmpb, CONST_STR_LEN("."));
2337 buffer_append_uint_hex_lc(tmpb, (uintptr_t)pconf); /*(stack/heap addr)*/
2338 buffer_append_string_len(tmpb, CONST_STR_LEN("~"));
2339 if (buffer_string_length(tmpb) < PATH_MAX
2340 && 0 == linkat(AT_FDCWD, src->ptr, AT_FDCWD, tmpb->ptr, 0)) {
2342 rc = rename(tmpb->ptr, dst->ptr);
2344 /* unconditionally unlink() src if rename() succeeds, just in case
2345 * dst previously existed and was already hard-linked to src. From
2346 * 'man -s 2 rename':
2347 * If oldpath and newpath are existing hard links referring to the
2348 * same file, then rename() does nothing, and returns a success
2349 * status.
2350 * This introduces a small race condition between the rename() and
2351 * unlink() should new file have been created at src in the middle,
2352 * though unlikely if locks are used since locks have not yet been
2353 * released. */
2354 unlink(tmpb->ptr);
2356 return rc;
2360 static int
2361 webdav_copytmp_rename (const plugin_config * const pconf,
2362 const physical_st * const src,
2363 const physical_st * const dst,
2364 const int overwrite)
2366 buffer * const tmpb = pconf->tmpb;
2367 buffer_copy_buffer(tmpb, dst->path);
2368 buffer_append_string_len(tmpb, CONST_STR_LEN("."));
2369 buffer_append_int(tmpb, (long)getpid());
2370 buffer_append_string_len(tmpb, CONST_STR_LEN("."));
2371 buffer_append_uint_hex_lc(tmpb, (uintptr_t)pconf); /*(stack/heap addr)*/
2372 buffer_append_string_len(tmpb, CONST_STR_LEN("~"));
2373 if (buffer_string_length(tmpb) >= PATH_MAX)
2374 return 414; /* URI Too Long */
2376 /* code does not currently support symlinks in webdav collections;
2377 * disallow symlinks as target when opening src and dst */
2378 struct stat st;
2379 const int ifd = fdevent_open_cloexec(src->path->ptr, 0, O_RDONLY, 0);
2380 if (ifd < 0)
2381 return 403; /* Forbidden */
2382 if (0 != fstat(ifd, &st) || !S_ISREG(st.st_mode)) {
2383 close(ifd);
2384 return 403; /* Forbidden */
2386 const int ofd = fdevent_open_cloexec(tmpb->ptr, 0,
2387 O_WRONLY | O_CREAT | O_EXCL | O_TRUNC,
2388 WEBDAV_FILE_MODE);
2389 if (ofd < 0) {
2390 close(ifd);
2391 return 403; /* Forbidden */
2394 /* perform *blocking* copy (not O_NONBLOCK);
2395 * blocks server from doing any other work until after copy completes
2396 * (should reach here only if unable to use link() and rename()
2397 * due to copy/move crossing device boundaries within the workspace) */
2398 int rc = webdav_fcopyfile_sz(ifd, ofd, st.st_size);
2400 close(ifd);
2401 const int wc = close(ofd);
2403 if (0 != rc || 0 != wc) {
2404 /* error reading or writing files */
2405 rc = (0 != wc && wc == ENOSPC) ? 507 : 403;
2406 unlink(tmpb->ptr);
2407 return rc;
2410 #ifndef RENAME_NOREPLACE /*(renameat2() not well-supported yet)*/
2411 if (!overwrite) {
2412 struct stat stb;
2413 if (0 == lstat(dst->path->ptr, &stb) || errno != ENOENT)
2414 return 412; /* Precondition Failed */
2415 /* TOC-TOU race between lstat() and rename(),
2416 * but this is reasonable attempt to not overwrite existing entity */
2418 if (0 == rename(tmpb->ptr, dst->path->ptr))
2419 #else
2420 if (0 == renameat2(AT_FDCWD, tmpb->ptr,
2421 AT_FDCWD, dst->path->ptr,
2422 overwrite ? 0 : RENAME_NOREPLACE))
2423 #endif
2425 /* unconditional stat cache deletion
2426 * (not worth extra syscall/race to detect overwritten or not) */
2427 stat_cache_delete_entry(pconf->srv, CONST_BUF_LEN(dst->path));
2428 return 0;
2430 else {
2431 const int errnum = errno;
2432 unlink(tmpb->ptr);
2433 switch (errnum) {
2434 case ENOENT:
2435 case ENOTDIR:
2436 case EISDIR: return 409; /* Conflict */
2437 case EEXIST: return 412; /* Precondition Failed */
2438 default: return 403; /* Forbidden */
2444 static int
2445 webdav_copymove_file (const plugin_config * const pconf,
2446 const physical_st * const src,
2447 const physical_st * const dst,
2448 int * const flags)
2450 const int overwrite = (*flags & WEBDAV_FLAG_OVERWRITE);
2451 if (*flags & WEBDAV_FLAG_MOVE_RENAME) {
2452 #ifndef RENAME_NOREPLACE /*(renameat2() not well-supported yet)*/
2453 if (!overwrite) {
2454 struct stat st;
2455 if (0 == lstat(dst->path->ptr, &st) || errno != ENOENT)
2456 return 412; /* Precondition Failed */
2457 /* TOC-TOU race between lstat() and rename(),
2458 * but this is reasonable attempt to not overwrite existing entity*/
2460 if (0 == rename(src->path->ptr, dst->path->ptr))
2461 #else
2462 if (0 == renameat2(AT_FDCWD, src->path->ptr,
2463 AT_FDCWD, dst->path->ptr,
2464 overwrite ? 0 : RENAME_NOREPLACE))
2465 #endif
2467 /* unconditionally unlink() src if rename() succeeds, just in case
2468 * dst previously existed and was already hard-linked to src. From
2469 * 'man -s 2 rename':
2470 * If oldpath and newpath are existing hard links referring to the
2471 * same file, then rename() does nothing, and returns a success
2472 * status.
2473 * This introduces a small race condition between the rename() and
2474 * unlink() should new file have been created at src in the middle,
2475 * though unlikely if locks are used since locks have not yet been
2476 * released. */
2477 if (overwrite) unlink(src->path->ptr);
2478 /* unconditional stat cache deletion
2479 * (not worth extra syscall/race to detect overwritten or not) */
2480 stat_cache_delete_entry(pconf->srv, CONST_BUF_LEN(dst->path));
2481 stat_cache_delete_entry(pconf->srv, CONST_BUF_LEN(src->path));
2482 webdav_prop_move_uri(pconf, src->rel_path, dst->rel_path);
2483 return 0;
2485 else if (errno == EEXIST)
2486 return 412; /* Precondition Failed */
2488 else if (*flags & WEBDAV_FLAG_COPY_LINK) {
2489 if (0 == linkat(AT_FDCWD, src->path->ptr, AT_FDCWD, dst->path->ptr, 0)){
2490 webdav_prop_copy_uri(pconf, src->rel_path, dst->rel_path);
2491 return 0;
2493 else if (errno == EEXIST) {
2494 if (!overwrite)
2495 return 412; /* Precondition Failed */
2496 if (0 == webdav_linktmp_rename(pconf, src->path, dst->path)) {
2497 webdav_prop_copy_uri(pconf, src->rel_path, dst->rel_path);
2498 return 0;
2501 else if (errno == EXDEV) {
2502 *flags &= ~WEBDAV_FLAG_COPY_LINK;
2503 *flags |= WEBDAV_FLAG_COPY_XDEV;
2507 /* link() or rename() failed; fall back to copy to tempfile and rename() */
2508 int status = webdav_copytmp_rename(pconf, src, dst, overwrite);
2509 if (0 == status) {
2510 webdav_prop_copy_uri(pconf, src->rel_path, dst->rel_path);
2511 if (*flags & (WEBDAV_FLAG_MOVE_RENAME|WEBDAV_FLAG_MOVE_XDEV))
2512 webdav_delete_file(pconf, src);
2513 /*(copy successful, but how should we report if delete fails?)*/
2515 return status;
2519 static int
2520 webdav_mkdir (const plugin_config * const pconf,
2521 const physical_st * const dst,
2522 const int overwrite)
2524 if (0 == mkdir(dst->path->ptr, WEBDAV_DIR_MODE)) {
2525 webdav_parent_modified(pconf, dst->path);
2526 return 0;
2529 switch (errno) {
2530 case EEXIST:
2531 case ENOTDIR: break;
2532 case ENOENT: return 409; /* Conflict */
2533 case EPERM:
2534 default: return 403; /* Forbidden */
2537 /* [RFC4918] 9.3.1 MKCOL Status Codes
2538 * 405 (Method Not Allowed) -
2539 * MKCOL can only be executed on an unmapped URL.
2541 if (overwrite < 0) /*(mod_webdav_mkcol() passes overwrite = -1)*/
2542 return (errno != ENOTDIR)
2543 ? 405 /* Method Not Allowed */
2544 : 409; /* Conflict */
2546 #ifdef __COVERITY__
2547 force_assert(2 <= dst->path->used);
2548 force_assert(2 <= dst->rel_path->used);
2549 #endif
2551 struct stat st;
2552 int status;
2553 dst->path->ptr[dst->path->used-2] = '\0'; /*(trailing slash)*/
2554 status = lstat(dst->path->ptr, &st);
2555 dst->path->ptr[dst->path->used-2] = '/'; /*(restore slash)*/
2556 if (0 != status) /* still ENOTDIR or *just* disappeared */
2557 return 409; /* Conflict */
2559 if (!overwrite) /* copying into a non-dir ? */
2560 return 409; /* Conflict */
2562 if (S_ISDIR(st.st_mode))
2563 return 0;
2565 dst->path->ptr[dst->path->used-2] = '\0'; /*(trailing slash)*/
2566 dst->rel_path->ptr[dst->rel_path->used-2] = '\0';
2567 status = webdav_delete_file(pconf, dst);
2568 dst->path->ptr[dst->path->used-2] = '/'; /*(restore slash)*/
2569 dst->rel_path->ptr[dst->rel_path->used-2] = '/';
2570 if (0 != status)
2571 return status;
2573 webdav_parent_modified(pconf, dst->path);
2574 return (0 == mkdir(dst->path->ptr, WEBDAV_DIR_MODE))
2576 : 409; /* Conflict */
2580 static int
2581 webdav_copymove_dir (const plugin_config * const pconf,
2582 physical_st * const src,
2583 physical_st * const dst,
2584 buffer * const b,
2585 int flags)
2587 /* NOTE: merging collections is NON-CONFORMANT behavior
2588 * (specified in [RFC4918])
2590 * However, merging collections during COPY/MOVE might be expected behavior
2591 * by client, as merging is the behavior of unix cp -r (recursive copy) as
2592 * well as how Microsoft Windows Explorer performs folder copies.
2594 * [RFC4918] 9.8.4 COPY and Overwriting Destination Resources
2595 * When a collection is overwritten, the membership of the destination
2596 * collection after the successful COPY request MUST be the same
2597 * membership as the source collection immediately before the COPY. Thus,
2598 * merging the membership of the source and destination collections
2599 * together in the destination is not a compliant behavior.
2600 * [Ed: strange how non-compliance statement is immediately followed by:]
2601 * In general, if clients require the state of the destination URL to be
2602 * wiped out prior to a COPY (e.g., to force live properties to be reset),
2603 * then the client could send a DELETE to the destination before the COPY
2604 * request to ensure this reset.
2605 * [Ed: if non-compliant merge behavior is the default here, and were it to
2606 * not be desired by client, client could send a DELETE to the destination
2607 * before issuing COPY. There is no easy way to obtain merge behavior
2608 * (were it not the non-compliant default here) unless the client recurses
2609 * into the source and destination, and creates a list of objects that need
2610 * to be copied. This could fail or miss files due to racing with other
2611 * clients. All of this might forget to emphasize that wiping out an
2612 * existing destination collection (a recursive operation) is dangerous and
2613 * would happen if the client set Overwrite: T or omitted setting Overwrite
2614 * since Overwrite: T is default (client must explicitly set Overwrite: F)]
2615 * [RFC4918] 9.9.3 MOVE and the Overwrite Header
2616 * If a resource exists at the destination and the Overwrite header is
2617 * "T", then prior to performing the move, the server MUST perform a
2618 * DELETE with "Depth: infinity" on the destination resource. If the
2619 * Overwrite header is set to "F", then the operation will fail.
2622 /* NOTE: aborting if 507 Insufficient Storage is NON-CONFORMANT behavior
2623 * [RFC4918] specifies that as much as possible of COPY or MOVE
2624 * should be completed.
2627 /* ??? upon encountering errors, should src->rel_path or dst->rel_path
2628 * be used in XML error ??? */
2630 struct stat st;
2631 int status;
2632 int dfd;
2634 int make_destdir = 1;
2635 const int overwrite = (flags & WEBDAV_FLAG_OVERWRITE);
2636 if (flags & WEBDAV_FLAG_MOVE_RENAME) {
2637 #ifndef RENAME_NOREPLACE /*(renameat2() not well-supported yet)*/
2638 if (!overwrite) {
2639 if (0 == lstat(dst->path->ptr, &st) || errno != ENOENT) {
2640 webdav_xml_response_status(b, src->rel_path, 412);
2641 return 412; /* Precondition Failed */
2643 /* TOC-TOU race between lstat() and rename(),
2644 * but this is reasonable attempt to not overwrite existing entity*/
2646 if (0 == rename(src->path->ptr, dst->path->ptr))
2647 #else
2648 if (0 == renameat2(AT_FDCWD, src->path->ptr,
2649 AT_FDCWD, dst->path->ptr,
2650 overwrite ? 0 : RENAME_NOREPLACE))
2651 #endif
2653 webdav_prop_move_uri_col(pconf, src->rel_path, dst->rel_path);
2654 return 0;
2656 else {
2657 switch (errno) {
2658 case EEXIST:
2659 case ENOTEMPTY:
2660 if (!overwrite) {
2661 webdav_xml_response_status(b, src->rel_path, 412);
2662 return 412; /* Precondition Failed */
2664 make_destdir = 0;
2665 break;
2666 case ENOTDIR:
2667 if (!overwrite) {
2668 webdav_xml_response_status(b, src->rel_path, 409);
2669 return 409; /* Conflict */
2672 #ifdef __COVERITY__
2673 force_assert(2 <= dst->path->used);
2674 #endif
2676 dst->path->ptr[dst->path->used-2] = '\0'; /*(trailing slash)*/
2677 status = lstat(dst->path->ptr, &st);
2678 dst->path->ptr[dst->path->used-2] = '/'; /*(restore slash)*/
2679 if (0 == status) {
2680 if (S_ISDIR(st.st_mode)) {
2681 make_destdir = 0;
2682 break;
2685 #ifdef __COVERITY__
2686 force_assert(2 <= dst->rel_path->used);
2687 #endif
2689 dst->path->ptr[dst->path->used-2] = '\0'; /*(remove slash)*/
2690 dst->rel_path->ptr[dst->rel_path->used-2] = '\0';
2691 status = webdav_delete_file(pconf, dst);
2692 dst->path->ptr[dst->path->used-2] = '/'; /*(restore slash)*/
2693 dst->rel_path->ptr[dst->rel_path->used-2] = '/';
2694 if (0 != status) {
2695 webdav_xml_response_status(b, src->rel_path, status);
2696 return status;
2699 if (0 == rename(src->path->ptr, dst->path->ptr)) {
2700 webdav_prop_move_uri_col(pconf, src->rel_path,
2701 dst->rel_path);
2702 return 0;
2705 break;
2706 case EXDEV:
2707 flags &= ~WEBDAV_FLAG_MOVE_RENAME;
2708 flags |= WEBDAV_FLAG_MOVE_XDEV;
2709 /* (if overwrite, then could switch to WEBDAV_FLAG_COPY_XDEV
2710 * and set a flag so that before returning from this routine,
2711 * directory is deleted recursively, instead of deleting each
2712 * file after each copy. Only reliable if overwrite is set
2713 * since if it is not set, an error would leave file copies in
2714 * two places and would be difficult to recover if !overwrite)
2715 * (collections typically do not cross devices, so this is not
2716 * expected to be a common case) */
2717 break;
2718 default:
2719 break;
2724 if (make_destdir) {
2725 if (0 != (status = webdav_mkdir(pconf, dst, overwrite))) {
2726 webdav_xml_response_status(b, src->rel_path, status);
2727 return status;
2731 webdav_prop_copy_uri(pconf, src->rel_path, dst->rel_path);
2733 /* copy from src to dst (and, if move, then delete src)
2734 * src and dst are modified in place to extend path,
2735 * so be sure to restore to base each loop iter */
2737 const uint32_t src_path_used = src->path->used;
2738 const uint32_t src_rel_path_used = src->rel_path->used;
2739 const uint32_t dst_path_used = dst->path->used;
2740 const uint32_t dst_rel_path_used = dst->rel_path->used;
2742 dfd = fdevent_open_dirname(src->path->ptr, 0);
2743 DIR * const srcdir = (dfd >= 0) ? fdopendir(dfd) : NULL;
2744 if (NULL == srcdir) {
2745 if (dfd >= 0) close(dfd);
2746 webdav_xml_response_status(b, src->rel_path, 403);
2747 return 403; /* Forbidden */
2749 mode_t d_type;
2750 int multi_status = 0;
2751 struct dirent *de;
2752 while (NULL != (de = readdir(srcdir))) {
2753 if (de->d_name[0] == '.'
2754 && (de->d_name[1] == '\0'
2755 || (de->d_name[1] == '.' && de->d_name[2] == '\0')))
2756 continue; /* ignore "." and ".." */
2758 #ifdef _DIRENT_HAVE_D_TYPE
2759 if (de->d_type != DT_UNKNOWN)
2760 d_type = DTTOIF(de->d_type);
2761 else
2762 #endif
2764 if (0 != fstatat(dfd, de->d_name, &st, AT_SYMLINK_NOFOLLOW))
2765 continue; /* file *just* disappeared? */
2766 d_type = st.st_mode;
2769 const uint32_t len = (uint32_t) _D_EXACT_NAMLEN(de);
2770 if (flags & WEBDAV_FLAG_LC_NAMES) /*(needed at least for rel_path)*/
2771 webdav_str_len_to_lower(de->d_name, len);
2773 buffer_append_string_len(src->path, de->d_name, len);
2774 buffer_append_string_len(dst->path, de->d_name, len);
2775 buffer_append_string_len(src->rel_path, de->d_name, len);
2776 buffer_append_string_len(dst->rel_path, de->d_name, len);
2778 if (S_ISDIR(d_type)) { /* recursive call; depth first */
2779 buffer_append_string_len(src->path, CONST_STR_LEN("/"));
2780 buffer_append_string_len(dst->path, CONST_STR_LEN("/"));
2781 buffer_append_string_len(src->rel_path, CONST_STR_LEN("/"));
2782 buffer_append_string_len(dst->rel_path, CONST_STR_LEN("/"));
2783 status = webdav_copymove_dir(pconf, src, dst, b, flags);
2784 if (0 != status)
2785 multi_status = 1;
2787 else if (S_ISREG(d_type)) {
2788 status = webdav_copymove_file(pconf, src, dst, &flags);
2789 if (0 != status)
2790 webdav_xml_response_status(b, src->rel_path, status);
2792 #if 0
2793 else if (S_ISLNK(d_type)) {
2794 /*(might entertain support in future, including readlink()
2795 * and changing dst symlink to be relative to new location.
2796 * (or, if absolute to the old location, then absolute to new)
2797 * Be sure to hard-link using linkat() w/o AT_SYMLINK_FOLLOW)*/
2799 #endif
2800 else {
2801 status = 0;
2804 src->path->ptr[ (src->path->used = src_path_used) -1] = '\0';
2805 src->rel_path->ptr[(src->rel_path->used = src_rel_path_used)-1] = '\0';
2806 dst->path->ptr[ (dst->path->used = dst_path_used) -1] = '\0';
2807 dst->rel_path->ptr[(dst->rel_path->used = dst_rel_path_used)-1] = '\0';
2809 if (507 == status) {
2810 multi_status = 507; /* Insufficient Storage */
2811 break;
2814 closedir(srcdir);
2816 if (0 == multi_status) {
2817 if (flags & (WEBDAV_FLAG_MOVE_RENAME|WEBDAV_FLAG_MOVE_XDEV)) {
2818 status = webdav_delete_dir(pconf, src, b, flags); /* content */
2819 if (0 != status) {
2820 webdav_xml_response_status(b, src->rel_path, status);
2821 multi_status = 1;
2826 return multi_status;
2830 typedef struct webdav_propfind_bufs {
2831 connection * restrict con;
2832 const plugin_config * restrict pconf;
2833 physical_st * restrict dst;
2834 buffer * restrict b;
2835 buffer * restrict b_200;
2836 buffer * restrict b_404;
2837 webdav_property_names proplist;
2838 int allprop;
2839 int propname;
2840 int lockdiscovery;
2841 int depth;
2842 struct stat st;
2843 } webdav_propfind_bufs;
2846 enum webdav_live_props_e {
2847 WEBDAV_PROP_UNSET = -1 /* (enum value to avoid compiler warning)*/
2848 ,WEBDAV_PROP_ALL = 0 /* (ALL not really a prop; internal use) */
2849 /*,WEBDAV_PROP_CREATIONDATE*/ /* (located in database, if present) */
2850 /*,WEBDAV_PROP_DISPLAYNAME*/ /* (located in database, if present) */
2851 /*,WEBDAV_PROP_GETCONTENTLANGUAGE*/ /* (located in database, if present) */
2852 ,WEBDAV_PROP_GETCONTENTLENGTH
2853 ,WEBDAV_PROP_GETCONTENTTYPE
2854 ,WEBDAV_PROP_GETETAG
2855 ,WEBDAV_PROP_GETLASTMODIFIED
2856 /*,WEBDAV_PROP_LOCKDISCOVERY*/ /* (located in database, if present) */
2857 ,WEBDAV_PROP_RESOURCETYPE
2858 /*,WEBDAV_PROP_SOURCE*/ /* not implemented; removed in RFC4918 */
2859 ,WEBDAV_PROP_SUPPORTEDLOCK
2863 #ifdef USE_PROPPATCH
2865 struct live_prop_list {
2866 const char *prop;
2867 const uint32_t len;
2868 enum webdav_live_props_e pnum;
2871 static const struct live_prop_list live_properties[] = { /*(namespace "DAV:")*/
2872 /* { CONST_STR_LEN("creationdate"), WEBDAV_PROP_CREATIONDATE }*/
2873 /*,{ CONST_STR_LEN("displayname"), WEBDAV_PROP_DISPLAYNAME }*/
2874 /*,{ CONST_STR_LEN("getcontentlanguage"), WEBDAV_PROP_GETCONTENTLANGUAGE}*/
2875 { CONST_STR_LEN("getcontentlength"), WEBDAV_PROP_GETCONTENTLENGTH }
2876 ,{ CONST_STR_LEN("getcontenttype"), WEBDAV_PROP_GETCONTENTTYPE }
2877 ,{ CONST_STR_LEN("getetag"), WEBDAV_PROP_GETETAG }
2878 ,{ CONST_STR_LEN("getlastmodified"), WEBDAV_PROP_GETLASTMODIFIED }
2879 #ifdef USE_LOCKS
2880 /*,{ CONST_STR_LEN("lockdiscovery"), WEBDAV_PROP_LOCKDISCOVERY }*/
2881 #endif
2882 ,{ CONST_STR_LEN("resourcetype"), WEBDAV_PROP_RESOURCETYPE }
2883 /*,{ CONST_STR_LEN("source"), WEBDAV_PROP_SOURCE }*/
2884 #ifdef USE_LOCKS
2885 ,{ CONST_STR_LEN("supportedlock"), WEBDAV_PROP_SUPPORTEDLOCK }
2886 #endif
2888 ,{ NULL, 0, WEBDAV_PROP_UNSET }
2891 /* protected live properties
2892 * (must also protect creationdate and lockdiscovery in database) */
2893 static const struct live_prop_list protected_props[] = { /*(namespace "DAV:")*/
2894 { CONST_STR_LEN("creationdate"), WEBDAV_PROP_UNSET
2895 /*WEBDAV_PROP_CREATIONDATE*/ }
2896 /*,{ CONST_STR_LEN("displayname"), WEBDAV_PROP_DISPLAYNAME }*/
2897 /*,{ CONST_STR_LEN("getcontentlanguage"), WEBDAV_PROP_GETCONTENTLANGUAGE}*/
2898 ,{ CONST_STR_LEN("getcontentlength"), WEBDAV_PROP_GETCONTENTLENGTH }
2899 ,{ CONST_STR_LEN("getcontenttype"), WEBDAV_PROP_GETCONTENTTYPE }
2900 ,{ CONST_STR_LEN("getetag"), WEBDAV_PROP_GETETAG }
2901 ,{ CONST_STR_LEN("getlastmodified"), WEBDAV_PROP_GETLASTMODIFIED }
2902 ,{ CONST_STR_LEN("lockdiscovery"), WEBDAV_PROP_UNSET
2903 /*WEBDAV_PROP_LOCKDISCOVERY*/ }
2904 ,{ CONST_STR_LEN("resourcetype"), WEBDAV_PROP_RESOURCETYPE }
2905 /*,{ CONST_STR_LEN("source"), WEBDAV_PROP_SOURCE }*/
2906 ,{ CONST_STR_LEN("supportedlock"), WEBDAV_PROP_SUPPORTEDLOCK }
2908 ,{ NULL, 0, WEBDAV_PROP_UNSET }
2911 #endif
2914 static int
2915 webdav_propfind_live_props (const webdav_propfind_bufs * const restrict pb,
2916 const enum webdav_live_props_e pnum)
2918 buffer * const restrict b = pb->b_200;
2919 switch (pnum) {
2920 case WEBDAV_PROP_ALL:
2921 /*(fall through)*/
2922 /*case WEBDAV_PROP_CREATIONDATE:*/ /* (located in database, if present)*/
2923 #if 0
2924 case WEBDAV_PROP_CREATIONDATE: {
2925 /* st->st_ctim
2926 * defined by POSIX.1-2008 as last file status change timestamp
2927 * and is no long create-time (as it may have been on older filesystems)
2928 * Therefore, this should return Not Found.
2929 * [RFC4918] 15.1 creationdate Property
2930 * The DAV:creationdate property SHOULD be defined on all DAV
2931 * compliant resources. If present, it contains a timestamp of the
2932 * moment when the resource was created. Servers that are incapable
2933 * of persistently recording the creation date SHOULD instead leave
2934 * it undefined (i.e. report "Not Found").
2935 * (future: might store creationdate in database when PUT creates file
2936 * or LOCK creates empty file, or MKCOL creates collection (dir),
2937 * i.e. wherever the status is 201 Created)
2939 struct tm tm;
2940 char ctime_buf[sizeof("2005-08-18T07:27:16Z")];
2941 if (__builtin_expect( (NULL != gmtime_r(&pb->st.st_ctime, &tm)), 1)) {
2942 buffer_append_string_len(b, CONST_STR_LEN(
2943 "<D:creationdate ns0:dt=\"dateTime.tz\">"));
2944 buffer_append_string_len(b, ctime_buf,
2945 strftime(ctime_buf, sizeof(ctime_buf),
2946 "%Y-%m-%dT%TZ", &tm));
2947 buffer_append_string_len(b, CONST_STR_LEN(
2948 "</D:creationdate>"));
2950 else if (pnum != WEBDAV_PROP_ALL)
2951 return -1; /* invalid; report 'not found' */
2952 if (pnum != WEBDAV_PROP_ALL) return 0;/* found *//*(else fall through)*/
2953 __attribute_fallthrough__
2955 #endif
2956 /*case WEBDAV_PROP_DISPLAYNAME:*/ /* (located in database, if present)*/
2957 /*case WEBDAV_PROP_GETCONTENTLANGUAGE:*/ /* (located in db, if present)*/
2958 #if 0
2959 case WEBDAV_PROP_GETCONTENTLANGUAGE:
2960 /* [RFC4918] 15.3 getcontentlanguage Property
2961 * SHOULD NOT be protected, so that clients can reset the language.
2962 * [...]
2963 * The DAV:getcontentlanguage property MUST be defined on any
2964 * DAV-compliant resource that returns the Content-Language header on
2965 * a GET.
2966 * (future: server does not currently set Content-Language and this
2967 * module would need to somehow find out if another module set it)
2969 buffer_append_string_len(b, CONST_STR_LEN(
2970 "<D:getcontentlanguage>en</D:getcontentlanguage>"));
2971 if (pnum != WEBDAV_PROP_ALL) return 0;/* found *//*(else fall through)*/
2972 __attribute_fallthrough__
2973 #endif
2974 case WEBDAV_PROP_GETCONTENTLENGTH:
2975 buffer_append_string_len(b, CONST_STR_LEN(
2976 "<D:getcontentlength>"));
2977 buffer_append_int(b, pb->st.st_size);
2978 buffer_append_string_len(b, CONST_STR_LEN(
2979 "</D:getcontentlength>"));
2980 if (pnum != WEBDAV_PROP_ALL) return 0;/* found *//*(else fall through)*/
2981 __attribute_fallthrough__
2982 case WEBDAV_PROP_GETCONTENTTYPE:
2983 /* [RFC4918] 15.5 getcontenttype Property
2984 * Potentially protected if the server prefers to assign content types
2985 * on its own (see also discussion in Section 9.7.1).
2986 * (server currently assigns content types)
2988 * [RFC4918] 15 DAV Properties
2989 * For properties defined based on HTTP GET response headers
2990 * (DAV:get*), the header value could include LWS as defined
2991 * in [RFC2616], Section 4.2. Server implementors SHOULD strip
2992 * LWS from these values before using as WebDAV property
2993 * values.
2994 * e.g. application/xml;charset="utf-8"
2995 * instead of: application/xml; charset="utf-8"
2996 * (documentation-only; no check is done here to remove LWS)
2998 if (S_ISDIR(pb->st.st_mode)) {
2999 buffer_append_string_len(b, CONST_STR_LEN(
3000 "<D:getcontenttype>httpd/unix-directory</D:getcontenttype>"));
3002 else {
3003 /* provide content type by extension
3004 * Note: not currently supporting filesystem xattr */
3005 const buffer *ct =
3006 stat_cache_mimetype_by_ext(pb->con, CONST_BUF_LEN(pb->dst->path));
3007 if (NULL != ct) {
3008 buffer_append_string_len(b, CONST_STR_LEN(
3009 "<D:getcontenttype>"));
3010 buffer_append_string_buffer(b, ct);
3011 buffer_append_string_len(b, CONST_STR_LEN(
3012 "</D:getcontenttype>"));
3014 else {
3015 if (pnum != WEBDAV_PROP_ALL)
3016 return -1; /* invalid; report 'not found' */
3019 if (pnum != WEBDAV_PROP_ALL) return 0;/* found *//*(else fall through)*/
3020 __attribute_fallthrough__
3021 case WEBDAV_PROP_GETETAG:
3022 if (0 != pb->con->etag_flags) {
3023 buffer *etagb = pb->con->physical.etag;
3024 etag_create(etagb, &pb->st, pb->con->etag_flags);
3025 etag_mutate(etagb, etagb);
3026 buffer_append_string_len(b, CONST_STR_LEN(
3027 "<D:getetag>"));
3028 buffer_append_string_buffer(b, etagb);
3029 buffer_append_string_len(b, CONST_STR_LEN(
3030 "</D:getetag>"));
3031 buffer_clear(etagb);
3033 else if (pnum != WEBDAV_PROP_ALL)
3034 return -1; /* invalid; report 'not found' */
3035 if (pnum != WEBDAV_PROP_ALL) return 0;/* found *//*(else fall through)*/
3036 __attribute_fallthrough__
3037 case WEBDAV_PROP_GETLASTMODIFIED:
3039 buffer_append_string_len(b, CONST_STR_LEN(
3040 "<D:getlastmodified ns0:dt=\"dateTime.rfc1123\">"));
3041 buffer_append_strftime(b, "%a, %d %b %Y %H:%M:%S GMT",
3042 gmtime(&pb->st.st_mtime));
3043 buffer_append_string_len(b, CONST_STR_LEN(
3044 "</D:getlastmodified>"));
3046 if (pnum != WEBDAV_PROP_ALL) return 0;/* found *//*(else fall through)*/
3047 __attribute_fallthrough__
3048 #if 0
3049 #ifdef USE_LOCKS
3050 case WEBDAV_PROP_LOCKDISCOVERY:
3051 /* database query for locks occurs in webdav_propfind_resource_props()*/
3052 if (pnum != WEBDAV_PROP_ALL) return 0;/* found *//*(else fall through)*/
3053 __attribute_fallthrough__
3054 #endif
3055 #endif
3056 case WEBDAV_PROP_RESOURCETYPE:
3057 if (S_ISDIR(pb->st.st_mode))
3058 buffer_append_string_len(b, CONST_STR_LEN(
3059 "<D:resourcetype><D:collection/></D:resourcetype>"));
3060 else
3061 buffer_append_string_len(b, CONST_STR_LEN(
3062 "<D:resourcetype/>"));
3063 if (pnum != WEBDAV_PROP_ALL) return 0;/* found *//*(else fall through)*/
3064 __attribute_fallthrough__
3065 /*case WEBDAV_PROP_SOURCE:*/ /* not impl; removed in RFC4918 */
3066 #ifdef USE_LOCKS
3067 case WEBDAV_PROP_SUPPORTEDLOCK:
3068 buffer_append_string_len(b, CONST_STR_LEN(
3069 "<D:supportedlock>"
3070 "<D:lockentry>"
3071 "<D:lockscope><D:exclusive/></D:lockscope>"
3072 "<D:locktype><D:write/></D:locktype>"
3073 "</D:lockentry>"
3074 "<D:lockentry>"
3075 "<D:lockscope><D:shared/></D:lockscope>"
3076 "<D:locktype><D:write/></D:locktype>"
3077 "</D:lockentry>"
3078 "</D:supportedlock>"));
3079 if (pnum != WEBDAV_PROP_ALL) return 0;/* found *//*(else fall through)*/
3080 __attribute_fallthrough__
3081 #endif
3082 default: /* WEBDAV_PROP_UNSET */
3083 return -1; /* not found */
3085 return 0; /* found (WEBDAV_PROP_ALL) */
3089 #ifdef USE_LOCKS
3090 static void
3091 webdav_propfind_lockdiscovery_cb (void * const vdata,
3092 const webdav_lockdata * const lockdata)
3094 webdav_xml_activelock((buffer *)vdata, lockdata, NULL, 0);
3096 #endif
3099 static void
3100 webdav_propfind_resource_props (const webdav_propfind_bufs * const restrict pb)
3102 const webdav_property_names * const props = &pb->proplist;
3103 if (props->used) { /* "props" or "allprop"+"include" */
3104 const webdav_property_name *prop = props->ptr;
3105 for (int i = 0; i < props->used; ++i, ++prop) {
3106 if (NULL == prop->name /*(flag indicating prop is live prop enum)*/
3107 ? 0 == webdav_propfind_live_props(pb, (enum webdav_live_props_e)
3108 prop->namelen)
3109 : 0 == webdav_prop_select_prop(pb->pconf, pb->dst->rel_path,
3110 prop, pb->b_200))
3111 continue;
3113 /*(error obtaining prop if reached)*/
3114 webdav_xml_prop(pb->b_404, prop, NULL, 0);
3118 if (pb->allprop) {
3119 webdav_propfind_live_props(pb, WEBDAV_PROP_ALL);
3120 webdav_prop_select_props(pb->pconf, pb->dst->rel_path, pb->b_200);
3123 #ifdef USE_LOCKS
3124 if (pb->lockdiscovery) {
3125 /* pb->lockdiscovery > 0:
3126 * report locks resource or containing (parent) collections
3127 * pb->lockdiscovery < 0:
3128 * report only those locks on specific resource
3129 * While this is not compliant with RFC, it may reduces quite a bit of
3130 * redundancy for propfind on Depth: 1 and Depth: infinity when there
3131 * are locks on parent collections. The client receiving this propfind
3132 * XML response should easily know that locks on collections apply to
3133 * the members of those collections and to further nested collections
3135 * future: might be many, many fewer database queries if make a single
3136 * query for the locks in the collection directory tree and parse the
3137 * results, rather than querying the database for each resource */
3138 buffer_append_string_len(pb->b_200, CONST_STR_LEN(
3139 "<D:lockdiscovery>"));
3140 webdav_lock_activelocks(pb->pconf, pb->dst->rel_path,
3141 (pb->lockdiscovery > 0),
3142 webdav_propfind_lockdiscovery_cb, pb->b_200);
3143 buffer_append_string_len(pb->b_200, CONST_STR_LEN(
3144 "</D:lockdiscovery>"));
3146 #endif
3150 static void
3151 webdav_propfind_resource_propnames (const webdav_propfind_bufs *
3152 const restrict pb)
3154 static const char live_propnames[] =
3155 "<getcontentlength/>\n"
3156 "<getcontenttype/>\n"
3157 "<getetag/>\n"
3158 "<getlastmodified/>\n"
3159 "<resourcetype/>\n"
3160 #ifdef USE_LOCKS
3161 "<supportedlock/>\n"
3162 "<lockdiscovery/>\n"
3163 #endif
3165 /* list live_properties which are not in database, plus "lockdiscovery" */
3166 buffer_append_string_len(pb->b_200,live_propnames,sizeof(live_propnames)-1);
3168 /* list properties in database 'properties' table for resource */
3169 webdav_prop_select_propnames(pb->pconf, pb->dst->rel_path, pb->b_200);
3173 __attribute_cold__
3174 static void
3175 webdav_propfind_resource_403 (const webdav_propfind_bufs * const restrict pb)
3177 buffer * const restrict b = pb->b;
3178 buffer_append_string_len(b, CONST_STR_LEN(
3179 "<D:response>\n"));
3180 webdav_xml_href(b, pb->dst->rel_path);
3181 buffer_append_string_len(b, CONST_STR_LEN(
3182 "<D:propstat>\n"));
3183 webdav_xml_status(b, 403); /* Forbidden */
3184 buffer_append_string_len(b, CONST_STR_LEN(
3185 "</D:propstat>\n"
3186 "</D:response>\n"));
3190 static void
3191 webdav_propfind_resource (const webdav_propfind_bufs * const restrict pb)
3193 buffer_clear(pb->b_200);
3194 buffer_clear(pb->b_404);
3196 if (!pb->propname)
3197 webdav_propfind_resource_props(pb);
3198 else
3199 webdav_propfind_resource_propnames(pb);
3201 /* buffer could get very large for large directory (or Depth: infinity)
3202 * attempt to allocate in 8K chunks, rather than default realloc in
3203 * 64-byte chunks (see buffer.h) which will lead to exponentially more
3204 * expensive copy behavior as buffer is resized over and over and over
3206 * future: avoid (potential) excessive memory usage by accumulating output
3207 * in temporary file
3209 buffer * const restrict b = pb->b;
3210 buffer * const restrict b_200 = pb->b_200;
3211 buffer * const restrict b_404 = pb->b_404;
3212 if (b->size - b->used < b_200->used + b_404->used + 1024) {
3213 size_t sz = b->used + BUFFER_MAX_REUSE_SIZE
3214 + b_200->used + b_404->used + 1024;
3215 /*(optimization; buffer is extended as needed)*/
3216 buffer_string_prepare_append(b, sz & (BUFFER_MAX_REUSE_SIZE-1));
3219 buffer_append_string_len(b, CONST_STR_LEN(
3220 "<D:response>\n"));
3221 webdav_xml_href(b, pb->dst->rel_path);
3222 if (b_200->used > 1) /* !unset and !blank */
3223 webdav_xml_propstat(b, b_200, 200);
3224 if (b_404->used > 1) /* !unset and !blank */
3225 webdav_xml_propstat(b, b_404, 404);
3226 buffer_append_string_len(b, CONST_STR_LEN(
3227 "</D:response>\n"));
3231 static void
3232 webdav_propfind_dir (webdav_propfind_bufs * const restrict pb)
3234 const physical_st * const dst = pb->dst;
3235 const int dfd = fdevent_open_dirname(dst->path->ptr, 0);
3236 DIR * const dir = (dfd >= 0) ? fdopendir(dfd) : NULL;
3237 if (NULL == dir) {
3238 int errnum = errno;
3239 if (dfd >= 0) close(dfd);
3240 if (errnum != ENOENT)
3241 webdav_propfind_resource_403(pb); /* Forbidden */
3242 return;
3245 webdav_propfind_resource(pb);
3247 if (pb->lockdiscovery > 0)
3248 pb->lockdiscovery = -pb->lockdiscovery; /*(check locks on node only)*/
3250 /* dst is modified in place to extend path,
3251 * so be sure to restore to base each loop iter */
3252 const uint32_t dst_path_used = dst->path->used;
3253 const uint32_t dst_rel_path_used = dst->rel_path->used;
3254 const int flags =
3255 (pb->con->conf.force_lowercase_filenames ? WEBDAV_FLAG_LC_NAMES : 0);
3256 struct dirent *de;
3257 while (NULL != (de = readdir(dir))) {
3258 if (de->d_name[0] == '.'
3259 && (de->d_name[1] == '\0'
3260 || (de->d_name[1] == '.' && de->d_name[2] == '\0')))
3261 continue; /* ignore "." and ".." */
3263 if (0 != fstatat(dfd, de->d_name, &pb->st, AT_SYMLINK_NOFOLLOW))
3264 continue; /* file *just* disappeared? */
3266 const uint32_t len = (uint32_t) _D_EXACT_NAMLEN(de);
3267 if (flags & WEBDAV_FLAG_LC_NAMES) /*(needed by rel_path)*/
3268 webdav_str_len_to_lower(de->d_name, len);
3269 buffer_append_string_len(dst->path, de->d_name, len);
3270 buffer_append_string_len(dst->rel_path, de->d_name, len);
3271 if (S_ISDIR(pb->st.st_mode)) {
3272 buffer_append_string_len(dst->path, CONST_STR_LEN("/"));
3273 buffer_append_string_len(dst->rel_path, CONST_STR_LEN("/"));
3276 if (S_ISDIR(pb->st.st_mode) && -1 == pb->depth)
3277 webdav_propfind_dir(pb); /* recurse */
3278 else
3279 webdav_propfind_resource(pb);
3281 dst->path->ptr[ (dst->path->used = dst_path_used) -1] = '\0';
3282 dst->rel_path->ptr[(dst->rel_path->used = dst_rel_path_used)-1] = '\0';
3284 closedir(dir);
3288 static int
3289 webdav_open_chunk_file_rd (chunk * const c)
3291 if (c->file.fd < 0) /* open file if not already open *//*permit symlink*/
3292 c->file.fd = fdevent_open_cloexec(c->mem->ptr, 1, O_RDONLY, 0);
3293 return c->file.fd;
3297 static int
3298 webdav_mmap_file_rd (void ** const addr, const size_t length,
3299 const int fd, const off_t offset)
3301 /*(caller must ensure offset is properly aligned to mmap requirements)*/
3303 if (0 == length) {
3304 *addr = NULL; /*(something other than MAP_FAILED)*/
3305 return 0;
3308 #ifdef HAVE_MMAP
3310 *addr = mmap(NULL, length, PROT_READ, MAP_SHARED, fd, offset);
3311 if (*addr == MAP_FAILED && errno == EINVAL)
3312 *addr = mmap(NULL, length, PROT_READ, MAP_PRIVATE, fd, offset);
3313 return (*addr != MAP_FAILED ? 0 : -1);
3315 #else
3317 return -1;
3319 #endif
3323 static char *
3324 webdav_mmap_file_chunk (chunk * const c)
3326 /*(request body provided in temporary file, so ok to mmap().
3327 * Otherwise, must check defined(ENABLE_MMAP)) */
3328 /* chunk_reset() or chunk_free() will clean up mmap'd chunk */
3329 /* close c->file.fd only faster mmap() succeeds, since it will not
3330 * be able to be re-opened if it was a tmpfile that was unlinked */
3331 /*assert(c->type == FILE_CHUNK);*/
3332 if (MAP_FAILED != c->file.mmap.start)
3333 return c->file.mmap.start + c->offset;
3335 if (webdav_open_chunk_file_rd(c) < 0)
3336 return NULL;
3338 webdav_mmap_file_rd((void **)&c->file.mmap.start, (size_t)c->file.length,
3339 c->file.fd, 0);
3341 if (MAP_FAILED == c->file.mmap.start)
3342 return NULL;
3344 close(c->file.fd);
3345 c->file.fd = -1;
3346 c->file.mmap.length = c->file.length;
3347 return c->file.mmap.start + c->offset;
3351 #if defined(USE_PROPPATCH) || defined(USE_LOCKS)
3352 __attribute_noinline__
3353 static xmlDoc *
3354 webdav_parse_chunkqueue (connection * const con,
3355 const plugin_config * const pconf)
3357 /* read the chunks in to the XML document */
3358 xmlParserCtxtPtr ctxt = xmlCreatePushParserCtxt(NULL, NULL, NULL, 0, NULL);
3359 /* XXX: evaluate adding more xmlParserOptions */
3360 xmlCtxtUseOptions(ctxt, XML_PARSE_NOERROR | XML_PARSE_NOWARNING
3361 | XML_PARSE_PEDANTIC| XML_PARSE_NONET);
3362 char *xmlstr;
3363 chunkqueue * const cq = con->request_content_queue;
3364 size_t weWant = cq->bytes_in - cq->bytes_out;
3365 int err = XML_ERR_OK;
3367 while (weWant) {
3368 size_t weHave = 0;
3369 chunk *c = cq->first;
3370 char buf[16384];
3371 #ifdef __COVERITY__
3372 force_assert(0 == weWant || c != NULL);
3373 #endif
3375 if (c->type == MEM_CHUNK) {
3376 xmlstr = c->mem->ptr + c->offset;
3377 weHave = buffer_string_length(c->mem) - c->offset;
3379 else if (c->type == FILE_CHUNK) {
3380 xmlstr = webdav_mmap_file_chunk(c);
3381 /*xmlstr = c->file.mmap.start + c->offset;*/
3382 if (NULL != xmlstr) {
3383 weHave = c->file.length - c->offset;
3385 else {
3386 switch (errno) {
3387 case ENOSYS: case ENODEV: case EINVAL: break;
3388 default:
3389 log_perror(con->errh, __FILE__, __LINE__,
3390 "open() or mmap() '%*.s'",
3391 BUFFER_INTLEN_PTR(c->mem));
3393 if (webdav_open_chunk_file_rd(c) < 0) {
3394 log_perror(con->errh, __FILE__, __LINE__,
3395 "open() '%*.s'",
3396 BUFFER_INTLEN_PTR(c->mem));
3397 err = XML_IO_UNKNOWN;
3398 break;
3400 ssize_t rd = -1;
3401 do {
3402 if (-1 ==lseek(c->file.fd,c->file.start+c->offset,SEEK_SET))
3403 break;
3404 off_t len = c->file.length - c->offset;
3405 if (len > (off_t)sizeof(buf)) len = (off_t)sizeof(buf);
3406 rd = read(c->file.fd, buf, (size_t)len);
3407 } while (-1 == rd && errno == EINTR);
3408 if (rd >= 0) {
3409 xmlstr = buf;
3410 weHave = (size_t)rd;
3412 else {
3413 log_perror(con->errh, __FILE__, __LINE__,
3414 "read() '%*.s'",
3415 BUFFER_INTLEN_PTR(c->mem));
3416 err = XML_IO_UNKNOWN;
3417 break;
3421 else {
3422 log_error(con->errh, __FILE__, __LINE__,
3423 "unrecognized chunk type: %d", c->type);
3424 err = XML_IO_UNKNOWN;
3425 break;
3428 if (weHave > weWant) weHave = weWant;
3430 if (pconf->log_xml)
3431 log_error(con->errh, __FILE__, __LINE__,
3432 "XML-request-body: %.*s", (int)weHave, xmlstr);
3434 if (XML_ERR_OK != (err = xmlParseChunk(ctxt, xmlstr, weHave, 0))) {
3435 log_error(con->errh, __FILE__, __LINE__,
3436 "xmlParseChunk failed at: %lld %zu %d",
3437 (long long int)cq->bytes_out, weHave, err);
3438 break;
3441 weWant -= weHave;
3442 chunkqueue_mark_written(cq, weHave);
3445 if (XML_ERR_OK == err) {
3446 switch ((err = xmlParseChunk(ctxt, 0, 0, 1))) {
3447 case XML_ERR_DOCUMENT_END:
3448 case XML_ERR_OK:
3449 if (ctxt->wellFormed) {
3450 xmlDoc * const xml = ctxt->myDoc;
3451 xmlFreeParserCtxt(ctxt);
3452 return xml;
3454 break;
3455 default:
3456 log_error(con->errh, __FILE__, __LINE__,
3457 "xmlParseChunk failed at final packet: %d", err);
3458 break;
3462 xmlFreeDoc(ctxt->myDoc);
3463 xmlFreeParserCtxt(ctxt);
3464 return NULL;
3466 #endif
3469 #ifdef USE_LOCKS
3471 struct webdav_lock_token_submitted_st {
3472 buffer *tokens;
3473 int used;
3474 int size;
3475 const buffer *authn_user;
3476 buffer *b;
3477 int nlocks;
3478 int slocks;
3479 int smatch;
3483 static void
3484 webdav_lock_token_submitted_cb (void * const vdata,
3485 const webdav_lockdata * const lockdata)
3487 /* RFE: improve support for shared locks
3488 * (instead of treating match of any shared lock as sufficient,
3489 * even when there are different lockroots)
3490 * keep track of matched shared locks and unmatched shared locks and
3491 * ensure that each lockroot with shared locks has at least one match
3492 * (Will need to allocate strings for each URI with shared lock and keep
3493 * track whether or not a shared lock has been matched for that URI.
3494 * After walking all locks, must walk looking for unmatched URIs,
3495 * and must free these strings) */
3497 /* [RFC4918] 6.4 Lock Creator and Privileges
3498 * When a locked resource is modified, a server MUST check that the
3499 * authenticated principal matches the lock creator (in addition to
3500 * checking for valid lock token submission).
3503 struct webdav_lock_token_submitted_st * const cbdata =
3504 (struct webdav_lock_token_submitted_st *)vdata;
3505 const buffer * const locktoken = &lockdata->locktoken;
3506 const int shared = (lockdata->lockscope->used != sizeof("exclusive"));
3508 ++cbdata->nlocks;
3509 if (shared) ++cbdata->slocks;
3511 for (int i = 0; i < cbdata->used; ++i) {
3512 const buffer * const token = &cbdata->tokens[i];
3513 /* locktoken match (locktoken not '\0' terminated) */
3514 if (buffer_is_equal_string(token, CONST_BUF_LEN(locktoken))) {
3515 /*(0 length owner if no auth required to lock; not recommended)*/
3516 if (buffer_string_is_empty(lockdata->owner)/*no lock owner;match*/
3517 || buffer_is_equal_string(cbdata->authn_user,
3518 CONST_BUF_LEN(lockdata->owner))) {
3519 if (shared) ++cbdata->smatch;
3520 return; /* authenticated lock owner match */
3525 /* no match with lock tokens in request */
3526 if (!shared)
3527 webdav_xml_href(cbdata->b, &lockdata->lockroot);
3532 * check if request provides necessary locks to access the resource
3534 static int
3535 webdav_has_lock (connection * const con,
3536 const plugin_config * const pconf,
3537 const buffer * const uri)
3539 /* Note with regard to exclusive locks on collections: client should not be
3540 * able to obtain an exclusive lock on a collection if there are existing
3541 * locks on resource members inside the collection. Therefore, there is no
3542 * need to check here for locks on resource members inside collections.
3543 * (This ignores the possibility that an admin or some other privileged
3544 * or out-of-band process has added locks in spite of lock on collection.)
3545 * Revisit to properly support shared locks. */
3547 struct webdav_lock_token_submitted_st cbdata;
3548 cbdata.b = buffer_init();
3549 cbdata.tokens = NULL;
3550 cbdata.used = 0;
3551 cbdata.size = 0;
3552 cbdata.nlocks = 0;
3553 cbdata.slocks = 0;
3554 cbdata.smatch = 0;
3556 /* XXX: maybe add config switch to require that authentication occurred? */
3557 buffer owner = { NULL, 0, 0 };/*owner (not authenticated)(auth_user unset)*/
3558 data_string * const authn_user = (data_string *)
3559 array_get_element_klen(con->environment,
3560 CONST_STR_LEN("REMOTE_USER"));
3561 cbdata.authn_user = authn_user ? authn_user->value : &owner;
3563 const buffer * const h =
3564 http_header_request_get(con, HTTP_HEADER_OTHER, CONST_STR_LEN("If"));
3566 if (!buffer_is_empty(h)) {
3567 /* parse "If" request header for submitted lock tokens
3568 * While the below not a pedantic, validating parse, if the header
3569 * is non-conformant or contains unencoded characters, the result
3570 * will be misidentified or ignored lock tokens, which will result
3571 * in fail closed -- secure default behavior -- if those lock
3572 * tokens are required. It is highly unlikely that misparsing the "If"
3573 * request header will result in a valid lock token since lock tokens
3574 * should be unique, and opaquelocktoken should be globally unique */
3575 char *p = h->ptr;
3576 do {
3577 #if 0
3578 while (*p == ' ' || *p == '\t') ++p;
3579 if (*p == '<') { /* Resource-Tag */
3580 do { ++p; } while (*p != '>' && *p != '\0');
3581 if (*p == '\0') break;
3582 do { ++p; } while (*p == ' ' || *p == '\t');
3584 #endif
3586 while (*p != '(' && *p != '\0') ++p;
3588 /* begin List in No-tag-list or Tagged-list
3589 * List = "(" 1*Condition ")"
3590 * Condition = ["Not"] (State-token | "[" entity-tag "]")
3592 int notflag = 0;
3593 while (*p != '\0' && *++p != ')') {
3594 while (*p == ' ' || *p == '\t') ++p;
3595 if ( (p[0] & 0xdf) == 'N'
3596 && (p[1] & 0xdf) == 'O'
3597 && (p[2] & 0xdf) == 'T') {
3598 notflag = 1;
3599 p += 3;
3600 while (*p == ' ' || *p == '\t') ++p;
3602 if (*p != '<') { /* '<' begins State-token (Coded-URL) */
3603 if (*p != '[') break; /* invalid syntax */
3604 /* '[' and ']' wrap entity-tag */
3605 char *etag = p+1;
3606 do { ++p; } while (*p != ']' && *p != '\0');
3607 if (*p != ']') break; /* invalid syntax */
3608 if (p == etag) continue; /* ignore entity-tag if empty */
3609 if (notflag) continue;/* ignore entity-tag in NOT context */
3610 if (0 == con->etag_flags) continue; /* ignore entity-tag */
3611 struct stat st;
3612 if (0 != lstat(con->physical.path->ptr, &st)) {
3613 http_status_set_error(con,412);/* Precondition Failed */
3614 return 0;
3616 if (S_ISDIR(st.st_mode)) continue;/*we ignore etag if dir*/
3617 buffer *etagb = con->physical.etag;
3618 etag_create(etagb, &st, con->etag_flags);
3619 etag_mutate(etagb, etagb);
3620 *p = '\0';
3621 int ematch = etag_is_equal(etagb, etag, 0);
3622 *p = ']';
3623 if (!ematch) {
3624 http_status_set_error(con,412);/* Precondition Failed */
3625 return 0;
3627 continue;
3630 if (p[1] == 'D'
3631 && 0 == strncmp(p, "<DAV:no-lock>",
3632 sizeof("<DAV:no-lock>")-1)) {
3633 if (0 == notflag) {
3634 http_status_set_error(con,412);/* Precondition Failed */
3635 return 0;
3637 p += sizeof("<DAV:no-lock>")-2; /* point p to '>' */
3638 continue;
3641 /* State-token (Coded-URL)
3642 * Coded-URL = "<" absolute-URI ">"
3643 * ; No linear whitespace (LWS) allowed in Coded-URL
3644 * ; absolute-URI defined in RFC 3986, Section 4.3
3646 if (cbdata.size == cbdata.used) {
3647 if (cbdata.size == 16) { /* arbitrary limit */
3648 http_status_set_error(con, 400); /* Bad Request */
3649 return 0;
3651 cbdata.tokens =
3652 realloc(cbdata.tokens, sizeof(*(cbdata.tokens)) * 16);
3653 force_assert(cbdata.tokens); /*(see above limit)*/
3655 cbdata.tokens[cbdata.used].ptr = p+1;
3657 do { ++p; } while (*p != '>' && *p != '\0');
3658 if (*p == '\0') break; /* (*p != '>') */
3660 cbdata.tokens[cbdata.used].used =
3661 (uint32_t)(p - cbdata.tokens[cbdata.used].ptr + 1);
3662 ++cbdata.used;
3664 } while (*p++ == ')'); /* end of List in No-tag-list or Tagged-list */
3667 webdav_lock_activelocks(pconf, uri, 1,
3668 webdav_lock_token_submitted_cb, &cbdata);
3670 if (NULL != cbdata.tokens)
3671 free(cbdata.tokens);
3673 int has_lock = 1;
3675 if (0 != cbdata.b->used)
3676 has_lock = 0;
3677 else if (0 == cbdata.nlocks) { /* resource is not locked at all */
3678 /* error if lock provided on source and no locks present on source;
3679 * not error if no locks on Destination, but "If" provided for source */
3680 if (cbdata.used && uri == con->physical.rel_path) {
3681 has_lock = -1;
3682 http_status_set_error(con, 412); /* Precondition Failed */
3684 #if 0 /*(treat no locks as if caller is holding an appropriate lock)*/
3685 else {
3686 has_lock = 0;
3687 webdav_xml_href(cbdata.b, uri);
3689 #endif
3692 /*(XXX: overly simplistic shared lock matching allows any match of shared
3693 * locks even when there are shared locks on multiple different lockroots.
3694 * Failure is misreported since unmatched shared locks are not added to
3695 * cbdata.b) */
3696 if (cbdata.slocks && !cbdata.smatch)
3697 has_lock = 0;
3699 if (!has_lock)
3700 webdav_xml_doc_error_lock_token_submitted(con, cbdata.b);
3702 buffer_free(cbdata.b);
3704 return (has_lock > 0);
3707 #else /* ! defined(USE_LOCKS) */
3709 #define webdav_has_lock(con, pconf, uri) 1
3711 #endif /* ! defined(USE_LOCKS) */
3714 static handler_t
3715 mod_webdav_propfind (connection * const con, const plugin_config * const pconf)
3717 if (con->request.content_length) {
3718 #ifdef USE_PROPPATCH
3719 if (con->state == CON_STATE_READ_POST) {
3720 handler_t rc = connection_handle_read_post_state(pconf->srv, con);
3721 if (rc != HANDLER_GO_ON) return rc;
3723 #else
3724 /* PROPFIND is idempotent and safe, so even if parsing XML input is not
3725 * supported, live properties can still be produced, so treat as allprop
3726 * request. NOTE: this behavior is NOT RFC CONFORMANT (and, well, if
3727 * compiled without XML support, this WebDAV implementation is already
3728 * non-compliant since it is missing support for XML request body).
3729 * RFC-compliant behavior would reject an ignored request body with
3730 * 415 Unsupported Media Type */
3731 #if 0
3732 http_status_set_error(con, 415); /* Unsupported Media Type */
3733 return HANDLER_FINISHED;
3734 #endif
3735 #endif
3738 webdav_propfind_bufs pb;
3740 /* [RFC4918] 9.1 PROPFIND Method
3741 * Servers MUST support "0" and "1" depth requests on WebDAV-compliant
3742 * resources and SHOULD support "infinity" requests. In practice, support
3743 * for infinite-depth requests MAY be disabled, due to the performance and
3744 * security concerns associated with this behavior. Servers SHOULD treat
3745 * a request without a Depth header as if a "Depth: infinity" header was
3746 * included.
3748 pb.allprop = 0;
3749 pb.propname = 0;
3750 pb.lockdiscovery= 0;
3751 pb.depth = webdav_parse_Depth(con);
3753 /* future: might add config option to enable Depth: infinity
3754 * (Depth: infinity is supported if this rejection is removed) */
3755 if (-1 == pb.depth) {
3756 webdav_xml_doc_error_propfind_finite_depth(con);
3757 return HANDLER_FINISHED;
3760 if (0 != lstat(con->physical.path->ptr, &pb.st)) {
3761 http_status_set_error(con, (errno == ENOENT) ? 404 : 403);
3762 return HANDLER_FINISHED;
3764 else if (S_ISDIR(pb.st.st_mode)) {
3765 if (con->physical.path->ptr[con->physical.path->used - 2] != '/') {
3766 buffer *vb = http_header_request_get(con, HTTP_HEADER_OTHER,
3767 CONST_STR_LEN("User-Agent"));
3768 if (vb && 0 == strncmp(vb->ptr, "Microsoft-WebDAV-MiniRedir/",
3769 sizeof("Microsoft-WebDAV-MiniRedir/")-1)) {
3770 /* workaround Microsoft-WebDAV-MiniRedir bug */
3771 /* (MS File Explorer unable to open folder if not redirected) */
3772 http_response_redirect_to_directory(pconf->srv, con, 308);
3773 return HANDLER_FINISHED;
3775 /* set "Content-Location" instead of sending 308 redirect to dir */
3776 if (!http_response_redirect_to_directory(pconf->srv, con, 0))
3777 return HANDLER_FINISHED;
3778 buffer_append_string_len(con->physical.path, CONST_STR_LEN("/"));
3779 buffer_append_string_len(con->physical.rel_path,CONST_STR_LEN("/"));
3782 else if (con->physical.path->ptr[con->physical.path->used - 2] == '/') {
3783 http_status_set_error(con, 403);
3784 return HANDLER_FINISHED;
3786 else if (0 != pb.depth) {
3787 http_status_set_error(con, 403);
3788 return HANDLER_FINISHED;
3791 pb.proplist.ptr = NULL;
3792 pb.proplist.used = 0;
3793 pb.proplist.size = 0;
3795 #ifdef USE_PROPPATCH
3796 xmlDocPtr xml = NULL;
3797 const xmlNode *rootnode = NULL;
3798 if (con->request.content_length) {
3799 if (NULL == (xml = webdav_parse_chunkqueue(con, pconf))) {
3800 http_status_set_error(con, 400); /* Bad Request */
3801 return HANDLER_FINISHED;
3803 rootnode = xmlDocGetRootElement(xml);
3806 if (NULL != rootnode
3807 && 0 == webdav_xmlstrcmp_fixed(rootnode->name, "propfind")) {
3808 for (const xmlNode *cmd = rootnode->children; cmd; cmd = cmd->next) {
3809 if (0 == webdav_xmlstrcmp_fixed(cmd->name, "allprop"))
3810 pb.allprop = pb.lockdiscovery = 1;
3811 else if (0 == webdav_xmlstrcmp_fixed(cmd->name, "propname"))
3812 pb.propname = 1;
3813 else if (0 != webdav_xmlstrcmp_fixed(cmd->name, "prop")
3814 && 0 != webdav_xmlstrcmp_fixed(cmd->name, "include"))
3815 continue;
3817 /* "prop" or "include": get prop by name */
3818 for (const xmlNode *prop = cmd->children; prop; prop = prop->next) {
3819 if (prop->type == XML_TEXT_NODE)
3820 continue; /* ignore WS */
3822 if (prop->ns && '\0' == *(char *)prop->ns->href
3823 && '\0' != *(char *)prop->ns->prefix) {
3824 log_error(con->errh, __FILE__, __LINE__,
3825 "no name space for: %s", prop->name);
3826 /* 422 Unprocessable Entity */
3827 http_status_set_error(con, 422);
3828 free(pb.proplist.ptr);
3829 xmlFreeDoc(xml);
3830 return HANDLER_FINISHED;
3833 /* add property to requested list */
3834 if (pb.proplist.size == pb.proplist.used) {
3835 if (pb.proplist.size == 32) {
3836 /* arbitrarily chosen limit of 32 */
3837 log_error(con->errh, __FILE__, __LINE__,
3838 "too many properties in request (> 32)");
3839 http_status_set_error(con, 400); /* Bad Request */
3840 free(pb.proplist.ptr);
3841 xmlFreeDoc(xml);
3842 return HANDLER_FINISHED;
3844 pb.proplist.ptr =
3845 realloc(pb.proplist.ptr, sizeof(*(pb.proplist.ptr)) * 32);
3846 force_assert(pb.proplist.ptr); /*(see above limit)*/
3849 const size_t namelen = strlen((char *)prop->name);
3850 if (prop->ns && 0 == strcmp((char *)prop->ns->href, "DAV:")) {
3851 if (namelen == sizeof("lockdiscovery")-1
3852 && 0 == memcmp(prop->name,
3853 CONST_STR_LEN("lockdiscovery"))) {
3854 pb.lockdiscovery = 1;
3855 continue;
3857 const struct live_prop_list *list = live_properties;
3858 while (0 != list->len
3859 && (list->len != namelen
3860 || 0 != memcmp(prop->name,list->prop,list->len)))
3861 ++list;
3862 if (NULL != list->prop) {
3863 if (cmd->name[0] == 'p') { /* "prop", not "include" */
3864 pb.proplist.ptr[pb.proplist.used].name = NULL;
3865 pb.proplist.ptr[pb.proplist.used].namelen =
3866 list->pnum;
3867 pb.proplist.used++;
3868 } /* (else skip; will already be part of allprop) */
3869 continue;
3871 if (cmd->name[0] == 'i') /* allprop "include", not "prop" */
3872 continue; /*(all props in db returned with allprop)*/
3873 /* dead props or props in "DAV:" ns not handed above */
3876 /* save pointers directly into parsed xmlDoc
3877 * Therefore, MUST NOT call xmlFreeDoc(xml)
3878 * until also done with pb.proplist */
3879 webdav_property_name * const propname =
3880 pb.proplist.ptr + pb.proplist.used++;
3881 if (prop->ns) {
3882 propname->ns = (char *)prop->ns->href;
3883 propname->nslen = strlen(propname->ns);
3885 else {
3886 propname->ns = "";
3887 propname->nslen = 0;
3889 propname->name = (char *)prop->name;
3890 propname->namelen = namelen;
3894 #endif
3896 if (NULL == pb.proplist.ptr && !pb.propname)
3897 pb.allprop = pb.lockdiscovery = 1;
3899 pb.con = con;
3900 pb.pconf = pconf;
3901 pb.dst = &con->physical;
3902 pb.b = /*(optimization; buf extended as needed)*/
3903 chunkqueue_append_buffer_open_sz(con->write_queue, BUFFER_MAX_REUSE_SIZE);
3904 pb.b_200 = buffer_init();
3905 pb.b_404 = buffer_init();
3907 buffer_string_prepare_copy(pb.b_200, BUFFER_MAX_REUSE_SIZE-1);
3908 buffer_string_prepare_copy(pb.b_404, BUFFER_MAX_REUSE_SIZE-1);
3910 webdav_xml_doctype(pb.b, con);
3911 buffer_append_string_len(pb.b, CONST_STR_LEN(
3912 "<D:multistatus xmlns:D=\"DAV:\" " MOD_WEBDAV_XMLNS_NS0 ">\n"));
3914 if (0 != pb.depth) /*(must be collection or else error returned above)*/
3915 webdav_propfind_dir(&pb);
3916 else
3917 webdav_propfind_resource(&pb);
3919 buffer_append_string_len(pb.b, CONST_STR_LEN(
3920 "</D:multistatus>\n"));
3922 if (pconf->log_xml)
3923 log_error(con->errh, __FILE__, __LINE__, "XML-response-body: %.*s",
3924 BUFFER_INTLEN_PTR(pb.b));
3926 http_status_set_fin(con, 207); /* Multi-status */
3928 buffer_free(pb.b_404);
3929 buffer_free(pb.b_200);
3930 #ifdef USE_PROPPATCH
3931 if (pb.proplist.ptr)
3932 free(pb.proplist.ptr);
3933 if (NULL != xml)
3934 xmlFreeDoc(xml);
3935 #endif
3937 return HANDLER_FINISHED;
3941 static handler_t
3942 mod_webdav_mkcol (connection * const con, const plugin_config * const pconf)
3944 const int status = webdav_mkdir(pconf, &con->physical, -1);
3945 if (0 == status)
3946 http_status_set_fin(con, 201); /* Created */
3947 else
3948 http_status_set_error(con, status);
3950 return HANDLER_FINISHED;
3954 static handler_t
3955 mod_webdav_delete (connection * const con, const plugin_config * const pconf)
3957 /* reject DELETE if original URI sent with fragment ('litmus' warning) */
3958 if (NULL != strchr(con->request.orig_uri->ptr, '#')) {
3959 http_status_set_error(con, 403);
3960 return HANDLER_FINISHED;
3963 struct stat st;
3964 if (-1 == lstat(con->physical.path->ptr, &st)) {
3965 http_status_set_error(con, (errno == ENOENT) ? 404 : 403);
3966 return HANDLER_FINISHED;
3969 if (0 != webdav_if_match_or_unmodified_since(con, &st)) {
3970 http_status_set_error(con, 412); /* Precondition Failed */
3971 return HANDLER_FINISHED;
3974 if (S_ISDIR(st.st_mode)) {
3975 if (con->physical.path->ptr[con->physical.path->used - 2] != '/') {
3976 #if 0 /*(issues warning for /usr/bin/litmus copymove test)*/
3977 http_response_redirect_to_directory(pconf->srv, con, 308);
3978 return HANDLER_FINISHED; /* 308 Permanent Redirect */
3979 /* Alternatively, could append '/' to con->physical.path
3980 * and con->physical.rel_path, set Content-Location in
3981 * response headers, and continue to serve the request */
3982 #else
3983 buffer_append_string_len(con->physical.path, CONST_STR_LEN("/"));
3984 buffer_append_string_len(con->physical.rel_path,CONST_STR_LEN("/"));
3985 #if 0 /*(Content-Location not very useful to client after DELETE)*/
3986 /*(? should it be request.uri or orig_uri ?)*/
3987 /*(should be url-encoded path)*/
3988 buffer_append_string_len(con->request.uri, CONST_STR_LEN("/"));
3989 http_header_response_set(con, HTTP_HEADER_CONTENT_LOCATION,
3990 CONST_STR_LEN("Content-Location"),
3991 CONST_BUF_LEN(con->request.uri));
3992 #endif
3993 #endif
3995 /* require "infinity" if Depth request header provided */
3996 if (-1 != webdav_parse_Depth(con)) {
3997 /* [RFC4918] 9.6.1 DELETE for Collections
3998 * The DELETE method on a collection MUST act as if a
3999 * "Depth: infinity" header was used on it. A client MUST NOT
4000 * submit a Depth header with a DELETE on a collection with any
4001 * value but infinity.
4003 http_status_set_error(con, 400); /* Bad Request */
4004 return HANDLER_FINISHED;
4007 buffer * const ms = buffer_init(); /* multi-status */
4009 const int flags = (con->conf.force_lowercase_filenames)
4010 ? WEBDAV_FLAG_LC_NAMES
4011 : 0;
4012 if (0 == webdav_delete_dir(pconf, &con->physical, ms, flags)) {
4013 /* Note: this does not destroy locks if an error occurs,
4014 * which is not a problem if lock is only on the collection
4015 * being moved, but might need finer updates if there are
4016 * locks on internal elements that are successfully deleted */
4017 webdav_lock_delete_uri_col(pconf, con->physical.rel_path);
4018 http_status_set_fin(con, 204); /* No Content */
4020 else {
4021 webdav_xml_doc_multistatus(con, pconf, ms); /* 207 Multi-status */
4024 buffer_free(ms);
4025 /* invalidate stat cache of src if DELETE, whether or not successful */
4026 stat_cache_delete_dir(pconf->srv, CONST_BUF_LEN(con->physical.path));
4028 else if (con->physical.path->ptr[con->physical.path->used - 2] == '/')
4029 http_status_set_error(con, 403);
4030 else {
4031 const int status = webdav_delete_file(pconf, &con->physical);
4032 if (0 == status) {
4033 webdav_lock_delete_uri(pconf, con->physical.rel_path);
4034 http_status_set_fin(con, 204); /* No Content */
4036 else
4037 http_status_set_error(con, status);
4040 return HANDLER_FINISHED;
4044 static ssize_t
4045 mod_webdav_write_cq_first_chunk (connection * const con, chunkqueue * const cq,
4046 const int fd)
4048 /* (Note: copying might take some time, temporarily pausing server) */
4049 chunk *c = cq->first;
4050 ssize_t wr = 0;
4052 switch(c->type) {
4053 case FILE_CHUNK:
4054 if (NULL != webdav_mmap_file_chunk(c)) {
4055 do {
4056 wr = write(fd, c->file.mmap.start+c->offset,
4057 c->file.length - c->offset);
4058 } while (-1 == wr && errno == EINTR);
4059 break;
4061 else {
4062 switch (errno) {
4063 case ENOSYS: case ENODEV: case EINVAL: break;
4064 default:
4065 log_perror(con->errh, __FILE__, __LINE__,
4066 "open() or mmap() '%*.s'",
4067 BUFFER_INTLEN_PTR(c->mem));
4070 if (webdav_open_chunk_file_rd(c) < 0) {
4071 http_status_set_error(con, 500); /* Internal Server Error */
4072 return -1;
4074 ssize_t rd = -1;
4075 char buf[16384];
4076 do {
4077 if (-1 == lseek(c->file.fd, c->file.start+c->offset, SEEK_SET))
4078 break;
4079 off_t len = c->file.length - c->offset;
4080 if (len > (off_t)sizeof(buf)) len = (off_t)sizeof(buf);
4081 rd = read(c->file.fd, buf, (size_t)len);
4082 } while (-1 == rd && errno == EINTR);
4083 if (rd >= 0) {
4084 do {
4085 wr = write(fd, buf, (size_t)rd);
4086 } while (-1 == wr && errno == EINTR);
4087 break;
4089 else {
4090 log_perror(con->errh, __FILE__, __LINE__,
4091 "read() '%*.s'",
4092 BUFFER_INTLEN_PTR(c->mem));
4093 http_status_set_error(con, 500); /* Internal Server Error */
4094 return -1;
4097 case MEM_CHUNK:
4098 do {
4099 wr = write(fd, c->mem->ptr + c->offset,
4100 buffer_string_length(c->mem) - c->offset);
4101 } while (-1 == wr && errno == EINTR);
4102 break;
4105 if (wr > 0) {
4106 chunkqueue_mark_written(cq, wr);
4108 else if (wr < 0)
4109 http_status_set_error(con, (errno == ENOSPC) ? 507 : 403);
4111 return wr;
4115 __attribute_noinline__
4116 static int
4117 mod_webdav_write_cq (connection* const con, chunkqueue* const cq, const int fd)
4119 chunkqueue_remove_finished_chunks(cq);
4120 while (!chunkqueue_is_empty(cq)) {
4121 if (mod_webdav_write_cq_first_chunk(con, cq, fd) < 0) return 0;
4123 return 1;
4127 #if (defined(__linux__) || defined(__CYGWIN__)) && defined(O_TMPFILE)
4128 static int
4129 mod_webdav_write_single_file_chunk (connection* const con, chunkqueue* const cq)
4131 /* cq might have mem chunks after initial tempfile chunk
4132 * due to chunkqueue_steal() if request body is small */
4133 /*assert(cq->first->type == FILE_CHUNK);*/
4134 /*assert(cq->first->next != NULL);*/
4135 chunk * const c = cq->first;
4136 cq->first = c->next;
4137 if (mod_webdav_write_cq(con, cq, c->file.fd)) {
4138 /*assert(cq->first == NULL);*/
4139 c->next = NULL;
4140 cq->first = cq->last = c;
4141 return 1;
4143 else {
4144 /*assert(cq->first != NULL);*/
4145 c->next = cq->first;
4146 cq->first = c;
4147 return 0;
4150 #endif
4153 static handler_t
4154 mod_webdav_put_0 (connection * const con, const plugin_config * const pconf)
4156 if (0 != webdav_if_match_or_unmodified_since(con, NULL)) {
4157 http_status_set_error(con, 412); /* Precondition Failed */
4158 return HANDLER_FINISHED;
4161 /* special-case PUT 0-length file */
4162 int fd;
4163 fd = fdevent_open_cloexec(con->physical.path->ptr, 0,
4164 O_WRONLY | O_CREAT | O_EXCL | O_TRUNC,
4165 WEBDAV_FILE_MODE);
4166 if (fd >= 0) {
4167 if (0 != con->etag_flags) {
4168 /*(skip sending etag if fstat() error; not expected)*/
4169 struct stat st;
4170 if (0 == fstat(fd, &st)) webdav_response_etag(pconf, con, &st);
4172 close(fd);
4173 webdav_parent_modified(pconf, con->physical.path);
4174 http_status_set_fin(con, 201); /* Created */
4175 return HANDLER_FINISHED;
4177 else if (errno == EISDIR) {
4178 http_status_set_error(con, 405); /* Method Not Allowed */
4179 return HANDLER_FINISHED;
4182 if (errno == ELOOP)
4183 webdav_delete_file(pconf, &con->physical); /*(ignore result)*/
4184 /*(attempt unlink(); target might be symlink
4185 * and above O_NOFOLLOW resulted in ELOOP)*/
4187 fd = fdevent_open_cloexec(con->physical.path->ptr, 0,
4188 O_WRONLY | O_CREAT | O_TRUNC,
4189 WEBDAV_FILE_MODE);
4190 if (fd >= 0) {
4191 close(fd);
4192 http_status_set_fin(con, 204); /* No Content */
4193 return HANDLER_FINISHED;
4196 http_status_set_error(con, 500); /* Internal Server Error */
4197 return HANDLER_FINISHED;
4201 static handler_t
4202 mod_webdav_put_prep (connection * const con, const plugin_config * const pconf)
4204 if (NULL != http_header_request_get(con, HTTP_HEADER_OTHER,
4205 CONST_STR_LEN("Content-Range"))) {
4206 if (pconf->deprecated_unsafe_partial_put_compat) return HANDLER_GO_ON;
4207 /* [RFC7231] 4.3.4 PUT
4208 * An origin server that allows PUT on a given target resource MUST
4209 * send a 400 (Bad Request) response to a PUT request that contains a
4210 * Content-Range header field (Section 4.2 of [RFC7233]), since the
4211 * payload is likely to be partial content that has been mistakenly
4212 * PUT as a full representation.
4214 http_status_set_error(con, 400); /* Bad Request */
4215 return HANDLER_FINISHED;
4218 const uint32_t used = con->physical.path->used;
4219 char *slash = con->physical.path->ptr + used - 2;
4220 if (*slash == '/') { /* disallow PUT on a collection (path ends in '/') */
4221 http_status_set_error(con, 400); /* Bad Request */
4222 return HANDLER_FINISHED;
4225 /* special-case PUT 0-length file */
4226 if (0 == con->request.content_length)
4227 return mod_webdav_put_0(con, pconf);
4229 /* Create temporary file in target directory (to store reqbody as received)
4230 * Temporary file is unlinked so that if receiving reqbody fails,
4231 * temp file is automatically cleaned up when fd is closed.
4232 * While being received, temporary file is not part of directory listings.
4233 * While this might result in extra copying, it is simple and robust. */
4234 int fd;
4235 size_t len = buffer_string_length(con->physical.path);
4236 #if (defined(__linux__) || defined(__CYGWIN__)) && defined(O_TMPFILE)
4237 slash = memrchr(con->physical.path->ptr, '/', len);
4238 if (slash == con->physical.path->ptr) slash = NULL;
4239 if (slash) *slash = '\0';
4240 fd = fdevent_open_cloexec(con->physical.path->ptr, 1,
4241 O_RDWR | O_TMPFILE | O_APPEND, WEBDAV_FILE_MODE);
4242 if (slash) *slash = '/';
4243 if (fd < 0)
4244 #endif
4246 buffer_append_string_len(con->physical.path, CONST_STR_LEN("-XXXXXX"));
4247 fd = fdevent_mkstemp_append(con->physical.path->ptr);
4248 if (fd >= 0) unlink(con->physical.path->ptr);
4249 buffer_string_set_length(con->physical.path, len);
4251 if (fd < 0) {
4252 http_status_set_error(con, 500); /* Internal Server Error */
4253 return HANDLER_FINISHED;
4256 /* copy all chunks even though expecting (at most) single MEM_CHUNK chunk
4257 * (still, loop on partial writes)
4258 * (Note: copying might take some time, temporarily pausing server)
4259 * (error status is set if error occurs) */
4260 chunkqueue * const cq = con->request_content_queue;
4261 off_t cqlen = chunkqueue_length(cq);
4262 if (!mod_webdav_write_cq(con, cq, fd)) {
4263 close(fd);
4264 return HANDLER_FINISHED;
4267 chunkqueue_reset(cq);
4268 if (0 != cqlen) /*(con->physical.path copied, then c->mem cleared below)*/
4269 chunkqueue_append_file_fd(cq, con->physical.path, fd, 0, cqlen);
4270 else {
4271 /*(must be non-zero for fd to be appended, then reset to 0-length)*/
4272 chunkqueue_append_file_fd(cq, con->physical.path, fd, 0, 1);
4273 cq->last->file.length = 0;
4274 cq->bytes_in = 0;
4276 buffer_clear(cq->last->mem); /* file already unlink()ed */
4277 chunkqueue_set_tempdirs(cq, cq->tempdirs, INTMAX_MAX);
4278 cq->last->file.is_temp = 1;
4280 return HANDLER_GO_ON;
4284 #if (defined(__linux__) || defined(__CYGWIN__)) && defined(O_TMPFILE)
4285 static int
4286 mod_webdav_put_linkat_rename (connection * const con,
4287 const plugin_config * const pconf,
4288 const char * const pathtemp)
4290 chunkqueue * const cq = con->request_content_queue;
4291 chunk *c = cq->first;
4293 char pathproc[32] = "/proc/self/fd/";
4294 li_itostrn(pathproc+sizeof("/proc/self/fd/")-1,
4295 sizeof(pathproc)-(sizeof("/proc/self/fd/")-1), (long)c->file.fd);
4296 if (0 == linkat(AT_FDCWD, pathproc, AT_FDCWD, pathtemp, AT_SYMLINK_FOLLOW)){
4297 struct stat st;
4298 #ifdef RENAME_NOREPLACE /*(renameat2() not well-supported yet)*/
4299 if (0 == renameat2(AT_FDCWD, pathtemp,
4300 AT_FDCWD, con->physical.path->ptr, RENAME_NOREPLACE))
4301 http_status_set_fin(con, 201); /* Created */
4302 else if (0 == rename(pathtemp, con->physical.path->ptr))
4303 http_status_set_fin(con, 204); /* No Content */ /*(replaced)*/
4304 else
4305 #else
4306 http_status_set_fin(con, 0 == lstat(con->physical.path->ptr, &st)
4307 ? 204 /* No Content */
4308 : 201); /* Created */
4309 if (201 == http_status_get(con))
4310 webdav_parent_modified(pconf, con->physical.path);
4311 if (0 != rename(pathtemp, con->physical.path->ptr))
4312 #endif
4314 if (errno == EISDIR)
4315 http_status_set_error(con, 405); /* Method Not Allowed */
4316 else
4317 http_status_set_error(con, 403); /* Forbidden */
4318 unlink(pathtemp);
4321 if (0 != con->etag_flags && http_status_get(con) < 300) { /*(201, 204)*/
4322 /*(skip sending etag if fstat() error; not expected)*/
4323 if (0 == fstat(c->file.fd, &st))
4324 webdav_response_etag(pconf, con, &st);
4327 chunkqueue_mark_written(cq, c->file.length);
4328 return 1;
4331 return 0;
4333 #endif
4336 __attribute_cold__
4337 static handler_t
4338 mod_webdav_put_deprecated_unsafe_partial_put_compat (connection * const con,
4339 const plugin_config *pconf,
4340 const buffer * const h)
4342 /* historical code performed very limited range parse (repeated here) */
4343 /* we only support <num>- ... */
4344 const char *num = h->ptr;
4345 off_t offset;
4346 char *err;
4347 if (0 != strncmp(num, "bytes", sizeof("bytes")-1)) {
4348 http_status_set_error(con, 501); /* Not Implemented */
4349 return HANDLER_FINISHED;
4351 num += 5; /* +5 for "bytes" */
4352 offset = strtoll(num, &err, 10); /*(strtoll() ignores leading whitespace)*/
4353 if (num == err || *err != '-' || offset < 0) {
4354 http_status_set_error(con, 501); /* Not Implemented */
4355 return HANDLER_FINISHED;
4358 const int fd = fdevent_open_cloexec(con->physical.path->ptr, 0,
4359 O_WRONLY, WEBDAV_FILE_MODE);
4360 if (fd < 0) {
4361 http_status_set_error(con, (errno == ENOENT) ? 404 : 403);
4362 return HANDLER_FINISHED;
4365 if (-1 == lseek(fd, offset, SEEK_SET)) {
4366 close(fd);
4367 http_status_set_error(con, 500); /* Internal Server Error */
4368 return HANDLER_FINISHED;
4371 /* copy all chunks even though expecting single chunk
4372 * (still, loop on partial writes)
4373 * (Note: copying might take some time, temporarily pausing server)
4374 * (error status is set if error occurs) */
4375 mod_webdav_write_cq(con, con->request_content_queue, fd);
4377 struct stat st;
4378 if (0 != con->etag_flags && !http_status_is_set(con)) {
4379 /*(skip sending etag if fstat() error; not expected)*/
4380 if (0 != fstat(fd, &st)) con->etag_flags = 0;
4383 const int wc = close(fd);
4384 if (0 != wc && !http_status_is_set(con))
4385 http_status_set_error(con, (errno == ENOSPC) ? 507 : 403);
4387 if (!http_status_is_set(con)) {
4388 http_status_set_fin(con, 204); /* No Content */
4389 if (0 != con->etag_flags) webdav_response_etag(pconf, con, &st);
4392 return HANDLER_FINISHED;
4396 static handler_t
4397 mod_webdav_put (connection * const con, const plugin_config * const pconf)
4399 if (con->state == CON_STATE_READ_POST) {
4400 int first_read = chunkqueue_is_empty(con->request_content_queue);
4401 handler_t rc = connection_handle_read_post_state(pconf->srv, con);
4402 if (rc != HANDLER_GO_ON) {
4403 if (first_read && rc == HANDLER_WAIT_FOR_EVENT
4404 && 0 != webdav_if_match_or_unmodified_since(con, NULL)) {
4405 http_status_set_error(con, 412); /* Precondition Failed */
4406 return HANDLER_FINISHED;
4408 return rc;
4412 if (0 != webdav_if_match_or_unmodified_since(con, NULL)) {
4413 http_status_set_error(con, 412); /* Precondition Failed */
4414 return HANDLER_FINISHED;
4417 if (pconf->deprecated_unsafe_partial_put_compat) {
4418 const buffer * const h =
4419 http_header_request_get(con, HTTP_HEADER_OTHER,
4420 CONST_STR_LEN("Content-Range"));
4421 if (NULL != h)
4422 return
4423 mod_webdav_put_deprecated_unsafe_partial_put_compat(con,pconf,h);
4426 /* construct temporary filename in same directory as target
4427 * (expect cq contains exactly one chunk:
4428 * the temporary FILE_CHUNK created in mod_webdav_put_prep())
4429 * (do not reuse c->mem buffer; if tmpfile was unlinked, c->mem is blank)
4430 * (since temporary file was unlinked, no guarantee of unique name,
4431 * so add pid and fd to avoid conflict with an unlikely parallel
4432 * PUT request being handled by same server pid (presumably by
4433 * same client using same lock token)) */
4434 chunkqueue * const cq = con->request_content_queue;
4435 chunk *c = cq->first;
4437 /* future: might support client specifying getcontenttype property
4438 * using Content-Type request header. However, [RFC4918] 9.7.1 notes:
4439 * Many servers do not allow configuring the Content-Type on a
4440 * per-resource basis in the first place. Thus, clients can't always
4441 * rely on the ability to directly influence the content type by
4442 * including a Content-Type request header
4445 /*(similar to beginning of webdav_linktmp_rename())*/
4446 buffer * const tmpb = pconf->tmpb;
4447 buffer_copy_buffer(tmpb, con->physical.path);
4448 buffer_append_string_len(tmpb, CONST_STR_LEN("."));
4449 buffer_append_int(tmpb, (long)getpid());
4450 buffer_append_string_len(tmpb, CONST_STR_LEN("."));
4451 if (c->type == MEM_CHUNK)
4452 buffer_append_uint_hex_lc(tmpb, (uintptr_t)pconf); /*(stack/heap addr)*/
4453 else
4454 buffer_append_int(tmpb, (long)c->file.fd);
4455 buffer_append_string_len(tmpb, CONST_STR_LEN("~"));
4457 if (buffer_string_length(tmpb) >= PATH_MAX) { /*(temp file path too long)*/
4458 http_status_set_error(con, 500); /* Internal Server Error */
4459 return HANDLER_FINISHED;
4462 const char *pathtemp = tmpb->ptr;
4464 #if (defined(__linux__) || defined(__CYGWIN__)) && defined(O_TMPFILE)
4465 if (c->type == FILE_CHUNK) { /*(reqbody contained in single tempfile)*/
4466 if (NULL != c->next) {
4467 /* if request body <= 64k, in-memory chunks might have been
4468 * moved to cq instead of appended to first chunk FILE_CHUNK */
4469 if (!mod_webdav_write_single_file_chunk(con, cq))
4470 return HANDLER_FINISHED;
4472 if (mod_webdav_put_linkat_rename(con, pconf, pathtemp))
4473 return HANDLER_FINISHED;
4474 /* attempt traditional copy (below) if linkat() failed for any reason */
4476 #endif
4478 const int fd = fdevent_open_cloexec(pathtemp, 0,
4479 O_WRONLY | O_CREAT | O_EXCL | O_TRUNC,
4480 WEBDAV_FILE_MODE);
4481 if (fd < 0) {
4482 http_status_set_error(con, 500); /* Internal Server Error */
4483 return HANDLER_FINISHED;
4486 /* copy all chunks even though expecting single chunk
4487 * (still, loop on partial writes)
4488 * (Note: copying might take some time, temporarily pausing server)
4489 * (error status is set if error occurs) */
4490 mod_webdav_write_cq(con, cq, fd);
4492 struct stat st;
4493 if (0 != con->etag_flags && !http_status_is_set(con)) {
4494 /*(skip sending etag if fstat() error; not expected)*/
4495 if (0 != fstat(fd, &st)) con->etag_flags = 0;
4498 const int wc = close(fd);
4499 if (0 != wc && !http_status_is_set(con))
4500 http_status_set_error(con, (errno == ENOSPC) ? 507 : 403);
4502 if (!http_status_is_set(con)) {
4503 struct stat ste;
4504 http_status_set_fin(con, 0 == lstat(con->physical.path->ptr, &ste)
4505 ? 204 /* No Content */
4506 : 201); /* Created */
4507 if (201 == http_status_get(con))
4508 webdav_parent_modified(pconf, con->physical.path);
4509 if (0 == rename(pathtemp, con->physical.path->ptr)) {
4510 if (0 != con->etag_flags) webdav_response_etag(pconf, con, &st);
4512 else {
4513 if (errno == EISDIR)
4514 http_status_set_error(con, 405); /* Method Not Allowed */
4515 else
4516 http_status_set_error(con, 500); /* Internal Server Error */
4517 unlink(pathtemp);
4520 else
4521 unlink(pathtemp);
4523 return HANDLER_FINISHED;
4527 static handler_t
4528 mod_webdav_copymove_b (connection * const con, const plugin_config * const pconf, buffer * const dst_path, buffer * const dst_rel_path)
4530 int flags = WEBDAV_FLAG_OVERWRITE /*(default)*/
4531 | (con->conf.force_lowercase_filenames
4532 ? WEBDAV_FLAG_LC_NAMES
4533 : 0)
4534 | (con->request.http_method == HTTP_METHOD_MOVE
4535 ? WEBDAV_FLAG_MOVE_RENAME
4536 : WEBDAV_FLAG_COPY_LINK);
4538 const buffer * const h =
4539 http_header_request_get(con,HTTP_HEADER_OTHER,CONST_STR_LEN("Overwrite"));
4540 if (NULL != h) {
4541 if (h->used != 2
4542 || ((h->ptr[0] & 0xdf) != 'F' && (h->ptr[0] & 0xdf) != 'T')) {
4543 http_status_set_error(con, 400); /* Bad Request */
4544 return HANDLER_FINISHED;
4546 if ((h->ptr[0] & 0xdf) == 'F')
4547 flags &= ~WEBDAV_FLAG_OVERWRITE;
4550 /* parse Destination
4552 * http://127.0.0.1:1025/dav/litmus/copydest
4554 * - host has to match Host: header in request
4555 * (or else would need to check that Destination is reachable from server
4556 * and authentication credentials grant privileges on Destination)
4557 * - query string on Destination, if present, is discarded
4559 * NOTE: Destination path is relative to document root and IS NOT re-run
4560 * through other modules on server (such as aliasing or rewrite or userdir)
4562 const buffer * const destination =
4563 http_header_request_get(con, HTTP_HEADER_OTHER,
4564 CONST_STR_LEN("Destination"));
4565 if (NULL == destination) {
4566 http_status_set_error(con, 400); /* Bad Request */
4567 return HANDLER_FINISHED;
4569 #ifdef __COVERITY__
4570 force_assert(2 <= destination->used);
4571 #endif
4573 const char *sep = destination->ptr, *start;
4574 if (*sep != '/') { /* path-absolute or absolute-URI form */
4575 start = sep;
4576 sep = start + buffer_string_length(con->uri.scheme);
4577 if (0 != strncmp(start, con->uri.scheme->ptr, sep - start)
4578 || sep[0] != ':' || sep[1] != '/' || sep[2] != '/') {
4579 http_status_set_error(con, 400); /* Bad Request */
4580 return HANDLER_FINISHED;
4582 start = sep + 3;
4584 if (NULL == (sep = strchr(start, '/'))) {
4585 http_status_set_error(con, 400); /* Bad Request */
4586 return HANDLER_FINISHED;
4588 if (!buffer_is_equal_string(con->uri.authority, start, sep - start)
4589 /* skip login info (even though it should not be present) */
4590 && (NULL == (start = (char *)memchr(start, '@', sep - start))
4591 || (++start, !buffer_is_equal_string(con->uri.authority,
4592 start, sep - start)))) {
4593 /* not the same host */
4594 http_status_set_error(con, 502); /* Bad Gateway */
4595 return HANDLER_FINISHED;
4598 start = sep; /* starts with '/' */
4600 physical_st dst;
4601 dst.path = dst_path;
4602 dst.rel_path = dst_rel_path;
4604 /* destination: remove query string, urldecode, path_simplify
4605 * and (maybe) lowercase for consistent destination URI path */
4606 buffer_copy_string_len(dst_rel_path, start,
4607 NULL == (sep = strchr(start, '?'))
4608 ? destination->ptr + destination->used-1 - start
4609 : sep - start);
4610 if (buffer_string_length(dst_rel_path) >= PATH_MAX) {
4611 http_status_set_error(con, 403); /* Forbidden */
4612 return HANDLER_FINISHED;
4614 buffer_urldecode_path(dst_rel_path);
4615 if (!buffer_is_valid_UTF8(dst_rel_path)) {
4616 /* invalid UTF-8 after url-decode */
4617 http_status_set_error(con, 400);
4618 return HANDLER_FINISHED;
4620 buffer_path_simplify(dst_rel_path, dst_rel_path);
4621 if (buffer_string_is_empty(dst_rel_path) || dst_rel_path->ptr[0] != '/') {
4622 http_status_set_error(con, 400);
4623 return HANDLER_FINISHED;
4626 if (flags & WEBDAV_FLAG_LC_NAMES)
4627 buffer_to_lower(dst_rel_path);
4629 /* Destination physical path
4630 * src con->physical.path might have been remapped with mod_alias.
4631 * (but mod_alias does not modify con->physical.rel_path)
4632 * Find matching prefix to support use of mod_alias to remap webdav root.
4633 * Aliasing of paths underneath the webdav root might not work.
4634 * Likewise, mod_rewrite URL rewriting might thwart this comparison.
4635 * Use mod_redirect instead of mod_alias to remap paths *under* webdav root.
4636 * Use mod_redirect instead of mod_rewrite on *any* parts of path to webdav.
4637 * (Related, use mod_auth to protect webdav root, but avoid attempting to
4638 * use mod_auth on paths underneath webdav root, as Destination is not
4639 * validated with mod_auth)
4641 * tl;dr: webdav paths and webdav properties are managed by mod_webdav,
4642 * so do not modify paths externally or else undefined behavior
4643 * or corruption may occur
4645 * find matching URI prefix (lowercased if WEBDAV_FLAG_LC_NAMES)
4646 * (con->physical.rel_path and dst_rel_path will always match leading '/')
4647 * check if remaining con->physical.rel_path matches suffix of
4648 * con->physical.path so that we can use the prefix to remap
4649 * Destination physical path */
4650 #ifdef __COVERITY__
4651 force_assert(0 != con->physical.rel_path->used);
4652 #endif
4653 uint32_t i, remain;
4655 const char * const p1 = con->physical.rel_path->ptr;
4656 const char * const p2 = dst_rel_path->ptr;
4657 for (i = 0; p1[i] && p1[i] == p2[i]; ++i) ;
4658 while (i != 0 && p1[--i] != '/') ; /* find matching directory path */
4660 remain = con->physical.rel_path->used - 1 - i;
4661 if (con->physical.path->used - 1 <= remain) { /*(should not happen)*/
4662 http_status_set_error(con, 403); /* Forbidden */
4663 return HANDLER_FINISHED;
4665 if (0 == memcmp(con->physical.rel_path->ptr+i, /*(suffix match)*/
4666 con->physical.path->ptr + con->physical.path->used-1-remain,
4667 remain)) { /*(suffix match)*/
4668 #ifdef __COVERITY__
4669 force_assert(2 <= dst_rel_path->used);
4670 #endif
4671 buffer_copy_string_len(dst_path, con->physical.path->ptr,
4672 con->physical.path->used - 1 - remain);
4673 buffer_append_string_len(dst_path,
4674 dst_rel_path->ptr+i,
4675 dst_rel_path->used - 1 - i);
4676 if (buffer_string_length(dst_path) >= PATH_MAX) {
4677 http_status_set_error(con, 403); /* Forbidden */
4678 return HANDLER_FINISHED;
4681 else { /*(not expected; some other module mucked with path or rel_path)*/
4682 /* unable to perform physical path remap here;
4683 * assume doc_root/rel_path and no remapping */
4684 #ifdef __COVERITY__
4685 force_assert(2 <= con->physical.doc_root->used);
4686 force_assert(2 <= dst_path->used);
4687 #endif
4688 buffer_copy_buffer(dst_path, con->physical.doc_root);
4689 if (dst_path->ptr[dst_path->used-2] == '/')
4690 --dst_path->used; /* since dst_rel_path begins with '/' */
4691 buffer_append_string_buffer(dst_path, dst_rel_path);
4692 if (buffer_string_length(dst_rel_path) >= PATH_MAX) {
4693 http_status_set_error(con, 403); /* Forbidden */
4694 return HANDLER_FINISHED;
4698 if (con->physical.path->used <= dst_path->used
4699 && 0 == memcmp(con->physical.path->ptr, dst_path->ptr,
4700 con->physical.path->used-1)
4701 && (con->physical.path->ptr[con->physical.path->used-2] == '/'
4702 || dst_path->ptr[con->physical.path->used-1] == '/'
4703 || dst_path->ptr[con->physical.path->used-1] == '\0')) {
4704 /* dst must not be nested under (or same as) src */
4705 http_status_set_error(con, 403); /* Forbidden */
4706 return HANDLER_FINISHED;
4709 struct stat st;
4710 if (-1 == lstat(con->physical.path->ptr, &st)) {
4711 /* don't known about it yet, unlink will fail too */
4712 http_status_set_error(con, (errno == ENOENT) ? 404 : 403);
4713 return HANDLER_FINISHED;
4716 if (0 != webdav_if_match_or_unmodified_since(con, &st)) {
4717 http_status_set_error(con, 412); /* Precondition Failed */
4718 return HANDLER_FINISHED;
4721 if (S_ISDIR(st.st_mode)) {
4722 if (con->physical.path->ptr[con->physical.path->used - 2] != '/') {
4723 http_response_redirect_to_directory(pconf->srv, con, 308);
4724 return HANDLER_FINISHED; /* 308 Permanent Redirect */
4725 /* Alternatively, could append '/' to con->physical.path
4726 * and con->physical.rel_path, set Content-Location in
4727 * response headers, and continue to serve the request. */
4730 /* ensure Destination paths end with '/' since dst is a collection */
4731 #ifdef __COVERITY__
4732 force_assert(2 <= dst_rel_path->used);
4733 #endif
4734 if (dst_rel_path->ptr[dst_rel_path->used - 2] != '/') {
4735 buffer_append_slash(dst_rel_path);
4736 buffer_append_slash(dst_path);
4739 /* check for lock on destination (after ensuring dst ends in '/') */
4740 if (!webdav_has_lock(con, pconf, dst_rel_path))
4741 return HANDLER_FINISHED; /* 423 Locked */
4743 const int depth = webdav_parse_Depth(con);
4744 if (1 == depth) {
4745 http_status_set_error(con, 400); /* Bad Request */
4746 return HANDLER_FINISHED;
4748 if (0 == depth) {
4749 if (con->request.http_method == HTTP_METHOD_MOVE) {
4750 http_status_set_error(con, 400); /* Bad Request */
4751 return HANDLER_FINISHED;
4753 /* optionally create collection, then copy properties */
4754 int status;
4755 if (0 == lstat(dst_path->ptr, &st)) {
4756 if (S_ISDIR(st.st_mode))
4757 status = 204; /* No Content */
4758 else if (flags & WEBDAV_FLAG_OVERWRITE) {
4759 status = webdav_mkdir(pconf, &dst, 1);
4760 if (0 == status) status = 204; /* No Content */
4762 else
4763 status = 412; /* Precondition Failed */
4765 else if (errno == ENOENT) {
4766 status = webdav_mkdir(pconf, &dst,
4767 !!(flags & WEBDAV_FLAG_OVERWRITE));
4768 if (0 == status) status = 201; /* Created */
4770 else
4771 status = 403; /* Forbidden */
4772 if (status < 300) {
4773 http_status_set_fin(con, status);
4774 webdav_prop_copy_uri(pconf,con->physical.rel_path,dst.rel_path);
4776 else
4777 http_status_set_error(con, status);
4778 return HANDLER_FINISHED;
4781 /* ensure destination is not nested in source */
4782 if (dst_rel_path->ptr[dst_rel_path->used - 2] != '/') {
4783 buffer_append_slash(dst_rel_path);
4784 buffer_append_slash(dst_path);
4787 buffer * const ms = buffer_init(); /* multi-status */
4788 if (0 == webdav_copymove_dir(pconf, &con->physical, &dst, ms, flags)) {
4789 if (con->request.http_method == HTTP_METHOD_MOVE)
4790 webdav_lock_delete_uri_col(pconf, con->physical.rel_path);
4791 /*(requiring lock on destination requires MKCOL create dst first)
4792 *(if no lock support, return 200 OK unconditionally
4793 * instead of 200 OK or 201 Created; not fully RFC-conformant)*/
4794 http_status_set_fin(con, 200); /* OK */
4796 else {
4797 /* Note: this does not destroy any locks if any error occurs,
4798 * which is not a problem if lock is only on the collection
4799 * being moved, but might need finer updates if there are
4800 * locks on internal elements that are successfully moved */
4801 webdav_xml_doc_multistatus(con, pconf, ms); /* 207 Multi-status */
4803 buffer_free(ms);
4804 /* invalidate stat cache of src if MOVE, whether or not successful */
4805 if (con->request.http_method == HTTP_METHOD_MOVE)
4806 stat_cache_delete_dir(pconf->srv,CONST_BUF_LEN(con->physical.path));
4807 return HANDLER_FINISHED;
4809 else if (con->physical.path->ptr[con->physical.path->used - 2] == '/') {
4810 http_status_set_error(con, 403); /* Forbidden */
4811 return HANDLER_FINISHED;
4813 else {
4814 /* check if client has lock for destination
4815 * Note: requiring a lock on non-collection means that destination
4816 * should always exist since the issuance of the lock creates the
4817 * resource, so client will always have to provide Overwrite: T
4818 * for direct operations on non-collections (files) */
4819 if (!webdav_has_lock(con, pconf, dst_rel_path))
4820 return HANDLER_FINISHED; /* 423 Locked */
4822 /* check if destination exists
4823 * (Destination should exist since lock is required,
4824 * and obtaining a lock will create the resource) */
4825 int rc = lstat(dst_path->ptr, &st);
4826 if (0 == rc && S_ISDIR(st.st_mode)) {
4827 /* file to dir/
4828 * append basename to physical path
4829 * future: might set Content-Location if dst_path does not end '/'*/
4830 if (NULL != (sep = strrchr(con->physical.path->ptr, '/'))) {
4831 #ifdef __COVERITY__
4832 force_assert(0 != dst_path->used);
4833 #endif
4834 size_t len = con->physical.path->used - 1
4835 - (sep - con->physical.path->ptr);
4836 if (dst_path->ptr[dst_path->used-1] == '/') {
4837 ++sep; /*(avoid double-slash in path)*/
4838 --len;
4840 buffer_append_string_len(dst_path, sep, len);
4841 buffer_append_string_len(dst_rel_path, sep, len);
4842 if (buffer_string_length(dst_path) >= PATH_MAX) {
4843 http_status_set_error(con, 403); /* Forbidden */
4844 return HANDLER_FINISHED;
4846 rc = lstat(dst_path->ptr, &st);
4847 /* target (parent collection) already exists */
4848 http_status_set_fin(con, 204); /* No Content */
4852 if (-1 == rc) {
4853 char *slash;
4854 switch (errno) {
4855 case ENOENT:
4856 if (http_status_is_set(con)) break;
4857 /* check that parent collection exists */
4858 if ((slash = strrchr(dst_path->ptr, '/'))) {
4859 *slash = '\0';
4860 if (0 == lstat(dst_path->ptr, &st) && S_ISDIR(st.st_mode)) {
4861 *slash = '/';
4862 /* new entity will be created */
4863 if (!http_status_is_set(con)) {
4864 webdav_parent_modified(pconf, dst_path);
4865 http_status_set_fin(con, 201); /* Created */
4867 break;
4870 /* fall through */
4871 /*case ENOTDIR:*/
4872 default:
4873 http_status_set_error(con, 409); /* Conflict */
4874 return HANDLER_FINISHED;
4877 else if (!(flags & WEBDAV_FLAG_OVERWRITE)) {
4878 /* destination exists, but overwrite is not set */
4879 http_status_set_error(con, 412); /* Precondition Failed */
4880 return HANDLER_FINISHED;
4882 else if (S_ISDIR(st.st_mode)) {
4883 /* destination exists, but is a dir, not a file */
4884 http_status_set_error(con, 409); /* Conflict */
4885 return HANDLER_FINISHED;
4887 else { /* resource already exists */
4888 http_status_set_fin(con, 204); /* No Content */
4891 rc = webdav_copymove_file(pconf, &con->physical, &dst, &flags);
4892 if (0 == rc) {
4893 if (con->request.http_method == HTTP_METHOD_MOVE)
4894 webdav_lock_delete_uri(pconf, con->physical.rel_path);
4896 else
4897 http_status_set_error(con, rc);
4899 return HANDLER_FINISHED;
4904 static handler_t
4905 mod_webdav_copymove (connection * const con, const plugin_config * const pconf)
4907 buffer *dst_path = buffer_init();
4908 buffer *dst_rel_path = buffer_init();
4909 handler_t rc = mod_webdav_copymove_b(con, pconf, dst_path, dst_rel_path);
4910 buffer_free(dst_rel_path);
4911 buffer_free(dst_path);
4912 return rc;
4916 #ifdef USE_PROPPATCH
4917 static handler_t
4918 mod_webdav_proppatch (connection * const con, const plugin_config * const pconf)
4920 if (!pconf->sql) {
4921 http_header_response_set(con, HTTP_HEADER_OTHER,
4922 CONST_STR_LEN("Allow"),
4923 CONST_STR_LEN("GET, HEAD, PROPFIND, DELETE, "
4924 "MKCOL, PUT, MOVE, COPY"));
4925 http_status_set_error(con, 405); /* Method Not Allowed */
4926 return HANDLER_FINISHED;
4929 if (0 == con->request.content_length) {
4930 http_status_set_error(con, 400); /* Bad Request */
4931 return HANDLER_FINISHED;
4934 if (con->state == CON_STATE_READ_POST) {
4935 handler_t rc = connection_handle_read_post_state(pconf->srv, con);
4936 if (rc != HANDLER_GO_ON) return rc;
4939 struct stat st;
4940 if (0 != lstat(con->physical.path->ptr, &st)) {
4941 http_status_set_error(con, (errno == ENOENT) ? 404 : 403);
4942 return HANDLER_FINISHED;
4945 if (0 != webdav_if_match_or_unmodified_since(con, &st)) {
4946 http_status_set_error(con, 412); /* Precondition Failed */
4947 return HANDLER_FINISHED;
4950 if (S_ISDIR(st.st_mode)) {
4951 if (con->physical.path->ptr[con->physical.path->used - 2] != '/') {
4952 buffer *vb = http_header_request_get(con, HTTP_HEADER_OTHER,
4953 CONST_STR_LEN("User-Agent"));
4954 if (vb && 0 == strncmp(vb->ptr, "Microsoft-WebDAV-MiniRedir/",
4955 sizeof("Microsoft-WebDAV-MiniRedir/")-1)) {
4956 /* workaround Microsoft-WebDAV-MiniRedir bug */
4957 /* (might not be necessary for PROPPATCH here,
4958 * but match behavior in mod_webdav_propfind() for PROPFIND) */
4959 http_response_redirect_to_directory(pconf->srv, con, 308);
4960 return HANDLER_FINISHED;
4962 /* set "Content-Location" instead of sending 308 redirect to dir */
4963 if (!http_response_redirect_to_directory(pconf->srv, con, 0))
4964 return HANDLER_FINISHED;
4965 buffer_append_string_len(con->physical.path, CONST_STR_LEN("/"));
4966 buffer_append_string_len(con->physical.rel_path,CONST_STR_LEN("/"));
4969 else if (con->physical.path->ptr[con->physical.path->used - 2] == '/') {
4970 http_status_set_error(con, 403);
4971 return HANDLER_FINISHED;
4974 xmlDocPtr const xml = webdav_parse_chunkqueue(con, pconf);
4975 if (NULL == xml) {
4976 http_status_set_error(con, 400); /* Bad Request */
4977 return HANDLER_FINISHED;
4980 const xmlNode * const rootnode = xmlDocGetRootElement(xml);
4981 if (NULL == rootnode
4982 || 0 != webdav_xmlstrcmp_fixed(rootnode->name, "propertyupdate")) {
4983 http_status_set_error(con, 422); /* Unprocessable Entity */
4984 xmlFreeDoc(xml);
4985 return HANDLER_FINISHED;
4988 if (!webdav_db_transaction_begin_immediate(pconf)) {
4989 http_status_set_error(con, 500); /* Internal Server Error */
4990 xmlFreeDoc(xml);
4991 return HANDLER_FINISHED;
4994 /* NOTE: selectively providing multi-status response is NON-CONFORMANT
4995 * (specified in [RFC4918])
4996 * However, PROPPATCH is all-or-nothing, so client should be able to
4997 * unequivocably know that all items in PROPPATCH succeeded if it receives
4998 * 204 No Content, or that items that are not listed with a failure status
4999 * in a multi-status response have the status of 424 Failed Dependency,
5000 * without the server having to be explicit. */
5002 /* UPDATE request, we know 'set' and 'remove' */
5003 buffer *ms = NULL; /*(multi-status)*/
5004 int update;
5005 for (const xmlNode *cmd = rootnode->children; cmd; cmd = cmd->next) {
5006 if (!(update = (0 == webdav_xmlstrcmp_fixed(cmd->name, "set")))) {
5007 if (0 != webdav_xmlstrcmp_fixed(cmd->name, "remove"))
5008 continue; /* skip; not "set" or "remove" */
5011 for (const xmlNode *props = cmd->children; props; props = props->next) {
5012 if (0 != webdav_xmlstrcmp_fixed(props->name, "prop"))
5013 continue;
5015 const xmlNode *prop = props->children;
5016 /* libxml2 will keep those blank (whitespace only) nodes */
5017 while (NULL != prop && xmlIsBlankNode(prop))
5018 prop = prop->next;
5019 if (NULL == prop)
5020 continue;
5021 if (prop->ns && '\0' == *(char *)prop->ns->href
5022 && '\0' != *(char *)prop->ns->prefix) {
5023 /* error: missing namespace for property */
5024 log_error(con->errh, __FILE__, __LINE__,
5025 "no namespace for: %s", prop->name);
5026 if (!ms) ms = buffer_init(); /* Unprocessable Entity */
5027 webdav_xml_propstat_status(ms, "", (char *)prop->name, 422);
5028 continue;
5031 /* XXX: ??? should blank namespace be normalized to "DAV:" ???
5032 * ??? should this also be done in propfind requests ??? */
5034 if (prop->ns && 0 == strcmp((char *)prop->ns->href, "DAV:")) {
5035 const size_t namelen = strlen((char *)prop->name);
5036 const struct live_prop_list *list = protected_props;
5037 while (0 != list->len
5038 && (list->len != namelen
5039 || 0 != memcmp(prop->name, list->prop, list->len)))
5040 ++list;
5041 if (NULL != list->prop) {
5042 /* error <DAV:cannot-modify-protected-property/> */
5043 if (!ms) ms = buffer_init();
5044 webdav_xml_propstat_protected(ms, (char *)prop->name,
5045 namelen, 403); /* Forbidden */
5046 continue;
5050 if (update) {
5051 if (!prop->children) continue;
5052 char * const propval = prop->children
5053 ? (char *)xmlNodeListGetString(xml, prop->children, 0)
5054 : NULL;
5055 webdav_prop_update(pconf, con->physical.rel_path,
5056 (char *)prop->name,
5057 prop->ns ? (char *)prop->ns->href : "",
5058 propval ? propval : "");
5059 xmlFree(propval);
5061 else
5062 webdav_prop_delete(pconf, con->physical.rel_path,
5063 (char *)prop->name,
5064 prop->ns ? (char *)prop->ns->href : "");
5068 if (NULL == ms
5069 ? webdav_db_transaction_commit(pconf)
5070 : webdav_db_transaction_rollback(pconf)) {
5071 if (NULL == ms) {
5072 buffer *vb = http_header_request_get(con, HTTP_HEADER_OTHER,
5073 CONST_STR_LEN("User-Agent"));
5074 if (vb && 0 == strncmp(vb->ptr, "Microsoft-WebDAV-MiniRedir/",
5075 sizeof("Microsoft-WebDAV-MiniRedir/")-1)) {
5076 /* workaround Microsoft-WebDAV-MiniRedir bug; 204 not handled */
5077 /* 200 without response body or 204 both incorrectly interpreted
5078 * as 507 Insufficient Storage by Microsoft-WebDAV-MiniRedir. */
5079 ms = buffer_init(); /* 207 Multi-status */
5080 webdav_xml_response_status(ms, con->physical.path, 200);
5083 if (NULL == ms)
5084 http_status_set_fin(con, 204); /* No Content */
5085 else /* 207 Multi-status */
5086 webdav_xml_doc_multistatus_response(con, pconf, ms);
5088 else
5089 http_status_set_error(con, 500); /* Internal Server Error */
5091 if (NULL != ms)
5092 buffer_free(ms);
5094 xmlFreeDoc(xml);
5095 return HANDLER_FINISHED;
5097 #endif
5100 #ifdef USE_LOCKS
5101 struct webdav_conflicting_lock_st {
5102 webdav_lockdata *lockdata;
5103 buffer *b;
5107 static void
5108 webdav_conflicting_lock_cb (void * const vdata,
5109 const webdav_lockdata * const lockdata)
5111 /* lock is not available if someone else has exclusive lock or if
5112 * client requested exclusive lock and others have shared locks */
5113 struct webdav_conflicting_lock_st * const cbdata =
5114 (struct webdav_conflicting_lock_st *)vdata;
5115 if (lockdata->lockscope->used == sizeof("exclusive")
5116 || cbdata->lockdata->lockscope->used == sizeof("exclusive"))
5117 webdav_xml_href(cbdata->b, &lockdata->lockroot);
5121 static handler_t
5122 mod_webdav_lock (connection * const con, const plugin_config * const pconf)
5125 * a mac wants to write
5127 * LOCK /dav/expire.txt HTTP/1.1\r\n
5128 * User-Agent: WebDAVFS/1.3 (01308000) Darwin/8.1.0 (Power Macintosh)\r\n
5129 * Accept: * / *\r\n
5130 * Depth: 0\r\n
5131 * Timeout: Second-600\r\n
5132 * Content-Type: text/xml; charset=\"utf-8\"\r\n
5133 * Content-Length: 229\r\n
5134 * Connection: keep-alive\r\n
5135 * Host: 192.168.178.23:1025\r\n
5136 * \r\n
5137 * <?xml version=\"1.0\" encoding=\"utf-8\"?>\n
5138 * <D:lockinfo xmlns:D=\"DAV:\">\n
5139 * <D:lockscope><D:exclusive/></D:lockscope>\n
5140 * <D:locktype><D:write/></D:locktype>\n
5141 * <D:owner>\n
5142 * <D:href>http://www.apple.com/webdav_fs/</D:href>\n
5143 * </D:owner>\n
5144 * </D:lockinfo>\n
5147 if (con->request.content_length) {
5148 if (con->state == CON_STATE_READ_POST) {
5149 handler_t rc = connection_handle_read_post_state(pconf->srv, con);
5150 if (rc != HANDLER_GO_ON) return rc;
5154 /* XXX: maybe add config switch to require that authentication occurred? */
5155 buffer owner = { NULL, 0, 0 };/*owner (not authenticated)(auth_user unset)*/
5156 data_string * const authn_user = (data_string *)
5157 array_get_element_klen(con->environment, CONST_STR_LEN("REMOTE_USER"));
5159 /* future: make max timeout configurable (e.g. pconf->lock_timeout_max)
5161 * [RFC4918] 10.7 Timeout Request Header
5162 * The "Second" TimeType specifies the number of seconds that will elapse
5163 * between granting of the lock at the server, and the automatic removal
5164 * of the lock. The timeout value for TimeType "Second" MUST NOT be
5165 * greater than 2^32-1.
5168 webdav_lockdata lockdata = {
5169 { NULL, 0, 0 }, /* locktoken */
5170 { con->physical.rel_path->ptr,con->physical.rel_path->used,0},/*lockroot*/
5171 { NULL, 0, 0 }, /* ownerinfo */
5172 (authn_user ? authn_user->value : &owner), /* owner */
5173 NULL, /* lockscope */
5174 NULL, /* locktype */
5175 -1, /* depth */
5176 600 /* timeout (arbitrary default lock timeout: 10 minutes) */
5179 const buffer *h =
5180 http_header_request_get(con, HTTP_HEADER_OTHER, CONST_STR_LEN("Timeout"));
5181 if (!buffer_is_empty(h)) {
5182 /* loosely parse Timeout request header and ignore "infinity" timeout */
5183 /* future: might implement config param for upper limit for timeout */
5184 const char *p = h->ptr;
5185 do {
5186 if ((*p | 0x20) == 's'
5187 && 0 == strncasecmp(p, CONST_STR_LEN("second-"))) {
5188 long t = strtol(p+sizeof("second-")-1, NULL, 10);
5189 if (0 < t && t < lockdata.timeout)
5190 lockdata.timeout = t > 5 ? t : 5;
5191 /*(arbitrary min timeout: 5 secs)*/
5192 else if (sizeof(long) != sizeof(int) && t > INT32_MAX)
5193 lockdata.timeout = INT32_MAX;
5194 /* while UINT32_MAX is actual limit in RFC4918,
5195 * sqlite more easily supports int, though could be
5196 * changed to use int64 to for the timeout param.
5197 * The "limitation" between timeouts that are many
5198 * *years* long does not really matter in reality. */
5199 break;
5201 #if 0
5202 else if ((*p | 0x20) == 'i'
5203 && 0 == strncasecmp(p, CONST_STR_LEN("infinity"))) {
5204 lockdata.timeout = INT32_MAX;
5205 break;
5207 #endif
5208 while (*p != ',' && *p != '\0') ++p;
5209 while (*p == ' ' || *p == '\t') ++p;
5210 } while (*p != '\0');
5213 if (con->request.content_length) {
5214 lockdata.depth = webdav_parse_Depth(con);
5215 if (1 == lockdata.depth) {
5216 /* [RFC4918] 9.10.3 Depth and Locking
5217 * Values other than 0 or infinity MUST NOT be used
5218 * with the Depth header on a LOCK method.
5220 http_status_set_error(con, 400); /* Bad Request */
5221 return HANDLER_FINISHED;
5224 xmlDocPtr const xml = webdav_parse_chunkqueue(con, pconf);
5225 if (NULL == xml) {
5226 http_status_set_error(con, 400); /* Bad Request */
5227 return HANDLER_FINISHED;
5230 const xmlNode * const rootnode = xmlDocGetRootElement(xml);
5231 if (NULL == rootnode
5232 || 0 != webdav_xmlstrcmp_fixed(rootnode->name, "lockinfo")) {
5233 http_status_set_error(con, 422); /* Unprocessable Entity */
5234 xmlFreeDoc(xml);
5235 return HANDLER_FINISHED;
5238 const xmlNode *lockinfo = rootnode->children;
5239 for (; lockinfo; lockinfo = lockinfo->next) {
5240 if (0 == webdav_xmlstrcmp_fixed(lockinfo->name, "lockscope")) {
5241 const xmlNode *value = lockinfo->children;
5242 for (; value; value = value->next) {
5243 if (0 == webdav_xmlstrcmp_fixed(value->name, "exclusive"))
5244 lockdata.lockscope=(const buffer *)&lockscope_exclusive;
5245 else if (0 == webdav_xmlstrcmp_fixed(value->name, "shared"))
5246 lockdata.lockscope=(const buffer *)&lockscope_shared;
5247 else {
5248 lockdata.lockscope=NULL; /* trigger error below loop */
5249 break;
5253 else if (0 == webdav_xmlstrcmp_fixed(lockinfo->name, "locktype")) {
5254 const xmlNode *value = lockinfo->children;
5255 for (; value; value = value->next) {
5256 if (0 == webdav_xmlstrcmp_fixed(value->name, "write"))
5257 lockdata.locktype = (const buffer *)&locktype_write;
5258 else {
5259 lockdata.locktype = NULL;/* trigger error below loop */
5260 break;
5264 else if (0 == webdav_xmlstrcmp_fixed(lockinfo->name, "owner")) {
5265 if (lockinfo->children)
5266 lockdata.ownerinfo.ptr =
5267 (char *)xmlNodeListGetString(xml, lockinfo->children, 0);
5268 if (lockdata.ownerinfo.ptr)
5269 lockdata.ownerinfo.used = strlen(lockdata.ownerinfo.ptr)+1;
5273 do { /*(resources are cleaned up after code block)*/
5275 if (NULL == lockdata.lockscope || NULL == lockdata.locktype) {
5276 /*(missing lockscope and locktype in lock request)*/
5277 http_status_set_error(con, 422); /* Unprocessable Entity */
5278 break; /* clean up resources and return HANDLER_FINISHED */
5281 /* check lock prior to potentially creating new resource,
5282 * and prior to using entropy to create uuid */
5283 struct webdav_conflicting_lock_st cbdata;
5284 cbdata.lockdata = &lockdata;
5285 cbdata.b = buffer_init();
5286 webdav_lock_activelocks(pconf, &lockdata.lockroot,
5287 (0 == lockdata.depth ? 1 : -1),
5288 webdav_conflicting_lock_cb, &cbdata);
5289 if (0 != cbdata.b->used) {
5290 /* 423 Locked */
5291 webdav_xml_doc_error_no_conflicting_lock(con, cbdata.b);
5292 buffer_free(cbdata.b);
5293 break; /* clean up resources and return HANDLER_FINISHED */
5295 buffer_free(cbdata.b);
5297 int created = 0;
5298 struct stat st;
5299 if (0 != lstat(con->physical.path->ptr, &st)) {
5300 /* [RFC4918] 7.3 Write Locks and Unmapped URLs
5301 * A successful lock request to an unmapped URL MUST result in
5302 * the creation of a locked (non-collection) resource with empty
5303 * content.
5304 * [...]
5305 * The response MUST indicate that a resource was created, by
5306 * use of the "201 Created" response code (a LOCK request to an
5307 * existing resource instead will result in 200 OK).
5308 * [RFC4918] 9.10.4 Locking Unmapped URLs
5309 * A successful LOCK method MUST result in the creation of an
5310 * empty resource that is locked (and that is not a collection)
5311 * when a resource did not previously exist at that URL. Later on,
5312 * the lock may go away but the empty resource remains. Empty
5313 * resources MUST then appear in PROPFIND responses including that
5314 * URL in the response scope. A server MUST respond successfully
5315 * to a GET request to an empty resource, either by using a 204
5316 * No Content response, or by using 200 OK with a Content-Length
5317 * header indicating zero length
5319 * unmapped resource; create empty file
5320 * (open() should fail if path ends in '/', but does not on some OS.
5321 * This is desired behavior since collection should be created
5322 * with MKCOL, and not via LOCK on an unmapped resource) */
5323 const int fd =
5324 (errno == ENOENT
5325 && con->physical.path->ptr[con->physical.path->used-2] != '/')
5326 ? fdevent_open_cloexec(con->physical.path->ptr, 0,
5327 O_WRONLY | O_CREAT | O_EXCL | O_TRUNC,
5328 WEBDAV_FILE_MODE)
5329 : -1;
5330 if (fd >= 0) {
5331 /*(skip sending etag if fstat() error; not expected)*/
5332 if (0 != fstat(fd, &st)) con->etag_flags = 0;
5333 close(fd);
5334 created = 1;
5335 webdav_parent_modified(pconf, con->physical.path);
5337 else if (errno != EEXIST) {
5338 http_status_set_error(con, 403); /* Forbidden */
5339 break; /* clean up resources and return HANDLER_FINISHED */
5341 else if (0 != lstat(con->physical.path->ptr, &st)) {
5342 http_status_set_error(con, 403); /* Forbidden */
5343 break; /* clean up resources and return HANDLER_FINISHED */
5345 lockdata.depth = 0; /* force Depth: 0 on non-collections */
5348 if (!created) {
5349 if (0 != webdav_if_match_or_unmodified_since(con, &st)) {
5350 http_status_set_error(con, 412); /* Precondition Failed */
5351 break; /* clean up resources and return HANDLER_FINISHED */
5355 if (created) {
5357 else if (S_ISDIR(st.st_mode)) {
5358 if (con->physical.path->ptr[con->physical.path->used - 2] != '/') {
5359 /* 308 Permanent Redirect */
5360 http_response_redirect_to_directory(pconf->srv, con, 308);
5361 break; /* clean up resources and return HANDLER_FINISHED */
5362 /* Alternatively, could append '/' to con->physical.path
5363 * and con->physical.rel_path, set Content-Location in
5364 * response headers, and continue to serve the request */
5367 else if (con->physical.path->ptr[con->physical.path->used - 2] == '/') {
5368 http_status_set_error(con, 403); /* Forbidden */
5369 break; /* clean up resources and return HANDLER_FINISHED */
5371 else if (0 != lockdata.depth)
5372 lockdata.depth = 0; /* force Depth: 0 on non-collections */
5374 /* create locktoken
5375 * (uuid_unparse() output is 36 chars + '\0') */
5376 uuid_t id;
5377 char lockstr[sizeof("<urn:uuid:>") + 36] = "<urn:uuid:";
5378 lockdata.locktoken.ptr = lockstr+1; /*(without surrounding <>)*/
5379 lockdata.locktoken.used = sizeof(lockstr)-2;/*(without surrounding <>)*/
5380 uuid_generate(id);
5381 uuid_unparse(id, lockstr+sizeof("<urn:uuid:")-1);
5383 /* XXX: consider fix TOC-TOU race condition by starting transaction
5384 * and re-running webdav_lock_activelocks() check before running
5385 * webdav_lock_acquire() (but both routines would need to be modified
5386 * to defer calling sqlite3_reset(stmt) to be part of transaction) */
5387 if (webdav_lock_acquire(pconf, &lockdata)) {
5388 lockstr[sizeof(lockstr)-2] = '>';
5389 http_header_response_set(con, HTTP_HEADER_OTHER,
5390 CONST_STR_LEN("Lock-Token"),
5391 lockstr, sizeof(lockstr)-1);
5392 webdav_xml_doc_lock_acquired(con, pconf, &lockdata);
5393 if (0 != con->etag_flags && !S_ISDIR(st.st_mode))
5394 webdav_response_etag(pconf, con, &st);
5395 http_status_set_fin(con, created ? 201 : 200); /* Created | OK */
5397 else /*(database error obtaining lock)*/
5398 http_status_set_error(con, 500); /* Internal Server Error */
5400 } while (0); /*(resources are cleaned up after code block)*/
5402 xmlFree(lockdata.ownerinfo.ptr);
5403 xmlFreeDoc(xml);
5404 return HANDLER_FINISHED;
5406 else {
5407 h = http_header_request_get(con,HTTP_HEADER_OTHER,CONST_STR_LEN("If"));
5408 if (NULL == h
5409 || h->used < 6 || h->ptr[1] != '<' || h->ptr[h->used-3] != '>') {
5410 /*(rejects value with trailing LWS, even though RFC-permitted)*/
5411 http_status_set_error(con, 400); /* Bad Request */
5412 return HANDLER_FINISHED;
5414 /* remove (< >) around token */
5415 lockdata.locktoken.ptr = h->ptr+2;
5416 lockdata.locktoken.used = h->used-4;
5417 /*(future: fill in from database, though exclusive write lock is the
5418 * only lock supported at the moment)*/
5419 lockdata.lockscope = (const buffer *)&lockscope_exclusive;
5420 lockdata.locktype = (const buffer *)&locktype_write;
5421 lockdata.depth = 0;
5423 if (webdav_lock_refresh(pconf, &lockdata)) {
5424 webdav_xml_doc_lock_acquired(con, pconf, &lockdata);
5425 http_status_set_fin(con, 200); /* OK */
5427 else
5428 http_status_set_error(con, 412); /* Precondition Failed */
5430 return HANDLER_FINISHED;
5433 #endif
5436 #ifdef USE_LOCKS
5437 static handler_t
5438 mod_webdav_unlock (connection * const con, const plugin_config * const pconf)
5440 const buffer * const h =
5441 http_header_request_get(con, HTTP_HEADER_OTHER,
5442 CONST_STR_LEN("Lock-Token"));
5443 if (NULL == h
5444 || h->used < 4 || h->ptr[0] != '<' || h->ptr[h->used-2] != '>') {
5445 /*(rejects value with trailing LWS, even though RFC-permitted)*/
5446 http_status_set_error(con, 400); /* Bad Request */
5447 return HANDLER_FINISHED;
5450 buffer owner = { NULL, 0, 0 };/*owner (not authenticated)(auth_user unset)*/
5451 data_string * const authn_user = (data_string *)
5452 array_get_element_klen(con->environment, CONST_STR_LEN("REMOTE_USER"));
5454 webdav_lockdata lockdata = {
5455 { h->ptr+1, h->used-2, 0 }, /* locktoken (remove < > around token) */
5456 { con->physical.rel_path->ptr,con->physical.rel_path->used,0},/*lockroot*/
5457 { NULL, 0, 0 }, /* ownerinfo (unused for unlock) */
5458 (authn_user ? authn_user->value : &owner), /* owner */
5459 NULL, /* lockscope (unused for unlock) */
5460 NULL, /* locktype (unused for unlock) */
5461 0, /* depth (unused for unlock) */
5462 0 /* timeout (unused for unlock) */
5465 /* check URI (lockroot) and depth in scope for locktoken and authorized */
5466 switch (webdav_lock_match(pconf, &lockdata)) {
5467 case 0:
5468 if (webdav_lock_release(pconf, &lockdata)) {
5469 http_status_set_fin(con, 204); /* No Content */
5470 return HANDLER_FINISHED;
5472 /* fall through */
5473 default:
5474 case -1: /* lock does not exist */
5475 case -2: /* URI not in scope of locktoken and depth */
5476 /* 409 Conflict */
5477 webdav_xml_doc_error_lock_token_matches_request_uri(con);
5478 return HANDLER_FINISHED;
5479 case -3: /* not owner/not authorized to remove lock */
5480 http_status_set_error(con, 403); /* Forbidden */
5481 return HANDLER_FINISHED;
5484 #endif
5487 SUBREQUEST_FUNC(mod_webdav_subrequest_handler)
5489 const plugin_config * const pconf =
5490 (plugin_config *)con->plugin_ctx[((plugin_data *)p_d)->id];
5491 if (NULL == pconf) return HANDLER_GO_ON; /*(should not happen)*/
5492 UNUSED(srv);
5494 switch (con->request.http_method) {
5495 case HTTP_METHOD_PROPFIND:
5496 return mod_webdav_propfind(con, pconf);
5497 case HTTP_METHOD_MKCOL:
5498 return mod_webdav_mkcol(con, pconf);
5499 case HTTP_METHOD_DELETE:
5500 return mod_webdav_delete(con, pconf);
5501 case HTTP_METHOD_PUT:
5502 return mod_webdav_put(con, pconf);
5503 case HTTP_METHOD_MOVE:
5504 case HTTP_METHOD_COPY:
5505 return mod_webdav_copymove(con, pconf);
5506 #ifdef USE_PROPPATCH
5507 case HTTP_METHOD_PROPPATCH:
5508 return mod_webdav_proppatch(con, pconf);
5509 #endif
5510 #ifdef USE_LOCKS
5511 case HTTP_METHOD_LOCK:
5512 return mod_webdav_lock(con, pconf);
5513 case HTTP_METHOD_UNLOCK:
5514 return mod_webdav_unlock(con, pconf);
5515 #endif
5516 default:
5517 http_status_set_error(con, 501); /* Not Implemented */
5518 return HANDLER_FINISHED;
5523 PHYSICALPATH_FUNC(mod_webdav_physical_handler)
5525 /* physical path is set up */
5526 /*assert(0 != con->physical.path->used);*/
5527 #ifdef __COVERITY__
5528 force_assert(2 <= con->physical.path->used);
5529 #endif
5531 int check_readonly = 0;
5532 int check_lock_src = 0;
5533 int reject_reqbody = 0;
5535 /* check for WebDAV request methods handled by this module */
5536 switch (con->request.http_method) {
5537 case HTTP_METHOD_GET:
5538 case HTTP_METHOD_HEAD:
5539 case HTTP_METHOD_POST:
5540 default:
5541 return HANDLER_GO_ON;
5542 case HTTP_METHOD_PROPFIND:
5543 case HTTP_METHOD_LOCK:
5544 break;
5545 case HTTP_METHOD_UNLOCK:
5546 reject_reqbody = 1;
5547 break;
5548 case HTTP_METHOD_DELETE:
5549 case HTTP_METHOD_MOVE:
5550 reject_reqbody = 1; /*(fall through)*/ __attribute_fallthrough__
5551 case HTTP_METHOD_PROPPATCH:
5552 case HTTP_METHOD_PUT:
5553 check_readonly = check_lock_src = 1;
5554 break;
5555 case HTTP_METHOD_COPY:
5556 case HTTP_METHOD_MKCOL:
5557 check_readonly = reject_reqbody = 1;
5558 break;
5561 plugin_config pconf;
5562 mod_webdav_patch_connection(srv, con, (plugin_data *)p_d, &pconf);
5563 if (!pconf.enabled) return HANDLER_GO_ON;
5565 if (check_readonly && pconf.is_readonly) {
5566 http_status_set_error(con, 403); /* Forbidden */
5567 return HANDLER_FINISHED;
5570 if (reject_reqbody && con->request.content_length) {
5571 /* [RFC4918] 8.4 Required Bodies in Requests
5572 * Servers MUST examine all requests for a body, even when a
5573 * body was not expected. In cases where a request body is
5574 * present but would be ignored by a server, the server MUST
5575 * reject the request with 415 (Unsupported Media Type).
5577 http_status_set_error(con, 415); /* Unsupported Media Type */
5578 return HANDLER_FINISHED;
5581 if (check_lock_src && !webdav_has_lock(con, &pconf, con->physical.rel_path))
5582 return HANDLER_FINISHED; /* 423 Locked */
5584 /* initial setup for methods */
5585 switch (con->request.http_method) {
5586 case HTTP_METHOD_PUT:
5587 if (mod_webdav_put_prep(con, &pconf) == HANDLER_FINISHED)
5588 return HANDLER_FINISHED;
5589 break;
5590 default:
5591 break;
5594 con->mode = ((plugin_data *)p_d)->id;
5595 con->conf.stream_request_body = 0;
5596 con->plugin_ctx[((plugin_data *)p_d)->id] = &pconf;
5597 const handler_t rc =
5598 mod_webdav_subrequest_handler(srv, con, p_d); /*p->handle_subrequest()*/
5599 if (rc == HANDLER_FINISHED || rc == HANDLER_ERROR)
5600 con->plugin_ctx[((plugin_data *)p_d)->id] = NULL;
5601 else { /* e.g. HANDLER_WAIT_FOR_RD */
5602 plugin_config * const save_pconf =
5603 (plugin_config *)malloc(sizeof(pconf));
5604 force_assert(save_pconf);
5605 memcpy(save_pconf, &pconf, sizeof(pconf));
5606 con->plugin_ctx[((plugin_data *)p_d)->id] = save_pconf;
5608 return rc;
5612 CONNECTION_FUNC(mod_webdav_handle_reset) {
5613 /* free plugin_config if allocated and saved to per-request storage */
5614 void ** const restrict dptr = &con->plugin_ctx[((plugin_data *)p_d)->id];
5615 if (*dptr) {
5616 free(*dptr);
5617 *dptr = NULL;
5618 chunkqueue_set_tempdirs(con->request_content_queue, /* reset sz */
5619 con->request_content_queue->tempdirs, 0);
5621 UNUSED(srv);
5622 return HANDLER_GO_ON;