Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit daf6ca1

Browse filesBrowse files
committed
Extend documentation of the HTML forms example. #81
1 parent 8975d22 commit daf6ca1
Copy full SHA for daf6ca1

File tree

Expand file treeCollapse file tree

2 files changed

+134
-20
lines changed
Filter options
Expand file treeCollapse file tree

2 files changed

+134
-20
lines changed

‎README.md

Copy file name to clipboardExpand all lines: README.md
+5-4Lines changed: 5 additions & 4 deletions
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

Copy file name to clipboardExpand all lines: examples/HTML-Forms/HTML-Forms.ino
+129-16Lines changed: 129 additions & 16 deletions
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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.