forked from fhessel/esp32_https_server
-
Notifications
You must be signed in to change notification settings - Fork 2
/
HTML-Forms.ino
490 lines (420 loc) · 19.2 KB
/
HTML-Forms.ino
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
/**
* Example for the ESP32 HTTP(S) Webserver
*
* IMPORTANT NOTE:
* To run this script, you need to
* 1) Enter your WiFi SSID and PSK below this comment
* 2) Make sure to have certificate data available. You will find a
* shell script and instructions to do so in the library folder
* under extras/
*
* This script will install an HTTPS Server on your ESP32 with the following
* functionalities:
* - Show simple page on web server root that includes some HTML Forms
* - Define a POST handler that handles the forms using the HTTPBodyParser API
* provided by the library.
* - 404 for everything else
*/
// TODO: Configure your WiFi here
#define WIFI_SSID "<your ssid goes here>"
#define WIFI_PSK "<your pre-shared key goes here>"
// Include certificate data (see note above)
#include "cert.h"
#include "private_key.h"
// We will use wifi
#include <WiFi.h>
// We will use SPIFFS and FS
#include <SPIFFS.h>
#include <FS.h>
// Includes for the server
#include <HTTPSServer.hpp>
#include <SSLCert.hpp>
#include <HTTPRequest.hpp>
#include <HTTPResponse.hpp>
#include <HTTPBodyParser.hpp>
#include <HTTPMultipartBodyParser.hpp>
#include <HTTPURLEncodedBodyParser.hpp>
// We need to specify some content-type mapping, so the resources get delivered with the
// right content type and are displayed correctly in the browser
char contentTypes[][2][32] = {
{".txt", "text/plain"},
{".html", "text/html"},
{".png", "image/png"},
{".jpg", "image/jpg"},
{"", ""}
};
// The HTTPS Server comes in a separate namespace. For easier use, include it here.
using namespace httpsserver;
// Create an SSL certificate object from the files included above
SSLCert cert = SSLCert(
example_crt_DER, example_crt_DER_len,
example_key_DER, example_key_DER_len
);
// Create an SSL-enabled server that uses the certificate
// The contstructor takes some more parameters, but we go for default values here.
HTTPSServer secureServer = HTTPSServer(&cert);
// Declare some handler functions for the various URLs on the server
// See the static-page example for how handler functions work.
// The comments in setup() describe what each handler function does in this example.
void handleRoot(HTTPRequest * req, HTTPResponse * res);
void handleFormUpload(HTTPRequest * req, HTTPResponse * res);
void handleFormEdit(HTTPRequest * req, HTTPResponse * res);
void handleFile(HTTPRequest * req, HTTPResponse * res);
void handleDirectory(HTTPRequest * req, HTTPResponse * res);
void handle404(HTTPRequest * req, HTTPResponse * res);
// As we have a file editor where the content of a file is pasted into a <textarea>,
// we need to take care of encoding special characters correctly.
std::string htmlEncode(std::string data) {
// Quick and dirty: doesn't handle control chars and such. Don't use it in production code
const char *p = data.c_str();
std::string rv = "";
while(p && *p) {
char escapeChar = *p++;
switch(escapeChar) {
case '&': rv += "&"; break;
case '<': rv += "<"; break;
case '>': rv += ">"; break;
case '"': rv += """; break;
case '\'': rv += "'"; break;
case '/': rv += "/"; break;
default: rv += escapeChar; break;
}
}
return rv;
}
void setup() {
// For logging
Serial.begin(115200);
// Connect to WiFi
Serial.println("Setting up WiFi");
WiFi.begin(WIFI_SSID, WIFI_PSK);
while (WiFi.status() != WL_CONNECTED) {
Serial.print(".");
delay(500);
}
Serial.print("Connected. IP=");
Serial.println(WiFi.localIP());
// Setup filesystem
if (!SPIFFS.begin(true)) Serial.println("Mounting SPIFFS failed");
// For every resource available on the server, we need to create a ResourceNode
// The ResourceNode links URL and HTTP method to a handler function
// The root node shows a static page with a link to the file directory and a small
// HTML form that allows uploading new forms
ResourceNode * nodeRoot = new ResourceNode("/", "GET", &handleRoot);
// The handleFormUpload handler handles the file upload from the root node. As the form
// is submitted via post, we need to specify that as handler method here:
ResourceNode * nodeFormUpload = new ResourceNode("/upload", "POST", &handleFormUpload);
// For the editor, we use the same handler function and register it with the GET and POST
// method. The handler decides what to do based on the method used to call it:
ResourceNode * nodeFormEdit = new ResourceNode("/edit", "GET", &handleFormEdit);
ResourceNode * nodeFormEditDone = new ResourceNode("/edit", "POST", &handleFormEdit);
// To keep track of all uploaded files, we provide a directory listing here with an edit
// button for text-based files:
ResourceNode * nodeDirectory = new ResourceNode("/public", "GET", &handleDirectory);
// And of course we need some way to retrieve the file again. We use the placeholder
// feature in the path to do so:
ResourceNode * nodeFile = new ResourceNode("/public/*", "GET", &handleFile);
// 404 node has no URL as it is used for all requests that don't match anything else
ResourceNode * node404 = new ResourceNode("", "GET", &handle404);
// Add all nodes to the server so they become accessible:
secureServer.registerNode(nodeRoot);
secureServer.registerNode(nodeFormUpload);
secureServer.registerNode(nodeFormEdit);
secureServer.registerNode(nodeFormEditDone);
secureServer.registerNode(nodeDirectory);
secureServer.registerNode(nodeFile);
secureServer.setDefaultNode(node404);
Serial.println("Starting server...");
secureServer.start();
if (secureServer.isRunning()) {
Serial.println("Server ready.");
}
}
void loop() {
// This call will let the server do its work
secureServer.loop();
// Other code would go here...
delay(1);
}
void handleRoot(HTTPRequest * req, HTTPResponse * res) {
// Status code is 200 OK by default.
// We want to deliver a simple HTML page, so we send a corresponding content type:
res->setHeader("Content-Type", "text/html");
// Just the regular HTML document structure, nothing special to forms here....
res->println("<!DOCTYPE html>");
res->println("<html>");
res->println("<head><title>Very simple file server</title></head>");
res->println("<body>");
res->println("<h1>Very simple file server</h1>");
res->println("<p>This is a very simple file server to demonstrate the use of POST forms. </p>");
// The link to the file listing (/public is produced by handleDirectory())
res->println("<h2>List existing files</h2>");
res->println("<p>See <a href=\"/public\">/public</a> to list existing files and retrieve or edit them.</p>");
// Here comes the upload form. Note the enctype="multipart/form-data". Only by setting that enctype, you
// will be able to upload a file. If you miss it, the file field will only contain the filename.
// Method is POST, which matches the way that nodeFormUpload is configured in setup().
res->println("<h2>Upload new file</h2>");
res->println("<p>This form allows you to upload files (text, jpg and png supported best). It demonstrates multipart/form-data.</p>");
res->println("<form method=\"POST\" action=\"/upload\" enctype=\"multipart/form-data\">");
res->println("file: <input type=\"file\" name=\"file\"><br>");
res->println("<input type=\"submit\" value=\"Upload\">");
res->println("</form>");
res->println("</body>");
res->println("</html>");
}
void handleFormUpload(HTTPRequest * req, HTTPResponse * res) {
// First, we need to check the encoding of the form that we have received.
// The browser will set the Content-Type request header, so we can use it for that purpose.
// Then we select the body parser based on the encoding.
// Actually we do this only for documentary purposes, we know the form is going
// to be multipart/form-data.
HTTPBodyParser *parser;
std::string contentType = req->getHeader("Content-Type");
// The content type may have additional properties after a semicolon, for exampel:
// Content-Type: text/html;charset=utf-8
// Content-Type: multipart/form-data;boundary=------s0m3w31rdch4r4c73rs
// As we're interested only in the actual mime _type_, we strip everything after the
// first semicolon, if one exists:
size_t semicolonPos = contentType.find(";");
if (semicolonPos != std::string::npos) {
contentType = contentType.substr(0, semicolonPos);
}
// Now, we can decide based on the content type:
if (contentType == "multipart/form-data") {
parser = new HTTPMultipartBodyParser(req);
} else {
Serial.printf("Unknown POST Content-Type: %s\n", contentType.c_str());
return;
}
res->println("<html><head><title>File Upload</title></head><body><h1>File Upload</h1>");
// We iterate over the fields. Any field with a filename is uploaded.
// Note that the BodyParser consumes the request body, meaning that you can iterate over the request's
// fields only a single time. The reason for this is that it allows you to handle large requests
// which would not fit into memory.
bool didwrite = false;
// parser->nextField() will move the parser to the next field in the request body (field meaning a
// form field, if you take the HTML perspective). After the last field has been processed, nextField()
// returns false and the while loop ends.
while(parser->nextField()) {
// For Multipart data, each field has three properties:
// The name ("name" value of the <input> tag)
// The filename (If it was a <input type="file">, this is the filename on the machine of the
// user uploading it)
// The mime type (It is determined by the client. So do not trust this value and blindly start
// parsing files only if the type matches)
std::string name = parser->getFieldName();
std::string filename = parser->getFieldFilename();
std::string mimeType = parser->getFieldMimeType();
// We log all three values, so that you can observe the upload on the serial monitor:
Serial.printf("handleFormUpload: field name='%s', filename='%s', mimetype='%s'\n", name.c_str(), filename.c_str(), mimeType.c_str());
// Double check that it is what we expect
if (name != "file") {
Serial.println("Skipping unexpected field");
break;
}
// You should check file name validity and all that, but we skip that to make the core
// concepts of the body parser functionality easier to understand.
std::string pathname = "/public/" + filename;
// Create a new file on spiffs to stream the data into
File file = SPIFFS.open(pathname.c_str(), "w");
size_t fileLength = 0;
didwrite = true;
// With endOfField you can check whether the end of field has been reached or if there's
// still data pending. With multipart bodies, you cannot know the field size in advance.
while (!parser->endOfField()) {
byte buf[512];
size_t readLength = parser->read(buf, 512);
file.write(buf, readLength);
fileLength += readLength;
}
file.close();
res->printf("<p>Saved %d bytes to %s</p>", (int)fileLength, pathname.c_str());
}
if (!didwrite) {
res->println("<p>Did not write any file</p>");
}
res->println("</body></html>");
delete parser;
}
void handleFormEdit(HTTPRequest * req, HTTPResponse * res) {
// This handler function does two things:
// For GET: Show an editor
// For POST: Handle editor submit
if (req->getMethod() == "GET") {
// Initial request. Get filename from request parameters and return form.
// The filename is in the URL, so we need to use the query params here:
// (URL is like /edit?filename=something.txt)
auto params = req->getParams();
std::string filename;
bool hasFilename = params->getQueryParameter("filename", filename);
std::string pathname = std::string("/public/") + filename;
// Start writing the HTML output
res->println("<html><head><title>Edit File</title><head><body>");
// Try to open the file from SPIFFS
File file = SPIFFS.open(pathname.c_str());
if (!hasFilename) {
// No ?filename=something parameter was given
res->println("<p>No filename specified.</p>");
} else if (!file.available()) {
// The file didn't exist in the SPIFFS
res->printf("<p>File not found: %s</p>\n", pathname.c_str());
} else {
// We have a file, render the form:
res->printf("<h2>Edit content of %s</h2>\n", pathname.c_str());
// Start writing the form. The file content will be shown in a <textarea>, so there is
// no file upload happening (from the HTML perspective). For that reason, we use the
// x-www-form-urlencoded enctype as it is much more efficient:
res->println("<form method=\"POST\" enctype=\"application/x-www-form-urlencoded\">");
// Store the filename hidden in the form so that we know which file to update when the form
// is submitted
res->printf("<input name=\"filename\" type=\"hidden\" value=\"%s\">", filename.c_str());
res->print("<textarea name=\"content\" rows=\"24\" cols=\"80\">");
// Read the file from SPIFFS and write it to the HTTP response body
size_t length = 0;
do {
char buffer[256];
length = file.read((uint8_t *)buffer, 256);
std::string bufferString(buffer, length);
// htmlEncode handles conversions of < to < so that the form is rendered correctly
bufferString = htmlEncode(bufferString);
res->write((uint8_t *)bufferString.c_str(), bufferString.size());
} while (length > 0);
// Finalize the form with a submitt button
res->println("</textarea><br>");
res->println("<input type=\"submit\" value=\"Save\">");
res->println("</form>");
}
res->println("</body></html>");
} else { // method != GET
// Assume POST request. Contains submitted data.
res->println("<html><head><title>File Edited</title><head><body><h1>File Edited</h1>");
// The form is submitted with the x-www-form-urlencoded content type, so we need the
// HTTPURLEncodedBodyParser to read the fields.
// Note that the content of the file's content comes from a <textarea>, so we
// can use the URL encoding here, since no file upload from an <input type="file"
// is involved.
HTTPURLEncodedBodyParser parser(req);
// The bodyparser will consume the request body. That means you can iterate over the
// fields only ones. For that reason, we need to create variables for all fields that
// we expect. So when parsing is done, you can process the field values from your
// temporary variables.
std::string filename;
bool savedFile = false;
// Iterate over the fields from the request body by calling nextField(). This function
// will update the field name and value of the body parsers. If the last field has been
// reached, it will return false and the while loop stops.
while(parser.nextField()) {
// Get the field name, so that we can decide what the value is for
std::string name = parser.getFieldName();
if (name == "filename") {
// Read the filename from the field's value, add the /public prefix and store it in
// the filename variable.
char buf[512];
size_t readLength = parser.read((byte *)buf, 512);
filename = std::string("/public/") + std::string(buf, readLength);
} else if (name == "content") {
// Browsers must return the fields in the order that they are placed in
// the HTML form, so if the broweser behaves correctly, this condition will
// never be true. We include it for safety reasons.
if (filename == "") {
res->println("<p>Error: form contained content before filename.</p>");
break;
}
// With parser.read() and parser.endOfField(), we can stream the field content
// into a buffer. That allows handling arbitrarily-sized field contents. Here,
// we use it and write the file contents directly to the SPIFFS:
size_t fieldLength = 0;
File file = SPIFFS.open(filename.c_str(), "w");
savedFile = true;
while (!parser.endOfField()) {
byte buf[512];
size_t readLength = parser.read(buf, 512);
file.write(buf, readLength);
fieldLength += readLength;
}
file.close();
res->printf("<p>Saved %d bytes to %s</p>", int(fieldLength), filename.c_str());
} else {
res->printf("<p>Unexpected field %s</p>", name.c_str());
}
}
if (!savedFile) {
res->println("<p>No file to save...</p>");
}
res->println("</body></html>");
}
}
void handleDirectory(HTTPRequest * req, HTTPResponse * res) {
res->println("<html><head><title>File Listing</title><head><body>");
// We read the SPIFFS folder public and render all files to the HTML page:
File d = SPIFFS.open("/public");
if (!d.isDirectory()) {
res->println("<p>No files found.</p>");
} else {
res->println("<h1>File Listing</h1>");
res->println("<ul>");
File f = d.openNextFile();
while (f) {
std::string pathname(f.name());
// We render a link to /public/... for each file that we find
res->printf("<li><a href=\"%s\">%s</a>", pathname.c_str(), pathname.c_str());
if (pathname.rfind(".txt") != std::string::npos) {
// And if the file is a text file, we also include an editor link like
// /edit?filename=... to open the editor, which is created by handleFormEdit.
std::string filename = pathname.substr(8); // Remove /public/
res->printf(" <a href=\"/edit?filename=%s\">[edit]</a>", filename.c_str());
}
res->println("</li>");
f = d.openNextFile();
}
res->println("</ul>");
}
res->println("</body></html>");
}
void handleFile(HTTPRequest * req, HTTPResponse * res) {
std::string filename = req->getRequestString();
// Check if the file exists
if (!SPIFFS.exists(filename.c_str())) {
// Send "404 Not Found" as response, as the file doesn't seem to exist
res->setStatusCode(404);
res->setStatusText("Not found");
res->println("404 Not Found");
return;
}
File file = SPIFFS.open(filename.c_str());
// Set length
res->setHeader("Content-Length", httpsserver::intToString(file.size()));
// Content-Type is guessed using the definition of the contentTypes-table defined above
int cTypeIdx = 0;
do {
if(filename.rfind(contentTypes[cTypeIdx][0])!=std::string::npos) {
res->setHeader("Content-Type", contentTypes[cTypeIdx][1]);
break;
}
cTypeIdx+=1;
} while(strlen(contentTypes[cTypeIdx][0])>0);
// Read the file and write it to the response
uint8_t buffer[256];
size_t length = 0;
do {
length = file.read(buffer, 256);
res->write(buffer, length);
} while (length > 0);
file.close();
}
void handle404(HTTPRequest * req, HTTPResponse * res) {
// Discard request body, if we received any
// We do this, as this is the default node and may also server POST/PUT requests
req->discardRequestBody();
// Set the response status
res->setStatusCode(404);
res->setStatusText("Not Found");
// Set content type of the response
res->setHeader("Content-Type", "text/html");
// Write a tiny HTML page
res->println("<!DOCTYPE html>");
res->println("<html>");
res->println("<head><title>Not Found</title></head>");
res->println("<body><h1>404 Not Found</h1><p>The requested resource was not found on this server.</p></body>");
res->println("</html>");
}