Skip to content

Commit a341caf

Browse files
authored
Squashing all body parser commits to have a reasonable history.
Parsers for both multipart and form-encoded form POST handlers implemented. The API is geared towards handling file uploads and other large data items, and require the fields to be handled sequentially. An example program that allows uploading, downloading and editing (for text files) is supplied.
1 parent 2bd0b57 commit a341caf

10 files changed

+575
-94
lines changed

.gitignore

+4-1
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,7 @@ cert.h
6666
private_key.h
6767

6868
# Do not push VS Code project files
69-
.vscode
69+
.vscode
70+
71+
# Ignore platformio work directory
72+
.pio

examples/HTML-Forms/HTML-Forms.ino

+238-63
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,27 @@
2727
// We will use wifi
2828
#include <WiFi.h>
2929

30+
// We will use SPIFFS and FS
31+
#include <SPIFFS.h>
32+
#include <FS.h>
33+
3034
// Includes for the server
3135
#include <HTTPSServer.hpp>
3236
#include <SSLCert.hpp>
3337
#include <HTTPRequest.hpp>
3438
#include <HTTPResponse.hpp>
39+
#include <HTTPBodyParser.hpp>
40+
#include <HTTPMultipartBodyParser.hpp>
41+
#include <HTTPURLEncodedBodyParser.hpp>
42+
43+
// We need to specify some content-type mapping, so the resources get delivered with the
44+
// right content type and are displayed correctly in the browser
45+
char contentTypes[][2][32] = {
46+
{".txt", "text/plain"},
47+
{".png", "image/png"},
48+
{".jpg", "image/jpg"},
49+
{"", ""}
50+
};
3551

3652
// The HTTPS Server comes in a separate namespace. For easier use, include it here.
3753
using namespace httpsserver;
@@ -51,13 +67,34 @@ HTTPSServer secureServer = HTTPSServer(&cert);
5167
// which are pointers to the request data (read request body, headers, ...) and
5268
// to the response data (write response, set status code, ...)
5369
void handleRoot(HTTPRequest * req, HTTPResponse * res);
54-
void handleForm(HTTPRequest * req, HTTPResponse * res);
70+
void handleFormUpload(HTTPRequest * req, HTTPResponse * res);
71+
void handleFormEdit(HTTPRequest * req, HTTPResponse * res);
72+
void handleFile(HTTPRequest * req, HTTPResponse * res);
73+
void handleDirectory(HTTPRequest * req, HTTPResponse * res);
5574
void handle404(HTTPRequest * req, HTTPResponse * res);
5675

