27
27
// We will use wifi
28
28
#include < WiFi.h>
29
29
30
+ // We will use SPIFFS and FS
31
+ #include < SPIFFS.h>
32
+ #include < FS.h>
33
+
30
34
// Includes for the server
31
35
#include < HTTPSServer.hpp>
32
36
#include < SSLCert.hpp>
33
37
#include < HTTPRequest.hpp>
34
38
#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
+ };
35
51
36
52
// The HTTPS Server comes in a separate namespace. For easier use, include it here.
37
53
using namespace httpsserver ;
@@ -51,13 +67,34 @@ HTTPSServer secureServer = HTTPSServer(&cert);
51
67
// which are pointers to the request data (read request body, headers, ...) and
52
68
// to the response data (write response, set status code, ...)
53
69
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);
55
74
void handle404 (HTTPRequest * req, HTTPResponse * res);
56
75
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 += " &" ; break ;
84
+ case ' <' : rv += " <" ; break ;
85
+ case ' >' : rv += " >" ; break ;
86
+ case ' "' : rv += " "" ; break ;
87
+ case ' \' ' : rv += " '" ; break ;
88
+ case ' /' : rv += " /" ; break ;
89
+ default : rv += escapeChar; break ;
90
+ }
91
+ }
92
+ return rv;
93
+ }
94
+
57
95
void setup () {
58
96
// For logging
59
97
Serial.begin (115200 );
60
-
61
98
// Connect to WiFi
62
99
Serial.println (" Setting up WiFi" );
63
100
WiFi.begin (WIFI_SSID, WIFI_PSK);
@@ -68,17 +105,28 @@ void setup() {
68
105
Serial.print (" Connected. IP=" );
69
106
Serial.println (WiFi.localIP ());
70
107
108
+ // Setup filesystem
109
+ if (!SPIFFS.begin (true )) Serial.println (" Mounting SPIFFS failed" );
110
+
71
111
// For every resource available on the server, we need to create a ResourceNode
72
112
// The ResourceNode links URL and HTTP method to a handler function
73
113
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);
75
119
76
120
// 404 node has no URL as it is used for all requests that don't match anything else
77
121
ResourceNode * node404 = new ResourceNode (" " , " GET" , &handle404);
78
122
79
123
// Add the root nodes to the server
80
124
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);
82
130
83
131
// Add the 404 not found node to the server.
84
132
// The path is ignored for the default node.
@@ -108,73 +156,200 @@ void handleRoot(HTTPRequest * req, HTTPResponse * res) {
108
156
// you would write to Serial etc.
109
157
res->println (" <!DOCTYPE html>" );
110
158
res->println (" <html>" );
111
- res->println (" <head><title>Hello World! </title></head>" );
159
+ res->println (" <head><title>Very simple file server </title></head>" );
112
160
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>" );
152
171
res->println (" </body>" );
153
172
res->println (" </html>" );
154
173
}
155
174
156
- void handleForm (HTTPRequest * req, HTTPResponse * res) {
175
+ void handleFormUpload (HTTPRequest * req, HTTPResponse * res) {
157
176
// First, we need to check the encoding of the form that we have received.
158
177
// The browser will set the Content-Type request header, so we can use it for that purpose.
159
178
// 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 ();
178
353
}
179
354
180
355
void handle404 (HTTPRequest * req, HTTPResponse * res) {
0 commit comments