From 6f86862edbcf4b71823e556824636dbaff752990 Mon Sep 17 00:00:00 2001 From: Vincent Sanders Date: Sat, 28 Apr 2012 13:24:09 +0000 Subject: cleanup and reorganise low level cache source ready for extending with disc cache svn path=/trunk/netsurf/; revision=13893 --- content/llcache.c | 3374 ++++++++++++++++++++++++++--------------------------- 1 file changed, 1659 insertions(+), 1715 deletions(-) (limited to 'content/llcache.c') diff --git a/content/llcache.c b/content/llcache.c index 503fea40c..29f1a557c 100644 --- a/content/llcache.c +++ b/content/llcache.c @@ -144,6 +144,7 @@ struct llcache_object { struct llcache_s { /** Handler for fetch-related queries */ llcache_query_callback query_cb; + /** Data for fetch-related query handler */ void *query_cb_pw; @@ -164,757 +165,714 @@ static lwc_string *llcache_file_lwc; static lwc_string *llcache_about_lwc; static lwc_string *llcache_resource_lwc; +/* forward referenced callback function */ +static void llcache_fetch_callback(const fetch_msg *msg, void *p); -static nserror llcache_object_user_new(llcache_handle_callback cb, void *pw, - llcache_object_user **user); -static nserror llcache_object_user_destroy(llcache_object_user *user); -static nserror llcache_object_retrieve(nsurl *url, uint32_t flags, - nsurl *referer, const llcache_post_data *post, - uint32_t redirect_count, llcache_object **result); -static nserror llcache_object_retrieve_from_cache(nsurl *url, - uint32_t flags, nsurl *referer, - const llcache_post_data *post, uint32_t redirect_count, - llcache_object **result); -static bool llcache_object_is_fresh(const llcache_object *object); -static nserror llcache_object_cache_update(llcache_object *object); -static nserror llcache_object_clone_cache_data(llcache_object *source, - llcache_object *destination, bool deep); -static nserror llcache_object_fetch(llcache_object *object, uint32_t flags, - nsurl *referer, const llcache_post_data *post, - uint32_t redirect_count); -static nserror llcache_object_refetch(llcache_object *object); +/****************************************************************************** + * Low-level cache internals * + ******************************************************************************/ -static nserror llcache_object_new(nsurl *url, llcache_object **result); -static nserror llcache_object_destroy(llcache_object *object); -static nserror llcache_object_add_user(llcache_object *object, - llcache_object_user *user); -static nserror llcache_object_remove_user(llcache_object *object, - llcache_object_user *user); -static llcache_object_user *llcache_object_find_user( - const llcache_handle *handle); - -static nserror llcache_object_add_to_list(llcache_object *object, - llcache_object **list); -static nserror llcache_object_remove_from_list(llcache_object *object, - llcache_object **list); -static bool llcache_object_in_list(const llcache_object *object, - const llcache_object *list); +/** + * Create a new object user + * + * \param cb Callback routine + * \param pw Private data for callback + * \param user Pointer to location to receive result + * \return NSERROR_OK on success, appropriate error otherwise + */ +static nserror llcache_object_user_new(llcache_handle_callback cb, void *pw, + llcache_object_user **user) +{ + llcache_handle *h; + llcache_object_user *u; -static nserror llcache_object_notify_users(llcache_object *object); + h = calloc(1, sizeof(llcache_handle)); + if (h == NULL) + return NSERROR_NOMEM; -static nserror llcache_object_snapshot(llcache_object *object, - llcache_object **snapshot); + u = calloc(1, sizeof(llcache_object_user)); + if (u == NULL) { + free(h); + return NSERROR_NOMEM; + } -static nserror llcache_post_data_clone(const llcache_post_data *orig, - llcache_post_data **clone); + h->cb = cb; + h->pw = pw; -static nserror llcache_query_handle_response(bool proceed, void *cbpw); + u->handle = h; -static void llcache_fetch_callback(const fetch_msg *msg, void *p); -static nserror llcache_fetch_redirect(llcache_object *object, - const char *target, llcache_object **replacement); -static nserror llcache_fetch_notmodified(llcache_object *object, - llcache_object **replacement); -static nserror llcache_fetch_split_header(const uint8_t *data, size_t len, - char **name, char **value); -static nserror llcache_fetch_parse_header(llcache_object *object, - const uint8_t *data, size_t len, char **name, char **value); -static nserror llcache_fetch_process_header(llcache_object *object, - const uint8_t *data, size_t len); -static nserror llcache_fetch_process_data(llcache_object *object, - const uint8_t *data, size_t len); -static nserror llcache_fetch_auth(llcache_object *object, - const char *realm); -static nserror llcache_fetch_cert_error(llcache_object *object, - const struct ssl_cert_info *certs, size_t num); +#ifdef LLCACHE_TRACE + LOG(("Created user %p (%p, %p, %p)", u, h, (void *) cb, pw)); +#endif -/* Destroy headers */ -static inline void llcache_destroy_headers(llcache_object *object) -{ - while (object->num_headers > 0) { - object->num_headers--; + *user = u; - free(object->headers[object->num_headers].name); - free(object->headers[object->num_headers].value); - } - free(object->headers); - object->headers = NULL; + return NSERROR_OK; } -/* Invalidate cache control data */ -static inline void llcache_invalidate_cache_control_data(llcache_object *object) +/** + * Destroy an object user + * + * \param user User to destroy + * \return NSERROR_OK on success, appropriate error otherwise + * + * \pre User is not attached to an object + */ +static nserror llcache_object_user_destroy(llcache_object_user *user) { - free(object->cache.etag); - memset(&(object->cache), 0, sizeof(llcache_cache_control)); +#ifdef LLCACHE_TRACE + LOG(("Destroyed user %p", user)); +#endif + + assert(user->next == NULL); + assert(user->prev == NULL); + + if (user->handle != NULL) + free(user->handle); - object->cache.age = INVALID_AGE; - object->cache.max_age = INVALID_AGE; + free(user); + + return NSERROR_OK; } +/** + * Remove a user from a low-level cache object + * + * \param object Object to remove user from + * \param user User to remove + * \return NSERROR_OK. + */ +static nserror llcache_object_remove_user(llcache_object *object, + llcache_object_user *user) +{ + assert(user != NULL); + assert(object != NULL); + assert(object->users != NULL); + assert(user->handle == NULL || user->handle->object == object); + assert((user->prev != NULL) || (object->users == user)); + + if (user == object->users) + object->users = user->next; + else + user->prev->next = user->next; + + if (user->next != NULL) + user->next->prev = user->prev; + + user->next = user->prev = NULL; + +#ifdef LLCACHE_TRACE + LOG(("Removing user %p from %p", user, object)); +#endif -/****************************************************************************** - * Public API * - ******************************************************************************/ + return NSERROR_OK; +} -/* See llcache.h for documentation */ -nserror -llcache_initialise(llcache_query_callback cb, void *pw, uint32_t llcache_limit) +/** + * Iterate the users of an object, calling their callbacks. + * + * \param object The object to iterate + * \param event The event to pass to the callback. + * \return NSERROR_OK on success, appropriate error otherwise. + */ +static nserror llcache_send_event_to_users(llcache_object *object, + llcache_event *event) { - llcache = calloc(1, sizeof(struct llcache_s)); - if (llcache == NULL) { - return NSERROR_NOMEM; - } + nserror error = NSERROR_OK; + llcache_object_user *user, *next_user; + + user = object->users; + while (user != NULL) { + user->iterator_target = true; - llcache->query_cb = cb; - llcache->query_cb_pw = pw; - llcache->limit = llcache_limit; + error = user->handle->cb(user->handle, event, + user->handle->pw); - /* Create static scheme strings */ - if (lwc_intern_string("file", SLEN("file"), - &llcache_file_lwc) != lwc_error_ok) - return NSERROR_NOMEM; + next_user = user->next; - if (lwc_intern_string("about", SLEN("about"), - &llcache_about_lwc) != lwc_error_ok) - return NSERROR_NOMEM; + user->iterator_target = false; - if (lwc_intern_string("resource", SLEN("resource"), - &llcache_resource_lwc) != lwc_error_ok) - return NSERROR_NOMEM; + if (user->queued_for_delete) { + llcache_object_remove_user(object, user); + llcache_object_user_destroy(user); + } - LOG(("llcache initialised with a limit of %d bytes", llcache_limit)); + if (error != NSERROR_OK) + break; - return NSERROR_OK; + user = next_user; + } + + return error; } -/* See llcache.h for documentation */ -void llcache_finalise(void) +/** + * Create a new low-level cache object + * + * \param url URL of object to create + * \param result Pointer to location to receive result + * \return NSERROR_OK on success, appropriate error otherwise + */ +static nserror llcache_object_new(nsurl *url, llcache_object **result) { - llcache_object *object, *next; - - /* Clean uncached objects */ - for (object = llcache->uncached_objects; object != NULL; object = next) { - llcache_object_user *user, *next_user; - - next = object->next; - - for (user = object->users; user != NULL; user = next_user) { - next_user = user->next; + llcache_object *obj = calloc(1, sizeof(llcache_object)); + if (obj == NULL) + return NSERROR_NOMEM; - if (user->handle != NULL) - free(user->handle); +#ifdef LLCACHE_TRACE + LOG(("Created object %p (%s)", obj, nsurl_access(url))); +#endif - free(user); - } + obj->url = nsurl_ref(url); - /* Fetch system has already been destroyed */ - object->fetch.fetch = NULL; + *result = obj; - llcache_object_destroy(object); - } + return NSERROR_OK; +} - /* Clean cached objects */ - for (object = llcache->cached_objects; object != NULL; object = next) { - llcache_object_user *user, *next_user; +/** + * Clone a POST data object + * + * \param orig Object to clone + * \param clone Pointer to location to receive clone + * \return NSERROR_OK on success, appropriate error otherwise + */ +static nserror llcache_post_data_clone(const llcache_post_data *orig, + llcache_post_data **clone) +{ + llcache_post_data *post_clone; - next = object->next; + post_clone = calloc(1, sizeof(llcache_post_data)); + if (post_clone == NULL) + return NSERROR_NOMEM; - for (user = object->users; user != NULL; user = next_user) { - next_user = user->next; + post_clone->type = orig->type; - if (user->handle != NULL) - free(user->handle); + /* Deep-copy the type-specific data */ + if (orig->type == LLCACHE_POST_URL_ENCODED) { + post_clone->data.urlenc = strdup(orig->data.urlenc); + if (post_clone->data.urlenc == NULL) { + free(post_clone); - free(user); + return NSERROR_NOMEM; } + } else { + post_clone->data.multipart = fetch_multipart_data_clone( + orig->data.multipart); + if (post_clone->data.multipart == NULL) { + free(post_clone); - /* Fetch system has already been destroyed */ - object->fetch.fetch = NULL; - - llcache_object_destroy(object); + return NSERROR_NOMEM; + } } - /* Unref static scheme lwc strings */ - lwc_string_unref(llcache_file_lwc); - lwc_string_unref(llcache_about_lwc); - lwc_string_unref(llcache_resource_lwc); + *clone = post_clone; - free(llcache); - llcache = NULL; + return NSERROR_OK; } -/* See llcache.h for documentation */ -nserror llcache_poll(void) +/** + * Split a fetch header into name and value + * + * \param data Header string + * \param len Byte length of header + * \param name Pointer to location to receive header name + * \param value Pointer to location to receive header value + * \return NSERROR_OK on success, appropriate error otherwise + */ +static nserror llcache_fetch_split_header(const uint8_t *data, size_t len, + char **name, char **value) { - llcache_object *object; - - fetch_poll(); - - /* Catch new users up with state of objects */ - for (object = llcache->cached_objects; object != NULL; - object = object->next) { - llcache_object_notify_users(object); - } + char *n, *v; + const uint8_t *colon; - for (object = llcache->uncached_objects; object != NULL; - object = object->next) { - llcache_object_notify_users(object); + /* Find colon */ + colon = (const uint8_t *) strchr((const char *) data, ':'); + if (colon == NULL) { + /* Failed, assume a key with no value */ + n = strdup((const char *) data); + if (n == NULL) + return NSERROR_NOMEM; + + v = strdup(""); + if (v == NULL) { + free(n); + return NSERROR_NOMEM; + } + } else { + /* Split header into name & value */ + + /* Strip leading whitespace from name */ + while (data[0] == ' ' || data[0] == '\t' || + data[0] == '\r' || data[0] == '\n') { + data++; + } + + /* Strip trailing whitespace from name */ + while (colon > data && (colon[-1] == ' ' || + colon[-1] == '\t' || colon[-1] == '\r' || + colon[-1] == '\n')) + colon--; + + n = strndup((const char *) data, colon - data); + if (n == NULL) + return NSERROR_NOMEM; + + /* Find colon again */ + while (*colon != ':') { + colon++; + } + + /* Skip over colon and any subsequent whitespace */ + do { + colon++; + } while (*colon == ' ' || *colon == '\t' || + *colon == '\r' || *colon == '\n'); + + /* Strip trailing whitespace from value */ + while (len > 0 && (data[len - 1] == ' ' || + data[len - 1] == '\t' || + data[len - 1] == '\r' || + data[len - 1] == '\n')) { + len--; + } + + v = strndup((const char *) colon, len - (colon - data)); + if (v == NULL) { + free(n); + return NSERROR_NOMEM; + } } + *name = n; + *value = v; + return NSERROR_OK; } -/* See llcache.h for documentation */ -nserror llcache_handle_retrieve(nsurl *url, uint32_t flags, - nsurl *referer, const llcache_post_data *post, - llcache_handle_callback cb, void *pw, - llcache_handle **result) +/** + * Parse a fetch header + * + * \param object Object to parse header for + * \param data Header string + * \param len Byte length of header + * \param name Pointer to location to receive header name + * \param value Pointer to location to receive header value + * \return NSERROR_OK on success, appropriate error otherwise + * + * \note This function also has the side-effect of updating + * the cache control data for the object if an interesting + * header is encountered + */ +static nserror llcache_fetch_parse_header(llcache_object *object, + const uint8_t *data, size_t len, char **name, char **value) { nserror error; - llcache_object_user *user; - llcache_object *object; - /* Can we fetch this URL at all? */ - if (fetch_can_fetch(url) == false) - return NSERROR_NO_FETCH_HANDLER; + /* Set fetch response time if not already set */ + if (object->cache.res_time == 0) + object->cache.res_time = time(NULL); - /* Create a new object user */ - error = llcache_object_user_new(cb, pw, &user); + /* Decompose header into name-value pair */ + error = llcache_fetch_split_header(data, len, name, value); if (error != NSERROR_OK) return error; - /* Retrieve a suitable object from the cache, - * creating a new one if needed. */ - error = llcache_object_retrieve(url, flags, referer, post, 0, &object); - if (error != NSERROR_OK) { - llcache_object_user_destroy(user); - return error; - } + /* Parse cache headers to populate cache control data */ +#define SKIP_ST(p) while (*p != '\0' && (*p == ' ' || *p == '\t')) p++ - /* Add user to object */ - llcache_object_add_user(object, user); + if (5 < len && strcasecmp(*name, "Date") == 0) { + /* extract Date header */ + object->cache.date = curl_getdate(*value, NULL); + } else if (4 < len && strcasecmp(*name, "Age") == 0) { + /* extract Age header */ + if ('0' <= **value && **value <= '9') + object->cache.age = atoi(*value); + } else if (8 < len && strcasecmp(*name, "Expires") == 0) { + /* extract Expires header */ + object->cache.expires = curl_getdate(*value, NULL); + } else if (14 < len && strcasecmp(*name, "Cache-Control") == 0) { + /* extract and parse Cache-Control header */ + const char *start = *value; + const char *comma = *value; - *result = user->handle; + while (*comma != '\0') { + while (*comma != '\0' && *comma != ',') + comma++; - return NSERROR_OK; -} + if (8 < comma - start && (strncasecmp(start, + "no-cache", 8) == 0 || + strncasecmp(start, "no-store", 8) == 0)) + /* When we get a disk cache we should + * distinguish between these two */ + object->cache.no_cache = LLCACHE_VALIDATE_ALWAYS; + else if (7 < comma - start && + strncasecmp(start, "max-age", 7) == 0) { + /* Find '=' */ + while (start < comma && *start != '=') + start++; -/* See llcache.h for documentation */ -nserror llcache_handle_change_callback(llcache_handle *handle, - llcache_handle_callback cb, void *pw) -{ - handle->cb = cb; - handle->pw = pw; + /* Skip over it */ + start++; - return NSERROR_OK; -} + /* Skip whitespace */ + SKIP_ST(start); -/* See llcache.h for documentation */ -nserror llcache_handle_release(llcache_handle *handle) -{ - nserror error = NSERROR_OK; - llcache_object *object = handle->object; - llcache_object_user *user = llcache_object_find_user(handle); + if (start < comma) + object->cache.max_age = atoi(start); + } - assert(user != NULL); + if (*comma != '\0') { + /* Skip past comma */ + comma++; + /* Skip whitespace */ + SKIP_ST(comma); + } - if (user->iterator_target) { - /* Can't remove / delete user object if it's - * the target of an iterator */ - user->queued_for_delete = true; - } else { - /* Remove the user from the object and destroy it */ - error = llcache_object_remove_user(object, user); - if (error == NSERROR_OK) { - error = llcache_object_user_destroy(user); + /* Set start for next token */ + start = comma; } + } else if (5 < len && strcasecmp(*name, "ETag") == 0) { + /* extract ETag header */ + free(object->cache.etag); + object->cache.etag = strdup(*value); + if (object->cache.etag == NULL) + return NSERROR_NOMEM; + } else if (14 < len && strcasecmp(*name, "Last-Modified") == 0) { + /* extract Last-Modified header */ + object->cache.last_modified = curl_getdate(*value, NULL); } - - return error; + +#undef SKIP_ST + + return NSERROR_OK; } -/* See llcache.h for documentation */ -nserror llcache_handle_clone(llcache_handle *handle, llcache_handle **result) +/* Destroy headers */ +static inline void llcache_destroy_headers(llcache_object *object) { - nserror error; - llcache_object_user *newuser; - - error = llcache_object_user_new(handle->cb, handle->pw, &newuser); - if (error == NSERROR_OK) { - llcache_object_add_user(handle->object, newuser); - newuser->handle->state = handle->state; - *result = newuser->handle; + while (object->num_headers > 0) { + object->num_headers--; + + free(object->headers[object->num_headers].name); + free(object->headers[object->num_headers].value); } - - return error; + free(object->headers); + object->headers = NULL; } -/* See llcache.h for documentation */ -nserror llcache_handle_abort(llcache_handle *handle) +/* Invalidate cache control data */ +static inline void llcache_invalidate_cache_control_data(llcache_object *object) { - llcache_object_user *user = llcache_object_find_user(handle); - llcache_object *object = handle->object, *newobject; - nserror error = NSERROR_OK; - bool all_alone = true; - - /* Determine if we are the only user */ - if (user->prev != NULL) - all_alone = false; - if (user->next != NULL) - all_alone = false; - - if (all_alone == false) { - /* We must snapshot this object */ - error = llcache_object_snapshot(object, &newobject); - if (error != NSERROR_OK) - return error; + free(object->cache.etag); + memset(&(object->cache), 0, sizeof(llcache_cache_control)); - /* Move across to the new object */ - if (user->iterator_target) { - /* User is current iterator target, clone it */ - llcache_object_user *newuser = - calloc(1, sizeof(llcache_object_user)); - if (newuser == NULL) { - llcache_object_destroy(newobject); - return NSERROR_NOMEM; - } + object->cache.age = INVALID_AGE; + object->cache.max_age = INVALID_AGE; +} - /* Move handle across to clone */ - newuser->handle = user->handle; - user->handle = NULL; +/** + * Process a fetch header + * + * \param object Object being fetched + * \param data Header string + * \param len Byte length of header + * \return NSERROR_OK on success, appropriate error otherwise + */ +static nserror llcache_fetch_process_header(llcache_object *object, + const uint8_t *data, size_t len) +{ + nserror error; + char *name, *value; + llcache_header *temp; - /* Mark user as needing deletion */ - user->queued_for_delete = true; + /* The headers for multiple HTTP responses may be delivered to us if + * the fetch layer receives a 401 response for which it has + * authentication credentials. This will result in a silent re-request + * after which we'll receive the actual response headers for the + * object we want to fetch (assuming that the credentials were correct + * of course) + * + * Therefore, if the header is an HTTP response start marker, then we + * must discard any headers we've read so far, reset the cache data + * that we might have computed, and start again. + */ + /** \todo Properly parse the response line */ + if (strncmp((const char *) data, "HTTP/", SLEN("HTTP/")) == 0) { + time_t req_time = object->cache.req_time; - llcache_object_add_user(newobject, newuser); - } else { - llcache_object_remove_user(object, user); - llcache_object_add_user(newobject, user); - } - - /* Add new object to uncached list */ - llcache_object_add_to_list(newobject, - &llcache->uncached_objects); - } else { - /* We're the only user, so abort any fetch in progress */ - if (object->fetch.fetch != NULL) { - fetch_abort(object->fetch.fetch); - object->fetch.fetch = NULL; - } - - object->fetch.state = LLCACHE_FETCH_COMPLETE; - - /* Invalidate cache control data */ llcache_invalidate_cache_control_data(object); - } - - return error; -} - -/* See llcache.h for documentation */ -nserror llcache_handle_force_stream(llcache_handle *handle) -{ - llcache_object_user *user = llcache_object_find_user(handle); - llcache_object *object = handle->object; - /* Cannot stream if there are multiple users */ - if (user->prev != NULL || user->next != NULL) - return NSERROR_OK; + /* Restore request time, so we compute object's age correctly */ + object->cache.req_time = req_time; - /* Forcibly uncache this object */ - if (llcache_object_in_list(object, llcache->cached_objects)) { - llcache_object_remove_from_list(object, - &llcache->cached_objects); - llcache_object_add_to_list(object, &llcache->uncached_objects); + llcache_destroy_headers(object); } - object->fetch.flags |= LLCACHE_RETRIEVE_STREAM_DATA; - - return NSERROR_OK; -} + error = llcache_fetch_parse_header(object, data, len, &name, &value); + if (error != NSERROR_OK) + return error; -/* See llcache.h for documentation */ -nserror llcache_handle_invalidate_cache_data(llcache_handle *handle) -{ - if (handle->object != NULL && handle->object->fetch.fetch == NULL && - handle->object->cache.no_cache == - LLCACHE_VALIDATE_FRESH) { - handle->object->cache.no_cache = LLCACHE_VALIDATE_ONCE; + /* Append header data to the object's headers array */ + temp = realloc(object->headers, (object->num_headers + 1) * + sizeof(llcache_header)); + if (temp == NULL) { + free(name); + free(value); + return NSERROR_NOMEM; } - return NSERROR_OK; -} + object->headers = temp; -/* See llcache.h for documentation */ -nsurl *llcache_handle_get_url(const llcache_handle *handle) -{ - return handle->object != NULL ? handle->object->url : NULL; -} + object->headers[object->num_headers].name = name; + object->headers[object->num_headers].value = value; -/* See llcache.h for documentation */ -const uint8_t *llcache_handle_get_source_data(const llcache_handle *handle, - size_t *size) -{ - *size = handle->object != NULL ? handle->object->source_len : 0; + object->num_headers++; - return handle->object != NULL ? handle->object->source_data : NULL; + return NSERROR_OK; } -/* See llcache.h for documentation */ -const char *llcache_handle_get_header(const llcache_handle *handle, - const char *key) +/** + * (Re)fetch an object + * + * \param object Object to refetch + * \return NSERROR_OK on success, appropriate error otherwise + * + * \pre The fetch parameters in object->fetch must be populated + */ +static nserror llcache_object_refetch(llcache_object *object) { - const llcache_object *object = handle->object; - size_t i; - - if (object == NULL) - return NULL; + const char *urlenc = NULL; + struct fetch_multipart_data *multipart = NULL; + char **headers = NULL; + int header_idx = 0; - /* About as trivial as possible */ - for (i = 0; i < object->num_headers; i++) { - if (strcasecmp(key, object->headers[i].name) == 0) - return object->headers[i].value; + if (object->fetch.post != NULL) { + if (object->fetch.post->type == LLCACHE_POST_URL_ENCODED) + urlenc = object->fetch.post->data.urlenc; + else + multipart = object->fetch.post->data.multipart; } - return NULL; -} + /* Generate cache-control headers */ + headers = malloc(3 * sizeof(char *)); + if (headers == NULL) + return NSERROR_NOMEM; -/* See llcache.h for documentation */ -bool llcache_handle_references_same_object(const llcache_handle *a, - const llcache_handle *b) -{ - return a->object == b->object; -} + if (object->cache.etag != NULL) { + const size_t len = SLEN("If-None-Match: ") + + strlen(object->cache.etag) + 1; -/****************************************************************************** - * Low-level cache internals * - ******************************************************************************/ + headers[header_idx] = malloc(len); + if (headers[header_idx] == NULL) { + free(headers); + return NSERROR_NOMEM; + } -/** - * Create a new object user - * - * \param cb Callback routine - * \param pw Private data for callback - * \param user Pointer to location to receive result - * \return NSERROR_OK on success, appropriate error otherwise - */ -nserror llcache_object_user_new(llcache_handle_callback cb, void *pw, - llcache_object_user **user) -{ - llcache_handle *h; - llcache_object_user *u; + snprintf(headers[header_idx], len, "If-None-Match: %s", + object->cache.etag); - h = calloc(1, sizeof(llcache_handle)); - if (h == NULL) - return NSERROR_NOMEM; + header_idx++; + } + if (object->cache.date != 0) { + /* Maximum length of an RFC 1123 date is 29 bytes */ + const size_t len = SLEN("If-Modified-Since: ") + 29 + 1; - u = calloc(1, sizeof(llcache_object_user)); - if (u == NULL) { - free(h); - return NSERROR_NOMEM; + headers[header_idx] = malloc(len); + if (headers[header_idx] == NULL) { + while (--header_idx >= 0) + free(headers[header_idx]); + free(headers); + return NSERROR_NOMEM; + } + + snprintf(headers[header_idx], len, "If-Modified-Since: %s", + rfc1123_date(object->cache.date)); + + header_idx++; } + headers[header_idx] = NULL; - h->cb = cb; - h->pw = pw; + /* Reset cache control data */ + llcache_invalidate_cache_control_data(object); + object->cache.req_time = time(NULL); - u->handle = h; + /* Reset fetch state */ + object->fetch.state = LLCACHE_FETCH_INIT; #ifdef LLCACHE_TRACE - LOG(("Created user %p (%p, %p, %p)", u, h, (void *) cb, pw)); + LOG(("Refetching %p", object)); #endif - *user = u; + /* Kick off fetch */ + object->fetch.fetch = fetch_start(object->url, object->fetch.referer, + llcache_fetch_callback, object, + object->fetch.flags & LLCACHE_RETRIEVE_NO_ERROR_PAGES, + urlenc, multipart, + object->fetch.flags & LLCACHE_RETRIEVE_VERIFIABLE, + (const char **) headers); + + /* Clean up cache-control headers */ + while (--header_idx >= 0) + free(headers[header_idx]); + free(headers); + + /* Did we succeed in creating a fetch? */ + if (object->fetch.fetch == NULL) + return NSERROR_NOMEM; return NSERROR_OK; } /** - * Destroy an object user + * Kick-off a fetch for an object * - * \param user User to destroy + * \param object Object to fetch + * \param flags Fetch flags + * \param referer Referring URL, or NULL for none + * \param post POST data, or NULL for GET + * \param redirect_count Number of redirects followed so far * \return NSERROR_OK on success, appropriate error otherwise * - * \pre User is not attached to an object - */ -nserror llcache_object_user_destroy(llcache_object_user *user) -{ -#ifdef LLCACHE_TRACE - LOG(("Destroyed user %p", user)); -#endif - - assert(user->next == NULL); - assert(user->prev == NULL); - - if (user->handle != NULL) - free(user->handle); - - free(user); - - return NSERROR_OK; -} - -/** - * Iterate the users of an object, calling their callbacks. - * - * \param object The object to iterate - * \param event The event to pass to the callback. - * \return NSERROR_OK on success, appropriate error otherwise. + * \pre object::url must contain the URL to fetch + * \pre If there is a freshness validation candidate, + * object::candidate and object::cache must be filled in + * \pre There must not be a fetch in progress for \a object */ -static nserror llcache_send_event_to_users(llcache_object *object, - llcache_event *event) +static nserror llcache_object_fetch(llcache_object *object, uint32_t flags, + nsurl *referer, const llcache_post_data *post, + uint32_t redirect_count) { - nserror error = NSERROR_OK; - llcache_object_user *user, *next_user; - - user = object->users; - while (user != NULL) { - user->iterator_target = true; - - error = user->handle->cb(user->handle, event, - user->handle->pw); + nserror error; + nsurl *referer_clone = NULL; + llcache_post_data *post_clone = NULL; - next_user = user->next; +#ifdef LLCACHE_TRACE + LOG(("Starting fetch for %p", object)); +#endif - user->iterator_target = false; + if (post != NULL) { + error = llcache_post_data_clone(post, &post_clone); + if (error != NSERROR_OK) + return error; + } - if (user->queued_for_delete) { - llcache_object_remove_user(object, user); - llcache_object_user_destroy(user); - } + if (referer != NULL) + referer_clone = nsurl_ref(referer); - if (error != NSERROR_OK) - break; + object->fetch.flags = flags; + object->fetch.referer = referer_clone; + object->fetch.post = post_clone; + object->fetch.redirect_count = redirect_count; - user = next_user; - } - - return error; + return llcache_object_refetch(object); } /** - * Retrieve an object from the cache, fetching it if necessary. + * Destroy a low-level cache object * - * \param url URL of object to retrieve - * \param flags Fetch flags - * \param referer Referring URL, or NULL if none - * \param post POST data, or NULL for a GET request - * \param redirect_count Number of redirects followed so far - * \param result Pointer to location to recieve retrieved object + * \param object Object to destroy * \return NSERROR_OK on success, appropriate error otherwise + * + * \pre Object is detached from cache list + * \pre Object has no users + * \pre Object is not a candidate (i.e. object::candidate_count == 0) */ -nserror llcache_object_retrieve(nsurl *url, uint32_t flags, - nsurl *referer, const llcache_post_data *post, - uint32_t redirect_count, llcache_object **result) +static nserror llcache_object_destroy(llcache_object *object) { - nserror error; - llcache_object *obj; - bool has_query; - nsurl *defragmented_url; + size_t i; #ifdef LLCACHE_TRACE - LOG(("Retrieve %s (%x, %s, %p)", url, flags, referer, post)); + LOG(("Destroying object %p", object)); #endif - /** - * Caching Rules: - * - * 1) Forced fetches are never cached - * 2) POST requests are never cached - */ - - /* Look for a query segment */ - has_query = nsurl_has_component(url, NSURL_QUERY); + nsurl_unref(object->url); + free(object->source_data); - /* Get rid of any url fragment */ - if (nsurl_has_component(url, NSURL_FRAGMENT)) { - error = nsurl_defragment(url, &defragmented_url); - if (error != NSERROR_OK) - return error; - } else { - defragmented_url = nsurl_ref(url); + if (object->fetch.fetch != NULL) { + fetch_abort(object->fetch.fetch); + object->fetch.fetch = NULL; } - if (flags & LLCACHE_RETRIEVE_FORCE_FETCH || post != NULL) { - /* Create new object */ - error = llcache_object_new(defragmented_url, &obj); - if (error != NSERROR_OK) { - nsurl_unref(defragmented_url); - return error; - } + if (object->fetch.referer != NULL) + nsurl_unref(object->fetch.referer); - /* Attempt to kick-off fetch */ - error = llcache_object_fetch(obj, flags, referer, post, - redirect_count); - if (error != NSERROR_OK) { - llcache_object_destroy(obj); - nsurl_unref(defragmented_url); - return error; + if (object->fetch.post != NULL) { + if (object->fetch.post->type == LLCACHE_POST_URL_ENCODED) { + free(object->fetch.post->data.urlenc); + } else { + fetch_multipart_data_destroy( + object->fetch.post->data.multipart); } - /* Add new object to uncached list */ - llcache_object_add_to_list(obj, &llcache->uncached_objects); - } else { - error = llcache_object_retrieve_from_cache(defragmented_url, - flags, referer, post, redirect_count, &obj); - if (error != NSERROR_OK) { - nsurl_unref(defragmented_url); - return error; - } + free(object->fetch.post); + } - /* Returned object is already in the cached list */ + free(object->cache.etag); + + for (i = 0; i < object->num_headers; i++) { + free(object->headers[i].name); + free(object->headers[i].value); } - - obj->has_query = has_query; + free(object->headers); + + free(object); -#ifdef LLCACHE_TRACE - LOG(("Retrieved %p", obj)); -#endif - - *result = obj; - - nsurl_unref(defragmented_url); - return NSERROR_OK; } /** - * Retrieve a potentially cached object + * Add a low-level cache object to a cache list * - * \param url URL of object to retrieve - * \param flags Fetch flags - * \param referer Referring URL, or NULL if none - * \param post POST data, or NULL for a GET request - * \param redirect_count Number of redirects followed so far - * \param result Pointer to location to recieve retrieved object - * \return NSERROR_OK on success, appropriate error otherwise + * \param object Object to add + * \param list List to add to + * \return NSERROR_OK */ -nserror llcache_object_retrieve_from_cache(nsurl *url, uint32_t flags, - nsurl *referer, const llcache_post_data *post, - uint32_t redirect_count, llcache_object **result) +static nserror llcache_object_add_to_list(llcache_object *object, + llcache_object **list) { - nserror error; - llcache_object *obj, *newest = NULL; - -#ifdef LLCACHE_TRACE - LOG(("Searching cache for %s (%x %s %p)", url, flags, referer, post)); -#endif - - /* Search for the most recently fetched matching object */ - for (obj = llcache->cached_objects; obj != NULL; obj = obj->next) { + object->prev = NULL; + object->next = *list; - if ((newest == NULL || - obj->cache.req_time > newest->cache.req_time) && - nsurl_compare(obj->url, url, - NSURL_COMPLETE) == true) { - newest = obj; - } - } + if (*list != NULL) + (*list)->prev = object; + *list = object; - if (newest != NULL && llcache_object_is_fresh(newest)) { - /* Found a suitable object, and it's still fresh, so use it */ - obj = newest; + return NSERROR_OK; +} -#ifdef LLCACHE_TRACE - LOG(("Found fresh %p", obj)); -#endif +/** + * Determine if an object is still fresh + * + * \param object Object to consider + * \return True if object is still fresh, false otherwise + */ +static bool llcache_object_is_fresh(const llcache_object *object) +{ + const llcache_cache_control *cd = &object->cache; + int current_age, freshness_lifetime; + time_t now = time(NULL); - /* The client needs to catch up with the object's state. - * This will occur the next time that llcache_poll is called. - */ - } else if (newest != NULL) { - /* Found a candidate object but it needs freshness validation */ + /* Calculate staleness of cached object as per RFC 2616 13.2.3/13.2.4 */ + current_age = max(0, (cd->res_time - cd->date)); + current_age = max(current_age, (cd->age == INVALID_AGE) ? 0 : cd->age); + current_age += cd->res_time - cd->req_time + now - cd->res_time; - /* Create a new object */ - error = llcache_object_new(url, &obj); - if (error != NSERROR_OK) - return error; + /* Determine freshness lifetime of this object */ + if (cd->max_age != INVALID_AGE) + freshness_lifetime = cd->max_age; + else if (cd->expires != 0) + freshness_lifetime = cd->expires - cd->date; + else if (cd->last_modified != 0) + freshness_lifetime = (now - cd->last_modified) / 10; + else + freshness_lifetime = 0; #ifdef LLCACHE_TRACE - LOG(("Found candidate %p (%p)", obj, newest)); -#endif - - /* Clone candidate's cache data */ - error = llcache_object_clone_cache_data(newest, obj, true); - if (error != NSERROR_OK) { - llcache_object_destroy(obj); - return error; - } - - /* Record candidate, so we can fall back if it is still fresh */ - newest->candidate_count++; - obj->candidate = newest; - - /* Attempt to kick-off fetch */ - error = llcache_object_fetch(obj, flags, referer, post, - redirect_count); - if (error != NSERROR_OK) { - newest->candidate_count--; - llcache_object_destroy(obj); - return error; - } - - /* Add new object to cache */ - llcache_object_add_to_list(obj, &llcache->cached_objects); - } else { - /* No object found; create a new one */ - /* Create new object */ - error = llcache_object_new(url, &obj); - if (error != NSERROR_OK) - return error; - -#ifdef LLCACHE_TRACE - LOG(("Not found %p", obj)); -#endif - - /* Attempt to kick-off fetch */ - error = llcache_object_fetch(obj, flags, referer, post, - redirect_count); - if (error != NSERROR_OK) { - llcache_object_destroy(obj); - return error; - } - - /* Add new object to cache */ - llcache_object_add_to_list(obj, &llcache->cached_objects); - } - - *result = obj; - - return NSERROR_OK; -} - -/** - * Determine if an object is still fresh - * - * \param object Object to consider - * \return True if object is still fresh, false otherwise - */ -bool llcache_object_is_fresh(const llcache_object *object) -{ - const llcache_cache_control *cd = &object->cache; - int current_age, freshness_lifetime; - time_t now = time(NULL); - - /* Calculate staleness of cached object as per RFC 2616 13.2.3/13.2.4 */ - current_age = max(0, (cd->res_time - cd->date)); - current_age = max(current_age, (cd->age == INVALID_AGE) ? 0 : cd->age); - current_age += cd->res_time - cd->req_time + now - cd->res_time; - - /* Determine freshness lifetime of this object */ - if (cd->max_age != INVALID_AGE) - freshness_lifetime = cd->max_age; - else if (cd->expires != 0) - freshness_lifetime = cd->expires - cd->date; - else if (cd->last_modified != 0) - freshness_lifetime = (now - cd->last_modified) / 10; - else - freshness_lifetime = 0; - -#ifdef LLCACHE_TRACE - LOG(("%p: (%d > %d || %d != %d)", object, - freshness_lifetime, current_age, - object->fetch.state, LLCACHE_FETCH_COMPLETE)); + LOG(("%p: (%d > %d || %d != %d)", object, + freshness_lifetime, current_age, + object->fetch.state, LLCACHE_FETCH_COMPLETE)); #endif /* The object is fresh if: @@ -932,20 +890,6 @@ bool llcache_object_is_fresh(const llcache_object *object) object->fetch.state != LLCACHE_FETCH_COMPLETE)); } -/** - * Update an object's cache state - * - * \param object Object to update cache for - * \return NSERROR_OK. - */ -nserror llcache_object_cache_update(llcache_object *object) -{ - if (object->cache.date == 0) - object->cache.date = time(NULL); - - return NSERROR_OK; -} - /** * Clone an object's cache data * @@ -956,7 +900,7 @@ nserror llcache_object_cache_update(llcache_object *object) * * \post If \a deep is false, then any pointers in \a source will be set to NULL */ -nserror llcache_object_clone_cache_data(llcache_object *source, +static nserror llcache_object_clone_cache_data(llcache_object *source, llcache_object *destination, bool deep) { /* ETag must be first, as it can fail when deep cloning */ @@ -1004,215 +948,194 @@ nserror llcache_object_clone_cache_data(llcache_object *source, } /** - * Kick-off a fetch for an object + * Retrieve a potentially cached object * - * \param object Object to fetch + * \param url URL of object to retrieve * \param flags Fetch flags - * \param referer Referring URL, or NULL for none - * \param post POST data, or NULL for GET + * \param referer Referring URL, or NULL if none + * \param post POST data, or NULL for a GET request * \param redirect_count Number of redirects followed so far + * \param result Pointer to location to recieve retrieved object * \return NSERROR_OK on success, appropriate error otherwise - * - * \pre object::url must contain the URL to fetch - * \pre If there is a freshness validation candidate, - * object::candidate and object::cache must be filled in - * \pre There must not be a fetch in progress for \a object */ -nserror llcache_object_fetch(llcache_object *object, uint32_t flags, +static nserror llcache_object_retrieve_from_cache(nsurl *url, uint32_t flags, nsurl *referer, const llcache_post_data *post, - uint32_t redirect_count) + uint32_t redirect_count, llcache_object **result) { nserror error; - nsurl *referer_clone = NULL; - llcache_post_data *post_clone = NULL; + llcache_object *obj, *newest = NULL; #ifdef LLCACHE_TRACE - LOG(("Starting fetch for %p", object)); + LOG(("Searching cache for %s (%x %s %p)", url, flags, referer, post)); #endif - if (post != NULL) { - error = llcache_post_data_clone(post, &post_clone); - if (error != NSERROR_OK) - return error; - } - - if (referer != NULL) - referer_clone = nsurl_ref(referer); - - object->fetch.flags = flags; - object->fetch.referer = referer_clone; - object->fetch.post = post_clone; - object->fetch.redirect_count = redirect_count; - - return llcache_object_refetch(object); -} - -/** - * (Re)fetch an object - * - * \param object Object to refetch - * \return NSERROR_OK on success, appropriate error otherwise - * - * \pre The fetch parameters in object->fetch must be populated - */ -nserror llcache_object_refetch(llcache_object *object) -{ - const char *urlenc = NULL; - struct fetch_multipart_data *multipart = NULL; - char **headers = NULL; - int header_idx = 0; + /* Search for the most recently fetched matching object */ + for (obj = llcache->cached_objects; obj != NULL; obj = obj->next) { - if (object->fetch.post != NULL) { - if (object->fetch.post->type == LLCACHE_POST_URL_ENCODED) - urlenc = object->fetch.post->data.urlenc; - else - multipart = object->fetch.post->data.multipart; + if ((newest == NULL || + obj->cache.req_time > newest->cache.req_time) && + nsurl_compare(obj->url, url, + NSURL_COMPLETE) == true) { + newest = obj; + } } - /* Generate cache-control headers */ - headers = malloc(3 * sizeof(char *)); - if (headers == NULL) - return NSERROR_NOMEM; - - if (object->cache.etag != NULL) { - const size_t len = SLEN("If-None-Match: ") + - strlen(object->cache.etag) + 1; + if (newest != NULL && llcache_object_is_fresh(newest)) { + /* Found a suitable object, and it's still fresh, so use it */ + obj = newest; - headers[header_idx] = malloc(len); - if (headers[header_idx] == NULL) { - free(headers); - return NSERROR_NOMEM; - } +#ifdef LLCACHE_TRACE + LOG(("Found fresh %p", obj)); +#endif - snprintf(headers[header_idx], len, "If-None-Match: %s", - object->cache.etag); + /* The client needs to catch up with the object's state. + * This will occur the next time that llcache_poll is called. + */ + } else if (newest != NULL) { + /* Found a candidate object but it needs freshness validation */ - header_idx++; - } - if (object->cache.date != 0) { - /* Maximum length of an RFC 1123 date is 29 bytes */ - const size_t len = SLEN("If-Modified-Since: ") + 29 + 1; + /* Create a new object */ + error = llcache_object_new(url, &obj); + if (error != NSERROR_OK) + return error; - headers[header_idx] = malloc(len); - if (headers[header_idx] == NULL) { - while (--header_idx >= 0) - free(headers[header_idx]); - free(headers); - return NSERROR_NOMEM; - } +#ifdef LLCACHE_TRACE + LOG(("Found candidate %p (%p)", obj, newest)); +#endif - snprintf(headers[header_idx], len, "If-Modified-Since: %s", - rfc1123_date(object->cache.date)); + /* Clone candidate's cache data */ + error = llcache_object_clone_cache_data(newest, obj, true); + if (error != NSERROR_OK) { + llcache_object_destroy(obj); + return error; + } - header_idx++; - } - headers[header_idx] = NULL; + /* Record candidate, so we can fall back if it is still fresh */ + newest->candidate_count++; + obj->candidate = newest; - /* Reset cache control data */ - llcache_invalidate_cache_control_data(object); - object->cache.req_time = time(NULL); + /* Attempt to kick-off fetch */ + error = llcache_object_fetch(obj, flags, referer, post, + redirect_count); + if (error != NSERROR_OK) { + newest->candidate_count--; + llcache_object_destroy(obj); + return error; + } - /* Reset fetch state */ - object->fetch.state = LLCACHE_FETCH_INIT; + /* Add new object to cache */ + llcache_object_add_to_list(obj, &llcache->cached_objects); + } else { + /* No object found; create a new one */ + /* Create new object */ + error = llcache_object_new(url, &obj); + if (error != NSERROR_OK) + return error; #ifdef LLCACHE_TRACE - LOG(("Refetching %p", object)); + LOG(("Not found %p", obj)); #endif - /* Kick off fetch */ - object->fetch.fetch = fetch_start(object->url, object->fetch.referer, - llcache_fetch_callback, object, - object->fetch.flags & LLCACHE_RETRIEVE_NO_ERROR_PAGES, - urlenc, multipart, - object->fetch.flags & LLCACHE_RETRIEVE_VERIFIABLE, - (const char **) headers); + /* Attempt to kick-off fetch */ + error = llcache_object_fetch(obj, flags, referer, post, + redirect_count); + if (error != NSERROR_OK) { + llcache_object_destroy(obj); + return error; + } - /* Clean up cache-control headers */ - while (--header_idx >= 0) - free(headers[header_idx]); - free(headers); + /* Add new object to cache */ + llcache_object_add_to_list(obj, &llcache->cached_objects); + } - /* Did we succeed in creating a fetch? */ - if (object->fetch.fetch == NULL) - return NSERROR_NOMEM; + *result = obj; return NSERROR_OK; } /** - * Create a new low-level cache object + * Retrieve an object from the cache, fetching it if necessary. * - * \param url URL of object to create - * \param result Pointer to location to receive result + * \param url URL of object to retrieve + * \param flags Fetch flags + * \param referer Referring URL, or NULL if none + * \param post POST data, or NULL for a GET request + * \param redirect_count Number of redirects followed so far + * \param result Pointer to location to recieve retrieved object * \return NSERROR_OK on success, appropriate error otherwise */ -nserror llcache_object_new(nsurl *url, llcache_object **result) +static nserror llcache_object_retrieve(nsurl *url, uint32_t flags, + nsurl *referer, const llcache_post_data *post, + uint32_t redirect_count, llcache_object **result) { - llcache_object *obj = calloc(1, sizeof(llcache_object)); - if (obj == NULL) - return NSERROR_NOMEM; + nserror error; + llcache_object *obj; + bool has_query; + nsurl *defragmented_url; #ifdef LLCACHE_TRACE - LOG(("Created object %p (%s)", obj, nsurl_access(url))); + LOG(("Retrieve %s (%x, %s, %p)", url, flags, referer, post)); #endif - obj->url = nsurl_ref(url); - - *result = obj; - - return NSERROR_OK; -} - -/** - * Destroy a low-level cache object - * - * \param object Object to destroy - * \return NSERROR_OK on success, appropriate error otherwise - * - * \pre Object is detached from cache list - * \pre Object has no users - * \pre Object is not a candidate (i.e. object::candidate_count == 0) - */ -nserror llcache_object_destroy(llcache_object *object) -{ - size_t i; - -#ifdef LLCACHE_TRACE - LOG(("Destroying object %p", object)); -#endif + /** + * Caching Rules: + * + * 1) Forced fetches are never cached + * 2) POST requests are never cached + */ - nsurl_unref(object->url); - free(object->source_data); + /* Look for a query segment */ + has_query = nsurl_has_component(url, NSURL_QUERY); - if (object->fetch.fetch != NULL) { - fetch_abort(object->fetch.fetch); - object->fetch.fetch = NULL; + /* Get rid of any url fragment */ + if (nsurl_has_component(url, NSURL_FRAGMENT)) { + error = nsurl_defragment(url, &defragmented_url); + if (error != NSERROR_OK) + return error; + } else { + defragmented_url = nsurl_ref(url); } - if (object->fetch.referer != NULL) - nsurl_unref(object->fetch.referer); - - if (object->fetch.post != NULL) { - if (object->fetch.post->type == LLCACHE_POST_URL_ENCODED) { - free(object->fetch.post->data.urlenc); - } else { - fetch_multipart_data_destroy( - object->fetch.post->data.multipart); + if (flags & LLCACHE_RETRIEVE_FORCE_FETCH || post != NULL) { + /* Create new object */ + error = llcache_object_new(defragmented_url, &obj); + if (error != NSERROR_OK) { + nsurl_unref(defragmented_url); + return error; } - free(object->fetch.post); - } + /* Attempt to kick-off fetch */ + error = llcache_object_fetch(obj, flags, referer, post, + redirect_count); + if (error != NSERROR_OK) { + llcache_object_destroy(obj); + nsurl_unref(defragmented_url); + return error; + } - free(object->cache.etag); + /* Add new object to uncached list */ + llcache_object_add_to_list(obj, &llcache->uncached_objects); + } else { + error = llcache_object_retrieve_from_cache(defragmented_url, + flags, referer, post, redirect_count, &obj); + if (error != NSERROR_OK) { + nsurl_unref(defragmented_url); + return error; + } - for (i = 0; i < object->num_headers; i++) { - free(object->headers[i].name); - free(object->headers[i].value); + /* Returned object is already in the cached list */ } - free(object->headers); - - free(object); + + obj->has_query = has_query; +#ifdef LLCACHE_TRACE + LOG(("Retrieved %p", obj)); +#endif + + *result = obj; + + nsurl_unref(defragmented_url); + return NSERROR_OK; } @@ -1223,7 +1146,7 @@ nserror llcache_object_destroy(llcache_object *object) * \param user User to add * \return NSERROR_OK. */ -nserror llcache_object_add_user(llcache_object *object, +static nserror llcache_object_add_user(llcache_object *object, llcache_object_user *user) { assert(user->next == NULL); @@ -1246,590 +1169,400 @@ nserror llcache_object_add_user(llcache_object *object, } /** - * Remove a user from a low-level cache object + * Handle FETCH_REDIRECT event * - * \param object Object to remove user from - * \param user User to remove - * \return NSERROR_OK. + * \param object Object being redirected + * \param target Target of redirect (may be relative) + * \param replacement Pointer to location to receive replacement object + * \return NSERROR_OK on success, appropriate error otherwise */ -nserror llcache_object_remove_user(llcache_object *object, - llcache_object_user *user) +static nserror llcache_fetch_redirect(llcache_object *object, const char *target, + llcache_object **replacement) { - assert(user != NULL); - assert(object != NULL); - assert(object->users != NULL); - assert(user->handle == NULL || user->handle->object == object); - assert((user->prev != NULL) || (object->users == user)); - - if (user == object->users) - object->users = user->next; - else - user->prev->next = user->next; + nserror error; + llcache_object *dest; + llcache_object_user *user, *next; + const llcache_post_data *post = object->fetch.post; + nsurl *url; + lwc_string *scheme; + lwc_string *object_scheme; + bool match; + /* Extract HTTP response code from the fetch object */ + long http_code = fetch_http_code(object->fetch.fetch); - if (user->next != NULL) - user->next->prev = user->prev; + /* Abort fetch for this object */ + fetch_abort(object->fetch.fetch); + object->fetch.fetch = NULL; - user->next = user->prev = NULL; + /* Invalidate the cache control data */ + llcache_invalidate_cache_control_data(object); + + /* And mark it complete */ + object->fetch.state = LLCACHE_FETCH_COMPLETE; -#ifdef LLCACHE_TRACE - LOG(("Removing user %p from %p", user, object)); -#endif + /* Forcibly stop redirecting if we've followed too many redirects */ +#define REDIRECT_LIMIT 10 + if (object->fetch.redirect_count > REDIRECT_LIMIT) { + llcache_event event; - return NSERROR_OK; -} + LOG(("Too many nested redirects")); -/** - * Find a user of a low-level cache object - * - * \param handle External cache handle to search for - * \return Pointer to corresponding user, or NULL if not found - */ -llcache_object_user *llcache_object_find_user(const llcache_handle *handle) -{ - llcache_object_user *user; + event.type = LLCACHE_EVENT_ERROR; + event.data.msg = messages_get("BadRedirect"); + + return llcache_send_event_to_users(object, &event); + } +#undef REDIRECT_LIMIT - assert(handle->object != NULL); + /* Make target absolute */ + error = nsurl_join(object->url, target, &url); + if (error != NSERROR_OK) + return error; - for (user = handle->object->users; user != NULL; user = user->next) { - if (user->handle == handle) - break; + /* Reject attempts to redirect from unvalidated to validated schemes + * A "validated" scheme is one over which we have some guarantee that + * the source is trustworthy. */ + object_scheme = nsurl_get_component(object->url, NSURL_SCHEME); + scheme = nsurl_get_component(url, NSURL_SCHEME); + + /* resource: and about: are allowed to redirect anywhere */ + if ((lwc_string_isequal(object_scheme, llcache_resource_lwc, + &match) == lwc_error_ok && match == false) && + (lwc_string_isequal(object_scheme, llcache_about_lwc, + &match) == lwc_error_ok && match == false)) { + /* file, about and resource are not valid redirect targets */ + if ((lwc_string_isequal(object_scheme, llcache_file_lwc, + &match) == lwc_error_ok && match == true) || + (lwc_string_isequal(object_scheme, llcache_about_lwc, + &match) == lwc_error_ok && match == true) || + (lwc_string_isequal(object_scheme, llcache_resource_lwc, + &match) == lwc_error_ok && match == true)) { + lwc_string_unref(object_scheme); + lwc_string_unref(scheme); + nsurl_unref(url); + return NSERROR_OK; + } } - return user; -} + lwc_string_unref(scheme); + lwc_string_unref(object_scheme); -/** - * Add a low-level cache object to a cache list - * - * \param object Object to add - * \param list List to add to - * \return NSERROR_OK - */ -nserror llcache_object_add_to_list(llcache_object *object, - llcache_object **list) -{ - object->prev = NULL; - object->next = *list; + /* Bail out if we've no way of handling this URL */ + if (fetch_can_fetch(url) == false) { + nsurl_unref(url); + return NSERROR_OK; + } - if (*list != NULL) - (*list)->prev = object; - *list = object; + if (http_code == 301 || http_code == 302 || http_code == 303) { + /* 301, 302, 303 redirects are all unconditional GET requests */ + post = NULL; + } else if (http_code != 307 || post != NULL) { + /** \todo 300, 305, 307 with POST */ + nsurl_unref(url); + return NSERROR_OK; + } - return NSERROR_OK; -} + /* Attempt to fetch target URL */ + error = llcache_object_retrieve(url, object->fetch.flags, + object->fetch.referer, post, + object->fetch.redirect_count + 1, &dest); -/** - * Remove a low-level cache object from a cache list - * - * \param object Object to remove - * \param list List to remove from - * \return NSERROR_OK + /* No longer require url */ + nsurl_unref(url); + + if (error != NSERROR_OK) + return error; + + /* Move user(s) to replacement object */ + for (user = object->users; user != NULL; user = next) { + next = user->next; + + llcache_object_remove_user(object, user); + llcache_object_add_user(dest, user); + } + + /* Dest is now our object */ + *replacement = dest; + + return NSERROR_OK; +} + +/** + * Update an object's cache state + * + * \param object Object to update cache for + * \return NSERROR_OK. */ -nserror llcache_object_remove_from_list(llcache_object *object, - llcache_object **list) +static nserror llcache_object_cache_update(llcache_object *object) { - if (object == *list) - *list = object->next; - else - object->prev->next = object->next; + if (object->cache.date == 0) + object->cache.date = time(NULL); - if (object->next != NULL) - object->next->prev = object->prev; + return NSERROR_OK; +} + +/** + * Handle FETCH_NOTMODIFIED event + * + * \param object Object to process + * \param replacement Pointer to location to receive replacement object + * \return NSERROR_OK. + */ +static nserror llcache_fetch_notmodified(llcache_object *object, + llcache_object **replacement) +{ + /* There may be no candidate if the server erroneously responded + * to an unconditional request with a 304 Not Modified response. + * In this case, we simply retain the initial object, having + * invalidated it and marked it as complete. + */ + if (object->candidate != NULL) { + llcache_object_user *user, *next; + + /* Move user(s) to candidate content */ + for (user = object->users; user != NULL; user = next) { + next = user->next; + + llcache_object_remove_user(object, user); + llcache_object_add_user(object->candidate, user); + } + + /* Candidate is no longer a candidate for us */ + object->candidate->candidate_count--; + + /* Clone our cache control data into the candidate */ + llcache_object_clone_cache_data(object, object->candidate, + false); + /* Bring candidate's cache data up to date */ + llcache_object_cache_update(object->candidate); + /* Revert no-cache to normal, if required */ + if (object->candidate->cache.no_cache == + LLCACHE_VALIDATE_ONCE) { + object->candidate->cache.no_cache = + LLCACHE_VALIDATE_FRESH; + } + + /* Candidate is now our object */ + *replacement = object->candidate; + object->candidate = NULL; + } else { + /* There was no candidate: retain object */ + *replacement = object; + } + + /* Ensure fetch has stopped */ + fetch_abort(object->fetch.fetch); + object->fetch.fetch = NULL; + + /* Invalidate our cache-control data */ + llcache_invalidate_cache_control_data(object); + + /* Mark it complete */ + object->fetch.state = LLCACHE_FETCH_COMPLETE; + + /* Old object will be flushed from the cache on the next poll */ return NSERROR_OK; } /** - * Determine if a low-level cache object resides in a given list + * Process a chunk of fetched data * - * \param object Object to search for - * \param list List to search in - * \return True if object resides in list, false otherwise + * \param object Object being fetched + * \param data Data to process + * \param len Byte length of data + * \return NSERROR_OK on success, appropriate error otherwise. */ -bool llcache_object_in_list(const llcache_object *object, - const llcache_object *list) +static nserror llcache_fetch_process_data(llcache_object *object, const uint8_t *data, + size_t len) { - while (list != NULL) { - if (list == object) - break; + /* Resize source buffer if it's too small */ + if (object->source_len + len >= object->source_alloc) { + const size_t new_len = object->source_len + len + 64 * 1024; + uint8_t *temp = realloc(object->source_data, new_len); + if (temp == NULL) + return NSERROR_NOMEM; - list = list->next; + object->source_data = temp; + object->source_alloc = new_len; } - return list != NULL; + /* Append this data chunk to source buffer */ + memcpy(object->source_data + object->source_len, data, len); + object->source_len += len; + + return NSERROR_OK; } /** - * Notify users of an object's current state + * Handle a query response * - * \param object Object to notify users about + * \param proceed Whether to proceed with fetch + * \param cbpw Our context for query * \return NSERROR_OK on success, appropriate error otherwise */ -nserror llcache_object_notify_users(llcache_object *object) +static nserror llcache_query_handle_response(bool proceed, void *cbpw) { - nserror error; - llcache_object_user *user, *next_user; llcache_event event; + llcache_object *object = cbpw; -#ifdef LLCACHE_TRACE - bool emitted_notify = false; -#endif + object->fetch.outstanding_query = false; - /** - * State transitions and event emission for users. - * Rows: user state. Cols: object state. - * - * User\Obj INIT HEADERS DATA COMPLETE - * INIT - T T* T* - * HEADERS - - T T* - * DATA - - M T - * COMPLETE - - - - - * - * T => transition user to object state - * M => no transition required, but may need to emit event - * - * The transitions marked with an asterisk can be removed by moving - * the user context into the subsequent state and then reevaluating. - * - * Events are issued as follows: - * - * HAD_HEADERS: on transition from HEADERS -> DATA state - * HAD_DATA : in DATA state, whenever there's new source data - * DONE : on transition from DATA -> COMPLETE state - */ + /* Refetch, using existing fetch parameters, if client allows us to */ + if (proceed) + return llcache_object_refetch(object); - for (user = object->users; user != NULL; user = next_user) { - /* Emit necessary events to bring the user up-to-date */ - llcache_handle *handle = user->handle; - const llcache_fetch_state objstate = object->fetch.state; + /* Invalidate cache-control data */ + llcache_invalidate_cache_control_data(object); - /* Flag that this user is the current iteration target - * in case the client attempts to destroy it underneath us */ - user->iterator_target = true; + /* Mark it complete */ + object->fetch.state = LLCACHE_FETCH_COMPLETE; - /* A note on the computation of next_user: - * - * Within this loop, we may make a number of calls to - * client code. Our contract with clients is that they - * can do whatever they like from within their callback - * handlers. This is so that we limit the pain of - * reentrancy to this module alone. - * - * One of the things a client can do from within its - * callback handler is to remove users from this object's - * user list. In the common case, the user they attempt - * to remove is the current iteration target, and we - * already protect against that causing problems here. - * However, no such protection exists if the client - * attempts to remove other users from this object's - * user list. - * - * Therefore, we cannot compute next_user up-front - * and expect it to remain valid across calls to - * client code (as the identity of the next user - * in the list may change underneath us). Instead, - * we must compute next_user at the point where we - * are about to cause another iteration of this loop - * (i.e. at the very end, and also at the points where - * continue is used) - */ + /* Inform client(s) that object fetch failed */ + event.type = LLCACHE_EVENT_ERROR; + /** \todo More appropriate error message */ + event.data.msg = messages_get("FetchFailed"); + + return llcache_send_event_to_users(object, &event); +} -#ifdef LLCACHE_TRACE - if (handle->state != objstate) { - if (emitted_notify == false) { - LOG(("Notifying users of %p", object)); - emitted_notify = true; - } +/** + * Handle an authentication request + * + * \param object Object being fetched + * \param realm Authentication realm + * \return NSERROR_OK on success, appropriate error otherwise. + */ +static nserror llcache_fetch_auth(llcache_object *object, const char *realm) +{ + const char *auth; + nserror error = NSERROR_OK; - LOG(("User %p state: %d Object state: %d", - user, handle->state, objstate)); - } -#endif + /* Abort fetch for this object */ + fetch_abort(object->fetch.fetch); + object->fetch.fetch = NULL; - /* User: INIT, Obj: HEADERS, DATA, COMPLETE => User->HEADERS */ - if (handle->state == LLCACHE_FETCH_INIT && - objstate > LLCACHE_FETCH_INIT) { - handle->state = LLCACHE_FETCH_HEADERS; - } + /* Invalidate cache-control data */ + llcache_invalidate_cache_control_data(object); - /* User: HEADERS, Obj: DATA, COMPLETE => User->DATA */ - if (handle->state == LLCACHE_FETCH_HEADERS && - objstate > LLCACHE_FETCH_HEADERS) { - handle->state = LLCACHE_FETCH_DATA; + /* Destroy headers */ + llcache_destroy_headers(object); - /* Emit HAD_HEADERS event */ - event.type = LLCACHE_EVENT_HAD_HEADERS; + /* If there was no realm, then default to the URL */ + /** \todo If there was no WWW-Authenticate header, use response body */ + if (realm == NULL) + realm = nsurl_access(object->url); - error = handle->cb(handle, &event, handle->pw); + auth = urldb_get_auth_details(nsurl_access(object->url), realm); - if (user->queued_for_delete) { - next_user = user->next; - llcache_object_remove_user(object, user); - llcache_object_user_destroy(user); - - if (error != NSERROR_OK) - return error; - - continue; - } else if (error == NSERROR_NEED_DATA) { - /* User requested replay */ - handle->state = LLCACHE_FETCH_HEADERS; + if (auth == NULL || object->fetch.tried_with_auth == true) { + /* No authentication details, or tried what we had, so ask */ + object->fetch.tried_with_auth = false; - /* Continue with the next user -- we'll - * reemit the event next time round */ - user->iterator_target = false; - next_user = user->next; - continue; - } else if (error != NSERROR_OK) { - user->iterator_target = false; - return error; - } - } + if (llcache->query_cb != NULL) { + llcache_query query; - /* User: DATA, Obj: DATA, COMPLETE, more source available */ - if (handle->state == LLCACHE_FETCH_DATA && - objstate >= LLCACHE_FETCH_DATA && - object->source_len > handle->bytes) { - size_t orig_handle_read; + /* Emit query for authentication details */ + query.type = LLCACHE_QUERY_AUTH; + query.url = object->url; + query.data.auth.realm = realm; - /* Construct HAD_DATA event */ - event.type = LLCACHE_EVENT_HAD_DATA; - event.data.data.buf = - object->source_data + handle->bytes; - event.data.data.len = - object->source_len - handle->bytes; + object->fetch.outstanding_query = true; - /* Update record of last byte emitted */ - if (object->fetch.flags & - LLCACHE_RETRIEVE_STREAM_DATA) { - /* Streaming, so reset to zero to - * minimise amount of cached source data. - * Additionally, we don't support replay - * when streaming. */ - orig_handle_read = 0; - handle->bytes = object->source_len = 0; - } else { - orig_handle_read = handle->bytes; - handle->bytes = object->source_len; - } + error = llcache->query_cb(&query, llcache->query_cb_pw, + llcache_query_handle_response, object); + } else { + llcache_event event; - /* Emit event */ - error = handle->cb(handle, &event, handle->pw); - if (user->queued_for_delete) { - next_user = user->next; - llcache_object_remove_user(object, user); - llcache_object_user_destroy(user); + /* Mark object complete */ + object->fetch.state = LLCACHE_FETCH_COMPLETE; - if (error != NSERROR_OK) - return error; + /* Inform client(s) that object fetch failed */ + event.type = LLCACHE_EVENT_ERROR; + /** \todo More appropriate error message */ + event.data.msg = messages_get("FetchFailed"); + + error = llcache_send_event_to_users(object, &event); + } + } else { + /* Flag that we've tried to refetch with credentials, so + * that if the fetch fails again, we ask the user again */ + object->fetch.tried_with_auth = true; + error = llcache_object_refetch(object); + } - continue; - } else if (error == NSERROR_NEED_DATA) { - /* User requested replay */ - handle->bytes = orig_handle_read; + return error; +} - /* Continue with the next user -- we'll - * reemit the data next time round */ - user->iterator_target = false; - next_user = user->next; - continue; - } else if (error != NSERROR_OK) { - user->iterator_target = false; - return error; - } - } +/** + * Handle a TLS certificate verification failure + * + * \param object Object being fetched + * \param certs Certificate chain + * \param num Number of certificates in chain + * \return NSERROR_OK on success, appropriate error otherwise + */ +static nserror llcache_fetch_cert_error(llcache_object *object, + const struct ssl_cert_info *certs, size_t num) +{ + nserror error = NSERROR_OK; - /* User: DATA, Obj: COMPLETE => User->COMPLETE */ - if (handle->state == LLCACHE_FETCH_DATA && - objstate > LLCACHE_FETCH_DATA) { - handle->state = LLCACHE_FETCH_COMPLETE; + /* Fetch has been stopped, and destroyed. Invalidate object's pointer */ + object->fetch.fetch = NULL; - /* Emit DONE event */ - event.type = LLCACHE_EVENT_DONE; + /* Invalidate cache-control data */ + llcache_invalidate_cache_control_data(object); - error = handle->cb(handle, &event, handle->pw); - if (user->queued_for_delete) { - next_user = user->next; - llcache_object_remove_user(object, user); - llcache_object_user_destroy(user); + if (llcache->query_cb != NULL) { + llcache_query query; - if (error != NSERROR_OK) - return error; + /* Emit query for TLS */ + query.type = LLCACHE_QUERY_SSL; + query.url = object->url; + query.data.ssl.certs = certs; + query.data.ssl.num = num; - continue; - } else if (error == NSERROR_NEED_DATA) { - /* User requested replay */ - handle->state = LLCACHE_FETCH_DATA; + object->fetch.outstanding_query = true; - /* Continue with the next user -- we'll - * reemit the event next time round */ - user->iterator_target = false; - next_user = user->next; - continue; - } else if (error != NSERROR_OK) { - user->iterator_target = false; - return error; - } - } + error = llcache->query_cb(&query, llcache->query_cb_pw, + llcache_query_handle_response, object); + } else { + llcache_event event; - /* No longer the target of an iterator */ - user->iterator_target = false; + /* Mark object complete */ + object->fetch.state = LLCACHE_FETCH_COMPLETE; - next_user = user->next; + /* Inform client(s) that object fetch failed */ + event.type = LLCACHE_EVENT_ERROR; + /** \todo More appropriate error message */ + event.data.msg = messages_get("FetchFailed"); + + error = llcache_send_event_to_users(object, &event); } - return NSERROR_OK; + return error; } /** - * Make a snapshot of the current state of an llcache_object. - * - * This has the side-effect of the new object being non-cacheable, - * also not-fetching and not a candidate for any other object. - * - * Also note that this new object has no users and at least one - * should be assigned to it before llcache_clean is entered or it - * will be immediately cleaned up. + * Handler for fetch events * - * \param object The object to take a snapshot of - * \param snapshot Pointer to receive snapshot of \a object - * \return NSERROR_OK on success, appropriate error otherwise - */ -nserror llcache_object_snapshot(llcache_object *object, - llcache_object **snapshot) -{ - llcache_object *newobj; - nserror error; - - error = llcache_object_new(object->url, &newobj); - - if (error != NSERROR_OK) - return error; - - newobj->has_query = object->has_query; - - newobj->source_alloc = newobj->source_len = object->source_len; - - if (object->source_len > 0) { - newobj->source_data = malloc(newobj->source_alloc); - if (newobj->source_data == NULL) { - llcache_object_destroy(newobj); - return NSERROR_NOMEM; - } - memcpy(newobj->source_data, object->source_data, - newobj->source_len); - } - - if (object->num_headers > 0) { - newobj->headers = calloc(sizeof(llcache_header), - object->num_headers); - if (newobj->headers == NULL) { - llcache_object_destroy(newobj); - return NSERROR_NOMEM; - } - while (newobj->num_headers < object->num_headers) { - llcache_header *nh = - &(newobj->headers[newobj->num_headers]); - llcache_header *oh = - &(object->headers[newobj->num_headers]); - newobj->num_headers += 1; - nh->name = strdup(oh->name); - nh->value = strdup(oh->value); - if (nh->name == NULL || nh->value == NULL) { - llcache_object_destroy(newobj); - return NSERROR_NOMEM; - } - } - } - - newobj->fetch.state = LLCACHE_FETCH_COMPLETE; - - *snapshot = newobj; - - return NSERROR_OK; -} - -/** - * Attempt to clean the cache + * \param msg Fetch event + * \param p Our private data */ -void llcache_clean(void) +static void llcache_fetch_callback(const fetch_msg *msg, void *p) { - llcache_object *object, *next; - uint32_t llcache_size = 0; + nserror error = NSERROR_OK; + llcache_object *object = p; + llcache_event event; #ifdef LLCACHE_TRACE - LOG(("Attempting cache clean")); + LOG(("Fetch event %d for %p", msg->type, object)); #endif - /* Candidates for cleaning are (in order of priority): - * - * 1) Uncacheable objects with no users - * 2) Stale cacheable objects with no users or pending fetches - * 3) Fresh cacheable objects with no users or pending fetches - */ - - /* 1) Uncacheable objects with no users or fetches */ - for (object = llcache->uncached_objects; object != NULL; object = next) { - next = object->next; - - /* The candidate count of uncacheable objects is always 0 */ - if (object->users == NULL && object->candidate_count == 0 && - object->fetch.fetch == NULL && - object->fetch.outstanding_query == false) { -#ifdef LLCACHE_TRACE - LOG(("Found victim %p", object)); -#endif - llcache_object_remove_from_list(object, - &llcache->uncached_objects); - llcache_object_destroy(object); - } else { - llcache_size += object->source_len + sizeof(*object); - } - } - - /* 2) Stale cacheable objects with no users or pending fetches */ - for (object = llcache->cached_objects; object != NULL; object = next) { - next = object->next; - - if (object->users == NULL && object->candidate_count == 0 && - llcache_object_is_fresh(object) == false && - object->fetch.fetch == NULL && - object->fetch.outstanding_query == false) { -#ifdef LLCACHE_TRACE - LOG(("Found victim %p", object)); -#endif - llcache_object_remove_from_list(object, - &llcache->cached_objects); - llcache_object_destroy(object); - } else { - llcache_size += object->source_len + sizeof(*object); - } - } - - if (llcache->limit < llcache_size) { - /* 3) Fresh cacheable objects with - * no users or pending fetches */ - for (object = llcache->cached_objects; object != NULL; - object = next) { - next = object->next; - - if (object->users == NULL && - object->candidate_count == 0 && - object->fetch.fetch == NULL && - object->fetch.outstanding_query == - false) { -#ifdef LLCACHE_TRACE - LOG(("Found victim %p", object)); -#endif - llcache_size -= - object->source_len + sizeof(*object); - - llcache_object_remove_from_list(object, - &llcache->cached_objects); - llcache_object_destroy(object); - } - } - } - -#ifdef LLCACHE_TRACE - LOG(("Size: %u", llcache_size)); -#endif - -} - -/** - * Clone a POST data object - * - * \param orig Object to clone - * \param clone Pointer to location to receive clone - * \return NSERROR_OK on success, appropriate error otherwise - */ -nserror llcache_post_data_clone(const llcache_post_data *orig, - llcache_post_data **clone) -{ - llcache_post_data *post_clone; - - post_clone = calloc(1, sizeof(llcache_post_data)); - if (post_clone == NULL) - return NSERROR_NOMEM; - - post_clone->type = orig->type; - - /* Deep-copy the type-specific data */ - if (orig->type == LLCACHE_POST_URL_ENCODED) { - post_clone->data.urlenc = strdup(orig->data.urlenc); - if (post_clone->data.urlenc == NULL) { - free(post_clone); - - return NSERROR_NOMEM; - } - } else { - post_clone->data.multipart = fetch_multipart_data_clone( - orig->data.multipart); - if (post_clone->data.multipart == NULL) { - free(post_clone); - - return NSERROR_NOMEM; - } - } - - *clone = post_clone; - - return NSERROR_OK; -} - -/** - * Handle a query response - * - * \param proceed Whether to proceed with fetch - * \param cbpw Our context for query - * \return NSERROR_OK on success, appropriate error otherwise - */ -nserror llcache_query_handle_response(bool proceed, void *cbpw) -{ - llcache_event event; - llcache_object *object = cbpw; - - object->fetch.outstanding_query = false; - - /* Refetch, using existing fetch parameters, if client allows us to */ - if (proceed) - return llcache_object_refetch(object); - - /* Invalidate cache-control data */ - llcache_invalidate_cache_control_data(object); - - /* Mark it complete */ - object->fetch.state = LLCACHE_FETCH_COMPLETE; - - /* Inform client(s) that object fetch failed */ - event.type = LLCACHE_EVENT_ERROR; - /** \todo More appropriate error message */ - event.data.msg = messages_get("FetchFailed"); - - return llcache_send_event_to_users(object, &event); -} - -/** - * Handler for fetch events - * - * \param msg Fetch event - * \param p Our private data - */ -void llcache_fetch_callback(const fetch_msg *msg, void *p) -{ - nserror error = NSERROR_OK; - llcache_object *object = p; - llcache_event event; - -#ifdef LLCACHE_TRACE - LOG(("Fetch event %d for %p", msg->type, object)); -#endif - - switch (msg->type) { - case FETCH_HEADER: - /* Received a fetch header */ - object->fetch.state = LLCACHE_FETCH_HEADERS; + switch (msg->type) { + case FETCH_HEADER: + /* Received a fetch header */ + object->fetch.state = LLCACHE_FETCH_HEADERS; error = llcache_fetch_process_header(object, msg->data.header_or_data.buf, @@ -1990,569 +1723,780 @@ void llcache_fetch_callback(const fetch_msg *msg, void *p) } /** - * Handle FETCH_REDIRECT event + * Find a user of a low-level cache object * - * \param object Object being redirected - * \param target Target of redirect (may be relative) - * \param replacement Pointer to location to receive replacement object + * \param handle External cache handle to search for + * \return Pointer to corresponding user, or NULL if not found + */ +static llcache_object_user *llcache_object_find_user(const llcache_handle *handle) +{ + llcache_object_user *user; + + assert(handle->object != NULL); + + for (user = handle->object->users; user != NULL; user = user->next) { + if (user->handle == handle) + break; + } + + return user; +} + +/** + * Remove a low-level cache object from a cache list + * + * \param object Object to remove + * \param list List to remove from + * \return NSERROR_OK + */ +static nserror llcache_object_remove_from_list(llcache_object *object, + llcache_object **list) +{ + if (object == *list) + *list = object->next; + else + object->prev->next = object->next; + + if (object->next != NULL) + object->next->prev = object->prev; + + return NSERROR_OK; +} + +/** + * Determine if a low-level cache object resides in a given list + * + * \param object Object to search for + * \param list List to search in + * \return True if object resides in list, false otherwise + */ +static bool llcache_object_in_list(const llcache_object *object, + const llcache_object *list) +{ + while (list != NULL) { + if (list == object) + break; + + list = list->next; + } + + return list != NULL; +} + +/** + * Notify users of an object's current state + * + * \param object Object to notify users about * \return NSERROR_OK on success, appropriate error otherwise */ -nserror llcache_fetch_redirect(llcache_object *object, const char *target, - llcache_object **replacement) +static nserror llcache_object_notify_users(llcache_object *object) { nserror error; - llcache_object *dest; - llcache_object_user *user, *next; - const llcache_post_data *post = object->fetch.post; - nsurl *url; - lwc_string *scheme; - lwc_string *object_scheme; - bool match; - /* Extract HTTP response code from the fetch object */ - long http_code = fetch_http_code(object->fetch.fetch); + llcache_object_user *user, *next_user; + llcache_event event; - /* Abort fetch for this object */ - fetch_abort(object->fetch.fetch); - object->fetch.fetch = NULL; - - /* Invalidate the cache control data */ - llcache_invalidate_cache_control_data(object); +#ifdef LLCACHE_TRACE + bool emitted_notify = false; +#endif - /* And mark it complete */ - object->fetch.state = LLCACHE_FETCH_COMPLETE; - - /* Forcibly stop redirecting if we've followed too many redirects */ -#define REDIRECT_LIMIT 10 - if (object->fetch.redirect_count > REDIRECT_LIMIT) { - llcache_event event; + /** + * State transitions and event emission for users. + * Rows: user state. Cols: object state. + * + * User\Obj INIT HEADERS DATA COMPLETE + * INIT - T T* T* + * HEADERS - - T T* + * DATA - - M T + * COMPLETE - - - - + * + * T => transition user to object state + * M => no transition required, but may need to emit event + * + * The transitions marked with an asterisk can be removed by moving + * the user context into the subsequent state and then reevaluating. + * + * Events are issued as follows: + * + * HAD_HEADERS: on transition from HEADERS -> DATA state + * HAD_DATA : in DATA state, whenever there's new source data + * DONE : on transition from DATA -> COMPLETE state + */ - LOG(("Too many nested redirects")); + for (user = object->users; user != NULL; user = next_user) { + /* Emit necessary events to bring the user up-to-date */ + llcache_handle *handle = user->handle; + const llcache_fetch_state objstate = object->fetch.state; - event.type = LLCACHE_EVENT_ERROR; - event.data.msg = messages_get("BadRedirect"); - - return llcache_send_event_to_users(object, &event); - } -#undef REDIRECT_LIMIT + /* Flag that this user is the current iteration target + * in case the client attempts to destroy it underneath us */ + user->iterator_target = true; - /* Make target absolute */ - error = nsurl_join(object->url, target, &url); - if (error != NSERROR_OK) - return error; + /* A note on the computation of next_user: + * + * Within this loop, we may make a number of calls to + * client code. Our contract with clients is that they + * can do whatever they like from within their callback + * handlers. This is so that we limit the pain of + * reentrancy to this module alone. + * + * One of the things a client can do from within its + * callback handler is to remove users from this object's + * user list. In the common case, the user they attempt + * to remove is the current iteration target, and we + * already protect against that causing problems here. + * However, no such protection exists if the client + * attempts to remove other users from this object's + * user list. + * + * Therefore, we cannot compute next_user up-front + * and expect it to remain valid across calls to + * client code (as the identity of the next user + * in the list may change underneath us). Instead, + * we must compute next_user at the point where we + * are about to cause another iteration of this loop + * (i.e. at the very end, and also at the points where + * continue is used) + */ - /* Reject attempts to redirect from unvalidated to validated schemes - * A "validated" scheme is one over which we have some guarantee that - * the source is trustworthy. */ - object_scheme = nsurl_get_component(object->url, NSURL_SCHEME); - scheme = nsurl_get_component(url, NSURL_SCHEME); +#ifdef LLCACHE_TRACE + if (handle->state != objstate) { + if (emitted_notify == false) { + LOG(("Notifying users of %p", object)); + emitted_notify = true; + } - /* resource: and about: are allowed to redirect anywhere */ - if ((lwc_string_isequal(object_scheme, llcache_resource_lwc, - &match) == lwc_error_ok && match == false) && - (lwc_string_isequal(object_scheme, llcache_about_lwc, - &match) == lwc_error_ok && match == false)) { - /* file, about and resource are not valid redirect targets */ - if ((lwc_string_isequal(object_scheme, llcache_file_lwc, - &match) == lwc_error_ok && match == true) || - (lwc_string_isequal(object_scheme, llcache_about_lwc, - &match) == lwc_error_ok && match == true) || - (lwc_string_isequal(object_scheme, llcache_resource_lwc, - &match) == lwc_error_ok && match == true)) { - lwc_string_unref(object_scheme); - lwc_string_unref(scheme); - nsurl_unref(url); - return NSERROR_OK; + LOG(("User %p state: %d Object state: %d", + user, handle->state, objstate)); } - } +#endif - lwc_string_unref(scheme); - lwc_string_unref(object_scheme); + /* User: INIT, Obj: HEADERS, DATA, COMPLETE => User->HEADERS */ + if (handle->state == LLCACHE_FETCH_INIT && + objstate > LLCACHE_FETCH_INIT) { + handle->state = LLCACHE_FETCH_HEADERS; + } - /* Bail out if we've no way of handling this URL */ - if (fetch_can_fetch(url) == false) { - nsurl_unref(url); - return NSERROR_OK; - } + /* User: HEADERS, Obj: DATA, COMPLETE => User->DATA */ + if (handle->state == LLCACHE_FETCH_HEADERS && + objstate > LLCACHE_FETCH_HEADERS) { + handle->state = LLCACHE_FETCH_DATA; - if (http_code == 301 || http_code == 302 || http_code == 303) { - /* 301, 302, 303 redirects are all unconditional GET requests */ - post = NULL; - } else if (http_code != 307 || post != NULL) { - /** \todo 300, 305, 307 with POST */ - nsurl_unref(url); - return NSERROR_OK; - } + /* Emit HAD_HEADERS event */ + event.type = LLCACHE_EVENT_HAD_HEADERS; + + error = handle->cb(handle, &event, handle->pw); + + if (user->queued_for_delete) { + next_user = user->next; + llcache_object_remove_user(object, user); + llcache_object_user_destroy(user); + + if (error != NSERROR_OK) + return error; + + continue; + } else if (error == NSERROR_NEED_DATA) { + /* User requested replay */ + handle->state = LLCACHE_FETCH_HEADERS; + + /* Continue with the next user -- we'll + * reemit the event next time round */ + user->iterator_target = false; + next_user = user->next; + continue; + } else if (error != NSERROR_OK) { + user->iterator_target = false; + return error; + } + } + + /* User: DATA, Obj: DATA, COMPLETE, more source available */ + if (handle->state == LLCACHE_FETCH_DATA && + objstate >= LLCACHE_FETCH_DATA && + object->source_len > handle->bytes) { + size_t orig_handle_read; + + /* Construct HAD_DATA event */ + event.type = LLCACHE_EVENT_HAD_DATA; + event.data.data.buf = + object->source_data + handle->bytes; + event.data.data.len = + object->source_len - handle->bytes; + + /* Update record of last byte emitted */ + if (object->fetch.flags & + LLCACHE_RETRIEVE_STREAM_DATA) { + /* Streaming, so reset to zero to + * minimise amount of cached source data. + * Additionally, we don't support replay + * when streaming. */ + orig_handle_read = 0; + handle->bytes = object->source_len = 0; + } else { + orig_handle_read = handle->bytes; + handle->bytes = object->source_len; + } + + /* Emit event */ + error = handle->cb(handle, &event, handle->pw); + if (user->queued_for_delete) { + next_user = user->next; + llcache_object_remove_user(object, user); + llcache_object_user_destroy(user); + + if (error != NSERROR_OK) + return error; + + continue; + } else if (error == NSERROR_NEED_DATA) { + /* User requested replay */ + handle->bytes = orig_handle_read; + + /* Continue with the next user -- we'll + * reemit the data next time round */ + user->iterator_target = false; + next_user = user->next; + continue; + } else if (error != NSERROR_OK) { + user->iterator_target = false; + return error; + } + } + + /* User: DATA, Obj: COMPLETE => User->COMPLETE */ + if (handle->state == LLCACHE_FETCH_DATA && + objstate > LLCACHE_FETCH_DATA) { + handle->state = LLCACHE_FETCH_COMPLETE; + + /* Emit DONE event */ + event.type = LLCACHE_EVENT_DONE; + + error = handle->cb(handle, &event, handle->pw); + if (user->queued_for_delete) { + next_user = user->next; + llcache_object_remove_user(object, user); + llcache_object_user_destroy(user); - /* Attempt to fetch target URL */ - error = llcache_object_retrieve(url, object->fetch.flags, - object->fetch.referer, post, - object->fetch.redirect_count + 1, &dest); + if (error != NSERROR_OK) + return error; - /* No longer require url */ - nsurl_unref(url); + continue; + } else if (error == NSERROR_NEED_DATA) { + /* User requested replay */ + handle->state = LLCACHE_FETCH_DATA; - if (error != NSERROR_OK) - return error; + /* Continue with the next user -- we'll + * reemit the event next time round */ + user->iterator_target = false; + next_user = user->next; + continue; + } else if (error != NSERROR_OK) { + user->iterator_target = false; + return error; + } + } - /* Move user(s) to replacement object */ - for (user = object->users; user != NULL; user = next) { - next = user->next; + /* No longer the target of an iterator */ + user->iterator_target = false; - llcache_object_remove_user(object, user); - llcache_object_add_user(dest, user); + next_user = user->next; } - /* Dest is now our object */ - *replacement = dest; - - return NSERROR_OK; + return NSERROR_OK; } /** - * Handle FETCH_NOTMODIFIED event + * Make a snapshot of the current state of an llcache_object. * - * \param object Object to process - * \param replacement Pointer to location to receive replacement object - * \return NSERROR_OK. + * This has the side-effect of the new object being non-cacheable, + * also not-fetching and not a candidate for any other object. + * + * Also note that this new object has no users and at least one + * should be assigned to it before llcache_clean is entered or it + * will be immediately cleaned up. + * + * \param object The object to take a snapshot of + * \param snapshot Pointer to receive snapshot of \a object + * \return NSERROR_OK on success, appropriate error otherwise */ -nserror llcache_fetch_notmodified(llcache_object *object, - llcache_object **replacement) +static nserror llcache_object_snapshot(llcache_object *object, + llcache_object **snapshot) { - /* There may be no candidate if the server erroneously responded - * to an unconditional request with a 304 Not Modified response. - * In this case, we simply retain the initial object, having - * invalidated it and marked it as complete. - */ - if (object->candidate != NULL) { - llcache_object_user *user, *next; - - /* Move user(s) to candidate content */ - for (user = object->users; user != NULL; user = next) { - next = user->next; + llcache_object *newobj; + nserror error; + + error = llcache_object_new(object->url, &newobj); + + if (error != NSERROR_OK) + return error; + + newobj->has_query = object->has_query; - llcache_object_remove_user(object, user); - llcache_object_add_user(object->candidate, user); + newobj->source_alloc = newobj->source_len = object->source_len; + + if (object->source_len > 0) { + newobj->source_data = malloc(newobj->source_alloc); + if (newobj->source_data == NULL) { + llcache_object_destroy(newobj); + return NSERROR_NOMEM; } - - /* Candidate is no longer a candidate for us */ - object->candidate->candidate_count--; - - /* Clone our cache control data into the candidate */ - llcache_object_clone_cache_data(object, object->candidate, - false); - /* Bring candidate's cache data up to date */ - llcache_object_cache_update(object->candidate); - /* Revert no-cache to normal, if required */ - if (object->candidate->cache.no_cache == - LLCACHE_VALIDATE_ONCE) { - object->candidate->cache.no_cache = - LLCACHE_VALIDATE_FRESH; + memcpy(newobj->source_data, object->source_data, + newobj->source_len); + } + + if (object->num_headers > 0) { + newobj->headers = calloc(sizeof(llcache_header), + object->num_headers); + if (newobj->headers == NULL) { + llcache_object_destroy(newobj); + return NSERROR_NOMEM; + } + while (newobj->num_headers < object->num_headers) { + llcache_header *nh = + &(newobj->headers[newobj->num_headers]); + llcache_header *oh = + &(object->headers[newobj->num_headers]); + newobj->num_headers += 1; + nh->name = strdup(oh->name); + nh->value = strdup(oh->value); + if (nh->name == NULL || nh->value == NULL) { + llcache_object_destroy(newobj); + return NSERROR_NOMEM; + } } - - /* Candidate is now our object */ - *replacement = object->candidate; - object->candidate = NULL; - } else { - /* There was no candidate: retain object */ - *replacement = object; } - - /* Ensure fetch has stopped */ - fetch_abort(object->fetch.fetch); - object->fetch.fetch = NULL; - - /* Invalidate our cache-control data */ - llcache_invalidate_cache_control_data(object); - - /* Mark it complete */ - object->fetch.state = LLCACHE_FETCH_COMPLETE; - - /* Old object will be flushed from the cache on the next poll */ - + + newobj->fetch.state = LLCACHE_FETCH_COMPLETE; + + *snapshot = newobj; + return NSERROR_OK; } + +/****************************************************************************** + * Public API * + ******************************************************************************/ + /** - * Split a fetch header into name and value - * - * \param data Header string - * \param len Byte length of header - * \param name Pointer to location to receive header name - * \param value Pointer to location to receive header value - * \return NSERROR_OK on success, appropriate error otherwise + * Attempt to clean the cache */ -nserror llcache_fetch_split_header(const uint8_t *data, size_t len, - char **name, char **value) +/* Exported interface documented in llcache.h */ +void llcache_clean(void) { - char *n, *v; - const uint8_t *colon; + llcache_object *object, *next; + uint32_t llcache_size = 0; - /* Find colon */ - colon = (const uint8_t *) strchr((const char *) data, ':'); - if (colon == NULL) { - /* Failed, assume a key with no value */ - n = strdup((const char *) data); - if (n == NULL) - return NSERROR_NOMEM; +#ifdef LLCACHE_TRACE + LOG(("Attempting cache clean")); +#endif - v = strdup(""); - if (v == NULL) { - free(n); - return NSERROR_NOMEM; - } - } else { - /* Split header into name & value */ + /* Candidates for cleaning are (in order of priority): + * + * 1) Uncacheable objects with no users + * 2) Stale cacheable objects with no users or pending fetches + * 3) Fresh cacheable objects with no users or pending fetches + */ - /* Strip leading whitespace from name */ - while (data[0] == ' ' || data[0] == '\t' || - data[0] == '\r' || data[0] == '\n') { - data++; - } + /* 1) Uncacheable objects with no users or fetches */ + for (object = llcache->uncached_objects; object != NULL; object = next) { + next = object->next; - /* Strip trailing whitespace from name */ - while (colon > data && (colon[-1] == ' ' || - colon[-1] == '\t' || colon[-1] == '\r' || - colon[-1] == '\n')) - colon--; + /* The candidate count of uncacheable objects is always 0 */ + if ((object->users == NULL) && + (object->candidate_count == 0) && + (object->fetch.fetch == NULL) && + (object->fetch.outstanding_query == false)) { +#ifdef LLCACHE_TRACE + LOG(("Found victim %p", object)); +#endif + llcache_object_remove_from_list(object, + &llcache->uncached_objects); + llcache_object_destroy(object); + } else { + llcache_size += object->source_len + sizeof(*object); + } + } - n = strndup((const char *) data, colon - data); - if (n == NULL) - return NSERROR_NOMEM; + /* 2) Stale cacheable objects with no users or pending fetches */ + for (object = llcache->cached_objects; object != NULL; object = next) { + next = object->next; - /* Find colon again */ - while (*colon != ':') { - colon++; + if ((object->users == NULL) && + (object->candidate_count == 0) && + (llcache_object_is_fresh(object) == false) && + (object->fetch.fetch == NULL) && + (object->fetch.outstanding_query == false)) { +#ifdef LLCACHE_TRACE + LOG(("Found victim %p", object)); +#endif + llcache_object_remove_from_list(object, + &llcache->cached_objects); + llcache_object_destroy(object); + } else { + llcache_size += object->source_len + sizeof(*object); } + } - /* Skip over colon and any subsequent whitespace */ - do { - colon++; - } while (*colon == ' ' || *colon == '\t' || - *colon == '\r' || *colon == '\n'); + /* 3) Fresh cacheable objects with no users or pending + * fetches, only if the cache exceeds the configured size. + */ + if (llcache->limit < llcache_size) { + for (object = llcache->cached_objects; object != NULL; + object = next) { + next = object->next; - /* Strip trailing whitespace from value */ - while (len > 0 && (data[len - 1] == ' ' || - data[len - 1] == '\t' || - data[len - 1] == '\r' || - data[len - 1] == '\n')) { - len--; - } + if (object->users == NULL && + object->candidate_count == 0 && + object->fetch.fetch == NULL && + object->fetch.outstanding_query == + false) { +#ifdef LLCACHE_TRACE + LOG(("Found victim %p", object)); +#endif + llcache_size -= + object->source_len + sizeof(*object); - v = strndup((const char *) colon, len - (colon - data)); - if (v == NULL) { - free(n); - return NSERROR_NOMEM; + llcache_object_remove_from_list(object, + &llcache->cached_objects); + llcache_object_destroy(object); + } } } - *name = n; - *value = v; +#ifdef LLCACHE_TRACE + LOG(("Size: %u", llcache_size)); +#endif - return NSERROR_OK; } -/** - * Parse a fetch header - * - * \param object Object to parse header for - * \param data Header string - * \param len Byte length of header - * \param name Pointer to location to receive header name - * \param value Pointer to location to receive header value - * \return NSERROR_OK on success, appropriate error otherwise - * - * \note This function also has the side-effect of updating - * the cache control data for the object if an interesting - * header is encountered - */ -nserror llcache_fetch_parse_header(llcache_object *object, - const uint8_t *data, size_t len, char **name, char **value) +/* See llcache.h for documentation */ +nserror +llcache_initialise(llcache_query_callback cb, void *pw, uint32_t llcache_limit) { - nserror error; + llcache = calloc(1, sizeof(struct llcache_s)); + if (llcache == NULL) { + return NSERROR_NOMEM; + } - /* Set fetch response time if not already set */ - if (object->cache.res_time == 0) - object->cache.res_time = time(NULL); + llcache->query_cb = cb; + llcache->query_cb_pw = pw; + llcache->limit = llcache_limit; - /* Decompose header into name-value pair */ - error = llcache_fetch_split_header(data, len, name, value); - if (error != NSERROR_OK) - return error; + /* Create static scheme strings */ + if (lwc_intern_string("file", SLEN("file"), + &llcache_file_lwc) != lwc_error_ok) + return NSERROR_NOMEM; - /* Parse cache headers to populate cache control data */ -#define SKIP_ST(p) while (*p != '\0' && (*p == ' ' || *p == '\t')) p++ + if (lwc_intern_string("about", SLEN("about"), + &llcache_about_lwc) != lwc_error_ok) + return NSERROR_NOMEM; - if (5 < len && strcasecmp(*name, "Date") == 0) { - /* extract Date header */ - object->cache.date = curl_getdate(*value, NULL); - } else if (4 < len && strcasecmp(*name, "Age") == 0) { - /* extract Age header */ - if ('0' <= **value && **value <= '9') - object->cache.age = atoi(*value); - } else if (8 < len && strcasecmp(*name, "Expires") == 0) { - /* extract Expires header */ - object->cache.expires = curl_getdate(*value, NULL); - } else if (14 < len && strcasecmp(*name, "Cache-Control") == 0) { - /* extract and parse Cache-Control header */ - const char *start = *value; - const char *comma = *value; + if (lwc_intern_string("resource", SLEN("resource"), + &llcache_resource_lwc) != lwc_error_ok) + return NSERROR_NOMEM; - while (*comma != '\0') { - while (*comma != '\0' && *comma != ',') - comma++; + LOG(("llcache initialised with a limit of %d bytes", llcache_limit)); - if (8 < comma - start && (strncasecmp(start, - "no-cache", 8) == 0 || - strncasecmp(start, "no-store", 8) == 0)) - /* When we get a disk cache we should - * distinguish between these two */ - object->cache.no_cache = LLCACHE_VALIDATE_ALWAYS; - else if (7 < comma - start && - strncasecmp(start, "max-age", 7) == 0) { - /* Find '=' */ - while (start < comma && *start != '=') - start++; + return NSERROR_OK; +} - /* Skip over it */ - start++; +/* See llcache.h for documentation */ +void llcache_finalise(void) +{ + llcache_object *object, *next; - /* Skip whitespace */ - SKIP_ST(start); + /* Clean uncached objects */ + for (object = llcache->uncached_objects; object != NULL; object = next) { + llcache_object_user *user, *next_user; - if (start < comma) - object->cache.max_age = atoi(start); - } + next = object->next; - if (*comma != '\0') { - /* Skip past comma */ - comma++; - /* Skip whitespace */ - SKIP_ST(comma); - } + for (user = object->users; user != NULL; user = next_user) { + next_user = user->next; - /* Set start for next token */ - start = comma; + if (user->handle != NULL) + free(user->handle); + + free(user); } - } else if (5 < len && strcasecmp(*name, "ETag") == 0) { - /* extract ETag header */ - free(object->cache.etag); - object->cache.etag = strdup(*value); - if (object->cache.etag == NULL) - return NSERROR_NOMEM; - } else if (14 < len && strcasecmp(*name, "Last-Modified") == 0) { - /* extract Last-Modified header */ - object->cache.last_modified = curl_getdate(*value, NULL); - } -#undef SKIP_ST + /* Fetch system has already been destroyed */ + object->fetch.fetch = NULL; - return NSERROR_OK; -} + llcache_object_destroy(object); + } -/** - * Process a fetch header - * - * \param object Object being fetched - * \param data Header string - * \param len Byte length of header - * \return NSERROR_OK on success, appropriate error otherwise - */ -nserror llcache_fetch_process_header(llcache_object *object, - const uint8_t *data, size_t len) -{ - nserror error; - char *name, *value; - llcache_header *temp; + /* Clean cached objects */ + for (object = llcache->cached_objects; object != NULL; object = next) { + llcache_object_user *user, *next_user; - /* The headers for multiple HTTP responses may be delivered to us if - * the fetch layer receives a 401 response for which it has - * authentication credentials. This will result in a silent re-request - * after which we'll receive the actual response headers for the - * object we want to fetch (assuming that the credentials were correct - * of course) - * - * Therefore, if the header is an HTTP response start marker, then we - * must discard any headers we've read so far, reset the cache data - * that we might have computed, and start again. - */ - /** \todo Properly parse the response line */ - if (strncmp((const char *) data, "HTTP/", SLEN("HTTP/")) == 0) { - time_t req_time = object->cache.req_time; + next = object->next; - llcache_invalidate_cache_control_data(object); + for (user = object->users; user != NULL; user = next_user) { + next_user = user->next; - /* Restore request time, so we compute object's age correctly */ - object->cache.req_time = req_time; + if (user->handle != NULL) + free(user->handle); - llcache_destroy_headers(object); - } + free(user); + } - error = llcache_fetch_parse_header(object, data, len, &name, &value); - if (error != NSERROR_OK) - return error; + /* Fetch system has already been destroyed */ + object->fetch.fetch = NULL; - /* Append header data to the object's headers array */ - temp = realloc(object->headers, (object->num_headers + 1) * - sizeof(llcache_header)); - if (temp == NULL) { - free(name); - free(value); - return NSERROR_NOMEM; + llcache_object_destroy(object); } - object->headers = temp; + /* Unref static scheme lwc strings */ + lwc_string_unref(llcache_file_lwc); + lwc_string_unref(llcache_about_lwc); + lwc_string_unref(llcache_resource_lwc); - object->headers[object->num_headers].name = name; - object->headers[object->num_headers].value = value; + free(llcache); + llcache = NULL; +} - object->num_headers++; +/* See llcache.h for documentation */ +nserror llcache_poll(void) +{ + llcache_object *object; + + fetch_poll(); + + /* Catch new users up with state of objects */ + for (object = llcache->cached_objects; object != NULL; + object = object->next) { + llcache_object_notify_users(object); + } + + for (object = llcache->uncached_objects; object != NULL; + object = object->next) { + llcache_object_notify_users(object); + } return NSERROR_OK; } -/** - * Process a chunk of fetched data - * - * \param object Object being fetched - * \param data Data to process - * \param len Byte length of data - * \return NSERROR_OK on success, appropriate error otherwise. - */ -nserror llcache_fetch_process_data(llcache_object *object, const uint8_t *data, - size_t len) +/* See llcache.h for documentation */ +nserror llcache_handle_retrieve(nsurl *url, uint32_t flags, + nsurl *referer, const llcache_post_data *post, + llcache_handle_callback cb, void *pw, + llcache_handle **result) { - /* Resize source buffer if it's too small */ - if (object->source_len + len >= object->source_alloc) { - const size_t new_len = object->source_len + len + 64 * 1024; - uint8_t *temp = realloc(object->source_data, new_len); - if (temp == NULL) - return NSERROR_NOMEM; + nserror error; + llcache_object_user *user; + llcache_object *object; + + /* Can we fetch this URL at all? */ + if (fetch_can_fetch(url) == false) + return NSERROR_NO_FETCH_HANDLER; + + /* Create a new object user */ + error = llcache_object_user_new(cb, pw, &user); + if (error != NSERROR_OK) + return error; - object->source_data = temp; - object->source_alloc = new_len; + /* Retrieve a suitable object from the cache, + * creating a new one if needed. */ + error = llcache_object_retrieve(url, flags, referer, post, 0, &object); + if (error != NSERROR_OK) { + llcache_object_user_destroy(user); + return error; } - /* Append this data chunk to source buffer */ - memcpy(object->source_data + object->source_len, data, len); - object->source_len += len; + /* Add user to object */ + llcache_object_add_user(object, user); + + *result = user->handle; return NSERROR_OK; } -/** - * Handle an authentication request - * - * \param object Object being fetched - * \param realm Authentication realm - * \return NSERROR_OK on success, appropriate error otherwise. - */ -nserror llcache_fetch_auth(llcache_object *object, const char *realm) +/* See llcache.h for documentation */ +nserror llcache_handle_change_callback(llcache_handle *handle, + llcache_handle_callback cb, void *pw) { - const char *auth; - nserror error = NSERROR_OK; + handle->cb = cb; + handle->pw = pw; - /* Abort fetch for this object */ - fetch_abort(object->fetch.fetch); - object->fetch.fetch = NULL; + return NSERROR_OK; +} - /* Invalidate cache-control data */ - llcache_invalidate_cache_control_data(object); +/* See llcache.h for documentation */ +nserror llcache_handle_release(llcache_handle *handle) +{ + nserror error = NSERROR_OK; + llcache_object *object = handle->object; + llcache_object_user *user = llcache_object_find_user(handle); - /* Destroy headers */ - llcache_destroy_headers(object); + assert(user != NULL); - /* If there was no realm, then default to the URL */ - /** \todo If there was no WWW-Authenticate header, use response body */ - if (realm == NULL) - realm = nsurl_access(object->url); + if (user->iterator_target) { + /* Can't remove / delete user object if it's + * the target of an iterator */ + user->queued_for_delete = true; + } else { + /* Remove the user from the object and destroy it */ + error = llcache_object_remove_user(object, user); + if (error == NSERROR_OK) { + error = llcache_object_user_destroy(user); + } + } + + return error; +} - auth = urldb_get_auth_details(nsurl_access(object->url), realm); +/* See llcache.h for documentation */ +nserror llcache_handle_clone(llcache_handle *handle, llcache_handle **result) +{ + nserror error; + llcache_object_user *newuser; + + error = llcache_object_user_new(handle->cb, handle->pw, &newuser); + if (error == NSERROR_OK) { + llcache_object_add_user(handle->object, newuser); + newuser->handle->state = handle->state; + *result = newuser->handle; + } + + return error; +} - if (auth == NULL || object->fetch.tried_with_auth == true) { - /* No authentication details, or tried what we had, so ask */ - object->fetch.tried_with_auth = false; +/* See llcache.h for documentation */ +nserror llcache_handle_abort(llcache_handle *handle) +{ + llcache_object_user *user = llcache_object_find_user(handle); + llcache_object *object = handle->object, *newobject; + nserror error = NSERROR_OK; + bool all_alone = true; + + /* Determine if we are the only user */ + if (user->prev != NULL) + all_alone = false; + if (user->next != NULL) + all_alone = false; + + if (all_alone == false) { + /* We must snapshot this object */ + error = llcache_object_snapshot(object, &newobject); + if (error != NSERROR_OK) + return error; - if (llcache->query_cb != NULL) { - llcache_query query; + /* Move across to the new object */ + if (user->iterator_target) { + /* User is current iterator target, clone it */ + llcache_object_user *newuser = + calloc(1, sizeof(llcache_object_user)); + if (newuser == NULL) { + llcache_object_destroy(newobject); + return NSERROR_NOMEM; + } - /* Emit query for authentication details */ - query.type = LLCACHE_QUERY_AUTH; - query.url = object->url; - query.data.auth.realm = realm; + /* Move handle across to clone */ + newuser->handle = user->handle; + user->handle = NULL; - object->fetch.outstanding_query = true; + /* Mark user as needing deletion */ + user->queued_for_delete = true; - error = llcache->query_cb(&query, llcache->query_cb_pw, - llcache_query_handle_response, object); + llcache_object_add_user(newobject, newuser); } else { - llcache_event event; - - /* Mark object complete */ - object->fetch.state = LLCACHE_FETCH_COMPLETE; - - /* Inform client(s) that object fetch failed */ - event.type = LLCACHE_EVENT_ERROR; - /** \todo More appropriate error message */ - event.data.msg = messages_get("FetchFailed"); - - error = llcache_send_event_to_users(object, &event); + llcache_object_remove_user(object, user); + llcache_object_add_user(newobject, user); } + + /* Add new object to uncached list */ + llcache_object_add_to_list(newobject, + &llcache->uncached_objects); } else { - /* Flag that we've tried to refetch with credentials, so - * that if the fetch fails again, we ask the user again */ - object->fetch.tried_with_auth = true; - error = llcache_object_refetch(object); + /* We're the only user, so abort any fetch in progress */ + if (object->fetch.fetch != NULL) { + fetch_abort(object->fetch.fetch); + object->fetch.fetch = NULL; + } + + object->fetch.state = LLCACHE_FETCH_COMPLETE; + + /* Invalidate cache control data */ + llcache_invalidate_cache_control_data(object); } - + return error; } -/** - * Handle a TLS certificate verification failure - * - * \param object Object being fetched - * \param certs Certificate chain - * \param num Number of certificates in chain - * \return NSERROR_OK on success, appropriate error otherwise - */ -nserror llcache_fetch_cert_error(llcache_object *object, - const struct ssl_cert_info *certs, size_t num) +/* See llcache.h for documentation */ +nserror llcache_handle_force_stream(llcache_handle *handle) { - nserror error = NSERROR_OK; + llcache_object_user *user = llcache_object_find_user(handle); + llcache_object *object = handle->object; - /* Fetch has been stopped, and destroyed. Invalidate object's pointer */ - object->fetch.fetch = NULL; + /* Cannot stream if there are multiple users */ + if (user->prev != NULL || user->next != NULL) + return NSERROR_OK; - /* Invalidate cache-control data */ - llcache_invalidate_cache_control_data(object); + /* Forcibly uncache this object */ + if (llcache_object_in_list(object, llcache->cached_objects)) { + llcache_object_remove_from_list(object, + &llcache->cached_objects); + llcache_object_add_to_list(object, &llcache->uncached_objects); + } - if (llcache->query_cb != NULL) { - llcache_query query; + object->fetch.flags |= LLCACHE_RETRIEVE_STREAM_DATA; - /* Emit query for TLS */ - query.type = LLCACHE_QUERY_SSL; - query.url = object->url; - query.data.ssl.certs = certs; - query.data.ssl.num = num; + return NSERROR_OK; +} - object->fetch.outstanding_query = true; +/* See llcache.h for documentation */ +nserror llcache_handle_invalidate_cache_data(llcache_handle *handle) +{ + if (handle->object != NULL && handle->object->fetch.fetch == NULL && + handle->object->cache.no_cache == + LLCACHE_VALIDATE_FRESH) { + handle->object->cache.no_cache = LLCACHE_VALIDATE_ONCE; + } - error = llcache->query_cb(&query, llcache->query_cb_pw, - llcache_query_handle_response, object); - } else { - llcache_event event; + return NSERROR_OK; +} - /* Mark object complete */ - object->fetch.state = LLCACHE_FETCH_COMPLETE; +/* See llcache.h for documentation */ +nsurl *llcache_handle_get_url(const llcache_handle *handle) +{ + return handle->object != NULL ? handle->object->url : NULL; +} - /* Inform client(s) that object fetch failed */ - event.type = LLCACHE_EVENT_ERROR; - /** \todo More appropriate error message */ - event.data.msg = messages_get("FetchFailed"); - - error = llcache_send_event_to_users(object, &event); +/* See llcache.h for documentation */ +const uint8_t *llcache_handle_get_source_data(const llcache_handle *handle, + size_t *size) +{ + *size = handle->object != NULL ? handle->object->source_len : 0; + + return handle->object != NULL ? handle->object->source_data : NULL; +} + +/* See llcache.h for documentation */ +const char *llcache_handle_get_header(const llcache_handle *handle, + const char *key) +{ + const llcache_object *object = handle->object; + size_t i; + + if (object == NULL) + return NULL; + + /* About as trivial as possible */ + for (i = 0; i < object->num_headers; i++) { + if (strcasecmp(key, object->headers[i].name) == 0) + return object->headers[i].value; } - return error; + return NULL; +} + +/* See llcache.h for documentation */ +bool llcache_handle_references_same_object(const llcache_handle *a, + const llcache_handle *b) +{ + return a->object == b->object; } -- cgit v1.2.3