Improve ETag Caching

This also adds support for ETag caching for the settings pages
Also fixed some issues with the previous caching not being RFC 7232 compliant.
This commit is contained in:
Woody 2023-12-21 02:21:43 +01:00
parent bd20c83919
commit fd6ce57003
No known key found for this signature in database
GPG Key ID: 9872D7F5072789B2
2 changed files with 94 additions and 104 deletions

View File

@ -182,7 +182,7 @@ ${result}
const result = hexdump(buf);
const chunk = `
// Autogenerated from ${srcDir}/${s.file}, do not edit!!
const uint16_t ${s.name}_length = ${result.length};
const uint16_t ${s.name}_length = ${buf.length};
const uint8_t ${s.name}[] PROGMEM = {
${result}
};

View File

@ -15,8 +15,9 @@
* Integrated HTTP web server page declarations
*/
bool handleIfNoneMatchCacheHeader(AsyncWebServerRequest* request);
void setStaticContentCacheHeaders(AsyncWebServerResponse *response);
bool handleIfNoneMatchCacheHeader(AsyncWebServerRequest* request, int code, const String &eTagSuffix = "");
void setStaticContentCacheHeaders(AsyncWebServerResponse *response, int code, const String &eTagSuffix = "");
void handleStaticContent(AsyncWebServerRequest *request, const String &path, int code, const String &contentType, const uint8_t *content, size_t len, bool gzip = true, const String &eTagSuffix = "");
// define flash strings once (saves flash memory)
static const char s_redirecting[] PROGMEM = "Redirecting...";
@ -113,22 +114,14 @@ void initServer()
DefaultHeaders::Instance().addHeader(F("Access-Control-Allow-Headers"), "*");
#ifdef WLED_ENABLE_WEBSOCKETS
#ifndef WLED_DISABLE_2D
server.on("/liveview2D", HTTP_GET, [](AsyncWebServerRequest *request){
if (handleIfNoneMatchCacheHeader(request)) return;
AsyncWebServerResponse *response = request->beginResponse_P(200, "text/html", PAGE_liveviewws2D, PAGE_liveviewws2D_length);
response->addHeader(FPSTR(s_content_enc),"gzip");
setStaticContentCacheHeaders(response);
request->send(response);
#ifndef WLED_DISABLE_2D
server.on("/liveview2D", HTTP_GET, [](AsyncWebServerRequest *request) {
handleStaticContent(request, "", 200, "text/html", PAGE_liveviewws2D, PAGE_liveviewws2D_length);
});
#endif
#endif
server.on("/liveview", HTTP_GET, [](AsyncWebServerRequest *request){
if (handleIfNoneMatchCacheHeader(request)) return;
AsyncWebServerResponse *response = request->beginResponse_P(200, "text/html", PAGE_liveview, PAGE_liveview_length);
response->addHeader(FPSTR(s_content_enc),"gzip");
setStaticContentCacheHeaders(response);
request->send(response);
server.on("/liveview", HTTP_GET, [](AsyncWebServerRequest *request) {
handleStaticContent(request, "", 200, "text/html", PAGE_liveview, PAGE_liveview_length);
});
//settings page
@ -138,17 +131,12 @@ void initServer()
// "/settings/settings.js&p=x" request also handled by serveSettings()
server.on("/style.css", HTTP_GET, [](AsyncWebServerRequest *request){
if (handleIfNoneMatchCacheHeader(request)) return;
AsyncWebServerResponse *response = request->beginResponse_P(200, "text/css", PAGE_settingsCss, PAGE_settingsCss_length);
response->addHeader(FPSTR(s_content_enc),"gzip");
setStaticContentCacheHeaders(response);
request->send(response);
server.on("/style.css", HTTP_GET, [](AsyncWebServerRequest *request) {
handleStaticContent(request, "/style.css", 200, "text/css", PAGE_settingsCss, PAGE_settingsCss_length);
});
server.on("/favicon.ico", HTTP_GET, [](AsyncWebServerRequest *request) {
if (handleFileRead(request, "/favicon.ico")) return;
request->send_P(200, "image/x-icon", favicon, 156);
handleStaticContent(request, "/favicon.ico", 200, "image/x-icon", favicon, favicon_length, false);
});
server.on("/skin.css", HTTP_GET, [](AsyncWebServerRequest *request) {
@ -236,12 +224,8 @@ void initServer()
});
#ifdef WLED_ENABLE_USERMOD_PAGE
server.on("/u", HTTP_GET, [](AsyncWebServerRequest *request){
if (handleIfNoneMatchCacheHeader(request)) return;
AsyncWebServerResponse *response = request->beginResponse_P(200, "text/html", PAGE_usermod, PAGE_usermod_length);
response->addHeader(FPSTR(s_content_enc),"gzip");
setStaticContentCacheHeaders(response);
request->send(response);
server.on("/u", HTTP_GET, [](AsyncWebServerRequest *request) {
handleStaticContent(request, "", 200, "text/html", PAGE_usermod, PAGE_usermod_length);
});
#endif
@ -325,44 +309,29 @@ void initServer()
});
#endif
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
if (captivePortal(request)) return;
if (!showWelcomePage || request->hasArg(F("sliders"))){
serveIndex(request);
if (!showWelcomePage || request->hasArg(F("sliders"))) {
handleStaticContent(request, "/index.htm", 200, "text/html", PAGE_index, PAGE_index_L);
} else {
serveSettings(request);
}
});
#ifdef WLED_ENABLE_PIXART
server.on("/pixart.htm", HTTP_GET, [](AsyncWebServerRequest *request){
if (handleFileRead(request, "/pixart.htm")) return;
if (handleIfNoneMatchCacheHeader(request)) return;
AsyncWebServerResponse *response = request->beginResponse_P(200, "text/html", PAGE_pixart, PAGE_pixart_L);
response->addHeader(FPSTR(s_content_enc),"gzip");
setStaticContentCacheHeaders(response);
request->send(response);
#ifdef WLED_ENABLE_PIXART
server.on("/pixart.htm", HTTP_GET, [](AsyncWebServerRequest *request) {
handleStaticContent(request, "/pixart.htm", 200, "text/html", PAGE_pixart, PAGE_pixart_L);
});
#endif
#ifndef WLED_DISABLE_PXMAGIC
server.on("/pxmagic.htm", HTTP_GET, [](AsyncWebServerRequest *request){
if (handleFileRead(request, "/pxmagic.htm")) return;
if (handleIfNoneMatchCacheHeader(request)) return;
AsyncWebServerResponse *response = request->beginResponse_P(200, "text/html", PAGE_pxmagic, PAGE_pxmagic_L);
response->addHeader(FPSTR(s_content_enc),"gzip");
setStaticContentCacheHeaders(response);
request->send(response);
#ifndef WLED_DISABLE_PXMAGIC
server.on("/pxmagic.htm", HTTP_GET, [](AsyncWebServerRequest *request) {
handleStaticContent(request, "/pxmagic.htm", 200, "text/html", PAGE_pxmagic, PAGE_pxmagic_L);
});
#endif
#endif
server.on("/cpal.htm", HTTP_GET, [](AsyncWebServerRequest *request){
if (handleFileRead(request, "/cpal.htm")) return;
if (handleIfNoneMatchCacheHeader(request)) return;
AsyncWebServerResponse *response = request->beginResponse_P(200, "text/html", PAGE_cpal, PAGE_cpal_L);
response->addHeader(FPSTR(s_content_enc),"gzip");
setStaticContentCacheHeaders(response);
request->send(response);
server.on("/cpal.htm", HTTP_GET, [](AsyncWebServerRequest *request) {
handleStaticContent(request, "/cpal.htm", 200, "text/html", PAGE_cpal, PAGE_cpal_L);
});
#ifdef WLED_ENABLE_WEBSOCKETS
@ -388,26 +357,36 @@ void initServer()
#ifndef WLED_DISABLE_ALEXA
if(espalexa.handleAlexaApiCall(request)) return;
#endif
if(handleFileRead(request, request->url())) return;
AsyncWebServerResponse *response = request->beginResponse_P(404, "text/html", PAGE_404, PAGE_404_length);
response->addHeader(FPSTR(s_content_enc),"gzip");
setStaticContentCacheHeaders(response);
request->send(response);
handleStaticContent(request, request->url(), 404, "text/html", PAGE_404, PAGE_404_length);
});
}
bool handleIfNoneMatchCacheHeader(AsyncWebServerRequest *request) {
void generateEtag(char *etag, const String &eTagSuffix) {
char eTagSuffixBuf[eTagSuffix.length() + 1];
eTagSuffix.toCharArray(eTagSuffixBuf, eTagSuffix.length() + 1);
sprintf_P(etag, PSTR("%7d-%02x-%s"), VERSION, cacheInvalidate, eTagSuffixBuf);
}
bool handleIfNoneMatchCacheHeader(AsyncWebServerRequest *request, int code, const String &eTagSuffix) {
// Only send 304 (Not Modified) if response code is 200 (OK)
if (code != 200) return false;
AsyncWebHeader *header = request->getHeader("If-None-Match");
char etag[11];
sprintf_P(etag, PSTR("%7d-%02x"), VERSION, cacheInvalidate);
char etag[12 + eTagSuffix.length()];
generateEtag(etag, eTagSuffix);
if (header && header->value() == etag) {
request->send(304);
AsyncWebServerResponse *response = request->beginResponse(304);
setStaticContentCacheHeaders(response, code, eTagSuffix);
request->send(response);
return true;
}
return false;
}
void setStaticContentCacheHeaders(AsyncWebServerResponse *response) {
void setStaticContentCacheHeaders(AsyncWebServerResponse *response, int code, const String &eTagSuffix) {
// Only send ETag for 200 (OK) responses
if (code != 200) return;
// https://medium.com/@codebyamir/a-web-developers-guide-to-browser-caching-cc41f3b73e7c
#ifndef WLED_DEBUG
// this header name is misleading, "no-cache" will not disable cache,
@ -416,25 +395,37 @@ void setStaticContentCacheHeaders(AsyncWebServerResponse *response) {
#else
response->addHeader(F("Cache-Control"), "no-store,max-age=0"); // prevent caching if debug build
#endif
char etag[11];
sprintf_P(etag, PSTR("%7d-%02x"), VERSION, cacheInvalidate);
char etag[12 + eTagSuffix.length()];
generateEtag(etag, eTagSuffix);
response->addHeader(F("ETag"), etag);
}
void serveIndex(AsyncWebServerRequest* request)
{
if (handleFileRead(request, "/index.htm")) return;
if (handleIfNoneMatchCacheHeader(request)) return;
AsyncWebServerResponse *response = request->beginResponse_P(200, "text/html", PAGE_index, PAGE_index_L);
response->addHeader(FPSTR(s_content_enc),"gzip");
setStaticContentCacheHeaders(response);
/**
* Handels the request for a static file.
* If the file was found in the filesystem, it will be sent to the client.
* Otherwise it will be checked if the browser cached the file and if so, a 304 response will be sent.
* If the file was not found in the filesystem and not in the browser cache, the request will be handled as a 200 response with the content of the page.
*
* @param request The request object
* @param path If a file with this path exists in the filesystem, it will be sent to the client. Set to "" to skip this check.
* @param code The HTTP status code
* @param contentType The content type of the web page
* @param content Content of the web page
* @param len Length of the content
* @param gzip Optional. Defaults to true. If false, the gzip header will not be added.
* @param eTagSuffix Optional. Defaults to "". A suffix that will be added to the ETag header. This can be used to invalidate the cache for a specific page.
*/
void handleStaticContent(AsyncWebServerRequest *request, const String &path, int code, const String &contentType, const uint8_t *content, size_t len, bool gzip, const String &eTagSuffix) {
if (path != "" && handleFileRead(request, path)) return;
if (handleIfNoneMatchCacheHeader(request, code, eTagSuffix)) return;
AsyncWebServerResponse *response = request->beginResponse_P(code, contentType, content, len);
if (gzip) response->addHeader(FPSTR(s_content_enc), "gzip");
setStaticContentCacheHeaders(response, code, eTagSuffix);
request->send(response);
}
String msgProcessor(const String& var)
{
if (var == "MSG") {
@ -536,13 +527,11 @@ void serveSettingsJS(AsyncWebServerRequest* request)
}
void serveSettings(AsyncWebServerRequest* request, bool post)
{
void serveSettings(AsyncWebServerRequest* request, bool post) {
byte subPage = 0, originalSubPage = 0;
const String& url = request->url();
if (url.indexOf("sett") >= 0)
{
if (url.indexOf("sett") >= 0) {
if (url.indexOf(".js") > 0) subPage = SUBPAGE_JS;
else if (url.indexOf(".css") > 0) subPage = SUBPAGE_CSS;
else if (url.indexOf("wifi") > 0) subPage = SUBPAGE_WIFI;
@ -603,22 +592,25 @@ void serveSettings(AsyncWebServerRequest* request, bool post)
}
}
AsyncWebServerResponse *response;
switch (subPage)
{
case SUBPAGE_WIFI : response = request->beginResponse_P(200, "text/html", PAGE_settings_wifi, PAGE_settings_wifi_length); break;
case SUBPAGE_LEDS : response = request->beginResponse_P(200, "text/html", PAGE_settings_leds, PAGE_settings_leds_length); break;
case SUBPAGE_UI : response = request->beginResponse_P(200, "text/html", PAGE_settings_ui, PAGE_settings_ui_length); break;
case SUBPAGE_SYNC : response = request->beginResponse_P(200, "text/html", PAGE_settings_sync, PAGE_settings_sync_length); break;
case SUBPAGE_TIME : response = request->beginResponse_P(200, "text/html", PAGE_settings_time, PAGE_settings_time_length); break;
case SUBPAGE_SEC : response = request->beginResponse_P(200, "text/html", PAGE_settings_sec, PAGE_settings_sec_length); break;
int code = 200;
String contentType = "text/html";
const uint8_t* content;
size_t len;
switch (subPage) {
case SUBPAGE_WIFI : content = PAGE_settings_wifi; len = PAGE_settings_wifi_length; break;
case SUBPAGE_LEDS : content = PAGE_settings_leds; len = PAGE_settings_leds_length; break;
case SUBPAGE_UI : content = PAGE_settings_ui; len = PAGE_settings_ui_length; break;
case SUBPAGE_SYNC : content = PAGE_settings_sync; len = PAGE_settings_sync_length; break;
case SUBPAGE_TIME : content = PAGE_settings_time; len = PAGE_settings_time_length; break;
case SUBPAGE_SEC : content = PAGE_settings_sec; len = PAGE_settings_sec_length; break;
#ifdef WLED_ENABLE_DMX
case SUBPAGE_DMX : response = request->beginResponse_P(200, "text/html", PAGE_settings_dmx, PAGE_settings_dmx_length); break;
case SUBPAGE_DMX : content = PAGE_settings_dmx; len = PAGE_settings_dmx_length; break;
#endif
case SUBPAGE_UM : response = request->beginResponse_P(200, "text/html", PAGE_settings_um, PAGE_settings_um_length); break;
case SUBPAGE_UPDATE : response = request->beginResponse_P(200, "text/html", PAGE_update, PAGE_update_length); break;
case SUBPAGE_UM : content = PAGE_settings_um; len = PAGE_settings_um_length; break;
case SUBPAGE_UPDATE : content = PAGE_update; len = PAGE_update_length; break;
#ifndef WLED_DISABLE_2D
case SUBPAGE_2D : response = request->beginResponse_P(200, "text/html", PAGE_settings_2D, PAGE_settings_2D_length); break;
case SUBPAGE_2D : content = PAGE_settings_2D; len = PAGE_settings_2D_length; break;
#endif
case SUBPAGE_LOCK : {
correctPIN = !strlen(settingsPIN); // lock if a pin is set
@ -626,13 +618,11 @@ void serveSettings(AsyncWebServerRequest* request, bool post)
serveMessage(request, 200, strlen(settingsPIN) > 0 ? PSTR("Settings locked") : PSTR("No PIN set"), FPSTR(s_redirecting), 1);
return;
}
case SUBPAGE_PINREQ : response = request->beginResponse_P(401, "text/html", PAGE_settings_pin, PAGE_settings_pin_length); break;
case SUBPAGE_CSS : response = request->beginResponse_P(200, "text/css", PAGE_settingsCss, PAGE_settingsCss_length); break;
case SUBPAGE_JS : serveSettingsJS(request); return;
case SUBPAGE_WELCOME : response = request->beginResponse_P(200, "text/html", PAGE_welcome, PAGE_welcome_length); break;
default: response = request->beginResponse_P(200, "text/html", PAGE_settings, PAGE_settings_length); break;
case SUBPAGE_PINREQ : content = PAGE_settings_pin; len = PAGE_settings_pin_length; code = 401; break;
case SUBPAGE_CSS : content = PAGE_settingsCss; len = PAGE_settingsCss_length; contentType = "text/css"; break;
case SUBPAGE_JS : serveSettingsJS(request); return;
case SUBPAGE_WELCOME : content = PAGE_welcome; len = PAGE_welcome_length; break;
default: content = PAGE_settings; len = PAGE_settings_length; break;
}
response->addHeader(FPSTR(s_content_enc),"gzip");
setStaticContentCacheHeaders(response);
request->send(response);
handleStaticContent(request, "", code, contentType, content, len);
}