/*
* mod_webdav - WEBDAV support for lighttpd
*
* Fully-rewritten from original
* Copyright(c) 2019 Glenn Strauss gstrauss()gluelogic.com All rights reserved
* License: BSD 3-clause (same as lighttpd)
*/
/*
* Note: This plugin is a basic implementation of [RFC4918] WebDAV
*
* Version Control System (VCS) backing WebDAV is recommended instead
* and Subversion (svn) is one such VCS supporting WebDAV.
*
* status: *** EXPERIMENTAL *** (and likely insecure encoding/decoding)
*
* future:
*
* TODO: moving props should delete any existing props instead of
* preserving any that are not overwritten with UPDATE OR REPLACE
* (and, if merging directories, be careful when doing so)
* TODO: add proper support for locks with "shared" lockscope
* (instead of treating match of any shared lock as sufficient,
* even when there are different lockroots)
* TODO: does libxml2 xml-decode (html-decode),
* or must I do so to normalize input?
* TODO: add strict enforcement of property names to be valid XML tags
* (or encode as such before putting into database)
* & " < >
* TODO: should we be using xmlNodeListGetString() or xmlBufNodeDump()
* and how does it handle encoding and entity-inlining of external refs?
* Must xml decode/encode (normalize) before storing user data in database
* if going to add that data verbatim to xml doc returned in queries
* TODO: when walking xml nodes, should add checks for "DAV:" namespace
* TODO: consider where it might be useful/informative to check
* SQLITE_OK != sqlite3_reset() or SQLITE_OK != sqlite3_bind_...() or ...
* (in some cases no rows returned is ok, while in other cases it is not)
* TODO: Unsupported: !r->conf.follow_symlink is not currently honored;
* symlinks are followed. Supporting !r->conf.follow_symlinks would
* require operating system support for POSIX.1-2008 *at() commands,
* and reworking the mod_webdav code to exclusively operate with *at()
* commands, for example, replacing unlink() with unlinkat().
*
* RFE: add config option whether or not to include locktoken and ownerinfo
* in PROPFIND lockdiscovery
*
* deficiencies
* - incomplete "shared" lock support
* - review code for proper decoding/encoding of elements from/to XML and db
* - preserve XML info in scope on dead properties, e.g. xml:lang
*
* [RFC4918] 4.3 Property Values
* Servers MUST preserve the following XML Information Items (using the
* terminology from [REC-XML-INFOSET]) in storage and transmission of dead
* properties: ...
* [RFC4918] 14.26 set XML Element
* The 'set' element MUST contain only a 'prop' element. The elements
* contained by the 'prop' element inside the 'set' element MUST specify the
* name and value of properties that are set on the resource identified by
* Request-URI. If a property already exists, then its value is replaced.
* Language tagging information appearing in the scope of the 'prop' element
* (in the "xml:lang" attribute, if present) MUST be persistently stored along
* with the property, and MUST be subsequently retrievable using PROPFIND.
* [RFC4918] F.2 Changes for Server Implementations
* Strengthened server requirements for storage of property values, in
* particular persistence of language information (xml:lang), whitespace, and
* XML namespace information (see Section 4.3).
*
* resource usage containment
* - filesystem I/O operations might take a non-trivial amount of time,
* blocking the server from responding to other requests during this time.
* Potential solution: have a thread dedicated to handling webdav requests
* and serialize such requests in each thread dedicated to handling webdav.
* (Limit number of such dedicated threads.) Remove write interest from
* connection during this period so that server will not trigger any timeout
* on the connection.
* - recursive directory operations are depth-first and may consume a large
* number of file descriptors if the directory hierarchy is deep.
* Potential solution: serialize webdav requests into dedicated thread (above)
* Potential solution: perform breadth-first directory traversal and pwrite()
* directory paths into a temporary file. After reading each directory,
* close() the dirhandle and pread() next directory from temporary file.
* (Keeping list of directories in memory might result in large memory usage)
* - flush response to client (or to intermediate temporary file) at regular
* intervals or triggers to avoid response consume large amount of memory
* during operations on large collection hierarchies (with lots of nested
* directories)
*
* beware of security concerns involved in enabling WebDAV
* on publicly accessible servers
* - (general) [RFC4918] 20 Security Considersations
* - (specifically) [RFC4918] 20.6 Implications of XML Entities
* - TODO review usage of xml libs for security, resource usage, containment
* libxml2 vs expat vs ...
* http://xmlbench.sourceforge.net/
* http://stackoverflow.com/questions/399704/xml-parser-for-c
* http://tibleiz.net/asm-xml/index.html
* http://dev.yorhel.nl/yxml
* - how might mod_webdav be affected by mod_openssl setting REMOTE_USER?
* - when encoding href in responses, also ensure proper XML encoding
* (do we need to ENCODING_REL_URI and then ENCODING_MINIMAL_XML?)
* - TODO: any (non-numeric) data that goes into database should be encoded
* before being sent back to user, not just href. Perhaps anything that
* is not an href should be stored in database in XML-encoded form.
*
* consider implementing a set of (reasonable) limits on such things as
* - max number of collections
* - max number of objects in a collection
* - max number of properties per object
* - max length of property name
* - max length of property value
* - max length of locktoken, lockroot, ownerinfo
* - max number of locks held by a client, or by an owner
* - max number of locks on a resource (shared locks)
* - ...
*
* robustness
* - should check return value from sqlite3_reset(stmt) for REPLACE, UPDATE,
* DELETE statements (which is when commit occurs and locks are obtained/fail)
* - handle SQLITE_BUSY (e.g. other processes modifying db and hold locks)
* https://www.sqlite.org/lang_transaction.html
* https://www.sqlite.org/rescode.html#busy
* https://www.sqlite.org/c3ref/busy_handler.html
* https://www.sqlite.org/c3ref/busy_timeout.html
* - periodically execute query to delete expired locks
* (MOD_WEBDAV_SQLITE_LOCKS_DELETE_EXPIRED)
* (should defend against removing locks protecting long-running operations
* that are in progress on the server)
* - having all requests go through database, including GET and HEAD would allow
* for better transactional semantics, instead of the current race conditions
* inherent in multiple (and many) filesystem operations. All queries would
* go through database, which would map to objects on disk, and copy and move
* would simply be database entries to objects with reference counts and
* copy-on-write semantics (instead of potential hard-links on disk).
* lstat() information could also be stored in database. Right now, if a file
* is copied or moved or deleted, the status of the property update in the db
* is discarded, whether it succeeds or not, since file operation succeeded.
* (Then again, it might also be okay if props do not exist on a given file.)
* On the other hand, if everything went through database, then etag could be
* stored in database and could be updated upon PUT (or MOVE/COPY/DELETE).
* There would also need to be a way to trigger a rescan of filesystem to
* bring the database into sync with any out-of-band changes.
*
*
* notes:
*
* - lstat() used instead of stat_cache_*() since the stat_cache might have
* expired data, as stat_cache is not invalidated by outside modifications,
* such as WebDAV PUT method (unless FAM is used)
*
* - SQLite database can be run in WAL mode (https://sqlite.org/wal.html)
* though mod_webdav does not provide a mechanism to configure WAL.
* Instead, once lighttpd starts up mod_webdav and creates the database,
* set WAL mode on the database from the command and then restart lighttpd.
*/
/* linkat() fstatat() unlinkat() fdopendir() */
#if !defined(_XOPEN_SOURCE) || _XOPEN_SOURCE-0 < 700
#undef _XOPEN_SOURCE
#define _XOPEN_SOURCE 700
/* NetBSD dirent.h improperly hides fdopendir() (POSIX.1-2008) declaration
* which should be visible with _XOPEN_SOURCE 700 or _POSIX_C_SOURCE 200809L */
#ifdef __NetBSD__
#define _NETBSD_SOURCE
#endif
#endif
/* DT_UNKNOWN DTTOIF() */
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#ifdef __FreeBSD__ /* FreeBSD strict compliance hides libc extensions */
/*#define _XOPEN_SOURCE 700*//*(defined above)*/
#define _ISOC11_SOURCE 1
#define __BSD_VISIBLE 1
#endif
#include "first.h" /* first */
#include <sys/types.h>
#include "sys-dirent.h"
#include "sys-mmap.h"
#include <sys/types.h>
#include "sys-stat.h"
#include "sys-time.h"
#include "sys-unistd.h" /* <unistd.h> getpid() linkat() rmdir() unlinkat() */
#include <errno.h>
#include <fcntl.h>
#include <stdio.h> /* rename() */
#include <stdlib.h> /* strtol() */
#include <string.h>
#ifdef RENAME_NOREPLACE /*(renameat2() not well-supported yet)*/
#ifndef __ANDROID_API__ /*(not yet Android?)*/
#define HAVE_RENAMEAT2
#endif
#endif
#ifdef _DIRENT_HAVE_D_TYPE
#ifndef DTTOIF /* missing on Android? */
#define DTTOIF(dirtype) ((dirtype) << 12)
#endif
#endif
#ifdef HAVE_COPY_FILE_RANGE
#ifdef __FreeBSD__
typedef off_t loff_t;
#endif
#else
#ifdef __linux__
#include <sys/ioctl.h>
#include <linux/fs.h> /* ioctl(..., FICLONE, ...) */
#endif
#endif
#ifdef _WIN32
#define VC_EXTRALEAN
#define WIN32_LEAN_AND_MEAN
#include <windows.h> /* CopyFile() */
#endif
#ifdef AT_FDCWD
#ifndef _ATFILE_SOURCE
#define _ATFILE_SOURCE
#endif
#endif
#ifndef AT_SYMLINK_NOFOLLOW
#define AT_SYMLINK_NOFOLLOW 0
#endif
/* Note: filesystem access race conditions exist without _ATFILE_SOURCE */
#ifndef _ATFILE_SOURCE
/*(trigger linkat() fail to fallback logic in mod_webdav.c)*/
#define linkat(odfd,opath,ndfd,npath,flags) -1
#endif
#ifndef PATH_MAX
#define PATH_MAX 4096
#endif
#ifndef MOD_WEBDAV_BUILD_MINIMAL
#if defined(HAVE_LIBXML_H) && defined(HAVE_SQLITE3_H)
#define USE_LOCKS
#define USE_PROPPATCH
/* minor: libxml2 includes stdlib.h in headers, too */
#include <libxml/tree.h>
#include <libxml/parser.h>
#include <sqlite3.h>
#endif /* defined(HAVE_LIBXML_H) && defined(HAVE_SQLITE3_H) */
#endif /* MOD_WEBDAV_BUILD_MINIMAL */
#include "base.h"
#include "buffer.h"
#include "chunk.h"
#include "fdevent.h"
#include "http_chunk.h"
#include "http_date.h"
#include "http_etag.h"
#include "http_header.h"
#include "http_status.h"
#include "log.h"
#include "request.h"
#include "response.h" /* http_response_redirect_to_directory() */
#include "stat_cache.h" /* stat_cache_mimetype_by_ext() */
#include "plugin.h"
#if (defined(__linux__) || defined(__CYGWIN__)) && defined(O_TMPFILE)
static int has_proc_self_fd;
#endif
#define http_status_set_error(r,status) http_status_set_err_fin((r),(status))
typedef physical physical_st;
#define WEBDAV_FILE_MODE S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH
#define WEBDAV_DIR_MODE S_IRWXU|S_IRWXG|S_IRWXO
#define WEBDAV_FLAG_LC_NAMES 0x01
#define WEBDAV_FLAG_OVERWRITE 0x02
#define WEBDAV_FLAG_MOVE_RENAME 0x04
#define WEBDAV_FLAG_COPY_LINK 0x08
#define WEBDAV_FLAG_MOVE_XDEV 0x10
#define WEBDAV_FLAG_COPY_XDEV 0x20
#define WEBDAV_FLAG_NO_CLONE 0x40
#define webdav_xmlstrcmp_fixed(s, fixed) \
strncmp((const char *)(s), (fixed), sizeof(fixed))
#include <ctype.h> /* isupper() tolower() */
__attribute_noinline__
static void
webdav_str_len_to_lower (char * const ss, const uint32_t len)
{
/*(caller must ensure that len not truncated to (int);
* for current intended use, NAME_MAX typically <= 255)*/
unsigned char * const restrict s = (unsigned char *)ss;
for (int i = 0; i < (int)len; ++i) {
if (isupper(s[i]))
s[i] = tolower(s[i]);
}
}
typedef struct {
#ifdef USE_PROPPATCH
sqlite3 *sqlh;
sqlite3_stmt *stmt_props_select_propnames;
sqlite3_stmt *stmt_props_select_props;
sqlite3_stmt *stmt_props_select_prop;
sqlite3_stmt *stmt_props_update_prop;
sqlite3_stmt *stmt_props_delete_prop;
sqlite3_stmt *stmt_props_copy;
sqlite3_stmt *stmt_props_move;
sqlite3_stmt *stmt_props_move_col;
sqlite3_stmt *stmt_props_delete;
sqlite3_stmt *stmt_locks_acquire;
sqlite3_stmt *stmt_locks_refresh;
sqlite3_stmt *stmt_locks_release;
sqlite3_stmt *stmt_locks_read;
sqlite3_stmt *stmt_locks_read_uri;
sqlite3_stmt *stmt_locks_read_uri_infinity;
sqlite3_stmt *stmt_locks_read_uri_members;
sqlite3_stmt *stmt_locks_delete_uri;
sqlite3_stmt *stmt_locks_delete_uri_col;
#else
int dummy;
#endif
} sql_config;
enum { /* opts bitflags */
MOD_WEBDAV_UNSAFE_PARTIAL_PUT_COMPAT = 0x1
,MOD_WEBDAV_UNSAFE_PROPFIND_FOLLOW_SYMLINK = 0x2
,MOD_WEBDAV_PROPFIND_DEPTH_INFINITY = 0x4
,MOD_WEBDAV_CPYTMP_PARTIAL_PUT = 0x8
};
typedef struct {
unsigned short enabled;
unsigned short is_readonly;
unsigned short log_xml;
unsigned short opts;
sql_config *sql;
buffer *tmpb;
buffer *sqlite_db_name; /* not used after worker init */
} plugin_config;
typedef struct {
PLUGIN_DATA;
plugin_config defaults;
} plugin_data;
INIT_FUNC(mod_webdav_init);
FREE_FUNC(mod_webdav_free);
SETDEFAULTS_FUNC(mod_webdav_set_defaults);
SERVER_FUNC(mod_webdav_worker_init);
REQUEST_FUNC(mod_webdav_uri_handler);
REQUEST_FUNC(mod_webdav_physical_handler);
REQUEST_FUNC(mod_webdav_subrequest_handler);
REQUEST_FUNC(mod_webdav_handle_reset);
static const plugin mod_webdav_plugin = {
.name = "webdav",
.version = LIGHTTPD_VERSION_ID,
.init = mod_webdav_init,
.cleanup = mod_webdav_free,
.set_defaults = mod_webdav_set_defaults,
.worker_init = mod_webdav_worker_init,
.handle_uri_clean = mod_webdav_uri_handler,
.handle_physical = mod_webdav_physical_handler,
.handle_subrequest = mod_webdav_subrequest_handler,
.handle_request_reset = mod_webdav_handle_reset
};
INIT_FUNC(mod_webdav_init) {
plugin_data * const pd = ck_calloc(1, sizeof(plugin_data));
pd->self = &mod_webdav_plugin;
return pd;
}
__attribute_cold__
__declspec_dllexport__
int mod_webdav_plugin_init(plugin *p);
int mod_webdav_plugin_init(plugin *p) {
memcpy(p, &mod_webdav_plugin, sizeof(plugin));
return 0;
}
FREE_FUNC(mod_webdav_free) {
plugin_data * const p = (plugin_data *)p_d;
if (NULL == p->cvlist) return;
/* (init i to 0 if global context; to 1 to skip empty global context) */
for (int i = !p->cvlist[0].v.u2[1], used = p->nconfig; i < used; ++i) {
config_plugin_value_t *cpv = p->cvlist + p->cvlist[i].v.u2[0];
for (; -1 != cpv->k_id; ++cpv) {
switch (cpv->k_id) {
#ifdef USE_PROPPATCH
case 0: /* webdav.sqlite-db-name */
if (cpv->vtype == T_CONFIG_LOCAL) {
sql_config * const sql = cpv->v.v;
if (!sql->sqlh) {
free(sql);
continue;
}
sqlite3_finalize(sql->stmt_props_select_propnames);
sqlite3_finalize(sql->stmt_props_select_props);
sqlite3_finalize(sql->stmt_props_select_prop);
sqlite3_finalize(sql->stmt_props_update_prop);
sqlite3_finalize(sql->stmt_props_delete_prop);
sqlite3_finalize(sql->stmt_props_copy);
sqlite3_finalize(sql->stmt_props_move);
sqlite3_finalize(sql->stmt_props_move_col);
sqlite3_finalize(sql->stmt_props_delete);
sqlite3_finalize(sql->stmt_locks_acquire);
sqlite3_finalize(sql->stmt_locks_refresh);
sqlite3_finalize(sql->stmt_locks_release);
sqlite3_finalize(sql->stmt_locks_read);
sqlite3_finalize(sql->stmt_locks_read_uri);
sqlite3_finalize(sql->stmt_locks_read_uri_infinity);
sqlite3_finalize(sql->stmt_locks_read_uri_members);
sqlite3_finalize(sql->stmt_locks_delete_uri);
sqlite3_finalize(sql->stmt_locks_delete_uri_col);
sqlite3_close(sql->sqlh);
free(sql);
}
break;
#endif
default:
break;
}
}
}
/*(potentially disruptive if other modules also use sqlite)*/
/*sqlite3_shutdown();*/
}
static void mod_webdav_merge_config_cpv(plugin_config * const pconf, const config_plugin_value_t * const cpv) {
switch (cpv->k_id) { /* index into static config_plugin_keys_t cpk[] */
case 0: /* webdav.sqlite-db-name */
if (cpv->vtype == T_CONFIG_LOCAL)
pconf->sql = cpv->v.v;
break;
case 1: /* webdav.activate */
pconf->enabled = (unsigned short)cpv->v.u;
break;
case 2: /* webdav.is-readonly */
pconf->is_readonly = (unsigned short)cpv->v.u;
break;
case 3: /* webdav.log-xml */
pconf->log_xml = (unsigned short)cpv->v.u;
break;
case 4: /* webdav.opts */
if (cpv->vtype == T_CONFIG_LOCAL)
pconf->opts = (unsigned short)cpv->v.u;
break;
default:/* should not happen */
return;
}
}
static void mod_webdav_merge_config(plugin_config * const pconf, const config_plugin_value_t *cpv) {
do {
mod_webdav_merge_config_cpv(pconf, cpv);
} while ((++cpv)->k_id != -1);
}
static void mod_webdav_patch_config(request_st * const r, plugin_data * const p, plugin_config * const pconf) {
memcpy(pconf, &p->defaults, sizeof(plugin_config));
for (int i = 1, used = p->nconfig; i < used; ++i) {
if (config_check_cond(r, (uint32_t)p->cvlist[i].k_id))
mod_webdav_merge_config(pconf, p->cvlist + p->cvlist[i].v.u2[0]);
}
}
__attribute_cold__
static int mod_webdav_sqlite3_init (const char * restrict s, log_error_st *errh);
SETDEFAULTS_FUNC(mod_webdav_set_defaults) {
static const config_plugin_keys_t cpk[] = {
{ CONST_STR_LEN("webdav.sqlite-db-name"),
T_CONFIG_STRING,
T_CONFIG_SCOPE_CONNECTION }
,{ CONST_STR_LEN("webdav.activate"),
T_CONFIG_BOOL,
T_CONFIG_SCOPE_CONNECTION }
,{ CONST_STR_LEN("webdav.is-readonly"),
T_CONFIG_BOOL,
T_CONFIG_SCOPE_CONNECTION }
,{ CONST_STR_LEN("webdav.log-xml"),
T_CONFIG_BOOL,
T_CONFIG_SCOPE_CONNECTION }
,{ CONST_STR_LEN("webdav.opts"),
T_CONFIG_ARRAY_KVANY,
T_CONFIG_SCOPE_CONNECTION }
,{ NULL, 0,
T_CONFIG_UNSET,
T_CONFIG_SCOPE_UNSET }
};
plugin_data * const p = p_d;
if (!config_plugin_values_init(srv, p, cpk, "mod_webdav"))
return HANDLER_ERROR;
#ifdef USE_PROPPATCH
int sqlrc = sqlite3_config(SQLITE_CONFIG_SINGLETHREAD);
if (sqlrc != SQLITE_OK) {
log_error(srv->errh, __FILE__, __LINE__, "sqlite3_config(): %s",
sqlite3_errstr(sqlrc));
/*(performance option since our use is not threaded; not fatal)*/
/*return HANDLER_ERROR;*/
}
#endif
/* process and validate config directives
* (init i to 0 if global context; to 1 to skip empty global context) */
for (int i = !p->cvlist[0].v.u2[1]; i < p->nconfig; ++i) {
config_plugin_value_t *cpv = p->cvlist + p->cvlist[i].v.u2[0];
for (; -1 != cpv->k_id; ++cpv) {
switch (cpv->k_id) {
case 0: /* webdav.sqlite-db-name */
if (!buffer_is_blank(cpv->v.b)) {
if (!mod_webdav_sqlite3_init(cpv->v.b->ptr, srv->errh))
return HANDLER_ERROR;
}
break;
case 1: /* webdav.activate */
case 2: /* webdav.is-readonly */
case 3: /* webdav.log-xml */
break;
case 4: /* webdav.opts */
if (cpv->v.a->used) {
unsigned short opts = 0;
for (uint32_t j = 0, used = cpv->v.a->used; j < used; ++j) {
data_string *ds = (data_string *)cpv->v.a->data[j];
if (buffer_eq_slen(&ds->key,
CONST_STR_LEN("deprecated-unsafe-partial-put"))
&& config_plugin_value_to_bool((data_unset *)ds,0)) {
opts |= MOD_WEBDAV_UNSAFE_PARTIAL_PUT_COMPAT;
continue;
}
if (buffer_eq_slen(&ds->key,
CONST_STR_LEN("propfind-depth-infinity"))
&& config_plugin_value_to_bool((data_unset *)ds,0)) {
opts |= MOD_WEBDAV_PROPFIND_DEPTH_INFINITY;
continue;
}
if (buffer_eq_slen(&ds->key,
CONST_STR_LEN("unsafe-propfind-follow-symlink"))
&& config_plugin_value_to_bool((data_unset *)ds,0)) {
opts |= MOD_WEBDAV_UNSAFE_PROPFIND_FOLLOW_SYMLINK;
continue;
}
if (buffer_eq_slen(&ds->key,
CONST_STR_LEN("partial-put-copy-modify"))
&& config_plugin_value_to_bool((data_unset *)ds,0)) {
opts |= MOD_WEBDAV_CPYTMP_PARTIAL_PUT;
continue;
}
log_error(srv->errh, __FILE__, __LINE__,
"unrecognized webdav.opts: %s", ds->key.ptr);
return HANDLER_ERROR;
}
cpv->v.u = opts;
cpv->vtype = T_CONFIG_LOCAL;
}
break;
default:/* should not happen */
break;
}
}
}
p->defaults.tmpb = srv->tmp_buf;
/* initialize p->defaults from global config context */
if (p->nconfig > 0 && p->cvlist->v.u2[1]) {
const config_plugin_value_t *cpv = p->cvlist + p->cvlist->v.u2[0];
if (-1 != cpv->k_id)
mod_webdav_merge_config(&p->defaults, cpv);
}
#if (defined(__linux__) || defined(__CYGWIN__)) && defined(O_TMPFILE)
struct stat st;
has_proc_self_fd = (0 == stat("/proc/self/fd", &st));
#endif
return HANDLER_GO_ON;
}
URIHANDLER_FUNC(mod_webdav_uri_handler)
{
if (r->http_method != HTTP_METHOD_OPTIONS)
return HANDLER_GO_ON;
plugin_config pconf;
mod_webdav_patch_config(r, (plugin_data *)p_d, &pconf);
if (!pconf.enabled) return HANDLER_GO_ON;
/* [RFC4918] 18 DAV Compliance Classes */
#ifdef USE_LOCKS
if (pconf.sql)
http_header_response_set(r, HTTP_HEADER_OTHER,
CONST_STR_LEN("DAV"),
CONST_STR_LEN("1,2,3"));
else
#endif
http_header_response_set(r, HTTP_HEADER_OTHER,
CONST_STR_LEN("DAV"),
CONST_STR_LEN("1,3"));
/* instruct MS Office Web Folders to use DAV
* (instead of MS FrontPage Extensions)
* http://www.zorched.net/2006/03/01/more-webdav-tips-tricks-and-bugs/ */
http_header_response_set(r, HTTP_HEADER_OTHER,
CONST_STR_LEN("MS-Author-Via"),
CONST_STR_LEN("DAV"));
if (pconf.is_readonly)
http_header_response_append(r, HTTP_HEADER_ALLOW,
CONST_STR_LEN("Allow"),
CONST_STR_LEN("PROPFIND"));
#ifdef USE_PROPPATCH
else if (pconf.sql)
http_header_response_append(r, HTTP_HEADER_ALLOW,
CONST_STR_LEN("Allow"),
#ifdef USE_LOCKS
CONST_STR_LEN(
"PROPFIND, DELETE, MKCOL, PUT, MOVE, COPY, PROPPATCH, LOCK, UNLOCK")
#else
CONST_STR_LEN(
"PROPFIND, DELETE, MKCOL, PUT, MOVE, COPY, PROPPATCH")
#endif
);
#endif
else
http_header_response_append(r, HTTP_HEADER_ALLOW,
CONST_STR_LEN("Allow"),
CONST_STR_LEN(
"PROPFIND, DELETE, MKCOL, PUT, MOVE, COPY")
);
return HANDLER_GO_ON;
}
static void
webdav_double_buffer (request_st * const r, buffer * const b)
{
/* send parts of XML to r->write_queue; surrounding XML tags added later.
* http_chunk_append_buffer() is safe to use here since r->resp_body_started
* has not been set, so r->resp_send_chunked can not be set yet */
if (buffer_clen(b) > 60000) {
http_chunk_append_buffer(r, b); /*(might move/steal/reset buffer)*/
/*buffer_clear(b);*//*http_chunk_append_buffer() clears*/
}
}
#ifdef USE_LOCKS
typedef struct webdav_lockdata_wr {
buffer locktoken;
buffer lockroot;
buffer ownerinfo;
buffer *owner; /* NB: caller must provide writable storage */
const buffer *lockscope; /* future: might use enum, store int in db */
const buffer *locktype; /* future: might use enum, store int in db */
int depth;
int timeout; /* offset from now, not absolute time_t */
} webdav_lockdata_wr;
typedef struct webdav_lockdata {
buffer locktoken;
buffer lockroot;
buffer ownerinfo;
const buffer *owner;
const buffer *lockscope; /* future: might use enum, store int in db */
const buffer *locktype; /* future: might use enum, store int in db */
int depth;
int timeout; /* offset from now, not absolute time_t */
} webdav_lockdata;
typedef struct { const char *ptr; uint32_t used; uint32_t size; } tagb;
static const tagb lockscope_exclusive =
{ "exclusive", sizeof("exclusive"), 0 };
static const tagb lockscope_shared =
{ "shared", sizeof("shared"), 0 };
static const tagb locktype_write =
{ "write", sizeof("write"), 0 };
#endif
typedef struct {
const char *ns;
const char *name;
uint32_t nslen;
uint32_t namelen;
} webdav_property_name;
typedef struct {
webdav_property_name *ptr;
int used;
} webdav_property_names;
/*
* http://www.w3.org/TR/1998/NOTE-XML-data-0105/
* The datatype attribute "dt" is defined in the namespace named
* "urn:uuid:C2F41010-65B3-11d1-A29F-00AA00C14882/".
* (See the XML Namespaces Note at the W3C site for details of namespaces.)
* The full URN of the attribute is
* "urn:uuid:C2F41010-65B3-11d1-A29F-00AA00C14882/dt".
* http://www.w3.org/TR/1998/NOTE-xml-names-0119
* http://www.w3.org/TR/1998/WD-xml-names-19980327
* http://lists.xml.org/archives/xml-dev/200101/msg00924.html
* http://lists.xml.org/archives/xml-dev/200101/msg00929.html
* http://lists.xml.org/archives/xml-dev/200101/msg00930.html
* (Microsoft) Namespace Guidelines
* https://msdn.microsoft.com/en-us/library/ms879470%28v=exchg.65%29.aspx
* (Microsoft) XML Persistence Format
* https://msdn.microsoft.com/en-us/library/ms676547%28v=vs.85%29.aspx
* http://www.xml.com/pub/a/2002/06/26/vocabularies.html
* The "Uuid" namespaces is the namespace
* "uuid:C2F41010-65B3-11d1-A29F-00AA00C14882",
* mainly found in association with the MS Office
* namespace on the http://www.omg.org website.
* http://www.data2type.de/en/xml-xslt-xslfo/wordml/wordml-introduction/the-root-element/
* xmlns:dt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882"
* By using the prefix dt, the namespace declares an attribute which
* determines the data type of a value. The name of the underlying schema
* is dt.xsd and it can be found in the folder for Excel schemas.
*/
#define MOD_WEBDAV_XMLNS_NS0 "xmlns:ns0=\"urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/\""
__attribute_cold__
__attribute_noinline__
static void
webdav_xml_log_response (request_st * const r)
{
chunkqueue * const cq = &r->write_queue;
log_error_st * const errh = r->conf.errh;
if (chunkqueue_length(cq) <= 65536)
chunkqueue_read_squash(cq, errh);
char *s;
uint32_t len;
for (chunk *c = cq->first; c; c = c->next) {
switch (c->type) {
case MEM_CHUNK:
s = c->mem->ptr + c->offset;
len = buffer_clen(c->mem) - (uint32_t)c->offset;
break;
case FILE_CHUNK:
/*(safe to mmap tempfiles from response XML)*/
/*(response body provided in temporary file, so ok to mmap().
* Otherwise, must access through sys_setjmp_eval3()) */
/*(tempfiles (and xml response) should easily fit in uint32_t
* and are not expected to already be mmap'd. Avoid >= 128k
* requirement of chunkqueue_chunk_file_view() by using viewadj)*/
len = (uint32_t)(c->file.length - c->offset);
{
const chunk_file_view * const restrict cfv =
chunkqueue_chunk_file_viewadj(c, (off_t)len, r->conf.errh);
s = (cfv && chunk_file_view_dlen(cfv, c->offset) >= (off_t)len)
? chunk_file_view_dptr(cfv, c->offset)
: NULL;
}
if (s == NULL) continue;
break;
default:
continue;
}
log_error(errh, __FILE__, __LINE__, "XML-response-body: %.*s",
(int)len, s);
}
}
static void
webdav_xml_doctype (buffer * const b, request_st * const r)
{
http_header_response_set(r, HTTP_HEADER_CONTENT_TYPE,
CONST_STR_LEN("Content-Type"),
CONST_STR_LEN("application/xml;charset=utf-8"));
buffer_copy_string_len(b, CONST_STR_LEN(
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"));
}
static void
webdav_xml_prop (buffer * const b,
const webdav_property_name * const prop,
const char * const value, const uint32_t vlen)
{
if (0 == vlen) {
struct const_iovec iov[] = {
{ CONST_STR_LEN("<") }
,{ prop->name, prop->namelen }
,{ CONST_STR_LEN(" xmlns=\"") }
,{ prop->ns, prop->nslen }
,{ CONST_STR_LEN("\"/>") }
};
buffer_append_iovec(b, iov, sizeof(iov)/sizeof(*iov));
}
else {
struct const_iovec iov[] = {
{ CONST_STR_LEN("<") }
,{ prop->name, prop->namelen }
,{ CONST_STR_LEN(" xmlns=\"") }
,{ prop->ns, prop->nslen }
,{ CONST_STR_LEN("\">") }
,{ value, vlen }
,{ CONST_STR_LEN("</") }
,{ prop->name, prop->namelen }
,{ CONST_STR_LEN(">") }
};
buffer_append_iovec(b, iov, sizeof(iov)/sizeof(*iov));
}
}
static void
webdav_xml_href (buffer * const b, const buffer * const href)
{
buffer_append_string_len(b, CONST_STR_LEN(
"<D:href>"));
buffer_append_string_encoded(b, BUF_PTR_LEN(href), ENCODING_REL_URI);
buffer_append_string_len(b, CONST_STR_LEN(
"</D:href>\n"));
}
static void
webdav_xml_status (buffer * const b, const int status)
{
buffer_append_string_len(b, CONST_STR_LEN(
"<D:status>HTTP/1.1 "));
http_status_append(b, status);
buffer_append_string_len(b, CONST_STR_LEN(
"</D:status>\n"));
}
#ifdef USE_PROPPATCH
__attribute_cold__
static void
webdav_xml_propstat_protected (buffer * const b, const char * const propname,
const uint32_t len, const int status)
{
buffer_append_str3(b, CONST_STR_LEN(
"<D:propstat>\n"
"<D:prop><DAV:"),
propname, len, CONST_STR_LEN(
"/></D:prop>\n"
"<D:error><DAV:cannot-modify-protected-property/></D:error>\n"));
webdav_xml_status(b, status); /* 403 */
buffer_append_string_len(b, CONST_STR_LEN(
"</D:propstat>\n"));
}
#endif
#ifdef USE_PROPPATCH
__attribute_cold__
static void
webdav_xml_propstat_status (buffer * const b, const char * const ns,
const char * const name, const int status)
{
struct const_iovec iov[] = {
{ CONST_STR_LEN(
"<D:propstat>\n"
"<D:prop><") }
,{ ns, strlen(ns) }
,{ name, strlen(name) }
,{ CONST_STR_LEN(
"/></D:prop>\n") }
};
buffer_append_iovec(b, iov, sizeof(iov)/sizeof(*iov));
webdav_xml_status(b, status);
buffer_append_string_len(b, CONST_STR_LEN(
"</D:propstat>\n"));
}
#endif
static void
webdav_xml_propstat (buffer * const b, buffer * const value, const int status)
{
buffer_append_str3(b, CONST_STR_LEN(
"<D:propstat>\n"
"<D:prop>\n"),
BUF_PTR_LEN(value), CONST_STR_LEN(
"</D:prop>\n"));
webdav_xml_status(b, status);
buffer_append_string_len(b, CONST_STR_LEN(
"</D:propstat>\n"));
}
__attribute_cold__
static void
webdav_xml_response_status (request_st * const r,
const buffer * const href,
const int status)
{
buffer * const b = chunk_buffer_acquire();
buffer_append_string_len(b, CONST_STR_LEN(
"<D:response>\n"));
webdav_xml_href(b, href);
webdav_xml_status(b, status);
buffer_append_string_len(b, CONST_STR_LEN(
"</D:response>\n"));
/*(under extreme error conditions, write() to tempfile for each error)*/
http_chunk_append_buffer(r, b); /*(might move/steal/reset buffer)*/
chunk_buffer_release(b);
}
#ifdef USE_LOCKS
static void
webdav_xml_activelock (buffer * const b,
const webdav_lockdata * const lockdata,
const char * const tbuf, uint32_t tbuf_len)
{
struct const_iovec iov[] = {
{ CONST_STR_LEN(
"<D:activelock>\n"
"<D:lockscope>"
"<D:") }
,{ BUF_PTR_LEN(lockdata->lockscope) }
,{ CONST_STR_LEN(
"/>"
"</D:lockscope>\n"
"<D:locktype>"
"<D:") }
,{ BUF_PTR_LEN(lockdata->locktype) }
,{ CONST_STR_LEN(
"/>"
"</D:locktype>\n"
"<D:depth>") }
,{ CONST_STR_LEN(
"infinity") } /*(iov[5] might be changed in below)*/
,{ CONST_STR_LEN(
"</D:depth>\n"
"<D:timeout>") }
};
if (0 == lockdata->depth) {
iov[5].iov_base = "0";
iov[5].iov_len = sizeof("0")-1;
}
buffer_append_iovec(b, iov, sizeof(iov)/sizeof(*iov));
if (0 != tbuf_len)
buffer_append_string_len(b, tbuf, tbuf_len); /* "Second-..." */
else {
buffer_append_string_len(b, CONST_STR_LEN("Second-"));
buffer_append_int(b, lockdata->timeout);
}
struct const_iovec iovb[] = {
{ CONST_STR_LEN(
"</D:timeout>\n"
"<D:owner>") }
,{ "", 0 } /*(iov[1] filled in below)*/
,{ CONST_STR_LEN(
"</D:owner>\n"
"<D:locktoken>\n"
"<D:href>") } /*webdav_xml_href_raw();*/
,{ BUF_PTR_LEN(&lockdata->locktoken) } /*(as-is; not URL-encoded)*/
,{ CONST_STR_LEN(
"</D:href>\n"
"</D:locktoken>\n"
"<D:lockroot>\n") }
};
if (!buffer_is_blank(&lockdata->ownerinfo)) {
iov[1].iov_base = lockdata->ownerinfo.ptr;
iov[1].iov_len = buffer_clen(&lockdata->ownerinfo);
}
buffer_append_iovec(b, iovb, sizeof(iovb)/sizeof(*iovb));
webdav_xml_href(b, &lockdata->lockroot);
buffer_append_string_len(b, CONST_STR_LEN(
"</D:lockroot>\n"
"</D:activelock>\n"));
}
#endif
static void
webdav_xml_doc_multistatus (request_st * const r,
const plugin_config * const pconf)
{
http_status_set_fin(r, 207); /* Multi-status */
chunkqueue * const cq = &r->write_queue;
buffer * const b = chunkqueue_prepend_buffer_open(cq);
webdav_xml_doctype(b, r);
buffer_append_string_len(b, CONST_STR_LEN(
"<D:multistatus xmlns:D=\"DAV:\">\n"));
chunkqueue_prepend_buffer_commit(cq);
chunkqueue_append_mem(cq, CONST_STR_LEN(
"</D:multistatus>\n"));
if (pconf->log_xml)
webdav_xml_log_response(r);
}
#ifdef USE_PROPPATCH
static void
webdav_xml_doc_multistatus_response (request_st * const r,
const plugin_config * const pconf,
buffer * const ms)
{
http_status_set_fin(r, 207); /* Multi-status */
chunkqueue * const cq = &r->write_queue;
buffer * const b = chunkqueue_prepend_buffer_open(cq);
webdav_xml_doctype(b, r);
buffer_append_string_len(b, CONST_STR_LEN(
"<D:multistatus xmlns:D=\"DAV:\">\n"
"<D:response>\n"));
webdav_xml_href(b, &r->physical.rel_path);
chunkqueue_prepend_buffer_commit(cq);
chunkqueue_append_buffer(cq, ms); /*(might move/steal/reset buffer)*/
chunkqueue_append_mem(cq, CONST_STR_LEN(
"</D:response>\n"
"</D:multistatus>\n"));
if (pconf->log_xml)
webdav_xml_log_response(r);
}
#endif
#ifdef USE_LOCKS
static void
webdav_xml_doc_lock_acquired (request_st * const r,
const plugin_config * const pconf,
const webdav_lockdata * const lockdata)
{
/*(http_status is set by caller to 200 OK or 201 Created)*/
char tbuf[32] = "Second-";
const uint32_t tbuf_len = sizeof("Second-")-1 +
li_itostrn(tbuf+sizeof("Second-")-1, sizeof(tbuf)-(sizeof("Second-")-1),
lockdata->timeout);
http_header_response_set(r, HTTP_HEADER_OTHER,
CONST_STR_LEN("Timeout"),
tbuf, tbuf_len);
buffer * const b =
chunkqueue_append_buffer_open_sz(&r->write_queue, 1024);
webdav_xml_doctype(b, r);
buffer_append_string_len(b, CONST_STR_LEN(
"<D:prop xmlns:D=\"DAV:\">\n"
"<D:lockdiscovery>\n"));
webdav_xml_activelock(b, lockdata, tbuf, tbuf_len);
buffer_append_string_len(b, CONST_STR_LEN(
"</D:lockdiscovery>\n"
"</D:prop>\n"));
chunkqueue_append_buffer_commit(&r->write_queue);
if (pconf->log_xml)
webdav_xml_log_response(r);
}
#endif
/*
* [RFC4918] 16 Precondition/Postcondition XML Elements
*/
/*
* 403 Forbidden
* "<D:error><DAV:cannot-modify-protected-property/></D:error>"
*
* 403 Forbidden
* "<D:error><DAV:no-external-entities/></D:error>"
*
* 409 Conflict
* "<D:error><DAV:preserved-live-properties/></D:error>"
*/
__attribute_cold__
static void
webdav_xml_doc_error_propfind_finite_depth (request_st * const r)
{
http_status_set_fin(r, 403); /* Forbidden */
buffer * const b =
chunkqueue_append_buffer_open_sz(&r->write_queue, 256);
webdav_xml_doctype(b, r);
buffer_append_string_len(b, CONST_STR_LEN(
"<D:error><DAV:propfind-finite-depth/></D:error>\n"));
chunkqueue_append_buffer_commit(&r->write_queue);
}
#ifdef USE_LOCKS
__attribute_cold__
static void
webdav_xml_doc_error_lock_token_matches_request_uri (request_st * const r)
{
http_status_set_fin(r, 409); /* Conflict */
buffer * const b =
chunkqueue_append_buffer_open_sz(&r->write_queue, 256);
webdav_xml_doctype(b, r);
buffer_append_string_len(b, CONST_STR_LEN(
"<D:error><DAV:lock-token-matches-request-uri/></D:error>\n"));
chunkqueue_append_buffer_commit(&r->write_queue);
}
#endif
#ifdef USE_LOCKS
__attribute_cold__
static void
webdav_xml_doc_423_locked (request_st * const r, buffer * const hrefs,
const char * const errtag, const uint32_t errtaglen)
{
http_status_set_fin(r, 423); /* Locked */
chunkqueue * const cq = &r->write_queue;
buffer * const b = chunkqueue_prepend_buffer_open(cq);
webdav_xml_doctype(b, r);
buffer_append_str3(b,
CONST_STR_LEN(
"<D:error xmlns:D=\"DAV:\">\n"
"<D:"),
errtag, errtaglen,
CONST_STR_LEN(
">\n"));
chunkqueue_prepend_buffer_commit(cq);
buffer_append_str3(hrefs,
CONST_STR_LEN(
"</D:"),
errtag, errtaglen,
CONST_STR_LEN(
">\n"
"</D:error>\n"));
chunkqueue_append_buffer(cq, hrefs); /*(might move/steal/reset buffer)*/
}
#endif
#ifdef USE_LOCKS
__attribute_cold__
static void
webdav_xml_doc_error_lock_token_submitted (request_st * const r,
buffer * const hrefs)
{
webdav_xml_doc_423_locked(r, hrefs,
CONST_STR_LEN("lock-token-submitted"));
}
#endif
#ifdef USE_LOCKS
__attribute_cold__
static void
webdav_xml_doc_error_no_conflicting_lock (request_st * const r,
buffer * const hrefs)
{
webdav_xml_doc_423_locked(r, hrefs,
CONST_STR_LEN("no-conflicting-lock"));
}
#endif
#ifdef USE_PROPPATCH
#define MOD_WEBDAV_SQLITE_CREATE_TABLE_PROPERTIES \
"CREATE TABLE IF NOT EXISTS properties (" \
" resource TEXT NOT NULL," \
" prop TEXT NOT NULL," \
" ns TEXT NOT NULL," \
" value TEXT NOT NULL," \
" PRIMARY KEY(resource, prop, ns))"
#define MOD_WEBDAV_SQLITE_CREATE_TABLE_LOCKS \
"CREATE TABLE IF NOT EXISTS locks (" \
" locktoken TEXT NOT NULL," \
" resource TEXT NOT NULL," \
" lockscope TEXT NOT NULL," \
" locktype TEXT NOT NULL," \
" owner TEXT NOT NULL," \
" ownerinfo TEXT NOT NULL," \
" depth INT NOT NULL," \
" timeout TIMESTAMP NOT NULL," \
" PRIMARY KEY(locktoken))"
#define MOD_WEBDAV_SQLITE_PROPS_SELECT_PROPNAMES \
"SELECT prop, ns FROM properties WHERE resource = ?"
#define MOD_WEBDAV_SQLITE_PROPS_SELECT_PROP \
"SELECT value FROM properties WHERE resource = ? AND prop = ? AND ns = ?"
#define MOD_WEBDAV_SQLITE_PROPS_SELECT_PROPS \
"SELECT prop, ns, value FROM properties WHERE resource = ?"
#define MOD_WEBDAV_SQLITE_PROPS_UPDATE_PROP \
"REPLACE INTO properties (resource, prop, ns, value) VALUES (?, ?, ?, ?)"
#define MOD_WEBDAV_SQLITE_PROPS_DELETE_PROP \
"DELETE FROM properties WHERE resource = ? AND prop = ? AND ns = ?"
#define MOD_WEBDAV_SQLITE_PROPS_DELETE \
"DELETE FROM properties WHERE resource = ?"
#define MOD_WEBDAV_SQLITE_PROPS_COPY \
"INSERT INTO properties" \
" SELECT ?, prop, ns, value FROM properties WHERE resource = ?"
#define MOD_WEBDAV_SQLITE_PROPS_MOVE \
"UPDATE OR REPLACE properties SET resource = ? WHERE resource = ?"
#define MOD_WEBDAV_SQLITE_PROPS_MOVE_COL \
"UPDATE OR REPLACE properties SET resource = ? || SUBSTR(resource, ?)" \
" WHERE SUBSTR(resource, 1, ?) = ?"
#define MOD_WEBDAV_SQLITE_LOCKS_ACQUIRE \
"INSERT INTO locks" \
" (locktoken,resource,lockscope,locktype,owner,ownerinfo,depth,timeout)" \
" VALUES (?,?,?,?,?,?,?, CURRENT_TIME + ?)"
#define MOD_WEBDAV_SQLITE_LOCKS_REFRESH \
"UPDATE locks SET timeout = CURRENT_TIME + ? WHERE locktoken = ?"
#define MOD_WEBDAV_SQLITE_LOCKS_RELEASE \
"DELETE FROM locks WHERE locktoken = ?"
#define MOD_WEBDAV_SQLITE_LOCKS_READ \
"SELECT resource, owner, depth" \
" FROM locks WHERE locktoken = ?"
#define MOD_WEBDAV_SQLITE_LOCKS_READ_URI \
"SELECT" \
" locktoken,resource,lockscope,locktype,owner,ownerinfo,depth," \
"timeout - CURRENT_TIME" \
" FROM locks WHERE resource = ?"
#define MOD_WEBDAV_SQLITE_LOCKS_READ_URI_INFINITY \
"SELECT" \
" locktoken,resource,lockscope,locktype,owner,ownerinfo,depth," \
"timeout - CURRENT_TIME" \
" FROM locks" \
" WHERE depth = -1 AND resource = SUBSTR(?, 1, LENGTH(resource))"
#define MOD_WEBDAV_SQLITE_LOCKS_READ_URI_MEMBERS \
"SELECT" \
" locktoken,resource,lockscope,locktype,owner,ownerinfo,depth," \
"timeout - CURRENT_TIME" \
" FROM locks WHERE SUBSTR(resource, 1, ?) = ?"
#define MOD_WEBDAV_SQLITE_LOCKS_DELETE_URI \
"DELETE FROM locks WHERE resource = ?"
#define MOD_WEBDAV_SQLITE_LOCKS_DELETE_URI_COL \
"DELETE FROM locks WHERE SUBSTR(resource, 1, ?) = ?"
/*"DELETE FROM locks WHERE locktoken LIKE ? || '%'"*/
/*(not currently used)*/
#define MOD_WEBDAV_SQLITE_LOCKS_DELETE_EXPIRED \
"DELETE FROM locks WHERE timeout < CURRENT_TIME"
#endif /* USE_PROPPATCH */
__attribute_cold__
static int
mod_webdav_sqlite3_init (const char * const restrict dbname,
log_error_st * const errh)
{
#ifndef USE_PROPPATCH
log_error(errh, __FILE__, __LINE__,
"Sorry, no sqlite3 and libxml2 support include, "
"compile with --with-webdav-props");
UNUSED(dbname);
return 0;
#else /* USE_PROPPATCH */
int sqlrc = sqlite3_initialize();
if (sqlrc != SQLITE_OK) {
log_error(errh, __FILE__, __LINE__, "sqlite3_initialize(): %s",
sqlite3_errstr(sqlrc));
return 0;
}
/*(expects (plugin_config *s) (log_error_st *errh) (char *err))*/
#define MOD_WEBDAV_SQLITE_CREATE_TABLE(query, label) \
if (sqlite3_exec(sqlh, query, NULL, NULL, &err) != SQLITE_OK) { \
if (0 != strcmp(err, "table " label " already exists")) { \
log_error(errh, __FILE__, __LINE__, \
"create table " label ": %s", err); \
sqlite3_free(err); \
sqlite3_close(sqlh); \
return 0; \
} \
sqlite3_free(err); \
}
sqlite3 *sqlh;
sqlrc = sqlite3_open_v2(dbname, &sqlh,
SQLITE_OPEN_READWRITE|SQLITE_OPEN_CREATE, NULL);
if (sqlrc != SQLITE_OK) {
log_error(errh, __FILE__, __LINE__, "sqlite3_open() '%s': %s",
dbname, sqlh ? sqlite3_errmsg(sqlh) : sqlite3_errstr(sqlrc));
if (sqlh) sqlite3_close(sqlh);
return 0;
}
char *err = NULL;
MOD_WEBDAV_SQLITE_CREATE_TABLE( MOD_WEBDAV_SQLITE_CREATE_TABLE_PROPERTIES,
"properties");
MOD_WEBDAV_SQLITE_CREATE_TABLE( MOD_WEBDAV_SQLITE_CREATE_TABLE_LOCKS,
"locks");
/* add ownerinfo column to locks table (update older mod_webdav sqlite db)
* (could check if 'PRAGMA user_version;' is 0, add column, and increment)*/
#define MOD_WEBDAV_SQLITE_SELECT_LOCKS_OWNERINFO_TEST \
"SELECT COUNT(*) FROM locks WHERE ownerinfo = ''"
#define MOD_WEBDAV_SQLITE_ALTER_TABLE_LOCKS \
"ALTER TABLE locks ADD COLUMN ownerinfo TEXT NOT NULL DEFAULT ''"
if (sqlite3_exec(sqlh, MOD_WEBDAV_SQLITE_SELECT_LOCKS_OWNERINFO_TEST,
NULL, NULL, &err) != SQLITE_OK) {
sqlite3_free(err); /* "no such column: ownerinfo" */
if (sqlite3_exec(sqlh, MOD_WEBDAV_SQLITE_ALTER_TABLE_LOCKS,
NULL, NULL, &err) != SQLITE_OK) {
log_error(errh, __FILE__, __LINE__, "alter table locks: %s", err);
sqlite3_free(err);
sqlite3_close(sqlh);
return 0;
}
}
sqlite3_close(sqlh);
return 1;
#endif /* USE_PROPPATCH */
}
#ifdef USE_PROPPATCH
__attribute_noinline__
static int
mod_webdav_sqlite3_prepare (sql_config * const restrict sql,
const char *query,
size_t qsz,
sqlite3_stmt **stmt,
log_error_st * const errh)
{
#ifdef SQLITE_PREPARE_PERSISTENT
if (sqlite3_prepare_v3(sql->sqlh, query, qsz, SQLITE_PREPARE_PERSISTENT,
stmt, NULL) != SQLITE_OK)
#else
if (sqlite3_prepare_v2(sql->sqlh, query, qsz,
stmt, NULL) != SQLITE_OK)
#endif
{
log_error(errh, __FILE__, __LINE__, "sqlite3_prepare(): %s",
sqlite3_errmsg(sql->sqlh));
return 0;
}
return 1;
}
__attribute_cold__
static int
mod_webdav_sqlite3_prep (sql_config * const restrict sql,
const char * const sqlite_db_name,
log_error_st * const errh)
{
/*(expects (sql_config *sql) (log_error_st *errh))*/
/*(note: include nul byte in sizeof(query) passed to sqlite3_prepare*())*/
#define MOD_WEBDAV_SQLITE_PREPARE_STMT(query, stmt) \
if (!mod_webdav_sqlite3_prepare(sql, query, sizeof(query), &stmt, errh)) \
return 0;
int sqlrc = sqlite3_open_v2(sqlite_db_name, &sql->sqlh,
SQLITE_OPEN_READWRITE, NULL);
if (sqlrc != SQLITE_OK) {
log_error(errh, __FILE__, __LINE__, "sqlite3_open() '%s': %s",
sqlite_db_name,
sql->sqlh
? sqlite3_errmsg(sql->sqlh)
: sqlite3_errstr(sqlrc));
return 0;
}
#ifdef SQLITE_DBCONFIG_DQS_DDL
sqlite3_db_config(sql->sqlh, SQLITE_DBCONFIG_DQS_DDL, 0, NULL);
#endif
#ifdef SQLITE_DBCONFIG_DQS_DML
sqlite3_db_config(sql->sqlh, SQLITE_DBCONFIG_DQS_DML, 0, NULL);
#endif
/* future: perhaps not all statements should be prepared;
* infrequently executed statements could be run with sqlite3_exec(),
* or prepared and finalized on each use, as needed */
MOD_WEBDAV_SQLITE_PREPARE_STMT( MOD_WEBDAV_SQLITE_PROPS_SELECT_PROPNAMES,
sql->stmt_props_select_propnames);
MOD_WEBDAV_SQLITE_PREPARE_STMT( MOD_WEBDAV_SQLITE_PROPS_SELECT_PROPS,
sql->stmt_props_select_props);
MOD_WEBDAV_SQLITE_PREPARE_STMT( MOD_WEBDAV_SQLITE_PROPS_SELECT_PROP,
sql->stmt_props_select_prop);
MOD_WEBDAV_SQLITE_PREPARE_STMT( MOD_WEBDAV_SQLITE_PROPS_UPDATE_PROP,
sql->stmt_props_update_prop);
MOD_WEBDAV_SQLITE_PREPARE_STMT( MOD_WEBDAV_SQLITE_PROPS_DELETE_PROP,
sql->stmt_props_delete_prop);
MOD_WEBDAV_SQLITE_PREPARE_STMT( MOD_WEBDAV_SQLITE_PROPS_COPY,
sql->stmt_props_copy);
MOD_WEBDAV_SQLITE_PREPARE_STMT( MOD_WEBDAV_SQLITE_PROPS_MOVE,
sql->stmt_props_move);
MOD_WEBDAV_SQLITE_PREPARE_STMT( MOD_WEBDAV_SQLITE_PROPS_MOVE_COL,
sql->stmt_props_move_col);
MOD_WEBDAV_SQLITE_PREPARE_STMT( MOD_WEBDAV_SQLITE_PROPS_DELETE,
sql->stmt_props_delete);
MOD_WEBDAV_SQLITE_PREPARE_STMT( MOD_WEBDAV_SQLITE_LOCKS_ACQUIRE,
sql->stmt_locks_acquire);
MOD_WEBDAV_SQLITE_PREPARE_STMT( MOD_WEBDAV_SQLITE_LOCKS_REFRESH,
sql->stmt_locks_refresh);
MOD_WEBDAV_SQLITE_PREPARE_STMT( MOD_WEBDAV_SQLITE_LOCKS_RELEASE,
sql->stmt_locks_release);
MOD_WEBDAV_SQLITE_PREPARE_STMT( MOD_WEBDAV_SQLITE_LOCKS_READ,
sql->stmt_locks_read);
MOD_WEBDAV_SQLITE_PREPARE_STMT( MOD_WEBDAV_SQLITE_LOCKS_READ_URI,
sql->stmt_locks_read_uri);
MOD_WEBDAV_SQLITE_PREPARE_STMT( MOD_WEBDAV_SQLITE_LOCKS_READ_URI_INFINITY,
sql->stmt_locks_read_uri_infinity);
MOD_WEBDAV_SQLITE_PREPARE_STMT( MOD_WEBDAV_SQLITE_LOCKS_READ_URI_MEMBERS,
sql->stmt_locks_read_uri_members);
MOD_WEBDAV_SQLITE_PREPARE_STMT( MOD_WEBDAV_SQLITE_LOCKS_DELETE_URI,
sql->stmt_locks_delete_uri);
MOD_WEBDAV_SQLITE_PREPARE_STMT( MOD_WEBDAV_SQLITE_LOCKS_DELETE_URI_COL,
sql->stmt_locks_delete_uri_col);
return 1;
}
#endif /* USE_PROPPATCH */
__attribute_cold__
SERVER_FUNC(mod_webdav_worker_init)
{
#ifdef USE_PROPPATCH
/* open sqlite databases and prepare SQL statements in each worker process
*
* https://www.sqlite.org/faq.html
* Under Unix, you should not carry an open SQLite database
* across a fork() system call into the child process.
*/
plugin_data * const p = (plugin_data *)p_d;
/* (init i to 0 if global context; to 1 to skip empty global context) */
for (int i = !p->cvlist[0].v.u2[1], used = p->nconfig; i < used; ++i) {
config_plugin_value_t *cpv = p->cvlist + p->cvlist[i].v.u2[0];
for (; -1 != cpv->k_id; ++cpv) {
switch (cpv->k_id) {
#ifdef USE_PROPPATCH
case 0: /* webdav.sqlite-db-name */
if (!buffer_is_blank(cpv->v.b)) {
const char * const dbname = cpv->v.b->ptr;
cpv->v.v = ck_calloc(1, sizeof(sql_config));
cpv->vtype = T_CONFIG_LOCAL;
if (!mod_webdav_sqlite3_prep(cpv->v.v, dbname, srv->errh))
return HANDLER_ERROR;
/*(update p->defaults after init)*/
if (0 == i) p->defaults.sql = cpv->v.v;
}
break;
#endif
default:
break;
}
}
}
#else
UNUSED(srv);
UNUSED(p_d);
#endif /* USE_PROPPATCH */
return HANDLER_GO_ON;
}
#ifdef USE_PROPPATCH
static int
webdav_db_transaction (const plugin_config * const pconf,
const char * const action)
{
if (!pconf->sql)
return 1;
char *err = NULL;
if (SQLITE_OK == sqlite3_exec(pconf->sql->sqlh, action, NULL, NULL, &err))
return 1;
else {
#if 0
fprintf(stderr, "%s: %s: %s\n", __func__, action, err);
log_error(pconf->errh, __FILE__, __LINE__,
"%s: %s: %s\n", __func__, action, err);
#endif
sqlite3_free(err);
return 0;
}
}
#define webdav_db_transaction_begin(pconf) \
webdav_db_transaction(pconf, "BEGIN;")
#define webdav_db_transaction_begin_immediate(pconf) \
webdav_db_transaction(pconf, "BEGIN IMMEDIATE;")
#define webdav_db_transaction_commit(pconf) \
webdav_db_transaction(pconf, "COMMIT;")
#define webdav_db_transaction_rollback(pconf) \
webdav_db_transaction(pconf, "ROLLBACK;")
#else
#define webdav_db_transaction_begin(pconf) 1
#define webdav_db_transaction_begin_immediate(pconf) 1
#define webdav_db_transaction_commit(pconf) 1
#define webdav_db_transaction_rollback(pconf) 1
#endif
#ifdef USE_LOCKS
static int
webdav_lock_match (const plugin_config * const pconf,
const webdav_lockdata * const lockdata)
{
if (!pconf->sql)
return 0;
sqlite3_stmt * const stmt = pconf->sql->stmt_locks_read;
if (!stmt)
return 0;
sqlite3_bind_text(
stmt, 1, BUF_PTR_LEN(&lockdata->locktoken), SQLITE_STATIC);
int status = -1; /* if lock does not exist */
if (SQLITE_ROW == sqlite3_step(stmt)) {
const char *text = (char *)sqlite3_column_text(stmt, 0); /* resource */
uint32_t text_len = (uint32_t) sqlite3_column_bytes(stmt, 0);
if (text_len < lockdata->lockroot.used
&& 0 == memcmp(lockdata->lockroot.ptr, text, text_len)
&& (text_len == lockdata->lockroot.used-1
|| -1 == sqlite3_column_int(stmt, 2))) { /* depth */
text = (char *)sqlite3_column_text(stmt, 1); /* owner */
text_len = (uint32_t)sqlite3_column_bytes(stmt, 1);
if (0 == text_len /*(if no auth required to lock; not recommended)*/
|| buffer_eq_slen(lockdata->owner, text, text_len))
status = 0; /* success; lock match */
else {
/*(future: might check if owner is a privileged admin user)*/
status = -3; /* not lock owner; not authorized */
}
}
else
status = -2; /* URI is not in scope of lock */
}
sqlite3_reset(stmt);
/* status
* 0 lock exists and uri in scope and owner is privileged/owns lock
* -1 lock does not exist
* -2 URI is not in scope of lock
* -3 owner does not own lock/is not privileged
*/
return status;
}
#endif
#ifdef USE_LOCKS
static void
webdav_lock_activelocks_lockdata (sqlite3_stmt * const stmt,
webdav_lockdata_wr * const lockdata)
{
lockdata->locktoken.ptr = (char *)sqlite3_column_text(stmt, 0);
lockdata->locktoken.used = sqlite3_column_bytes(stmt, 0);
lockdata->lockroot.ptr = (char *)sqlite3_column_text(stmt, 1);
lockdata->lockroot.used = sqlite3_column_bytes(stmt, 1);
lockdata->lockscope =
(sqlite3_column_bytes(stmt, 2) == (int)sizeof("exclusive")-1)
? (const buffer *)&lockscope_exclusive
: (const buffer *)&lockscope_shared;
lockdata->locktype = (const buffer *)&locktype_write;
lockdata->owner->ptr = (char *)sqlite3_column_text(stmt, 4);
lockdata->owner->used = sqlite3_column_bytes(stmt, 4);
lockdata->ownerinfo.ptr = (char *)sqlite3_column_text(stmt, 5);
lockdata->ownerinfo.used = sqlite3_column_bytes(stmt, 5);
lockdata->depth = sqlite3_column_int(stmt, 6);
lockdata->timeout = sqlite3_column_int(stmt, 7);
if (lockdata->locktoken.used) ++lockdata->locktoken.used;
if (lockdata->lockroot.used) ++lockdata->lockroot.used;
if (lockdata->owner->used) ++lockdata->owner->used;
if (lockdata->ownerinfo.used) ++lockdata->ownerinfo.used;
}
typedef
void webdav_lock_activelocks_cb(void * const vdata,
const webdav_lockdata * const lockdata);
static void
webdav_lock_activelocks (const plugin_config * const pconf,
const buffer * const uri,
const int expand_checks,
webdav_lock_activelocks_cb * const lock_cb,
void * const vdata)
{
webdav_lockdata lockdata;
buffer owner = { NULL, 0, 0 };
lockdata.locktoken.size = 0;
lockdata.lockroot.size = 0;
lockdata.ownerinfo.size = 0;
lockdata.owner = &owner;
if (!pconf->sql)
return;
/* check for locks with Depth: 0 (and Depth: infinity if 0==expand_checks)*/
sqlite3_stmt *stmt = pconf->sql->stmt_locks_read_uri;
if (!stmt || !pconf->sql->stmt_locks_read_uri_infinity
|| !pconf->sql->stmt_locks_read_uri_members)
return;
sqlite3_bind_text(stmt, 1, BUF_PTR_LEN(uri), SQLITE_STATIC);
while (SQLITE_ROW == sqlite3_step(stmt)) {
/* (avoid duplication with query below if infinity lock on collection)
* (infinity locks are rejected on non-collections elsewhere) */
if (0 != expand_checks && -1 == sqlite3_column_int(stmt, 6) /*depth*/)
continue;
webdav_lock_activelocks_lockdata(stmt, (webdav_lockdata_wr *)&lockdata);
if (lockdata.timeout > 0)
lock_cb(vdata, &lockdata);
}
sqlite3_reset(stmt);
if (0 == expand_checks)
return;
/* check for locks with Depth: infinity
* (i.e. collections: self (if collection) or containing collections) */
stmt = pconf->sql->stmt_locks_read_uri_infinity;
sqlite3_bind_text(stmt, 1, BUF_PTR_LEN(uri), SQLITE_STATIC);
while (SQLITE_ROW == sqlite3_step(stmt)) {
webdav_lock_activelocks_lockdata(stmt, (webdav_lockdata_wr *)&lockdata);
if (lockdata.timeout > 0)
lock_cb(vdata, &lockdata);
}
sqlite3_reset(stmt);
if (1 == expand_checks)
return;
#ifdef __COVERITY__
force_assert(0 != uri->used);
#endif
/* check for locks on members within (internal to) collection */
stmt = pconf->sql->stmt_locks_read_uri_members;
sqlite3_bind_int( stmt, 1, (int)uri->used-1);
sqlite3_bind_text(stmt, 2, BUF_PTR_LEN(uri), SQLITE_STATIC);
while (SQLITE_ROW == sqlite3_step(stmt)) {
/* (avoid duplication with query above for exact resource match) */
if (uri->used-1 == (uint32_t)sqlite3_column_bytes(stmt, 1) /*resource*/)
continue;
webdav_lock_activelocks_lockdata(stmt, (webdav_lockdata_wr *)&lockdata);
if (lockdata.timeout > 0)
lock_cb(vdata, &lockdata);
}
sqlite3_reset(stmt);
}
#endif
static int
webdav_lock_delete_uri (const plugin_config * const pconf,
const buffer * const uri)
{
#ifdef USE_LOCKS
if (!pconf->sql)
return 0;
sqlite3_stmt * const stmt = pconf->sql->stmt_locks_delete_uri;
if (!stmt)
return 0;
sqlite3_bind_text(stmt, 1, BUF_PTR_LEN(uri), SQLITE_STATIC);
int status = 1;
while (SQLITE_DONE != sqlite3_step(stmt)) {
status = 0;
#if 0
fprintf(stderr, "%s: %s\n", __func__, sqlite3_errmsg(pconf->sql->sqlh));
log_error(pconf->errh, __FILE__, __LINE__,
"%s: %s", __func__, sqlite3_errmsg(pconf->sql->sqlh));
#endif
}
sqlite3_reset(stmt);
return status;
#else
UNUSED(pconf);
UNUSED(uri);
return 1;
#endif
}
static int
webdav_lock_delete_uri_col (const plugin_config * const pconf,
const buffer * const uri)
{
#ifdef USE_LOCKS
if (!pconf->sql)
return 0;
sqlite3_stmt * const stmt = pconf->sql->stmt_locks_delete_uri_col;
if (!stmt)
return 0;
#ifdef __COVERITY__
force_assert(0 != uri->used);
#endif
sqlite3_bind_int( stmt, 1, (int)uri->used-1);
sqlite3_bind_text(stmt, 2, BUF_PTR_LEN(uri), SQLITE_STATIC);
int status = 1;
while (SQLITE_DONE != sqlite3_step(stmt)) {
status = 0;
#if 0
fprintf(stderr, "%s: %s\n", __func__, sqlite3_errmsg(pconf->sql->sqlh));
log_error(pconf->errh, __FILE__, __LINE__,
"%s: %s", __func__, sqlite3_errmsg(pconf->sql->sqlh));
#endif
}
sqlite3_reset(stmt);
return status;
#else
UNUSED(pconf);
UNUSED(uri);
return 1;
#endif
}
#ifdef USE_LOCKS
static int
webdav_lock_acquire (const plugin_config * const pconf,
const webdav_lockdata * const lockdata)
{
/*
* future:
* only lockscope:"exclusive" and locktype:"write" currently supported,
* so inserting strings into database is extraneous, and anyway should
* be enums instead of strings, since there are limited supported values
*/
if (!pconf->sql)
return 0;
sqlite3_stmt * const stmt = pconf->sql->stmt_locks_acquire;
if (!stmt)
return 0;
sqlite3_bind_text(
stmt, 1, BUF_PTR_LEN(&lockdata->locktoken), SQLITE_STATIC);
sqlite3_bind_text(
stmt, 2, BUF_PTR_LEN(&lockdata->lockroot), SQLITE_STATIC);
sqlite3_bind_text(
stmt, 3, BUF_PTR_LEN(lockdata->lockscope), SQLITE_STATIC);
sqlite3_bind_text(
stmt, 4, BUF_PTR_LEN(lockdata->locktype), SQLITE_STATIC);
if (lockdata->owner->used)
sqlite3_bind_text(
stmt, 5, BUF_PTR_LEN(lockdata->owner), SQLITE_STATIC);
else
sqlite3_bind_text(
stmt, 5, CONST_STR_LEN(""), SQLITE_STATIC);
if (lockdata->ownerinfo.used)
sqlite3_bind_text(
stmt, 6, BUF_PTR_LEN(&lockdata->ownerinfo), SQLITE_STATIC);
else
sqlite3_bind_text(
stmt, 6, CONST_STR_LEN(""), SQLITE_STATIC);
sqlite3_bind_int(
stmt, 7, lockdata->depth);
sqlite3_bind_int(
stmt, 8, lockdata->timeout);
int status = 1;
if (SQLITE_DONE != sqlite3_step(stmt)) {
status = 0;
#if 0
fprintf(stderr, "%s: %s\n", __func__, sqlite3_errmsg(pconf->sql->sqlh));
log_error(pconf->errh, __FILE__, __LINE__,
"%s: %s", __func__, sqlite3_errmsg(pconf->sql->sqlh));
#endif
}
sqlite3_reset(stmt);
return status;
}
#endif
#ifdef USE_LOCKS
static int
webdav_lock_refresh (const plugin_config * const pconf,
webdav_lockdata * const lockdata)
{
if (!pconf->sql)
return 0;
sqlite3_stmt * const stmt = pconf->sql->stmt_locks_refresh;
if (!stmt)
return 0;
const buffer * const locktoken = &lockdata->locktoken;
sqlite3_bind_text(stmt, 1, BUF_PTR_LEN(locktoken), SQLITE_STATIC);
sqlite3_bind_int( stmt, 2, lockdata->timeout);
int status = 1;
if (SQLITE_DONE != sqlite3_step(stmt)) {
status = 0;
#if 0
fprintf(stderr, "%s: %s\n", __func__, sqlite3_errmsg(pconf->sql->sqlh));
log_error(pconf->errh, __FILE__, __LINE__,
"%s: %s", __func__, sqlite3_errmsg(pconf->sql->sqlh));
#endif
}
sqlite3_reset(stmt);
/*(future: fill in lockscope, locktype, depth from database)*/
return status;
}
#endif
#ifdef USE_LOCKS
static int
webdav_lock_release (const plugin_config * const pconf,
const webdav_lockdata * const lockdata)
{
if (!pconf->sql)
return 0;
sqlite3_stmt * const stmt = pconf->sql->stmt_locks_release;
if (!stmt)
return 0;
sqlite3_bind_text(
stmt, 1, BUF_PTR_LEN(&lockdata->locktoken), SQLITE_STATIC);
int status = 0;
if (SQLITE_DONE == sqlite3_step(stmt))
status = (0 != sqlite3_changes(pconf->sql->sqlh));
else {
#if 0
fprintf(stderr, "%s: %s\n", __func__, sqlite3_errmsg(pconf->sql->sqlh));
log_error(pconf->errh, __FILE__, __LINE__,
"%s: %s", __func__, sqlite3_errmsg(pconf->sql->sqlh));
#endif
}
sqlite3_reset(stmt);
return status;
}
#endif
static int
webdav_prop_move_uri (const plugin_config * const pconf,
const buffer * const src,
const buffer * const dst)
{
#ifdef USE_PROPPATCH
if (!pconf->sql)
return 0;
sqlite3_stmt * const stmt = pconf->sql->stmt_props_move;
if (!stmt)
return 0;
sqlite3_bind_text(stmt, 1, BUF_PTR_LEN(dst), SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, BUF_PTR_LEN(src), SQLITE_STATIC);
if (SQLITE_DONE != sqlite3_step(stmt)) {
#if 0
fprintf(stderr, "%s: %s\n", __func__, sqlite3_errmsg(pconf->sql->sqlh));
log_error(pconf->errh, __FILE__, __LINE__,
"%s: %s", __func__, sqlite3_errmsg(pconf->sql->sqlh));
#endif
}
sqlite3_reset(stmt);
#else
UNUSED(pconf);
UNUSED(src);
UNUSED(dst);
#endif
return 0;
}
static int
webdav_prop_move_uri_col (const plugin_config * const pconf,
const buffer * const src,
const buffer * const dst)
{
#ifdef USE_PROPPATCH
if (!pconf->sql)
return 0;
sqlite3_stmt * const stmt = pconf->sql->stmt_props_move_col;
if (!stmt)
return 0;
#ifdef __COVERITY__
force_assert(0 != src->used);
#endif
sqlite3_bind_text(stmt, 1, BUF_PTR_LEN(dst), SQLITE_STATIC);
sqlite3_bind_int( stmt, 2, (int)src->used);
sqlite3_bind_int( stmt, 3, (int)src->used-1);
sqlite3_bind_text(stmt, 4, BUF_PTR_LEN(src), SQLITE_STATIC);
if (SQLITE_DONE != sqlite3_step(stmt)) {
#if 0
fprintf(stderr, "%s: %s\n", __func__, sqlite3_errmsg(pconf->sql->sqlh));
log_error(pconf->errh, __FILE__, __LINE__,
"%s: %s", __func__, sqlite3_errmsg(pconf->sql->sqlh));
#endif
}
sqlite3_reset(stmt);
#else
UNUSED(pconf);
UNUSED(src);
UNUSED(dst);
#endif
return 0;
}
static int
webdav_prop_delete_uri (const plugin_config * const pconf,
const buffer * const uri)
{
#ifdef USE_PROPPATCH
if (!pconf->sql)
return 0;
sqlite3_stmt * const stmt = pconf->sql->stmt_props_delete;
if (!stmt)
return 0;
sqlite3_bind_text(stmt, 1, BUF_PTR_LEN(uri), SQLITE_STATIC);
if (SQLITE_DONE != sqlite3_step(stmt)) {
#if 0
fprintf(stderr, "%s: %s\n", __func__, sqlite3_errmsg(pconf->sql->sqlh));
log_error(pconf->errh, __FILE__, __LINE__,
"%s: %s", __func__, sqlite3_errmsg(pconf->sql->sqlh));
#endif
}
sqlite3_reset(stmt);
#else
UNUSED(pconf);
UNUSED(uri);
#endif
return 0;
}
static int
webdav_prop_copy_uri (const plugin_config * const pconf,
const buffer * const src,
const buffer * const dst)
{
#ifdef USE_PROPPATCH
if (!pconf->sql)
return 0;
sqlite3_stmt * const stmt = pconf->sql->stmt_props_copy;
if (!stmt)
return 0;
sqlite3_bind_text(stmt, 1, BUF_PTR_LEN(dst), SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, BUF_PTR_LEN(src), SQLITE_STATIC);
if (SQLITE_DONE != sqlite3_step(stmt)) {
#if 0
fprintf(stderr, "%s: %s\n", __func__, sqlite3_errmsg(pconf->sql->sqlh));
log_error(pconf->errh, __FILE__, __LINE__,
"%s: %s", __func__, sqlite3_errmsg(pconf->sql->sqlh));
#endif
}
sqlite3_reset(stmt);
#else
UNUSED(pconf);
UNUSED(dst);
UNUSED(src);
#endif
return 0;
}
#ifdef USE_PROPPATCH
static int
webdav_prop_delete (const plugin_config * const pconf,
const buffer * const uri,
const char * const prop_name,
const char * const prop_ns)
{
if (!pconf->sql)
return 0;
sqlite3_stmt * const stmt = pconf->sql->stmt_props_delete_prop;
if (!stmt)
return 0;
sqlite3_bind_text(stmt, 1, BUF_PTR_LEN(uri), SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, prop_name, strlen(prop_name), SQLITE_STATIC);
sqlite3_bind_text(stmt, 3, prop_ns, strlen(prop_ns), SQLITE_STATIC);
if (SQLITE_DONE != sqlite3_step(stmt)) {
#if 0
fprintf(stderr, "%s: %s\n", __func__, sqlite3_errmsg(pconf->sql->sqlh));
log_error(pconf->errh, __FILE__, __LINE__,
"%s: %s", __func__, sqlite3_errmsg(pconf->sql->sqlh));
#endif
}
sqlite3_reset(stmt);
return 0;
}
#endif
#ifdef USE_PROPPATCH
static int
webdav_prop_update (const plugin_config * const pconf,
const buffer * const uri,
const char * const prop_name,
const char * const prop_ns,
const char * const prop_value)
{
if (!pconf->sql)
return 0;
sqlite3_stmt * const stmt = pconf->sql->stmt_props_update_prop;
if (!stmt)
return 0;
sqlite3_bind_text(stmt, 1, BUF_PTR_LEN(uri), SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, prop_name, strlen(prop_name), SQLITE_STATIC);
sqlite3_bind_text(stmt, 3, prop_ns, strlen(prop_ns), SQLITE_STATIC);
sqlite3_bind_text(stmt, 4, prop_value, strlen(prop_value), SQLITE_STATIC);
if (SQLITE_DONE != sqlite3_step(stmt)) {
#if 0
fprintf(stderr, "%s: %s\n", __func__, sqlite3_errmsg(pconf->sql->sqlh));
log_error(pconf->errh, __FILE__, __LINE__,
"%s: %s", __func__, sqlite3_errmsg(pconf->sql->sqlh));
#endif
}
sqlite3_reset(stmt);
return 0;
}
#endif
static int
webdav_prop_select_prop (const plugin_config * const pconf,
const buffer * const uri,
const webdav_property_name * const prop,
buffer * const b)
{
#ifdef USE_PROPPATCH
if (!pconf->sql)
return -1;
sqlite3_stmt * const stmt = pconf->sql->stmt_props_select_prop;
if (!stmt)
return -1; /* not found */
sqlite3_bind_text(stmt, 1, BUF_PTR_LEN(uri), SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, prop->name, prop->namelen, SQLITE_STATIC);
sqlite3_bind_text(stmt, 3, prop->ns, prop->nslen, SQLITE_STATIC);
if (SQLITE_ROW == sqlite3_step(stmt)) {
webdav_xml_prop(b, prop, (char *)sqlite3_column_text(stmt, 0),
(uint32_t)sqlite3_column_bytes(stmt, 0));
sqlite3_reset(stmt);
return 0; /* found */
}
sqlite3_reset(stmt);
#else
UNUSED(pconf);
UNUSED(uri);
UNUSED(prop);
UNUSED(b);
#endif
return -1; /* not found */
}
static void
webdav_prop_select_props (const plugin_config * const pconf,
const buffer * const uri,
buffer * const b)
{
#ifdef USE_PROPPATCH
if (!pconf->sql)
return;
sqlite3_stmt * const stmt = pconf->sql->stmt_props_select_props;
if (!stmt)
return;
sqlite3_bind_text(stmt, 1, BUF_PTR_LEN(uri), SQLITE_STATIC);
while (SQLITE_ROW == sqlite3_step(stmt)) {
webdav_property_name prop;
prop.ns = (char *)sqlite3_column_text(stmt, 1);
prop.name = (char *)sqlite3_column_text(stmt, 0);
prop.nslen = (uint32_t)sqlite3_column_bytes(stmt, 1);
prop.namelen = (uint32_t)sqlite3_column_bytes(stmt, 0);
webdav_xml_prop(b, &prop, (char *)sqlite3_column_text(stmt, 2),
(uint32_t)sqlite3_column_bytes(stmt, 2));
}
sqlite3_reset(stmt);
#else
UNUSED(pconf);
UNUSED(uri);
UNUSED(b);
#endif
}
static int
webdav_prop_select_propnames (const plugin_config * const pconf,
const buffer * const uri,
buffer * const b)
{
#ifdef USE_PROPPATCH
if (!pconf->sql)
return 0;
sqlite3_stmt * const stmt = pconf->sql->stmt_props_select_propnames;
if (!stmt)
return 0;
/* get all property names (EMPTY) */
sqlite3_bind_text(stmt, 1, BUF_PTR_LEN(uri), SQLITE_STATIC);
while (SQLITE_ROW == sqlite3_step(stmt)) {
webdav_property_name prop;
prop.ns = (char *)sqlite3_column_text(stmt, 1);
prop.name = (char *)sqlite3_column_text(stmt, 0);
prop.nslen = (uint32_t)sqlite3_column_bytes(stmt, 1);
prop.namelen = (uint32_t)sqlite3_column_bytes(stmt, 0);
webdav_xml_prop(b, &prop, NULL, 0);
}
sqlite3_reset(stmt);
#else
UNUSED(pconf);
UNUSED(uri);
UNUSED(b);
#endif
return 0;
}
#if defined(__APPLE__) && defined(__MACH__)
#if __ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ >= 101200
#include <sys/attr.h>
#include <sys/clonefile.h>/* clonefile() *//* OS X 10.12+ */
#endif
#if __ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ >= 1050
#include <copyfile.h> /* fcopyfile() *//* OS X 10.5+ */
#endif
#endif
#ifdef HAVE_ELFTC_COPYFILE/* __FreeBSD__ */
#include <libelftc.h> /* elftc_copyfile() */
#endif
#ifdef __linux__
#include <sys/sendfile.h> /* sendfile() */
#endif
/* file copy (blocking)
* fds should point to regular files (S_ISREG()) (not dir, symlink, or other)
* fds should not have O_NONBLOCK flag set
* (unless O_NONBLOCK not relevant for files on a given operating system)
* isz should be size of input file, and is a param to avoid extra fstat()
* since size is needed for Linux sendfile(), as well as posix_fadvise().
* caller should handle fchmod() and copying extended attribute, if desired
*/
__attribute_noinline__
static int
webdav_fcopyfile_sz (int ifd, int ofd, off_t isz)
{
if (0 == isz)
return 0;
/* Note: copy acceleration does not handle if ifd is extended during copy
* (file should not be modified during copy with proper WebDAV locking) */
#ifndef _WIN32
/*(file descriptors to *regular files* on most OS ignore O_NONBLOCK)*/
/*fcntl(ifd, F_SETFL, fcntl(ifd, F_GETFL, 0) & ~O_NONBLOCK);*/
/*fcntl(ofd, F_SETFL, fcntl(ofd, F_GETFL, 0) & ~O_NONBLOCK);*/
#endif
#if defined(__APPLE__) && defined(__MACH__)
#if __ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ >= 1050
if (0 == fcopyfile(ifd, ofd, NULL, COPYFILE_ALL))
return 0;
if (0 != lseek(ifd, 0, SEEK_SET)) return -1;
if (0 != lseek(ofd, 0, SEEK_SET)) return -1;
#endif
#endif
#ifdef HAVE_ELFTC_COPYFILE /* __FreeBSD__ */
if (0 == elftc_copyfile(ifd, ofd))
return 0;
if (0 != lseek(ifd, 0, SEEK_SET)) return -1;
if (0 != lseek(ofd, 0, SEEK_SET)) return -1;
#endif
#ifdef __linux__ /* Linux 2.6.33+ sendfile() supports file-to-file copy */
#if defined HAVE_SYS_SENDFILE_H && defined HAVE_SENDFILE \
&& defined(__linux__) && !defined HAVE_SENDFILE_BROKEN
off_t offset = 0;
#if defined(_LP64) || defined(__LP64__) || defined(_WIN64)
while (offset < isz && sendfile(ifd,ofd,&offset,(size_t)(isz-offset)) >= 0);
#else
while (offset < isz
&& sendfile(ifd, ofd, &offset,
(size_t)(isz-offset < INT32_MAX
? isz-offset
: (INT32_MAX & ~(131072-1)))) >= 0)
;
#endif
if (offset == isz)
return 0;
/*lseek(ifd, 0, SEEK_SET);*/ /*(ifd offset not modified due to &offset arg)*/
if (0 != lseek(ofd, 0, SEEK_SET)) return -1;
#endif
#endif
ssize_t rd, wr, off;
char buf[16384];
isz = 0;
do {
do {
rd = read(ifd, buf, sizeof(buf));
} while (-1 == rd && errno == EINTR);
if (rd <= 0) break;
off = 0;
do {
wr = write(ofd, buf+off, (size_t)(rd-off));
} while (wr >= 0 ? (off += wr) != rd : errno == EINTR);
if (wr < 0) return -1;
} while ((isz += rd)); /*(always true when reached w/ largefile support)*/
#if (defined(__APPLE__) && defined(__MACH__) \
&& __ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ >= 1050) \
|| defined(HAVE_ELFTC_COPYFILE) /* __FreeBSD__ */ \
|| (defined HAVE_SYS_SENDFILE_H && defined HAVE_SENDFILE \
&& defined(__linux__) && !defined HAVE_SENDFILE_BROKEN)
/*(file may have been truncated during prior copy acceleration attempt)*/
if (0 == rd)
return ftruncate(ofd, isz);
#endif
return (int)rd;
}
#ifdef USE_PROPPATCH
__attribute_cold__
__attribute_noinline__
static handler_t
webdav_405_no_db (request_st * const r)
{
http_header_response_set(r, HTTP_HEADER_ALLOW,
CONST_STR_LEN("Allow"),
CONST_STR_LEN("GET, HEAD, PROPFIND, DELETE, MKCOL, PUT, MOVE, COPY"));
http_status_set_error(r, 405); /* Method Not Allowed */
return HANDLER_FINISHED;
}
#endif
#ifdef USE_PROPPATCH
__attribute_pure__
static int
webdav_reqbody_type_xml (request_st * const r)
{
const buffer * const vb =
http_header_request_get(r, HTTP_HEADER_CONTENT_TYPE,
CONST_STR_LEN("Content-Type"));
if (!vb) return 0;
const char * const semi = strchr(vb->ptr, ';');
uint32_t len = semi ? (uint32_t)(semi - vb->ptr) : buffer_clen(vb);
return ((len==15 && 0==memcmp(vb->ptr, "application/xml", 15))
|| (len==8 && 0==memcmp(vb->ptr, "text/xml", 8)));
}
#endif
static int
webdav_if_match_or_unmodified_since (request_st * const r, struct stat *st)
{
const buffer *im = (0 != r->conf.etag_flags)
? http_header_request_get(r, HTTP_HEADER_IF_MATCH,
CONST_STR_LEN("If-Match"))
: NULL;
const buffer *inm = (0 != r->conf.etag_flags)
? http_header_request_get(r, HTTP_HEADER_IF_NONE_MATCH,
CONST_STR_LEN("If-None-Match"))
: NULL;
const buffer *ius =
http_header_request_get(r, HTTP_HEADER_IF_UNMODIFIED_SINCE,
CONST_STR_LEN("If-Unmodified-Since"));
if (NULL == im && NULL == inm && NULL == ius) return 0;
struct stat stp;
if (NULL == st)
st = (0 == lstat(r->physical.path.ptr, &stp)) ? &stp : NULL;
buffer * const etagb = r->tmp_buf;
buffer_clear(etagb);
if (NULL != st && (NULL != im || NULL != inm)) {
http_etag_create(etagb, st, r->conf.etag_flags);
}
if (NULL != im) {
if (NULL == st || !http_etag_matches(etagb, im->ptr, 0))
return 412; /* Precondition Failed */
}
if (NULL != inm) {
if (NULL == st
? (errno != ENOENT && errno != ENOTDIR)
: http_etag_matches(etagb, inm->ptr, 1))
return 412; /* Precondition Failed */
}
if (NULL != ius) {
if (NULL == st)
return 412; /* Precondition Failed */
if (http_date_if_modified_since(BUF_PTR_LEN(ius), TIME64_CAST(st->st_mtime)))
return 412; /* Precondition Failed */
}
return 0;
}
static void
webdav_response_etag (request_st * const r, struct stat *st)
{
buffer *etagb = NULL;
if (0 != r->conf.etag_flags) {
etagb = http_header_response_set_ptr(r, HTTP_HEADER_ETAG,
CONST_STR_LEN("ETag"));
http_etag_create(etagb, st, r->conf.etag_flags);
}
stat_cache_update_entry(BUF_PTR_LEN(&r->physical.path), st, etagb);
}
static void
webdav_parent_modified (const buffer *path)
{
uint32_t dirlen = buffer_clen(path);
const char *fn = path->ptr;
#ifdef __COVERITY__
force_assert(0 != dirlen);
#endif
/*force_assert(fn[0] == '/');*/
if (fn[dirlen-1] == '/') --dirlen;
if (0 != dirlen) while (fn[--dirlen] != '/') ;
if (0 == dirlen) dirlen = 1; /* root dir ("/") */
stat_cache_invalidate_entry(fn, dirlen);
}
__attribute_pure__
static int
webdav_parse_Depth (const request_st * const r)
{
/* Depth = "Depth" ":" ("0" | "1" | "infinity") */
/* check first char only;
* ignore MS-WDVSE "noroot" extensions "1,noroot" and "infinity,noroot" */
const buffer * const h =
http_header_request_get(r, HTTP_HEADER_OTHER, CONST_STR_LEN("Depth"));
if (NULL != h) {
/* (leading LWS is removed during header parsing in request.c) */
switch (*h->ptr) {
case '0': return 0;
case '1': return 1;
/*case 'i':*/ /* e.g. "infinity" */
/*case 'I':*/ /* e.g. "Infinity" */
default: return -1;/* treat not-'0' and not-'1' as "infinity" */
}
}
return -1; /* default value is -1 to represent "infinity" */
}
#ifndef _ATFILE_SOURCE
#define webdav_unlinkat(pconf,dst,dfd,d_name) webdav_delete_file((pconf),(dst))
#else
static int
webdav_unlinkat (const plugin_config * const pconf,
const physical_st * const dst,
const int dfd, const char * const d_name)
{
if (0 == unlinkat(dfd, d_name, 0)) {
stat_cache_delete_entry(BUF_PTR_LEN(&dst->path));
return webdav_prop_delete_uri(pconf, &dst->rel_path);
}
switch(errno) {
case EACCES: case EPERM: return 403; /* Forbidden */
case ENOENT: return 404; /* Not Found */
default: return 501; /* Not Implemented */
}
}
#endif
static int
webdav_delete_file (const plugin_config * const pconf,
const physical_st * const dst)
{
if (0 == unlink(dst->path.ptr)) {
stat_cache_delete_entry(BUF_PTR_LEN(&dst->path));
return webdav_prop_delete_uri(pconf, &dst->rel_path);
}
switch(errno) {
case EACCES: case EPERM: return 403; /* Forbidden */
case ENOENT: return 404; /* Not Found */
default: return 501; /* Not Implemented */
}
}
static int
webdav_delete_dir (const plugin_config * const pconf,
physical_st * const dst,
request_st * const r,
const int flags)
{
int multi_status = 0;
#ifndef _ATFILE_SOURCE /*(not using fdopendir unless _ATFILE_SOURCE)*/
const int dfd = -1;
DIR * const dir = opendir(dst->path.ptr);
#else
const int dfd = fdevent_open_dirname(dst->path.ptr, 0);
DIR * const dir = (dfd >= 0) ? fdopendir(dfd) : NULL;
#endif
if (NULL == dir) {
if (dfd >= 0) close(dfd);
webdav_xml_response_status(r, &dst->rel_path, 403);
return 1;
}
/* dst is modified in place to extend path,
* so be sure to restore to base each loop iter */
const uint32_t dst_path_used = dst->path.used;
const uint32_t dst_rel_path_used = dst->rel_path.used;
int s_isdir = 0;
struct dirent *de;
while (NULL != (de = readdir(dir))) {
if (de->d_name[0] == '.'
&& (de->d_name[1] == '\0'
|| (de->d_name[1] == '.' && de->d_name[2] == '\0')))
continue; /* ignore "." and ".." */
#ifdef _DIRENT_HAVE_D_TYPE
if (de->d_type != DT_UNKNOWN)
s_isdir = (de->d_type == DT_DIR);
else
#endif
{
#ifdef _ATFILE_SOURCE
struct stat st;
if (0 != fstatat(dfd, de->d_name, &st, AT_SYMLINK_NOFOLLOW))
continue; /* file *just* disappeared? */
/* parent rmdir() will fail later if file still exists
* and fstatat() failed for other reasons */
s_isdir = S_ISDIR(st.st_mode);
#endif
}
const uint32_t len = (uint32_t) _D_EXACT_NAMLEN(de);
if (flags & WEBDAV_FLAG_LC_NAMES) /*(needed at least for rel_path)*/
webdav_str_len_to_lower(de->d_name, len);
buffer_append_string_len(&dst->path, de->d_name, len);
buffer_append_string_len(&dst->rel_path, de->d_name, len);
#ifndef _ATFILE_SOURCE
#ifdef _DIRENT_HAVE_D_TYPE
if (de->d_type == DT_UNKNOWN)
#endif
{
struct stat st;
if (0 != stat(dst->path.ptr, &st)) {
dst->path.ptr[ (dst->path.used = dst_path_used) -1]='\0';
dst->rel_path.ptr[(dst->rel_path.used = dst_rel_path_used)-1]='\0';
continue; /* file *just* disappeared? */
}
s_isdir = S_ISDIR(st.st_mode);
}
#endif
if (s_isdir) {
buffer_append_char(&dst->path, '/');
buffer_append_char(&dst->rel_path, '/');
multi_status |= webdav_delete_dir(pconf, dst, r, flags);
}
else {
int status =
webdav_unlinkat(pconf, dst, dfd, de->d_name);
if (0 != status) {
webdav_xml_response_status(r, &dst->rel_path, status);
multi_status = 1;
}
}
dst->path.ptr[ (dst->path.used = dst_path_used) -1] = '\0';
dst->rel_path.ptr[(dst->rel_path.used = dst_rel_path_used)-1] = '\0';
}
closedir(dir);
if (0 == multi_status) {
int rmdir_status;
if (0 == rmdir(dst->path.ptr))
rmdir_status = webdav_prop_delete_uri(pconf, &dst->rel_path);
else {
switch(errno) {
case EACCES:
case EPERM: rmdir_status = 403; break; /* Forbidden */
case ENOENT: rmdir_status = 404; break; /* Not Found */
default: rmdir_status = 501; break; /* Not Implemented */
}
}
if (0 != rmdir_status) {
webdav_xml_response_status(r, &dst->rel_path, rmdir_status);
multi_status = 1;
}
}
return multi_status;
}
#ifndef _ATFILE_SOURCE
#define webdav_linktmp_rename(pconf, src, dst) -1
#else
static int
webdav_linktmp_rename (const plugin_config * const pconf,
const buffer * const src,
const buffer * const dst)
{
buffer * const tmpb = pconf->tmpb;
int rc = -1; /*(not zero)*/
buffer_clear(tmpb);
buffer_append_str2(tmpb, BUF_PTR_LEN(dst),
CONST_STR_LEN("."));
buffer_append_int(tmpb, (long)getpid());
buffer_append_char(tmpb, '.');
buffer_append_uint_hex_lc(tmpb, (uintptr_t)pconf); /*(stack/heap addr)*/
buffer_append_char(tmpb, '~');
if (buffer_clen(tmpb) < PATH_MAX
&& 0 == linkat(AT_FDCWD, src->ptr, AT_FDCWD, tmpb->ptr, 0)) {
rc = rename(tmpb->ptr, dst->ptr);
/* unconditionally unlink() src if rename() succeeds, just in case
* dst previously existed and was already hard-linked to src. From
* 'man -s 2 rename':
* If oldpath and newpath are existing hard links referring to the
* same file, then rename() does nothing, and returns a success
* status.
* This introduces a small race condition between the rename() and
* unlink() should new file have been created at src in the middle,
* though unlikely if locks are used since locks have not yet been
* released. */
unlink(tmpb->ptr);
}
return rc;
}
#endif
static int
webdav_copytmp_rename (const plugin_config * const pconf,
const physical_st * const src,
const physical_st * const dst,
int * const flags)
{
#if defined(__APPLE__) && defined(__MACH__)
#if __ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ >= 101200 /* 10.12+ */
if (!(*flags & (WEBDAV_FLAG_COPY_XDEV
|WEBDAV_FLAG_MOVE_XDEV
|WEBDAV_FLAG_NO_CLONE)) && src != dst) {
if (0==clonefile(src->path.ptr,dst->path.ptr,CLONE_NOFOLLOW))
/* target did not exist; skip stat_cache_delete_entry() */
return 0; /* copied */
else {
switch (errno) {
case ENOTSUP:
*flags |= WEBDAV_FLAG_NO_CLONE;
break;
case EXDEV:
if (*flags & WEBDAV_FLAG_COPY_LINK) {
*flags &= ~WEBDAV_FLAG_COPY_LINK;
*flags |= WEBDAV_FLAG_COPY_XDEV;
}
break;
case EEXIST:
if (!(*flags & WEBDAV_FLAG_OVERWRITE))
return 412; /* Precondition Failed */
break;
default:
break;
}
}
}
#endif
#endif
buffer * const tmpb = pconf->tmpb;
buffer_clear(tmpb);
buffer_append_str2(tmpb, BUF_PTR_LEN(&dst->path),
CONST_STR_LEN("."));
buffer_append_int(tmpb, (long)getpid());
buffer_append_char(tmpb, '.');
buffer_append_uint_hex_lc(tmpb, (uintptr_t)pconf); /*(stack/heap addr)*/
buffer_append_char(tmpb, '~');
if (buffer_clen(tmpb) >= PATH_MAX)
return 414; /* URI Too Long */
do {
#if defined(__APPLE__) && defined(__MACH__)
#if __ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ >= 101200 /* 10.12+ */
if (!(*flags & (WEBDAV_FLAG_COPY_XDEV
|WEBDAV_FLAG_MOVE_XDEV
|WEBDAV_FLAG_NO_CLONE))) {
if (0 == clonefile(src->path.ptr, tmpb->ptr, CLONE_NOFOLLOW))
break; /* copied */
else {
switch (errno) {
case ENOTSUP:
*flags |= WEBDAV_FLAG_NO_CLONE;
break;
case EXDEV:
if (*flags & WEBDAV_FLAG_COPY_LINK) {
*flags &= ~WEBDAV_FLAG_COPY_LINK;
*flags |= WEBDAV_FLAG_COPY_XDEV;
}
break;
default:
break;
}
}
}
#endif
#endif
/* code does not currently support symlinks in webdav collections;
* disallow symlinks as target when opening src and dst */
struct stat st;
const int ifd = fdevent_open_cloexec(src->path.ptr, 0, O_RDONLY, 0);
if (ifd < 0)
return 403; /* Forbidden */
if (0 != fstat(ifd, &st) || !S_ISREG(st.st_mode)) {
close(ifd);
return 403; /* Forbidden */
}
#ifdef _WIN32
/* Windows is frequently incompatible with similar functions from other OS.
* https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-copyfile
* Symbolic link behavior—If the source file is a symbolic link,
* the actual file copied is the target of the symbolic link.
* If the destination file already exists and is a symbolic link,
* the target of the symbolic link is overwritten by the source file.
* Therefore, open and check src file above, and keep fd open during copy.
* (and pass flag to CopyFile() to fail if target exists)
* (assumes typical windows filesystem behavior where an opened file can not
* be replaced while it is held open. XXX: is this true?)
* Aside: WebDAV does not support symlinks, so there is already the
* assumption that the collection does not contain symlinks unless
* there is some alternate means to access the containing volume.
*/
if (CopyFile((LPTSTR)src->path.ptr, (LPTSTR)tmpb->ptr, TRUE)) {
close(ifd);
break; /* copied */
}
#endif
const int ofd = fdevent_open_cloexec(tmpb->ptr, 0,
O_WRONLY | O_CREAT | O_EXCL | O_TRUNC,
WEBDAV_FILE_MODE);
if (ofd < 0) {
close(ifd);
return 403; /* Forbidden */
}
/* perform *blocking* copy (not O_NONBLOCK);
* blocks server from doing any other work until after copy completes
* (should reach here only if unable to use link() and rename()
* due to copy/move crossing device boundaries within the workspace) */
int rc = 0;
do {
if (0 == st.st_size)
break; /* copied */
#ifdef HAVE_COPY_FILE_RANGE
if (!(*flags & WEBDAV_FLAG_NO_CLONE)) {
loff_t ioff = 0; /*(provide offset ptr so ifd offset not changed)*/
loff_t ooff = 0; /*(provide offset ptr so ofd offset not changed)*/
off_t ilen = st.st_size;
ssize_t wr;
do {
#if defined(_LP64) || defined(__LP64__) || defined(_WIN64)
wr = copy_file_range(ifd, &ioff, ofd, &ooff, (size_t)ilen, 0);
#else
wr = copy_file_range(ifd, &ioff, ofd, &ooff,
(size_t)(ilen < INT32_MAX
? ilen
: (INT32_MAX & ~(131072-1))),
0);
#endif
} while (wr > 0 && (ilen -= wr));
if (__builtin_expect( (0 == ilen), 1))
break; /* copied */
if (-1 == wr) {
rc = errno;
if (rc == ENOSPC)
break;
if (rc == EXDEV) {
/*(cross-filesystem copies introduced in Linux 5.3)
*(overload WEBDAV_FLAG_NO_CLONE to indicate
* no cross-filesystem copy_file_range() support) */
*flags |= WEBDAV_FLAG_NO_CLONE;
if (*flags & WEBDAV_FLAG_COPY_LINK) {
*flags &= ~WEBDAV_FLAG_COPY_LINK;
*flags |= WEBDAV_FLAG_COPY_XDEV;
}
}
}
/*(ifd truncated if (0 == wr && ilen != 0))*/
if (0 != ooff && 0 != ftruncate(ofd, 0)) {
if (0 == rc) rc = errno;
break;
}
/* fallback, retry if copy_file_range() did not finish */
}
#elif defined(FICLONE) /* defined(__linux__) */
/*(redundant if copy_file_range() available)*/
if (!(*flags & (WEBDAV_FLAG_COPY_XDEV
|WEBDAV_FLAG_MOVE_XDEV
|WEBDAV_FLAG_NO_CLONE))) {
rc = ioctl(ofd, FICLONE, ifd);
if (__builtin_expect( (0 == rc), 1))
break; /* copied */
/*(reached if filesystem does not support reflinks or fds not on
* same mounted filesystem. If this code is reached, link() was
* not used, e.g. due to enabling "deprecated-unsafe-partial-put")*/
if (errno == EXDEV) {
if (*flags & WEBDAV_FLAG_COPY_LINK) {
*flags &= ~WEBDAV_FLAG_COPY_LINK;
*flags |= WEBDAV_FLAG_COPY_XDEV;
}
}
else
*flags |= WEBDAV_FLAG_NO_CLONE;
}
#endif
rc = webdav_fcopyfile_sz(ifd, ofd, st.st_size);
if (__builtin_expect( (0 != rc), 0))
rc = errno;
} while (0);
close(ifd);
if (src == dst && 0 == rc) {
/*(note: special-case (src == dst) to copy into tempfile w/o rename,
* expecting input flags = 0, returning open tmpfile fd in *flags
* (or -1 if not opened), and returning tmpfile name in pconf->tmpb) */
*flags = ofd;
return 0;
}
const int wc = close(ofd);
if (__builtin_expect( (0 != wc), 0) && 0 == rc)
rc = errno;
if (__builtin_expect( (0 != rc), 0)) {
/* error reading or writing files */
rc = (rc == ENOSPC) ? 507 : 403;
unlink(tmpb->ptr);
return rc;
}
} while (0);
if (src == dst) {
/*(note: special-case (src == dst) to copy into tempfile w/o rename,
* expecting input flags = 0, returning open tmpfile fd in *flags
* (or -1 if not opened), and returning tmpfile name in pconf->tmpb) */
*flags = -1;
return 0;
}
const int overwrite = (*flags & WEBDAV_FLAG_OVERWRITE);
#ifndef HAVE_RENAMEAT2
if (!overwrite) {
struct stat stb;
if (0 == lstat(dst->path.ptr, &stb) || errno != ENOENT) {
unlink(tmpb->ptr);
return 412; /* Precondition Failed */
}
/* TOC-TOU race between lstat() and rename(),
* but this is reasonable attempt to not overwrite existing entity */
}
if (0 == rename(tmpb->ptr, dst->path.ptr))
#else
if (0 == renameat2(AT_FDCWD, tmpb->ptr,
AT_FDCWD, dst->path.ptr,
overwrite ? 0 : RENAME_NOREPLACE))
#endif
{
/* unconditional stat cache deletion
* (not worth extra syscall/race to detect overwritten or not) */
stat_cache_delete_entry(BUF_PTR_LEN(&dst->path));
return 0;
}
else {
const int errnum = errno;
unlink(tmpb->ptr);
switch (errnum) {
case ENOENT:
case ENOTDIR:
case EISDIR: return 409; /* Conflict */
case EEXIST: return 412; /* Precondition Failed */
default: return 403; /* Forbidden */
}
}
}
static int
webdav_copymove_file (const plugin_config * const pconf,
const physical_st * const src,
const physical_st * const dst,
int * const flags)
{
const int overwrite = (*flags & WEBDAV_FLAG_OVERWRITE);
if (*flags & WEBDAV_FLAG_MOVE_RENAME) {
#ifndef HAVE_RENAMEAT2
if (!overwrite) {
struct stat st;
if (0 == lstat(dst->path.ptr, &st) || errno != ENOENT)
return 412; /* Precondition Failed */
/* TOC-TOU race between lstat() and rename(),
* but this is reasonable attempt to not overwrite existing entity*/
}
if (0 == rename(src->path.ptr, dst->path.ptr))
#else
if (0 == renameat2(AT_FDCWD, src->path.ptr,
AT_FDCWD, dst->path.ptr,
overwrite ? 0 : RENAME_NOREPLACE))
#endif
{
/* unconditionally unlink() src if rename() succeeds, just in case
* dst previously existed and was already hard-linked to src. From
* 'man -s 2 rename':
* If oldpath and newpath are existing hard links referring to the
* same file, then rename() does nothing, and returns a success
* status.
* This introduces a small race condition between the rename() and
* unlink() should new file have been created at src in the middle,
* though unlikely if locks are used since locks have not yet been
* released. */
if (overwrite) unlink(src->path.ptr);
/* unconditional stat cache deletion
* (not worth extra syscall/race to detect overwritten or not) */
stat_cache_delete_entry(BUF_PTR_LEN(&dst->path));
stat_cache_delete_entry(BUF_PTR_LEN(&src->path));
webdav_prop_move_uri(pconf, &src->rel_path, &dst->rel_path);
return 0;
}
else if (errno == EEXIST)
return 412; /* Precondition Failed */
}
else if (*flags & WEBDAV_FLAG_COPY_LINK) {
if (0 == linkat(AT_FDCWD, src->path.ptr, AT_FDCWD, dst->path.ptr, 0)){
webdav_prop_copy_uri(pconf, &src->rel_path, &dst->rel_path);
return 0;
}
else if (errno == EEXIST) {
if (!overwrite)
return 412; /* Precondition Failed */
if (0 == webdav_linktmp_rename(pconf, &src->path, &dst->path)) {
webdav_prop_copy_uri(pconf, &src->rel_path, &dst->rel_path);
return 0;
}
}
else if (errno == EXDEV) {
*flags &= ~WEBDAV_FLAG_COPY_LINK;
*flags |= WEBDAV_FLAG_COPY_XDEV;
}
}
/* link() or rename() failed; fall back to copy to tempfile and rename() */
int status = webdav_copytmp_rename(pconf, src, dst, flags);
if (0 == status) {
webdav_prop_copy_uri(pconf, &src->rel_path, &dst->rel_path);
if (*flags & (WEBDAV_FLAG_MOVE_RENAME|WEBDAV_FLAG_MOVE_XDEV))
webdav_delete_file(pconf, src);
/*(copy successful, but how should we report if delete fails?)*/
}
return status;
}
static int
webdav_mkdir (const plugin_config * const pconf,
const physical_st * const dst,
const int overwrite)
{
if (0 == mkdir(dst->path.ptr, WEBDAV_DIR_MODE)) {
webdav_parent_modified(&dst->path);
return 0;
}
switch (errno) {
case EEXIST:
case ENOTDIR: break;
case ENOENT: return 409; /* Conflict */
case EPERM:
default: return 403; /* Forbidden */
}
/* [RFC4918] 9.3.1 MKCOL Status Codes
* 405 (Method Not Allowed) -
* MKCOL can only be executed on an unmapped URL.
*/
if (overwrite < 0) /*(mod_webdav_mkcol() passes overwrite = -1)*/
return (errno != ENOTDIR)
? 405 /* Method Not Allowed */
: 409; /* Conflict */
#ifdef __COVERITY__
force_assert(2 <= dst->path.used);
force_assert(2 <= dst->rel_path.used);
#endif
struct stat st;
int status;
dst->path.ptr[dst->path.used-2] = '\0'; /*(trailing slash)*/
status = lstat(dst->path.ptr, &st);
dst->path.ptr[dst->path.used-2] = '/'; /*(restore slash)*/
if (0 != status) /* still ENOTDIR or *just* disappeared */
return 409; /* Conflict */
if (!overwrite) /* copying into a non-dir ? */
return 409; /* Conflict */
if (S_ISDIR(st.st_mode))
return 0;
dst->path.ptr[dst->path.used-2] = '\0'; /*(trailing slash)*/
dst->rel_path.ptr[dst->rel_path.used-2] = '\0';
status = webdav_delete_file(pconf, dst);
dst->path.ptr[dst->path.used-2] = '/'; /*(restore slash)*/
dst->rel_path.ptr[dst->rel_path.used-2] = '/';
if (0 != status)
return status;
webdav_parent_modified(&dst->path);
return (0 == mkdir(dst->path.ptr, WEBDAV_DIR_MODE))
? 0
: 409; /* Conflict */
}
static int
webdav_copymove_dir (const plugin_config * const pconf,
physical_st * const src,
physical_st * const dst,
request_st * const r,
int flags)
{
/* NOTE: merging collections is NON-CONFORMANT behavior
* (specified in [RFC4918])
*
* However, merging collections during COPY/MOVE might be expected behavior
* by client, as merging is the behavior of unix cp -r (recursive copy) as
* well as how Microsoft Windows Explorer performs folder copies.
*
* [RFC4918] 9.8.4 COPY and Overwriting Destination Resources
* When a collection is overwritten, the membership of the destination
* collection after the successful COPY request MUST be the same
* membership as the source collection immediately before the COPY. Thus,
* merging the membership of the source and destination collections
* together in the destination is not a compliant behavior.
* [Ed: strange how non-compliance statement is immediately followed by:]
* In general, if clients require the state of the destination URL to be
* wiped out prior to a COPY (e.g., to force live properties to be reset),
* then the client could send a DELETE to the destination before the COPY
* request to ensure this reset.
* [Ed: if non-compliant merge behavior is the default here, and were it to
* not be desired by client, client could send a DELETE to the destination
* before issuing COPY. There is no easy way to obtain merge behavior
* (were it not the non-compliant default here) unless the client recurses
* into the source and destination, and creates a list of objects that need
* to be copied. This could fail or miss files due to racing with other
* clients. All of this might forget to emphasize that wiping out an
* existing destination collection (a recursive operation) is dangerous and
* would happen if the client set Overwrite: T or omitted setting Overwrite
* since Overwrite: T is default (client must explicitly set Overwrite: F)]
* [RFC4918] 9.9.3 MOVE and the Overwrite Header
* If a resource exists at the destination and the Overwrite header is
* "T", then prior to performing the move, the server MUST perform a
* DELETE with "Depth: infinity" on the destination resource. If the
* Overwrite header is set to "F", then the operation will fail.
*/
/* NOTE: aborting if 507 Insufficient Storage is NON-CONFORMANT behavior
* [RFC4918] specifies that as much as possible of COPY or MOVE
* should be completed.
*/
/* ??? upon encountering errors, should src->rel_path or dst->rel_path
* be used in XML error ??? */
struct stat st;
int status;
int dfd;
int make_destdir = 1;
const int overwrite = (flags & WEBDAV_FLAG_OVERWRITE);
if (flags & WEBDAV_FLAG_MOVE_RENAME) {
#ifndef HAVE_RENAMEAT2
if (!overwrite) {
if (0 == lstat(dst->path.ptr, &st) || errno != ENOENT) {
webdav_xml_response_status(r, &src->rel_path, 412);
return 412; /* Precondition Failed */
}
/* TOC-TOU race between lstat() and rename(),
* but this is reasonable attempt to not overwrite existing entity*/
}
if (0 == rename(src->path.ptr, dst->path.ptr))
#else
if (0 == renameat2(AT_FDCWD, src->path.ptr,
AT_FDCWD, dst->path.ptr,
overwrite ? 0 : RENAME_NOREPLACE))
#endif
{
webdav_prop_move_uri_col(pconf, &src->rel_path, &dst->rel_path);
return 0;
}
else {
switch (errno) {
case EEXIST:
#if EEXIST != ENOTEMPTY
case ENOTEMPTY:
#endif
if (!overwrite) {
webdav_xml_response_status(r, &src->rel_path, 412);
return 412; /* Precondition Failed */
}
make_destdir = 0;
break;
case ENOTDIR:
if (!overwrite) {
webdav_xml_response_status(r, &src->rel_path, 409);
return 409; /* Conflict */
}
#ifdef __COVERITY__
force_assert(2 <= dst->path.used);
#endif
dst->path.ptr[dst->path.used-2] = '\0'; /*(trailing slash)*/
status = lstat(dst->path.ptr, &st);
dst->path.ptr[dst->path.used-2] = '/'; /*(restore slash)*/
if (0 == status) {
if (S_ISDIR(st.st_mode)) {
make_destdir = 0;
break;
}
#ifdef __COVERITY__
force_assert(2 <= dst->rel_path.used);
#endif
dst->path.ptr[dst->path.used-2] = '\0'; /*(remove slash)*/
dst->rel_path.ptr[dst->rel_path.used-2] = '\0';
status = webdav_delete_file(pconf, dst);
dst->path.ptr[dst->path.used-2] = '/'; /*(restore slash)*/
dst->rel_path.ptr[dst->rel_path.used-2] = '/';
if (0 != status) {
webdav_xml_response_status(r, &src->rel_path, status);
return status;
}
if (0 == rename(src->path.ptr, dst->path.ptr)) {
webdav_prop_move_uri_col(pconf, &src->rel_path,
&dst->rel_path);
return 0;
}
}
break;
case EXDEV:
flags &= ~WEBDAV_FLAG_MOVE_RENAME;
flags |= WEBDAV_FLAG_MOVE_XDEV;
/* (if overwrite, then could switch to WEBDAV_FLAG_COPY_XDEV
* and set a flag so that before returning from this routine,
* directory is deleted recursively, instead of deleting each
* file after each copy. Only reliable if overwrite is set
* since if it is not set, an error would leave file copies in
* two places and would be difficult to recover if !overwrite)
* (collections typically do not cross devices, so this is not
* expected to be a common case) */
break;
default:
break;
}
}
}
if (make_destdir) {
if (0 != (status = webdav_mkdir(pconf, dst, overwrite))) {
webdav_xml_response_status(r, &src->rel_path, status);
return status;
}
}
webdav_prop_copy_uri(pconf, &src->rel_path, &dst->rel_path);
/* copy from src to dst (and, if move, then delete src)
* src and dst are modified in place to extend path,
* so be sure to restore to base each loop iter */
const uint32_t src_path_used = src->path.used;
const uint32_t src_rel_path_used = src->rel_path.used;
const uint32_t dst_path_used = dst->path.used;
const uint32_t dst_rel_path_used = dst->rel_path.used;
#ifndef _ATFILE_SOURCE /*(not using fdopendir unless _ATFILE_SOURCE)*/
dfd = -1;
DIR * const srcdir = opendir(src->path.ptr);
#else
dfd = fdevent_open_dirname(src->path.ptr, 0);
DIR * const srcdir = (dfd >= 0) ? fdopendir(dfd) : NULL;
#endif
if (NULL == srcdir) {
if (dfd >= 0) close(dfd);
webdav_xml_response_status(r, &src->rel_path, 403);
return 403; /* Forbidden */
}
mode_t d_type = 0;
int multi_status = 0;
struct dirent *de;
while (NULL != (de = readdir(srcdir))) {
if (de->d_name[0] == '.'
&& (de->d_name[1] == '\0'
|| (de->d_name[1] == '.' && de->d_name[2] == '\0')))
continue; /* ignore "." and ".." */
#ifdef _DIRENT_HAVE_D_TYPE
if (de->d_type != DT_UNKNOWN)
d_type = DTTOIF(de->d_type);
else
#endif
{
#ifdef _ATFILE_SOURCE
if (0 != fstatat(dfd, de->d_name, &st, AT_SYMLINK_NOFOLLOW))
continue; /* file *just* disappeared? */
d_type = st.st_mode;
#endif
}
const uint32_t len = (uint32_t) _D_EXACT_NAMLEN(de);
if (flags & WEBDAV_FLAG_LC_NAMES) /*(needed at least for rel_path)*/
webdav_str_len_to_lower(de->d_name, len);
buffer_append_string_len(&src->path, de->d_name, len);
buffer_append_string_len(&dst->path, de->d_name, len);
buffer_append_string_len(&src->rel_path, de->d_name, len);
buffer_append_string_len(&dst->rel_path, de->d_name, len);
#ifndef _ATFILE_SOURCE
#ifdef _DIRENT_HAVE_D_TYPE
if (de->d_type == DT_UNKNOWN)
#endif
{
if (0 != stat(src->path.ptr, &st)) {
src->path.ptr[ (src->path.used = src_path_used) -1]='\0';
src->rel_path.ptr[(src->rel_path.used = src_rel_path_used)-1]='\0';
dst->path.ptr[ (dst->path.used = dst_path_used) -1]='\0';
dst->rel_path.ptr[(dst->rel_path.used = dst_rel_path_used)-1]='\0';
continue; /* file *just* disappeared? */
}
d_type = st.st_mode;
}
#endif
if (S_ISDIR(d_type)) { /* recursive call; depth first */
buffer_append_char(&src->path, '/');
buffer_append_char(&dst->path, '/');
buffer_append_char(&src->rel_path, '/');
buffer_append_char(&dst->rel_path, '/');
status = webdav_copymove_dir(pconf, src, dst, r, flags);
if (0 != status)
multi_status = 1;
}
else if (S_ISREG(d_type)) {
status = webdav_copymove_file(pconf, src, dst, &flags);
if (0 != status)
webdav_xml_response_status(r, &src->rel_path, status);
}
#if 0
else if (S_ISLNK(d_type)) {
/*(might entertain support in future, including readlink()
* and changing dst symlink to be relative to new location.
* (or, if absolute to the old location, then absolute to new)
* Be sure to hard-link using linkat() w/o AT_SYMLINK_FOLLOW)*/
}
#endif
else {
status = 0;
}
src->path.ptr[ (src->path.used = src_path_used) -1] = '\0';
src->rel_path.ptr[(src->rel_path.used = src_rel_path_used)-1] = '\0';
dst->path.ptr[ (dst->path.used = dst_path_used) -1] = '\0';
dst->rel_path.ptr[(dst->rel_path.used = dst_rel_path_used)-1] = '\0';
if (507 == status) {
multi_status = 507; /* Insufficient Storage */
break;
}
}
closedir(srcdir);
if (0 == multi_status) {
if (flags & (WEBDAV_FLAG_MOVE_RENAME|WEBDAV_FLAG_MOVE_XDEV)) {
status = webdav_delete_dir(pconf, src, r, flags); /* content */
if (0 != status) {
webdav_xml_response_status(r, &src->rel_path, status);
multi_status = 1;
}
}
}
return multi_status;
}
typedef struct webdav_propfind_bufs {
request_st * restrict r;
const plugin_config * restrict pconf;
physical_st * restrict dst;
buffer * restrict b;
buffer * restrict b_200;
buffer * restrict b_404;
webdav_property_names proplist;
int allprop;
int propname;
int lockdiscovery;
int depth;
int recursed;
int atflags;
struct stat st;
} webdav_propfind_bufs;
enum webdav_live_props_e {
WEBDAV_PROP_UNSET = -1 /* (enum value to avoid compiler warning)*/
,WEBDAV_PROP_ALL = 0 /* (ALL not really a prop; internal use) */
/*,WEBDAV_PROP_CREATIONDATE*/ /* (located in database, if present) */
/*,WEBDAV_PROP_DISPLAYNAME*/ /* (located in database, if present) */
/*,WEBDAV_PROP_GETCONTENTLANGUAGE*/ /* (located in database, if present) */
,WEBDAV_PROP_GETCONTENTLENGTH
,WEBDAV_PROP_GETCONTENTTYPE
,WEBDAV_PROP_GETETAG
,WEBDAV_PROP_GETLASTMODIFIED
/*,WEBDAV_PROP_LOCKDISCOVERY*/ /* (located in database, if present) */
,WEBDAV_PROP_RESOURCETYPE
/*,WEBDAV_PROP_SOURCE*/ /* not implemented; removed in RFC4918 */
,WEBDAV_PROP_SUPPORTEDLOCK
};
#ifdef USE_PROPPATCH
struct live_prop_list {
const char *prop;
const uint32_t len;
enum webdav_live_props_e pnum;
};
static const struct live_prop_list live_properties[] = { /*(namespace "DAV:")*/
/* { CONST_STR_LEN("creationdate"), WEBDAV_PROP_CREATIONDATE }*/
/*,{ CONST_STR_LEN("displayname"), WEBDAV_PROP_DISPLAYNAME }*/
/*,{ CONST_STR_LEN("getcontentlanguage"), WEBDAV_PROP_GETCONTENTLANGUAGE}*/
{ CONST_STR_LEN("getcontentlength"), WEBDAV_PROP_GETCONTENTLENGTH }
,{ CONST_STR_LEN("getcontenttype"), WEBDAV_PROP_GETCONTENTTYPE }
,{ CONST_STR_LEN("getetag"), WEBDAV_PROP_GETETAG }
,{ CONST_STR_LEN("getlastmodified"), WEBDAV_PROP_GETLASTMODIFIED }
#ifdef USE_LOCKS
/*,{ CONST_STR_LEN("lockdiscovery"), WEBDAV_PROP_LOCKDISCOVERY }*/
#endif
,{ CONST_STR_LEN("resourcetype"), WEBDAV_PROP_RESOURCETYPE }
/*,{ CONST_STR_LEN("source"), WEBDAV_PROP_SOURCE }*/
#ifdef USE_LOCKS
,{ CONST_STR_LEN("supportedlock"), WEBDAV_PROP_SUPPORTEDLOCK }
#endif
,{ NULL, 0, WEBDAV_PROP_UNSET }
};
/* protected live properties
* (must also protect creationdate and lockdiscovery in database) */
static const struct live_prop_list protected_props[] = { /*(namespace "DAV:")*/
{ CONST_STR_LEN("creationdate"), WEBDAV_PROP_UNSET
/*WEBDAV_PROP_CREATIONDATE*/ }
/*,{ CONST_STR_LEN("displayname"), WEBDAV_PROP_DISPLAYNAME }*/
/*,{ CONST_STR_LEN("getcontentlanguage"), WEBDAV_PROP_GETCONTENTLANGUAGE}*/
,{ CONST_STR_LEN("getcontentlength"), WEBDAV_PROP_GETCONTENTLENGTH }
,{ CONST_STR_LEN("getcontenttype"), WEBDAV_PROP_GETCONTENTTYPE }
,{ CONST_STR_LEN("getetag"), WEBDAV_PROP_GETETAG }
,{ CONST_STR_LEN("getlastmodified"), WEBDAV_PROP_GETLASTMODIFIED }
,{ CONST_STR_LEN("lockdiscovery"), WEBDAV_PROP_UNSET
/*WEBDAV_PROP_LOCKDISCOVERY*/ }
,{ CONST_STR_LEN("resourcetype"), WEBDAV_PROP_RESOURCETYPE }
/*,{ CONST_STR_LEN("source"), WEBDAV_PROP_SOURCE }*/
,{ CONST_STR_LEN("supportedlock"), WEBDAV_PROP_SUPPORTEDLOCK }
,{ NULL, 0, WEBDAV_PROP_UNSET }
};
#endif
static int
webdav_propfind_live_props (const webdav_propfind_bufs * const restrict pb,
const enum webdav_live_props_e pnum)
{
buffer * const restrict b = pb->b_200;
switch (pnum) {
case WEBDAV_PROP_ALL:
/*__attribute_fallthrough__*/
/*case WEBDAV_PROP_CREATIONDATE:*/ /* (located in database, if present)*/
#if 0
case WEBDAV_PROP_CREATIONDATE: {
/* st->st_ctim
* defined by POSIX.1-2008 as last file status change timestamp
* and is no long create-time (as it may have been on older filesystems)
* Therefore, this should return Not Found.
* [RFC4918] 15.1 creationdate Property
* The DAV:creationdate property SHOULD be defined on all DAV
* compliant resources. If present, it contains a timestamp of the
* moment when the resource was created. Servers that are incapable
* of persistently recording the creation date SHOULD instead leave
* it undefined (i.e. report "Not Found").
* (future: might store creationdate in database when PUT creates file
* or LOCK creates empty file, or MKCOL creates collection (dir),
* i.e. wherever the status is 201 Created)
*/
struct tm tm;
if (__builtin_expect( (NULL != gmtime64_r(&pb->st.st_ctime, &tm)), 1)) {
buffer_append_string_len(b, CONST_STR_LEN(
"<D:creationdate ns0:dt=\"dateTime.tz\">"));
#ifdef __MINGW32__
buffer_append_strftime(b, "%Y-%m-%dT%H:%M:%SZ", &tm));
#else
buffer_append_strftime(b, "%FT%TZ", &tm));
#endif
buffer_append_string_len(b, CONST_STR_LEN(
"</D:creationdate>"));
}
else if (pnum != WEBDAV_PROP_ALL)
return -1; /* invalid; report 'not found' */
if (pnum != WEBDAV_PROP_ALL) return 0;/* found *//*(else fall through)*/
__attribute_fallthrough__
}
#endif
/*case WEBDAV_PROP_DISPLAYNAME:*/ /* (located in database, if present)*/
/*case WEBDAV_PROP_GETCONTENTLANGUAGE:*/ /* (located in db, if present)*/
#if 0
case WEBDAV_PROP_GETCONTENTLANGUAGE:
/* [RFC4918] 15.3 getcontentlanguage Property
* SHOULD NOT be protected, so that clients can reset the language.
* [...]
* The DAV:getcontentlanguage property MUST be defined on any
* DAV-compliant resource that returns the Content-Language header on
* a GET.
* (future: server does not currently set Content-Language and this
* module would need to somehow find out if another module set it)
*/
buffer_append_string_len(b, CONST_STR_LEN(
"<D:getcontentlanguage>en</D:getcontentlanguage>"));
if (pnum != WEBDAV_PROP_ALL) return 0;/* found *//*(else fall through)*/
__attribute_fallthrough__
#endif
case WEBDAV_PROP_GETCONTENTLENGTH:
buffer_append_string_len(b, CONST_STR_LEN(
"<D:getcontentlength>"));
buffer_append_int(b, pb->st.st_size);
buffer_append_string_len(b, CONST_STR_LEN(
"</D:getcontentlength>"));
if (pnum != WEBDAV_PROP_ALL) return 0;/* found *//*(else fall through)*/
__attribute_fallthrough__
case WEBDAV_PROP_GETCONTENTTYPE:
/* [RFC4918] 15.5 getcontenttype Property
* Potentially protected if the server prefers to assign content types
* on its own (see also discussion in Section 9.7.1).
* (server currently assigns content types)
*
* [RFC4918] 15 DAV Properties
* For properties defined based on HTTP GET response headers
* (DAV:get*), the header value could include LWS as defined
* in [RFC2616], Section 4.2. Server implementors SHOULD strip
* LWS from these values before using as WebDAV property
* values.
* e.g. application/xml;charset=utf-8
* instead of: application/xml; charset="utf-8"
* (documentation-only; no check is done here to remove LWS)
*/
if (S_ISDIR(pb->st.st_mode)) {
buffer_append_string_len(b, CONST_STR_LEN(
"<D:getcontenttype>httpd/unix-directory</D:getcontenttype>"));
}
else {
/* provide content type by extension
* Note: not currently supporting filesystem xattr */
const array * const mtypes = pb->r->conf.mimetypes;
const buffer *ct =
stat_cache_mimetype_by_ext(mtypes, BUF_PTR_LEN(&pb->dst->path));
if (NULL != ct) {
buffer_append_str3(b,
CONST_STR_LEN(
"<D:getcontenttype>"),
BUF_PTR_LEN(ct),
CONST_STR_LEN(
"</D:getcontenttype>"));
}
else {
if (pnum != WEBDAV_PROP_ALL)
return -1; /* invalid; report 'not found' */
}
}
if (pnum != WEBDAV_PROP_ALL) return 0;/* found *//*(else fall through)*/
__attribute_fallthrough__
case WEBDAV_PROP_GETETAG:
if (0 != pb->r->conf.etag_flags) {
buffer * const etagb = pb->r->tmp_buf;
http_etag_create(etagb, &pb->st, pb->r->conf.etag_flags);
buffer_append_str3(b,
CONST_STR_LEN(
"<D:getetag>"),
BUF_PTR_LEN(etagb),
CONST_STR_LEN(
"</D:getetag>"));
}
else if (pnum != WEBDAV_PROP_ALL)
return -1; /* invalid; report 'not found' */
if (pnum != WEBDAV_PROP_ALL) return 0;/* found *//*(else fall through)*/
__attribute_fallthrough__
case WEBDAV_PROP_GETLASTMODIFIED:
buffer_append_string_len(b, CONST_STR_LEN(
"<D:getlastmodified ns0:dt=\"dateTime.rfc1123\">"));
http_date_time_append(b, pb->st.st_mtime);
buffer_append_string_len(b, CONST_STR_LEN(
"</D:getlastmodified>"));
if (pnum != WEBDAV_PROP_ALL) return 0;/* found *//*(else fall through)*/
__attribute_fallthrough__
#if 0
#ifdef USE_LOCKS
case WEBDAV_PROP_LOCKDISCOVERY:
/* database query for locks occurs in webdav_propfind_resource_props()*/
if (pnum != WEBDAV_PROP_ALL) return 0;/* found *//*(else fall through)*/
__attribute_fallthrough__
#endif
#endif
case WEBDAV_PROP_RESOURCETYPE:
if (S_ISDIR(pb->st.st_mode))
buffer_append_string_len(b, CONST_STR_LEN(
"<D:resourcetype><D:collection/></D:resourcetype>"));
else
buffer_append_string_len(b, CONST_STR_LEN(
"<D:resourcetype/>"));
if (pnum != WEBDAV_PROP_ALL) return 0;/* found *//*(else fall through)*/
__attribute_fallthrough__
/*case WEBDAV_PROP_SOURCE:*/ /* not impl; removed in RFC4918 */
#ifdef USE_LOCKS
case WEBDAV_PROP_SUPPORTEDLOCK:
buffer_append_string_len(b, CONST_STR_LEN(
"<D:supportedlock>"
"<D:lockentry>"
"<D:lockscope><D:exclusive/></D:lockscope>"
"<D:locktype><D:write/></D:locktype>"
"</D:lockentry>"
"<D:lockentry>"
"<D:lockscope><D:shared/></D:lockscope>"
"<D:locktype><D:write/></D:locktype>"
"</D:lockentry>"
"</D:supportedlock>"));
if (pnum != WEBDAV_PROP_ALL) return 0;/* found *//*(else fall through)*/
__attribute_fallthrough__
#endif
default: /* WEBDAV_PROP_UNSET */
if (pnum == WEBDAV_PROP_ALL) break;
return -1; /* not found */
}
return 0; /* found (WEBDAV_PROP_ALL) */
}
#ifdef USE_LOCKS
static void
webdav_propfind_lockdiscovery_cb (void * const vdata,
const webdav_lockdata * const lockdata)
{
webdav_xml_activelock((buffer *)vdata, lockdata, NULL, 0);
}
#endif
static void
webdav_propfind_resource_props (const webdav_propfind_bufs * const restrict pb)
{
const webdav_property_names * const props = &pb->proplist;
if (props->used) { /* "props" or "allprop"+"include" */
const webdav_property_name *prop = props->ptr;
for (int i = 0; i < props->used; ++i, ++prop) {
if (NULL == prop->name /*(flag indicating prop is live prop enum)*/
? 0 == webdav_propfind_live_props(pb, (enum webdav_live_props_e)
prop->namelen)
: 0 == webdav_prop_select_prop(pb->pconf, &pb->dst->rel_path,
prop, pb->b_200))
continue;
/*(error obtaining prop if reached)*/
if (prop->name)
webdav_xml_prop(pb->b_404, prop, NULL, 0);
else {
#ifdef USE_PROPPATCH
const struct live_prop_list *list = live_properties;
while (0 != list->len && (uint32_t)list->pnum != prop->namelen)
++list;
if (0 != list->len) { /*(list->pnum == prop->namelen)*/
webdav_property_name lprop =
{ prop->ns, list->prop, prop->nslen, list->len };
webdav_xml_prop(pb->b_404, &lprop, NULL, 0);
}
#endif
}
}
}
if (pb->allprop) {
webdav_propfind_live_props(pb, WEBDAV_PROP_ALL);
webdav_prop_select_props(pb->pconf, &pb->dst->rel_path, pb->b_200);
}
#ifdef USE_LOCKS
if (pb->lockdiscovery) {
/* pb->lockdiscovery > 0:
* report locks resource or containing (parent) collections
* pb->lockdiscovery < 0:
* report only those locks on specific resource
* While this is not compliant with RFC, it may reduces quite a bit of
* redundancy for propfind on Depth: 1 and Depth: infinity when there
* are locks on parent collections. The client receiving this propfind
* XML response should easily know that locks on collections apply to
* the members of those collections and to further nested collections
*
* future: might be many, many fewer database queries if make a single
* query for the locks in the collection directory tree and parse the
* results, rather than querying the database for each resource */
buffer_append_string_len(pb->b_200, CONST_STR_LEN(
"<D:lockdiscovery>"));
webdav_lock_activelocks(pb->pconf, &pb->dst->rel_path,
(pb->lockdiscovery > 0),
webdav_propfind_lockdiscovery_cb, pb->b_200);
buffer_append_string_len(pb->b_200, CONST_STR_LEN(
"</D:lockdiscovery>"));
}
#endif
}
static void
webdav_propfind_resource_propnames (const webdav_propfind_bufs *
const restrict pb)
{
static const char live_propnames[] =
"<getcontentlength/>\n"
"<getcontenttype/>\n"
"<getetag/>\n"
"<getlastmodified/>\n"
"<resourcetype/>\n"
#ifdef USE_LOCKS
"<supportedlock/>\n"
"<lockdiscovery/>\n"
#endif
;
/* list live_properties which are not in database, plus "lockdiscovery" */
buffer_append_string_len(pb->b_200,live_propnames,sizeof(live_propnames)-1);
/* list properties in database 'properties' table for resource */
webdav_prop_select_propnames(pb->pconf, &pb->dst->rel_path, pb->b_200);
}
__attribute_cold__
static void
webdav_propfind_resource_403 (const webdav_propfind_bufs * const restrict pb)
{
buffer * const restrict b = pb->b;
buffer_append_string_len(b, CONST_STR_LEN(
"<D:response>\n"));
webdav_xml_href(b, &pb->dst->rel_path);
buffer_append_string_len(b, CONST_STR_LEN(
"<D:propstat>\n"));
webdav_xml_status(b, 403); /* Forbidden */
buffer_append_string_len(b, CONST_STR_LEN(
"</D:propstat>\n"
"</D:response>\n"));
webdav_double_buffer(pb->r, b);
}
static void
webdav_propfind_resource (const webdav_propfind_bufs * const restrict pb)
{
buffer_clear(pb->b_200);
buffer_clear(pb->b_404);
if (!pb->propname)
webdav_propfind_resource_props(pb);
else
webdav_propfind_resource_propnames(pb);
/* buffer could get very large for large directory (or Depth: infinity)
* attempt to allocate in 8K chunks, rather than default realloc in
* 64-byte chunks (see buffer.h) which will lead to exponentially more
* expensive copy behavior as buffer is resized over and over and over
*
* future: avoid (potential) excessive memory usage by accumulating output
* in temporary file
*/
buffer * const restrict b = pb->b;
buffer * const restrict b_200 = pb->b_200;
buffer * const restrict b_404 = pb->b_404;
if (b->size - b->used < b_200->used + b_404->used + 1024) {
size_t sz = b->used + 8192-1 + b_200->used + b_404->used + 1024 - 1;
/*(optimization; buffer is extended as needed)*/
buffer_string_prepare_append(b, sz & (8192-1));
}
buffer_append_string_len(b, CONST_STR_LEN(
"<D:response>\n"));
webdav_xml_href(b, &pb->dst->rel_path);
if (!buffer_is_blank(b_200))
webdav_xml_propstat(b, b_200, 200);
if (!buffer_is_blank(b_404))
webdav_xml_propstat(b, b_404, 404);
buffer_append_string_len(b, CONST_STR_LEN(
"</D:response>\n"));
webdav_double_buffer(pb->r, b);
}
static void
webdav_propfind_dir (webdav_propfind_bufs * const restrict pb)
{
/* arbitrary recursion limit to prevent infinite loops,
* e.g. due to symlink loops, or excessive resource usage */
if (++pb->recursed > 100) return;
physical_st * const dst = pb->dst;
#ifndef _ATFILE_SOURCE /*(not using fdopendir unless _ATFILE_SOURCE)*/
const int dfd = -1;
DIR * const dir = opendir(dst->path.ptr);
#else
const int dfd = fdevent_open_dirname(dst->path.ptr,
(pb->atflags != AT_SYMLINK_NOFOLLOW));
DIR * const dir = (dfd >= 0) ? fdopendir(dfd) : NULL;
#endif
if (NULL == dir) {
int errnum = errno;
if (dfd >= 0) close(dfd);
if (errnum != ENOENT)
webdav_propfind_resource_403(pb); /* Forbidden */
return;
}
webdav_propfind_resource(pb);
if (pb->lockdiscovery > 0)
pb->lockdiscovery = -pb->lockdiscovery; /*(check locks on node only)*/
/* dst is modified in place to extend path,
* so be sure to restore to base each loop iter */
const uint32_t dst_path_used = dst->path.used;
const uint32_t dst_rel_path_used = dst->rel_path.used;
const int flags =
(pb->r->conf.force_lowercase_filenames ? WEBDAV_FLAG_LC_NAMES : 0);
struct dirent *de;
while (NULL != (de = readdir(dir))) {
if (de->d_name[0] == '.'
&& (de->d_name[1] == '\0'
|| (de->d_name[1] == '.' && de->d_name[2] == '\0')))
continue; /* ignore "." and ".." */
#ifdef _ATFILE_SOURCE
if (0 != fstatat(dfd, de->d_name, &pb->st, pb->atflags))
continue; /* file *just* disappeared? */
#endif
const uint32_t len = (uint32_t) _D_EXACT_NAMLEN(de);
if (flags & WEBDAV_FLAG_LC_NAMES) /*(needed by rel_path)*/
webdav_str_len_to_lower(de->d_name, len);
buffer_append_string_len(&dst->path, de->d_name, len);
buffer_append_string_len(&dst->rel_path, de->d_name, len);
#ifndef _ATFILE_SOURCE
if (0 != stat(dst->path.ptr, &pb->st)) {
dst->path.ptr[ (dst->path.used = dst_path_used) -1]='\0';
dst->rel_path.ptr[(dst->rel_path.used = dst_rel_path_used)-1]='\0';
continue; /* file *just* disappeared? */
}
#endif
if (S_ISDIR(pb->st.st_mode)) {
buffer_append_char(&dst->path, '/');
buffer_append_char(&dst->rel_path, '/');
}
if (S_ISDIR(pb->st.st_mode) && -1 == pb->depth)
webdav_propfind_dir(pb); /* recurse */
else
webdav_propfind_resource(pb);
dst->path.ptr[ (dst->path.used = dst_path_used) -1] = '\0';
dst->rel_path.ptr[(dst->rel_path.used = dst_rel_path_used)-1] = '\0';
}
closedir(dir);
}
#if defined(USE_PROPPATCH) || defined(USE_LOCKS)
static char *
webdav_mmap_file_chunk (chunk * const c, log_error_st * const errh)
{
#ifdef HAVE_MMAP
/*(request body provided in temporary file, so ok to mmap().
* Otherwise, must access through sys_setjmp_eval3()) */
/*assert(c->type == FILE_CHUNK);*/
const off_t len = c->file.length - c->offset;
const chunk_file_view * const restrict cfv =
chunkqueue_chunk_file_view(c, len, errh);
return (cfv && chunk_file_view_dlen(cfv, c->offset) >= len)
? chunk_file_view_dptr(cfv, c->offset)
: NULL;
#else
UNUSED(c);
UNUSED(errh);
return NULL;
#endif
}
__attribute_noinline__
static xmlDoc *
webdav_parse_chunkqueue (request_st * const r,
const plugin_config * const pconf)
{
/* parse the XML document */
xmlParserCtxtPtr ctxt = xmlCreatePushParserCtxt(NULL, NULL, NULL, 0, NULL);
/* XXX: evaluate adding more xmlParserOptions */
xmlCtxtUseOptions(ctxt, XML_PARSE_NOERROR | XML_PARSE_NOWARNING
| XML_PARSE_PEDANTIC| XML_PARSE_NONET);
char *xmlstr;
chunkqueue * const cq = &r->reqbody_queue;
size_t weWant = chunkqueue_length(cq);
int err = XML_ERR_OK;
while (weWant) {
size_t weHave = 0;
chunk *c = cq->first;
char buf[16384];
#ifdef __COVERITY__
force_assert(0 == weWant || c != NULL);
#endif
if (c->type == MEM_CHUNK) {
xmlstr = c->mem->ptr + c->offset;
weHave = buffer_clen(c->mem) - c->offset;
}
else if (c->type == FILE_CHUNK) {
xmlstr = webdav_mmap_file_chunk(c, r->conf.errh);
if (NULL != xmlstr) {
weHave = c->file.length - c->offset;
}
else {
char *data = buf;
uint32_t dlen = sizeof(buf);
if (0 == chunkqueue_peek_data(cq,&data,&dlen,r->conf.errh,0)) {
xmlstr = data;
weHave = dlen;
}
else {
err = XML_IO_UNKNOWN;
break;
}
}
}
else {
log_error(r->conf.errh, __FILE__, __LINE__,
"unrecognized chunk type: %d", c->type);
err = XML_IO_UNKNOWN;
break;
}
if (weHave > weWant) weHave = weWant;
if (pconf->log_xml)
log_error(r->conf.errh, __FILE__, __LINE__,
"XML-request-body: %.*s", (int)weHave, xmlstr);
if (XML_ERR_OK != (err = xmlParseChunk(ctxt, xmlstr, weHave, 0))) {
log_error(r->conf.errh, __FILE__, __LINE__,
"xmlParseChunk failed at: %lld %zu %d",
(long long int)cq->bytes_out, weHave, err);
break;
}
weWant -= weHave;
chunkqueue_mark_written(cq, weHave);
}
if (XML_ERR_OK == err) {
switch ((err = xmlParseChunk(ctxt, 0, 0, 1))) {
case XML_ERR_DOCUMENT_END:
case XML_ERR_OK:
if (ctxt->wellFormed) {
xmlDoc * const xml = ctxt->myDoc;
xmlFreeParserCtxt(ctxt);
return xml;
}
break;
default:
log_error(r->conf.errh, __FILE__, __LINE__,
"xmlParseChunk failed at final packet: %d", err);
break;
}
}
xmlFreeDoc(ctxt->myDoc);
xmlFreeParserCtxt(ctxt);
return NULL;
}
#endif
#ifdef USE_LOCKS
struct webdav_lock_token_submitted_st {
buffer *tokens;
int used;
const buffer *authn_user;
buffer *b;
request_st *r;
int nlocks;
int slocks;
int smatch;
};
static void
webdav_lock_token_submitted_cb (void * const vdata,
const webdav_lockdata * const lockdata)
{
/* RFE: improve support for shared locks
* (instead of treating match of any shared lock as sufficient,
* even when there are different lockroots)
* keep track of matched shared locks and unmatched shared locks and
* ensure that each lockroot with shared locks has at least one match
* (Will need to allocate strings for each URI with shared lock and keep
* track whether or not a shared lock has been matched for that URI.
* After walking all locks, must walk looking for unmatched URIs,
* and must free these strings) */
/* [RFC4918] 6.4 Lock Creator and Privileges
* When a locked resource is modified, a server MUST check that the
* authenticated principal matches the lock creator (in addition to
* checking for valid lock token submission).
*/
struct webdav_lock_token_submitted_st * const cbdata =
(struct webdav_lock_token_submitted_st *)vdata;
const buffer * const locktoken = &lockdata->locktoken;
const int shared = (lockdata->lockscope->used != sizeof("exclusive"));
++cbdata->nlocks;
if (shared) ++cbdata->slocks;
for (int i = 0; i < cbdata->used; ++i) {
const buffer * const token = &cbdata->tokens[i];
/* locktoken match (locktoken not '\0' terminated) */
if (buffer_eq_slen(token, BUF_PTR_LEN(locktoken))) {
/*(0 length owner if no auth required to lock; not recommended)*/
if (buffer_is_blank(lockdata->owner)/*no lock owner;match*/
|| buffer_eq_slen(cbdata->authn_user,
BUF_PTR_LEN(lockdata->owner))) {
if (shared) ++cbdata->smatch;
return; /* authenticated lock owner match */
}
}
}
/* no match with lock tokens in request */
if (!shared) {
webdav_xml_href(cbdata->b, &lockdata->lockroot);
webdav_double_buffer(cbdata->r, cbdata->b);
}
}
/**
* check if request provides necessary locks to access the resource
*/
static int
webdav_has_lock (request_st * const r,
const plugin_config * const pconf,
const buffer * const uri)
{
/* Note with regard to exclusive locks on collections: client should not be
* able to obtain an exclusive lock on a collection if there are existing
* locks on resource members inside the collection. Therefore, there is no
* need to check here for locks on resource members inside collections.
* (This ignores the possibility that an admin or some other privileged
* or out-of-band process has added locks in spite of lock on collection.)
* Revisit to properly support shared locks. */
struct webdav_lock_token_submitted_st cbdata;
cbdata.b = chunk_buffer_acquire();
cbdata.r = r;
cbdata.tokens = NULL;
cbdata.used = 0;
cbdata.nlocks = 0;
cbdata.slocks = 0;
cbdata.smatch = 0;
/* XXX: maybe add config switch to require that authentication occurred? */
buffer owner = { NULL, 0, 0 };/*owner (not authenticated)(auth_user unset)*/
const data_string * const authn_user = (const data_string *)
array_get_element_klen(&r->env, CONST_STR_LEN("REMOTE_USER"));
cbdata.authn_user = authn_user ? &authn_user->value : &owner;
const buffer * const h =
http_header_request_get(r, HTTP_HEADER_OTHER, CONST_STR_LEN("If"));
if (h) {
/* parse "If" request header for submitted lock tokens
* While the below not a pedantic, validating parse, if the header
* is non-conformant or contains unencoded characters, the result
* will be misidentified or ignored lock tokens, which will result
* in fail closed -- secure default behavior -- if those lock
* tokens are required. It is highly unlikely that misparsing the "If"
* request header will result in a valid lock token since lock tokens
* should be unique, and opaquelocktoken should be globally unique */
char *p = h->ptr;
do {
#if 0
while (*p == ' ' || *p == '\t') ++p;
if (*p == '<') { /* Resource-Tag */
do { ++p; } while (*p != '>' && *p != '\0');
if (*p == '\0') break;
do { ++p; } while (*p == ' ' || *p == '\t');
}
#endif
while (*p != '(' && *p != '\0') ++p;
/* begin List in No-tag-list or Tagged-list
* List = "(" 1*Condition ")"
* Condition = ["Not"] (State-token | "[" entity-tag "]")
*/
int notflag = 0;
while (*p != '\0' && *++p != ')') {
while (*p == ' ' || *p == '\t') ++p;
if ( (p[0] & 0xdf) == 'N'
&& (p[1] & 0xdf) == 'O'
&& (p[2] & 0xdf) == 'T') {
notflag = 1;
p += 3;
while (*p == ' ' || *p == '\t') ++p;
}
if (*p != '<') { /* '<' begins State-token (Coded-URL) */
if (*p != '[') break; /* invalid syntax */
/* '[' and ']' wrap entity-tag */
char *etag = p+1;
do { ++p; } while (*p != ']' && *p != '\0');
if (*p != ']') break; /* invalid syntax */
if (p == etag) continue; /* ignore entity-tag if empty */
if (notflag) continue;/* ignore entity-tag in NOT context */
if (0 == r->conf.etag_flags) continue; /*ignore entity-tag*/
struct stat st;
if (0 != lstat(r->physical.path.ptr, &st)) {
http_status_set_error(r, 412); /* Precondition Failed */
chunk_buffer_release(cbdata.b);
return 0;
}
if (S_ISDIR(st.st_mode)) continue;/*we ignore etag if dir*/
buffer * const etagb = r->tmp_buf;
http_etag_create(etagb, &st, r->conf.etag_flags);
*p = '\0';
int ematch = http_etag_matches(etagb, etag, 0);
*p = ']';
if (!ematch) {
http_status_set_error(r, 412); /* Precondition Failed */
chunk_buffer_release(cbdata.b);
return 0;
}
continue;
}
if (p[1] == 'D'
&& 0 == strncmp(p, "<DAV:no-lock>",
sizeof("<DAV:no-lock>")-1)) {
if (0 == notflag) {
http_status_set_error(r, 412); /* Precondition Failed */
chunk_buffer_release(cbdata.b);
return 0;
}
p += sizeof("<DAV:no-lock>")-2; /* point p to '>' */
continue;
}
/* State-token (Coded-URL)
* Coded-URL = "<" absolute-URI ">"
* ; No linear whitespace (LWS) allowed in Coded-URL
* ; absolute-URI defined in RFC 3986, Section 4.3
*/
if (!(cbdata.used & (16-1))) {
if (cbdata.used == 16) { /* arbitrary limit */
http_status_set_error(r, 400); /* Bad Request */
chunk_buffer_release(cbdata.b);
return 0;
}
ck_realloc_u32((void **)&cbdata.tokens, (uint32_t)cbdata.used,
16, sizeof(*cbdata.tokens));
}
cbdata.tokens[cbdata.used].ptr = p+1;
do { ++p; } while (*p != '>' && *p != '\0');
if (*p == '\0') break; /* (*p != '>') */
cbdata.tokens[cbdata.used].used =
(uint32_t)(p - cbdata.tokens[cbdata.used].ptr + 1);
++cbdata.used;
}
} while (*p++ == ')'); /* end of List in No-tag-list or Tagged-list */
}
webdav_lock_activelocks(pconf, uri, 1,
webdav_lock_token_submitted_cb, &cbdata);
if (NULL != cbdata.tokens)
free(cbdata.tokens);
int has_lock = 1;
if (0 != cbdata.b->used || !chunkqueue_is_empty(&r->write_queue))
has_lock = 0;
else if (0 == cbdata.nlocks) { /* resource is not locked at all */
/* error if lock provided on source and no locks present on source;
* not error if no locks on Destination, but "If" provided for source */
if (cbdata.used && uri == &r->physical.rel_path) {
has_lock = -1;
http_status_set_error(r, 412); /* Precondition Failed */
}
#if 0 /*(treat no locks as if caller is holding an appropriate lock)*/
else {
has_lock = 0;
webdav_xml_href(cbdata.b, uri);
}
#endif
}
/*(XXX: overly simplistic shared lock matching allows any match of shared
* locks even when there are shared locks on multiple different lockroots.
* Failure is misreported since unmatched shared locks are not added to
* cbdata.b) */
if (cbdata.slocks && !cbdata.smatch)
has_lock = 0;
if (!has_lock)
webdav_xml_doc_error_lock_token_submitted(r, cbdata.b);
chunk_buffer_release(cbdata.b);
return (has_lock > 0);
}
#else /* ! defined(USE_LOCKS) */
#define webdav_has_lock(r, pconf, uri) 1
#endif /* ! defined(USE_LOCKS) */
static handler_t
mod_webdav_propfind (request_st * const r, const plugin_config * const pconf)
{
if (r->reqbody_length) {
#ifdef USE_PROPPATCH
if (r->state == CON_STATE_READ_POST) {
handler_t rc = r->con->reqbody_read(r);
if (rc != HANDLER_GO_ON) return rc;
}
if (!webdav_reqbody_type_xml(r)) {
http_status_set_error(r, 415); /* Unsupported Media Type */
return HANDLER_FINISHED;
}
#else
/* PROPFIND is idempotent and safe, so even if parsing XML input is not
* supported, live properties can still be produced, so treat as allprop
* request. NOTE: this behavior is NOT RFC CONFORMANT (and, well, if
* compiled without XML support, this WebDAV implementation is already
* non-compliant since it is missing support for XML request body).
* RFC-compliant behavior would reject an ignored request body with
* 415 Unsupported Media Type */
#if 0
http_status_set_error(r, 415); /* Unsupported Media Type */
return HANDLER_FINISHED;
#endif
#endif
}
webdav_propfind_bufs pb;
/* [RFC4918] 9.1 PROPFIND Method
* Servers MUST support "0" and "1" depth requests on WebDAV-compliant
* resources and SHOULD support "infinity" requests. In practice, support
* for infinite-depth requests MAY be disabled, due to the performance and
* security concerns associated with this behavior. Servers SHOULD treat
* a request without a Depth header as if a "Depth: infinity" header was
* included.
*/
pb.allprop = 0;
pb.propname = 0;
pb.lockdiscovery= 0;
pb.recursed = 0;
pb.depth = webdav_parse_Depth(r);
if (-1 == pb.depth && !(pconf->opts & MOD_WEBDAV_PROPFIND_DEPTH_INFINITY)) {
webdav_xml_doc_error_propfind_finite_depth(r);
return HANDLER_FINISHED;
}
pb.atflags =
((pconf->opts & MOD_WEBDAV_UNSAFE_PROPFIND_FOLLOW_SYMLINK)
&& pconf->is_readonly)
? 0 /* non-standard */
: AT_SYMLINK_NOFOLLOW; /* WebDAV does not have symlink concept */
if (pb.atflags == AT_SYMLINK_NOFOLLOW
? 0 != lstat(r->physical.path.ptr, &pb.st)
: 0 != stat(r->physical.path.ptr, &pb.st)) { /* non-standard */
http_status_set_error(r, (errno == ENOENT) ? 404 : 403);
return HANDLER_FINISHED;
}
else if (S_ISDIR(pb.st.st_mode)) {
if (!buffer_has_pathsep_suffix(&r->physical.path)) {
/* set "Content-Location" instead of sending 308 redirect to dir */
if (0 != http_response_redirect_to_directory(r, 0))
return HANDLER_FINISHED;
buffer_append_char(&r->physical.path, '/');
buffer_append_char(&r->physical.rel_path, '/');
}
}
else if (buffer_has_pathsep_suffix(&r->physical.path)) {
http_status_set_error(r, 403);
return HANDLER_FINISHED;
}
else {
pb.depth = 0;
}
pb.proplist.ptr = NULL;
pb.proplist.used = 0;
#ifdef USE_PROPPATCH
xmlDocPtr xml = NULL;
const xmlNode *rootnode = NULL;
if (r->reqbody_length) {
if (NULL == (xml = webdav_parse_chunkqueue(r, pconf))) {
http_status_set_error(r, 400); /* Bad Request */
return HANDLER_FINISHED;
}
rootnode = xmlDocGetRootElement(xml);
}
if (NULL != rootnode
&& 0 == webdav_xmlstrcmp_fixed(rootnode->name, "propfind")) {
for (const xmlNode *cmd = rootnode->children; cmd; cmd = cmd->next) {
if (0 == webdav_xmlstrcmp_fixed(cmd->name, "allprop"))
pb.allprop = pb.lockdiscovery = 1;
else if (0 == webdav_xmlstrcmp_fixed(cmd->name, "propname"))
pb.propname = 1;
else if (0 != webdav_xmlstrcmp_fixed(cmd->name, "prop")
&& 0 != webdav_xmlstrcmp_fixed(cmd->name, "include"))
continue;
/* "prop" or "include": get prop by name */
for (const xmlNode *prop = cmd->children; prop; prop = prop->next) {
if (prop->type == XML_TEXT_NODE)
continue; /* ignore WS */
if (prop->ns && '\0' == *(char *)prop->ns->href
&& '\0' != *(char *)prop->ns->prefix) {
log_error(r->conf.errh, __FILE__, __LINE__,
"no name space for: %s", prop->name);
/* 422 Unprocessable Entity */
http_status_set_error(r, 422);
free(pb.proplist.ptr);
xmlFreeDoc(xml);
return HANDLER_FINISHED;
}
/* add property to requested list */
if (!(pb.proplist.used & (32-1))) {
if (pb.proplist.used == 32) {
/* arbitrarily chosen limit of 32 */
log_error(r->conf.errh, __FILE__, __LINE__,
"too many properties in request (> 32)");
http_status_set_error(r, 400); /* Bad Request */
free(pb.proplist.ptr);
xmlFreeDoc(xml);
return HANDLER_FINISHED;
}
ck_realloc_u32((void **)&pb.proplist.ptr,
(uint32_t)pb.proplist.used,
32, sizeof(*pb.proplist.ptr));
}
const size_t namelen = strlen((char *)prop->name);
if (prop->ns && 0 == strcmp((char *)prop->ns->href, "DAV:")) {
if (namelen == sizeof("lockdiscovery")-1
&& 0 == memcmp(prop->name,
CONST_STR_LEN("lockdiscovery"))) {
pb.lockdiscovery = 1;
continue;
}
const struct live_prop_list *list = live_properties;
while (0 != list->len
&& (list->len != namelen
|| 0 != memcmp(prop->name,list->prop,list->len)))
++list;
if (NULL != list->prop) {
if (cmd->name[0] == 'p') { /* "prop", not "include" */
pb.proplist.ptr[pb.proplist.used].ns = "";
pb.proplist.ptr[pb.proplist.used].nslen = 0;
pb.proplist.ptr[pb.proplist.used].name = NULL;
pb.proplist.ptr[pb.proplist.used].namelen =
list->pnum;
pb.proplist.used++;
} /* (else skip; will already be part of allprop) */
continue;
}
if (cmd->name[0] == 'i') /* allprop "include", not "prop" */
continue; /*(all props in db returned with allprop)*/
/* dead props or props in "DAV:" ns not handed above */
}
/* save pointers directly into parsed xmlDoc
* Therefore, MUST NOT call xmlFreeDoc(xml)
* until also done with pb.proplist */
webdav_property_name * const propname =
pb.proplist.ptr + pb.proplist.used++;
if (prop->ns) {
propname->ns = (char *)prop->ns->href;
propname->nslen = strlen(propname->ns);
}
else {
propname->ns = "";
propname->nslen = 0;
}
propname->name = (char *)prop->name;
propname->namelen = namelen;
}
}
}
#endif
if (NULL == pb.proplist.ptr && !pb.propname)
pb.allprop = pb.lockdiscovery = 1;
pb.r = r;
pb.pconf = pconf;
pb.dst = &r->physical;
pb.b = chunk_buffer_acquire();
pb.b_200 = chunk_buffer_acquire();
pb.b_404 = chunk_buffer_acquire();
/*(optimization; buf extended as needed)*/
chunk_buffer_prepare_append(pb.b, 8192);
webdav_xml_doctype(pb.b, r);
buffer_append_string_len(pb.b, CONST_STR_LEN(
"<D:multistatus xmlns:D=\"DAV:\" " MOD_WEBDAV_XMLNS_NS0 ">\n"));
if (0 != pb.depth) /*(must be collection or else error returned above)*/
webdav_propfind_dir(&pb);
else
webdav_propfind_resource(&pb);
buffer_append_string_len(pb.b, CONST_STR_LEN(
"</D:multistatus>\n"));
http_chunk_append_buffer(r, pb.b); /*(might move/steal/reset buffer)*/
chunk_buffer_release(pb.b);
http_status_set_fin(r, 207); /* Multi-status */
chunk_buffer_release(pb.b_404);
chunk_buffer_release(pb.b_200);
#ifdef USE_PROPPATCH
if (pb.proplist.ptr)
free(pb.proplist.ptr);
if (NULL != xml)
xmlFreeDoc(xml);
#endif
if (pconf->log_xml)
webdav_xml_log_response(r);
return HANDLER_FINISHED;
}
static handler_t
mod_webdav_mkcol (request_st * const r, const plugin_config * const pconf)
{
const int status = webdav_mkdir(pconf, &r->physical, -1);
if (0 == status)
http_status_set_fin(r, 201); /* Created */
else
http_status_set_error(r, status);
return HANDLER_FINISHED;
}
static handler_t
mod_webdav_delete (request_st * const r, const plugin_config * const pconf)
{
/* reject DELETE if original URI sent with fragment ('litmus' warning) */
if (NULL != strchr(r->target_orig.ptr, '#')) {
http_status_set_error(r, 403);
return HANDLER_FINISHED;
}
struct stat st;
if (-1 == lstat(r->physical.path.ptr, &st)) {
http_status_set_error(r, (errno == ENOENT) ? 404 : 403);
return HANDLER_FINISHED;
}
if (0 != webdav_if_match_or_unmodified_since(r, &st)) {
http_status_set_error(r, 412); /* Precondition Failed */
return HANDLER_FINISHED;
}
if (S_ISDIR(st.st_mode)) {
if (!buffer_has_pathsep_suffix(&r->physical.path)) {
#if 0 /*(issues warning for /usr/bin/litmus copymove test)*/
http_response_redirect_to_directory(r, 308);
return HANDLER_FINISHED; /* 308 Permanent Redirect */
/* Alternatively, could append '/' to r->physical.path
* and r->physical.rel_path, set Content-Location in
* response headers, and continue to serve the request */
#else
buffer_append_char(&r->physical.path, '/');
buffer_append_char(&r->physical.rel_path, '/');
#if 0 /*(Content-Location not very useful to client after DELETE)*/
/*(? should it be target or target_orig ?)*/
/*(should be url-encoded path)*/
buffer_append_char(&r->target, '/');
http_header_response_set(r, HTTP_HEADER_CONTENT_LOCATION,
CONST_STR_LEN("Content-Location"),
BUF_PTR_LEN(&r->target));
#endif
#endif
}
/* require "infinity" if Depth request header provided */
if (-1 != webdav_parse_Depth(r)) {
/* [RFC4918] 9.6.1 DELETE for Collections
* The DELETE method on a collection MUST act as if a
* "Depth: infinity" header was used on it. A client MUST NOT
* submit a Depth header with a DELETE on a collection with any
* value but infinity.
*/
http_status_set_error(r, 400); /* Bad Request */
return HANDLER_FINISHED;
}
const int flags = (r->conf.force_lowercase_filenames)
? WEBDAV_FLAG_LC_NAMES
: 0;
if (0 == webdav_delete_dir(pconf, &r->physical, r, flags)) {
/* Note: this does not destroy locks if an error occurs,
* which is not a problem if lock is only on the collection
* being moved, but might need finer updates if there are
* locks on internal elements that are successfully deleted */
webdav_lock_delete_uri_col(pconf, &r->physical.rel_path);
http_status_set_fin(r, 204); /* No Content */
}
else {
webdav_xml_doc_multistatus(r, pconf); /* 207 Multi-status */
}
/* invalidate stat cache of src if DELETE, whether or not successful */
stat_cache_delete_dir(BUF_PTR_LEN(&r->physical.path));
}
else if (buffer_has_pathsep_suffix(&r->physical.path))
http_status_set_error(r, 403);
else {
const int status = webdav_delete_file(pconf, &r->physical);
if (0 == status) {
webdav_lock_delete_uri(pconf, &r->physical.rel_path);
http_status_set_fin(r, 204); /* No Content */
}
else
http_status_set_error(r, status);
}
return HANDLER_FINISHED;
}
__attribute_noinline__
static int
mod_webdav_write_cq (request_st * const r, chunkqueue * const cq, const int fd)
{
/* (Note: copying might take some time, temporarily pausing server) */
while (!chunkqueue_is_empty(cq)) {
ssize_t wr = chunkqueue_write_chunk(fd, cq, r->conf.errh);
if (__builtin_expect( (wr > 0), 1))
chunkqueue_mark_written(cq, wr);
else if (wr < 0) {
http_status_set_error(r, (errno == ENOSPC) ? 507 : 403);
return 0;
}
else /*(wr == 0)*/
chunkqueue_remove_finished_chunks(cq);
}
return 1;
}
#if (defined(__linux__) || defined(__CYGWIN__)) && defined(O_TMPFILE)
static int
mod_webdav_write_single_file_chunk (request_st * const r, chunkqueue * const cq)
{
/* cq might have mem chunks after initial tempfile chunk
* due to chunkqueue_steal() if request body is small */
/*assert(cq->first->type == FILE_CHUNK);*/
/*assert(cq->first->next != NULL);*/
chunk * const c = cq->first;
cq->first = c->next;
const off_t len = chunkqueue_length(cq);
const off_t bytes_out = cq->bytes_out;
if (mod_webdav_write_cq(r, cq, c->file.fd)) {
/*assert(cq->first == NULL);*/
/* chunks merged; chunkqueue length did not change,
* so restore cq->bytes_out instead of chunkqueue_file_update() */
cq->bytes_out = bytes_out;
c->file.length = len;
c->next = NULL;
cq->first = cq->last = c;
return 1;
}
else {
/*assert(cq->first != NULL);*/
c->next = cq->first;
cq->first = c;
return 0;
}
}
#endif
static handler_t
mod_webdav_put_0 (request_st * const r, const plugin_config * const pconf)
{
if (0 != webdav_if_match_or_unmodified_since(r, NULL)) {
http_status_set_error(r, 412); /* Precondition Failed */
return HANDLER_FINISHED;
}
/* special-case PUT 0-length file */
int fd;
fd = fdevent_open_cloexec(r->physical.path.ptr, 0,
O_WRONLY | O_CREAT | O_EXCL | O_TRUNC,
WEBDAV_FILE_MODE);
if (fd >= 0) {
if (0 != r->conf.etag_flags) {
/*(skip sending etag if fstat() error; not expected)*/
struct stat st;
if (0 == fstat(fd, &st)) webdav_response_etag(r, &st);
}
close(fd);
webdav_parent_modified(&r->physical.path);
http_status_set_fin(r, 201); /* Created */
return HANDLER_FINISHED;
}
else if (errno == EISDIR) {
http_status_set_error(r, 405); /* Method Not Allowed */
return HANDLER_FINISHED;
}
if (errno == ELOOP)
webdav_delete_file(pconf, &r->physical); /*(ignore result)*/
/*(attempt unlink(); target might be symlink
* and above O_NOFOLLOW resulted in ELOOP)*/
fd = fdevent_open_cloexec(r->physical.path.ptr, 0,
O_WRONLY | O_CREAT | O_TRUNC,
WEBDAV_FILE_MODE);
if (fd >= 0) {
close(fd);
http_status_set_fin(r, 204); /* No Content */
return HANDLER_FINISHED;
}
/*if (errno == EACCES || errno == EPERM || errno == EROFS)*/
{
log_perror(r->conf.errh, __FILE__, __LINE__,
"open(%s, O_WRONLY|...)", r->physical.path.ptr);
#ifdef __linux__
log_error(r->conf.errh, __FILE__, __LINE__,
"check systemd lighttpd.service; e.g. ProtectHome=yes");
#endif
}
http_status_set_error(r, 500); /* Internal Server Error */
return HANDLER_FINISHED;
}
static handler_t
mod_webdav_put_prep (request_st * const r, const plugin_config * const pconf)
{
if (buffer_has_pathsep_suffix(&r->physical.path)) {
/* disallow PUT on a collection (path ends in '/') */
http_status_set_error(r, 400); /* Bad Request */
return HANDLER_FINISHED;
}
if (light_btst(r->rqst_htags, HTTP_HEADER_CONTENT_RANGE)) {
if (pconf->opts & (MOD_WEBDAV_UNSAFE_PARTIAL_PUT_COMPAT
|MOD_WEBDAV_CPYTMP_PARTIAL_PUT))
return HANDLER_GO_ON;
/* [RFC7231] 4.3.4 PUT
* An origin server that allows PUT on a given target resource MUST
* send a 400 (Bad Request) response to a PUT request that contains a
* Content-Range header field (Section 4.2 of [RFC7233]), since the
* payload is likely to be partial content that has been mistakenly
* PUT as a full representation.
*/
http_status_set_error(r, 400); /* Bad Request */
return HANDLER_FINISHED;
}
/* special-case PUT 0-length file */
if (0 == r->reqbody_length)
return mod_webdav_put_0(r, pconf);
/* Create temporary file in target directory (to store reqbody as received)
* Temporary file is unlinked so that if receiving reqbody fails,
* temp file is automatically cleaned up when fd is closed.
* While being received, temporary file is not part of directory listings.
* While this might result in extra copying, it is simple and robust. */
int fd;
size_t len = buffer_clen(&r->physical.path);
#if (defined(__linux__) || defined(__CYGWIN__)) && defined(O_TMPFILE)
char *slash = memrchr(r->physical.path.ptr, '/', len);
if (slash == r->physical.path.ptr) slash = NULL;
if (slash) *slash = '\0';
fd = fdevent_open_cloexec(r->physical.path.ptr, 1,
O_RDWR | O_TMPFILE | O_APPEND, WEBDAV_FILE_MODE);
if (slash) *slash = '/';
if (fd < 0)
#endif
{
buffer_append_string_len(&r->physical.path, CONST_STR_LEN("-XXXXXX"));
fd = fdevent_mkostemp(r->physical.path.ptr, 0);
if (fd >= 0) unlink(r->physical.path.ptr);
buffer_truncate(&r->physical.path, len);
}
if (fd < 0) {
switch (errno) {
case ENOENT: /* parent collection does not exist */
case ENOTDIR:
http_status_set_error(r, 409); /* Conflict */
break;
/*case EACCES: case EPERM: case EROFS:*/
default:
log_perror(r->conf.errh, __FILE__, __LINE__,
"open() tmpfile for %s", r->physical.path.ptr);
#ifdef __linux__
log_error(r->conf.errh, __FILE__, __LINE__,
"check systemd lighttpd.service; e.g. ProtectHome=yes");
#endif
http_status_set_error(r, 500); /* Internal Server Error */
break;
}
return HANDLER_FINISHED;
}
/* copy all chunks even though expecting (at most) single MEM_CHUNK chunk
* (still, loop on partial writes)
* (Note: copying might take some time, temporarily pausing server)
* (error status is set if error occurs) */
chunkqueue * const cq = &r->reqbody_queue;
off_t cqlen = chunkqueue_length(cq);
if (!mod_webdav_write_cq(r, cq, fd)) {
close(fd);
return HANDLER_FINISHED;
}
chunkqueue_reset(cq);
if (0 != cqlen) /*(r->physical.path copied, then c->mem cleared below)*/
chunkqueue_append_file_fd(cq, &r->physical.path, fd, 0, cqlen);
else {
/*(must be non-zero for fd to be appended, then reset to 0-length)*/
chunkqueue_append_file_fd(cq, &r->physical.path, fd, 0, 1);
cq->last->file.length = 0;
cq->bytes_in = 0;
}
#ifdef __COVERITY__
/* chunkqueue_append_file_fd() does not update cq->last when 0 == cqlen,
* and that is handled above, so cq->last is never NULL here */
force_assert(cq->last);
#endif
buffer_clear(cq->last->mem); /* file already unlink()ed */
cq->upload_temp_file_size = (off_t)((1uLL << (sizeof(off_t)*8-1))-1);
cq->last->file.is_temp = 1;
return HANDLER_GO_ON;
}
#if (defined(__linux__) || defined(__CYGWIN__)) && defined(O_TMPFILE)
static int
mod_webdav_put_linkat_rename (request_st * const r,
const char * const pathtemp)
{
if (!has_proc_self_fd) return 0;
chunkqueue * const cq = &r->reqbody_queue;
chunk *c = cq->first;
char pathproc[32] = "/proc/self/fd/";
size_t plen =
li_itostrn(pathproc+sizeof("/proc/self/fd/")-1,
sizeof(pathproc)-(sizeof("/proc/self/fd/")-1), c->file.fd);
pathproc[sizeof("/proc/self/fd/")-1+plen] = '\0';
if (0 == linkat(AT_FDCWD, pathproc, AT_FDCWD, pathtemp, AT_SYMLINK_FOLLOW)){
struct stat st;
#ifdef HAVE_RENAMEAT2
if (0 == renameat2(AT_FDCWD, pathtemp,
AT_FDCWD, r->physical.path.ptr, RENAME_NOREPLACE))
http_status_set_fin(r, 201); /* Created */
else if (0 == rename(pathtemp, r->physical.path.ptr))
http_status_set_fin(r, 204); /* No Content */ /*(replaced)*/
else
#else
http_status_set_fin(r, 0 == lstat(r->physical.path.ptr, &st)
? 204 /* No Content */
: 201); /* Created */
if (201 == http_status_get(r))
webdav_parent_modified(&r->physical.path);
if (0 != rename(pathtemp, r->physical.path.ptr))
#endif
{
if (errno == EISDIR)
http_status_set_error(r, 405); /* Method Not Allowed */
else
http_status_set_error(r, 403); /* Forbidden */
unlink(pathtemp);
}
if (0 != r->conf.etag_flags
&& http_status_get(r) < 300) { /*(201, 204)*/
/*(skip sending etag if fstat() error; not expected)*/
if (0 == fstat(c->file.fd, &st))
webdav_response_etag(r, &st);
}
chunkqueue_mark_written(cq, c->file.length); /*(c->offset == 0)*/
return 1;
}
return 0;
}
#endif
static handler_t
mod_webdav_put_range (request_st * const r, const buffer * const h,
const plugin_config * const pconf)
{
/* historical code performed very limited range parse (repeated here) */
/* we only support <num>- ... */
const char *num = h->ptr;
off_t offset;
char *err;
if (0 != strncmp(num, "bytes ", sizeof("bytes ")-1)) {
http_status_set_error(r, 501); /* Not Implemented */
return HANDLER_FINISHED;
}
num += sizeof("bytes ")-1; /* +6 for "bytes " */
offset = strtoll(num, &err, 10); /*(strtoll() ignores leading whitespace)*/
if (num == err || *err != '-' || offset < 0) {
http_status_set_error(r, 501); /* Not Implemented */
return HANDLER_FINISHED;
}
const int ifd = fdevent_open_cloexec(r->physical.path.ptr, 0,
O_WRONLY, WEBDAV_FILE_MODE);
if (ifd < 0) {
http_status_set_error(r, (errno == ENOENT) ? 404 : 403);
return HANDLER_FINISHED;
}
int fd = ifd;
struct stat st;
if ((pconf->opts & MOD_WEBDAV_CPYTMP_PARTIAL_PUT)
&& (!(pconf->opts & MOD_WEBDAV_UNSAFE_PARTIAL_PUT_COMPAT)
|| 0 != fstat(ifd,&st) || st.st_size != offset || st.st_nlink > 1)){
/* open tmpfile and copy source for modify and rename (below this block)
* if cpytmp mode enabled and if unsafe partial put compat not enabled
* or if not appending or if nlink == 1 (modifying in-place is safe when
* appending and nlink == 1 since lighttpd is single-threaded) */
/* future: might rework for reuse since src is already open here in ifd,
* and might be opened again (and closed!) in webdav_copytmp_rename() */
fd = 0; /*(overloaded as input 'flags' to webdav_copytmp_rename())*/
int rc = webdav_copytmp_rename(pconf, &r->physical, &r->physical, &fd);
/*(fd may now be open to temporary file whose name is in pconf->tmpb
* since we passed webdav_copytmp_rename() with (src == dst))*/
if (0 != rc) {
close(ifd);
http_status_set_error(r, rc);
return HANDLER_FINISHED;
}
if (-1 == fd) {
/* open tmp file; file clone in webdav_copytmp_rename() did not */
fd = fdevent_open_cloexec(pconf->tmpb->ptr, 0,
O_WRONLY, WEBDAV_FILE_MODE);
if (fd < 0) {
close(ifd);
unlink(pconf->tmpb->ptr);
http_status_set_error(r, 403);
return HANDLER_FINISHED;
}
}
close(ifd); /*(close ifd after opening temporary file so (fd != ifd))*/
}
#ifdef HAVE_COPY_FILE_RANGE
/* use Linux copy_file_range() if available
* (Linux 4.5, but glibc 2.27 provides a user-space emulation)
* fd_in and fd_out must be on same mount (handled in mod_webdav_put_prep())
* before Linux 5.3
* check that reqbody is contained in single tempfile and open fd (expected)
* (Note: copying might take some time, temporarily pausing server)
*/
chunkqueue * const cq = &r->reqbody_queue;
chunk *c = cq->first;
off_t cqlen = chunkqueue_length(cq);
if (c->type == FILE_CHUNK && NULL == c->next && c->file.fd >= 0) {
loff_t zoff = 0;
loff_t ooff = offset;
ssize_t wr;
do {
#if defined(_LP64) || defined(__LP64__) || defined(_WIN64)
wr = copy_file_range(c->file.fd,&zoff,fd,&ooff,(size_t)cqlen, 0);
#else
wr = copy_file_range(c->file.fd,&zoff,fd,&ooff,
(size_t)(cqlen < INT32_MAX
? cqlen
: (INT32_MAX & ~(131072-1))),
0);
#endif
} while (wr > 0 && (cqlen -= wr));
/*(ignore if c->file.fd truncated (wr == 0 && cqlen != 0); fail below)*/
}
off_t wrote = chunkqueue_length(cq) - cqlen;
offset += wrote;
chunkqueue_mark_written(cq, wrote);
if (0 != cqlen) /* fallback, retry if copy_file_range() did not finish */
#endif
{
if (-1 == lseek(fd, offset, SEEK_SET)) {
close(fd);
if (fd != ifd)
unlink(pconf->tmpb->ptr);
http_status_set_error(r, 500); /* Internal Server Error */
return HANDLER_FINISHED;
}
/* copy all chunks even though expecting single chunk
* (still, loop on partial writes)
* (Note: copying might take some time, temporarily pausing server)
* (error status is set if error occurs) */
mod_webdav_write_cq(r, &r->reqbody_queue, fd);
}
if (fd != ifd) {
#ifndef HAVE_RENAMEAT2
if (0 == rename(pconf->tmpb->ptr, r->physical.path.ptr))
#else
if (0 == renameat2(AT_FDCWD, pconf->tmpb->ptr,
AT_FDCWD, r->physical.path.ptr, 0))
#endif
{
/* unconditional stat cache deletion */
stat_cache_delete_entry(BUF_PTR_LEN(&r->physical.path));
}
else {
switch (errno) {
case ENOENT:
case ENOTDIR:
case EISDIR: http_status_set_error(r, 409); break; /* Conflict */
default: http_status_set_error(r, 403); break; /* Forbidden */
}
unlink(pconf->tmpb->ptr);
}
}
if (0 != r->conf.etag_flags && !http_status_is_set(r)) {
/*(skip sending etag if fstat() error; not expected)*/
if (0 != fstat(fd, &st)) r->conf.etag_flags = 0;
}
const int wc = close(fd);
if (0 != wc && !http_status_is_set(r))
http_status_set_error(r, (errno == ENOSPC) ? 507 : 403);
if (!http_status_is_set(r)) {
http_status_set_fin(r, 204); /* No Content */
if (0 != r->conf.etag_flags) webdav_response_etag(r, &st);
}
return HANDLER_FINISHED;
}
static handler_t
mod_webdav_put (request_st * const r, const plugin_config * const pconf)
{
if (r->state == CON_STATE_READ_POST) {
int first_read = chunkqueue_is_empty(&r->reqbody_queue);
handler_t rc = r->con->reqbody_read(r);
if (rc != HANDLER_GO_ON) {
if (first_read && rc == HANDLER_WAIT_FOR_EVENT
&& 0 != webdav_if_match_or_unmodified_since(r, NULL)) {
http_status_set_error(r, 412); /* Precondition Failed */
return HANDLER_FINISHED;
}
return rc;
}
}
if (0 != webdav_if_match_or_unmodified_since(r, NULL)) {
http_status_set_error(r, 412); /* Precondition Failed */
return HANDLER_FINISHED;
}
if (pconf->opts & (MOD_WEBDAV_UNSAFE_PARTIAL_PUT_COMPAT
|MOD_WEBDAV_CPYTMP_PARTIAL_PUT)) {
const buffer * const h =
http_header_request_get(r, HTTP_HEADER_CONTENT_RANGE,
CONST_STR_LEN("Content-Range"));
if (NULL != h)
return mod_webdav_put_range(r, h, pconf);
}
/* construct temporary filename in same directory as target
* (expect cq contains exactly one chunk:
* the temporary FILE_CHUNK created in mod_webdav_put_prep())
* (do not reuse c->mem buffer; if tmpfile was unlinked, c->mem is blank)
* (since temporary file was unlinked, no guarantee of unique name,
* so add pid and fd to avoid conflict with an unlikely parallel
* PUT request being handled by same server pid (presumably by
* same client using same lock token)) */
chunkqueue * const cq = &r->reqbody_queue;
chunk *c = cq->first;
/* future: might support client specifying getcontenttype property
* using Content-Type request header. However, [RFC4918] 9.7.1 notes:
* Many servers do not allow configuring the Content-Type on a
* per-resource basis in the first place. Thus, clients can't always
* rely on the ability to directly influence the content type by
* including a Content-Type request header
*/
/*(similar to beginning of webdav_linktmp_rename())*/
buffer * const tmpb = pconf->tmpb;
buffer_clear(tmpb);
buffer_append_str2(tmpb, BUF_PTR_LEN(&r->physical.path),
CONST_STR_LEN("."));
buffer_append_int(tmpb, (long)getpid());
buffer_append_char(tmpb, '.');
if (c->type == MEM_CHUNK)
buffer_append_uint_hex_lc(tmpb, (uintptr_t)pconf); /*(stack/heap addr)*/
else
buffer_append_int(tmpb, (long)c->file.fd);
buffer_append_char(tmpb, '~');
if (buffer_clen(tmpb) >= PATH_MAX) { /*(temp file path too long)*/
http_status_set_error(r, 500); /* Internal Server Error */
return HANDLER_FINISHED;
}
const char *pathtemp = tmpb->ptr;
#if (defined(__linux__) || defined(__CYGWIN__)) && defined(O_TMPFILE)
if (c->type == FILE_CHUNK) { /*(reqbody contained in single tempfile)*/
if (NULL != c->next) {
/* if request body <= 64k, in-memory chunks might have been
* moved to cq instead of appended to first chunk FILE_CHUNK */
if (!mod_webdav_write_single_file_chunk(r, cq))
return HANDLER_FINISHED;
}
if (mod_webdav_put_linkat_rename(r, pathtemp))
return HANDLER_FINISHED;
/* attempt traditional copy (below) if linkat() failed for any reason */
}
#endif
const int fd = fdevent_open_cloexec(pathtemp, 0,
O_WRONLY | O_CREAT | O_EXCL | O_TRUNC,
WEBDAV_FILE_MODE);
if (fd < 0) {
http_status_set_error(r, 500); /* Internal Server Error */
return HANDLER_FINISHED;
}
/* copy all chunks even though expecting single chunk
* (still, loop on partial writes)
* (Note: copying might take some time, temporarily pausing server)
* (error status is set if error occurs) */
mod_webdav_write_cq(r, cq, fd);
struct stat st;
if (0 != r->conf.etag_flags && !http_status_is_set(r)) {
/*(skip sending etag if fstat() error; not expected)*/
if (0 != fstat(fd, &st)) r->conf.etag_flags = 0;
}
const int wc = close(fd);
if (0 != wc && !http_status_is_set(r))
http_status_set_error(r, (errno == ENOSPC) ? 507 : 403);
if (!http_status_is_set(r)) {
struct stat ste;
http_status_set_fin(r, 0 == lstat(r->physical.path.ptr, &ste)
? 204 /* No Content */
: 201); /* Created */
if (201 == http_status_get(r))
webdav_parent_modified(&r->physical.path);
if (0 == rename(pathtemp, r->physical.path.ptr)) {
if (0 != r->conf.etag_flags) webdav_response_etag(r, &st);
}
else {
if (errno == EISDIR)
http_status_set_error(r, 405); /* Method Not Allowed */
else
http_status_set_error(r, 500); /* Internal Server Error */
unlink(pathtemp);
}
}
else
unlink(pathtemp);
return HANDLER_FINISHED;
}
static handler_t
mod_webdav_copymove_b (request_st * const r, const plugin_config * const pconf, physical_st * const dst)
{
buffer * const dst_path = &dst->path;
buffer * const dst_rel_path = &dst->rel_path;
int flags = WEBDAV_FLAG_OVERWRITE /*(default)*/
| (r->conf.force_lowercase_filenames
? WEBDAV_FLAG_LC_NAMES
: 0)
| (r->http_method == HTTP_METHOD_MOVE
? WEBDAV_FLAG_MOVE_RENAME
: ((pconf->opts & MOD_WEBDAV_UNSAFE_PARTIAL_PUT_COMPAT)
&& !(pconf->opts & MOD_WEBDAV_CPYTMP_PARTIAL_PUT))
? 0
: WEBDAV_FLAG_COPY_LINK);
const buffer * const h =
http_header_request_get(r,HTTP_HEADER_OTHER,CONST_STR_LEN("Overwrite"));
if (NULL != h) {
if (h->used != 2
|| ((h->ptr[0] & 0xdf) != 'F' && (h->ptr[0] & 0xdf) != 'T')) {
http_status_set_error(r, 400); /* Bad Request */
return HANDLER_FINISHED;
}
if ((h->ptr[0] & 0xdf) == 'F')
flags &= ~WEBDAV_FLAG_OVERWRITE;
}
/* parse Destination
*
* http://127.0.0.1:1025/dav/litmus/copydest
*
* - host has to match Host: header in request
* (or else would need to check that Destination is reachable from server
* and authentication credentials grant privileges on Destination)
* - query string on Destination, if present, is discarded
*
* NOTE: Destination path is relative to document root and IS NOT re-run
* through other modules on server (such as aliasing or rewrite or userdir)
*/
const buffer * const destination =
http_header_request_get(r,HTTP_HEADER_OTHER,CONST_STR_LEN("Destination"));
if (NULL == destination) {
http_status_set_error(r, 400); /* Bad Request */
return HANDLER_FINISHED;
}
#ifdef __COVERITY__
force_assert(2 <= destination->used);
#endif
const char *sep = destination->ptr, *start;
if (*sep != '/') { /* path-absolute or absolute-URI form */
start = sep;
sep = start + buffer_clen(&r->uri.scheme);
if (0 != strncmp(start, r->uri.scheme.ptr, sep - start)
|| sep[0] != ':' || sep[1] != '/' || sep[2] != '/') {
http_status_set_error(r, 400); /* Bad Request */
return HANDLER_FINISHED;
}
start = sep + 3;
if (NULL == (sep = strchr(start, '/'))) {
http_status_set_error(r, 400); /* Bad Request */
return HANDLER_FINISHED;
}
if (!buffer_eq_slen(&r->uri.authority, start, sep - start)
/* skip login info (even though it should not be present) */
&& (NULL == (start = (char *)memchr(start, '@', sep - start))
|| (++start, !buffer_eq_slen(&r->uri.authority,
start, sep - start)))) {
/* not the same host */
http_status_set_error(r, 502); /* Bad Gateway */
return HANDLER_FINISHED;
}
}
start = sep; /* starts with '/' */
/* destination: remove query string, urldecode, path_simplify
* and (maybe) lowercase for consistent destination URI path */
buffer_copy_string_len(dst_rel_path, start,
NULL == (sep = strchr(start, '?'))
? destination->ptr + destination->used-1 - start
: sep - start);
if (buffer_clen(dst_rel_path) >= PATH_MAX) {
http_status_set_error(r, 403); /* Forbidden */
return HANDLER_FINISHED;
}
buffer_urldecode_path(dst_rel_path);
if (!buffer_is_valid_UTF8(dst_rel_path)) {
/* invalid UTF-8 after url-decode */
http_status_set_error(r, 400);
return HANDLER_FINISHED;
}
buffer_path_simplify(dst_rel_path);
if (buffer_is_blank(dst_rel_path) || dst_rel_path->ptr[0] != '/') {
http_status_set_error(r, 400);
return HANDLER_FINISHED;
}
if (flags & WEBDAV_FLAG_LC_NAMES)
buffer_to_lower(dst_rel_path);
/* Destination physical path
* src r->physical.path might have been remapped with mod_alias.
* (but mod_alias does not modify r->physical.rel_path)
* Find matching prefix to support use of mod_alias to remap webdav root.
* Aliasing of paths underneath the webdav root might not work.
* Likewise, mod_rewrite URL rewriting might thwart this comparison.
* Use mod_redirect instead of mod_alias to remap paths *under* webdav root.
* Use mod_redirect instead of mod_rewrite on *any* parts of path to webdav.
* (Related, use mod_auth to protect webdav root, but avoid attempting to
* use mod_auth on paths underneath webdav root, as Destination is not
* validated with mod_auth)
*
* tl;dr: webdav paths and webdav properties are managed by mod_webdav,
* so do not modify paths externally or else undefined behavior
* or corruption may occur
*
* find matching URI prefix (lowercased if WEBDAV_FLAG_LC_NAMES)
* (r->physical.rel_path and dst_rel_path will always match leading '/')
* check if remaining r->physical.rel_path matches suffix of
* r->physical.path so that we can use the prefix to remap
* Destination physical path */
#ifdef __COVERITY__
force_assert(0 != r->physical.rel_path.used);
#endif
uint32_t i, remain;
{
const char * const p1 = r->physical.rel_path.ptr;
const char * const p2 = dst_rel_path->ptr;
for (i = 0; p1[i] && p1[i] == p2[i]; ++i) ;
while (i != 0 && p1[--i] != '/') ; /* find matching directory path */
}
remain = r->physical.rel_path.used - 1 - i;
if (r->physical.path.used - 1 <= remain) { /*(should not happen)*/
http_status_set_error(r, 403); /* Forbidden */
return HANDLER_FINISHED;
}
if (0 == memcmp(r->physical.rel_path.ptr+i, /*(suffix match)*/
r->physical.path.ptr + r->physical.path.used-1-remain,
remain)) { /*(suffix match)*/
#ifdef __COVERITY__
force_assert(2 <= dst_rel_path->used);
#endif
buffer_copy_path_len2(dst_path,
r->physical.path.ptr,
r->physical.path.used - 1 - remain,
dst_rel_path->ptr+i,
dst_rel_path->used - 1 - i);
if (buffer_clen(dst_path) >= PATH_MAX) {
http_status_set_error(r, 403); /* Forbidden */
return HANDLER_FINISHED;
}
}
else { /*(not expected; some other module mucked with path or rel_path)*/
/* unable to perform physical path remap here;
* assume doc_root/rel_path and no remapping */
buffer_copy_path_len2(dst_path, BUF_PTR_LEN(&r->physical.doc_root),
BUF_PTR_LEN(dst_rel_path));
if (buffer_clen(dst_path) >= PATH_MAX) {
http_status_set_error(r, 403); /* Forbidden */
return HANDLER_FINISHED;
}
}
if (r->physical.path.used <= dst_path->used
&& 0 == memcmp(r->physical.path.ptr, dst_path->ptr,
r->physical.path.used-1)
&& (buffer_has_pathsep_suffix(&r->physical.path)
|| dst_path->ptr[r->physical.path.used-1] == '/'
|| dst_path->ptr[r->physical.path.used-1] == '\0')) {
/* dst must not be nested under (or same as) src */
http_status_set_error(r, 403); /* Forbidden */
return HANDLER_FINISHED;
}
struct stat st;
if (-1 == lstat(r->physical.path.ptr, &st)) {
/* don't known about it yet, unlink will fail too */
http_status_set_error(r, (errno == ENOENT) ? 404 : 403);
return HANDLER_FINISHED;
}
if (0 != webdav_if_match_or_unmodified_since(r, &st)) {
http_status_set_error(r, 412); /* Precondition Failed */
return HANDLER_FINISHED;
}
if (S_ISDIR(st.st_mode)) {
if (!buffer_has_pathsep_suffix(&r->physical.path)) {
http_response_redirect_to_directory(r, 308);
return HANDLER_FINISHED; /* 308 Permanent Redirect */
/* Alternatively, could append '/' to r->physical.path
* and r->physical.rel_path, set Content-Location in
* response headers, and continue to serve the request. */
}
/* ensure Destination paths end with '/' since dst is a collection */
if (!buffer_has_slash_suffix(dst_rel_path)) {
buffer_append_slash(dst_rel_path);
buffer_append_slash(dst_path);
}
/* check for lock on destination (after ensuring dst ends in '/') */
if (!webdav_has_lock(r, pconf, dst_rel_path))
return HANDLER_FINISHED; /* 423 Locked */
const int depth = webdav_parse_Depth(r);
if (1 == depth) {
http_status_set_error(r, 400); /* Bad Request */
return HANDLER_FINISHED;
}
if (0 == depth) {
if (r->http_method == HTTP_METHOD_MOVE) {
http_status_set_error(r, 400); /* Bad Request */
return HANDLER_FINISHED;
}
/* optionally create collection, then copy properties */
int status;
if (0 == lstat(dst_path->ptr, &st)) {
if (S_ISDIR(st.st_mode))
status = 204; /* No Content */
else if (flags & WEBDAV_FLAG_OVERWRITE) {
status = webdav_mkdir(pconf, dst, 1);
if (0 == status) status = 204; /* No Content */
}
else
status = 412; /* Precondition Failed */
}
else if (errno == ENOENT) {
status = webdav_mkdir(pconf, dst,
!!(flags & WEBDAV_FLAG_OVERWRITE));
if (0 == status) status = 201; /* Created */
}
else
status = 403; /* Forbidden */
if (status < 300) {
http_status_set_fin(r, status);
webdav_prop_copy_uri(pconf, &r->physical.rel_path,
dst_rel_path);
}
else
http_status_set_error(r, status);
return HANDLER_FINISHED;
}
if (0 == webdav_copymove_dir(pconf, &r->physical, dst, r, flags)) {
if (r->http_method == HTTP_METHOD_MOVE)
webdav_lock_delete_uri_col(pconf, &r->physical.rel_path);
/*(requiring lock on destination requires MKCOL create dst first)
*(if no lock support, return 200 OK unconditionally
* instead of 200 OK or 201 Created; not fully RFC-conformant)*/
http_status_set_fin(r, 200); /* OK */
}
else {
/* Note: this does not destroy any locks if any error occurs,
* which is not a problem if lock is only on the collection
* being moved, but might need finer updates if there are
* locks on internal elements that are successfully moved */
webdav_xml_doc_multistatus(r, pconf); /* 207 Multi-status */
}
/* invalidate stat cache of src if MOVE, whether or not successful */
if (r->http_method == HTTP_METHOD_MOVE)
stat_cache_delete_dir(BUF_PTR_LEN(&r->physical.path));
return HANDLER_FINISHED;
}
else if (buffer_has_pathsep_suffix(&r->physical.path)) {
http_status_set_error(r, 403); /* Forbidden */
return HANDLER_FINISHED;
}
else {
/* check if client has lock for destination
* Note: requiring a lock on non-collection means that destination
* should always exist since the issuance of the lock creates the
* resource, so client will always have to provide Overwrite: T
* for direct operations on non-collections (files) */
if (!webdav_has_lock(r, pconf, dst_rel_path))
return HANDLER_FINISHED; /* 423 Locked */
/* check if destination exists
* (Destination should exist since lock is required,
* and obtaining a lock will create the resource) */
int rc = lstat(dst_path->ptr, &st);
if (0 == rc && S_ISDIR(st.st_mode)) {
/* file to dir/
* append basename to physical path
* future: might set Content-Location if dst_path does not end '/'*/
if (NULL != (sep = strrchr(r->physical.path.ptr, '/'))) {
size_t len = r->physical.path.used - 1
- (sep - r->physical.path.ptr);
if (buffer_has_pathsep_suffix(dst_path)) {
++sep; /*(avoid double-slash in path)*/
--len;
}
buffer_append_string_len(dst_path, sep, len);
buffer_append_string_len(dst_rel_path, sep, len);
if (buffer_clen(dst_path) >= PATH_MAX) {
http_status_set_error(r, 403); /* Forbidden */
return HANDLER_FINISHED;
}
rc = lstat(dst_path->ptr, &st);
/* target (parent collection) already exists */
http_status_set_fin(r, 204); /* No Content */
}
}
if (-1 == rc) {
char *slash;
switch (errno) {
case ENOENT:
if (http_status_is_set(r)) break;
/* check that parent collection exists */
if ((slash = strrchr(dst_path->ptr, '/'))) {
*slash = '\0';
if (0 == lstat(dst_path->ptr, &st) && S_ISDIR(st.st_mode)) {
*slash = '/';
/* new entity will be created */
if (!http_status_is_set(r)) {
webdav_parent_modified(dst_path);
http_status_set_fin(r, 201); /* Created */
}
break;
}
}
__attribute_fallthrough__
/*case ENOTDIR:*/
default:
http_status_set_error(r, 409); /* Conflict */
return HANDLER_FINISHED;
}
}
else if (!(flags & WEBDAV_FLAG_OVERWRITE)) {
/* destination exists, but overwrite is not set */
http_status_set_error(r, 412); /* Precondition Failed */
return HANDLER_FINISHED;
}
else if (S_ISDIR(st.st_mode)) {
/* destination exists, but is a dir, not a file */
http_status_set_error(r, 409); /* Conflict */
return HANDLER_FINISHED;
}
else { /* resource already exists */
http_status_set_fin(r, 204); /* No Content */
}
rc = webdav_copymove_file(pconf, &r->physical, dst, &flags);
if (0 == rc) {
if (r->http_method == HTTP_METHOD_MOVE)
webdav_lock_delete_uri(pconf, &r->physical.rel_path);
}
else
http_status_set_error(r, rc);
return HANDLER_FINISHED;
}
}
static handler_t
mod_webdav_copymove (request_st * const r, const plugin_config * const pconf)
{
buffer *dst_path = chunk_buffer_acquire();
buffer *dst_rel_path = chunk_buffer_acquire();
physical_st dst;
dst.path = *dst_path;
dst.rel_path = *dst_rel_path;
handler_t rc = mod_webdav_copymove_b(r, pconf, &dst);
*dst_path = dst.path;
*dst_rel_path = dst.rel_path;
chunk_buffer_release(dst_rel_path);
chunk_buffer_release(dst_path);
return rc;
}
#ifdef USE_PROPPATCH
static handler_t
mod_webdav_proppatch (request_st * const r, const plugin_config * const pconf)
{
if (!pconf->sql)
return webdav_405_no_db(r);
if (r->state == CON_STATE_READ_POST) {
handler_t rc = r->con->reqbody_read(r);
if (rc != HANDLER_GO_ON) return rc;
}
if (0 == r->reqbody_length) {
http_status_set_error(r, 400); /* Bad Request */
return HANDLER_FINISHED;
}
if (!webdav_reqbody_type_xml(r)) {
http_status_set_error(r, 415); /* Unsupported Media Type */
return HANDLER_FINISHED;
}
struct stat st;
if (0 != lstat(r->physical.path.ptr, &st)) {
http_status_set_error(r, (errno == ENOENT) ? 404 : 403);
return HANDLER_FINISHED;
}
if (0 != webdav_if_match_or_unmodified_since(r, &st)) {
http_status_set_error(r, 412); /* Precondition Failed */
return HANDLER_FINISHED;
}
if (S_ISDIR(st.st_mode)) {
if (!buffer_has_pathsep_suffix(&r->physical.path)) {
/* set "Content-Location" instead of sending 308 redirect to dir */
if (0 != http_response_redirect_to_directory(r, 0))
return HANDLER_FINISHED;
buffer_append_char(&r->physical.path, '/');
buffer_append_char(&r->physical.rel_path, '/');
}
}
else if (buffer_has_pathsep_suffix(&r->physical.path)) {
http_status_set_error(r, 403);
return HANDLER_FINISHED;
}
xmlDocPtr const xml = webdav_parse_chunkqueue(r, pconf);
if (NULL == xml) {
http_status_set_error(r, 400); /* Bad Request */
return HANDLER_FINISHED;
}
const xmlNode * const rootnode = xmlDocGetRootElement(xml);
if (NULL == rootnode
|| 0 != webdav_xmlstrcmp_fixed(rootnode->name, "propertyupdate")) {
http_status_set_error(r, 422); /* Unprocessable Entity */
xmlFreeDoc(xml);
return HANDLER_FINISHED;
}
if (!webdav_db_transaction_begin_immediate(pconf)) {
http_status_set_error(r, 500); /* Internal Server Error */
xmlFreeDoc(xml);
return HANDLER_FINISHED;
}
/* NOTE: selectively providing multi-status response is NON-CONFORMANT
* (specified in [RFC4918])
* However, PROPPATCH is all-or-nothing, so client should be able to
* unequivocably know that all items in PROPPATCH succeeded if it receives
* 204 No Content, or that items that are not listed with a failure status
* in a multi-status response have the status of 424 Failed Dependency,
* without the server having to be explicit. */
/* UPDATE request, we know 'set' and 'remove' */
buffer *ms = NULL; /*(multi-status)*/
int update;
for (const xmlNode *cmd = rootnode->children; cmd; cmd = cmd->next) {
if (!(update = (0 == webdav_xmlstrcmp_fixed(cmd->name, "set")))) {
if (0 != webdav_xmlstrcmp_fixed(cmd->name, "remove"))
continue; /* skip; not "set" or "remove" */
}
for (const xmlNode *props = cmd->children; props; props = props->next) {
if (0 != webdav_xmlstrcmp_fixed(props->name, "prop"))
continue;
const xmlNode *prop = props->children;
/* libxml2 will keep those blank (whitespace only) nodes */
while (NULL != prop && xmlIsBlankNode(prop))
prop = prop->next;
if (NULL == prop)
continue;
if (prop->ns && '\0' == *(char *)prop->ns->href
&& '\0' != *(char *)prop->ns->prefix) {
/* error: missing namespace for property */
log_error(r->conf.errh, __FILE__, __LINE__,
"no namespace for: %s", prop->name);
if (!ms) ms = chunk_buffer_acquire(); /* Unprocessable Entity */
webdav_xml_propstat_status(ms, "", (char *)prop->name, 422);
webdav_double_buffer(r, ms);
continue;
}
/* XXX: ??? should blank namespace be normalized to "DAV:" ???
* ??? should this also be done in propfind requests ??? */
if (prop->ns && 0 == strcmp((char *)prop->ns->href, "DAV:")) {
const size_t namelen = strlen((char *)prop->name);
const struct live_prop_list *list = protected_props;
while (0 != list->len
&& (list->len != namelen
|| 0 != memcmp(prop->name, list->prop, list->len)))
++list;
if (NULL != list->prop) {
/* error <DAV:cannot-modify-protected-property/> */
if (!ms) ms = chunk_buffer_acquire();
webdav_xml_propstat_protected(ms, (char *)prop->name,
namelen, 403); /* Forbidden */
webdav_double_buffer(r, ms);
continue;
}
}
if (update) {
if (!prop->children) continue;
char * const propval = prop->children
? (char *)xmlNodeListGetString(xml, prop->children, 0)
: NULL;
webdav_prop_update(pconf, &r->physical.rel_path,
(char *)prop->name,
prop->ns ? (char *)prop->ns->href : "",
propval ? propval : "");
xmlFree(propval);
}
else
webdav_prop_delete(pconf, &r->physical.rel_path,
(char *)prop->name,
prop->ns ? (char *)prop->ns->href : "");
}
}
if (NULL == ms
? webdav_db_transaction_commit(pconf)
: webdav_db_transaction_rollback(pconf)) {
if (NULL == ms) {
const buffer *vb =
http_header_request_get(r, HTTP_HEADER_USER_AGENT,
CONST_STR_LEN("User-Agent"));
if (vb && 0 == strncmp(vb->ptr, "Microsoft-WebDAV-MiniRedir/",
sizeof("Microsoft-WebDAV-MiniRedir/")-1)) {
/* workaround Microsoft-WebDAV-MiniRedir bug; 204 not handled */
/* 200 without response body or 204 both incorrectly interpreted
* as 507 Insufficient Storage by Microsoft-WebDAV-MiniRedir. */
ms = chunk_buffer_acquire(); /* 207 Multi-status */ /*(flag)*/
webdav_xml_response_status(r, &r->physical.path, 200);
}
}
if (NULL == ms)
http_status_set_fin(r, 204); /* No Content */
else /* 207 Multi-status */
webdav_xml_doc_multistatus_response(r, pconf, ms);
}
else
http_status_set_error(r, 500); /* Internal Server Error */
if (NULL != ms)
chunk_buffer_release(ms);
xmlFreeDoc(xml);
return HANDLER_FINISHED;
}
#endif
#ifdef USE_LOCKS
struct webdav_conflicting_lock_st {
webdav_lockdata *lockdata;
buffer *b;
request_st *r;
};
static void
webdav_conflicting_lock_cb (void * const vdata,
const webdav_lockdata * const lockdata)
{
/* lock is not available if someone else has exclusive lock or if
* client requested exclusive lock and others have shared locks */
struct webdav_conflicting_lock_st * const cbdata =
(struct webdav_conflicting_lock_st *)vdata;
if (lockdata->lockscope->used == sizeof("exclusive")
|| cbdata->lockdata->lockscope->used == sizeof("exclusive")) {
webdav_xml_href(cbdata->b, &lockdata->lockroot);
webdav_double_buffer(cbdata->r, cbdata->b);
}
}
#include "rand.h" /* li_rand_pseudo_bytes() */
static void
webdav_uuid_v4 (char * const s) /* s receives 36 chars of output */
{
/* generate UUID version 4: random number based */
char uuid[16];
/*(could have written into s+20 instead of uuid[] of stack
* except that would violate 'restrict' keyword on li_tohex_lc() args)*/
/*(li_rand_pseudo_bytes() aims to produce high-quality pseudorandom bytes)*/
/*(If using li_rand_bytes() instead, then check return value)*/
li_rand_pseudo_bytes((unsigned char *)uuid, sizeof(uuid));
/* set version 4 */
uuid[6] &= 0x0f;
uuid[6] |= 0x40;
/* set variant (always DCE 1.1 only) */
uuid[8] &= 0x3f;
uuid[8] |= 0x80;
/* stringify UUID */
#if 0 /* write string into hex[] and then memcpy() into s */
char hex[32];
li_tohex_lc(hex, sizeof(hex), uuid, sizeof(uuid));
memcpy(s, hex, 8);
s[8] = '-';
memcpy(s+9, hex, 4);
s[13] = '-';
memcpy(s+14, hex, 4);
s[18] = '-';
memcpy(s+19, hex, 4);
s[23] = '-';
memcpy(s+24, hex, 12);
#else /* write string into s and then memmove() in-place */
li_tohex_lc(s+4, 36-4, uuid, sizeof(uuid));
memmove(s, s+4, 8);
s[8] = '-';
memmove(s+9, s+4+8, 4);
s[13] = '-';
memmove(s+14, s+4+8+4, 4);
s[18] = '-';
memmove(s+19, s+4+8+4+4, 4);
s[23] = '-';
#endif
}
static handler_t
mod_webdav_lock (request_st * const r, const plugin_config * const pconf)
{
/**
* a mac wants to write
*
* LOCK /dav/expire.txt HTTP/1.1\r\n
* User-Agent: WebDAVFS/1.3 (01308000) Darwin/8.1.0 (Power Macintosh)\r\n
* Accept: * / *\r\n
* Depth: 0\r\n
* Timeout: Second-600\r\n
* Content-Type: text/xml;charset=utf-8\r\n
* Content-Length: 229\r\n
* Connection: keep-alive\r\n
* Host: 192.168.178.23:1025\r\n
* \r\n
* <?xml version=\"1.0\" encoding=\"utf-8\"?>\n
* <D:lockinfo xmlns:D=\"DAV:\">\n
* <D:lockscope><D:exclusive/></D:lockscope>\n
* <D:locktype><D:write/></D:locktype>\n
* <D:owner>\n
* <D:href>http://www.apple.com/webdav_fs/</D:href>\n
* </D:owner>\n
* </D:lockinfo>\n
*/
if (r->reqbody_length) {
if (r->state == CON_STATE_READ_POST) {
handler_t rc = r->con->reqbody_read(r);
if (rc != HANDLER_GO_ON) return rc;
}
}
/* XXX: maybe add config switch to require that authentication occurred? */
buffer owner = { NULL, 0, 0 };/*owner (not authenticated)(auth_user unset)*/
const data_string * const authn_user = (const data_string *)
array_get_element_klen(&r->env, CONST_STR_LEN("REMOTE_USER"));
/* future: make max timeout configurable (e.g. pconf->lock_timeout_max)
*
* [RFC4918] 10.7 Timeout Request Header
* The "Second" TimeType specifies the number of seconds that will elapse
* between granting of the lock at the server, and the automatic removal
* of the lock. The timeout value for TimeType "Second" MUST NOT be
* greater than 2^32-1.
*/
webdav_lockdata lockdata = {
{ NULL, 0, 0 }, /* locktoken */
{ r->physical.rel_path.ptr, r->physical.rel_path.used, 0}, /*lockroot*/
{ NULL, 0, 0 }, /* ownerinfo */
(authn_user ? &authn_user->value : &owner), /* owner */
NULL, /* lockscope */
NULL, /* locktype */
-1, /* depth */
600 /* timeout (arbitrary default lock timeout: 10 minutes) */
};
const buffer *h =
http_header_request_get(r, HTTP_HEADER_OTHER, CONST_STR_LEN("Timeout"));
if (h) {
/* loosely parse Timeout request header and ignore "infinity" timeout */
/* future: might implement config param for upper limit for timeout */
const char *p = h->ptr;
do {
if ((*p | 0x20) == 's'
&& buffer_eq_icase_ssn(p, CONST_STR_LEN("second-"))) {
long t = strtol(p+sizeof("second-")-1, NULL, 10);
if (0 < t && t < lockdata.timeout)
lockdata.timeout = t > 5 ? t : 5;
/*(arbitrary min timeout: 5 secs)*/
else if (sizeof(long) != sizeof(int) && t > INT32_MAX)
lockdata.timeout = INT32_MAX;
/* while UINT32_MAX is actual limit in RFC4918,
* sqlite more easily supports int, though could be
* changed to use int64 to for the timeout param.
* The "limitation" between timeouts that are many
* *years* long does not really matter in reality. */
break;
}
#if 0
else if ((*p | 0x20) == 'i'
&& buffer_eq_icase_ssn(p, CONST_STR_LEN("infinity"))) {
lockdata.timeout = INT32_MAX;
break;
}
#endif
while (*p != ',' && *p != '\0') ++p;
while (*p == ' ' || *p == '\t') ++p;
} while (*p != '\0');
}
if (r->reqbody_length) {
lockdata.depth = webdav_parse_Depth(r);
if (1 == lockdata.depth) {
/* [RFC4918] 9.10.3 Depth and Locking
* Values other than 0 or infinity MUST NOT be used
* with the Depth header on a LOCK method.
*/
http_status_set_error(r, 400); /* Bad Request */
return HANDLER_FINISHED;
}
xmlDocPtr const xml = webdav_parse_chunkqueue(r, pconf);
if (NULL == xml) {
http_status_set_error(r, 400); /* Bad Request */
return HANDLER_FINISHED;
}
const xmlNode * const rootnode = xmlDocGetRootElement(xml);
if (NULL == rootnode
|| 0 != webdav_xmlstrcmp_fixed(rootnode->name, "lockinfo")) {
http_status_set_error(r, 422); /* Unprocessable Entity */
xmlFreeDoc(xml);
return HANDLER_FINISHED;
}
const xmlNode *lockinfo = rootnode->children;
for (; lockinfo; lockinfo = lockinfo->next) {
if (0 == webdav_xmlstrcmp_fixed(lockinfo->name, "lockscope")) {
const xmlNode *value = lockinfo->children;
for (; value; value = value->next) {
if (0 == webdav_xmlstrcmp_fixed(value->name, "exclusive"))
lockdata.lockscope=(const buffer *)&lockscope_exclusive;
else if (0 == webdav_xmlstrcmp_fixed(value->name, "shared"))
lockdata.lockscope=(const buffer *)&lockscope_shared;
else {
lockdata.lockscope=NULL; /* trigger error below loop */
break;
}
}
}
else if (0 == webdav_xmlstrcmp_fixed(lockinfo->name, "locktype")) {
const xmlNode *value = lockinfo->children;
for (; value; value = value->next) {
if (0 == webdav_xmlstrcmp_fixed(value->name, "write"))
lockdata.locktype = (const buffer *)&locktype_write;
else {
lockdata.locktype = NULL;/* trigger error below loop */
break;
}
}
}
else if (0 == webdav_xmlstrcmp_fixed(lockinfo->name, "owner")) {
if (lockinfo->children)
lockdata.ownerinfo.ptr =
(char *)xmlNodeListGetString(xml, lockinfo->children, 0);
if (lockdata.ownerinfo.ptr)
lockdata.ownerinfo.used = strlen(lockdata.ownerinfo.ptr)+1;
}
}
do { /*(resources are cleaned up after code block)*/
if (NULL == lockdata.lockscope || NULL == lockdata.locktype) {
/*(missing lockscope and locktype in lock request)*/
http_status_set_error(r, 422); /* Unprocessable Entity */
break; /* clean up resources and return HANDLER_FINISHED */
}
/* check lock prior to potentially creating new resource,
* and prior to using entropy to create uuid */
struct webdav_conflicting_lock_st cbdata;
cbdata.lockdata = &lockdata;
cbdata.b = chunk_buffer_acquire();
cbdata.r = r;
webdav_lock_activelocks(pconf, &lockdata.lockroot,
(0 == lockdata.depth ? 1 : -1),
webdav_conflicting_lock_cb, &cbdata);
if (0 != cbdata.b->used || !chunkqueue_is_empty(&r->write_queue)) {
/* 423 Locked */
webdav_xml_doc_error_no_conflicting_lock(r, cbdata.b);
chunk_buffer_release(cbdata.b);
break; /* clean up resources and return HANDLER_FINISHED */
}
chunk_buffer_release(cbdata.b);
int created = 0;
struct stat st;
if (0 != lstat(r->physical.path.ptr, &st)) {
/* [RFC4918] 7.3 Write Locks and Unmapped URLs
* A successful lock request to an unmapped URL MUST result in
* the creation of a locked (non-collection) resource with empty
* content.
* [...]
* The response MUST indicate that a resource was created, by
* use of the "201 Created" response code (a LOCK request to an
* existing resource instead will result in 200 OK).
* [RFC4918] 9.10.4 Locking Unmapped URLs
* A successful LOCK method MUST result in the creation of an
* empty resource that is locked (and that is not a collection)
* when a resource did not previously exist at that URL. Later on,
* the lock may go away but the empty resource remains. Empty
* resources MUST then appear in PROPFIND responses including that
* URL in the response scope. A server MUST respond successfully
* to a GET request to an empty resource, either by using a 204
* No Content response, or by using 200 OK with a Content-Length
* header indicating zero length
*
* unmapped resource; create empty file
* (open() should fail if path ends in '/', but does not on some OS.
* This is desired behavior since collection should be created
* with MKCOL, and not via LOCK on an unmapped resource) */
const int fd =
(errno == ENOENT && !buffer_has_pathsep_suffix(&r->physical.path))
? fdevent_open_cloexec(r->physical.path.ptr, 0,
O_WRONLY | O_CREAT | O_EXCL | O_TRUNC,
WEBDAV_FILE_MODE)
: -1;
if (fd >= 0) {
/*(skip sending etag if fstat() error; not expected)*/
if (0 != fstat(fd, &st)) r->conf.etag_flags = 0;
close(fd);
created = 1;
webdav_parent_modified(&r->physical.path);
}
else if (errno != EEXIST) {
http_status_set_error(r, 403); /* Forbidden */
break; /* clean up resources and return HANDLER_FINISHED */
}
else if (0 != lstat(r->physical.path.ptr, &st)) {
http_status_set_error(r, 403); /* Forbidden */
break; /* clean up resources and return HANDLER_FINISHED */
}
lockdata.depth = 0; /* force Depth: 0 on non-collections */
}
if (!created) {
if (0 != webdav_if_match_or_unmodified_since(r, &st)) {
http_status_set_error(r, 412); /* Precondition Failed */
break; /* clean up resources and return HANDLER_FINISHED */
}
}
if (created) {
}
else if (S_ISDIR(st.st_mode)) {
if (!buffer_has_pathsep_suffix(&r->physical.path)) {
/* 308 Permanent Redirect */
http_response_redirect_to_directory(r, 308);
break; /* clean up resources and return HANDLER_FINISHED */
/* Alternatively, could append '/' to r->physical.path
* and r->physical.rel_path, set Content-Location in
* response headers, and continue to serve the request */
}
}
else if (buffer_has_pathsep_suffix(&r->physical.path)) {
http_status_set_error(r, 403); /* Forbidden */
break; /* clean up resources and return HANDLER_FINISHED */
}
else if (0 != lockdata.depth)
lockdata.depth = 0; /* force Depth: 0 on non-collections */
/* create locktoken
* (uuid v4 string is 36 chars) */
char lockstr[sizeof("<urn:uuid:>") + 36] = "<urn:uuid:";
lockdata.locktoken.ptr = lockstr+1; /*(without surrounding <>)*/
lockdata.locktoken.used = sizeof(lockstr)-2;/*(without surrounding <>)*/
lockstr[sizeof(lockstr)-2] = '\0';
webdav_uuid_v4(lockstr+sizeof("<urn:uuid:")-1);
/* XXX: consider fix TOC-TOU race condition by starting transaction
* and re-running webdav_lock_activelocks() check before running
* webdav_lock_acquire() (but both routines would need to be modified
* to defer calling sqlite3_reset(stmt) to be part of transaction) */
if (webdav_lock_acquire(pconf, &lockdata)) {
lockstr[sizeof(lockstr)-2] = '>';
http_header_response_set(r, HTTP_HEADER_OTHER,
CONST_STR_LEN("Lock-Token"),
lockstr, sizeof(lockstr)-1);
webdav_xml_doc_lock_acquired(r, pconf, &lockdata);
if (0 != r->conf.etag_flags && !S_ISDIR(st.st_mode))
webdav_response_etag(r, &st);
http_status_set_fin(r, created ? 201 : 200); /* Created | OK */
}
else /*(database error obtaining lock)*/
http_status_set_error(r, 500); /* Internal Server Error */
} while (0); /*(resources are cleaned up after code block)*/
xmlFree(lockdata.ownerinfo.ptr);
xmlFreeDoc(xml);
return HANDLER_FINISHED;
}
else {
h = http_header_request_get(r, HTTP_HEADER_OTHER, CONST_STR_LEN("If"));
if (NULL == h
|| h->used < 6 || h->ptr[1] != '<' || h->ptr[h->used-3] != '>') {
/*(rejects value with trailing LWS, even though RFC-permitted)*/
http_status_set_error(r, 400); /* Bad Request */
return HANDLER_FINISHED;
}
/* remove (< >) around token */
lockdata.locktoken.ptr = h->ptr+2;
lockdata.locktoken.used = h->used-4;
/*(future: fill in from database, though exclusive write lock is the
* only lock supported at the moment)*/
lockdata.lockscope = (const buffer *)&lockscope_exclusive;
lockdata.locktype = (const buffer *)&locktype_write;
lockdata.depth = 0;
if (webdav_lock_refresh(pconf, &lockdata)) {
webdav_xml_doc_lock_acquired(r, pconf, &lockdata);
http_status_set_fin(r, 200); /* OK */
}
else
http_status_set_error(r, 412); /* Precondition Failed */
return HANDLER_FINISHED;
}
}
#endif
#ifdef USE_LOCKS
static handler_t
mod_webdav_unlock (request_st * const r, const plugin_config * const pconf)
{
const buffer * const h =
http_header_request_get(r, HTTP_HEADER_OTHER,
CONST_STR_LEN("Lock-Token"));
if (NULL == h
|| h->used < 4 || h->ptr[0] != '<' || h->ptr[h->used-2] != '>') {
/*(rejects value with trailing LWS, even though RFC-permitted)*/
http_status_set_error(r, 400); /* Bad Request */
return HANDLER_FINISHED;
}
buffer owner = { NULL, 0, 0 };/*owner (not authenticated)(auth_user unset)*/
const data_string * const authn_user = (const data_string *)
array_get_element_klen(&r->env, CONST_STR_LEN("REMOTE_USER"));
webdav_lockdata lockdata = {
{ h->ptr+1, h->used-2, 0 }, /* locktoken (remove < > around token) */
{ r->physical.rel_path.ptr, r->physical.rel_path.used, 0}, /*lockroot*/
{ NULL, 0, 0 }, /* ownerinfo (unused for unlock) */
(authn_user ? &authn_user->value : &owner), /* owner */
NULL, /* lockscope (unused for unlock) */
NULL, /* locktype (unused for unlock) */
0, /* depth (unused for unlock) */
0 /* timeout (unused for unlock) */
};
/* check URI (lockroot) and depth in scope for locktoken and authorized */
switch (webdav_lock_match(pconf, &lockdata)) {
case 0:
if (webdav_lock_release(pconf, &lockdata)) {
http_status_set_fin(r, 204); /* No Content */
return HANDLER_FINISHED;
}
__attribute_fallthrough__
default:
case -1: /* lock does not exist */
case -2: /* URI not in scope of locktoken and depth */
/* 409 Conflict */
webdav_xml_doc_error_lock_token_matches_request_uri(r);
return HANDLER_FINISHED;
case -3: /* not owner/not authorized to remove lock */
http_status_set_error(r, 403); /* Forbidden */
return HANDLER_FINISHED;
}
}
#endif
SUBREQUEST_FUNC(mod_webdav_subrequest_handler)
{
const plugin_config * const pconf =
(plugin_config *)r->plugin_ctx[((plugin_data *)p_d)->id];
if (NULL == pconf) return HANDLER_GO_ON; /*(should not happen)*/
switch (r->http_method) {
case HTTP_METHOD_PROPFIND:
return mod_webdav_propfind(r, pconf);
case HTTP_METHOD_MKCOL:
return mod_webdav_mkcol(r, pconf);
case HTTP_METHOD_DELETE:
return mod_webdav_delete(r, pconf);
case HTTP_METHOD_PUT:
return mod_webdav_put(r, pconf);
case HTTP_METHOD_MOVE:
case HTTP_METHOD_COPY:
return mod_webdav_copymove(r, pconf);
#ifdef USE_PROPPATCH
case HTTP_METHOD_PROPPATCH:
return mod_webdav_proppatch(r, pconf);
#endif
#ifdef USE_LOCKS
case HTTP_METHOD_LOCK:
return mod_webdav_lock(r, pconf);
case HTTP_METHOD_UNLOCK:
return mod_webdav_unlock(r, pconf);
#endif
default:
http_status_set_error(r, 501); /* Not Implemented */
return HANDLER_FINISHED;
}
}
PHYSICALPATH_FUNC(mod_webdav_physical_handler)
{
/* physical path is set up */
/*assert(0 != r->physical.path.used);*/
#ifdef __COVERITY__
force_assert(2 <= r->physical.path.used);
#endif
int check_readonly = 0;
int check_lock_src = 0;
int reject_reqbody = 0;
/* check for WebDAV request methods handled by this module */
switch (r->http_method) {
case HTTP_METHOD_GET:
case HTTP_METHOD_HEAD:
case HTTP_METHOD_POST:
default:
return HANDLER_GO_ON;
case HTTP_METHOD_PROPFIND:
case HTTP_METHOD_LOCK:
break;
case HTTP_METHOD_UNLOCK:
reject_reqbody = 1;
break;
case HTTP_METHOD_DELETE:
case HTTP_METHOD_MOVE:
reject_reqbody = 1;
__attribute_fallthrough__
case HTTP_METHOD_PROPPATCH:
case HTTP_METHOD_PUT:
check_readonly = check_lock_src = 1;
break;
case HTTP_METHOD_COPY:
case HTTP_METHOD_MKCOL:
check_readonly = reject_reqbody = 1;
break;
}
plugin_config pconf;
mod_webdav_patch_config(r, (plugin_data *)p_d, &pconf);
if (!pconf.enabled) return HANDLER_GO_ON;
if (check_readonly && pconf.is_readonly) {
http_status_set_error(r, 403); /* Forbidden */
return HANDLER_FINISHED;
}
if (r->reqbody_length) {
if (reject_reqbody) {
/* [RFC4918] 8.4 Required Bodies in Requests
* Servers MUST examine all requests for a body, even when a
* body was not expected. In cases where a request body is
* present but would be ignored by a server, the server MUST
* reject the request with 415 (Unsupported Media Type).
*/
http_status_set_error(r, 415); /* Unsupported Media Type */
return HANDLER_FINISHED;
}
/* [RFC7694]
* Hypertext Transfer Protocol (HTTP) Client-Initiated Content-Encoding
* (future: might add support for gzip or other content-encodings used
* by client on request body) */
const buffer * const vb =
http_header_request_get(r, HTTP_HEADER_CONTENT_ENCODING,
CONST_STR_LEN("Content-Encoding"));
if (vb != NULL) { /*("identity" not expected; should not be listed)*/
http_header_response_set(r, HTTP_HEADER_ACCEPT_ENCODING,
CONST_STR_LEN("Accept-Encoding"),
CONST_STR_LEN("identity"));
http_status_set_error(r, 415); /* Unsupported Media Type */
return HANDLER_FINISHED;
}
}
if (check_lock_src && !webdav_has_lock(r, &pconf, &r->physical.rel_path))
return HANDLER_FINISHED; /* 423 Locked */
/* initial setup for methods */
switch (r->http_method) {
case HTTP_METHOD_PUT:
if (mod_webdav_put_prep(r, &pconf) == HANDLER_FINISHED)
return HANDLER_FINISHED;
break;
default:
break;
}
r->handler_module = (plugin_data_base *)p_d;
r->conf.stream_request_body &=
~(FDEVENT_STREAM_REQUEST | FDEVENT_STREAM_REQUEST_BUFMIN);
r->plugin_ctx[((plugin_data *)p_d)->id] = &pconf;
const handler_t rc =
mod_webdav_subrequest_handler(r, p_d); /*pd->self->handle_subrequest()*/
if (rc == HANDLER_FINISHED || rc == HANDLER_ERROR)
r->plugin_ctx[((plugin_data *)p_d)->id] = NULL;
else /* e.g. HANDLER_WAIT_FOR_EVENT */
r->plugin_ctx[((plugin_data *)p_d)->id] = /* save pconf */
memcpy(ck_malloc(sizeof(pconf)), &pconf, sizeof(pconf));
return rc;
}
REQUEST_FUNC(mod_webdav_handle_reset) {
/* free plugin_config if allocated and saved to per-request storage */
void ** const restrict dptr =
&r->plugin_ctx[((plugin_data *)p_d)->id];
if (*dptr) {
free(*dptr);
*dptr = NULL;
chunkqueue_set_tempdirs(&r->reqbody_queue, 0); /* reset sz */
}
return HANDLER_GO_ON;
}
↑ V560 A part of conditional expression is always true: !pb.propname.
↑ V560 A part of conditional expression is always true: NULL == pb.proplist.ptr.
↑ V666 Consider inspecting seventh argument of the function 'buffer_append_str3'. It is possible that the value does not correspond with the length of a string which was passed with the second argument.
↑ V1115 The 'webdav_parse_Depth' function annotated with the 'pure' attribute has side effects.
↑ V1118 Excessive file permissions can lead to vulnerabilities. Consider restricting file permissions.
↑ V1118 Excessive file permissions can lead to vulnerabilities. Consider restricting file permissions.
↑ V506 Pointer to local variable 'pconf' is stored outside the scope of this variable. Such a pointer will become invalid.
↑ V574 The 'p->cvlist' pointer is used simultaneously as an array and as a pointer to single object. Check lines: 532, 591.
↑ V574 The 'p->cvlist' pointer is used simultaneously as an array and as a pointer to single object. Check lines: 532, 592.
↑ V656 Variables 'dst_path', 'dst_rel_path' are initialized through the call to the same function. It's probably an error or un-optimized code. Consider inspecting the 'chunk_buffer_acquire()' expression. Check lines: 5439, 5440.