From d15018cb99e14f6751d27b216f6353e38b7c2961 Mon Sep 17 00:00:00 2001 From: chme Date: Fri, 30 Mar 2018 08:56:57 +0200 Subject: [PATCH] [httpd/jsonapi] Add cache control headers to some json api endpoints Adds utility functions to httpd.c for checking the request headers for either an "If-None-Match" or an "If-Not-Modified-Since" headers. If the header value is found and it matches the current value for the requested resource, we return early with a http response code 403 (Not Modified). If the request header value is not present or does not match we add the current ETag/Last-Modified values to the response headers and process the request normally. --- src/httpd.c | 85 +++++++++++++++++++++++++++++++++++++-------- src/httpd.h | 7 ++++ src/httpd_jsonapi.c | 68 ++++++++++++++++++++++++++++++++++-- 3 files changed, 142 insertions(+), 18 deletions(-) diff --git a/src/httpd.c b/src/httpd.c index 6c514127..714d40f0 100644 --- a/src/httpd.c +++ b/src/httpd.c @@ -268,6 +268,75 @@ httpd_redirect_to_admin(struct evhttp_request *req) httpd_send_reply(req, HTTP_MOVETEMP, "Moved", NULL, HTTPD_SEND_NO_GZIP); } +/* + * Checks if the given ETag matches the "If-None-Match" request header + * + * If the request does not contains a "If-None-Match" header value or the value + * does not match the given ETag, it returns false (modified) and adds the + * "Cache-Control" and "ETag" headers to the response header. + * + * @param req The request with request and response headers + * @param etag The valid ETag for the requested resource + * @return True if the given ETag matches the request-header-value "If-None-Match", otherwise false + */ +bool +httpd_request_etag_matches(struct evhttp_request *req, const char *etag) +{ + struct evkeyvalq *input_headers; + struct evkeyvalq *output_headers; + const char *none_match; + + input_headers = evhttp_request_get_input_headers(req); + none_match = evhttp_find_header(input_headers, "If-None-Match"); + + // Return not modified, if given timestamp matches "If-Modified-Since" request header + if (none_match && (strcasecmp(etag, none_match) == 0)) + return true; + + // Add cache headers to allow client side caching + output_headers = evhttp_request_get_output_headers(req); + evhttp_add_header(output_headers, "Cache-Control", "private"); + evhttp_add_header(output_headers, "ETag", etag); + + return false; +} + +/* + * Checks if the given timestamp matches the "If-Modified-Since" request header + * + * If the request does not contains a "If-Modified-Since" header value or the value + * does not match the given timestamp, it returns false (modified) and adds the + * "Cache-Control" and "Last-Modified" headers to the response header. + * + * @param req The request with request and response headers + * @param mtime The last modified timestamp for the requested resource + * @return True if the given timestamp matches the request-header-value "If-Modified-Since", otherwise false + */ +bool +httpd_request_not_modified_since(struct evhttp_request *req, const time_t *mtime) +{ + struct evkeyvalq *input_headers; + struct evkeyvalq *output_headers; + char last_modified[1000]; + const char *modified_since; + + input_headers = evhttp_request_get_input_headers(req); + modified_since = evhttp_find_header(input_headers, "If-Modified-Since"); + + strftime(last_modified, sizeof(last_modified), "%a, %d %b %Y %H:%M:%S %Z", gmtime(mtime)); + + // Return not modified, if given timestamp matches "If-Modified-Since" request header + if (modified_since && (strcasecmp(last_modified, modified_since) == 0)) + return true; + + // Add cache headers to allow client side caching + output_headers = evhttp_request_get_output_headers(req); + evhttp_add_header(output_headers, "Cache-Control", "private"); + evhttp_add_header(output_headers, "Last-Modified", last_modified); + + return false; +} + static void serve_file(struct evhttp_request *req, const char *uri) { @@ -276,15 +345,11 @@ serve_file(struct evhttp_request *req, const char *uri) char deref[PATH_MAX]; char *ctype; struct evbuffer *evbuf; - struct evkeyvalq *input_headers; struct evkeyvalq *output_headers; struct stat sb; int fd; int i; uint8_t buf[4096]; - const char *modified_since; - char last_modified[1000]; - struct tm *tm_modified; bool slashed; int ret; @@ -384,13 +449,7 @@ serve_file(struct evhttp_request *req, const char *uri) return; } - tm_modified = gmtime(&sb.st_mtime); - strftime(last_modified, sizeof(last_modified), "%a, %d %b %Y %H:%M:%S %Z", tm_modified); - - input_headers = evhttp_request_get_input_headers(req); - modified_since = evhttp_find_header(input_headers, "If-Modified-Since"); - - if (modified_since && strcasecmp(last_modified, modified_since) == 0) + if (httpd_request_not_modified_since(req, &sb.st_mtime)) { httpd_send_reply(req, HTTP_NOTMODIFIED, NULL, NULL, HTTPD_SEND_NO_GZIP); return; @@ -448,10 +507,6 @@ serve_file(struct evhttp_request *req, const char *uri) output_headers = evhttp_request_get_output_headers(req); evhttp_add_header(output_headers, "Content-Type", ctype); - // Allow browsers to cache the file - evhttp_add_header(output_headers, "Cache-Control", "private"); - evhttp_add_header(output_headers, "Last-Modified", last_modified); - httpd_send_reply(req, HTTP_OK, "OK", evbuf, HTTPD_SEND_NO_GZIP); evbuffer_free(evbuf); diff --git a/src/httpd.h b/src/httpd.h index 2c2d599b..03a87898 100644 --- a/src/httpd.h +++ b/src/httpd.h @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -100,6 +101,12 @@ httpd_request_parse(struct evhttp_request *req, struct httpd_uri_parsed *uri_par void httpd_stream_file(struct evhttp_request *req, int id); +bool +httpd_request_not_modified_since(struct evhttp_request *req, const time_t *mtime); + +bool +httpd_request_etag_matches(struct evhttp_request *req, const char *etag); + /* * Gzips an evbuffer * diff --git a/src/httpd_jsonapi.c b/src/httpd_jsonapi.c index 944f15e6..ed9f1bee 100644 --- a/src/httpd_jsonapi.c +++ b/src/httpd_jsonapi.c @@ -1706,6 +1706,7 @@ jsonapi_reply_queue(struct httpd_request *hreq) int start_pos, end_pos; int version; int count; + char etag[21]; struct player_status status; struct db_queue_item queue_item; json_object *reply; @@ -1713,12 +1714,16 @@ jsonapi_reply_queue(struct httpd_request *hreq) json_object *item; int ret = 0; - memset(&query_params, 0, sizeof(struct query_params)); - reply = json_object_new_object(); - version = db_admin_getint(DB_ADMIN_QUEUE_VERSION); count = db_queue_get_count(); + snprintf(etag, sizeof(etag), "%d", version); + if (httpd_request_etag_matches(hreq->req, etag)) + return HTTP_NOTMODIFIED; + + memset(&query_params, 0, sizeof(struct query_params)); + reply = json_object_new_object(); + json_object_object_add(reply, "version", json_object_new_int(version)); json_object_object_add(reply, "count", json_object_new_int(count)); @@ -1880,6 +1885,7 @@ jsonapi_reply_player_volume(struct httpd_request *hreq) static int jsonapi_reply_library_artists(struct httpd_request *hreq) { + time_t db_update; struct query_params query_params; const char *param; enum media_kind media_kind; @@ -1888,6 +1894,11 @@ jsonapi_reply_library_artists(struct httpd_request *hreq) int total; int ret = 0; + db_update = (time_t) db_admin_getint64(DB_ADMIN_START_TIME); + if (db_update && httpd_request_not_modified_since(hreq->req, &db_update)) + return HTTP_NOTMODIFIED; + + media_kind = 0; param = evhttp_find_header(hreq->query, "media_kind"); if (param) @@ -1940,10 +1951,16 @@ jsonapi_reply_library_artists(struct httpd_request *hreq) static int jsonapi_reply_library_artist(struct httpd_request *hreq) { + time_t db_update; const char *artist_id; json_object *reply; int ret = 0; + db_update = (time_t) db_admin_getint64(DB_ADMIN_START_TIME); + if (db_update && httpd_request_not_modified_since(hreq->req, &db_update)) + return HTTP_NOTMODIFIED; + + artist_id = hreq->uri_parsed->path_parts[3]; reply = fetch_artist(artist_id); @@ -1969,6 +1986,7 @@ jsonapi_reply_library_artist(struct httpd_request *hreq) static int jsonapi_reply_library_artist_albums(struct httpd_request *hreq) { + time_t db_update; struct query_params query_params; const char *artist_id; json_object *reply; @@ -1976,6 +1994,11 @@ jsonapi_reply_library_artist_albums(struct httpd_request *hreq) int total; int ret = 0; + db_update = (time_t) db_admin_getint64(DB_ADMIN_START_TIME); + if (db_update && httpd_request_not_modified_since(hreq->req, &db_update)) + return HTTP_NOTMODIFIED; + + artist_id = hreq->uri_parsed->path_parts[3]; reply = json_object_new_object(); @@ -2018,6 +2041,7 @@ jsonapi_reply_library_artist_albums(struct httpd_request *hreq) static int jsonapi_reply_library_albums(struct httpd_request *hreq) { + time_t db_update; struct query_params query_params; const char *param; enum media_kind media_kind; @@ -2026,6 +2050,11 @@ jsonapi_reply_library_albums(struct httpd_request *hreq) int total; int ret = 0; + db_update = (time_t) db_admin_getint64(DB_ADMIN_START_TIME); + if (db_update && httpd_request_not_modified_since(hreq->req, &db_update)) + return HTTP_NOTMODIFIED; + + media_kind = 0; param = evhttp_find_header(hreq->query, "media_kind"); if (param) @@ -2078,10 +2107,16 @@ jsonapi_reply_library_albums(struct httpd_request *hreq) static int jsonapi_reply_library_album(struct httpd_request *hreq) { + time_t db_update; const char *album_id; json_object *reply; int ret = 0; + db_update = (time_t) db_admin_getint64(DB_ADMIN_START_TIME); + if (db_update && httpd_request_not_modified_since(hreq->req, &db_update)) + return HTTP_NOTMODIFIED; + + album_id = hreq->uri_parsed->path_parts[3]; reply = fetch_album(album_id); @@ -2107,6 +2142,7 @@ jsonapi_reply_library_album(struct httpd_request *hreq) static int jsonapi_reply_library_album_tracks(struct httpd_request *hreq) { + time_t db_update; struct query_params query_params; const char *album_id; json_object *reply; @@ -2114,6 +2150,11 @@ jsonapi_reply_library_album_tracks(struct httpd_request *hreq) int total; int ret = 0; + db_update = (time_t) db_admin_getint64(DB_ADMIN_START_TIME); + if (db_update && httpd_request_not_modified_since(hreq->req, &db_update)) + return HTTP_NOTMODIFIED; + + album_id = hreq->uri_parsed->path_parts[3]; reply = json_object_new_object(); @@ -2156,12 +2197,18 @@ jsonapi_reply_library_album_tracks(struct httpd_request *hreq) static int jsonapi_reply_library_playlists(struct httpd_request *hreq) { + time_t db_update; struct query_params query_params; json_object *reply; json_object *items; int total; int ret = 0; + db_update = (time_t) db_admin_getint64(DB_ADMIN_START_TIME); + if (db_update && httpd_request_not_modified_since(hreq->req, &db_update)) + return HTTP_NOTMODIFIED; + + reply = json_object_new_object(); items = json_object_new_array(); json_object_object_add(reply, "items", items); @@ -2202,10 +2249,16 @@ jsonapi_reply_library_playlists(struct httpd_request *hreq) static int jsonapi_reply_library_playlist(struct httpd_request *hreq) { + time_t db_update; const char *playlist_id; json_object *reply; int ret = 0; + db_update = (time_t) db_admin_getint64(DB_ADMIN_START_TIME); + if (db_update && httpd_request_not_modified_since(hreq->req, &db_update)) + return HTTP_NOTMODIFIED; + + playlist_id = hreq->uri_parsed->path_parts[3]; reply = fetch_playlist(playlist_id); @@ -2231,6 +2284,7 @@ jsonapi_reply_library_playlist(struct httpd_request *hreq) static int jsonapi_reply_library_playlist_tracks(struct httpd_request *hreq) { + time_t db_update; struct query_params query_params; json_object *reply; json_object *items; @@ -2238,6 +2292,11 @@ jsonapi_reply_library_playlist_tracks(struct httpd_request *hreq) int total; int ret = 0; + db_update = (time_t) db_admin_getint64(DB_ADMIN_START_TIME); + if (db_update && httpd_request_not_modified_since(hreq->req, &db_update)) + return HTTP_NOTMODIFIED; + + ret = safe_atoi32(hreq->uri_parsed->path_parts[3], &playlist_id); if (ret < 0) { @@ -2666,6 +2725,9 @@ jsonapi_request(struct evhttp_request *req, struct httpd_uri_parsed *uri_parsed) case HTTP_NOCONTENT: /* 204 No Content */ httpd_send_reply(req, status_code, "No Content", hreq->reply, HTTPD_SEND_NO_GZIP); break; + case HTTP_NOTMODIFIED: /* 304 Not Modified */ + httpd_send_reply(req, HTTP_NOTMODIFIED, NULL, NULL, HTTPD_SEND_NO_GZIP); + break; case HTTP_BADREQUEST: /* 400 Bad Request */ httpd_send_error(req, status_code, "Bad Request");