diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-27 06:33:50 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-27 06:33:50 +0000 |
commit | fe39ffb8b90ae4e002ed73fe98617cd590abb467 (patch) | |
tree | b80e5956907d8aeaaffe4e4f0c068c0e6157ce8b /modules/dav/fs/repos.c | |
parent | Initial commit. (diff) | |
download | apache2-upstream/2.4.56.tar.xz apache2-upstream/2.4.56.zip |
Adding upstream version 2.4.56.upstream/2.4.56upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'modules/dav/fs/repos.c')
-rw-r--r-- | modules/dav/fs/repos.c | 2269 |
1 files changed, 2269 insertions, 0 deletions
diff --git a/modules/dav/fs/repos.c b/modules/dav/fs/repos.c new file mode 100644 index 0000000..d38868c --- /dev/null +++ b/modules/dav/fs/repos.c @@ -0,0 +1,2269 @@ +/* Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* +** DAV filesystem-based repository provider +*/ + +#include "apr.h" +#include "apr_file_io.h" +#include "apr_strings.h" +#include "apr_buckets.h" + +#if APR_HAVE_UNISTD_H +#include <unistd.h> /* for getpid() */ +#endif + +#include "httpd.h" +#include "http_log.h" +#include "http_protocol.h" /* for ap_set_* (in dav_fs_set_headers) */ +#include "http_request.h" /* for ap_update_mtime() */ + +#include "mod_dav.h" +#include "repos.h" + + +/* to assist in debugging mod_dav's GET handling */ +#define DEBUG_GET_HANDLER 0 + +#define DAV_FS_COPY_BLOCKSIZE 16384 /* copy 16k at a time */ + +/* context needed to identify a resource */ +struct dav_resource_private { + apr_pool_t *pool; /* memory storage pool associated with request */ + const char *pathname; /* full pathname to resource */ + apr_finfo_t finfo; /* filesystem info */ + request_rec *r; +}; + +/* private context for doing a filesystem walk */ +typedef struct { + /* the input walk parameters */ + const dav_walk_params *params; + + /* reused as we walk */ + dav_walk_resource wres; + + dav_resource res1; + dav_resource_private info1; + dav_buffer path1; + dav_buffer uri_buf; + + /* MOVE/COPY need a secondary path */ + dav_resource res2; + dav_resource_private info2; + dav_buffer path2; + + dav_buffer locknull_buf; + +} dav_fs_walker_context; + +typedef struct { + int is_move; /* is this a MOVE? */ + dav_buffer work_buf; /* handy buffer for copymove_file() */ + + /* CALLBACK: this is a secondary resource managed specially for us */ + const dav_resource *res_dst; + + /* copied from dav_walk_params (they are invariant across the walk) */ + const dav_resource *root; + apr_pool_t *pool; + +} dav_fs_copymove_walk_ctx; + +/* an internal WALKTYPE to walk hidden files (the .DAV directory) */ +#define DAV_WALKTYPE_HIDDEN 0x4000 + +/* an internal WALKTYPE to call collections (again) after their contents */ +#define DAV_WALKTYPE_POSTFIX 0x8000 + +#define DAV_CALLTYPE_POSTFIX 1000 /* a private call type */ + + +/* pull this in from the other source file */ +extern const dav_hooks_locks dav_hooks_locks_fs; + +/* forward-declare the hook structures */ +static const dav_hooks_repository dav_hooks_repository_fs; +static const dav_hooks_liveprop dav_hooks_liveprop_fs; + +/* +** The namespace URIs that we use. This list and the enumeration must +** stay in sync. +*/ +static const char * const dav_fs_namespace_uris[] = +{ + "DAV:", + "http://apache.org/dav/props/", + + NULL /* sentinel */ +}; +enum { + DAV_FS_URI_DAV, /* the DAV: namespace URI */ + DAV_FS_URI_MYPROPS /* the namespace URI for our custom props */ +}; + +/* +** Does this platform support an executable flag? +** +** ### need a way to portably abstract this query +** +** DAV_FINFO_MASK gives the appropriate mask to use for the stat call +** used to get file attributes. +*/ +#ifndef WIN32 +#define DAV_FS_HAS_EXECUTABLE +#define DAV_FINFO_MASK (APR_FINFO_LINK | APR_FINFO_TYPE | APR_FINFO_INODE | \ + APR_FINFO_SIZE | APR_FINFO_CTIME | APR_FINFO_MTIME | \ + APR_FINFO_PROT) +#else +/* as above, but without APR_FINFO_PROT */ +#define DAV_FINFO_MASK (APR_FINFO_LINK | APR_FINFO_TYPE | APR_FINFO_INODE | \ + APR_FINFO_SIZE | APR_FINFO_CTIME | APR_FINFO_MTIME) +#endif + +/* +** The single property that we define (in the DAV_FS_URI_MYPROPS namespace) +*/ +#define DAV_PROPID_FS_executable 1 + +/* + * prefix for temporary files + */ +#define DAV_FS_TMP_PREFIX ".davfs.tmp" + +static const dav_liveprop_spec dav_fs_props[] = +{ + /* standard DAV properties */ + { + DAV_FS_URI_DAV, + "creationdate", + DAV_PROPID_creationdate, + 0 + }, + { + DAV_FS_URI_DAV, + "getcontentlength", + DAV_PROPID_getcontentlength, + 0 + }, + { + DAV_FS_URI_DAV, + "getetag", + DAV_PROPID_getetag, + 0 + }, + { + DAV_FS_URI_DAV, + "getlastmodified", + DAV_PROPID_getlastmodified, + 0 + }, + + /* our custom properties */ + { + DAV_FS_URI_MYPROPS, + "executable", + DAV_PROPID_FS_executable, + 0 /* handled special in dav_fs_is_writable */ + }, + + { 0 } /* sentinel */ +}; + +static const dav_liveprop_group dav_fs_liveprop_group = +{ + dav_fs_props, + dav_fs_namespace_uris, + &dav_hooks_liveprop_fs +}; + + +/* define the dav_stream structure for our use */ +struct dav_stream { + apr_pool_t *p; + apr_file_t *f; + const char *pathname; /* we may need to remove it at close time */ + char *temppath; + int unlink_on_error; +}; + +/* returns an appropriate HTTP status code given an APR status code for a + * failed I/O operation. ### use something besides 500? */ +#define MAP_IO2HTTP(e) (APR_STATUS_IS_ENOSPC(e) ? HTTP_INSUFFICIENT_STORAGE : \ + APR_STATUS_IS_ENOENT(e) ? HTTP_CONFLICT : \ + HTTP_INTERNAL_SERVER_ERROR) + +/* forward declaration for internal treewalkers */ +static dav_error * dav_fs_walk(const dav_walk_params *params, int depth, + dav_response **response); +static dav_error * dav_fs_internal_walk(const dav_walk_params *params, + int depth, int is_move, + const dav_resource *root_dst, + dav_response **response); + +/* -------------------------------------------------------------------- +** +** PRIVATE REPOSITORY FUNCTIONS +*/ +static request_rec *dav_fs_get_request_rec(const dav_resource *resource) +{ + return resource->info->r; +} + +apr_pool_t *dav_fs_pool(const dav_resource *resource) +{ + return resource->info->pool; +} + +const char *dav_fs_pathname(const dav_resource *resource) +{ + return resource->info->pathname; +} + +dav_error * dav_fs_dir_file_name( + const dav_resource *resource, + const char **dirpath_p, + const char **fname_p) +{ + dav_resource_private *ctx = resource->info; + + if (resource->collection) { + *dirpath_p = ctx->pathname; + if (fname_p != NULL) + *fname_p = NULL; + } + else { + const char *testpath, *rootpath; + char *dirpath = ap_make_dirstr_parent(ctx->pool, ctx->pathname); + apr_size_t dirlen = strlen(dirpath); + apr_status_t rv = APR_SUCCESS; + + testpath = dirpath; + if (dirlen > 0) { + rv = apr_filepath_root(&rootpath, &testpath, 0, ctx->pool); + } + + /* remove trailing slash from dirpath, unless it's a root path + */ + if ((rv == APR_SUCCESS && testpath && *testpath) + || rv == APR_ERELATIVE) { + if (dirpath[dirlen - 1] == '/') { + dirpath[dirlen - 1] = '\0'; + } + } + + /* ###: Looks like a response could be appropriate + * + * APR_SUCCESS here tells us the dir is a root + * APR_ERELATIVE told us we had no root (ok) + * APR_EINCOMPLETE an incomplete testpath told us + * there was no -file- name here! + * APR_EBADPATH or other errors tell us this file + * path is undecipherable + */ + + if (rv == APR_SUCCESS || rv == APR_ERELATIVE) { + *dirpath_p = dirpath; + if (fname_p != NULL) + *fname_p = ctx->pathname + dirlen; + } + else { + return dav_new_error(ctx->pool, HTTP_INTERNAL_SERVER_ERROR, 0, rv, + "An incomplete/bad path was found in " + "dav_fs_dir_file_name."); + } + } + + return NULL; +} + +/* Note: picked up from ap_gm_timestr_822() */ +/* NOTE: buf must be at least DAV_TIMEBUF_SIZE chars in size */ +static void dav_format_time(int style, apr_time_t sec, char *buf, apr_size_t buflen) +{ + apr_time_exp_t tms; + + /* ### what to do if fails? */ + (void) apr_time_exp_gmt(&tms, sec); + + if (style == DAV_STYLE_ISO8601) { + /* ### should we use "-00:00" instead of "Z" ?? */ + + /* 20 chars plus null term */ + apr_snprintf(buf, buflen, "%.4d-%.2d-%.2dT%.2d:%.2d:%.2dZ", + tms.tm_year + 1900, tms.tm_mon + 1, tms.tm_mday, + tms.tm_hour, tms.tm_min, tms.tm_sec); + return; + } + + /* RFC 822 date format; as strftime '%a, %d %b %Y %T GMT' */ + + /* 29 chars plus null term */ + apr_snprintf(buf, buflen, "%s, %.2d %s %d %.2d:%.2d:%.2d GMT", + apr_day_snames[tms.tm_wday], + tms.tm_mday, apr_month_snames[tms.tm_mon], + tms.tm_year + 1900, + tms.tm_hour, tms.tm_min, tms.tm_sec); +} + +/* Copy or move src to dst; src_finfo is used to propagate permissions + * bits across if non-NULL; dst_finfo must be non-NULL iff dst already + * exists. */ +static dav_error * dav_fs_copymove_file( + int is_move, + apr_pool_t * p, + const char *src, + const char *dst, + const apr_finfo_t *src_finfo, + const apr_finfo_t *dst_finfo, + dav_buffer *pbuf) +{ + dav_buffer work_buf = { 0 }; + apr_file_t *inf = NULL; + apr_file_t *outf = NULL; + apr_status_t status; + apr_fileperms_t perms; + + if (pbuf == NULL) + pbuf = &work_buf; + + /* Determine permissions to use for destination */ + if (src_finfo && src_finfo->valid & APR_FINFO_PROT + && src_finfo->protection & APR_UEXECUTE) { + perms = src_finfo->protection; + + if (dst_finfo != NULL) { + /* chmod it if it already exist */ + if ((status = apr_file_perms_set(dst, perms)) != APR_SUCCESS) { + return dav_new_error(p, HTTP_INTERNAL_SERVER_ERROR, 0, status, + "Could not set permissions on destination"); + } + } + } + else { + perms = APR_OS_DEFAULT; + } + + dav_set_bufsize(p, pbuf, DAV_FS_COPY_BLOCKSIZE); + + if ((status = apr_file_open(&inf, src, APR_READ | APR_BINARY, + APR_OS_DEFAULT, p)) != APR_SUCCESS) { + /* ### use something besides 500? */ + return dav_new_error(p, HTTP_INTERNAL_SERVER_ERROR, 0, status, + "Could not open file for reading"); + } + + /* ### do we need to deal with the umask? */ + status = apr_file_open(&outf, dst, APR_WRITE | APR_CREATE | APR_TRUNCATE + | APR_BINARY, perms, p); + if (status != APR_SUCCESS) { + apr_file_close(inf); + + return dav_new_error(p, MAP_IO2HTTP(status), 0, status, + "Could not open file for writing"); + } + + while (1) { + apr_size_t len = DAV_FS_COPY_BLOCKSIZE; + + status = apr_file_read(inf, pbuf->buf, &len); + if (status != APR_SUCCESS && status != APR_EOF) { + apr_status_t lcl_status; + + apr_file_close(inf); + apr_file_close(outf); + + if ((lcl_status = apr_file_remove(dst, p)) != APR_SUCCESS) { + /* ### ACK! Inconsistent state... */ + + /* ### use something besides 500? */ + return dav_new_error(p, HTTP_INTERNAL_SERVER_ERROR, 0, + lcl_status, + "Could not delete output after read " + "failure. Server is now in an " + "inconsistent state."); + } + + /* ### use something besides 500? */ + return dav_new_error(p, HTTP_INTERNAL_SERVER_ERROR, 0, status, + "Could not read input file"); + } + + if (status == APR_EOF) + break; + + /* write any bytes that were read */ + status = apr_file_write_full(outf, pbuf->buf, len, NULL); + if (status != APR_SUCCESS) { + apr_status_t lcl_status; + + apr_file_close(inf); + apr_file_close(outf); + + if ((lcl_status = apr_file_remove(dst, p)) != APR_SUCCESS) { + /* ### ACK! Inconsistent state... */ + + /* ### use something besides 500? */ + return dav_new_error(p, HTTP_INTERNAL_SERVER_ERROR, 0, + lcl_status, + "Could not delete output after write " + "failure. Server is now in an " + "inconsistent state."); + } + + return dav_new_error(p, MAP_IO2HTTP(status), 0, status, + "Could not write output file"); + } + } + + apr_file_close(inf); + apr_file_close(outf); + + if (is_move && (status = apr_file_remove(src, p)) != APR_SUCCESS) { + dav_error *err; + apr_status_t lcl_status; + + if (APR_STATUS_IS_ENOENT(status)) { + /* + * Something is wrong here but the result is what we wanted. + * We definitely should not remove the destination file. + */ + err = dav_new_error(p, HTTP_INTERNAL_SERVER_ERROR, 0, status, + apr_psprintf(p, "Could not remove source " + "file %s after move to %s. The " + "server may be in an " + "inconsistent state.", src, dst)); + return err; + } + else if ((lcl_status = apr_file_remove(dst, p)) != APR_SUCCESS) { + /* ### ACK. this creates an inconsistency. do more!? */ + + /* ### use something besides 500? */ + return dav_new_error(p, HTTP_INTERNAL_SERVER_ERROR, 0, lcl_status, + "Could not remove source or destination " + "file. Server is now in an inconsistent " + "state."); + } + + /* ### use something besides 500? */ + err = dav_new_error(p, HTTP_INTERNAL_SERVER_ERROR, 0, status, + "Could not remove source file after move. " + "Destination was removed to ensure consistency."); + return err; + } + + return NULL; +} + +/* copy/move a file from within a state dir to another state dir */ +/* ### need more buffers to replace the pool argument */ +static dav_error * dav_fs_copymove_state( + int is_move, + apr_pool_t * p, + const char *src_dir, const char *src_file, + const char *dst_dir, const char *dst_file, + dav_buffer *pbuf) +{ + apr_finfo_t src_finfo; /* finfo for source file */ + apr_finfo_t dst_state_finfo; /* finfo for STATE directory */ + apr_status_t rv; + const char *src; + const char *dst; + + /* build the propset pathname for the source file */ + src = apr_pstrcat(p, src_dir, "/" DAV_FS_STATE_DIR "/", src_file, NULL); + + /* the source file doesn't exist */ + rv = apr_stat(&src_finfo, src, APR_FINFO_NORM, p); + if (rv != APR_SUCCESS && rv != APR_INCOMPLETE) { + return NULL; + } + + /* build the pathname for the destination state dir */ + dst = apr_pstrcat(p, dst_dir, "/" DAV_FS_STATE_DIR, NULL); + + /* ### do we need to deal with the umask? */ + + /* ensure that it exists */ + rv = apr_dir_make(dst, APR_OS_DEFAULT, p); + if (rv != APR_SUCCESS) { + if (!APR_STATUS_IS_EEXIST(rv)) { + /* ### use something besides 500? */ + return dav_new_error(p, HTTP_INTERNAL_SERVER_ERROR, 0, rv, + "Could not create internal state directory"); + } + } + + /* get info about the state directory */ + rv = apr_stat(&dst_state_finfo, dst, APR_FINFO_NORM, p); + if (rv != APR_SUCCESS && rv != APR_INCOMPLETE) { + /* Ack! Where'd it go? */ + /* ### use something besides 500? */ + return dav_new_error(p, HTTP_INTERNAL_SERVER_ERROR, 0, rv, + "State directory disappeared"); + } + + /* The mkdir() may have failed because a *file* exists there already */ + if (dst_state_finfo.filetype != APR_DIR) { + /* ### try to recover by deleting this file? (and mkdir again) */ + /* ### use something besides 500? */ + return dav_new_error(p, HTTP_INTERNAL_SERVER_ERROR, 0, 0, + "State directory is actually a file"); + } + + /* append the target file to the state directory pathname */ + dst = apr_pstrcat(p, dst, "/", dst_file, NULL); + + /* copy/move the file now */ + if (is_move) { + /* try simple rename first */ + rv = apr_file_rename(src, dst, p); + if (APR_STATUS_IS_EXDEV(rv)) { + return dav_fs_copymove_file(is_move, p, src, dst, NULL, NULL, pbuf); + } + if (rv != APR_SUCCESS) { + /* ### use something besides 500? */ + return dav_new_error(p, HTTP_INTERNAL_SERVER_ERROR, 0, rv, + "Could not move state file."); + } + } + else + { + /* gotta copy (and delete) */ + return dav_fs_copymove_file(is_move, p, src, dst, NULL, NULL, pbuf); + } + + return NULL; +} + +static dav_error *dav_fs_copymoveset(int is_move, apr_pool_t *p, + const dav_resource *src, + const dav_resource *dst, + dav_buffer *pbuf) +{ + const char *src_dir; + const char *src_file; + const char *src_state1; + const char *src_state2; + const char *dst_dir; + const char *dst_file; + const char *dst_state1; + const char *dst_state2; + dav_error *err; + + /* Get directory and filename for resources */ + /* ### should test these result values... */ + (void) dav_fs_dir_file_name(src, &src_dir, &src_file); + (void) dav_fs_dir_file_name(dst, &dst_dir, &dst_file); + + /* Get the corresponding state files for each resource */ + dav_dbm_get_statefiles(p, src_file, &src_state1, &src_state2); + dav_dbm_get_statefiles(p, dst_file, &dst_state1, &dst_state2); +#if DAV_DEBUG + if ((src_state2 != NULL && dst_state2 == NULL) || + (src_state2 == NULL && dst_state2 != NULL)) { + return dav_new_error(p, HTTP_INTERNAL_SERVER_ERROR, 0, 0, + "DESIGN ERROR: dav_dbm_get_statefiles() " + "returned inconsistent results."); + } +#endif + + err = dav_fs_copymove_state(is_move, p, + src_dir, src_state1, + dst_dir, dst_state1, + pbuf); + + if (err == NULL && src_state2 != NULL) { + err = dav_fs_copymove_state(is_move, p, + src_dir, src_state2, + dst_dir, dst_state2, + pbuf); + + if (err != NULL) { + /* ### CRAP. inconsistency. */ + /* ### should perform some cleanup at the target if we still + ### have the original files */ + + /* Change the error to reflect the bad server state. */ + err->status = HTTP_INTERNAL_SERVER_ERROR; + err->desc = + "Could not fully copy/move the properties. " + "The server is now in an inconsistent state."; + } + } + + return err; +} + +static dav_error *dav_fs_deleteset(apr_pool_t *p, const dav_resource *resource) +{ + const char *dirpath; + const char *fname; + const char *state1; + const char *state2; + const char *pathname; + apr_status_t status; + + /* Get directory, filename, and state-file names for the resource */ + /* ### should test this result value... */ + (void) dav_fs_dir_file_name(resource, &dirpath, &fname); + dav_dbm_get_statefiles(p, fname, &state1, &state2); + + /* build the propset pathname for the file */ + pathname = apr_pstrcat(p, + dirpath, + "/" DAV_FS_STATE_DIR "/", + state1, + NULL); + + /* note: we may get ENOENT if the state dir is not present */ + if ((status = apr_file_remove(pathname, p)) != APR_SUCCESS + && !APR_STATUS_IS_ENOENT(status)) { + return dav_new_error(p, HTTP_INTERNAL_SERVER_ERROR, 0, status, + "Could not remove properties."); + } + + if (state2 != NULL) { + /* build the propset pathname for the file */ + pathname = apr_pstrcat(p, + dirpath, + "/" DAV_FS_STATE_DIR "/", + state2, + NULL); + + if ((status = apr_file_remove(pathname, p)) != APR_SUCCESS + && !APR_STATUS_IS_ENOENT(status)) { + /* ### CRAP. only removed half. */ + return dav_new_error(p, HTTP_INTERNAL_SERVER_ERROR, 0, status, + "Could not fully remove properties. " + "The server is now in an inconsistent " + "state."); + } + } + + return NULL; +} + +/* -------------------------------------------------------------------- +** +** REPOSITORY HOOK FUNCTIONS +*/ + +static dav_error * dav_fs_get_resource( + request_rec *r, + const char *root_dir, + const char *label, + int use_checked_in, + dav_resource **result_resource) +{ + dav_resource_private *ctx; + dav_resource *resource; + char *s; + char *filename; + apr_size_t len; + + /* ### optimize this into a single allocation! */ + + /* Create private resource context descriptor */ + ctx = apr_pcalloc(r->pool, sizeof(*ctx)); + ctx->finfo = r->finfo; + ctx->r = r; + + /* ### this should go away */ + ctx->pool = r->pool; + + /* Preserve case on OSes which fold canonical filenames */ +#if 0 + /* ### not available in Apache 2.0 yet */ + filename = r->case_preserved_filename; +#else + filename = r->filename; +#endif + + /* + ** If there is anything in the path_info, then this indicates that the + ** entire path was not used to specify the file/dir. We want to append + ** it onto the filename so that we get a "valid" pathname for null + ** resources. + */ + s = apr_pstrcat(r->pool, filename, r->path_info, NULL); + + /* make sure the pathname does not have a trailing "/" */ + len = strlen(s); + if (len > 1 && s[len - 1] == '/') { + s[len - 1] = '\0'; + } + ctx->pathname = s; + + /* Create resource descriptor */ + resource = apr_pcalloc(r->pool, sizeof(*resource)); + resource->type = DAV_RESOURCE_TYPE_REGULAR; + resource->info = ctx; + resource->hooks = &dav_hooks_repository_fs; + resource->pool = r->pool; + + /* make sure the URI does not have a trailing "/" */ + len = strlen(r->uri); + if (len > 1 && r->uri[len - 1] == '/') { + s = apr_pstrmemdup(r->pool, r->uri, len-1); + resource->uri = s; + } + else { + resource->uri = r->uri; + } + + if (r->finfo.filetype != APR_NOFILE) { + resource->exists = 1; + resource->collection = r->finfo.filetype == APR_DIR; + + /* unused info in the URL will indicate a null resource */ + + if (r->path_info != NULL && *r->path_info != '\0') { + if (resource->collection) { + /* only a trailing "/" is allowed */ + if (*r->path_info != '/' || r->path_info[1] != '\0') { + + /* + ** This URL/filename represents a locknull resource or + ** possibly a destination of a MOVE/COPY + */ + resource->exists = 0; + resource->collection = 0; + } + } + else + { + /* + ** The base of the path refers to a file -- nothing should + ** be in path_info. The resource is simply an error: it + ** can't be a null or a locknull resource. + */ + return dav_new_error(r->pool, HTTP_BAD_REQUEST, 0, 0, + "The URL contains extraneous path " + "components. The resource could not " + "be identified."); + } + + /* retain proper integrity across the structures */ + if (!resource->exists) { + ctx->finfo.filetype = APR_NOFILE; + } + } + } + + *result_resource = resource; + return NULL; +} + +static dav_error * dav_fs_get_parent_resource(const dav_resource *resource, + dav_resource **result_parent) +{ + dav_resource_private *ctx = resource->info; + dav_resource_private *parent_ctx; + dav_resource *parent_resource; + apr_status_t rv; + char *dirpath; + const char *testroot; + const char *testpath; + + /* If we're at the root of the URL space, then there is no parent. */ + if (strcmp(resource->uri, "/") == 0) { + *result_parent = NULL; + return NULL; + } + + /* If given resource is root, then there is no parent. + * Unless we can retrieve the filepath root, this is + * intendend to fail. If we split the root and + * no path info remains, then we also fail. + */ + testpath = ctx->pathname; + rv = apr_filepath_root(&testroot, &testpath, 0, ctx->pool); + if ((rv != APR_SUCCESS && rv != APR_ERELATIVE) + || !testpath || !*testpath) { + *result_parent = NULL; + return NULL; + } + + /* ### optimize this into a single allocation! */ + + /* Create private resource context descriptor */ + parent_ctx = apr_pcalloc(ctx->pool, sizeof(*parent_ctx)); + + /* ### this should go away */ + parent_ctx->pool = ctx->pool; + + dirpath = ap_make_dirstr_parent(ctx->pool, ctx->pathname); + if (strlen(dirpath) > 1 && dirpath[strlen(dirpath) - 1] == '/') + dirpath[strlen(dirpath) - 1] = '\0'; + parent_ctx->pathname = dirpath; + + parent_resource = apr_pcalloc(ctx->pool, sizeof(*parent_resource)); + parent_resource->info = parent_ctx; + parent_resource->collection = 1; + parent_resource->hooks = &dav_hooks_repository_fs; + parent_resource->pool = resource->pool; + + if (resource->uri != NULL) { + char *uri = ap_make_dirstr_parent(ctx->pool, resource->uri); + if (strlen(uri) > 1 && uri[strlen(uri) - 1] == '/') + uri[strlen(uri) - 1] = '\0'; + parent_resource->uri = uri; + } + + rv = apr_stat(&parent_ctx->finfo, parent_ctx->pathname, + APR_FINFO_NORM, ctx->pool); + if (rv == APR_SUCCESS || rv == APR_INCOMPLETE) { + parent_resource->exists = 1; + } + + *result_parent = parent_resource; + return NULL; +} + +static int dav_fs_is_same_resource( + const dav_resource *res1, + const dav_resource *res2) +{ + dav_resource_private *ctx1 = res1->info; + dav_resource_private *ctx2 = res2->info; + + if (res1->hooks != res2->hooks) + return 0; + + if ((ctx1->finfo.filetype != APR_NOFILE) && (ctx2->finfo.filetype != APR_NOFILE) + && (ctx1->finfo.valid & ctx2->finfo.valid & APR_FINFO_INODE)) { + return ctx1->finfo.inode == ctx2->finfo.inode; + } + else { + return strcmp(ctx1->pathname, ctx2->pathname) == 0; + } +} + +static int dav_fs_is_parent_resource( + const dav_resource *res1, + const dav_resource *res2) +{ + dav_resource_private *ctx1 = res1->info; + dav_resource_private *ctx2 = res2->info; + apr_size_t len1 = strlen(ctx1->pathname); + apr_size_t len2; + + if (res1->hooks != res2->hooks) + return 0; + + /* it is safe to use ctx2 now */ + len2 = strlen(ctx2->pathname); + + return (len2 > len1 + && memcmp(ctx1->pathname, ctx2->pathname, len1) == 0 + && ctx2->pathname[len1] == '/'); +} + +static apr_status_t tmpfile_cleanup(void *data) +{ + dav_stream *ds = data; + if (ds->temppath) { + apr_file_remove(ds->temppath, ds->p); + } + return APR_SUCCESS; +} + +/* custom mktemp that creates the file with APR_OS_DEFAULT permissions */ +static apr_status_t dav_fs_mktemp(apr_file_t **fp, char *templ, apr_pool_t *p) +{ + apr_status_t rv; + int num = ((getpid() << 7) + (apr_uintptr_t)templ % (1 << 16) ) % + ( 1 << 23 ) ; + char *numstr = templ + strlen(templ) - 6; + + ap_assert(numstr >= templ); + + do { + num = (num + 1) % ( 1 << 23 ); + apr_snprintf(numstr, 7, "%06x", num); + rv = apr_file_open(fp, templ, + APR_WRITE | APR_CREATE | APR_BINARY | APR_EXCL, + APR_OS_DEFAULT, p); + } while (APR_STATUS_IS_EEXIST(rv)); + + return rv; +} + +static dav_error * dav_fs_open_stream(const dav_resource *resource, + dav_stream_mode mode, + dav_stream **stream) +{ + apr_pool_t *p = resource->info->pool; + dav_stream *ds = apr_pcalloc(p, sizeof(*ds)); + apr_int32_t flags; + apr_status_t rv; + + switch (mode) { + default: + flags = APR_READ | APR_BINARY; + break; + + case DAV_MODE_WRITE_TRUNC: + flags = APR_WRITE | APR_CREATE | APR_TRUNCATE | APR_BINARY; + break; + case DAV_MODE_WRITE_SEEKABLE: + flags = APR_WRITE | APR_CREATE | APR_BINARY; + break; + } + + ds->p = p; + ds->pathname = resource->info->pathname; + ds->temppath = NULL; + ds->unlink_on_error = 0; + + if (mode == DAV_MODE_WRITE_TRUNC) { + ds->temppath = apr_pstrcat(p, ap_make_dirstr_parent(p, ds->pathname), + DAV_FS_TMP_PREFIX "XXXXXX", NULL); + rv = dav_fs_mktemp(&ds->f, ds->temppath, ds->p); + apr_pool_cleanup_register(p, ds, tmpfile_cleanup, + apr_pool_cleanup_null); + } + else if (mode == DAV_MODE_WRITE_SEEKABLE) { + rv = apr_file_open(&ds->f, ds->pathname, flags | APR_FOPEN_EXCL, + APR_OS_DEFAULT, ds->p); + if (rv == APR_SUCCESS) { + /* we have created a new file */ + ds->unlink_on_error = 1; + } + else if (APR_STATUS_IS_EEXIST(rv)) { + rv = apr_file_open(&ds->f, ds->pathname, flags, APR_OS_DEFAULT, + ds->p); + if (rv != APR_SUCCESS) { + return dav_new_error(p, MAP_IO2HTTP(rv), 0, rv, + apr_psprintf(p, "Could not open an existing " + "resource for writing: %s.", + ds->pathname)); + } + } + } + else { + rv = apr_file_open(&ds->f, ds->pathname, flags, APR_OS_DEFAULT, ds->p); + if (rv != APR_SUCCESS) { + return dav_new_error(p, MAP_IO2HTTP(rv), 0, rv, + apr_psprintf(p, "Could not open an existing " + "resource for reading: %s.", + ds->pathname)); + } + } + + if (rv != APR_SUCCESS) { + return dav_new_error(p, MAP_IO2HTTP(rv), 0, rv, + apr_psprintf(p, "An error occurred while opening " + "a resource for writing: %s.", + ds->pathname)); + } + + /* (APR registers cleanups for the fd with the pool) */ + + *stream = ds; + return NULL; +} + +static dav_error * dav_fs_close_stream(dav_stream *stream, int commit) +{ + apr_status_t rv; + + apr_file_close(stream->f); + + if (!commit) { + if (stream->temppath) { + apr_pool_cleanup_run(stream->p, stream, tmpfile_cleanup); + } + else if (stream->unlink_on_error) { + if ((rv = apr_file_remove(stream->pathname, stream->p)) + != APR_SUCCESS) { + /* ### use a better description? */ + return dav_new_error(stream->p, HTTP_INTERNAL_SERVER_ERROR, 0, + rv, + "There was a problem removing (rolling " + "back) the resource " + "when it was being closed."); + } + } + } + else if (stream->temppath) { + rv = apr_file_rename(stream->temppath, stream->pathname, stream->p); + if (rv) { + return dav_new_error(stream->p, HTTP_INTERNAL_SERVER_ERROR, 0, rv, + "There was a problem writing the file " + "atomically after writes."); + } + apr_pool_cleanup_kill(stream->p, stream, tmpfile_cleanup); + } + + return NULL; +} + +static dav_error * dav_fs_write_stream(dav_stream *stream, + const void *buf, apr_size_t bufsize) +{ + apr_status_t status; + + status = apr_file_write_full(stream->f, buf, bufsize, NULL); + if (APR_STATUS_IS_ENOSPC(status)) { + return dav_new_error(stream->p, HTTP_INSUFFICIENT_STORAGE, 0, status, + "There is not enough storage to write to " + "this resource."); + } + else if (status != APR_SUCCESS) { + /* ### use something besides 500? */ + return dav_new_error(stream->p, HTTP_INTERNAL_SERVER_ERROR, 0, status, + "An error occurred while writing to a " + "resource."); + } + return NULL; +} + +static dav_error * dav_fs_seek_stream(dav_stream *stream, apr_off_t abs_pos) +{ + apr_status_t status; + + if ((status = apr_file_seek(stream->f, APR_SET, &abs_pos)) + != APR_SUCCESS) { + /* ### should check whether apr_file_seek set abs_pos was set to the + * correct position? */ + /* ### use something besides 500? */ + return dav_new_error(stream->p, HTTP_INTERNAL_SERVER_ERROR, 0, status, + "Could not seek to specified position in the " + "resource."); + } + return NULL; +} + + +#if DEBUG_GET_HANDLER + +/* only define set_headers() and deliver() for debug purposes */ + + +static dav_error * dav_fs_set_headers(request_rec *r, + const dav_resource *resource) +{ + /* ### this function isn't really used since we have a get_pathname */ + if (!resource->exists) + return NULL; + + /* make sure the proper mtime is in the request record */ + ap_update_mtime(r, resource->info->finfo.mtime); + + /* ### note that these use r->filename rather than <resource> */ + ap_set_last_modified(r); + ap_set_etag(r); + + /* we accept byte-ranges */ + ap_set_accept_ranges(r); + + /* set up the Content-Length header */ + ap_set_content_length(r, resource->info->finfo.size); + + /* ### how to set the content type? */ + /* ### until this is resolved, the Content-Type header is busted */ + + return NULL; +} + +static dav_error * dav_fs_deliver(const dav_resource *resource, + ap_filter_t *output) +{ + apr_pool_t *pool = resource->pool; + apr_bucket_brigade *bb; + apr_file_t *fd; + apr_status_t status; + apr_bucket *bkt; + + /* Check resource type */ + if (resource->type != DAV_RESOURCE_TYPE_REGULAR + && resource->type != DAV_RESOURCE_TYPE_VERSION + && resource->type != DAV_RESOURCE_TYPE_WORKING) { + return dav_new_error(pool, HTTP_CONFLICT, 0, 0, + "Cannot GET this type of resource."); + } + if (resource->collection) { + return dav_new_error(pool, HTTP_CONFLICT, 0, 0, + "There is no default response to GET for a " + "collection."); + } + + if ((status = apr_file_open(&fd, resource->info->pathname, + APR_READ | APR_BINARY, 0, + pool)) != APR_SUCCESS) { + return dav_new_error(pool, HTTP_FORBIDDEN, 0, status, + "File permissions deny server access."); + } + + bb = apr_brigade_create(pool, output->c->bucket_alloc); + + apr_brigade_insert_file(bb, fd, 0, resource->info->finfo.size, pool); + + bkt = apr_bucket_eos_create(output->c->bucket_alloc); + APR_BRIGADE_INSERT_TAIL(bb, bkt); + + if ((status = ap_pass_brigade(output, bb)) != APR_SUCCESS) { + return dav_new_error(pool, AP_FILTER_ERROR, 0, status, + "Could not write contents to filter."); + } + + return NULL; +} + +#endif /* DEBUG_GET_HANDLER */ + + +static dav_error * dav_fs_create_collection(dav_resource *resource) +{ + dav_resource_private *ctx = resource->info; + apr_status_t status; + + status = apr_dir_make(ctx->pathname, APR_OS_DEFAULT, ctx->pool); + if (APR_STATUS_IS_ENOSPC(status)) { + return dav_new_error(ctx->pool, HTTP_INSUFFICIENT_STORAGE, 0, status, + "There is not enough storage to create " + "this collection."); + } + else if (APR_STATUS_IS_ENOENT(status)) { + return dav_new_error(ctx->pool, HTTP_CONFLICT, 0, status, + "Cannot create collection; intermediate " + "collection does not exist."); + } + else if (status != APR_SUCCESS) { + /* ### refine this error message? */ + return dav_new_error(ctx->pool, HTTP_FORBIDDEN, 0, status, + "Unable to create collection."); + } + + /* update resource state to show it exists as a collection */ + resource->exists = 1; + resource->collection = 1; + + return NULL; +} + +static dav_error * dav_fs_copymove_walker(dav_walk_resource *wres, + int calltype) +{ + apr_status_t status; + dav_fs_copymove_walk_ctx *ctx = wres->walk_ctx; + dav_resource_private *srcinfo = wres->resource->info; + dav_resource_private *dstinfo = ctx->res_dst->info; + dav_error *err = NULL; + + if (wres->resource->collection) { + if (calltype == DAV_CALLTYPE_POSTFIX) { + /* Postfix call for MOVE. delete the source dir. + * Note: when copying, we do not enable the postfix-traversal. + */ + /* ### we are ignoring any error here; what should we do? */ + (void) apr_dir_remove(srcinfo->pathname, ctx->pool); + } + else { + /* copy/move of a collection. Create the new, target collection */ + if ((status = apr_dir_make(dstinfo->pathname, APR_OS_DEFAULT, + ctx->pool)) != APR_SUCCESS) { + /* ### assume it was a permissions problem */ + /* ### need a description here */ + err = dav_new_error(ctx->pool, HTTP_FORBIDDEN, 0, status, NULL); + } + } + } + else { + err = dav_fs_copymove_file(ctx->is_move, ctx->pool, + srcinfo->pathname, dstinfo->pathname, + &srcinfo->finfo, + ctx->res_dst->exists ? &dstinfo->finfo : NULL, + &ctx->work_buf); + /* ### push a higher-level description? */ + } + + /* + ** If we have a "not so bad" error, then it might need to go into a + ** multistatus response. + ** + ** For a MOVE, it will always go into the multistatus. It could be + ** that everything has been moved *except* for the root. Using a + ** multistatus (with no errors for the other resources) will signify + ** this condition. + ** + ** For a COPY, we are traversing in a prefix fashion. If the root fails, + ** then we can just bail out now. + */ + if (err != NULL + && !ap_is_HTTP_SERVER_ERROR(err->status) + && (ctx->is_move + || !dav_fs_is_same_resource(wres->resource, ctx->root))) { + /* ### use errno to generate DAV:responsedescription? */ + dav_add_response(wres, err->status, NULL); + + /* the error is in the multistatus now. do not stop the traversal. */ + return NULL; + } + + return err; +} + +static dav_error *dav_fs_copymove_resource( + int is_move, + const dav_resource *src, + const dav_resource *dst, + int depth, + dav_response **response) +{ + dav_error *err = NULL; + dav_buffer work_buf = { 0 }; + + *response = NULL; + + /* if a collection, recursively copy/move it and its children, + * including the state dirs + */ + if (src->collection) { + dav_walk_params params = { 0 }; + dav_response *multi_status; + + params.walk_type = DAV_WALKTYPE_NORMAL | DAV_WALKTYPE_HIDDEN; + params.func = dav_fs_copymove_walker; + params.pool = src->info->pool; + params.root = src; + + /* params.walk_ctx is managed by dav_fs_internal_walk() */ + + /* postfix is needed for MOVE to delete source dirs */ + if (is_move) + params.walk_type |= DAV_WALKTYPE_POSTFIX; + + /* note that we return the error OR the multistatus. never both */ + + if ((err = dav_fs_internal_walk(¶ms, depth, is_move, dst, + &multi_status)) != NULL) { + /* on a "real" error, then just punt. nothing else to do. */ + return err; + } + + if ((*response = multi_status) != NULL) { + /* some multistatus responses exist. wrap them in a 207 */ + return dav_new_error(src->info->pool, HTTP_MULTI_STATUS, 0, 0, + "Error(s) occurred on some resources during " + "the COPY/MOVE process."); + } + + return NULL; + } + + /* not a collection */ + if ((err = dav_fs_copymove_file(is_move, src->info->pool, + src->info->pathname, dst->info->pathname, + &src->info->finfo, + dst->exists ? &dst->info->finfo : NULL, + &work_buf)) != NULL) { + /* ### push a higher-level description? */ + return err; + } + + /* copy/move properties as well */ + return dav_fs_copymoveset(is_move, src->info->pool, src, dst, &work_buf); +} + +static dav_error * dav_fs_copy_resource( + const dav_resource *src, + dav_resource *dst, + int depth, + dav_response **response) +{ + dav_error *err; + +#if DAV_DEBUG + if (src->hooks != dst->hooks) { + /* + ** ### strictly speaking, this is a design error; we should not + ** ### have reached this point. + */ + return dav_new_error(src->info->pool, HTTP_INTERNAL_SERVER_ERROR, 0, 0, + "DESIGN ERROR: a mix of repositories " + "was passed to copy_resource."); + } +#endif + + if ((err = dav_fs_copymove_resource(0, src, dst, depth, + response)) == NULL) { + + /* update state of destination resource to show it exists */ + dst->exists = 1; + dst->collection = src->collection; + } + + return err; +} + +static dav_error * dav_fs_move_resource( + dav_resource *src, + dav_resource *dst, + dav_response **response) +{ + dav_resource_private *srcinfo = src->info; + dav_resource_private *dstinfo = dst->info; + dav_error *err; + apr_status_t rv; + +#if DAV_DEBUG + if (src->hooks != dst->hooks) { + /* + ** ### strictly speaking, this is a design error; we should not + ** ### have reached this point. + */ + return dav_new_error(src->info->pool, HTTP_INTERNAL_SERVER_ERROR, 0, 0, + "DESIGN ERROR: a mix of repositories " + "was passed to move_resource."); + } +#endif + + + /* try rename first */ + rv = apr_file_rename(srcinfo->pathname, dstinfo->pathname, srcinfo->pool); + + /* if we can't simply rename, then do it the hard way... */ + if (APR_STATUS_IS_EXDEV(rv)) { + if ((err = dav_fs_copymove_resource(1, src, dst, DAV_INFINITY, + response)) == NULL) { + /* update resource states */ + dst->exists = 1; + dst->collection = src->collection; + src->exists = 0; + src->collection = 0; + } + + return err; + } + + /* no multistatus response */ + *response = NULL; + + if (rv != APR_SUCCESS) { + /* ### should have a better error than this. */ + return dav_new_error(srcinfo->pool, HTTP_INTERNAL_SERVER_ERROR, 0, rv, + "Could not rename resource."); + } + + /* Rename did work. Update resource states and move properties as well */ + dst->exists = 1; + dst->collection = src->collection; + src->exists = 0; + src->collection = 0; + + if ((err = dav_fs_copymoveset(1, src->info->pool, + src, dst, NULL)) == NULL) { + /* no error. we're done. go ahead and return now. */ + return NULL; + } + + /* error occurred during properties move; try to put resource back */ + if (apr_file_rename(dstinfo->pathname, srcinfo->pathname, + srcinfo->pool) != APR_SUCCESS) { + /* couldn't put it back! */ + return dav_push_error(srcinfo->pool, + HTTP_INTERNAL_SERVER_ERROR, 0, + "The resource was moved, but a failure " + "occurred during the move of its " + "properties. The resource could not be " + "restored to its original location. The " + "server is now in an inconsistent state.", + err); + } + + /* update resource states again */ + src->exists = 1; + src->collection = dst->collection; + dst->exists = 0; + dst->collection = 0; + + /* resource moved back, but properties may be inconsistent */ + return dav_push_error(srcinfo->pool, + HTTP_INTERNAL_SERVER_ERROR, 0, + "The resource was moved, but a failure " + "occurred during the move of its properties. " + "The resource was moved back to its original " + "location, but its properties may have been " + "partially moved. The server may be in an " + "inconsistent state.", + err); +} + +static dav_error * dav_fs_delete_walker(dav_walk_resource *wres, int calltype) +{ + dav_resource_private *info = wres->resource->info; + + /* do not attempt to remove a null resource, + * or a collection with children + */ + if (wres->resource->exists && + (!wres->resource->collection || calltype == DAV_CALLTYPE_POSTFIX)) { + /* try to remove the resource */ + apr_status_t result; + + result = wres->resource->collection + ? apr_dir_remove(info->pathname, wres->pool) + : apr_file_remove(info->pathname, wres->pool); + + /* + ** If an error occurred, then add it to multistatus response. + ** Note that we add it for the root resource, too. It is quite + ** possible to delete the whole darn tree, yet fail on the root. + ** + ** (also: remember we are deleting via a postfix traversal) + */ + if (result != APR_SUCCESS) { + /* ### assume there is a permissions problem */ + + /* ### use errno to generate DAV:responsedescription? */ + dav_add_response(wres, HTTP_FORBIDDEN, NULL); + } + } + + return NULL; +} + +static dav_error * dav_fs_remove_resource(dav_resource *resource, + dav_response **response) +{ + apr_status_t status; + dav_resource_private *info = resource->info; + + *response = NULL; + + /* if a collection, recursively remove it and its children, + * including the state dirs + */ + if (resource->collection) { + dav_walk_params params = { 0 }; + dav_error *err = NULL; + dav_response *multi_status; + + params.walk_type = (DAV_WALKTYPE_NORMAL + | DAV_WALKTYPE_HIDDEN + | DAV_WALKTYPE_POSTFIX); + params.func = dav_fs_delete_walker; + params.pool = info->pool; + params.root = resource; + + if ((err = dav_fs_walk(¶ms, DAV_INFINITY, + &multi_status)) != NULL) { + /* on a "real" error, then just punt. nothing else to do. */ + return err; + } + + if ((*response = multi_status) != NULL) { + /* some multistatus responses exist. wrap them in a 207 */ + return dav_new_error(info->pool, HTTP_MULTI_STATUS, 0, 0, + "Error(s) occurred on some resources during " + "the deletion process."); + } + + /* no errors... update resource state */ + resource->exists = 0; + resource->collection = 0; + + return NULL; + } + + /* not a collection; remove the file and its properties */ + if ((status = apr_file_remove(info->pathname, info->pool)) != APR_SUCCESS) { + /* ### put a description in here */ + return dav_new_error(info->pool, HTTP_FORBIDDEN, 0, status, NULL); + } + + /* update resource state */ + resource->exists = 0; + resource->collection = 0; + + /* remove properties and return its result */ + return dav_fs_deleteset(info->pool, resource); +} + +/* ### move this to dav_util? */ +/* Walk recursively down through directories, * + * including lock-null resources as we go. */ +static dav_error * dav_fs_walker(dav_fs_walker_context *fsctx, int depth) +{ + const dav_walk_params *params = fsctx->params; + apr_pool_t *pool = params->pool; + apr_status_t status; + dav_error *err = NULL; + int isdir = fsctx->res1.collection; + apr_finfo_t dirent; + apr_dir_t *dirp; + + /* ensure the context is prepared properly, then call the func */ + err = (*params->func)(&fsctx->wres, + isdir + ? DAV_CALLTYPE_COLLECTION + : DAV_CALLTYPE_MEMBER); + if (err != NULL) { + return err; + } + + if (depth == 0 || !isdir) { + return NULL; + } + + /* put a trailing slash onto the directory, in preparation for appending + * files to it as we discovery them within the directory */ + dav_check_bufsize(pool, &fsctx->path1, DAV_BUFFER_PAD); + fsctx->path1.buf[fsctx->path1.cur_len++] = '/'; + fsctx->path1.buf[fsctx->path1.cur_len] = '\0'; /* in pad area */ + + /* if a secondary path is present, then do that, too */ + if (fsctx->path2.buf != NULL) { + dav_check_bufsize(pool, &fsctx->path2, DAV_BUFFER_PAD); + fsctx->path2.buf[fsctx->path2.cur_len++] = '/'; + fsctx->path2.buf[fsctx->path2.cur_len] = '\0'; /* in pad area */ + } + + /* Note: the URI should ALREADY have a trailing "/" */ + + /* for this first pass of files, all resources exist */ + fsctx->res1.exists = 1; + + /* a file is the default; we'll adjust if we hit a directory */ + fsctx->res1.collection = 0; + fsctx->res2.collection = 0; + + /* open and scan the directory */ + if ((status = apr_dir_open(&dirp, fsctx->path1.buf, pool)) != APR_SUCCESS) { + /* ### need a better error */ + return dav_new_error(pool, HTTP_NOT_FOUND, 0, status, NULL); + } + while ((apr_dir_read(&dirent, APR_FINFO_DIRENT, dirp)) == APR_SUCCESS) { + apr_size_t len; + + len = strlen(dirent.name); + + /* avoid recursing into our current, parent, or state directories */ + if (dirent.name[0] == '.' + && (len == 1 || (dirent.name[1] == '.' && len == 2))) { + continue; + } + + if (params->walk_type & DAV_WALKTYPE_AUTH) { + /* ### need to authorize each file */ + /* ### example: .htaccess is normally configured to fail auth */ + + /* stuff in the state directory and temp files are never authorized! */ + if (!strcmp(dirent.name, DAV_FS_STATE_DIR) || + !strncmp(dirent.name, DAV_FS_TMP_PREFIX, + strlen(DAV_FS_TMP_PREFIX))) { + continue; + } + } + /* skip the state dir and temp files unless a HIDDEN is performed */ + if (!(params->walk_type & DAV_WALKTYPE_HIDDEN) + && (!strcmp(dirent.name, DAV_FS_STATE_DIR) || + !strncmp(dirent.name, DAV_FS_TMP_PREFIX, + strlen(DAV_FS_TMP_PREFIX)))) { + continue; + } + + /* append this file onto the path buffer (copy null term) */ + dav_buffer_place_mem(pool, &fsctx->path1, dirent.name, len + 1, 0); + + status = apr_stat(&fsctx->info1.finfo, fsctx->path1.buf, + DAV_FINFO_MASK, pool); + if (status != APR_SUCCESS && status != APR_INCOMPLETE) { + /* woah! where'd it go? */ + /* ### should have a better error here */ + err = dav_new_error(pool, HTTP_NOT_FOUND, 0, status, NULL); + break; + } + + /* copy the file to the URI, too. NOTE: we will pad an extra byte + for the trailing slash later. */ + dav_buffer_place_mem(pool, &fsctx->uri_buf, dirent.name, len + 1, 1); + + /* if there is a secondary path, then do that, too */ + if (fsctx->path2.buf != NULL) { + dav_buffer_place_mem(pool, &fsctx->path2, dirent.name, len + 1, 0); + } + + /* set up the (internal) pathnames for the two resources */ + fsctx->info1.pathname = fsctx->path1.buf; + fsctx->info2.pathname = fsctx->path2.buf; + + /* set up the URI for the current resource */ + fsctx->res1.uri = fsctx->uri_buf.buf; + + /* ### for now, only process regular files (e.g. skip symlinks) */ + if (fsctx->info1.finfo.filetype == APR_REG) { + /* call the function for the specified dir + file */ + if ((err = (*params->func)(&fsctx->wres, + DAV_CALLTYPE_MEMBER)) != NULL) { + /* ### maybe add a higher-level description? */ + break; + } + } + else if (fsctx->info1.finfo.filetype == APR_DIR) { + apr_size_t save_path_len = fsctx->path1.cur_len; + apr_size_t save_uri_len = fsctx->uri_buf.cur_len; + apr_size_t save_path2_len = fsctx->path2.cur_len; + + /* adjust length to incorporate the subdir name */ + fsctx->path1.cur_len += len; + fsctx->path2.cur_len += len; + + /* adjust URI length to incorporate subdir and a slash */ + fsctx->uri_buf.cur_len += len + 1; + fsctx->uri_buf.buf[fsctx->uri_buf.cur_len - 1] = '/'; + fsctx->uri_buf.buf[fsctx->uri_buf.cur_len] = '\0'; + + /* switch over to a collection */ + fsctx->res1.collection = 1; + fsctx->res2.collection = 1; + + /* recurse on the subdir */ + /* ### don't always want to quit on error from single child */ + if ((err = dav_fs_walker(fsctx, depth - 1)) != NULL) { + /* ### maybe add a higher-level description? */ + break; + } + + /* put the various information back */ + fsctx->path1.cur_len = save_path_len; + fsctx->path2.cur_len = save_path2_len; + fsctx->uri_buf.cur_len = save_uri_len; + + fsctx->res1.collection = 0; + fsctx->res2.collection = 0; + + /* assert: res1.exists == 1 */ + } + } + + /* ### check the return value of this? */ + apr_dir_close(dirp); + + if (err != NULL) + return err; + + if (params->walk_type & DAV_WALKTYPE_LOCKNULL) { + apr_size_t offset = 0; + + /* null terminate the directory name */ + fsctx->path1.buf[fsctx->path1.cur_len - 1] = '\0'; + + /* Include any lock null resources found in this collection */ + fsctx->res1.collection = 1; + if ((err = dav_fs_get_locknull_members(&fsctx->res1, + &fsctx->locknull_buf)) != NULL) { + /* ### maybe add a higher-level description? */ + return err; + } + + /* put a slash back on the end of the directory */ + fsctx->path1.buf[fsctx->path1.cur_len - 1] = '/'; + + /* these are all non-existent (files) */ + fsctx->res1.exists = 0; + fsctx->res1.collection = 0; + memset(&fsctx->info1.finfo, 0, sizeof(fsctx->info1.finfo)); + + while (offset < fsctx->locknull_buf.cur_len) { + apr_size_t len = strlen(fsctx->locknull_buf.buf + offset); + dav_lock *locks = NULL; + + /* + ** Append the locknull file to the paths and the URI. Note that + ** we don't have to pad the URI for a slash since a locknull + ** resource is not a collection. + */ + dav_buffer_place_mem(pool, &fsctx->path1, + fsctx->locknull_buf.buf + offset, len + 1, 0); + dav_buffer_place_mem(pool, &fsctx->uri_buf, + fsctx->locknull_buf.buf + offset, len + 1, 0); + if (fsctx->path2.buf != NULL) { + dav_buffer_place_mem(pool, &fsctx->path2, + fsctx->locknull_buf.buf + offset, + len + 1, 0); + } + + /* set up the (internal) pathnames for the two resources */ + fsctx->info1.pathname = fsctx->path1.buf; + fsctx->info2.pathname = fsctx->path2.buf; + + /* set up the URI for the current resource */ + fsctx->res1.uri = fsctx->uri_buf.buf; + + /* + ** To prevent a PROPFIND showing an expired locknull + ** resource, query the lock database to force removal + ** of both the lock entry and .locknull, if necessary.. + ** Sure, the query in PROPFIND would do this.. after + ** the locknull resource was already included in the + ** return. + ** + ** NOTE: we assume the caller has opened the lock database + ** if they have provided DAV_WALKTYPE_LOCKNULL. + */ + /* ### we should also look into opening it read-only and + ### eliding timed-out items from the walk, yet leaving + ### them in the locknull database until somebody opens + ### the thing writable. + */ + /* ### probably ought to use has_locks. note the problem + ### mentioned above, though... we would traverse this as + ### a locknull, but then a PROPFIND would load the lock + ### info, causing a timeout and the locks would not be + ### reported. Therefore, a null resource would be returned + ### in the PROPFIND. + ### + ### alternative: just load unresolved locks. any direct + ### locks will be timed out (correct). any indirect will + ### not (correct; consider if a parent timed out -- the + ### timeout routines do not walk and remove indirects; + ### even the resolve func would probably fail when it + ### tried to find a timed-out direct lock). + */ + if ((err = dav_lock_query(params->lockdb, &fsctx->res1, + &locks)) != NULL) { + /* ### maybe add a higher-level description? */ + return err; + } + + /* call the function for the specified dir + file */ + if (locks != NULL && + (err = (*params->func)(&fsctx->wres, + DAV_CALLTYPE_LOCKNULL)) != NULL) { + /* ### maybe add a higher-level description? */ + return err; + } + + offset += len + 1; + } + + /* reset the exists flag */ + fsctx->res1.exists = 1; + } + + if (params->walk_type & DAV_WALKTYPE_POSTFIX) { + /* replace the dirs' trailing slashes with null terms */ + fsctx->path1.buf[--fsctx->path1.cur_len] = '\0'; + fsctx->uri_buf.buf[--fsctx->uri_buf.cur_len] = '\0'; + if (fsctx->path2.buf != NULL) { + fsctx->path2.buf[--fsctx->path2.cur_len] = '\0'; + } + + /* this is a collection which exists */ + fsctx->res1.collection = 1; + + return (*params->func)(&fsctx->wres, DAV_CALLTYPE_POSTFIX); + } + + return NULL; +} + +static dav_error * dav_fs_internal_walk(const dav_walk_params *params, + int depth, int is_move, + const dav_resource *root_dst, + dav_response **response) +{ + dav_fs_walker_context fsctx = { 0 }; + dav_error *err; + dav_fs_copymove_walk_ctx cm_ctx = { 0 }; + +#if DAV_DEBUG + if ((params->walk_type & DAV_WALKTYPE_LOCKNULL) != 0 + && params->lockdb == NULL) { + return dav_new_error(params->pool, HTTP_INTERNAL_SERVER_ERROR, 0, 0, + "DESIGN ERROR: walker called to walk locknull " + "resources, but a lockdb was not provided."); + } +#endif + + fsctx.params = params; + fsctx.wres.walk_ctx = params->walk_ctx; + fsctx.wres.pool = params->pool; + + /* ### zero out versioned, working, baselined? */ + + fsctx.res1 = *params->root; + fsctx.res1.pool = params->pool; + + fsctx.res1.info = &fsctx.info1; + fsctx.info1 = *params->root->info; + + /* the pathname is stored in the path1 buffer */ + dav_buffer_init(params->pool, &fsctx.path1, fsctx.info1.pathname); + fsctx.info1.pathname = fsctx.path1.buf; + + if (root_dst != NULL) { + /* internal call from the COPY/MOVE code. set it up. */ + + fsctx.wres.walk_ctx = &cm_ctx; + cm_ctx.is_move = is_move; + cm_ctx.res_dst = &fsctx.res2; + cm_ctx.root = params->root; + cm_ctx.pool = params->pool; + + fsctx.res2 = *root_dst; + fsctx.res2.exists = 0; + fsctx.res2.collection = 0; + fsctx.res2.uri = NULL; /* we don't track this */ + fsctx.res2.pool = params->pool; + + fsctx.res2.info = &fsctx.info2; + fsctx.info2 = *root_dst->info; + + /* res2 does not exist -- clear its finfo structure */ + memset(&fsctx.info2.finfo, 0, sizeof(fsctx.info2.finfo)); + + /* the pathname is stored in the path2 buffer */ + dav_buffer_init(params->pool, &fsctx.path2, fsctx.info2.pathname); + fsctx.info2.pathname = fsctx.path2.buf; + } + + /* prep the URI buffer */ + dav_buffer_init(params->pool, &fsctx.uri_buf, params->root->uri); + + /* if we have a directory, then ensure the URI has a trailing "/" */ + if (fsctx.res1.collection + && fsctx.uri_buf.buf[fsctx.uri_buf.cur_len - 1] != '/') { + + /* this will fall into the pad area */ + fsctx.uri_buf.buf[fsctx.uri_buf.cur_len++] = '/'; + fsctx.uri_buf.buf[fsctx.uri_buf.cur_len] = '\0'; + } + + /* the current resource's URI is stored in the uri_buf buffer */ + fsctx.res1.uri = fsctx.uri_buf.buf; + + /* point the callback's resource at our structure */ + fsctx.wres.resource = &fsctx.res1; + + /* always return the error, and any/all multistatus responses */ + err = dav_fs_walker(&fsctx, depth); + *response = fsctx.wres.response; + return err; +} + +static dav_error * dav_fs_walk(const dav_walk_params *params, int depth, + dav_response **response) +{ + /* always return the error, and any/all multistatus responses */ + return dav_fs_internal_walk(params, depth, 0, NULL, response); +} + +/* dav_fs_etag: Creates an etag for the file path. + */ +static const char *dav_fs_getetag(const dav_resource *resource) +{ + etag_rec er; + + dav_resource_private *ctx = resource->info; + + if (!resource->exists || !ctx->r) { + return ""; + } + + er.vlist_validator = NULL; + er.request_time = ctx->r->request_time; + er.finfo = &ctx->finfo; + er.pathname = ctx->pathname; + er.fd = NULL; + er.force_weak = 0; + + return ap_make_etag_ex(ctx->r, &er); +} + +static const dav_hooks_repository dav_hooks_repository_fs = +{ + DEBUG_GET_HANDLER, /* normally: special GET handling not required */ + dav_fs_get_resource, + dav_fs_get_parent_resource, + dav_fs_is_same_resource, + dav_fs_is_parent_resource, + dav_fs_open_stream, + dav_fs_close_stream, + dav_fs_write_stream, + dav_fs_seek_stream, +#if DEBUG_GET_HANDLER + dav_fs_set_headers, + dav_fs_deliver, +#else + NULL, + NULL, +#endif + dav_fs_create_collection, + dav_fs_copy_resource, + dav_fs_move_resource, + dav_fs_remove_resource, + dav_fs_walk, + dav_fs_getetag, + NULL, + dav_fs_get_request_rec, + dav_fs_pathname +}; + +static dav_prop_insert dav_fs_insert_prop(const dav_resource *resource, + int propid, dav_prop_insert what, + apr_text_header *phdr) +{ + const char *value; + const char *s; + apr_pool_t *p = resource->info->pool; + const dav_liveprop_spec *info; + long global_ns; + + /* an HTTP-date can be 29 chars plus a null term */ + /* a 64-bit size can be 20 chars plus a null term */ + char buf[DAV_TIMEBUF_SIZE]; + + /* + ** None of FS provider properties are defined if the resource does not + ** exist. Just bail for this case. + ** + ** Even though we state that the FS properties are not defined, the + ** client cannot store dead values -- we deny that thru the is_writable + ** hook function. + */ + if (!resource->exists) + return DAV_PROP_INSERT_NOTDEF; + + switch (propid) { + case DAV_PROPID_creationdate: + /* + ** Closest thing to a creation date. since we don't actually + ** perform the operations that would modify ctime (after we + ** create the file), then we should be pretty safe here. + */ + dav_format_time(DAV_STYLE_ISO8601, + resource->info->finfo.ctime, + buf, sizeof(buf)); + value = buf; + break; + + case DAV_PROPID_getcontentlength: + /* our property, but not defined on collection resources */ + if (resource->collection) + return DAV_PROP_INSERT_NOTDEF; + + apr_snprintf(buf, sizeof(buf), "%" APR_OFF_T_FMT, resource->info->finfo.size); + value = buf; + break; + + case DAV_PROPID_getetag: + value = dav_fs_getetag(resource); + break; + + case DAV_PROPID_getlastmodified: + dav_format_time(DAV_STYLE_RFC822, + resource->info->finfo.mtime, + buf, sizeof(buf)); + value = buf; + break; + + case DAV_PROPID_FS_executable: + /* our property, but not defined on collection resources */ + if (resource->collection) + return DAV_PROP_INSERT_NOTDEF; + + /* our property, but not defined on this platform */ + if (!(resource->info->finfo.valid & APR_FINFO_UPROT)) + return DAV_PROP_INSERT_NOTDEF; + + /* the files are "ours" so we only need to check owner exec privs */ + if (resource->info->finfo.protection & APR_UEXECUTE) + value = "T"; + else + value = "F"; + break; + + default: + /* ### what the heck was this property? */ + return DAV_PROP_INSERT_NOTDEF; + } + + /* assert: value != NULL */ + + /* get the information and global NS index for the property */ + global_ns = dav_get_liveprop_info(propid, &dav_fs_liveprop_group, &info); + + /* assert: info != NULL && info->name != NULL */ + + /* DBG3("FS: inserting lp%d:%s (local %d)", ns, scan->name, scan->ns); */ + + if (what == DAV_PROP_INSERT_VALUE) { + s = apr_psprintf(p, "<lp%ld:%s>%s</lp%ld:%s>" DEBUG_CR, + global_ns, info->name, value, global_ns, info->name); + } + else if (what == DAV_PROP_INSERT_NAME) { + s = apr_psprintf(p, "<lp%ld:%s/>" DEBUG_CR, global_ns, info->name); + } + else { + /* assert: what == DAV_PROP_INSERT_SUPPORTED */ + s = apr_pstrcat(p, + "<D:supported-live-property D:name=\"", + info->name, + "\" D:namespace=\"", + dav_fs_namespace_uris[info->ns], + "\"/>" DEBUG_CR, NULL); + } + apr_text_append(p, phdr, s); + + /* we inserted what was asked for */ + return what; +} + +static int dav_fs_is_writable(const dav_resource *resource, int propid) +{ + const dav_liveprop_spec *info; + +#ifdef DAV_FS_HAS_EXECUTABLE + /* if we have the executable property, and this isn't a collection, + then the property is writable. */ + if (propid == DAV_PROPID_FS_executable && !resource->collection) + return 1; +#endif + + (void) dav_get_liveprop_info(propid, &dav_fs_liveprop_group, &info); + return info->is_writable; +} + +static dav_error *dav_fs_patch_validate(const dav_resource *resource, + const apr_xml_elem *elem, + int operation, + void **context, + int *defer_to_dead) +{ + const apr_text *cdata; + const apr_text *f_cdata; + char value; + dav_elem_private *priv = elem->priv; + + if (priv->propid != DAV_PROPID_FS_executable) { + *defer_to_dead = 1; + return NULL; + } + + if (operation == DAV_PROP_OP_DELETE) { + return dav_new_error(resource->info->pool, HTTP_CONFLICT, 0, 0, + "The 'executable' property cannot be removed."); + } + + cdata = elem->first_cdata.first; + + /* ### hmm. this isn't actually looking at all the possible text items */ + f_cdata = elem->first_child == NULL + ? NULL + : elem->first_child->following_cdata.first; + + /* DBG3("name=%s cdata=%s f_cdata=%s",elem->name,cdata ? cdata->text : "[null]",f_cdata ? f_cdata->text : "[null]"); */ + + if (cdata == NULL) { + if (f_cdata == NULL) { + return dav_new_error(resource->info->pool, HTTP_CONFLICT, 0, 0, + "The 'executable' property expects a single " + "character, valued 'T' or 'F'. There was no " + "value submitted."); + } + cdata = f_cdata; + } + else if (f_cdata != NULL) + goto too_long; + + if (cdata->next != NULL || strlen(cdata->text) != 1) + goto too_long; + + value = cdata->text[0]; + if (value != 'T' && value != 'F') { + return dav_new_error(resource->info->pool, HTTP_CONFLICT, 0, 0, + "The 'executable' property expects a single " + "character, valued 'T' or 'F'. The value " + "submitted is invalid."); + } + + *context = (void *)((long)(value == 'T')); + + return NULL; + + too_long: + return dav_new_error(resource->info->pool, HTTP_CONFLICT, 0, 0, + "The 'executable' property expects a single " + "character, valued 'T' or 'F'. The value submitted " + "has too many characters."); + +} + +static dav_error *dav_fs_patch_exec(const dav_resource *resource, + const apr_xml_elem *elem, + int operation, + void *context, + dav_liveprop_rollback **rollback_ctx) +{ + long value = context != NULL; + apr_fileperms_t perms = resource->info->finfo.protection; + apr_status_t status; + long old_value = (perms & APR_UEXECUTE) != 0; + + /* assert: prop == executable. operation == SET. */ + + /* don't do anything if there is no change. no rollback info either. */ + /* DBG2("new value=%d (old=%d)", value, old_value); */ + if (value == old_value) + return NULL; + + perms &= ~APR_UEXECUTE; + if (value) + perms |= APR_UEXECUTE; + + if ((status = apr_file_perms_set(resource->info->pathname, perms)) + != APR_SUCCESS) { + return dav_new_error(resource->info->pool, + HTTP_INTERNAL_SERVER_ERROR, 0, status, + "Could not set the executable flag of the " + "target resource."); + } + + /* update the resource and set up the rollback context */ + resource->info->finfo.protection = perms; + *rollback_ctx = (dav_liveprop_rollback *)old_value; + + return NULL; +} + +static void dav_fs_patch_commit(const dav_resource *resource, + int operation, + void *context, + dav_liveprop_rollback *rollback_ctx) +{ + /* nothing to do */ +} + +static dav_error *dav_fs_patch_rollback(const dav_resource *resource, + int operation, + void *context, + dav_liveprop_rollback *rollback_ctx) +{ + apr_fileperms_t perms = resource->info->finfo.protection & ~APR_UEXECUTE; + apr_status_t status; + int value = rollback_ctx != NULL; + + /* assert: prop == executable. operation == SET. */ + + /* restore the executable bit */ + if (value) + perms |= APR_UEXECUTE; + + if ((status = apr_file_perms_set(resource->info->pathname, perms)) + != APR_SUCCESS) { + return dav_new_error(resource->info->pool, + HTTP_INTERNAL_SERVER_ERROR, 0, status, + "After a failure occurred, the resource's " + "executable flag could not be restored."); + } + + /* restore the resource's state */ + resource->info->finfo.protection = perms; + + return NULL; +} + + +static const dav_hooks_liveprop dav_hooks_liveprop_fs = +{ + dav_fs_insert_prop, + dav_fs_is_writable, + dav_fs_namespace_uris, + dav_fs_patch_validate, + dav_fs_patch_exec, + dav_fs_patch_commit, + dav_fs_patch_rollback +}; + +static const dav_provider dav_fs_provider = +{ + &dav_hooks_repository_fs, + &dav_hooks_db_dbm, + &dav_hooks_locks_fs, + NULL, /* vsn */ + NULL, /* binding */ + NULL, /* search */ + + NULL /* ctx */ +}; + +void dav_fs_gather_propsets(apr_array_header_t *uris) +{ +#ifdef DAV_FS_HAS_EXECUTABLE + *(const char **)apr_array_push(uris) = + "<http://apache.org/dav/propset/fs/1>"; +#endif +} + +int dav_fs_find_liveprop(const dav_resource *resource, + const char *ns_uri, const char *name, + const dav_hooks_liveprop **hooks) +{ + /* don't try to find any liveprops if this isn't "our" resource */ + if (resource->hooks != &dav_hooks_repository_fs) + return 0; + return dav_do_find_liveprop(ns_uri, name, &dav_fs_liveprop_group, hooks); +} + +void dav_fs_insert_all_liveprops(request_rec *r, const dav_resource *resource, + dav_prop_insert what, apr_text_header *phdr) +{ + /* don't insert any liveprops if this isn't "our" resource */ + if (resource->hooks != &dav_hooks_repository_fs) + return; + + if (!resource->exists) { + /* a lock-null resource */ + /* + ** ### technically, we should insert empty properties. dunno offhand + ** ### what part of the spec said this, but it was essentially thus: + ** ### "the properties should be defined, but may have no value". + */ + return; + } + + (void) dav_fs_insert_prop(resource, DAV_PROPID_creationdate, + what, phdr); + (void) dav_fs_insert_prop(resource, DAV_PROPID_getcontentlength, + what, phdr); + (void) dav_fs_insert_prop(resource, DAV_PROPID_getlastmodified, + what, phdr); + (void) dav_fs_insert_prop(resource, DAV_PROPID_getetag, + what, phdr); + +#ifdef DAV_FS_HAS_EXECUTABLE + /* Only insert this property if it is defined for this platform. */ + (void) dav_fs_insert_prop(resource, DAV_PROPID_FS_executable, + what, phdr); +#endif + + /* ### we know the others aren't defined as liveprops */ +} + +void dav_fs_register(apr_pool_t *p) +{ + /* register the namespace URIs */ + dav_register_liveprop_group(p, &dav_fs_liveprop_group); + + /* register the repository provider */ + dav_register_provider(p, "filesystem", &dav_fs_provider); +} |