Skip to content

Commit daf6ca1

Browse files
committed
Extend documentation of the HTML forms example. fhessel#81
1 parent 8975d22 commit daf6ca1

File tree

2 files changed

+134
-20
lines changed

2 files changed

+134
-20
lines changed

README.md

+5-4
Original file line numberDiff line numberDiff line change
@@ -73,18 +73,19 @@ git clone https://github.com/fhessel/esp32_https_server.git
7373

7474
> **Note:** To run the examples (except for the _Self-Signed-Certificates_ example), you need to execute the script extras/create_cert.sh first (see [Issue #26](https://github.com/fhessel/esp32_https_server/issues/26) for Windows). This script will create a simple CA to sign certificates that are used with the examples. Some notes on the usage can be found in the extras/README.md file.
7575
76-
You will find several examples showing how you can use the library:
76+
You will find several examples showing how you can use the library (roughly ordered by complexity):
7777

7878
- [Static-Page](examples/Static-Page/Static-Page.ino): Short example showing how to serve some static resources with the server. You should start with this sketch and get familiar with it before having a look at the more complex examples.
7979
- [Parameters](examples/Parameters/Parameters.ino): Shows how you can access request parameters (the part after the question mark in the URL) or parameters in dynamic URLs (like /led/1, /led/2, ...)
80+
- [Parameter-Validation](examples/Parameter-Validation/Parameter-Validation.ino): Shows how you can integrate validator functions to do formal checks on parameters in your URL.
8081
- [Put-Post-Echo](examples/Put-Post-Echo/Put-Post-Echo.ino): Implements a simple echo service for PUT and POST requests that returns the request body as response body. Also shows how to differentiate between multiple HTTP methods for the same URL.
8182
- [HTTPS-and-HTTP](examples/HTTPS-and-HTTP/HTTPS-and-HTTP.ino): Shows how to serve resources via HTTP and HTTPS in parallel and how to check if the user is using a secure connection during request handling
83+
- [HTML-Forms](examples/HTML-Forms/HTML-Forms.ino): Shows how to use body parsers to handle requests created from HTML forms (access text field contents, handle file upload, etc.).
84+
- [Async-Server](examples/Async-Server/Async-Server.ino): Like the Static-Page example, but the server runs in a separate task on the ESP32, so you do not need to call the loop() function in your main sketch.
85+
- [Self-Signed-Certificate](examples/Self-Signed-Certificate/Self-Signed-Certificate.ino): Shows how to generate a self-signed certificate on the fly on the ESP when the sketch starts. You do not need to run `create_cert.sh` to use this example.
8286
- [Middleware](examples/Middleware/Middleware.ino): Shows how to use the middleware API for logging. Middleware functions are defined very similar to webservers like Express.
8387
- [Authentication](examples/Authentication/Authentication.ino): Implements a chain of two middleware functions to handle authentication and authorization using HTTP Basic Auth.
84-
- [Async-Server](examples/Async-Server/Async-Server.ino): Like the Static-Page example, but the server runs in a separate task on the ESP32, so you do not need to call the loop() function in your main sketch.
8588
- [Websocket-Chat](examples/Websocket-Chat/Websocket-Chat.ino): Provides a browser-based chat built on top of websockets. **Note:** Websockets are still under development!
86-
- [Parameter-Validation](examples/Parameter-Validation/Parameter-Validation.ino): Shows how you can integrate validator functions to do formal checks on parameters in your URL.
87-
- [Self-Signed-Certificate](examples/Self-Signed-Certificate/Self-Signed-Certificate.ino): Shows how to generate a self-signed certificate on the fly on the ESP when the sketch starts. You do not need to run `create_cert.sh` to use this example.
8889
- [REST-API](examples/REST-API/REST-API.ino): Uses [ArduinoJSON](https://arduinojson.org/) and [SPIFFS file upload](https://github.com/me-no-dev/arduino-esp32fs-plugin) to serve a small web interface that provides a REST API.
8990

9091
If you encounter error messages that cert.h or private\_key.h are missing when running an example, make sure to run create\_cert.sh first (see Setup Instructions).

examples/HTML-Forms/HTML-Forms.ino

+129-16
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,10 @@
4343
// We need to specify some content-type mapping, so the resources get delivered with the
4444
// right content type and are displayed correctly in the browser
4545
char contentTypes[][2][32] = {
46-
{".txt", "text/plain"},
47-
{".png", "image/png"},
48-
{".jpg", "image/jpg"},
46+
{".txt", "text/plain"},
47+
{".html", "text/html"},
48+
{".png", "image/png"},
49+
{".jpg", "image/jpg"},
4950
{"", ""}
5051
};
5152

@@ -63,18 +64,19 @@ SSLCert cert = SSLCert(
6364
HTTPSServer secureServer = HTTPSServer(&cert);
6465

6566
// Declare some handler functions for the various URLs on the server
66-
// The signature is always the same for those functions. They get two parameters,
67-
// which are pointers to the request data (read request body, headers, ...) and
68-
// to the response data (write response, set status code, ...)
67+
// See the static-page example for how handler functions work.
68+
// The comments in setup() describe what each handler function does in this example.
6969
void handleRoot(HTTPRequest * req, HTTPResponse * res);
7070
void handleFormUpload(HTTPRequest * req, HTTPResponse * res);
7171
void handleFormEdit(HTTPRequest * req, HTTPResponse * res);
7272
void handleFile(HTTPRequest * req, HTTPResponse * res);
7373
void handleDirectory(HTTPRequest * req, HTTPResponse * res);
7474
void handle404(HTTPRequest * req, HTTPResponse * res);
7575

76+
// As we have a file editor where the content of a file is pasted into a <textarea>,
77+
// we need to take care of encoding special characters correctly.
7678
std::string htmlEncode(std::string data) {
77-
// Quick and dirty: doesn't handle control chars and such.
79+
// Quick and dirty: doesn't handle control chars and such. Don't use it in production code
7880
const char *p = data.c_str();
7981
std::string rv = "";
8082
while(p && *p) {
@@ -110,26 +112,37 @@ void setup() {
110112

111113
// For every resource available on the server, we need to create a ResourceNode
112114
// The ResourceNode links URL and HTTP method to a handler function
115+
116+
// The root node shows a static page with a link to the file directory and a small
117+
// HTML form that allows uploading new forms
113118
ResourceNode * nodeRoot = new ResourceNode("/", "GET", &handleRoot);
119+
// The handleFormUpload handler handles the file upload from the root node. As the form
120+
// is submitted via post, we need to specify that as handler method here:
114121
ResourceNode * nodeFormUpload = new ResourceNode("/upload", "POST", &handleFormUpload);
122+
123+
// For the editor, we use the same handler function and register it with the GET and POST
124+
// method. The handler decides what to do based on the method used to call it:
115125
ResourceNode * nodeFormEdit = new ResourceNode("/edit", "GET", &handleFormEdit);
116126
ResourceNode * nodeFormEditDone = new ResourceNode("/edit", "POST", &handleFormEdit);
127+
128+
// To keep track of all uploaded files, we provide a directory listing here with an edit
129+
// button for text-based files:
117130
ResourceNode * nodeDirectory = new ResourceNode("/public", "GET", &handleDirectory);
131+
132+
// And of course we need some way to retrieve the file again. We use the placeholder
133+
// feature in the path to do so:
118134
ResourceNode * nodeFile = new ResourceNode("/public/*", "GET", &handleFile);
119135

120136
// 404 node has no URL as it is used for all requests that don't match anything else
121137
ResourceNode * node404 = new ResourceNode("", "GET", &handle404);
122138

123-
// Add the root nodes to the server
139+
// Add all nodes to the server so they become accessible:
124140
secureServer.registerNode(nodeRoot);
125141
secureServer.registerNode(nodeFormUpload);
126142
secureServer.registerNode(nodeFormEdit);
127143
secureServer.registerNode(nodeFormEditDone);
128144
secureServer.registerNode(nodeDirectory);
129145
secureServer.registerNode(nodeFile);
130-
131-
// Add the 404 not found node to the server.
132-
// The path is ignored for the default node.
133146
secureServer.setDefaultNode(node404);
134147

135148
Serial.println("Starting server...");
@@ -152,22 +165,28 @@ void handleRoot(HTTPRequest * req, HTTPResponse * res) {
152165
// We want to deliver a simple HTML page, so we send a corresponding content type:
153166
res->setHeader("Content-Type", "text/html");
154167

155-
// The response implements the Print interface, so you can use it just like
156-
// you would write to Serial etc.
168+
// Just the regular HTML document structure, nothing special to forms here....
157169
res->println("<!DOCTYPE html>");
158170
res->println("<html>");
159171
res->println("<head><title>Very simple file server</title></head>");
160172
res->println("<body>");
161173
res->println("<h1>Very simple file server</h1>");
162174
res->println("<p>This is a very simple file server to demonstrate the use of POST forms. </p>");
175+
176+
// The link to the file listing (/public is produced by handleDirectory())
163177
res->println("<h2>List existing files</h2>");
164178
res->println("<p>See <a href=\"/public\">/public</a> to list existing files and retrieve or edit them.</p>");
179+
180+
// Here comes the upload form. Note the enctype="multipart/form-data". Only by setting that enctype, you
181+
// will be able to upload a file. If you miss it, the file field will only contain the filename.
182+
// Method is POST, which matches the way that nodeFormUpload is configured in setup().
165183
res->println("<h2>Upload new file</h2>");
166184
res->println("<p>This form allows you to upload files (text, jpg and png supported best). It demonstrates multipart/form-data.</p>");
167185
res->println("<form method=\"POST\" action=\"/upload\" enctype=\"multipart/form-data\">");
168186
res->println("file: <input type=\"file\" name=\"file\"><br>");
169187
res->println("<input type=\"submit\" value=\"Upload\">");
170188
res->println("</form>");
189+
171190
res->println("</body>");
172191
res->println("</html>");
173192
}
@@ -180,34 +199,66 @@ void handleFormUpload(HTTPRequest * req, HTTPResponse * res) {
180199
// to be multipart/form-data.
181200
HTTPBodyParser *parser;
182201
std::string contentType = req->getHeader("Content-Type");
202+
203+
// The content type may have additional properties after a semicolon, for exampel:
204+
// Content-Type: text/html;charset=utf-8
205+
// Content-Type: multipart/form-data;boundary=------s0m3w31rdch4r4c73rs
206+
// As we're interested only in the actual mime _type_, we strip everything after the
207+
// first semicolon, if one exists:
183208
size_t semicolonPos = contentType.find(";");
184209
if (semicolonPos != std::string::npos) {
185210
contentType = contentType.substr(0, semicolonPos);
186211
}
212+
213+
// Now, we can decide based on the content type:
187214
if (contentType == "multipart/form-data") {
188215
parser = new HTTPMultipartBodyParser(req);
189216
} else {
190217
Serial.printf("Unknown POST Content-Type: %s\n", contentType.c_str());
191218
return;
192219
}
193-
// We iterate over the fields. Any field with a filename is uploaded
220+
194221
res->println("<html><head><title>File Upload</title></head><body><h1>File Upload</h1>");
222+
223+
// We iterate over the fields. Any field with a filename is uploaded.
224+
// Note that the BodyParser consumes the request body, meaning that you can iterate over the request's
225+
// fields only a single time. The reason for this is that it allows you to handle large requests
226+
// which would not fit into memory.
195227
bool didwrite = false;
228+
229+
// parser->nextField() will move the parser to the next field in the request body (field meaning a
230+
// form field, if you take the HTML perspective). After the last field has been processed, nextField()
231+
// returns false and the while loop ends.
196232
while(parser->nextField()) {
233+
// For Multipart data, each field has three properties:
234+
// The name ("name" value of the <input> tag)
235+
// The filename (If it was a <input type="file">, this is the filename on the machine of the
236+
// user uploading it)
237+
// The mime type (It is determined by the client. So do not trust this value and blindly start
238+
// parsing files only if the type matches)
197239
std::string name = parser->getFieldName();
198240
std::string filename = parser->getFieldFilename();
199241
std::string mimeType = parser->getFieldMimeType();
242+
// We log all three values, so that you can observe the upload on the serial monitor:
200243
Serial.printf("handleFormUpload: field name='%s', filename='%s', mimetype='%s'\n", name.c_str(), filename.c_str(), mimeType.c_str());
244+
201245
// Double check that it is what we expect
202246
if (name != "file") {
203247
Serial.println("Skipping unexpected field");
204248
break;
205249
}
206-
// Should check file name validity and all that, but we skip that.
250+
251+
// You should check file name validity and all that, but we skip that to make the core
252+
// concepts of the body parser functionality easier to understand.
207253
std::string pathname = "/public/" + filename;
254+
255+
// Create a new file on spiffs to stream the data into
208256
File file = SPIFFS.open(pathname.c_str(), "w");
209257
size_t fileLength = 0;
210258
didwrite = true;
259+
260+
// With endOfField you can check whether the end of field has been reached or if there's
261+
// still data pending. With multipart bodies, you cannot know the field size in advance.
211262
while (!parser->endOfField()) {
212263
byte buf[512];
213264
size_t readLength = parser->read(buf, 512);
@@ -225,54 +276,107 @@ void handleFormUpload(HTTPRequest * req, HTTPResponse * res) {
225276
}
226277

227278
void handleFormEdit(HTTPRequest * req, HTTPResponse * res) {
279+
// This handler function does two things:
280+
// For GET: Show an editor
281+
// For POST: Handle editor submit
228282
if (req->getMethod() == "GET") {
229283
// Initial request. Get filename from request parameters and return form.
284+
// The filename is in the URL, so we need to use the query params here:
285+
// (URL is like /edit?filename=something.txt)
230286
auto params = req->getParams();
231287
std::string filename;
232288
bool hasFilename = params->getQueryParameter("filename", filename);
233289
std::string pathname = std::string("/public/") + filename;
290+
291+
// Start writing the HTML output
234292
res->println("<html><head><title>Edit File</title><head><body>");
293+
294+
// Try to open the file from SPIFFS
235295
File file = SPIFFS.open(pathname.c_str());
236296
if (!hasFilename) {
297+
// No ?filename=something parameter was given
237298
res->println("<p>No filename specified.</p>");
299+
238300
} else if (!file.available()) {
301+
// The file didn't exist in the SPIFFS
239302
res->printf("<p>File not found: %s</p>\n", pathname.c_str());
303+
240304
} else {
305+
// We have a file, render the form:
241306
res->printf("<h2>Edit content of %s</h2>\n", pathname.c_str());
307+
308+
// Start writing the form. The file content will be shown in a <textarea>, so there is
309+
// no file upload happening (from the HTML perspective). For that reason, we use the
310+
// x-www-form-urlencoded enctype as it is much more efficient:
242311
res->println("<form method=\"POST\" enctype=\"application/x-www-form-urlencoded\">");
312+
313+
// Store the filename hidden in the form so that we know which file to update when the form
314+
// is submitted
243315
res->printf("<input name=\"filename\" type=\"hidden\" value=\"%s\">", filename.c_str());
244316
res->print("<textarea name=\"content\" rows=\"24\" cols=\"80\">");
245-
// Read the file and write it to the response
317+
318+
// Read the file from SPIFFS and write it to the HTTP response body
246319
size_t length = 0;
247320
do {
248321
char buffer[256];
249322
length = file.read((uint8_t *)buffer, 256);
250323
std::string bufferString(buffer, length);
324+
// htmlEncode handles conversions of < to &lt; so that the form is rendered correctly
251325
bufferString = htmlEncode(bufferString);
252326
res->write((uint8_t *)bufferString.c_str(), bufferString.size());
253327
} while (length > 0);
328+
329+
// Finalize the form with a submitt button
254330
res->println("</textarea><br>");
255331
res->println("<input type=\"submit\" value=\"Save\">");
256332
res->println("</form>");
257333
}
258334
res->println("</body></html>");
335+
259336
} else { // method != GET
260337
// Assume POST request. Contains submitted data.
261338
res->println("<html><head><title>File Edited</title><head><body><h1>File Edited</h1>");
339+
340+
// The form is submitted with the x-www-form-urlencoded content type, so we need the
341+
// HTTPURLEncodedBodyParser to read the fields.
342+
// Note that the content of the file's content comes from a <textarea>, so we
343+
// can use the URL encoding here, since no file upload from an <input type="file"
344+
// is involved.
262345
HTTPURLEncodedBodyParser parser(req);
346+
347+
// The bodyparser will consume the request body. That means you can iterate over the
348+
// fields only ones. For that reason, we need to create variables for all fields that
349+
// we expect. So when parsing is done, you can process the field values from your
350+
// temporary variables.
263351
std::string filename;
264352
bool savedFile = false;
353+
354+
// Iterate over the fields from the request body by calling nextField(). This function
355+
// will update the field name and value of the body parsers. If the last field has been
356+
// reached, it will return false and the while loop stops.
265357
while(parser.nextField()) {
358+
// Get the field name, so that we can decide what the value is for
266359
std::string name = parser.getFieldName();
360+
267361
if (name == "filename") {
362+
// Read the filename from the field's value, add the /public prefix and store it in
363+
// the filename variable.
268364
char buf[512];
269365
size_t readLength = parser.read((byte *)buf, 512);
270366
filename = std::string("/public/") + std::string(buf, readLength);
367+
271368
} else if (name == "content") {
369+
// Browsers must return the fields in the order that they are placed in
370+
// the HTML form, so if the broweser behaves correctly, this condition will
371+
// never be true. We include it for safety reasons.
272372
if (filename == "") {
273373
res->println("<p>Error: form contained content before filename.</p>");
274374
break;
275375
}
376+
377+
// With parser.read() and parser.endOfField(), we can stream the field content
378+
// into a buffer. That allows handling arbitrarily-sized field contents. Here,
379+
// we use it and write the file contents directly to the SPIFFS:
276380
size_t fieldLength = 0;
277381
File file = SPIFFS.open(filename.c_str(), "w");
278382
savedFile = true;
@@ -284,6 +388,7 @@ void handleFormEdit(HTTPRequest * req, HTTPResponse * res) {
284388
}
285389
file.close();
286390
res->printf("<p>Saved %d bytes to %s</p>", int(fieldLength), filename.c_str());
391+
287392
} else {
288393
res->printf("<p>Unexpected field %s</p>", name.c_str());
289394
}
@@ -297,7 +402,10 @@ void handleFormEdit(HTTPRequest * req, HTTPResponse * res) {
297402

298403
void handleDirectory(HTTPRequest * req, HTTPResponse * res) {
299404
res->println("<html><head><title>File Listing</title><head><body>");
405+
406+
// We read the SPIFFS folder public and render all files to the HTML page:
300407
File d = SPIFFS.open("/public");
408+
301409
if (!d.isDirectory()) {
302410
res->println("<p>No files found.</p>");
303411
} else {
@@ -306,8 +414,13 @@ void handleDirectory(HTTPRequest * req, HTTPResponse * res) {
306414
File f = d.openNextFile();
307415
while (f) {
308416
std::string pathname(f.name());
417+
418+
// We render a link to /public/... for each file that we find
309419
res->printf("<li><a href=\"%s\">%s</a>", pathname.c_str(), pathname.c_str());
420+
310421
if (pathname.rfind(".txt") != std::string::npos) {
422+
// And if the file is a text file, we also include an editor link like
423+
// /edit?filename=... to open the editor, which is created by handleFormEdit.
311424
std::string filename = pathname.substr(8); // Remove /public/
312425
res->printf(" <a href=\"/edit?filename=%s\">[edit]</a>", filename.c_str());
313426
}

0 commit comments

Comments
 (0)