0,0 → 1,2590 |
/* |
* 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/log.h" |
#include "utils/messages.h" |
#include "utils/nsurl.h" |
#include "utils/utils.h" |
|
/** Define to enable tracing of llcache operations. */ |
#undef LLCACHE_TRACE |
|
/** 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 */ |
bool has_query; /**< URL has a query segment */ |
|
/** \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; |
|
/* Static lwc_strings */ |
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); |
|
|
/****************************************************************************** |
* 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; |
|
#ifdef LLCACHE_TRACE |
LOG(("Created user %p (%p, %p, %p)", u, h, (void *) cb, pw)); |
#endif |
|
*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) |
{ |
#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; |
} |
|
/** |
* 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 |
|
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; |
|
#ifdef LLCACHE_TRACE |
LOG(("Created object %p (%s)", obj, nsurl_access(url))); |
#endif |
|
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; |
|
/* 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; |
} |
|
/** |
* 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) |
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; |
|
/* 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; |
|
#ifdef LLCACHE_TRACE |
LOG(("Refetching %p", object)); |
#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, |
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; |
|
#ifdef LLCACHE_TRACE |
LOG(("Starting fetch for %p", object)); |
#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); |
} |
|
/** |
* 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; |
|
#ifdef LLCACHE_TRACE |
LOG(("Destroying object %p", object)); |
#endif |
|
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; |
|
#ifdef LLCACHE_TRACE |
LOG(("%d:%d", freshness_lifetime, current_age)); |
#endif |
|
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); |
|
#ifdef LLCACHE_TRACE |
LOG(("%p: (%d > 0 || %d != %d)", object, |
remaining_lifetime, |
object->fetch.state, LLCACHE_FETCH_COMPLETE)); |
#endif |
|
/* 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; |
|
#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) { |
|
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; |
|
#ifdef LLCACHE_TRACE |
LOG(("Found fresh %p", obj)); |
#endif |
|
/* 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; |
|
#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; |
} |
|
/** |
* 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; |
bool has_query; |
nsurl *defragmented_url; |
|
#ifdef LLCACHE_TRACE |
LOG(("Retrieve %s (%x, %s, %p)", url, flags, referer, post)); |
#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); |
|
/* 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 (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; |
} |
|
/* 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 */ |
} |
|
obj->has_query = has_query; |
|
#ifdef LLCACHE_TRACE |
LOG(("Retrieved %p", obj)); |
#endif |
|
*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); |
|
user->handle->object = object; |
|
user->prev = NULL; |
user->next = object->users; |
|
if (object->users != NULL) |
object->users->prev = user; |
object->users = user; |
|
#ifdef LLCACHE_TRACE |
LOG(("Adding user %p to %p", user, object)); |
#endif |
|
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); |
|
/* 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) { |
llcache_event event; |
|
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; |
|
/* 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; |
} |
} |
|
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; |
|
#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; |
|
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) || |
(object->has_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->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; |
} |
|
|
/****************************************************************************** |
* 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; |
|
#ifdef LLCACHE_TRACE |
LOG(("Attempting cache clean")); |
#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; |
|
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 */ |
#ifdef LLCACHE_TRACE |
LOG(("Found stale cacheable object (%p) with no users or pending fetches", object)); |
#endif |
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)) { |
#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 |
|
} |
|
/* 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; |
|
/* Create static scheme strings */ |
if (lwc_intern_string("file", SLEN("file"), |
&llcache_file_lwc) != lwc_error_ok) |
return NSERROR_NOMEM; |
|
if (lwc_intern_string("about", SLEN("about"), |
&llcache_about_lwc) != lwc_error_ok) |
return NSERROR_NOMEM; |
|
if (lwc_intern_string("resource", SLEN("resource"), |
&llcache_resource_lwc) != lwc_error_ok) |
return NSERROR_NOMEM; |
|
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); |
} |
|
/* Unref static scheme lwc strings */ |
lwc_string_unref(llcache_file_lwc); |
lwc_string_unref(llcache_about_lwc); |
lwc_string_unref(llcache_resource_lwc); |
|
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; |
} |
|