diff options
Diffstat (limited to '')
-rw-r--r-- | modules/dav/main/util.c | 2152 |
1 files changed, 2152 insertions, 0 deletions
diff --git a/modules/dav/main/util.c b/modules/dav/main/util.c new file mode 100644 index 0000000..9f24604 --- /dev/null +++ b/modules/dav/main/util.c @@ -0,0 +1,2152 @@ +/* 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 extension module for Apache 2.0.* +** - various utilities, repository-independent +*/ + +#include "apr_strings.h" +#include "apr_lib.h" + +#define APR_WANT_STRFUNC +#include "apr_want.h" + +#include "mod_dav.h" + +#include "http_request.h" +#include "http_config.h" +#include "http_vhost.h" +#include "http_log.h" +#include "http_protocol.h" + +DAV_DECLARE(dav_error*) dav_new_error(apr_pool_t *p, int status, int error_id, + apr_status_t aprerr, const char *desc) +{ + dav_error *err = apr_pcalloc(p, sizeof(*err)); + + /* DBG3("dav_new_error: %d %d %s", status, error_id, desc ? desc : "(no desc)"); */ + + err->status = status; + err->error_id = error_id; + err->desc = desc; + err->aprerr = aprerr; + + return err; +} + +DAV_DECLARE(dav_error*) dav_new_error_tag(apr_pool_t *p, int status, + int error_id, apr_status_t aprerr, + const char *desc, + const char *namespace, + const char *tagname) +{ + dav_error *err = dav_new_error(p, status, error_id, aprerr, desc); + + err->tagname = tagname; + err->namespace = namespace; + + return err; +} + + +DAV_DECLARE(dav_error*) dav_push_error(apr_pool_t *p, int status, + int error_id, const char *desc, + dav_error *prev) +{ + dav_error *err = apr_pcalloc(p, sizeof(*err)); + + err->status = status; + err->error_id = error_id; + err->desc = desc; + err->prev = prev; + + return err; +} + +DAV_DECLARE(dav_error*) dav_join_error(dav_error *dest, dav_error *src) +{ + dav_error *curr = dest; + + /* src error doesn't exist so nothing to join just return dest */ + if (src == NULL) { + return dest; + } + + /* dest error doesn't exist so nothing to join just return src */ + if (curr == NULL) { + return src; + } + + /* find last error in dest stack */ + while (curr->prev != NULL) { + curr = curr->prev; + } + + /* add the src error onto end of dest stack and return it */ + curr->prev = src; + return dest; +} + +DAV_DECLARE(void) dav_check_bufsize(apr_pool_t * p, dav_buffer *pbuf, + apr_size_t extra_needed) +{ + /* grow the buffer if necessary */ + if (pbuf->cur_len + extra_needed > pbuf->alloc_len) { + char *newbuf; + + pbuf->alloc_len += extra_needed + DAV_BUFFER_PAD; + newbuf = apr_palloc(p, pbuf->alloc_len); + memcpy(newbuf, pbuf->buf, pbuf->cur_len); + pbuf->buf = newbuf; + } +} + +DAV_DECLARE(void) dav_set_bufsize(apr_pool_t * p, dav_buffer *pbuf, + apr_size_t size) +{ + /* NOTE: this does not retain prior contents */ + + /* NOTE: this function is used to init the first pointer, too, since + the PAD will be larger than alloc_len (0) for zeroed structures */ + + /* grow if we don't have enough for the requested size plus padding */ + if (size + DAV_BUFFER_PAD > pbuf->alloc_len) { + /* set the new length; min of MINSIZE */ + pbuf->alloc_len = size + DAV_BUFFER_PAD; + if (pbuf->alloc_len < DAV_BUFFER_MINSIZE) + pbuf->alloc_len = DAV_BUFFER_MINSIZE; + + pbuf->buf = apr_palloc(p, pbuf->alloc_len); + } + pbuf->cur_len = size; +} + + +/* initialize a buffer and copy the specified (null-term'd) string into it */ +DAV_DECLARE(void) dav_buffer_init(apr_pool_t *p, dav_buffer *pbuf, + const char *str) +{ + dav_set_bufsize(p, pbuf, strlen(str)); + memcpy(pbuf->buf, str, pbuf->cur_len + 1); +} + +/* append a string to the end of the buffer, adjust length */ +DAV_DECLARE(void) dav_buffer_append(apr_pool_t *p, dav_buffer *pbuf, + const char *str) +{ + apr_size_t len = strlen(str); + + dav_check_bufsize(p, pbuf, len + 1); + memcpy(pbuf->buf + pbuf->cur_len, str, len + 1); + pbuf->cur_len += len; +} + +/* place a string on the end of the buffer, do NOT adjust length */ +DAV_DECLARE(void) dav_buffer_place(apr_pool_t *p, dav_buffer *pbuf, + const char *str) +{ + apr_size_t len = strlen(str); + + dav_check_bufsize(p, pbuf, len + 1); + memcpy(pbuf->buf + pbuf->cur_len, str, len + 1); +} + +/* place some memory on the end of a buffer; do NOT adjust length */ +DAV_DECLARE(void) dav_buffer_place_mem(apr_pool_t *p, dav_buffer *pbuf, + const void *mem, apr_size_t amt, + apr_size_t pad) +{ + dav_check_bufsize(p, pbuf, amt + pad); + memcpy(pbuf->buf + pbuf->cur_len, mem, amt); +} + +/* +** dav_lookup_uri() +** +** Extension for ap_sub_req_lookup_uri() which can't handle absolute +** URIs properly. +** +** If NULL is returned, then an error occurred with parsing the URI or +** the URI does not match the current server. +*/ +DAV_DECLARE(dav_lookup_result) dav_lookup_uri(const char *uri, + request_rec * r, + int must_be_absolute) +{ + dav_lookup_result result = { 0 }; + const char *scheme; + apr_port_t port; + apr_uri_t comp; + char *new_file; + const char *domain; + + /* first thing to do is parse the URI into various components */ + if (apr_uri_parse(r->pool, uri, &comp) != APR_SUCCESS) { + result.err.status = HTTP_BAD_REQUEST; + result.err.desc = "Invalid syntax in Destination URI."; + return result; + } + + /* the URI must be an absoluteURI (WEBDAV S9.3) */ + if (comp.scheme == NULL && must_be_absolute) { + result.err.status = HTTP_BAD_REQUEST; + result.err.desc = "Destination URI must be an absolute URI."; + return result; + } + + /* the URI must not have a query (args) or a fragment */ + if (comp.query != NULL || comp.fragment != NULL) { + result.err.status = HTTP_BAD_REQUEST; + result.err.desc = + "Destination URI contains invalid components " + "(a query or a fragment)."; + return result; + } + + /* If the scheme or port was provided, then make sure that it matches + the scheme/port of this request. If the request must be absolute, + then require the (explicit/implicit) scheme/port be matching. + + ### hmm. if a port wasn't provided (does the parse return port==0?), + ### but we're on a non-standard port, then we won't detect that the + ### URI's port implies the wrong one. + */ + if (comp.scheme != NULL || comp.port != 0 || must_be_absolute) + { + /* ### not sure this works if the current request came in via https: */ + scheme = r->parsed_uri.scheme; + if (scheme == NULL) + scheme = ap_http_scheme(r); + + /* insert a port if the URI did not contain one */ + if (comp.port == 0) + comp.port = apr_uri_port_of_scheme(comp.scheme); + + /* now, verify that the URI uses the same scheme as the current. + request. the port must match our port. + */ + port = r->connection->local_addr->port; + if (strcasecmp(comp.scheme, scheme) != 0 +#ifdef APACHE_PORT_HANDLING_IS_BUSTED + || comp.port != port +#endif + ) { + result.err.status = HTTP_BAD_GATEWAY; + result.err.desc = apr_psprintf(r->pool, + "Destination URI refers to " + "different scheme or port " + "(%s://hostname:%d)" APR_EOL_STR + "(want: %s://hostname:%d)", + comp.scheme ? comp.scheme : scheme, + comp.port ? comp.port : port, + scheme, port); + return result; + } + } + + /* we have verified the scheme, port, and general structure */ + + /* + ** Hrm. IE5 will pass unqualified hostnames for both the + ** Host: and Destination: headers. This breaks the + ** http_vhost.c::matches_aliases function. + ** + ** For now, qualify unqualified comp.hostnames with + ** r->server->server_hostname. + ** + ** ### this is a big hack. Apache should provide a better way. + ** ### maybe the admin should list the unqualified hosts in a + ** ### <ServerAlias> block? + */ + if (comp.hostname != NULL + && strrchr(comp.hostname, '.') == NULL + && (domain = strchr(r->server->server_hostname, '.')) != NULL) { + comp.hostname = apr_pstrcat(r->pool, comp.hostname, domain, NULL); + } + + /* now, if a hostname was provided, then verify that it represents the + same server as the current connection. note that we just use our + port, since we've verified the URI matches ours */ +#ifdef APACHE_PORT_HANDLING_IS_BUSTED + if (comp.hostname != NULL && + !ap_matches_request_vhost(r, comp.hostname, port)) { + result.err.status = HTTP_BAD_GATEWAY; + result.err.desc = "Destination URI refers to a different server."; + return result; + } +#endif + + /* we have verified that the requested URI denotes the same server as + the current request. Therefore, we can use ap_sub_req_lookup_uri() */ + + /* reconstruct a URI as just the path */ + new_file = apr_uri_unparse(r->pool, &comp, APR_URI_UNP_OMITSITEPART); + + /* + * Lookup the URI and return the sub-request. Note that we use the + * same HTTP method on the destination. This allows the destination + * to apply appropriate restrictions (e.g. readonly). + */ + result.rnew = ap_sub_req_method_uri(r->method, new_file, r, NULL); + + return result; +} + +/* --------------------------------------------------------------- +** +** XML UTILITY FUNCTIONS +*/ + +/* validate that the root element uses a given DAV: tagname (TRUE==valid) */ +DAV_DECLARE(int) dav_validate_root(const apr_xml_doc *doc, + const char *tagname) +{ + return doc->root && + doc->root->ns == APR_XML_NS_DAV_ID && + strcmp(doc->root->name, tagname) == 0; +} + +/* find and return the (unique) child with a given DAV: tagname */ +DAV_DECLARE(apr_xml_elem *) dav_find_child(const apr_xml_elem *elem, + const char *tagname) +{ + apr_xml_elem *child = elem->first_child; + + for (; child; child = child->next) + if (child->ns == APR_XML_NS_DAV_ID && !strcmp(child->name, tagname)) + return child; + return NULL; +} + +/* gather up all the CDATA into a single string */ +DAV_DECLARE(const char *) dav_xml_get_cdata(const apr_xml_elem *elem, apr_pool_t *pool, + int strip_white) +{ + apr_size_t len = 0; + apr_text *scan; + const apr_xml_elem *child; + char *cdata; + char *s; + apr_size_t tlen; + const char *found_text = NULL; /* initialize to avoid gcc warning */ + int found_count = 0; + + for (scan = elem->first_cdata.first; scan != NULL; scan = scan->next) { + found_text = scan->text; + ++found_count; + len += strlen(found_text); + } + + for (child = elem->first_child; child != NULL; child = child->next) { + for (scan = child->following_cdata.first; + scan != NULL; + scan = scan->next) { + found_text = scan->text; + ++found_count; + len += strlen(found_text); + } + } + + /* some fast-path cases: + * 1) zero-length cdata + * 2) a single piece of cdata with no whitespace to strip + */ + if (len == 0) + return ""; + if (found_count == 1) { + if (!strip_white + || (!apr_isspace(*found_text) + && !apr_isspace(found_text[len - 1]))) + return found_text; + } + + cdata = s = apr_palloc(pool, len + 1); + + for (scan = elem->first_cdata.first; scan != NULL; scan = scan->next) { + tlen = strlen(scan->text); + memcpy(s, scan->text, tlen); + s += tlen; + } + + for (child = elem->first_child; child != NULL; child = child->next) { + for (scan = child->following_cdata.first; + scan != NULL; + scan = scan->next) { + tlen = strlen(scan->text); + memcpy(s, scan->text, tlen); + s += tlen; + } + } + + *s = '\0'; + + if (strip_white) { + /* trim leading whitespace */ + while (apr_isspace(*cdata)) { /* assume: return false for '\0' */ + ++cdata; + --len; + } + + /* trim trailing whitespace */ + while (len-- > 0 && apr_isspace(cdata[len])) + continue; + cdata[len + 1] = '\0'; + } + + return cdata; +} + +DAV_DECLARE(dav_xmlns_info *) dav_xmlns_create(apr_pool_t *pool) +{ + dav_xmlns_info *xi = apr_pcalloc(pool, sizeof(*xi)); + + xi->pool = pool; + xi->uri_prefix = apr_hash_make(pool); + xi->prefix_uri = apr_hash_make(pool); + + return xi; +} + +DAV_DECLARE(void) dav_xmlns_add(dav_xmlns_info *xi, + const char *prefix, const char *uri) +{ + /* this "should" not overwrite a prefix mapping */ + apr_hash_set(xi->prefix_uri, prefix, APR_HASH_KEY_STRING, uri); + + /* note: this may overwrite an existing URI->prefix mapping, but it + doesn't matter -- any prefix is usable to specify the URI. */ + apr_hash_set(xi->uri_prefix, uri, APR_HASH_KEY_STRING, prefix); +} + +DAV_DECLARE(const char *) dav_xmlns_add_uri(dav_xmlns_info *xi, + const char *uri) +{ + const char *prefix; + + if ((prefix = apr_hash_get(xi->uri_prefix, uri, + APR_HASH_KEY_STRING)) != NULL) + return prefix; + + prefix = apr_psprintf(xi->pool, "g%d", xi->count++); + dav_xmlns_add(xi, prefix, uri); + return prefix; +} + +DAV_DECLARE(const char *) dav_xmlns_get_uri(dav_xmlns_info *xi, + const char *prefix) +{ + return apr_hash_get(xi->prefix_uri, prefix, APR_HASH_KEY_STRING); +} + +DAV_DECLARE(const char *) dav_xmlns_get_prefix(dav_xmlns_info *xi, + const char *uri) +{ + return apr_hash_get(xi->uri_prefix, uri, APR_HASH_KEY_STRING); +} + +DAV_DECLARE(void) dav_xmlns_generate(dav_xmlns_info *xi, + apr_text_header *phdr) +{ + apr_hash_index_t *hi = apr_hash_first(xi->pool, xi->prefix_uri); + + for (; hi != NULL; hi = apr_hash_next(hi)) { + const void *prefix; + void *uri; + const char *s; + + apr_hash_this(hi, &prefix, NULL, &uri); + + s = apr_psprintf(xi->pool, " xmlns:%s=\"%s\"", + (const char *)prefix, (const char *)uri); + apr_text_append(xi->pool, phdr, s); + } +} + +/* --------------------------------------------------------------- +** +** Timeout header processing +** +*/ + +/* dav_get_timeout: If the Timeout: header exists, return a time_t + * when this lock is expected to expire. Otherwise, return + * a time_t of DAV_TIMEOUT_INFINITE. + * + * It's unclear if DAV clients are required to understand + * Seconds-xxx and Infinity time values. We assume that they do. + * In addition, for now, that's all we understand, too. + */ +DAV_DECLARE(time_t) dav_get_timeout(request_rec *r) +{ + time_t now, expires = DAV_TIMEOUT_INFINITE; + + const char *timeout_const = apr_table_get(r->headers_in, "Timeout"); + const char *timeout = apr_pstrdup(r->pool, timeout_const), *val; + + if (timeout == NULL) + return DAV_TIMEOUT_INFINITE; + + /* Use the first thing we understand, or infinity if + * we don't understand anything. + */ + + while ((val = ap_getword_white(r->pool, &timeout)) && strlen(val)) { + if (!strncmp(val, "Infinite", 8)) { + return DAV_TIMEOUT_INFINITE; + } + + if (!strncmp(val, "Second-", 7)) { + val += 7; + /* ### We need to handle overflow better: + * ### timeout will be <= 2^32 - 1 + */ + expires = atol(val); + now = time(NULL); + return now + expires; + } + } + + return DAV_TIMEOUT_INFINITE; +} + +/* --------------------------------------------------------------- +** +** If Header processing +** +*/ + +/* add_if_resource returns a new if_header, linking it to next_ih. + */ +static dav_if_header *dav_add_if_resource(apr_pool_t *p, dav_if_header *next_ih, + const char *uri, apr_size_t uri_len) +{ + dav_if_header *ih; + + if ((ih = apr_pcalloc(p, sizeof(*ih))) == NULL) + return NULL; + + ih->uri = uri; + ih->uri_len = uri_len; + ih->next = next_ih; + + return ih; +} + +/* add_if_state adds a condition to an if_header. + */ +static dav_error * dav_add_if_state(apr_pool_t *p, dav_if_header *ih, + const char *state_token, + dav_if_state_type t, int condition, + const dav_hooks_locks *locks_hooks) +{ + dav_if_state_list *new_sl; + + new_sl = apr_pcalloc(p, sizeof(*new_sl)); + + new_sl->condition = condition; + new_sl->type = t; + + if (t == dav_if_opaquelock) { + dav_error *err; + + if ((err = (*locks_hooks->parse_locktoken)(p, state_token, + &new_sl->locktoken)) != NULL) { + /* If the state token cannot be parsed, treat it as an + * unknown state; this will evaluate to "false" later + * during If header validation. */ + if (err->error_id == DAV_ERR_LOCK_UNK_STATE_TOKEN) { + new_sl->type = dav_if_unknown; + } + else { + /* ### maybe add a higher-level description */ + return err; + } + } + } + else + new_sl->etag = state_token; + + new_sl->next = ih->state; + ih->state = new_sl; + + return NULL; +} + +/* fetch_next_token returns the substring from str+1 + * to the next occurrence of char term, or \0, whichever + * occurs first. Leading whitespace is ignored. + */ +static char *dav_fetch_next_token(char **str, char term) +{ + char *sp; + char *token; + + token = *str + 1; + + while (*token && (*token == ' ' || *token == '\t')) + token++; + + if ((sp = strchr(token, term)) == NULL) + return NULL; + + *sp = '\0'; + *str = sp; + return token; +} + +/* dav_process_if_header: + * + * If NULL (no error) is returned, then **if_header points to the + * "If" productions structure (or NULL if "If" is not present). + * + * ### this part is bogus: + * If an error is encountered, the error is logged. Parent should + * return err->status. + */ +static dav_error * dav_process_if_header(request_rec *r, dav_if_header **p_ih) +{ + dav_error *err; + char *str; + char *list; + const char *state_token; + const char *uri = NULL; /* scope of current production; NULL=no-tag */ + apr_size_t uri_len = 0; + apr_status_t rv; + dav_if_header *ih = NULL; + apr_uri_t parsed_uri; + const dav_hooks_locks *locks_hooks = DAV_GET_HOOKS_LOCKS(r); + enum {no_tagged, tagged, unknown} list_type = unknown; + int condition; + + *p_ih = NULL; + + if ((str = apr_pstrdup(r->pool, apr_table_get(r->headers_in, "If"))) == NULL) + return NULL; + + while (*str) { + switch(*str) { + case '<': + /* Tagged-list production - following states apply to this uri */ + if (list_type == no_tagged + || ((uri = dav_fetch_next_token(&str, '>')) == NULL)) { + return dav_new_error(r->pool, HTTP_BAD_REQUEST, + DAV_ERR_IF_TAGGED, 0, + "Invalid If-header: unclosed \"<\" or " + "unexpected tagged-list production."); + } + + /* 2518 specifies this must be an absolute URI; just take the + * relative part for later comparison against r->uri */ + if ((rv = apr_uri_parse(r->pool, uri, &parsed_uri)) != APR_SUCCESS + || !parsed_uri.path) { + return dav_new_error(r->pool, HTTP_BAD_REQUEST, + DAV_ERR_IF_TAGGED, rv, + "Invalid URI in tagged If-header."); + } + /* note that parsed_uri.path is allocated; we can trash it */ + + /* clean up the URI a bit */ + ap_getparents(parsed_uri.path); + + /* the resources we will compare to have unencoded paths */ + if (ap_unescape_url(parsed_uri.path) != OK) { + return dav_new_error(r->pool, HTTP_BAD_REQUEST, + DAV_ERR_IF_TAGGED, rv, + "Invalid percent encoded URI in " + "tagged If-header."); + } + + uri_len = strlen(parsed_uri.path); + if (uri_len > 1 && parsed_uri.path[uri_len - 1] == '/') { + parsed_uri.path[--uri_len] = '\0'; + } + + uri = parsed_uri.path; + list_type = tagged; + break; + + case '(': + /* List production */ + + /* If a uri has not been encountered, this is a No-Tagged-List */ + if (list_type == unknown) + list_type = no_tagged; + + if ((list = dav_fetch_next_token(&str, ')')) == NULL) { + return dav_new_error(r->pool, HTTP_BAD_REQUEST, + DAV_ERR_IF_UNCLOSED_PAREN, 0, + "Invalid If-header: unclosed \"(\"."); + } + + if ((ih = dav_add_if_resource(r->pool, ih, uri, uri_len)) == NULL) { + /* ### dav_add_if_resource() should return an error for us! */ + return dav_new_error(r->pool, HTTP_BAD_REQUEST, + DAV_ERR_IF_PARSE, 0, + "Internal server error parsing \"If:\" " + "header."); + } + + condition = DAV_IF_COND_NORMAL; + + while (*list) { + /* List is the entire production (in a uri scope) */ + + switch (*list) { + case '<': + if ((state_token = dav_fetch_next_token(&list, '>')) == NULL) { + /* ### add a description to this error */ + return dav_new_error(r->pool, HTTP_BAD_REQUEST, + DAV_ERR_IF_PARSE, 0, NULL); + } + + if ((err = dav_add_if_state(r->pool, ih, state_token, dav_if_opaquelock, + condition, locks_hooks)) != NULL) { + /* ### maybe add a higher level description */ + return err; + } + condition = DAV_IF_COND_NORMAL; + break; + + case '[': + if ((state_token = dav_fetch_next_token(&list, ']')) == NULL) { + /* ### add a description to this error */ + return dav_new_error(r->pool, HTTP_BAD_REQUEST, + DAV_ERR_IF_PARSE, 0, NULL); + } + + if ((err = dav_add_if_state(r->pool, ih, state_token, dav_if_etag, + condition, locks_hooks)) != NULL) { + /* ### maybe add a higher level description */ + return err; + } + condition = DAV_IF_COND_NORMAL; + break; + + case 'N': + if (list[1] == 'o' && list[2] == 't') { + if (condition != DAV_IF_COND_NORMAL) { + return dav_new_error(r->pool, HTTP_BAD_REQUEST, + DAV_ERR_IF_MULTIPLE_NOT, 0, + "Invalid \"If:\" header: " + "Multiple \"not\" entries " + "for the same state."); + } + condition = DAV_IF_COND_NOT; + } + list += 2; + break; + + case ' ': + case '\t': + break; + + default: + return dav_new_error(r->pool, HTTP_BAD_REQUEST, + DAV_ERR_IF_UNK_CHAR, 0, + apr_psprintf(r->pool, + "Invalid \"If:\" " + "header: Unexpected " + "character encountered " + "(0x%02x, '%c').", + *list, *list)); + } + + list++; + } + break; + + case ' ': + case '\t': + break; + + default: + return dav_new_error(r->pool, HTTP_BAD_REQUEST, + DAV_ERR_IF_UNK_CHAR, 0, + apr_psprintf(r->pool, + "Invalid \"If:\" header: " + "Unexpected character " + "encountered (0x%02x, '%c').", + *str, *str)); + } + + str++; + } + + *p_ih = ih; + return NULL; +} + +static int dav_find_submitted_locktoken(const dav_if_header *if_header, + const dav_lock *lock_list, + const dav_hooks_locks *locks_hooks) +{ + for (; if_header != NULL; if_header = if_header->next) { + const dav_if_state_list *state_list; + + for (state_list = if_header->state; + state_list != NULL; + state_list = state_list->next) { + + if (state_list->type == dav_if_opaquelock) { + const dav_lock *lock; + + /* given state_list->locktoken, match it */ + + /* + ** The resource will have one or more lock tokens. We only + ** need to match one of them against any token in the + ** If: header. + ** + ** One token case: It is an exclusive or shared lock. Either + ** way, we must find it. + ** + ** N token case: They are shared locks. By policy, we need + ** to match only one. The resource's other + ** tokens may belong to somebody else (so we + ** shouldn't see them in the If: header anyway) + */ + for (lock = lock_list; lock != NULL; lock = lock->next) { + + if (!(*locks_hooks->compare_locktoken)(state_list->locktoken, lock->locktoken)) { + return 1; + } + } + } + } + } + + return 0; +} + +/* dav_validate_resource_state: + * Returns NULL if path/uri meets if-header and lock requirements + */ +static dav_error * dav_validate_resource_state(apr_pool_t *p, + const dav_resource *resource, + dav_lockdb *lockdb, + const dav_if_header *if_header, + int flags, + dav_buffer *pbuf, + request_rec *r) +{ + dav_error *err; + const char *uri; + const char *etag; + const dav_hooks_locks *locks_hooks = (lockdb ? lockdb->hooks : NULL); + const dav_if_header *ifhdr_scan; + dav_if_state_list *state_list; + dav_lock *lock_list; + dav_lock *lock; + int num_matched; + int num_that_apply; + int seen_locktoken; + apr_size_t uri_len; + const char *reason = NULL; + + /* DBG1("validate: <%s>", resource->uri); */ + + /* + ** The resource will have one of three states: + ** + ** 1) No locks. We have no special requirements that the user supply + ** specific locktokens. One of the state lists must match, and + ** we're done. + ** + ** 2) One exclusive lock. The locktoken must appear *anywhere* in the + ** If: header. Of course, asserting the token in a "Not" term will + ** quickly fail that state list :-). If the locktoken appears in + ** one of the state lists *and* one state list matches, then we're + ** done. + ** + ** 3) One or more shared locks. One of the locktokens must appear + ** *anywhere* in the If: header. If one of the locktokens appears, + ** and we match one state list, then we are done. + ** + ** The <seen_locktoken> variable determines whether we have seen one + ** of this resource's locktokens in the If: header. + */ + + /* + ** If this is a new lock request, <flags> will contain the requested + ** lock scope. Three rules apply: + ** + ** 1) Do not require a (shared) locktoken to be seen (when we are + ** applying another shared lock) + ** 2) If the scope is exclusive and we see any locks, fail. + ** 3) If the scope is shared and we see an exclusive lock, fail. + */ + + if (lockdb == NULL) { + /* we're in State 1. no locks. */ + lock_list = NULL; + } + else { + /* + ** ### hrm... we don't need to have these fully + ** ### resolved since we're only looking at the + ** ### locktokens... + ** + ** ### use get_locks w/ calltype=PARTIAL + */ + if ((err = dav_lock_query(lockdb, resource, &lock_list)) != NULL) { + return dav_push_error(p, + HTTP_INTERNAL_SERVER_ERROR, 0, + "The locks could not be queried for " + "verification against a possible \"If:\" " + "header.", + err); + } + + /* lock_list now determines whether we're in State 1, 2, or 3. */ + } + + /* + ** For a new, exclusive lock: if any locks exist, fail. + ** For a new, shared lock: if an exclusive lock exists, fail. + ** else, do not require a token to be seen. + */ + if (flags & DAV_LOCKSCOPE_EXCLUSIVE) { + if (lock_list != NULL) { + return dav_new_error(p, HTTP_LOCKED, 0, 0, + "Existing lock(s) on the requested resource " + "prevent an exclusive lock."); + } + + /* + ** There are no locks, so we can pretend that we've already met + ** any requirement to find the resource's locks in an If: header. + */ + seen_locktoken = 1; + } + else if (flags & DAV_LOCKSCOPE_SHARED) { + /* + ** Strictly speaking, we don't need this loop. Either the first + ** (and only) lock will be EXCLUSIVE, or none of them will be. + */ + for (lock = lock_list; lock != NULL; lock = lock->next) { + if (lock->scope == DAV_LOCKSCOPE_EXCLUSIVE) { + return dav_new_error(p, HTTP_LOCKED, 0, 0, + "The requested resource is already " + "locked exclusively."); + } + } + + /* + ** The locks on the resource (if any) are all shared. Set the + ** <seen_locktoken> flag to indicate that we do not need to find + ** the locks in an If: header. + */ + seen_locktoken = 1; + } + else { + /* + ** For methods other than LOCK: + ** + ** If we have no locks or if the resource is not being modified + ** (per RFC 4918 the lock token is not required on resources + ** we are not changing), then <seen_locktoken> can be set to true -- + ** pretending that we've already met the requirement of seeing one + ** of the resource's locks in the If: header. + ** + ** Otherwise, it must be cleared and we'll look for one. + */ + seen_locktoken = (lock_list == NULL + || flags & DAV_VALIDATE_NO_MODIFY); + } + + /* + ** If there is no If: header, then we can shortcut some logic: + ** + ** 1) if we do not need to find a locktoken in the (non-existent) If: + ** header, then we are successful. + ** + ** 2) if we must find a locktoken in the (non-existent) If: header, then + ** we fail. + */ + if (if_header == NULL) { + if (seen_locktoken) + return NULL; + + return dav_new_error(p, HTTP_LOCKED, 0, 0, + "This resource is locked and an \"If:\" header " + "was not supplied to allow access to the " + "resource."); + } + /* the If: header is present */ + + /* + ** If a dummy header is present (because of a Lock-Token: header), then + ** we are required to find that token in this resource's set of locks. + ** If we have no locks, then we immediately fail. + ** + ** This is a 400 (Bad Request) since they should only submit a locktoken + ** that actually exists. + ** + ** Don't issue this response if we're talking about the parent resource. + ** It is okay for that resource to NOT have this locktoken. + ** (in fact, it certainly will not: a dummy_header only occurs for the + ** UNLOCK method, the parent is checked only for locknull resources, + ** and the parent certainly does not have the (locknull's) locktoken) + */ + if (lock_list == NULL && if_header->dummy_header) { + if (flags & DAV_VALIDATE_IS_PARENT) + return NULL; + return dav_new_error(p, HTTP_BAD_REQUEST, 0, 0, + "The locktoken specified in the \"Lock-Token:\" " + "header is invalid because this resource has no " + "outstanding locks."); + } + + /* + ** Prepare the input URI. We want the URI to never have a trailing slash. + ** + ** When URIs are placed into the dav_if_header structure, they are + ** guaranteed to never have a trailing slash. If the URIs are equivalent, + ** then it doesn't matter if they both lack a trailing slash -- they're + ** still equivalent. + ** + ** Note: we could also ensure that a trailing slash is present on both + ** URIs, but the majority of URIs provided to us via a resource walk + ** will not contain that trailing slash. + */ + uri = resource->uri; + uri_len = strlen(uri); + if (uri[uri_len - 1] == '/') { + dav_set_bufsize(p, pbuf, uri_len); + memcpy(pbuf->buf, uri, uri_len); + pbuf->buf[--uri_len] = '\0'; + uri = pbuf->buf; + } + + /* get the resource's etag; we may need it during the checks */ + etag = (*resource->hooks->getetag)(resource); + + /* how many state_lists apply to this URI? */ + num_that_apply = 0; + + /* If there are if-headers, fail if this resource + * does not match at least one state_list. + */ + for (ifhdr_scan = if_header; + ifhdr_scan != NULL; + ifhdr_scan = ifhdr_scan->next) { + + /* DBG2("uri=<%s> if_uri=<%s>", uri, ifhdr_scan->uri ? ifhdr_scan->uri : "(no uri)"); */ + + if (ifhdr_scan->uri != NULL + && (uri_len != ifhdr_scan->uri_len + || memcmp(uri, ifhdr_scan->uri, uri_len) != 0)) { + /* + ** A tagged-list's URI doesn't match this resource's URI. + ** Skip to the next state_list to see if it will match. + */ + continue; + } + + /* this state_list applies to this resource */ + + /* + ** ### only one state_list should ever apply! a no-tag, or a tagged + ** ### where S9.4.2 states only one can match. + ** + ** ### revamp this code to loop thru ifhdr_scan until we find the + ** ### matching state_list. process it. stop. + */ + ++num_that_apply; + + /* To succeed, resource must match *all* of the states + * specified in the state_list. + */ + for (state_list = ifhdr_scan->state; + state_list != NULL; + state_list = state_list->next) { + + switch(state_list->type) { + case dav_if_etag: + { + const char *given_etag, *current_etag; + int mismatch; + + /* Do a weak entity comparison function as defined in + * RFC 2616 13.3.3. + */ + if (state_list->etag[0] == 'W' && + state_list->etag[1] == '/') { + given_etag = state_list->etag + 2; + } + else { + given_etag = state_list->etag; + } + if (etag[0] == 'W' && + etag[1] == '/') { + current_etag = etag + 2; + } + else { + current_etag = etag; + } + + mismatch = strcmp(given_etag, current_etag); + + if (state_list->condition == DAV_IF_COND_NORMAL && mismatch) { + /* + ** The specified entity-tag does not match the + ** entity-tag on the resource. This state_list is + ** not going to match. Bust outta here. + */ + reason = + "an entity-tag was specified, but the resource's " + "actual ETag does not match."; + goto state_list_failed; + } + else if (state_list->condition == DAV_IF_COND_NOT + && !mismatch) { + /* + ** The specified entity-tag DOES match the + ** entity-tag on the resource. This state_list is + ** not going to match. Bust outta here. + */ + reason = + "an entity-tag was specified using the \"Not\" form, " + "but the resource's actual ETag matches the provided " + "entity-tag."; + goto state_list_failed; + } + break; + } + + case dav_if_opaquelock: + if (lockdb == NULL) { + if (state_list->condition == DAV_IF_COND_NOT) { + /* the locktoken is definitely not there! (success) */ + continue; + } + + /* condition == DAV_IF_COND_NORMAL */ + + /* + ** If no lockdb is provided, then validation fails for + ** this state_list (NORMAL means we were supposed to + ** find the token, which we obviously cannot do without + ** a lock database). + ** + ** Go and try the next state list. + */ + reason = + "a State-token was supplied, but a lock database " + "is not available for to provide the required lock."; + goto state_list_failed; + } + + /* Resource validation 'fails' if: + * ANY of the lock->locktokens match + * a NOT state_list->locktoken, + * OR + * NONE of the lock->locktokens match + * a NORMAL state_list->locktoken. + */ + num_matched = 0; + for (lock = lock_list; lock != NULL; lock = lock->next) { + + /* + DBG2("compare: rsrc=%s ifhdr=%s", + (*locks_hooks->format_locktoken)(p, lock->locktoken), + (*locks_hooks->format_locktoken)(p, state_list->locktoken)); + */ + + /* nothing to do if the locktokens do not match. */ + if ((*locks_hooks->compare_locktoken)(state_list->locktoken, lock->locktoken)) { + continue; + } + + /* + ** We have now matched up one of the resource's locktokens + ** to a locktoken in a State-token in the If: header. + ** Note this fact, so that we can pass the overall + ** requirement of seeing at least one of the resource's + ** locktokens. + */ + seen_locktoken = 1; + + if (state_list->condition == DAV_IF_COND_NOT) { + /* + ** This state requires that the specified locktoken + ** is NOT present on the resource. But we just found + ** it. There is no way this state-list can now + ** succeed, so go try another one. + */ + reason = + "a State-token was supplied, which used a " + "\"Not\" condition. The State-token was found " + "in the locks on this resource"; + goto state_list_failed; + } + + /* condition == DAV_IF_COND_NORMAL */ + + /* Validate auth_user: If an authenticated user created + ** the lock, only the same user may submit that locktoken + ** to manipulate a resource. + */ + if (lock->auth_user && + (!r->user || + strcmp(lock->auth_user, r->user))) { + const char *errmsg; + + errmsg = apr_pstrcat(p, "User \"", + r->user, + "\" submitted a locktoken created " + "by user \"", + lock->auth_user, "\".", NULL); + return dav_new_error(p, HTTP_FORBIDDEN, 0, 0, errmsg); + } + + /* + ** We just matched a specified State-Token to one of the + ** resource's locktokens. + ** + ** Break out of the lock scan -- we only needed to find + ** one match (actually, there shouldn't be any other + ** matches in the lock list). + */ + num_matched = 1; + break; + } + + if (num_matched == 0 + && state_list->condition == DAV_IF_COND_NORMAL) { + /* + ** We had a NORMAL state, meaning that we should have + ** found the State-Token within the locks on this + ** resource. We didn't, so this state_list must fail. + */ + reason = + "a State-token was supplied, but it was not found " + "in the locks on this resource."; + goto state_list_failed; + } + + break; + + case dav_if_unknown: + /* Request is predicated on some unknown state token, + * which must be presumed to *not* match, so fail + * unless this is a Not condition. */ + + if (state_list->condition == DAV_IF_COND_NORMAL) { + reason = + "an unknown state token was supplied"; + goto state_list_failed; + } + break; + + } /* switch */ + } /* foreach ( state_list ) */ + + /* + ** We've checked every state in this state_list and none of them + ** have failed. Since all of them succeeded, then we have a matching + ** state list and we may be done. + ** + ** The next requirement is that we have seen one of the resource's + ** locktokens (if any). If we have, then we can just exit. If we + ** haven't, then we need to keep looking. + */ + if (seen_locktoken) { + /* woo hoo! */ + return NULL; + } + + /* + ** Haven't seen one. Let's break out of the search and just look + ** for a matching locktoken. + */ + break; + + /* + ** This label is used when we detect that a state_list is not + ** going to match this resource. We bust out and try the next + ** state_list. + */ + state_list_failed: + ; + + } /* foreach ( ifhdr_scan ) */ + + /* + ** The above loop exits for one of two reasons: + ** 1) a state_list matched and seen_locktoken is false. + ** 2) all if_header structures were scanned, without (1) occurring + */ + + if (ifhdr_scan == NULL) { + /* + ** We finished the loop without finding any matching state lists. + */ + + /* + ** If none of the state_lists apply to this resource, then we + ** may have succeeded. Note that this scenario implies a + ** tagged-list with no matching state_lists. If the If: header + ** was a no-tag-list, then it would have applied to this resource. + ** + ** S9.4.2 states that when no state_lists apply, then the header + ** should be ignored. + ** + ** If we saw one of the resource's locktokens, then we're done. + ** If we did not see a locktoken, then we fail. + */ + if (num_that_apply == 0) { + if (seen_locktoken) + return NULL; + + /* + ** We may have aborted the scan before seeing the locktoken. + ** Rescan the If: header to see if we can find the locktoken + ** somewhere. + ** + ** Note that seen_locktoken == 0 implies lock_list != NULL + ** which implies locks_hooks != NULL. + */ + if (dav_find_submitted_locktoken(if_header, lock_list, + locks_hooks)) { + /* + ** We found a match! We're set... none of the If: header + ** assertions apply (implicit success), and the If: header + ** specified the locktoken somewhere. We're done. + */ + return NULL; + } + + return dav_new_error(p, HTTP_LOCKED, 0 /* error_id */, 0, + "This resource is locked and the \"If:\" " + "header did not specify one of the " + "locktokens for this resource's lock(s)."); + } + /* else: one or more state_lists were applicable, but failed. */ + + /* + ** If the dummy_header did not match, then they specified an + ** incorrect token in the Lock-Token header. Forget whether the + ** If: statement matched or not... we'll tell them about the + ** bad Lock-Token first. That is considered a 400 (Bad Request). + */ + if (if_header->dummy_header) { + return dav_new_error(p, HTTP_BAD_REQUEST, 0, 0, + "The locktoken specified in the " + "\"Lock-Token:\" header did not specify one " + "of this resource's locktoken(s)."); + } + + if (reason == NULL) { + return dav_new_error(p, HTTP_PRECONDITION_FAILED, 0, 0, + "The preconditions specified by the \"If:\" " + "header did not match this resource."); + } + + return dav_new_error(p, HTTP_PRECONDITION_FAILED, 0, 0, + apr_psprintf(p, + "The precondition(s) specified by " + "the \"If:\" header did not match " + "this resource. At least one " + "failure is because: %s", reason)); + } + + /* assert seen_locktoken == 0 */ + + /* + ** ifhdr_scan != NULL implies we found a matching state_list. + ** + ** Since we're still here, it also means that we have not yet found + ** one the resource's locktokens in the If: header. + ** + ** Scan all the if_headers and states looking for one of this + ** resource's locktokens. Note that we need to go back and scan them + ** all -- we may have aborted a scan with a failure before we saw a + ** matching token. + ** + ** Note that seen_locktoken == 0 implies lock_list != NULL which implies + ** locks_hooks != NULL. + */ + if (dav_find_submitted_locktoken(if_header, lock_list, locks_hooks)) { + /* + ** We found a match! We're set... we have a matching state list, + ** and the If: header specified the locktoken somewhere. We're done. + */ + return NULL; + } + + /* + ** We had a matching state list, but the user agent did not specify one + ** of this resource's locktokens. Tell them so. + ** + ** Note that we need to special-case the message on whether a "dummy" + ** header exists. If it exists, yet we didn't see a needed locktoken, + ** then that implies the dummy header (Lock-Token header) did NOT + ** specify one of this resource's locktokens. (this implies something + ** in the real If: header matched) + ** + ** We want to note the 400 (Bad Request) in favor of a 423 (Locked). + */ + if (if_header->dummy_header) { + return dav_new_error(p, HTTP_BAD_REQUEST, 0, 0, + "The locktoken specified in the " + "\"Lock-Token:\" header did not specify one " + "of this resource's locktoken(s)."); + } + + return dav_new_error(p, HTTP_LOCKED, 1 /* error_id */, 0, + "This resource is locked and the \"If:\" header " + "did not specify one of the " + "locktokens for this resource's lock(s)."); +} + +/* dav_validate_walker: Walker callback function to validate resource state */ +static dav_error * dav_validate_walker(dav_walk_resource *wres, int calltype) +{ + dav_walker_ctx *ctx = wres->walk_ctx; + dav_error *err; + + if ((err = dav_validate_resource_state(ctx->w.pool, wres->resource, + ctx->w.lockdb, + ctx->if_header, ctx->flags, + &ctx->work_buf, ctx->r)) == NULL) { + /* There was no error, so just bug out. */ + return NULL; + } + + /* + ** If we have a serious server error, or if the request itself failed, + ** then just return error (not a multistatus). + */ + if (ap_is_HTTP_SERVER_ERROR(err->status) + || (*wres->resource->hooks->is_same_resource)(wres->resource, + ctx->w.root)) { + /* ### maybe push a higher-level description? */ + return err; + } + + /* associate the error with the current URI */ + dav_add_response(wres, err->status, NULL); + + return NULL; +} + +/* If-* header checking */ +static int dav_meets_conditions(request_rec *r, int resource_state) +{ + const char *if_match, *if_none_match; + int retVal; + + /* If-Match '*' fix. Resource existence not checked by ap_meets_conditions. + * If-Match '*' request should succeed only if the resource exists. */ + if ((if_match = apr_table_get(r->headers_in, "If-Match")) != NULL) { + if (if_match[0] == '*' && resource_state != DAV_RESOURCE_EXISTS) + return HTTP_PRECONDITION_FAILED; + } + + retVal = ap_meets_conditions(r); + + /* If-None-Match '*' fix. If-None-Match '*' request should succeed + * if the resource does not exist. */ + if (retVal == HTTP_PRECONDITION_FAILED) { + /* Note. If if_none_match != NULL, if_none_match is the culprit. + * Since, in presence of If-None-Match, + * other If-* headers are undefined. */ + if ((if_none_match = + apr_table_get(r->headers_in, "If-None-Match")) != NULL) { + if (if_none_match[0] == '*' + && resource_state != DAV_RESOURCE_EXISTS) { + return OK; + } + } + } + + return retVal; +} + +/* +** dav_validate_request: Validate if-headers (and check for locks) on: +** (1) r->filename @ depth; +** (2) Parent of r->filename if check_parent == 1 +** +** The check of parent should be done when it is necessary to verify that +** the parent collection will accept a new member (ie current resource +** state is null). +** +** Return OK on successful validation. +** On error, return appropriate HTTP_* code, and log error. If a multi-stat +** error is necessary, response will point to it, else NULL. +*/ +DAV_DECLARE(dav_error *) dav_validate_request(request_rec *r, + dav_resource *resource, + int depth, + dav_locktoken *locktoken, + dav_response **response, + int flags, + dav_lockdb *lockdb) +{ + dav_error *err; + int result; + dav_if_header *if_header; + int lock_db_opened_locally = 0; + const dav_hooks_locks *locks_hooks = DAV_GET_HOOKS_LOCKS(r); + const dav_hooks_repository *repos_hooks = resource->hooks; + dav_buffer work_buf = { 0 }; + dav_response *new_response; + int resource_state; + const char *etag; + int set_etag = 0; + +#if DAV_DEBUG + if (depth && response == NULL) { + /* + ** ### bleck. we can't return errors for other URIs unless we have + ** ### a "response" ptr. + */ + return dav_new_error(r->pool, HTTP_INTERNAL_SERVER_ERROR, 0, 0, + "DESIGN ERROR: dav_validate_request called " + "with depth>0, but no response ptr."); + } +#endif + + if (response != NULL) + *response = NULL; + + /* Set the ETag header required by dav_meets_conditions() */ + etag = apr_table_get(r->headers_out, "ETag"); + if (!etag) { + etag = (*resource->hooks->getetag)(resource); + if (etag && *etag) { + apr_table_set(r->headers_out, "ETag", etag); + set_etag = 1; + } + } + /* Do the standard checks for conditional requests using + * If-..-Since, If-Match etc */ + resource_state = dav_get_resource_state(r, resource); + result = dav_meets_conditions(r, resource_state); + if (set_etag) { + /* + * If we have set an ETag to headers out above for + * dav_meets_conditions() revert this here as we do not want to set + * the ETag in responses to requests with methods where this might not + * be desired. + */ + apr_table_unset(r->headers_out, "ETag"); + } + if (result != OK) { + return dav_new_error(r->pool, result, 0, 0, NULL); + } + + /* always parse (and later process) the If: header */ + if ((err = dav_process_if_header(r, &if_header)) != NULL) { + /* ### maybe add higher-level description */ + return err; + } + + /* If a locktoken was specified, create a dummy if_header with which + * to validate resources. In the interim, figure out why DAV uses + * locktokens in an if-header without a Lock-Token header to refresh + * locks, but a Lock-Token header without an if-header to remove them. + */ + if (locktoken != NULL) { + dav_if_header *ifhdr_new; + + ifhdr_new = apr_pcalloc(r->pool, sizeof(*ifhdr_new)); + ifhdr_new->uri = resource->uri; + ifhdr_new->uri_len = strlen(resource->uri); + ifhdr_new->dummy_header = 1; + + ifhdr_new->state = apr_pcalloc(r->pool, sizeof(*ifhdr_new->state)); + ifhdr_new->state->type = dav_if_opaquelock; + ifhdr_new->state->condition = DAV_IF_COND_NORMAL; + ifhdr_new->state->locktoken = locktoken; + + ifhdr_new->next = if_header; + if_header = ifhdr_new; + } + + /* + ** If necessary, open the lock database (read-only, lazily); + ** the validation process may need to retrieve or update lock info. + ** Otherwise, assume provided lockdb is valid and opened rw. + */ + if (lockdb == NULL) { + if (locks_hooks != NULL) { + if ((err = (*locks_hooks->open_lockdb)(r, 0, 0, &lockdb)) != NULL) { + /* ### maybe insert higher-level comment */ + return err; + } + lock_db_opened_locally = 1; + } + } + + /* (1) Validate the specified resource, at the specified depth. + * Avoid the walk there is no if_header and we aren't planning + * to modify this resource. */ + if (resource->exists && depth > 0 && !(!if_header && flags & DAV_VALIDATE_NO_MODIFY)) { + dav_walker_ctx ctx = { { 0 } }; + dav_response *multi_status; + + ctx.w.walk_type = DAV_WALKTYPE_NORMAL; + ctx.w.func = dav_validate_walker; + ctx.w.walk_ctx = &ctx; + ctx.w.pool = r->pool; + ctx.w.root = resource; + + ctx.if_header = if_header; + ctx.r = r; + ctx.flags = flags; + + if (lockdb != NULL) { + ctx.w.lockdb = lockdb; + ctx.w.walk_type |= DAV_WALKTYPE_LOCKNULL; + } + + err = (*repos_hooks->walk)(&ctx.w, DAV_INFINITY, &multi_status); + if (err == NULL) { + *response = multi_status; + } + /* else: implies a 5xx status code occurred. */ + } + else { + err = dav_validate_resource_state(r->pool, resource, lockdb, + if_header, flags, &work_buf, r); + } + + /* (2) Validate the parent resource if requested */ + if (err == NULL && (flags & DAV_VALIDATE_PARENT)) { + dav_resource *parent_resource; + + err = (*repos_hooks->get_parent_resource)(resource, &parent_resource); + + if (err == NULL && parent_resource == NULL) { + err = dav_new_error(r->pool, HTTP_FORBIDDEN, 0, 0, + "Cannot access parent of repository root."); + } + else if (err == NULL) { + err = dav_validate_resource_state(r->pool, parent_resource, lockdb, + if_header, + flags | DAV_VALIDATE_IS_PARENT, + &work_buf, r); + + /* + ** This error occurred on the parent resource. This implies that + ** we have to create a multistatus response (to report the error + ** against a URI other than the Request-URI). "Convert" this error + ** into a multistatus response. + */ + if (err != NULL) { + new_response = apr_pcalloc(r->pool, sizeof(*new_response)); + + new_response->href = parent_resource->uri; + new_response->status = err->status; + new_response->desc = + "A validation error has occurred on the parent resource, " + "preventing the operation on the resource specified by " + "the Request-URI."; + if (err->desc != NULL) { + new_response->desc = apr_pstrcat(r->pool, + new_response->desc, + " The error was: ", + err->desc, NULL); + } + + /* assert: DAV_VALIDATE_PARENT implies response != NULL */ + new_response->next = *response; + *response = new_response; + + err = NULL; + } + } + } + + if (lock_db_opened_locally) + (*locks_hooks->close_lockdb)(lockdb); + + /* + ** If we don't have a (serious) error, and we have multistatus responses, + ** then we need to construct an "error". This error will be the overall + ** status returned, and the multistatus responses will go into its body. + ** + ** For certain methods, the overall error will be a 424. The default is + ** to construct a standard 207 response. + */ + if (err == NULL && response != NULL && *response != NULL) { + apr_text *propstat = NULL; + + if ((flags & DAV_VALIDATE_USE_424) != 0) { + /* manufacture a 424 error to hold the multistatus response(s) */ + return dav_new_error(r->pool, HTTP_FAILED_DEPENDENCY, 0, 0, + "An error occurred on another resource, " + "preventing the requested operation on " + "this resource."); + } + + /* + ** Whatever caused the error, the Request-URI should have a 424 + ** associated with it since we cannot complete the method. + ** + ** For a LOCK operation, insert an empty DAV:lockdiscovery property. + ** For other methods, return a simple 424. + */ + if ((flags & DAV_VALIDATE_ADD_LD) != 0) { + propstat = apr_pcalloc(r->pool, sizeof(*propstat)); + propstat->text = + "<D:propstat>" DEBUG_CR + "<D:prop><D:lockdiscovery/></D:prop>" DEBUG_CR + "<D:status>HTTP/1.1 424 Failed Dependency</D:status>" DEBUG_CR + "</D:propstat>" DEBUG_CR; + } + + /* create the 424 response */ + new_response = apr_pcalloc(r->pool, sizeof(*new_response)); + new_response->href = resource->uri; + new_response->status = HTTP_FAILED_DEPENDENCY; + new_response->propresult.propstats = propstat; + new_response->desc = + "An error occurred on another resource, preventing the " + "requested operation on this resource."; + + new_response->next = *response; + *response = new_response; + + /* manufacture a 207 error for the multistatus response(s) */ + return dav_new_error(r->pool, HTTP_MULTI_STATUS, 0, 0, + "Error(s) occurred on resources during the " + "validation process."); + } + + return err; +} + +/* dav_get_locktoken_list: + * + * Sets ltl to a locktoken_list of all positive locktokens in header, + * else NULL if no If-header, or no positive locktokens. + */ +DAV_DECLARE(dav_error *) dav_get_locktoken_list(request_rec *r, + dav_locktoken_list **ltl) +{ + dav_error *err; + dav_if_header *if_header; + dav_if_state_list *if_state; + dav_locktoken_list *lock_token = NULL; + + *ltl = NULL; + + if ((err = dav_process_if_header(r, &if_header)) != NULL) { + /* ### add a higher-level description? */ + return err; + } + + while (if_header != NULL) { + if_state = if_header->state; /* Beginning of the if_state linked list */ + while (if_state != NULL) { + if (if_state->condition == DAV_IF_COND_NORMAL + && if_state->type == dav_if_opaquelock) { + lock_token = apr_pcalloc(r->pool, sizeof(dav_locktoken_list)); + lock_token->locktoken = if_state->locktoken; + lock_token->next = *ltl; + *ltl = lock_token; + } + if_state = if_state->next; + } + if_header = if_header->next; + } + if (*ltl == NULL) { + /* No nodes added */ + return dav_new_error(r->pool, HTTP_BAD_REQUEST, DAV_ERR_IF_ABSENT, 0, + "No locktokens were specified in the \"If:\" " + "header, so the refresh could not be performed."); + } + + return NULL; +} + +#if 0 /* not needed right now... */ + +static const char *strip_white(const char *s, apr_pool_t *pool) +{ + apr_size_t idx; + + /* trim leading whitespace */ + while (apr_isspace(*s)) /* assume: return false for '\0' */ + ++s; + + /* trim trailing whitespace */ + idx = strlen(s) - 1; + if (apr_isspace(s[idx])) { + char *s2 = apr_pstrdup(pool, s); + + while (apr_isspace(s2[idx]) && idx > 0) + --idx; + s2[idx + 1] = '\0'; + return s2; + } + + return s; +} +#endif + +#define DAV_LABEL_HDR "Label" + +/* dav_add_vary_header + * + * If there were any headers in the request which require a Vary header + * in the response, add it. + */ +DAV_DECLARE(void) dav_add_vary_header(request_rec *in_req, + request_rec *out_req, + const dav_resource *resource) +{ + const dav_hooks_vsn *vsn_hooks = DAV_GET_HOOKS_VSN(in_req); + + /* ### this is probably all wrong... I think there is a function in + ### the Apache API to add things to the Vary header. need to check */ + + /* Only versioning headers require a Vary response header, + * so only do this check if there is a versioning provider */ + if (vsn_hooks != NULL) { + const char *target = apr_table_get(in_req->headers_in, DAV_LABEL_HDR); + + /* If Target-Selector specified, add it to the Vary header */ + if (target != NULL) { + const char *vary = apr_table_get(out_req->headers_out, "Vary"); + + if (vary == NULL) + vary = DAV_LABEL_HDR; + else + vary = apr_pstrcat(out_req->pool, vary, "," DAV_LABEL_HDR, + NULL); + + apr_table_setn(out_req->headers_out, "Vary", vary); + } + } +} + +/* dav_can_auto_checkout + * + * Determine whether auto-checkout is enabled for a resource. + * r - the request_rec + * resource - the resource + * auto_version - the value of the auto_versionable hook for the resource + * lockdb - pointer to lock database (opened if necessary) + * auto_checkout - set to 1 if auto-checkout enabled + */ +static dav_error * dav_can_auto_checkout( + request_rec *r, + dav_resource *resource, + dav_auto_version auto_version, + dav_lockdb **lockdb, + int *auto_checkout) +{ + dav_error *err; + dav_lock *lock_list; + + *auto_checkout = 0; + + if (auto_version == DAV_AUTO_VERSION_ALWAYS) { + *auto_checkout = 1; + } + else if (auto_version == DAV_AUTO_VERSION_LOCKED) { + if (*lockdb == NULL) { + const dav_hooks_locks *locks_hooks = DAV_GET_HOOKS_LOCKS(r); + + if (locks_hooks == NULL) { + return dav_new_error(r->pool, HTTP_INTERNAL_SERVER_ERROR, 0, 0, + "Auto-checkout is only enabled for locked resources, " + "but there is no lock provider."); + } + + if ((err = (*locks_hooks->open_lockdb)(r, 0, 0, lockdb)) != NULL) { + return dav_push_error(r->pool, HTTP_INTERNAL_SERVER_ERROR, 0, + "Cannot open lock database to determine " + "auto-versioning behavior.", + err); + } + } + + if ((err = dav_lock_query(*lockdb, resource, &lock_list)) != NULL) { + return dav_push_error(r->pool, + HTTP_INTERNAL_SERVER_ERROR, 0, + "The locks could not be queried for " + "determining auto-versioning behavior.", + err); + } + + if (lock_list != NULL) + *auto_checkout = 1; + } + + return NULL; +} + +/* see mod_dav.h for docco */ +DAV_DECLARE(dav_error *) dav_auto_checkout( + request_rec *r, + dav_resource *resource, + int parent_only, + dav_auto_version_info *av_info) +{ + const dav_hooks_vsn *vsn_hooks = DAV_GET_HOOKS_VSN(r); + dav_lockdb *lockdb = NULL; + dav_error *err = NULL; + + /* Initialize results */ + memset(av_info, 0, sizeof(*av_info)); + + /* if no versioning provider, just return */ + if (vsn_hooks == NULL) + return NULL; + + /* check parent resource if requested or if resource must be created */ + if (!resource->exists || parent_only) { + dav_resource *parent; + + if ((err = (*resource->hooks->get_parent_resource)(resource, + &parent)) != NULL) + goto done; + + if (parent == NULL || !parent->exists) { + err = dav_new_error(r->pool, HTTP_CONFLICT, 0, 0, + apr_psprintf(r->pool, + "Missing one or more intermediate " + "collections. Cannot create resource %s.", + ap_escape_html(r->pool, resource->uri))); + goto done; + } + + av_info->parent_resource = parent; + + /* if parent versioned and not checked out, see if it can be */ + if (parent->versioned && !parent->working) { + int checkout_parent; + + if ((err = dav_can_auto_checkout(r, parent, + (*vsn_hooks->auto_versionable)(parent), + &lockdb, &checkout_parent)) + != NULL) { + goto done; + } + + if (!checkout_parent) { + err = dav_new_error(r->pool, HTTP_CONFLICT, 0, 0, + "<DAV:cannot-modify-checked-in-parent>"); + goto done; + } + + /* Try to checkout the parent collection. + * Note that auto-versioning can only be applied to a version selector, + * so no separate working resource will be created. + */ + if ((err = (*vsn_hooks->checkout)(parent, 1 /*auto_checkout*/, + 0, 0, 0, NULL, NULL)) + != NULL) + { + err = dav_push_error(r->pool, HTTP_CONFLICT, 0, + apr_psprintf(r->pool, + "Unable to auto-checkout parent collection. " + "Cannot create resource %s.", + ap_escape_html(r->pool, resource->uri)), + err); + goto done; + } + + /* remember that parent was checked out */ + av_info->parent_checkedout = 1; + } + } + + /* if only checking parent, we're done */ + if (parent_only) + goto done; + + /* if creating a new resource, see if it should be version-controlled */ + if (!resource->exists + && (*vsn_hooks->auto_versionable)(resource) == DAV_AUTO_VERSION_ALWAYS) { + + if ((err = (*vsn_hooks->vsn_control)(resource, NULL)) != NULL) { + err = dav_push_error(r->pool, HTTP_CONFLICT, 0, + apr_psprintf(r->pool, + "Unable to create versioned resource %s.", + ap_escape_html(r->pool, resource->uri)), + err); + goto done; + } + + /* remember that resource was created */ + av_info->resource_versioned = 1; + } + + /* if resource is versioned, make sure it is checked out */ + if (resource->versioned && !resource->working) { + int checkout_resource; + + if ((err = dav_can_auto_checkout(r, resource, + (*vsn_hooks->auto_versionable)(resource), + &lockdb, &checkout_resource)) != NULL) { + goto done; + } + + if (!checkout_resource) { + err = dav_new_error(r->pool, HTTP_CONFLICT, 0, 0, + "<DAV:cannot-modify-version-controlled-content>"); + goto done; + } + + /* Auto-versioning can only be applied to version selectors, so + * no separate working resource will be created. */ + if ((err = (*vsn_hooks->checkout)(resource, 1 /*auto_checkout*/, + 0, 0, 0, NULL, NULL)) + != NULL) + { + err = dav_push_error(r->pool, HTTP_CONFLICT, 0, + apr_psprintf(r->pool, + "Unable to checkout resource %s.", + ap_escape_html(r->pool, resource->uri)), + err); + goto done; + } + + /* remember that resource was checked out */ + av_info->resource_checkedout = 1; + } + +done: + + /* make sure lock database is closed */ + if (lockdb != NULL) + (*lockdb->hooks->close_lockdb)(lockdb); + + /* if an error occurred, undo any auto-versioning operations already done */ + if (err != NULL) { + dav_auto_checkin(r, resource, 1 /*undo*/, 0 /*unlock*/, av_info); + return err; + } + + return NULL; +} + +/* see mod_dav.h for docco */ +DAV_DECLARE(dav_error *) dav_auto_checkin( + request_rec *r, + dav_resource *resource, + int undo, + int unlock, + dav_auto_version_info *av_info) +{ + const dav_hooks_vsn *vsn_hooks = DAV_GET_HOOKS_VSN(r); + dav_error *err = NULL; + dav_auto_version auto_version; + + /* If no versioning provider, this is a no-op */ + if (vsn_hooks == NULL) + return NULL; + + /* If undoing auto-checkouts, then do uncheckouts */ + if (undo) { + if (resource != NULL) { + if (av_info->resource_checkedout) { + if ((err = (*vsn_hooks->uncheckout)(resource)) != NULL) { + return dav_push_error(r->pool, HTTP_INTERNAL_SERVER_ERROR, 0, + apr_psprintf(r->pool, + "Unable to undo auto-checkout " + "of resource %s.", + ap_escape_html(r->pool, resource->uri)), + err); + } + } + + if (av_info->resource_versioned) { + dav_response *response; + + /* ### should we do anything with the response? */ + if ((err = (*resource->hooks->remove_resource)(resource, + &response)) != NULL) { + return dav_push_error(r->pool, HTTP_INTERNAL_SERVER_ERROR, 0, + apr_psprintf(r->pool, + "Unable to undo auto-version-control " + "of resource %s.", + ap_escape_html(r->pool, resource->uri)), + err); + } + } + } + + if (av_info->parent_resource != NULL && av_info->parent_checkedout) { + if ((err = (*vsn_hooks->uncheckout)(av_info->parent_resource)) != NULL) { + return dav_push_error(r->pool, HTTP_INTERNAL_SERVER_ERROR, 0, + apr_psprintf(r->pool, + "Unable to undo auto-checkout " + "of parent collection %s.", + ap_escape_html(r->pool, av_info->parent_resource->uri)), + err); + } + } + + return NULL; + } + + /* If the resource was checked out, and auto-checkin is enabled, + * then check it in. + */ + if (resource != NULL && resource->working + && (unlock || av_info->resource_checkedout)) { + + auto_version = (*vsn_hooks->auto_versionable)(resource); + + if (auto_version == DAV_AUTO_VERSION_ALWAYS || + (unlock && (auto_version == DAV_AUTO_VERSION_LOCKED))) { + + if ((err = (*vsn_hooks->checkin)(resource, + 0 /*keep_checked_out*/, NULL)) + != NULL) { + return dav_push_error(r->pool, HTTP_INTERNAL_SERVER_ERROR, 0, + apr_psprintf(r->pool, + "Unable to auto-checkin resource %s.", + ap_escape_html(r->pool, resource->uri)), + err); + } + } + } + + /* If parent resource was checked out, and auto-checkin is enabled, + * then check it in. + */ + if (!unlock + && av_info->parent_checkedout + && av_info->parent_resource != NULL + && av_info->parent_resource->working) { + + auto_version = (*vsn_hooks->auto_versionable)(av_info->parent_resource); + + if (auto_version == DAV_AUTO_VERSION_ALWAYS) { + if ((err = (*vsn_hooks->checkin)(av_info->parent_resource, + 0 /*keep_checked_out*/, NULL)) + != NULL) { + return dav_push_error(r->pool, HTTP_INTERNAL_SERVER_ERROR, 0, + apr_psprintf(r->pool, + "Unable to auto-checkin parent collection %s.", + ap_escape_html(r->pool, av_info->parent_resource->uri)), + err); + } + } + } + + return NULL; +} |