| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 2bd0b57 commit a341caf
10 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -66,4 +66,7 @@ cert.h | |||
| 66 | 66 | private_key.h | |
| 67 | 67 | ||
| 68 | 68 | # Do not push VS Code project files | |
| 69 | - .vscode | ||
| 69 | + .vscode | ||
| 70 | + | ||
| 71 | + # Ignore platformio work directory | ||
| 72 | + .pio | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -27,11 +27,27 @@ | |||
| 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) { | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,3 @@ | |||
| 1 | + #include <Arduino.h> | ||
| 2 | + #include "Static-Page.ino" | ||
| 3 | + | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments