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