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.
3753using 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, ...)
5369void 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);
5574void 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 += " &" ; 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+
5795void 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
180355void handle404 (HTTPRequest * req, HTTPResponse * res) {
0 commit comments