diff --git a/networking/httpd.c b/networking/httpd.c index 94f7297ad..0297cf9ec 100644 --- a/networking/httpd.c +++ b/networking/httpd.c @@ -215,6 +215,19 @@ //config: Makes httpd send files using GZIP content encoding if the //config: client supports it and a pre-compressed .gz exists. //config: +//config:config FEATURE_HTTPD_ETAG +//config: bool "Support caching via ETag header" +//config: default y +//config: depends on HTTPD +//config: help +//config: If server responds with ETag then next time client (browser) +//config: resend it via If-None-Match header. +//config: Then httpd will check if file wasn't modified and if not, +//config: return 304 Not Modified status code. +//config: The ETag value is constructed from last modification date +//config: in unix epoch, and size: "hex(last_mod)-hex(file_size)". +//config: It's not completely reliable as hash functions but fair enough. +//config: //config:config FEATURE_HTTPD_LAST_MODIFIED //config: bool "Add Last-Modified header to response" //config: default y @@ -328,6 +341,7 @@ enum { HTTP_OK = 200, HTTP_PARTIAL_CONTENT = 206, HTTP_MOVED_TEMPORARILY = 302, + HTTP_NOT_MODIFIED = 304, HTTP_BAD_REQUEST = 400, /* malformed syntax */ HTTP_UNAUTHORIZED = 401, /* authentication needed, respond with auth hdr */ HTTP_NOT_FOUND = 404, @@ -345,7 +359,6 @@ enum { HTTP_NO_CONTENT = 204, HTTP_MULTIPLE_CHOICES = 300, HTTP_MOVED_PERMANENTLY = 301, - HTTP_NOT_MODIFIED = 304, HTTP_PAYMENT_REQUIRED = 402, HTTP_BAD_GATEWAY = 502, HTTP_SERVICE_UNAVAILABLE = 503, /* overload, maintenance */ @@ -358,6 +371,9 @@ static const uint16_t http_response_type[] ALIGN2 = { HTTP_PARTIAL_CONTENT, #endif HTTP_MOVED_TEMPORARILY, +#if ENABLE_FEATURE_HTTPD_ETAG + HTTP_NOT_MODIFIED, +#endif HTTP_REQUEST_TIMEOUT, HTTP_NOT_IMPLEMENTED, #if ENABLE_FEATURE_HTTPD_BASIC_AUTH @@ -374,7 +390,6 @@ static const uint16_t http_response_type[] ALIGN2 = { HTTP_NO_CONTENT, HTTP_MULTIPLE_CHOICES, HTTP_MOVED_PERMANENTLY, - HTTP_NOT_MODIFIED, HTTP_BAD_GATEWAY, HTTP_SERVICE_UNAVAILABLE, #endif @@ -389,6 +404,9 @@ static const struct { { "Partial Content", NULL }, #endif { "Found", NULL }, +#if ENABLE_FEATURE_HTTPD_ETAG + { "Not Modified" }, +#endif { "Request Timeout", "No request appeared within 60 seconds" }, { "Not Implemented", "The requested method is not recognized" }, #if ENABLE_FEATURE_HTTPD_BASIC_AUTH @@ -405,7 +423,6 @@ static const struct { { "No Content" }, { "Multiple Choices" }, { "Moved Permanently" }, - { "Not Modified" }, { "Bad Gateway", "" }, { "Service Unavailable", "" }, #endif @@ -419,6 +436,9 @@ struct globals { smallint content_gzip; #endif time_t last_mod; +#if ENABLE_FEATURE_HTTPD_ETAG + char *if_none_match; +#endif char *rmt_ip_str; /* for $REMOTE_ADDR and $REMOTE_PORT */ const char *bind_addr_or_port; @@ -453,6 +473,9 @@ struct globals { #define sizeof_hdr_buf COMMON_BUFSIZE char *hdr_ptr; int hdr_cnt; +#if ENABLE_FEATURE_HTTPD_ETAG + char etag[sizeof("'%llx-%llx'") + 2 * sizeof(long long)*3]; +#endif #if ENABLE_FEATURE_HTTPD_ERROR_PAGES const char *http_error_page[ARRAY_SIZE(http_response_type)]; #endif @@ -1208,6 +1231,10 @@ static void send_headers(unsigned responseNum) #if ENABLE_FEATURE_HTTPD_LAST_MODIFIED "Last-Modified: %s\r\n" #endif +#if ENABLE_FEATURE_HTTPD_ETAG + "ETag: %s\r\n" +#endif + /* Because of 4.4 (5), we can forgo sending of "Content-Length" * since we close connection afterwards, but it helps clients * to e.g. estimate download times, show progress bars etc. @@ -1217,6 +1244,9 @@ static void send_headers(unsigned responseNum) "Content-Length: %"OFF_FMT"u\r\n", #if ENABLE_FEATURE_HTTPD_LAST_MODIFIED date_str, +#endif +#if ENABLE_FEATURE_HTTPD_ETAG + G.etag, #endif file_size ); @@ -1730,6 +1760,7 @@ static NOINLINE void send_file_and_exit(const char *url, int what) } } else { fd = open(url, O_RDONLY); + /* file_size and last_mod are already populated */ } if (fd < 0) { if (DEBUG) @@ -1741,6 +1772,19 @@ static NOINLINE void send_file_and_exit(const char *url, int what) send_headers_and_exit(HTTP_NOT_FOUND); log_and_exit(); } +#if ENABLE_FEATURE_HTTPD_ETAG + /* ETag is "hex(last_mod)-hex(file_size)" e.g. "5e132e20-417" */ + sprintf(G.etag, "\"%llx-%llx\"", (unsigned long long)last_mod, (unsigned long long)file_size); + + if (G.if_none_match) { + if (DEBUG) + bb_perror_msg("If-None-Match and file's ETag are: '%s' '%s'\n", G.if_none_match, G.etag); + /* Weak ETag comparision. + * If-None-Match may have many ETags but they are quoted so we can use simple substring search */ + if (strstr(G.if_none_match, G.etag)) + send_headers_and_exit(HTTP_NOT_MODIFIED); + } +#endif /* If you want to know about EPIPE below * (happens if you abort downloads from local httpd): */ signal(SIGPIPE, SIG_IGN); @@ -2480,6 +2524,13 @@ static void handle_incoming_and_exit(const len_and_sockaddr *fromAddr) continue; } #endif +#if ENABLE_FEATURE_HTTPD_ETAG + if (STRNCASECMP(iobuf, "If-None-Match:") == 0) { + free(G.if_none_match); + G.if_none_match = xstrdup(skip_whitespace(iobuf + sizeof("If-None-Match:") - 1)); + continue; + } +#endif #if ENABLE_FEATURE_HTTPD_CGI if (cgi_type != CGI_NONE) { bool ct = (STRNCASECMP(iobuf, "Content-Type:") == 0);