76+
std::string htmlEncode(std::string data) {
77+
// Quick and dirty: doesn't handle control chars and such.
78+
const char *p = data.c_str();
79+
std::string rv = "";
80+
while(p && *p) {
81+
char escapeChar = *p++;
82+
switch(escapeChar) {
83+
case '&': rv += "&amp;"; break;
84+
case '<': rv += "&lt;"; break;
85+
case '>': rv += "&gt;"; break;
86+
case '"': rv += "&quot;"; break;
87+
case '\'': rv += "&#x27;"; break;
88+
case '/': rv += "&#x2F;"; break;
89+
default: rv += escapeChar; break;
90+
}
91+
}
92+
return rv;
93+
}
94+
5795
void setup() {
5896
// For logging
5997
Serial.begin(115200);
60-
6198
// Connect to WiFi
6299
Serial.println("Setting up WiFi");
63100
WiFi.begin(WIFI_SSID, WIFI_PSK);
@@ -68,17 +105,28 @@ void setup() {
68105
Serial.print("Connected. IP=");
69106
Serial.println(WiFi.localIP());
70107

108+
// Setup filesystem
109+
if (!SPIFFS.begin(true)) Serial.println("Mounting SPIFFS failed");
110+
71111
// For every resource available on the server, we need to create a ResourceNode
72112
// The ResourceNode links URL and HTTP method to a handler function
73113
ResourceNode * nodeRoot = new ResourceNode("/", "GET", &handleRoot);
74-
ResourceNode * nodeForm = new ResourceNode("/", "POST", &handleForm);
114+
ResourceNode * nodeFormUpload = new ResourceNode("/upload", "POST", &handleFormUpload);
115+
ResourceNode * nodeFormEdit = new ResourceNode("/edit", "GET", &handleFormEdit);
116+
ResourceNode * nodeFormEditDone = new ResourceNode("/edit", "POST", &handleFormEdit);
117+
ResourceNode * nodeDirectory = new ResourceNode("/public", "GET", &handleDirectory);
118+
ResourceNode * nodeFile = new ResourceNode("/public/*", "GET", &handleFile);
75119

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

79123
// Add the root nodes to the server
80124
secureServer.registerNode(nodeRoot);
81-
secureServer.registerNode(nodeForm);
125+
secureServer.registerNode(nodeFormUpload);
126+
secureServer.registerNode(nodeFormEdit);
127+
secureServer.registerNode(nodeFormEditDone);
128+
secureServer.registerNode(nodeDirectory);
129+
secureServer.registerNode(nodeFile);
82130

83131
// Add the 404 not found node to the server.
84132
// The path is ignored for the default node.
@@ -108,73 +156,200 @@ void handleRoot(HTTPRequest * req, HTTPResponse * res) {
108156
// you would write to Serial etc.
109157
res->println("<!DOCTYPE html>");
110158
res->println("<html>");
111-
res->println("<head><title>Hello World!</title></head>");
159+
res->println("<head><title>Very simple file server</title></head>");
112160
res->println("<body>");
113-
res->println("<h1>HTML Forms</h1>");
114-
res->println("<p>This page contains some forms to show you how form data can be evaluated on server side.");
115-
116-
// The following forms will all use the same target (/ - the server's root directory) and POST method, so
117-
// the data will go to the request body. They differ on the value of the enctype though.
118-
119-
// enctype=x-www-form-urlencoded
120-
// means that the parameters of form elements will be encoded like they would
121-
// be encoded if you would use GET as method and append them to the URL (just after a ? at the end of the
122-
// resource path). Different fields are separated by an &-character. Special characters have a specific encoding
123-
// using the %-character, so for example %20 is the representation of a space.
124-
// The body could look like this:
125-
//
126-
// foo=bar&bat=baz
127-
//
128-
// Where foo and bat are the variables and bar and baz the values.
129-
//
130-
// Advantages:
131-
// - Low overhead
132-
// Disadvantages:
133-
// - May be hard to read for humans
134-
// - Cannot be used for inputs with type=file (you will only get the filename, not the content)
135-
res->println("<form method=\"POST\" action=\"/\" enctype=\"x-www-form-urlencoded\">");
136-
res->println("<h2>enctype=x-www-form-urlencoded</h2>");
137-
res->println("</form>")
138-
139-
// enctype=multipart/form-data
140-
//
141-
// TODO: Explanatory text
142-
//
143-
// Advantages:
144-
// - Even longer text stays somewhat human readable
145-
// - Can be used for files and binary data
146-
// Disadvantages:
147-
// - Big overhead if used for some small string values
148-
res->println("<form method=\"POST\" action=\"/\" enctype=\"multipart/form-data\">");
149-
res->println("<h2>enctype=multipart/form-data</h2>");
150-
res->println("</form>")
151-
161+
res->println("<h1>Very simple file server</h1>");
162+
res->println("<p>This is a very simple file server to demonstrate the use of POST forms. </p>");
163+
res->println("<h2>List existing files</h2>");
164+
res->println("<p>See <a href=\"/public\">/public</a> to list existing files and retrieve or edit them.</p>");
165+
res->println("<h2>Upload new file</h2>");
166+
res->println("<p>This form allows you to upload files (text, jpg and png supported best). It demonstrates multipart/form-data.</p>");
167+
res->println("<form method=\"POST\" action=\"/upload\" enctype=\"multipart/form-data\">");
168+
res->println("file: <input type=\"file\" name=\"file\"><br>");
169+
res->println("<input type=\"submit\" value=\"Upload\">");
170+
res->println("</form>");
152171
res->println("</body>");
153172
res->println("</html>");
154173
}
155174

156-
void handleForm(HTTPRequest * req, HTTPResponse * res) {
175+
void handleFormUpload(HTTPRequest * req, HTTPResponse * res) {
157176
// First, we need to check the encoding of the form that we have received.
158177
// The browser will set the Content-Type request header, so we can use it for that purpose.
159178
// Then we select the body parser based on the encoding.
160-
// Note: This is only necessary if you expect various enctypes to be send to the same handler.
161-
// If you would have only one form on your page with a fixed enctype, just instantiate the
162-
// corresponding reader.
163-
164-
// TODO: Select Parser, instantiate it
165-
166-
// Now we iterate over the so-called fields of the BodyParser. This works in the same way for
167-
// all body parsers.
168-
// The interface is somewhat limited, so you cannot just call something like
169-
// myParser.get("fieldname"), because this would require the parser to cache the whole request
170-
// body. If you have your ESP32 attached to an external SD Card and you want to be able to upload
171-
// files that are larger than the ESP's memory to that card, this would not work.
172-
// This is why you iterate over the fields by using myParser.next() and check the name of the
173-
// current field with myParser.getFieldName(), and then process it with a buffer.
174-
// If you need random access to all fields' values, you need to parse them into a map or some similar
175-
// data structure by yourself and make sure that all fits into the memory.
176-
177-
// TODO: Iterate over fields
179+
// Actually we do this only for documentary purposes, we know the form is going
180+
// to be multipart/form-data.
181+
HTTPBodyParser *parser;
182+
std::string contentType = req->getHeader("Content-Type");
183+
size_t semicolonPos = contentType.find(";");
184+
if (semicolonPos != std::string::npos) contentType = contentType.substr(0, semicolonPos);
185+
if (contentType == "multipart/form-data") {
186+
parser = new HTTPMultipartBodyParser(req);
187+
} else {
188+
Serial.printf("Unknown POST Content-Type: %s\n", contentType.c_str());
189+
return;
190+
}
191+
// We iterate over the fields. Any field with a filename is uploaded
192+
res->println("<html><head><title>File Upload</title></head><body><h1>File Upload</h1>");
193+
bool didwrite = false;
194+
while(parser->nextField()) {
195+
std::string name = parser->getFieldName();
196+
std::string filename = parser->getFieldFilename();
197+
std::string mimeType = parser->getFieldMimeType();
198+
Serial.printf("handleFormUpload: field name='%s', filename='%s', mimetype='%s'\n", name.c_str(), filename.c_str(), mimeType.c_str());
199+
// Double check that it is what we expect
200+
if (name != "file") {
201+
Serial.println("Skipping unexpected field");
202+
break;
203+
}
204+
// Should check file name validity and all that, but we skip that.
205+
std::string pathname = "/public/" + filename;
206+
File file = SPIFFS.open(pathname.c_str(), "w");
207+
size_t fileLength = 0;
208+
didwrite = true;
209+
while (!parser->endOfField()) {
210+
byte buf[512];
211+
size_t readLength = parser->read(buf, 512);
212+
file.write(buf, readLength);
213+
fileLength += readLength;
214+
}
215+
file.close();
216+
res->printf("<p>Saved %d bytes to %s</p>", (int)fileLength, pathname.c_str());
217+
}
218+
if (!didwrite) res->println("<p>Did not write any file</p>");
219+
res->println("</body></html>");
220+
delete parser;
221+
}
222+
223+
void handleFormEdit(HTTPRequest * req, HTTPResponse * res) {
224+
if (req->getMethod() == "GET") {
225+
// Initial request. Get filename from request parameters and return form.
226+
auto params = req->getParams();
227+
std::string filename;
228+
bool hasFilename = params->getQueryParameter("filename", filename);
229+
std::string pathname = std::string("/public/") + filename;
230+
res->println("<html><head><title>Edit File</title><head><body>");
231+
File file = SPIFFS.open(pathname.c_str());
232+
if (!hasFilename) {
233+
res->println("<p>No filename specified.</p>");
234+
} else
235+
if (!file.available()) {
236+
res->printf("<p>File not found: %s</p>\n", pathname.c_str());
237+
} else {
238+
res->printf("<h2>Edit content of %s</h2>\n", pathname.c_str());
239+
res->println("<form method=\"POST\" enctype=\"application/x-www-form-urlencoded\">");
240+
res->printf("<input name=\"filename\" type=\"hidden\" value=\"%s\">", filename.c_str());
241+
res->print("<textarea name=\"content\" rows=\"24\" cols=\"80\">");
242+
// Read the file and write it to the response
243+
size_t length = 0;
244+
do {
245+
char buffer[256];
246+
length = file.read((uint8_t *)buffer, 256);
247+
std::string bufferString(buffer, length);
248+
bufferString = htmlEncode(bufferString);
249+
res->write((uint8_t *)bufferString.c_str(), bufferString.size());
250+
} while (length > 0);
251+
res->println("</textarea><br>");
252+
res->println("<input type=\"submit\" value=\"Save\">");
253+
res->println("</form>");
254+
}
255+
res->println("</body></html>");
256+
} else {
257+
// Assume POST request. Contains submitted data.
258+
res->println("<html><head><title>File Edited</title><head><body><h1>File Edited</h1>");
259+
HTTPURLEncodedBodyParser parser(req);
260+
std::string filename;
261+
bool savedFile = false;
262+
while(parser.nextField()) {
263+
std::string name = parser.getFieldName();
264+
if (name == "filename") {
265+
char buf[512];
266+
size_t readLength = parser.read((byte *)buf, 512);
267+
filename = std::string("/public/") + std::string(buf, readLength);
268+
} else
269+
if (name == "content") {
270+
if (filename == "") {
271+
res->println("<p>Error: form contained content before filename.</p>");
272+
break;
273+
}
274+
size_t fieldLength = 0;
275+
File file = SPIFFS.open(filename.c_str(), "w");
276+
savedFile = true;
277+
while (!parser.endOfField()) {
278+
byte buf[512];
279+
size_t readLength = parser.read(buf, 512);
280+
file.write(buf, readLength);
281+
fieldLength += readLength;
282+
}
283+
file.close();
284+
res->printf("<p>Saved %d bytes to %s</p>", int(fieldLength), filename.c_str());
285+
} else {
286+
res->printf("<p>Unexpected field %s</p>", name.c_str());
287+
}
288+
}
289+
if (!savedFile) res->println("<p>No file to save...</p>");
290+
res->println("</body></html>");
291+
}
292+
}
293+
294+
void handleDirectory(HTTPRequest * req, HTTPResponse * res) {
295+
res->println("<html><head><title>File Listing</title><head><body>");
296+
File d = SPIFFS.open("/public");
297+
if (!d.isDirectory()) {
298+
res->println("<p>No files found.</p>");
299+
} else {
300+
res->println("<h1>File Listing</h1>");
301+
res->println("<ul>");
302+
File f = d.openNextFile();
303+
while (f) {
304+
std::string pathname(f.name());
305+
res->printf("<li><a href=\"%s\">%s</a>", pathname.c_str(), pathname.c_str());
306+
if (pathname.rfind(".txt") != std::string::npos) {
307+
std::string filename = pathname.substr(8); // Remove /public/
308+
res->printf(" <a href=\"/edit?filename=%s\">[edit]</a>", filename.c_str());
309+
}
310+
res->println("</li>");
311+
f = d.openNextFile();
312+
}
313+
res->println("</ul>");
314+
}
315+
res->println("</body></html>");
316+
}
317+
318+
void handleFile(HTTPRequest * req, HTTPResponse * res) {
319+
std::string filename = req->getRequestString();
320+
// Check if the file exists
321+
if (!SPIFFS.exists(filename.c_str())) {
322+
// Send "404 Not Found" as response, as the file doesn't seem to exist
323+
res->setStatusCode(404);
324+
res->setStatusText("Not found");
325+
res->println("404 Not Found");
326+
return;
327+
}
328+
329+
File file = SPIFFS.open(filename.c_str());
330+
331+
// Set length
332+
res->setHeader("Content-Length", httpsserver::intToString(file.size()));
333+
334+
// Content-Type is guessed using the definition of the contentTypes-table defined above
335+
int cTypeIdx = 0;
336+
do {
337+
if(filename.rfind(contentTypes[cTypeIdx][0])!=std::string::npos) {
338+
res->setHeader("Content-Type", contentTypes[cTypeIdx][1]);
339+
break;
340+
}
341+
cTypeIdx+=1;
342+
} while(strlen(contentTypes[cTypeIdx][0])>0);
343+
344+
// Read the file and write it to the response
345+
uint8_t buffer[256];
346+
size_t length = 0;
347+
do {
348+
length = file.read(buffer, 256);
349+
res->write(buffer, length);
350+
} while (length > 0);
351+
352+
file.close();
178353
}
179354

180355
void handle404(HTTPRequest * req, HTTPResponse * res) {

examples/Static-Page/Static-Page.cpp

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#include <Arduino.h>
2+
#include "Static-Page.ino"
3+

0 commit comments

Comments
 (0)