/* 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 /* 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); } } 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, "An error occurred while opening a resource."); } /* (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 */ 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, HTTP_FORBIDDEN, 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-existant (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: Stolen from ap_make_etag. Creates a strong etag * for file path. * ### do we need to return weak tags sometimes? */ static const char *dav_fs_getetag(const dav_resource *resource) { dav_resource_private *ctx = resource->info; if (!resource->exists) return apr_pstrdup(ctx->pool, ""); if (ctx->finfo.filetype != APR_NOFILE) { return apr_psprintf(ctx->pool, "\"%" APR_UINT64_T_HEX_FMT "-%" APR_UINT64_T_HEX_FMT "-%" APR_UINT64_T_HEX_FMT "\"", (apr_uint64_t) ctx->finfo.inode, (apr_uint64_t) ctx->finfo.size, (apr_uint64_t) ctx->finfo.mtime); } return apr_psprintf(ctx->pool, "\"%" APR_UINT64_T_HEX_FMT "\"", (apr_uint64_t) ctx->finfo.mtime); } 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; int 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, "%s" DEBUG_CR, global_ns, info->name, value, global_ns, info->name); } else if (what == DAV_PROP_INSERT_NAME) { s = apr_psprintf(p, "" DEBUG_CR, global_ns, info->name); } else { /* assert: what == DAV_PROP_INSERT_SUPPORTED */ s = apr_psprintf(p, "" DEBUG_CR, info->name, dav_fs_namespace_uris[info->ns]); } 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) = ""; #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); }