Unverified Commit 89fd90d6 authored by Matthias Hertel's avatar Matthias Hertel Committed by GitHub

Supporting ETag http headers on static files (#7687)

* Supporting ETag http headers on static files

* Supporting ETag http headers on static files

* WebServer Example and Doku

* new template for readme added.

* example updated, som more TRACE output.

* better TRACE formatting.

* upload and trace enhancements

* Create .skip.esp32h2

* Update libraries/WebServer/examples/WebServer/data/index.htm
Co-authored-by: default avatarLucas Saavedra Vaz <lucassvaz@yahoo.com.br>

* files.htm trailing newLine

---------
Co-authored-by: default avatarJan Procházka <90197375+P-R-O-C-H-Y@users.noreply.github.com>
Co-authored-by: default avatarLucas Saavedra Vaz <lucassvaz@yahoo.com.br>
parent 5fcdb841
This diff is collapsed.
This diff is collapsed.
/**
* @file builtinfiles.h
* @brief This file is part of the WebServer example for the ESP8266WebServer.
*
* This file contains long, multiline text variables for all builtin resources.
*/
// used for $upload.htm
static const char uploadContent[] PROGMEM =
R"==(
<!doctype html>
<html lang='en'>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Upload</title>
</head>
<body style="width:300px">
<h1>Upload</h1>
<div><a href="/">Home</a></div>
<hr>
<div id='zone' style='width:16em;height:12em;padding:10px;background-color:#ddd'>Drop files here...</div>
<script>
// allow drag&drop of file objects
function dragHelper(e) {
e.stopPropagation();
e.preventDefault();
}
// allow drag&drop of file objects
function dropped(e) {
dragHelper(e);
var fls = e.dataTransfer.files;
var formData = new FormData();
for (var i = 0; i < fls.length; i++) {
formData.append('file', fls[i], '/' + fls[i].name);
}
fetch('/', { method: 'POST', body: formData }).then(function () {
window.alert('done.');
});
}
var z = document.getElementById('zone');
z.addEventListener('dragenter', dragHelper, false);
z.addEventListener('dragover', dragHelper, false);
z.addEventListener('drop', dropped, false);
</script>
</body>
)==";
// used for $upload.htm
static const char notFoundContent[] PROGMEM = R"==(
<html>
<head>
<title>Ressource not found</title>
</head>
<body>
<p>The ressource was not found.</p>
<p><a href="/">Start again</a></p>
</body>
)==";
<html>
<head>
<title>Files</title>
<link Content-Type="text/css" href="/style.css" rel="stylesheet" />
</head>
<body>
<h1>Files on Server</h1>
<p>These files are available on the server to be opened or delete:</p>
<div id="list">
</div>
<script>
// load and display all files after page loading has finished
window.addEventListener("load", function () {
fetch('/api/list')
.then(function (result) { return result.json(); })
.then(function (e) {
var listObj = document.querySelector('#list');
e.forEach(function (f) {
var entry = document.createElement("div");
var nameObj = document.createElement("a");
nameObj.href = '/' + f.name;
nameObj.innerText = '/' + f.name;
entry.appendChild(nameObj)
entry.appendChild(document.createTextNode(' (' + f.size + ') '));
var timeObj = document.createElement("span");
timeObj.innerText = (new Date(f.time*1000)).toLocaleString();
entry.appendChild(timeObj)
entry.appendChild(document.createTextNode(" "));
var delObj = document.createElement("span");
delObj.className = 'deleteFile';
delObj.innerText = ' [delete]';
entry.appendChild(delObj)
listObj.appendChild(entry)
});
})
.catch(function (err) {
window.alert(err);
});
});
window.addEventListener("click", function (evt) {
var t = evt.target;
if (t.className === 'deleteFile') {
var fname = t.parentElement.innerText;
fname = fname.split(' ')[0];
if (window.confirm("Delete " + fname + " ?")) {
fetch(fname, { method: 'DELETE' });
document.location.reload(false);
}
};
});
</script>
</body>
</html>
<html>
<head>
<title>HomePage</title>
<link Content-Type="text/css" href="/style.css" rel="stylesheet" />
</head>
<body>
<h1>Homepage of the WebServer Example</h1>
<p>The following pages are available:</p>
<ul>
<li><a href="/index.htm">/index.htm</a> - This page</li>
<li><a href="/files.htm">/files.htm</a> - Manage files on the server</li>
<li><a href="/$upload.htm">/$upload.htm</a> - Built-in upload utility</a></li>
<li><a href="/none.htm">/none.htm</a> - See the default response when files are not found.</a></li>
</ul>
<p>The following REST services are available:</p>
<ul>
<li><a href="/api/sysinfo">/api/sysinfo</a> - Some system level information</a></li>
<li><a href="/api/list">/api/list</a> - Array of all files</a></li>
</ul>
</body>
</html>
html, body {
color: #111111; font-family: Arial, ui-sans-serif, sans-serif; font-size: 1em; background-color: #f0f0f0;
}
#list > div {
margin: 0 0 0.5rem 0;
}
a { color: inherit; cursor: pointer; }
// Secrets for your local home network
// This is a "hard way" to configure your local WiFi network name and passphrase
// into the source code and the uploaded sketch.
//
// Using the WiFi Manager is preferred and avoids reprogramming when your network changes.
// See https://homeding.github.io/#page=/wifimanager.md
// ssid and passPhrase can be used when compiling for a specific environment as a 2. option.
// add you wifi network name and PassPhrase or use WiFi Manager
const char *ssid = "";
const char *passPhrase = "";
......@@ -38,6 +38,7 @@ static const char qop_auth[] PROGMEM = "qop=auth";
static const char qop_auth_quoted[] PROGMEM = "qop=\"auth\"";
static const char WWW_Authenticate[] = "WWW-Authenticate";
static const char Content_Length[] = "Content-Length";
static const char ETAG_HEADER[] = "If-None-Match";
WebServer::WebServer(IPAddress addr, int port)
......@@ -381,6 +382,11 @@ void WebServer::enableCrossOrigin(boolean value) {
enableCORS(value);
}
void WebServer::enableETag(bool enable, ETagFunction fn) {
_eTagEnabled = enable;
_eTagFunction = fn;
}
void WebServer::_prepareHeader(String& response, int code, const char* content_type, size_t contentLength) {
response = String(F("HTTP/1.")) + String(_currentVersion) + ' ';
response += String(code);
......@@ -585,13 +591,14 @@ String WebServer::header(String name) {
}
void WebServer::collectHeaders(const char* headerKeys[], const size_t headerKeysCount) {
_headerKeysCount = headerKeysCount + 1;
_headerKeysCount = headerKeysCount + 2;
if (_currentHeaders)
delete[]_currentHeaders;
_currentHeaders = new RequestArgument[_headerKeysCount];
_currentHeaders[0].key = FPSTR(AUTHORIZATION_HEADER);
for (int i = 1; i < _headerKeysCount; i++){
_currentHeaders[i].key = headerKeys[i-1];
_currentHeaders[1].key = FPSTR(ETAG_HEADER);
for (int i = 2; i < _headerKeysCount; i++){
_currentHeaders[i].key = headerKeys[i-2];
}
}
......
......@@ -27,6 +27,7 @@
#include <functional>
#include <memory>
#include <WiFi.h>
#include <FS.h>
#include "HTTP_Method.h"
#include "Uri.h"
......@@ -130,6 +131,8 @@ public:
void enableDelay(boolean value);
void enableCORS(boolean value = true);
void enableCrossOrigin(boolean value = true);
typedef std::function<String(FS &fs, const String &fName)> ETagFunction;
void enableETag(bool enable, ETagFunction fn = nullptr);
void setContentLength(const size_t contentLength);
void sendHeader(const String& name, const String& value, bool first = false);
......@@ -146,6 +149,9 @@ public:
return _currentClient.write(file);
}
bool _eTagEnabled = false;
ETagFunction _eTagFunction = nullptr;
protected:
virtual size_t _currentClientWrite(const char* b, size_t l) { return _currentClient.write( b, l ); }
virtual size_t _currentClientWrite_P(PGM_P b, size_t l) { return _currentClient.write_P( b, l ); }
......
......@@ -5,6 +5,8 @@
#include "mimetable.h"
#include "WString.h"
#include "Uri.h"
#include <MD5Builder.h>
#include <base64.h>
using namespace mime;
......@@ -91,6 +93,7 @@ public:
log_v("StaticRequestHandler::handle: request=%s _uri=%s\r\n", requestUri.c_str(), _uri.c_str());
String path(_path);
String eTagCode;
if (!_isFile) {
// Base URI doesn't point to a file.
......@@ -117,9 +120,26 @@ public:
if (!f || !f.available())
return false;
if (server._eTagEnabled) {
if (server._eTagFunction) {
eTagCode = (server._eTagFunction)(_fs, path);
} else {
eTagCode = calcETag(_fs, path);
}
if (server.header("If-None-Match") == eTagCode) {
server.send(304);
return true;
}
}
if (_cache_header.length() != 0)
server.sendHeader("Cache-Control", _cache_header);
if ((server._eTagEnabled) && (eTagCode.length() > 0)) {
server.sendHeader("ETag", eTagCode);
}
server.streamFile(f, contentType);
return true;
}
......@@ -139,6 +159,26 @@ public:
return String(buff);
}
// calculate an ETag for a file in filesystem based on md5 checksum
// that can be used in the http headers - include quotes.
static String calcETag(FS &fs, const String &path) {
String result;
// calculate eTag using md5 checksum
uint8_t md5_buf[16];
File f = fs.open(path, "r");
MD5Builder calcMD5;
calcMD5.begin();
calcMD5.addStream(f, f.size());
calcMD5.calculate();
calcMD5.getBytes(md5_buf);
f.close();
// create a minimal-length eTag using base64 byte[]->text encoding.
result = "\"" + base64::encode(md5_buf, 16) + "\"";
return(result);
} // calcETag
protected:
FS _fs;
String _uri;
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment