netsurf/content/llcache.c
Vincent Sanders da0b969f25 Improve llcache header processing
By skipping empty headers and correctly dealing with whitespace around
header names we store fewer entries with better adherance to allowed
values in http responses.
2014-03-09 16:22:28 +00:00

2573 lines
67 KiB
C

/*
* Copyright 2009 John-Mark Bell <jmb@netsurf-browser.org>
*
* This file is part of NetSurf, http://www.netsurf-browser.org/
*
* NetSurf is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 2 of the License.
*
* NetSurf is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/** \file
* Low-level resource cache (implementation)
*/
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <curl/curl.h>
#include "content/fetch.h"
#include "content/llcache.h"
#include "content/urldb.h"
#include "utils/corestrings.h"
#include "utils/log.h"
#include "utils/messages.h"
#include "utils/nsurl.h"
#include "utils/utils.h"
/** Define to enable tracing of llcache operations. */
#undef LLCACHE_TRACE
#ifdef LLCACHE_TRACE
#define LLCACHE_LOG(x) LOG(x)
#else
#define LLCACHE_LOG(x)
#endif
/** State of a low-level cache object fetch */
typedef enum {
LLCACHE_FETCH_INIT, /**< Initial state, before fetch */
LLCACHE_FETCH_HEADERS, /**< Fetching headers */
LLCACHE_FETCH_DATA, /**< Fetching object data */
LLCACHE_FETCH_COMPLETE /**< Fetch completed */
} llcache_fetch_state;
/** Type of low-level cache object */
typedef struct llcache_object llcache_object;
/** Handle to low-level cache object */
struct llcache_handle {
llcache_object *object; /**< Pointer to associated object */
llcache_handle_callback cb; /**< Client callback */
void *pw; /**< Client data */
llcache_fetch_state state; /**< Last known state of object fetch */
size_t bytes; /**< Last reported byte count */
};
/** Low-level cache object user record */
typedef struct llcache_object_user {
llcache_handle *handle; /**< Handle data for client */
bool iterator_target; /**< This is the an iterator target */
bool queued_for_delete; /**< This user is queued for deletion */
struct llcache_object_user *prev; /**< Previous in list */
struct llcache_object_user *next; /**< Next in list */
} llcache_object_user;
/** Low-level cache object fetch context */
typedef struct {
uint32_t flags; /**< Fetch flags */
nsurl *referer; /**< Referring URL, or NULL if none */
llcache_post_data *post; /**< POST data, or NULL for GET */
struct fetch *fetch; /**< Fetch handle for this object */
llcache_fetch_state state; /**< Current state of object fetch */
uint32_t redirect_count; /**< Count of redirects followed */
bool tried_with_auth; /**< Whether we've tried with auth */
bool tried_with_tls_downgrade; /**< Whether we've tried TLS <= 1.0 */
bool outstanding_query; /**< Waiting for a query response */
} llcache_fetch_ctx;
typedef enum {
LLCACHE_VALIDATE_FRESH, /**< Only revalidate if not fresh */
LLCACHE_VALIDATE_ALWAYS, /**< Always revalidate */
LLCACHE_VALIDATE_ONCE /**< Revalidate once only */
} llcache_validate;
/** Cache control data */
typedef struct {
time_t req_time; /**< Time of request */
time_t res_time; /**< Time of response */
time_t date; /**< Date: response header */
time_t expires; /**< Expires: response header */
#define INVALID_AGE -1
int age; /**< Age: response header */
int max_age; /**< Max-Age Cache-control parameter */
llcache_validate no_cache; /**< No-Cache Cache-control parameter */
char *etag; /**< Etag: response header */
time_t last_modified; /**< Last-Modified: response header */
} llcache_cache_control;
/** Representation of a fetch header */
typedef struct {
char *name; /**< Header name */
char *value; /**< Header value */
} llcache_header;
/** Low-level cache object */
/** \todo Consider whether a list is a sane container */
struct llcache_object {
llcache_object *prev; /**< Previous in list */
llcache_object *next; /**< Next in list */
nsurl *url; /**< Post-redirect URL for object */
/** \todo We need a generic dynamic buffer object */
uint8_t *source_data; /**< Source data for object */
size_t source_len; /**< Byte length of source data */
size_t source_alloc; /**< Allocated size of source buffer */
llcache_object_user *users; /**< List of users */
llcache_fetch_ctx fetch; /**< Fetch context for object */
llcache_cache_control cache; /**< Cache control data for object */
llcache_object *candidate; /**< Object to use, if fetch determines
* that it is still fresh */
uint32_t candidate_count; /**< Count of objects this is a
* candidate for */
llcache_header *headers; /**< Fetch headers */
size_t num_headers; /**< Number of fetch headers */
};
struct llcache_s {
/** Handler for fetch-related queries */
llcache_query_callback query_cb;
/** Data for fetch-related query handler */
void *query_cb_pw;
/** Head of the low-level cached object list */
llcache_object *cached_objects;
/** Head of the low-level uncached object list */
llcache_object *uncached_objects;
uint32_t limit;
};
/** low level cache state */
static struct llcache_s *llcache = NULL;
/* forward referenced callback function */
static void llcache_fetch_callback(const fetch_msg *msg, void *p);
/******************************************************************************
* Low-level cache internals *
******************************************************************************/
/**
* 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;
h = calloc(1, sizeof(llcache_handle));
if (h == NULL)
return NSERROR_NOMEM;
u = calloc(1, sizeof(llcache_object_user));
if (u == NULL) {
free(h);
return NSERROR_NOMEM;
}
h->cb = cb;
h->pw = pw;
u->handle = h;
LLCACHE_LOG(("Created user %p (%p, %p, %p)", u, h, (void *) cb, pw));
*user = u;
return NSERROR_OK;
}
/**
* 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)
{
LLCACHE_LOG(("Destroyed user %p", user));
assert(user->next == NULL);
assert(user->prev == NULL);
if (user->handle != NULL)
free(user->handle);
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;
LLCACHE_LOG(("Removing user %p from %p", user, object));
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.
*/
static nserror llcache_send_event_to_users(llcache_object *object,
llcache_event *event)
{
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);
next_user = user->next;
user->iterator_target = false;
if (user->queued_for_delete) {
llcache_object_remove_user(object, user);
llcache_object_user_destroy(user);
}
if (error != NSERROR_OK)
break;
user = next_user;
}
return error;
}
/**
* 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 *obj = calloc(1, sizeof(llcache_object));
if (obj == NULL)
return NSERROR_NOMEM;
LLCACHE_LOG(("Created object %p (%s)", obj, nsurl_access(url)));
obj->url = nsurl_ref(url);
*result = obj;
return NSERROR_OK;
}
/**
* 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;
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;
}
/**
* 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)
{
char *n, *v;
const uint8_t *colon;
/* Strip leading whitespace from name */
while (data[0] == ' ' || data[0] == '\t' ||
data[0] == '\r' || data[0] == '\n') {
data++;
}
/* Find colon */
colon = (const uint8_t *) strchr((const char *) data, ':');
if (colon == NULL) {
/* Failed, assume a key with no value */
colon = data + strlen((const char *)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;
v = strdup("");
if (v == NULL) {
free(n);
return NSERROR_NOMEM;
}
} else {
/* Split header into name & value */
/* 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;
}
/**
* 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;
/* Set fetch response time if not already set */
if (object->cache.res_time == 0)
object->cache.res_time = time(NULL);
/* Decompose header into name-value pair */
error = llcache_fetch_split_header(data, len, name, value);
if (error != NSERROR_OK)
return error;
/* Parse cache headers to populate cache control data */
#define SKIP_ST(p) while (*p != '\0' && (*p == ' ' || *p == '\t')) p++
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;
while (*comma != '\0') {
while (*comma != '\0' && *comma != ',')
comma++;
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++;
/* Skip over it */
start++;
/* Skip whitespace */
SKIP_ST(start);
if (start < comma)
object->cache.max_age = atoi(start);
}
if (*comma != '\0') {
/* Skip past comma */
comma++;
/* Skip whitespace */
SKIP_ST(comma);
}
/* 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) {
free(*name);
free(*value);
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
return NSERROR_OK;
}
/* Destroy headers */
static inline void llcache_destroy_headers(llcache_object *object)
{
while (object->num_headers > 0) {
object->num_headers--;
free(object->headers[object->num_headers].name);
free(object->headers[object->num_headers].value);
}
free(object->headers);
object->headers = NULL;
}
/* Invalidate cache control data */
static inline void llcache_invalidate_cache_control_data(llcache_object *object)
{
free(object->cache.etag);
memset(&(object->cache), 0, sizeof(llcache_cache_control));
object->cache.age = INVALID_AGE;
object->cache.max_age = INVALID_AGE;
}
/**
* 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;
/* 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_invalidate_cache_control_data(object);
/* Restore request time, so we compute object's age correctly */
object->cache.req_time = req_time;
llcache_destroy_headers(object);
}
error = llcache_fetch_parse_header(object, data, len, &name, &value);
if (error != NSERROR_OK) {
return error;
}
/* deal with empty header */
if (name[0] == 0) {
free(name);
free(value);
return NSERROR_OK;
}
/* 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;
}
object->headers = temp;
object->headers[object->num_headers].name = name;
object->headers[object->num_headers].value = value;
object->num_headers++;
return NSERROR_OK;
}
/**
* (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 char *urlenc = NULL;
struct fetch_multipart_data *multipart = NULL;
char **headers = NULL;
int header_idx = 0;
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;
}
/* 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;
headers[header_idx] = malloc(len);
if (headers[header_idx] == NULL) {
free(headers);
return NSERROR_NOMEM;
}
snprintf(headers[header_idx], len, "If-None-Match: %s",
object->cache.etag);
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;
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;
/* Reset cache control data */
llcache_invalidate_cache_control_data(object);
object->cache.req_time = time(NULL);
/* Reset fetch state */
object->fetch.state = LLCACHE_FETCH_INIT;
LLCACHE_LOG(("Refetching %p", object));
/* 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,
object->fetch.tried_with_tls_downgrade,
(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;
}
/**
* Kick-off a fetch for an object
*
* \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 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_object_fetch(llcache_object *object, uint32_t flags,
nsurl *referer, const llcache_post_data *post,
uint32_t redirect_count)
{
nserror error;
nsurl *referer_clone = NULL;
llcache_post_data *post_clone = NULL;
LLCACHE_LOG(("Starting fetch for %p", object));
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);
}
/**
* 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)
*/
static nserror llcache_object_destroy(llcache_object *object)
{
size_t i;
LLCACHE_LOG(("Destroying object %p", object));
nsurl_unref(object->url);
free(object->source_data);
if (object->fetch.fetch != NULL) {
fetch_abort(object->fetch.fetch);
object->fetch.fetch = NULL;
}
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);
}
free(object->fetch.post);
}
free(object->cache.etag);
for (i = 0; i < object->num_headers; i++) {
free(object->headers[i].name);
free(object->headers[i].value);
}
free(object->headers);
free(object);
return NSERROR_OK;
}
/**
* Add a low-level cache object to a cache list
*
* \param object Object to add
* \param list List to add to
* \return NSERROR_OK
*/
static nserror llcache_object_add_to_list(llcache_object *object,
llcache_object **list)
{
object->prev = NULL;
object->next = *list;
if (*list != NULL)
(*list)->prev = object;
*list = object;
return NSERROR_OK;
}
/**
* Determine the remaining lifetime of a cache object using the
*
* \param object Object to consider
* \return True if object is still fresh, false otherwise
*/
static int
llcache_object_rfc2616_remaining_lifetime(const llcache_cache_control *cd)
{
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;
LLCACHE_LOG(("%d:%d", freshness_lifetime, current_age));
if ((cd->no_cache == LLCACHE_VALIDATE_FRESH) &&
(freshness_lifetime > current_age)) {
/* object was not forbidden from being returned from
* the cache unvalidated (i.e. the response contained
* a no-cache directive)
*
* The object current age is within the freshness lifetime.
*/
return freshness_lifetime - current_age;
}
return 0; /* object has no remaining lifetime */
}
/**
* 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)
{
int remaining_lifetime;
const llcache_cache_control *cd = &object->cache;
remaining_lifetime = llcache_object_rfc2616_remaining_lifetime(cd);
LLCACHE_LOG(("%p: (%d > 0 || %d != %d)", object,
remaining_lifetime,
object->fetch.state, LLCACHE_FETCH_COMPLETE));
/* The object is fresh if:
* - it was not forbidden from being returned from the cache
* unvalidated.
*
* - it has remaining lifetime or still being fetched.
*/
return ((cd->no_cache == LLCACHE_VALIDATE_FRESH) &&
((remaining_lifetime > 0) ||
(object->fetch.state != LLCACHE_FETCH_COMPLETE)));
}
/**
* Clone an object's cache data
*
* \param source Source object containing cache data to clone
* \param destination Destination object to clone cache data into
* \param deep Whether to deep-copy the data or not
* \return NSERROR_OK on success, appropriate error otherwise
*
* \post If \a deep is false, then any pointers in \a source will be set to NULL
*/
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 */
if (source->cache.etag != NULL) {
char *etag = source->cache.etag;
if (deep) {
/* Copy the etag */
etag = strdup(source->cache.etag);
if (etag == NULL)
return NSERROR_NOMEM;
} else {
/* Destination takes ownership */
source->cache.etag = NULL;
}
if (destination->cache.etag != NULL)
free(destination->cache.etag);
destination->cache.etag = etag;
}
destination->cache.req_time = source->cache.req_time;
destination->cache.res_time = source->cache.res_time;
if (source->cache.date != 0)
destination->cache.date = source->cache.date;
if (source->cache.expires != 0)
destination->cache.expires = source->cache.expires;
if (source->cache.age != INVALID_AGE)
destination->cache.age = source->cache.age;
if (source->cache.max_age != INVALID_AGE)
destination->cache.max_age = source->cache.max_age;
if (source->cache.no_cache != LLCACHE_VALIDATE_FRESH)
destination->cache.no_cache = source->cache.no_cache;
if (source->cache.last_modified != 0)
destination->cache.last_modified = source->cache.last_modified;
return NSERROR_OK;
}
/**
* Retrieve a potentially cached 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
* \return NSERROR_OK on success, appropriate error otherwise
*/
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)
{
nserror error;
llcache_object *obj, *newest = NULL;
LLCACHE_LOG(("Searching cache for %s (%x %p %p)",
nsurl_access(url), flags, referer, post));
/* Search for the most recently fetched matching object */
for (obj = llcache->cached_objects; obj != NULL; obj = obj->next) {
if ((newest == NULL ||
obj->cache.req_time > newest->cache.req_time) &&
nsurl_compare(obj->url, url,
NSURL_COMPLETE) == true) {
newest = obj;
}
}
if (newest != NULL && llcache_object_is_fresh(newest)) {
/* Found a suitable object, and it's still fresh, so use it */
obj = newest;
LLCACHE_LOG(("Found fresh %p", obj));
/* 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 */
/* Create a new object */
error = llcache_object_new(url, &obj);
if (error != NSERROR_OK)
return error;
LLCACHE_LOG(("Found candidate %p (%p)", obj, newest));
/* 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;
LLCACHE_LOG(("Not found %p", obj));
/* 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;
}
/**
* Retrieve an object from the cache, fetching it if necessary.
*
* \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
*/
static nserror llcache_object_retrieve(nsurl *url, uint32_t flags,
nsurl *referer, const llcache_post_data *post,
uint32_t redirect_count, llcache_object **result)
{
nserror error;
llcache_object *obj;
nsurl *defragmented_url;
bool uncachable = false;
LLCACHE_LOG(("Retrieve %s (%x, %p, %p)",
nsurl_access(url), flags, referer, post));
/* Get rid of any url fragment */
error = nsurl_defragment(url, &defragmented_url);
if (error != NSERROR_OK)
return error;
/* determine if content is cachable */
if ((flags & LLCACHE_RETRIEVE_FORCE_FETCH) != 0) {
/* Forced fetches are never cached */
uncachable = true;
} else if (post != NULL) {
/* POST requests are never cached */
uncachable = true;
} else {
/* only http and https schemes are cached */
lwc_string *scheme;
bool match;
scheme = nsurl_get_component(defragmented_url, NSURL_SCHEME);
if (lwc_string_caseless_isequal(scheme, corestring_lwc_http,
&match) == lwc_error_ok &&
(match == false)) {
if (lwc_string_caseless_isequal(scheme, corestring_lwc_https,
&match) == lwc_error_ok &&
(match == false)) {
uncachable = true;
}
}
}
if (uncachable) {
/* Create new object */
error = llcache_object_new(defragmented_url, &obj);
if (error != NSERROR_OK) {
nsurl_unref(defragmented_url);
return error;
}
/* 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;
}
/* 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;
}
/* Returned object is already in the cached list */
}
LLCACHE_LOG(("Retrieved %p", obj));
*result = obj;
nsurl_unref(defragmented_url);
return NSERROR_OK;
}
/**
* Add a user to a low-level cache object
*
* \param object Object to add user to
* \param user User to add
* \return NSERROR_OK.
*/
static nserror llcache_object_add_user(llcache_object *object,
llcache_object_user *user)
{
assert(user->next == NULL);
assert(user->prev == NULL);
assert(user->handle != NULL);
user->handle->object = object;
user->prev = NULL;
user->next = object->users;
if (object->users != NULL)
object->users->prev = user;
object->users = user;
LLCACHE_LOG(("Adding user %p to %p", user, object));
return NSERROR_OK;
}
/**
* Handle FETCH_REDIRECT event
*
* \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
*/
static nserror llcache_fetch_redirect(llcache_object *object, const char *target,
llcache_object **replacement)
{
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_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);
/* 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) {
LOG(("Too many nested redirects"));
event.type = LLCACHE_EVENT_ERROR;
event.data.msg = messages_get("BadRedirect");
return llcache_send_event_to_users(object, &event);
}
#undef REDIRECT_LIMIT
/* Make target absolute */
error = nsurl_join(object->url, target, &url);
if (error != NSERROR_OK)
return error;
/* Inform users of redirect */
event.type = LLCACHE_EVENT_REDIRECT;
event.data.redirect.from = object->url;
event.data.redirect.to = url;
error = llcache_send_event_to_users(object, &event);
if (error != NSERROR_OK) {
nsurl_unref(url);
return error;
}
/* 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, corestring_lwc_resource,
&match) == lwc_error_ok && match == false) &&
(lwc_string_isequal(object_scheme, corestring_lwc_about,
&match) == lwc_error_ok && match == false)) {
/* file, about and resource are not valid redirect targets */
if ((lwc_string_isequal(object_scheme, corestring_lwc_file,
&match) == lwc_error_ok && match == true) ||
(lwc_string_isequal(object_scheme, corestring_lwc_about,
&match) == lwc_error_ok && match == true) ||
(lwc_string_isequal(object_scheme, corestring_lwc_resource,
&match) == lwc_error_ok && match == true)) {
lwc_string_unref(object_scheme);
lwc_string_unref(scheme);
nsurl_unref(url);
return NSERROR_OK;
}
}
lwc_string_unref(scheme);
lwc_string_unref(object_scheme);
/* Bail out if we've no way of handling this URL */
if (fetch_can_fetch(url) == false) {
nsurl_unref(url);
return NSERROR_OK;
}
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;
}
/* Attempt to fetch target URL */
error = llcache_object_retrieve(url, object->fetch.flags,
object->fetch.referer, post,
object->fetch.redirect_count + 1, &dest);
/* 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.
*/
static nserror llcache_object_cache_update(llcache_object *object)
{
if (object->cache.date == 0)
object->cache.date = time(NULL);
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;
}
/**
* 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.
*/
static nserror llcache_fetch_process_data(llcache_object *object, const uint8_t *data,
size_t len)
{
/* 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;
object->source_data = temp;
object->source_alloc = new_len;
}
/* Append this data chunk to source buffer */
memcpy(object->source_data + object->source_len, data, len);
object->source_len += len;
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
*/
static 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);
}
/**
* 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;
/* Abort fetch for this object */
fetch_abort(object->fetch.fetch);
object->fetch.fetch = NULL;
/* Invalidate cache-control data */
llcache_invalidate_cache_control_data(object);
/* Destroy headers */
llcache_destroy_headers(object);
/* 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);
auth = urldb_get_auth_details(object->url, realm);
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;
if (llcache->query_cb != NULL) {
llcache_query query;
/* Emit query for authentication details */
query.type = LLCACHE_QUERY_AUTH;
query.url = object->url;
query.data.auth.realm = realm;
object->fetch.outstanding_query = true;
error = llcache->query_cb(&query, llcache->query_cb_pw,
llcache_query_handle_response, object);
} 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);
}
} 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);
}
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;
/* Fetch has been stopped, and destroyed. Invalidate object's pointer */
object->fetch.fetch = NULL;
/* Invalidate cache-control data */
llcache_invalidate_cache_control_data(object);
if (llcache->query_cb != NULL) {
llcache_query query;
/* Emit query for TLS */
query.type = LLCACHE_QUERY_SSL;
query.url = object->url;
query.data.ssl.certs = certs;
query.data.ssl.num = num;
object->fetch.outstanding_query = true;
error = llcache->query_cb(&query, llcache->query_cb_pw,
llcache_query_handle_response, object);
} 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);
}
return error;
}
/**
* Handle a TLS connection setup failure
*
* \param object Object being fetched
* \return NSERROR_OK on success, appropriate error otherwise
*/
static nserror llcache_fetch_ssl_error(llcache_object *object)
{
nserror error = NSERROR_OK;
/* Fetch has been stopped, and destroyed. Invalidate object's pointer */
object->fetch.fetch = NULL;
/* Invalidate cache-control data */
llcache_invalidate_cache_control_data(object);
if (object->fetch.tried_with_tls_downgrade == true) {
/* Have already tried to downgrade, so give up */
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);
} else {
/* Flag that we've tried to downgrade, so that if the
* fetch fails again, we give up */
object->fetch.tried_with_tls_downgrade = true;
error = llcache_object_refetch(object);
}
return error;
}
/**
* Handler for fetch events
*
* \param msg Fetch event
* \param p Our private data
*/
static void llcache_fetch_callback(const fetch_msg *msg, void *p)
{
nserror error = NSERROR_OK;
llcache_object *object = p;
llcache_event event;
LLCACHE_LOG(("Fetch event %d for %p", msg->type, object));
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,
msg->data.header_or_data.len);
break;
/* 3xx responses */
case FETCH_REDIRECT:
/* Request resulted in a redirect */
/* Release candidate, if any */
if (object->candidate != NULL) {
object->candidate->candidate_count--;
object->candidate = NULL;
}
error = llcache_fetch_redirect(object,
msg->data.redirect, &object);
break;
case FETCH_NOTMODIFIED:
/* Conditional request determined that cached object is fresh */
error = llcache_fetch_notmodified(object, &object);
break;
/* Normal 2xx state machine */
case FETCH_DATA:
/* Received some data */
if (object->fetch.state != LLCACHE_FETCH_DATA) {
/* On entry into this state, check if we need to
* invalidate the cache control data. We are guaranteed
* to have received all response headers.
*
* There are two cases in which we want to suppress
* cacheing of an object:
*
* 1) The HTTP response code is not 200 or 203
* 2) The request URI had a query string and the
* response headers did not provide an explicit
* object expiration time.
*/
long http_code = fetch_http_code(object->fetch.fetch);
if ((http_code != 200 && http_code != 203) ||
(nsurl_has_component(object->url, NSURL_QUERY) &&
(object->cache.max_age == INVALID_AGE &&
object->cache.expires == 0))) {
/* Invalidate cache control data */
llcache_invalidate_cache_control_data(object);
}
/* Release candidate, if any */
if (object->candidate != NULL) {
object->candidate->candidate_count--;
object->candidate = NULL;
}
}
object->fetch.state = LLCACHE_FETCH_DATA;
error = llcache_fetch_process_data(object,
msg->data.header_or_data.buf,
msg->data.header_or_data.len);
break;
case FETCH_FINISHED:
/* Finished fetching */
{
uint8_t *temp;
object->fetch.state = LLCACHE_FETCH_COMPLETE;
object->fetch.fetch = NULL;
/* Shrink source buffer to required size */
temp = realloc(object->source_data,
object->source_len);
/* If source_len is 0, then temp may be NULL */
if (temp != NULL || object->source_len == 0) {
object->source_data = temp;
object->source_alloc = object->source_len;
}
llcache_object_cache_update(object);
}
break;
/* Out-of-band information */
case FETCH_ERROR:
/* An error occurred while fetching */
/* The fetch has has already been cleaned up by the fetcher */
object->fetch.state = LLCACHE_FETCH_COMPLETE;
object->fetch.fetch = NULL;
/* Release candidate, if any */
if (object->candidate != NULL) {
object->candidate->candidate_count--;
object->candidate = NULL;
}
/* Invalidate cache control data */
llcache_invalidate_cache_control_data(object);
/** \todo Consider using errorcode for something */
event.type = LLCACHE_EVENT_ERROR;
event.data.msg = msg->data.error;
error = llcache_send_event_to_users(object, &event);
break;
case FETCH_PROGRESS:
/* Progress update */
event.type = LLCACHE_EVENT_PROGRESS;
event.data.msg = msg->data.progress;
error = llcache_send_event_to_users(object, &event);
break;
/* Events requiring action */
case FETCH_AUTH:
/* Need Authentication */
/* Release candidate, if any */
if (object->candidate != NULL) {
object->candidate->candidate_count--;
object->candidate = NULL;
}
error = llcache_fetch_auth(object, msg->data.auth.realm);
break;
case FETCH_CERT_ERR:
/* Something went wrong when validating TLS certificates */
/* Release candidate, if any */
if (object->candidate != NULL) {
object->candidate->candidate_count--;
object->candidate = NULL;
}
error = llcache_fetch_cert_error(object,
msg->data.cert_err.certs,
msg->data.cert_err.num_certs);
break;
case FETCH_SSL_ERR:
/* TLS connection setup failed */
/* Release candidate, if any */
if (object->candidate != NULL) {
object->candidate->candidate_count--;
object->candidate = NULL;
}
error = llcache_fetch_ssl_error(object);
break;
}
/* Deal with any errors reported by event handlers */
if (error != NSERROR_OK) {
if (object->fetch.fetch != NULL) {
fetch_abort(object->fetch.fetch);
object->fetch.fetch = NULL;
/* Invalidate cache control data */
llcache_invalidate_cache_control_data(object);
object->fetch.state = LLCACHE_FETCH_COMPLETE;
}
return;
}
}
/**
* 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
*/
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
*/
static nserror llcache_object_notify_users(llcache_object *object)
{
nserror error;
llcache_object_user *user, *next_user;
llcache_event event;
#ifdef LLCACHE_TRACE
bool emitted_notify = false;
#endif
/**
* 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
*/
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;
/* Flag that this user is the current iteration target
* in case the client attempts to destroy it underneath us */
user->iterator_target = true;
/* 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)
*/
#ifdef LLCACHE_TRACE
if (handle->state != objstate) {
if (emitted_notify == false) {
LOG(("Notifying users of %p", object));
emitted_notify = true;
}
LOG(("User %p state: %d Object state: %d",
user, handle->state, objstate));
}
#endif
/* User: INIT, Obj: HEADERS, DATA, COMPLETE => User->HEADERS */
if (handle->state == LLCACHE_FETCH_INIT &&
objstate > LLCACHE_FETCH_INIT) {
handle->state = LLCACHE_FETCH_HEADERS;
}
/* User: HEADERS, Obj: DATA, COMPLETE => User->DATA */
if (handle->state == LLCACHE_FETCH_HEADERS &&
objstate > LLCACHE_FETCH_HEADERS) {
handle->state = LLCACHE_FETCH_DATA;
/* 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);
if (error != NSERROR_OK)
return error;
continue;
} else if (error == NSERROR_NEED_DATA) {
/* User requested replay */
handle->state = LLCACHE_FETCH_DATA;
/* 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;
}
}
/* No longer the target of an iterator */
user->iterator_target = false;
next_user = user->next;
}
return NSERROR_OK;
}
/**
* 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.
*
* \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
*/
static 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->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;
}
/******************************************************************************
* Public API *
******************************************************************************/
/**
* Attempt to clean the cache
*/
/* Exported interface documented in llcache.h */
void llcache_clean(void)
{
llcache_object *object, *next;
uint32_t llcache_size = 0;
int remaining_lifetime;
LLCACHE_LOG(("Attempting cache clean"));
/* 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)) {
LLCACHE_LOG(("Found victim %p", object));
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;
remaining_lifetime = llcache_object_rfc2616_remaining_lifetime(&object->cache);
if ((object->users == NULL) &&
(object->candidate_count == 0) &&
(object->fetch.fetch == NULL) &&
(object->fetch.outstanding_query == false)) {
if (remaining_lifetime > 0) {
/* object is fresh */
llcache_size += object->source_len + sizeof(*object);
} else {
/* object is not fresh */
LLCACHE_LOG(("Found stale cacheable object (%p) with no users or pending fetches", object));
llcache_object_remove_from_list(object,
&llcache->cached_objects);
llcache_object_destroy(object);
}
} else {
llcache_size += object->source_len + sizeof(*object);
}
}
/* 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;
if ((object->users == NULL) &&
(object->candidate_count == 0) &&
(object->fetch.fetch == NULL) &&
(object->fetch.outstanding_query == false)) {
LLCACHE_LOG(("Found victim %p", object));
llcache_size -=
object->source_len + sizeof(*object);
llcache_object_remove_from_list(object,
&llcache->cached_objects);
llcache_object_destroy(object);
}
}
}
LLCACHE_LOG(("Size: %u", llcache_size));
}
/* See llcache.h for documentation */
nserror
llcache_initialise(llcache_query_callback cb, void *pw, uint32_t llcache_limit)
{
llcache = calloc(1, sizeof(struct llcache_s));
if (llcache == NULL) {
return NSERROR_NOMEM;
}
llcache->query_cb = cb;
llcache->query_cb_pw = pw;
llcache->limit = llcache_limit;
LOG(("llcache initialised with a limit of %d bytes", llcache_limit));
return NSERROR_OK;
}
/* See llcache.h for documentation */
void llcache_finalise(void)
{
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;
if (user->handle != NULL)
free(user->handle);
free(user);
}
/* Fetch system has already been destroyed */
object->fetch.fetch = NULL;
llcache_object_destroy(object);
}
/* Clean cached objects */
for (object = llcache->cached_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;
if (user->handle != NULL)
free(user->handle);
free(user);
}
/* Fetch system has already been destroyed */
object->fetch.fetch = NULL;
llcache_object_destroy(object);
}
free(llcache);
llcache = NULL;
}
/* 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;
}
/* 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)
{
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;
/* 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;
}
/* Add user to object */
llcache_object_add_user(object, user);
*result = user->handle;
return NSERROR_OK;
}
/* 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;
return NSERROR_OK;
}
/* 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);
assert(user != NULL);
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;
}
/* 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;
}
/* 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;
/* 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;
}
/* Move handle across to clone */
newuser->handle = user->handle;
user->handle = NULL;
/* Mark user as needing deletion */
user->queued_for_delete = true;
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;
/* 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);
}
object->fetch.flags |= LLCACHE_RETRIEVE_STREAM_DATA;
return NSERROR_OK;
}
/* 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;
}
return NSERROR_OK;
}
/* See llcache.h for documentation */
nsurl *llcache_handle_get_url(const llcache_handle *handle)
{
return handle->object != NULL ? handle->object->url : NULL;
}
/* 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 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;
}