From 7861a98f1cebdebe623ceee85e6545c3f0757f50 Mon Sep 17 00:00:00 2001 From: Frank Hessel Date: Thu, 6 Dec 2018 20:05:16 +0100 Subject: [PATCH 1/9] First draft of bodyparser API --- examples/HTML-Forms/HTML-Forms.ino | 198 +++++++++++++++++++++++++++++ src/HTTPBodyParser.hpp | 62 +++++++++ src/HTTPMultipartBodyParser.cpp | 30 +++++ src/HTTPMultipartBodyParser.hpp | 22 ++++ src/HTTPURLEncodedBodyParser.cpp | 30 +++++ src/HTTPURLEncodedBodyParser.hpp | 22 ++++ 6 files changed, 364 insertions(+) create mode 100644 examples/HTML-Forms/HTML-Forms.ino create mode 100644 src/HTTPBodyParser.hpp create mode 100644 src/HTTPMultipartBodyParser.cpp create mode 100644 src/HTTPMultipartBodyParser.hpp create mode 100644 src/HTTPURLEncodedBodyParser.cpp create mode 100644 src/HTTPURLEncodedBodyParser.hpp diff --git a/examples/HTML-Forms/HTML-Forms.ino b/examples/HTML-Forms/HTML-Forms.ino new file mode 100644 index 0000000..ab8cf8b --- /dev/null +++ b/examples/HTML-Forms/HTML-Forms.ino @@ -0,0 +1,198 @@ +/** + * 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 "" +#define WIFI_PSK "" + +// Include certificate data (see note above) +#include "cert.h" +#include "private_key.h" + +// We will use wifi +#include + +// Includes for the server +#include +#include +#include +#include + +// 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 +// The signature is always the same for those functions. They get two parameters, +// which are pointers to the request data (read request body, headers, ...) and +// to the response data (write response, set status code, ...) +void handleRoot(HTTPRequest * req, HTTPResponse * res); +void handleForm(HTTPRequest * req, HTTPResponse * res); +void handle404(HTTPRequest * req, HTTPResponse * res); + +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()); + + // For every resource available on the server, we need to create a ResourceNode + // The ResourceNode links URL and HTTP method to a handler function + ResourceNode * nodeRoot = new ResourceNode("/", "GET", &handleRoot); + ResourceNode * nodeForm = new ResourceNode("/", "POST", &handleForm); + + // 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 the root nodes to the server + secureServer.registerNode(nodeRoot); + secureServer.registerNode(nodeForm); + + // Add the 404 not found node to the server. + // The path is ignored for the default node. + 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"); + + // The response implements the Print interface, so you can use it just like + // you would write to Serial etc. + res->println(""); + res->println(""); + res->println("Hello World!"); + res->println(""); + res->println("

HTML Forms

"); + res->println("

This page contains some forms to show you how form data can be evaluated on server side."); + + // The following forms will all use the same target (/ - the server's root directory) and POST method, so + // the data will go to the request body. They differ on the value of the enctype though. + + // enctype=x-www-form-urlencoded + // means that the parameters of form elements will be encoded like they would + // be encoded if you would use GET as method and append them to the URL (just after a ? at the end of the + // resource path). Different fields are separated by an &-character. Special characters have a specific encoding + // using the %-character, so for example %20 is the representation of a space. + // The body could look like this: + // + // foo=bar&bat=baz + // + // Where foo and bat are the variables and bar and baz the values. + // + // Advantages: + // - Low overhead + // Disadvantages: + // - May be hard to read for humans + // - Cannot be used for inputs with type=file (you will only get the filename, not the content) + res->println("

"); + res->println("

enctype=x-www-form-urlencoded

"); + res->println("
") + + // enctype=multipart/form-data + // + // TODO: Explanatory text + // + // Advantages: + // - Even longer text stays somewhat human readable + // - Can be used for files and binary data + // Disadvantages: + // - Big overhead if used for some small string values + res->println("
"); + res->println("

enctype=multipart/form-data

"); + res->println("
") + + res->println(""); + res->println(""); +} + +void handleForm(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. + // Note: This is only necessary if you expect various enctypes to be send to the same handler. + // If you would have only one form on your page with a fixed enctype, just instantiate the + // corresponding reader. + + // TODO: Select Parser, instantiate it + + // Now we iterate over the so-called fields of the BodyParser. This works in the same way for + // all body parsers. + // The interface is somewhat limited, so you cannot just call something like + // myParser.get("fieldname"), because this would require the parser to cache the whole request + // body. If you have your ESP32 attached to an external SD Card and you want to be able to upload + // files that are larger than the ESP's memory to that card, this would not work. + // This is why you iterate over the fields by using myParser.next() and check the name of the + // current field with myParser.getFieldName(), and then process it with a buffer. + // If you need random access to all fields' values, you need to parse them into a map or some similar + // data structure by yourself and make sure that all fits into the memory. + + // TODO: Iterate over fields +} + +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(""); + res->println(""); + res->println("Not Found"); + res->println("

404 Not Found

The requested resource was not found on this server.

"); + res->println(""); +} diff --git a/src/HTTPBodyParser.hpp b/src/HTTPBodyParser.hpp new file mode 100644 index 0000000..ba252b8 --- /dev/null +++ b/src/HTTPBodyParser.hpp @@ -0,0 +1,62 @@ +#ifndef SRC_HTTPBODYPARSER_HPP_ +#define SRC_HTTPBODYPARSER_HPP_ + +#include +#include +#include "HTTPRequest.hpp" + +namespace httpsserver { + +/** + * Superclass for various body parser implementations that can be used to + * interpret http-specific bodies (like x-www-form-urlencoded or multipart/form-data) + * + * To allow for arbitrary body length, the interface of the body parser provides access + * to one underlying "field" at a time. A field may be a value of the urlencoded string + * or a part of a multipart message. + * + * Using next() proceeds to the next field. + */ +class HTTPBodyParser { +public: + HTTPBodyParser(HTTPRequest * req): _request(req) {}; + + /** + * Proceeds to the next field of the body + * + * If a field has not been read completely, the remaining content is discarded. + * + * Returns true iff proceeding to the next field succeeded (ie there was a next field) + */ + virtual bool nextField() = 0; + + /** Returns the name of the current field */ + virtual std::string getFieldName() = 0; + + /** + * Returns the mime type of the current field. + * + * Note: This value is set by the client. It can be altered maliciously. Do NOT rely on it + * for anything that affects the security of your device or other clients connected to it! + * + * Not every BodyParser might provide this value, usually it's set to something like text/plain then + */ + virtual std::string getFieldMimeType() = 0; + + /** Returns the total length of the field */ + virtual size_t getLength() = 0; + + /** Returns the remaining length of the field. 0 means the field has been read completely */ + virtual size_t getRemainingLength() = 0; + + /** Reads a maximum of bufferSize bytes into buffer and returns the actual amount of bytes that have been read */ + virtual size_t read(byte* buffer, size_t bufferSize) = 0; + +protected: + /** The underlying request */ + HTTPRequest * _request; +}; + +} // namespace httpserver + +#endif \ No newline at end of file diff --git a/src/HTTPMultipartBodyParser.cpp b/src/HTTPMultipartBodyParser.cpp new file mode 100644 index 0000000..7296be0 --- /dev/null +++ b/src/HTTPMultipartBodyParser.cpp @@ -0,0 +1,30 @@ +#include "HTTPMultipartBodyParser.hpp" + +namespace httpsserver { + +bool HTTPMultipartBodyParser::nextField() { + return false; +} + +std::string HTTPMultipartBodyParser::getFieldName() { + return std::string("foo"); +} + +std::string HTTPMultipartBodyParser::getFieldMimeType() { + return std::string("text/plain"); +} + +size_t HTTPMultipartBodyParser::getLength() { + return 0; +} + +size_t HTTPMultipartBodyParser::getRemainingLength() { + return 0; +} + +size_t HTTPMultipartBodyParser::read(byte* buffer, size_t bufferSize) { + return 0; +} + + +} \ No newline at end of file diff --git a/src/HTTPMultipartBodyParser.hpp b/src/HTTPMultipartBodyParser.hpp new file mode 100644 index 0000000..20e40f8 --- /dev/null +++ b/src/HTTPMultipartBodyParser.hpp @@ -0,0 +1,22 @@ +#ifndef SRC_HTTPMULTIPARTBODYPARSER_HPP_ +#define SRC_HTTPMULTIPARTBODYPARSER_HPP_ + +#include +#include "HTTPBodyParser.hpp" + +namespace httpsserver { + +class HTTPMultipartBodyParser : public HTTPBodyParser { +public: + // From HTTPBodyParser + virtual bool nextField(); + virtual std::string getFieldName(); + virtual std::string getFieldMimeType(); + virtual size_t getLength(); + virtual size_t getRemainingLength(); + virtual size_t read(byte* buffer, size_t bufferSize); +}; + +} // namespace httpserver + +#endif \ No newline at end of file diff --git a/src/HTTPURLEncodedBodyParser.cpp b/src/HTTPURLEncodedBodyParser.cpp new file mode 100644 index 0000000..9d9d65b --- /dev/null +++ b/src/HTTPURLEncodedBodyParser.cpp @@ -0,0 +1,30 @@ +#include "HTTPURLEncodedBodyParser.hpp" + +namespace httpsserver { + +bool HTTPURLEncodedBodyParser::nextField() { + return false; +} + +std::string HTTPURLEncodedBodyParser::getFieldName() { + return std::string("foo"); +} + +std::string HTTPURLEncodedBodyParser::getFieldMimeType() { + return std::string("text/plain"); +} + +size_t HTTPURLEncodedBodyParser::getLength() { + return 0; +} + +size_t HTTPURLEncodedBodyParser::getRemainingLength() { + return 0; +} + +size_t HTTPURLEncodedBodyParser::read(byte* buffer, size_t bufferSize) { + return 0; +} + + +} \ No newline at end of file diff --git a/src/HTTPURLEncodedBodyParser.hpp b/src/HTTPURLEncodedBodyParser.hpp new file mode 100644 index 0000000..5b2eaf0 --- /dev/null +++ b/src/HTTPURLEncodedBodyParser.hpp @@ -0,0 +1,22 @@ +#ifndef SRC_HTTPURLENCODEDBODYPARSER_HPP_ +#define SRC_HTTPURLENCODEDBODYPARSER_HPP_ + +#include +#include "HTTPBodyParser.hpp" + +namespace httpsserver { + +class HTTPURLEncodedBodyParser : public HTTPBodyParser { +public: + // From HTTPBodyParser + virtual bool nextField(); + virtual std::string getFieldName(); + virtual std::string getFieldMimeType(); + virtual size_t getLength(); + virtual size_t getRemainingLength(); + virtual size_t read(byte* buffer, size_t bufferSize); +}; + +} // namespace httpserver + +#endif \ No newline at end of file From a341caf0afd222fedaede2259d3f46dcbc85f8a6 Mon Sep 17 00:00:00 2001 From: Jack Jansen Date: Fri, 21 Feb 2020 15:31:16 +0100 Subject: [PATCH 2/9] 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. --- .gitignore | 5 +- examples/HTML-Forms/HTML-Forms.ino | 301 +++++++++++++++++++++------ examples/Static-Page/Static-Page.cpp | 3 + src/HTTPBodyParser.hpp | 18 +- src/HTTPMultipartBodyParser.cpp | 236 ++++++++++++++++++++- src/HTTPMultipartBodyParser.hpp | 23 +- src/HTTPRequest.cpp | 4 + src/HTTPRequest.hpp | 1 + src/HTTPURLEncodedBodyParser.cpp | 64 +++++- src/HTTPURLEncodedBodyParser.hpp | 14 +- 10 files changed, 575 insertions(+), 94 deletions(-) create mode 100644 examples/Static-Page/Static-Page.cpp diff --git a/.gitignore b/.gitignore index e62800e..3c84a1a 100644 --- a/.gitignore +++ b/.gitignore @@ -66,4 +66,7 @@ cert.h private_key.h # Do not push VS Code project files -.vscode \ No newline at end of file +.vscode + +# Ignore platformio work directory +.pio diff --git a/examples/HTML-Forms/HTML-Forms.ino b/examples/HTML-Forms/HTML-Forms.ino index ab8cf8b..7002fc1 100644 --- a/examples/HTML-Forms/HTML-Forms.ino +++ b/examples/HTML-Forms/HTML-Forms.ino @@ -27,11 +27,27 @@ // We will use wifi #include +// We will use SPIFFS and FS +#include +#include + // Includes for the server #include #include #include #include +#include +#include +#include + +// 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"}, + {".png", "image/png"}, + {".jpg", "image/jpg"}, + {"", ""} +}; // The HTTPS Server comes in a separate namespace. For easier use, include it here. using namespace httpsserver; @@ -51,13 +67,34 @@ HTTPSServer secureServer = HTTPSServer(&cert); // which are pointers to the request data (read request body, headers, ...) and // to the response data (write response, set status code, ...) void handleRoot(HTTPRequest * req, HTTPResponse * res); -void handleForm(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); +std::string htmlEncode(std::string data) { + // Quick and dirty: doesn't handle control chars and such. + 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); @@ -68,17 +105,28 @@ void setup() { 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 ResourceNode * nodeRoot = new ResourceNode("/", "GET", &handleRoot); - ResourceNode * nodeForm = new ResourceNode("/", "POST", &handleForm); + ResourceNode * nodeFormUpload = new ResourceNode("/upload", "POST", &handleFormUpload); + ResourceNode * nodeFormEdit = new ResourceNode("/edit", "GET", &handleFormEdit); + ResourceNode * nodeFormEditDone = new ResourceNode("/edit", "POST", &handleFormEdit); + ResourceNode * nodeDirectory = new ResourceNode("/public", "GET", &handleDirectory); + 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 the root nodes to the server secureServer.registerNode(nodeRoot); - secureServer.registerNode(nodeForm); + secureServer.registerNode(nodeFormUpload); + secureServer.registerNode(nodeFormEdit); + secureServer.registerNode(nodeFormEditDone); + secureServer.registerNode(nodeDirectory); + secureServer.registerNode(nodeFile); // Add the 404 not found node to the server. // The path is ignored for the default node. @@ -108,73 +156,200 @@ void handleRoot(HTTPRequest * req, HTTPResponse * res) { // you would write to Serial etc. res->println(""); res->println(""); - res->println("Hello World!"); + res->println("Very simple file server"); res->println(""); - res->println("

HTML Forms

"); - res->println("

This page contains some forms to show you how form data can be evaluated on server side."); - - // The following forms will all use the same target (/ - the server's root directory) and POST method, so - // the data will go to the request body. They differ on the value of the enctype though. - - // enctype=x-www-form-urlencoded - // means that the parameters of form elements will be encoded like they would - // be encoded if you would use GET as method and append them to the URL (just after a ? at the end of the - // resource path). Different fields are separated by an &-character. Special characters have a specific encoding - // using the %-character, so for example %20 is the representation of a space. - // The body could look like this: - // - // foo=bar&bat=baz - // - // Where foo and bat are the variables and bar and baz the values. - // - // Advantages: - // - Low overhead - // Disadvantages: - // - May be hard to read for humans - // - Cannot be used for inputs with type=file (you will only get the filename, not the content) - res->println("

"); - res->println("

enctype=x-www-form-urlencoded

"); - res->println("
") - - // enctype=multipart/form-data - // - // TODO: Explanatory text - // - // Advantages: - // - Even longer text stays somewhat human readable - // - Can be used for files and binary data - // Disadvantages: - // - Big overhead if used for some small string values - res->println("
"); - res->println("

enctype=multipart/form-data

"); - res->println("
") - + res->println("

Very simple file server

"); + res->println("

This is a very simple file server to demonstrate the use of POST forms.

"); + res->println("

List existing files

"); + res->println("

See /public to list existing files and retrieve or edit them.

"); + res->println("

Upload new file

"); + res->println("

This form allows you to upload files (text, jpg and png supported best). It demonstrates multipart/form-data.

"); + res->println("
"); + res->println("file:
"); + res->println(""); + res->println("
"); res->println(""); res->println(""); } -void handleForm(HTTPRequest * req, HTTPResponse * res) { +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. - // Note: This is only necessary if you expect various enctypes to be send to the same handler. - // If you would have only one form on your page with a fixed enctype, just instantiate the - // corresponding reader. - - // TODO: Select Parser, instantiate it - - // Now we iterate over the so-called fields of the BodyParser. This works in the same way for - // all body parsers. - // The interface is somewhat limited, so you cannot just call something like - // myParser.get("fieldname"), because this would require the parser to cache the whole request - // body. If you have your ESP32 attached to an external SD Card and you want to be able to upload - // files that are larger than the ESP's memory to that card, this would not work. - // This is why you iterate over the fields by using myParser.next() and check the name of the - // current field with myParser.getFieldName(), and then process it with a buffer. - // If you need random access to all fields' values, you need to parse them into a map or some similar - // data structure by yourself and make sure that all fits into the memory. - - // TODO: Iterate over fields + // 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"); + size_t semicolonPos = contentType.find(";"); + if (semicolonPos != std::string::npos) contentType = contentType.substr(0, semicolonPos); + if (contentType == "multipart/form-data") { + parser = new HTTPMultipartBodyParser(req); + } else { + Serial.printf("Unknown POST Content-Type: %s\n", contentType.c_str()); + return; + } + // We iterate over the fields. Any field with a filename is uploaded + res->println("File Upload

File Upload

"); + bool didwrite = false; + while(parser->nextField()) { + std::string name = parser->getFieldName(); + std::string filename = parser->getFieldFilename(); + std::string mimeType = parser->getFieldMimeType(); + 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; + } + // Should check file name validity and all that, but we skip that. + std::string pathname = "/public/" + filename; + File file = SPIFFS.open(pathname.c_str(), "w"); + size_t fileLength = 0; + didwrite = true; + while (!parser->endOfField()) { + byte buf[512]; + size_t readLength = parser->read(buf, 512); + file.write(buf, readLength); + fileLength += readLength; + } + file.close(); + res->printf("

Saved %d bytes to %s

", (int)fileLength, pathname.c_str()); + } + if (!didwrite) res->println("

Did not write any file

"); + res->println(""); + delete parser; +} + +void handleFormEdit(HTTPRequest * req, HTTPResponse * res) { + if (req->getMethod() == "GET") { + // Initial request. Get filename from request parameters and return form. + auto params = req->getParams(); + std::string filename; + bool hasFilename = params->getQueryParameter("filename", filename); + std::string pathname = std::string("/public/") + filename; + res->println("Edit File"); + File file = SPIFFS.open(pathname.c_str()); + if (!hasFilename) { + res->println("

No filename specified.

"); + } else + if (!file.available()) { + res->printf("

File not found: %s

\n", pathname.c_str()); + } else { + res->printf("

Edit content of %s

\n", pathname.c_str()); + res->println("
"); + res->printf("", filename.c_str()); + res->print("
"); + res->println(""); + res->println("
"); + } + res->println(""); + } else { + // Assume POST request. Contains submitted data. + res->println("File Edited

File Edited

"); + HTTPURLEncodedBodyParser parser(req); + std::string filename; + bool savedFile = false; + while(parser.nextField()) { + std::string name = parser.getFieldName(); + if (name == "filename") { + char buf[512]; + size_t readLength = parser.read((byte *)buf, 512); + filename = std::string("/public/") + std::string(buf, readLength); + } else + if (name == "content") { + if (filename == "") { + res->println("

Error: form contained content before filename.

"); + break; + } + 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("

Saved %d bytes to %s

", int(fieldLength), filename.c_str()); + } else { + res->printf("

Unexpected field %s

", name.c_str()); + } + } + if (!savedFile) res->println("

No file to save...

"); + res->println(""); + } +} + +void handleDirectory(HTTPRequest * req, HTTPResponse * res) { + res->println("File Listing"); + File d = SPIFFS.open("/public"); + if (!d.isDirectory()) { + res->println("

No files found.

"); + } else { + res->println("

File Listing

"); + res->println("
    "); + File f = d.openNextFile(); + while (f) { + std::string pathname(f.name()); + res->printf("
  • %s", pathname.c_str(), pathname.c_str()); + if (pathname.rfind(".txt") != std::string::npos) { + std::string filename = pathname.substr(8); // Remove /public/ + res->printf(" [edit]", filename.c_str()); + } + res->println("
  • "); + f = d.openNextFile(); + } + res->println("
"); + } + res->println(""); +} + +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) { diff --git a/examples/Static-Page/Static-Page.cpp b/examples/Static-Page/Static-Page.cpp new file mode 100644 index 0000000..8efb357 --- /dev/null +++ b/examples/Static-Page/Static-Page.cpp @@ -0,0 +1,3 @@ +#include +#include "Static-Page.ino" + diff --git a/src/HTTPBodyParser.hpp b/src/HTTPBodyParser.hpp index ba252b8..6dad5b3 100644 --- a/src/HTTPBodyParser.hpp +++ b/src/HTTPBodyParser.hpp @@ -19,7 +19,10 @@ namespace httpsserver { */ class HTTPBodyParser { public: + const size_t unknownLength = 0x7ffffffe; + HTTPBodyParser(HTTPRequest * req): _request(req) {}; + virtual ~HTTPBodyParser() {} /** * Proceeds to the next field of the body @@ -33,6 +36,9 @@ class HTTPBodyParser { /** Returns the name of the current field */ virtual std::string getFieldName() = 0; + /** Returns the filename of the current field or an empty string */ + virtual std::string getFieldFilename() = 0; + /** * Returns the mime type of the current field. * @@ -43,14 +49,14 @@ class HTTPBodyParser { */ virtual std::string getFieldMimeType() = 0; - /** Returns the total length of the field */ - virtual size_t getLength() = 0; + /** + * Reads a maximum of bufferSize bytes into buffer and returns the actual amount of bytes that have been read + */ + virtual size_t read(byte* buffer, size_t bufferSize) = 0; - /** Returns the remaining length of the field. 0 means the field has been read completely */ - virtual size_t getRemainingLength() = 0; + /** Returns true when all field data has been read */ + virtual bool endOfField() = 0; - /** Reads a maximum of bufferSize bytes into buffer and returns the actual amount of bytes that have been read */ - virtual size_t read(byte* buffer, size_t bufferSize) = 0; protected: /** The underlying request */ diff --git a/src/HTTPMultipartBodyParser.cpp b/src/HTTPMultipartBodyParser.cpp index 7296be0..30e790c 100644 --- a/src/HTTPMultipartBodyParser.cpp +++ b/src/HTTPMultipartBodyParser.cpp @@ -1,29 +1,245 @@ #include "HTTPMultipartBodyParser.hpp" +#include + +const size_t MAXLINESIZE = 256; namespace httpsserver { +HTTPMultipartBodyParser::HTTPMultipartBodyParser(HTTPRequest * req) +: HTTPBodyParser(req), + peekBuffer(NULL), + peekBufferSize(0), + boundary(""), + lastBoundary(""), + fieldName(""), + fieldMimeType(""), + fieldFilename("") +{ + auto contentType = _request->getHeader("Content-Type"); +#ifdef DEBUG_MULTIPART_PARSER + Serial.print("Content type: "); + Serial.println(contentType.c_str()); +#endif + auto boundaryIndex = contentType.find("boundary="); + if(boundaryIndex == std::string::npos) { + HTTPS_LOGE("Multipart: missing boundary="); + discardBody(); + return; + } + //TODO: remove all magic constants + boundary = contentType.substr(boundaryIndex + 9); + auto commaIndex = boundary.find(';'); + boundary = "--" + boundary.substr(0, commaIndex); + if(boundary.size() > 72) { + HTTPS_LOGE("Multipart: boundary string too long"); + discardBody(); + } + lastBoundary = boundary + "--"; +} + +HTTPMultipartBodyParser::~HTTPMultipartBodyParser() { + if (peekBuffer) { + free(peekBuffer); + peekBuffer = NULL; + } +} + +void HTTPMultipartBodyParser::discardBody() { + if (peekBuffer) free(peekBuffer); + peekBuffer = NULL; + peekBufferSize = 0; + _request->discardRequestBody(); +} + +bool HTTPMultipartBodyParser::endOfBody() { + return peekBufferSize == 0 && _request->requestComplete(); +} + +void HTTPMultipartBodyParser::fillBuffer(size_t maxLen) { + // Fill the buffer with up to maxLen bytes (total length, including + // what was already in the buffer), but stop reading ahead once + // we have a CR in the buffer (because the upper layers will + // stop consuming there anyway, to forestall overrunning + // a boundary) + char *bufPtr; + if (peekBuffer == NULL) { + // Nothing in the buffer. Allocate one of the wanted size + peekBuffer = (char *)malloc(maxLen); + bufPtr = peekBuffer; + peekBufferSize = 0; + } else if (peekBufferSize < maxLen) { + // Something in the buffer, but not enough + peekBuffer = (char *)realloc(peekBuffer, maxLen); + bufPtr = peekBuffer + peekBufferSize; + } else { + // We already have enough data in the buffer. + return; + } + while(bufPtr < peekBuffer+maxLen) { + size_t didRead = _request->readChars(bufPtr, peekBuffer+maxLen-bufPtr); + if (didRead == 0) break; + bufPtr += didRead; + // We stop buffering once we have a CR in the buffer + if (memchr(peekBuffer, '\r', bufPtr-peekBuffer) != NULL) break; + } + peekBufferSize = bufPtr - peekBuffer; + if (peekBufferSize == 0) { + HTTPS_LOGE("Multipart incomplete"); + } +} + +void HTTPMultipartBodyParser::consumedBuffer(size_t consumed) { + if (consumed == 0) return; + if (consumed == peekBufferSize) { + free(peekBuffer); + peekBuffer = NULL; + peekBufferSize = 0; + } else { + memmove(peekBuffer, peekBuffer+consumed, peekBufferSize-consumed); + peekBufferSize -= consumed; + } +} + +bool HTTPMultipartBodyParser::skipCRLF() { + if (peekBufferSize < 2) fillBuffer(2); + if (peekBufferSize < 2) return false; + if (peekBuffer[0] != '\r') return false; + if (peekBuffer[1] != '\n') { + HTTPS_LOGE("Multipart incorrect line terminator"); + discardBody(); + return false; + } + consumedBuffer(2); + return true; +} + +std::string HTTPMultipartBodyParser::readLine() { + fillBuffer(MAXLINESIZE); + if (peekBufferSize == 0) return ""; + char *crPtr = (char *)memchr(peekBuffer, '\r', peekBufferSize); + if (crPtr == NULL) { + HTTPS_LOGE("Multipart line too long"); + discardBody(); + return ""; + } + size_t lineLength = crPtr-peekBuffer; + std::string rv(peekBuffer, lineLength); + consumedBuffer(lineLength); + skipCRLF(); + return rv; +} + +// Returns true if the buffer contains a boundary (or possibly lastBoundary) +bool HTTPMultipartBodyParser::peekBoundary() { + if (peekBuffer == NULL || peekBufferSize < boundary.size()) return false; + char *ptr = peekBuffer; + if (*ptr == '\r') ptr++; + if (*ptr == '\n') ptr++; + return memcmp(ptr, boundary.c_str(), boundary.size()) == 0; +} + bool HTTPMultipartBodyParser::nextField() { - return false; + fillBuffer(MAXLINESIZE); + while(!peekBoundary()) { + std::string dummy = readLine(); + if (endOfBody()) { + HTTPS_LOGE("Multipart missing last boundary"); + return false; + } + fillBuffer(MAXLINESIZE); + } + skipCRLF(); + std::string line = readLine(); + if (line == lastBoundary) { + discardBody(); + return false; + } + if (line != boundary) { + HTTPS_LOGE("Multipart incorrect boundary"); + return false; + } + // Read header lines up to and including blank line + fieldName = ""; + fieldMimeType = "text/plain"; + fieldFilename = ""; + while (true) { + line = readLine(); + if (line == "") break; + if (line.substr(0, 14) == "Content-Type: ") { + fieldMimeType = line.substr(14); + } + if (line.substr(0, 31) == "Content-Disposition: form-data;") { + // Parse name=value; or name="value"; fields. + std::string field; + line = line.substr(31); + while(true) { + size_t pos = line.find_first_not_of(' '); + if (pos != std::string::npos) { + line = line.substr(pos); + } + if (line == "") break; + pos = line.find(';'); + if (pos == std::string::npos) { + field = line; + line = ""; + } else { + field = line.substr(0, pos); + line = line.substr(pos+1); + } + pos = field.find('='); + if (pos == std::string::npos) { + HTTPS_LOGE("Multipart ill-formed form-data header"); + return false; + } + std::string headerName = field.substr(0, pos); + std::string headerValue = field.substr(pos+1); + if (headerValue.substr(0,1) == "\"") headerValue = headerValue.substr(1, headerValue.size()-2); + if (headerName == "name") { + fieldName = headerValue; + } + if (headerName == "filename") { + fieldFilename = headerValue; + } + } + } + } + if (fieldName == "") { + HTTPS_LOGE("Multipart missing name"); + return false; + } + return true; } std::string HTTPMultipartBodyParser::getFieldName() { - return std::string("foo"); + return fieldName; } -std::string HTTPMultipartBodyParser::getFieldMimeType() { - return std::string("text/plain"); +std::string HTTPMultipartBodyParser::getFieldFilename() { + return fieldFilename; } -size_t HTTPMultipartBodyParser::getLength() { - return 0; +std::string HTTPMultipartBodyParser::getFieldMimeType() { + return fieldMimeType; } - -size_t HTTPMultipartBodyParser::getRemainingLength() { - return 0; +bool HTTPMultipartBodyParser::endOfField() { + return peekBoundary(); } size_t HTTPMultipartBodyParser::read(byte* buffer, size_t bufferSize) { - return 0; + if (peekBoundary()) return 0; + size_t readSize = std::min(bufferSize, MAXLINESIZE); + fillBuffer(readSize); + if (peekBoundary()) return 0; + // We read at most up to a CR (so we don't miss a boundary that has been partially buffered) + // but we always read at least one byte so if the first byte in the buffer is a CR we do read it. + if (peekBufferSize > 1) { + char *crPtr = (char *)memchr(peekBuffer+1, '\r', peekBufferSize-1); + if (crPtr != NULL && crPtr - peekBuffer < bufferSize) bufferSize = crPtr - peekBuffer; + } + size_t copySize = std::min(bufferSize, peekBufferSize); + memcpy(buffer, peekBuffer, copySize); + consumedBuffer(copySize); + return copySize; } diff --git a/src/HTTPMultipartBodyParser.hpp b/src/HTTPMultipartBodyParser.hpp index 20e40f8..c106fc2 100644 --- a/src/HTTPMultipartBodyParser.hpp +++ b/src/HTTPMultipartBodyParser.hpp @@ -8,13 +8,30 @@ namespace httpsserver { class HTTPMultipartBodyParser : public HTTPBodyParser { public: - // From HTTPBodyParser + HTTPMultipartBodyParser(HTTPRequest * req); + ~HTTPMultipartBodyParser(); virtual bool nextField(); virtual std::string getFieldName(); + virtual std::string getFieldFilename(); virtual std::string getFieldMimeType(); - virtual size_t getLength(); - virtual size_t getRemainingLength(); + virtual bool endOfField(); virtual size_t read(byte* buffer, size_t bufferSize); +private: + std::string readLine(); + void fillBuffer(size_t maxLen); + void consumedBuffer(size_t consumed); + bool skipCRLF(); + bool peekBoundary(); + void discardBody(); + bool endOfBody(); + char *peekBuffer; + size_t peekBufferSize; + + std::string boundary; + std::string lastBoundary; + std::string fieldName; + std::string fieldMimeType; + std::string fieldFilename; }; } // namespace httpserver diff --git a/src/HTTPRequest.cpp b/src/HTTPRequest.cpp index 1b4fd6f..2a83a09 100644 --- a/src/HTTPRequest.cpp +++ b/src/HTTPRequest.cpp @@ -36,6 +36,10 @@ ResourceParameters * HTTPRequest::getParams() { return _params; } +HTTPHeaders * HTTPRequest::getHTTPHeaders() { + return _headers; +} + std::string HTTPRequest::getHeader(std::string const &name) { HTTPHeader * h = _headers->get(name); if (h != NULL) { diff --git a/src/HTTPRequest.hpp b/src/HTTPRequest.hpp index 5351607..7184180 100644 --- a/src/HTTPRequest.hpp +++ b/src/HTTPRequest.hpp @@ -38,6 +38,7 @@ class HTTPRequest { bool requestComplete(); void discardRequestBody(); ResourceParameters * getParams(); + HTTPHeaders *getHTTPHeaders(); std::string getBasicAuthUser(); std::string getBasicAuthPassword(); bool isSecure(); diff --git a/src/HTTPURLEncodedBodyParser.cpp b/src/HTTPURLEncodedBodyParser.cpp index 9d9d65b..48f5cc9 100644 --- a/src/HTTPURLEncodedBodyParser.cpp +++ b/src/HTTPURLEncodedBodyParser.cpp @@ -1,29 +1,75 @@ #include "HTTPURLEncodedBodyParser.hpp" namespace httpsserver { +HTTPURLEncodedBodyParser::HTTPURLEncodedBodyParser(HTTPRequest * req) +: HTTPBodyParser(req), + bodyBuffer(NULL), + bodyPtr(NULL), + bodyLength(0), + fieldBuffer(""), + fieldPtr(NULL), + fieldRemainingLength(0) +{ + bodyLength = _request->getContentLength(); + if (bodyLength) { + bodyBuffer = new char[bodyLength+1]; + _request->readChars(bodyBuffer, bodyLength); + bodyPtr = bodyBuffer; + bodyBuffer[bodyLength] = '\0'; + } +} + +HTTPURLEncodedBodyParser::~HTTPURLEncodedBodyParser() { + if (bodyBuffer) delete[] bodyBuffer; + bodyBuffer = NULL; +} bool HTTPURLEncodedBodyParser::nextField() { - return false; + fieldBuffer = ""; + fieldPtr = NULL; + fieldRemainingLength = 0; + + char *equalPtr = index(bodyPtr, '='); + if (equalPtr == NULL) return false; + fieldName = std::string(bodyPtr, equalPtr-bodyPtr); + + char *valuePtr = equalPtr + 1; + char *endPtr = index(valuePtr, '&'); + if (endPtr == NULL) { + endPtr = equalPtr + strlen(equalPtr); + bodyPtr = endPtr; + } else { + bodyPtr = endPtr+1; + } + fieldBuffer = std::string(valuePtr, endPtr - valuePtr); + fieldBuffer = urlDecode(fieldBuffer); + fieldRemainingLength = fieldBuffer.size(); + fieldPtr = fieldBuffer.c_str(); + return true; } std::string HTTPURLEncodedBodyParser::getFieldName() { - return std::string("foo"); + return fieldName; } -std::string HTTPURLEncodedBodyParser::getFieldMimeType() { - return std::string("text/plain"); +std::string HTTPURLEncodedBodyParser::getFieldFilename() { + return ""; } -size_t HTTPURLEncodedBodyParser::getLength() { - return 0; +std::string HTTPURLEncodedBodyParser::getFieldMimeType() { + return std::string("text/plain"); } -size_t HTTPURLEncodedBodyParser::getRemainingLength() { - return 0; +bool HTTPURLEncodedBodyParser::endOfField() { + return fieldRemainingLength <= 0; } size_t HTTPURLEncodedBodyParser::read(byte* buffer, size_t bufferSize) { - return 0; + if (bufferSize > fieldRemainingLength) bufferSize = fieldRemainingLength; + memcpy(buffer, fieldPtr, bufferSize); + fieldRemainingLength -= bufferSize; + fieldPtr += bufferSize; + return bufferSize; } diff --git a/src/HTTPURLEncodedBodyParser.hpp b/src/HTTPURLEncodedBodyParser.hpp index 5b2eaf0..f8529a7 100644 --- a/src/HTTPURLEncodedBodyParser.hpp +++ b/src/HTTPURLEncodedBodyParser.hpp @@ -9,12 +9,22 @@ namespace httpsserver { class HTTPURLEncodedBodyParser : public HTTPBodyParser { public: // From HTTPBodyParser + HTTPURLEncodedBodyParser(HTTPRequest * req); + ~HTTPURLEncodedBodyParser(); virtual bool nextField(); virtual std::string getFieldName(); + virtual std::string getFieldFilename(); virtual std::string getFieldMimeType(); - virtual size_t getLength(); - virtual size_t getRemainingLength(); + virtual bool endOfField(); virtual size_t read(byte* buffer, size_t bufferSize); +protected: + char *bodyBuffer; + char *bodyPtr; + size_t bodyLength; + std::string fieldName; + std::string fieldBuffer; + const char *fieldPtr; + size_t fieldRemainingLength; }; } // namespace httpserver From e79c3be24aa86a73afca08acbe55544305376da2 Mon Sep 17 00:00:00 2001 From: Frank Hessel Date: Sat, 29 Feb 2020 00:01:51 +0100 Subject: [PATCH 3/9] adjust coding style --- examples/HTML-Forms/HTML-Forms.ino | 346 +++++++++++++++-------------- src/HTTPMultipartBodyParser.cpp | 72 ++++-- src/HTTPURLEncodedBodyParser.cpp | 20 +- 3 files changed, 240 insertions(+), 198 deletions(-) diff --git a/examples/HTML-Forms/HTML-Forms.ino b/examples/HTML-Forms/HTML-Forms.ino index 7002fc1..d1460ed 100644 --- a/examples/HTML-Forms/HTML-Forms.ino +++ b/examples/HTML-Forms/HTML-Forms.ino @@ -117,7 +117,7 @@ void setup() { ResourceNode * nodeDirectory = new ResourceNode("/public", "GET", &handleDirectory); 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 + // 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 the root nodes to the server @@ -135,159 +135,163 @@ void setup() { Serial.println("Starting server..."); secureServer.start(); if (secureServer.isRunning()) { - Serial.println("Server ready."); + Serial.println("Server ready."); } } void loop() { - // This call will let the server do its work - secureServer.loop(); + // This call will let the server do its work + secureServer.loop(); - // Other code would go here... - delay(1); + // 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"); - - // The response implements the Print interface, so you can use it just like - // you would write to Serial etc. - res->println(""); - res->println(""); - res->println("Very simple file server"); - res->println(""); - res->println("

Very simple file server

"); - res->println("

This is a very simple file server to demonstrate the use of POST forms.

"); - res->println("

List existing files

"); - res->println("

See /public to list existing files and retrieve or edit them.

"); - res->println("

Upload new file

"); - res->println("

This form allows you to upload files (text, jpg and png supported best). It demonstrates multipart/form-data.

"); - res->println("
"); - res->println("file:
"); - res->println(""); - res->println("
"); - res->println(""); - res->println(""); + // 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"); + + // The response implements the Print interface, so you can use it just like + // you would write to Serial etc. + res->println(""); + res->println(""); + res->println("Very simple file server"); + res->println(""); + res->println("

Very simple file server

"); + res->println("

This is a very simple file server to demonstrate the use of POST forms.

"); + res->println("

List existing files

"); + res->println("

See /public to list existing files and retrieve or edit them.

"); + res->println("

Upload new file

"); + res->println("

This form allows you to upload files (text, jpg and png supported best). It demonstrates multipart/form-data.

"); + res->println("
"); + res->println("file:
"); + res->println(""); + res->println("
"); + res->println(""); + res->println(""); } 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"); - size_t semicolonPos = contentType.find(";"); - if (semicolonPos != std::string::npos) contentType = contentType.substr(0, semicolonPos); - if (contentType == "multipart/form-data") { - parser = new HTTPMultipartBodyParser(req); - } else { - Serial.printf("Unknown POST Content-Type: %s\n", contentType.c_str()); - return; - } - // We iterate over the fields. Any field with a filename is uploaded - res->println("File Upload

File Upload

"); - bool didwrite = false; - while(parser->nextField()) { - std::string name = parser->getFieldName(); - std::string filename = parser->getFieldFilename(); - std::string mimeType = parser->getFieldMimeType(); - 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; - } - // Should check file name validity and all that, but we skip that. - std::string pathname = "/public/" + filename; - File file = SPIFFS.open(pathname.c_str(), "w"); - size_t fileLength = 0; - didwrite = true; - while (!parser->endOfField()) { - byte buf[512]; - size_t readLength = parser->read(buf, 512); - file.write(buf, readLength); - fileLength += readLength; - } - file.close(); - res->printf("

Saved %d bytes to %s

", (int)fileLength, pathname.c_str()); - } - if (!didwrite) res->println("

Did not write any file

"); - res->println(""); - delete parser; + // 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"); + size_t semicolonPos = contentType.find(";"); + if (semicolonPos != std::string::npos) { + contentType = contentType.substr(0, semicolonPos); + } + if (contentType == "multipart/form-data") { + parser = new HTTPMultipartBodyParser(req); + } else { + Serial.printf("Unknown POST Content-Type: %s\n", contentType.c_str()); + return; + } + // We iterate over the fields. Any field with a filename is uploaded + res->println("File Upload

File Upload

"); + bool didwrite = false; + while(parser->nextField()) { + std::string name = parser->getFieldName(); + std::string filename = parser->getFieldFilename(); + std::string mimeType = parser->getFieldMimeType(); + 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; + } + // Should check file name validity and all that, but we skip that. + std::string pathname = "/public/" + filename; + File file = SPIFFS.open(pathname.c_str(), "w"); + size_t fileLength = 0; + didwrite = true; + while (!parser->endOfField()) { + byte buf[512]; + size_t readLength = parser->read(buf, 512); + file.write(buf, readLength); + fileLength += readLength; + } + file.close(); + res->printf("

Saved %d bytes to %s

", (int)fileLength, pathname.c_str()); + } + if (!didwrite) { + res->println("

Did not write any file

"); + } + res->println(""); + delete parser; } void handleFormEdit(HTTPRequest * req, HTTPResponse * res) { if (req->getMethod() == "GET") { - // Initial request. Get filename from request parameters and return form. - auto params = req->getParams(); - std::string filename; - bool hasFilename = params->getQueryParameter("filename", filename); - std::string pathname = std::string("/public/") + filename; - res->println("Edit File"); - File file = SPIFFS.open(pathname.c_str()); - if (!hasFilename) { - res->println("

No filename specified.

"); - } else - if (!file.available()) { - res->printf("

File not found: %s

\n", pathname.c_str()); - } else { - res->printf("

Edit content of %s

\n", pathname.c_str()); - res->println("
"); - res->printf("", filename.c_str()); - res->print("
"); - res->println(""); - res->println("
"); - } - res->println(""); - } else { - // Assume POST request. Contains submitted data. - res->println("File Edited

File Edited

"); - HTTPURLEncodedBodyParser parser(req); - std::string filename; - bool savedFile = false; - while(parser.nextField()) { - std::string name = parser.getFieldName(); - if (name == "filename") { - char buf[512]; - size_t readLength = parser.read((byte *)buf, 512); - filename = std::string("/public/") + std::string(buf, readLength); - } else - if (name == "content") { - if (filename == "") { - res->println("

Error: form contained content before filename.

"); - break; - } - 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("

Saved %d bytes to %s

", int(fieldLength), filename.c_str()); - } else { - res->printf("

Unexpected field %s

", name.c_str()); - } - } - if (!savedFile) res->println("

No file to save...

"); - res->println(""); + // Initial request. Get filename from request parameters and return form. + auto params = req->getParams(); + std::string filename; + bool hasFilename = params->getQueryParameter("filename", filename); + std::string pathname = std::string("/public/") + filename; + res->println("Edit File"); + File file = SPIFFS.open(pathname.c_str()); + if (!hasFilename) { + res->println("

No filename specified.

"); + } else if (!file.available()) { + res->printf("

File not found: %s

\n", pathname.c_str()); + } else { + res->printf("

Edit content of %s

\n", pathname.c_str()); + res->println("
"); + res->printf("", filename.c_str()); + res->print("
"); + res->println(""); + res->println("
"); + } + res->println(""); + } else { // method != GET + // Assume POST request. Contains submitted data. + res->println("File Edited

File Edited

"); + HTTPURLEncodedBodyParser parser(req); + std::string filename; + bool savedFile = false; + while(parser.nextField()) { + std::string name = parser.getFieldName(); + if (name == "filename") { + char buf[512]; + size_t readLength = parser.read((byte *)buf, 512); + filename = std::string("/public/") + std::string(buf, readLength); + } else if (name == "content") { + if (filename == "") { + res->println("

Error: form contained content before filename.

"); + break; + } + 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("

Saved %d bytes to %s

", int(fieldLength), filename.c_str()); + } else { + res->printf("

Unexpected field %s

", name.c_str()); + } + } + if (!savedFile) { + res->println("

No file to save...

"); + } + res->println(""); } } @@ -295,22 +299,22 @@ void handleDirectory(HTTPRequest * req, HTTPResponse * res) { res->println("File Listing"); File d = SPIFFS.open("/public"); if (!d.isDirectory()) { - res->println("

No files found.

"); + res->println("

No files found.

"); } else { - res->println("

File Listing

"); - res->println("
    "); - File f = d.openNextFile(); - while (f) { - std::string pathname(f.name()); - res->printf("
  • %s", pathname.c_str(), pathname.c_str()); - if (pathname.rfind(".txt") != std::string::npos) { - std::string filename = pathname.substr(8); // Remove /public/ - res->printf(" [edit]", filename.c_str()); - } - res->println("
  • "); - f = d.openNextFile(); - } - res->println("
"); + res->println("

File Listing

"); + res->println("
    "); + File f = d.openNextFile(); + while (f) { + std::string pathname(f.name()); + res->printf("
  • %s", pathname.c_str(), pathname.c_str()); + if (pathname.rfind(".txt") != std::string::npos) { + std::string filename = pathname.substr(8); // Remove /public/ + res->printf(" [edit]", filename.c_str()); + } + res->println("
  • "); + f = d.openNextFile(); + } + res->println("
"); } res->println(""); } @@ -353,21 +357,21 @@ void handleFile(HTTPRequest * req, HTTPResponse * res) { } 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(""); - res->println(""); - res->println("Not Found"); - res->println("

404 Not Found

The requested resource was not found on this server.

"); - res->println(""); + // 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(""); + res->println(""); + res->println("Not Found"); + res->println("

404 Not Found

The requested resource was not found on this server.

"); + res->println(""); } diff --git a/src/HTTPMultipartBodyParser.cpp b/src/HTTPMultipartBodyParser.cpp index 30e790c..c267447 100644 --- a/src/HTTPMultipartBodyParser.cpp +++ b/src/HTTPMultipartBodyParser.cpp @@ -5,8 +5,8 @@ const size_t MAXLINESIZE = 256; namespace httpsserver { -HTTPMultipartBodyParser::HTTPMultipartBodyParser(HTTPRequest * req) -: HTTPBodyParser(req), +HTTPMultipartBodyParser::HTTPMultipartBodyParser(HTTPRequest * req): + HTTPBodyParser(req), peekBuffer(NULL), peekBufferSize(0), boundary(""), @@ -45,7 +45,9 @@ HTTPMultipartBodyParser::~HTTPMultipartBodyParser() { } void HTTPMultipartBodyParser::discardBody() { - if (peekBuffer) free(peekBuffer); + if (peekBuffer) { + free(peekBuffer); + } peekBuffer = NULL; peekBufferSize = 0; _request->discardRequestBody(); @@ -77,10 +79,14 @@ void HTTPMultipartBodyParser::fillBuffer(size_t maxLen) { } while(bufPtr < peekBuffer+maxLen) { size_t didRead = _request->readChars(bufPtr, peekBuffer+maxLen-bufPtr); - if (didRead == 0) break; + if (didRead == 0) { + break; + } bufPtr += didRead; // We stop buffering once we have a CR in the buffer - if (memchr(peekBuffer, '\r', bufPtr-peekBuffer) != NULL) break; + if (memchr(peekBuffer, '\r', bufPtr-peekBuffer) != NULL) { + break; + } } peekBufferSize = bufPtr - peekBuffer; if (peekBufferSize == 0) { @@ -89,7 +95,9 @@ void HTTPMultipartBodyParser::fillBuffer(size_t maxLen) { } void HTTPMultipartBodyParser::consumedBuffer(size_t consumed) { - if (consumed == 0) return; + if (consumed == 0) { + return; + } if (consumed == peekBufferSize) { free(peekBuffer); peekBuffer = NULL; @@ -101,9 +109,15 @@ void HTTPMultipartBodyParser::consumedBuffer(size_t consumed) { } bool HTTPMultipartBodyParser::skipCRLF() { - if (peekBufferSize < 2) fillBuffer(2); - if (peekBufferSize < 2) return false; - if (peekBuffer[0] != '\r') return false; + if (peekBufferSize < 2) { + fillBuffer(2); + } + if (peekBufferSize < 2) { + return false; + } + if (peekBuffer[0] != '\r') { + return false; + } if (peekBuffer[1] != '\n') { HTTPS_LOGE("Multipart incorrect line terminator"); discardBody(); @@ -115,7 +129,9 @@ bool HTTPMultipartBodyParser::skipCRLF() { std::string HTTPMultipartBodyParser::readLine() { fillBuffer(MAXLINESIZE); - if (peekBufferSize == 0) return ""; + if (peekBufferSize == 0) { + return ""; + } char *crPtr = (char *)memchr(peekBuffer, '\r', peekBufferSize); if (crPtr == NULL) { HTTPS_LOGE("Multipart line too long"); @@ -131,10 +147,16 @@ std::string HTTPMultipartBodyParser::readLine() { // Returns true if the buffer contains a boundary (or possibly lastBoundary) bool HTTPMultipartBodyParser::peekBoundary() { - if (peekBuffer == NULL || peekBufferSize < boundary.size()) return false; + if (peekBuffer == NULL || peekBufferSize < boundary.size()) { + return false; + } char *ptr = peekBuffer; - if (*ptr == '\r') ptr++; - if (*ptr == '\n') ptr++; + if (*ptr == '\r') { + ptr++; + } + if (*ptr == '\n') { + ptr++; + } return memcmp(ptr, boundary.c_str(), boundary.size()) == 0; } @@ -164,7 +186,9 @@ bool HTTPMultipartBodyParser::nextField() { fieldFilename = ""; while (true) { line = readLine(); - if (line == "") break; + if (line == "") { + break; + } if (line.substr(0, 14) == "Content-Type: ") { fieldMimeType = line.substr(14); } @@ -193,7 +217,9 @@ bool HTTPMultipartBodyParser::nextField() { } std::string headerName = field.substr(0, pos); std::string headerValue = field.substr(pos+1); - if (headerValue.substr(0,1) == "\"") headerValue = headerValue.substr(1, headerValue.size()-2); + if (headerValue.substr(0,1) == "\"") { + headerValue = headerValue.substr(1, headerValue.size()-2); + } if (headerName == "name") { fieldName = headerValue; } @@ -221,20 +247,27 @@ std::string HTTPMultipartBodyParser::getFieldFilename() { std::string HTTPMultipartBodyParser::getFieldMimeType() { return fieldMimeType; } + bool HTTPMultipartBodyParser::endOfField() { return peekBoundary(); } size_t HTTPMultipartBodyParser::read(byte* buffer, size_t bufferSize) { - if (peekBoundary()) return 0; + if (peekBoundary()) { + return 0; + } size_t readSize = std::min(bufferSize, MAXLINESIZE); fillBuffer(readSize); - if (peekBoundary()) return 0; + if (peekBoundary()) { + return 0; + } // We read at most up to a CR (so we don't miss a boundary that has been partially buffered) // but we always read at least one byte so if the first byte in the buffer is a CR we do read it. if (peekBufferSize > 1) { char *crPtr = (char *)memchr(peekBuffer+1, '\r', peekBufferSize-1); - if (crPtr != NULL && crPtr - peekBuffer < bufferSize) bufferSize = crPtr - peekBuffer; + if (crPtr != NULL && crPtr - peekBuffer < bufferSize) { + bufferSize = crPtr - peekBuffer; + } } size_t copySize = std::min(bufferSize, peekBufferSize); memcpy(buffer, peekBuffer, copySize); @@ -242,5 +275,4 @@ size_t HTTPMultipartBodyParser::read(byte* buffer, size_t bufferSize) { return copySize; } - -} \ No newline at end of file +} /* namespace httpsserver */ diff --git a/src/HTTPURLEncodedBodyParser.cpp b/src/HTTPURLEncodedBodyParser.cpp index 48f5cc9..9e507a2 100644 --- a/src/HTTPURLEncodedBodyParser.cpp +++ b/src/HTTPURLEncodedBodyParser.cpp @@ -1,8 +1,9 @@ #include "HTTPURLEncodedBodyParser.hpp" namespace httpsserver { -HTTPURLEncodedBodyParser::HTTPURLEncodedBodyParser(HTTPRequest * req) -: HTTPBodyParser(req), + +HTTPURLEncodedBodyParser::HTTPURLEncodedBodyParser(HTTPRequest * req): + HTTPBodyParser(req), bodyBuffer(NULL), bodyPtr(NULL), bodyLength(0), @@ -20,7 +21,9 @@ HTTPURLEncodedBodyParser::HTTPURLEncodedBodyParser(HTTPRequest * req) } HTTPURLEncodedBodyParser::~HTTPURLEncodedBodyParser() { - if (bodyBuffer) delete[] bodyBuffer; + if (bodyBuffer) { + delete[] bodyBuffer; + } bodyBuffer = NULL; } @@ -30,7 +33,9 @@ bool HTTPURLEncodedBodyParser::nextField() { fieldRemainingLength = 0; char *equalPtr = index(bodyPtr, '='); - if (equalPtr == NULL) return false; + if (equalPtr == NULL) { + return false; + } fieldName = std::string(bodyPtr, equalPtr-bodyPtr); char *valuePtr = equalPtr + 1; @@ -65,12 +70,13 @@ bool HTTPURLEncodedBodyParser::endOfField() { } size_t HTTPURLEncodedBodyParser::read(byte* buffer, size_t bufferSize) { - if (bufferSize > fieldRemainingLength) bufferSize = fieldRemainingLength; + if (bufferSize > fieldRemainingLength) { + bufferSize = fieldRemainingLength; + } memcpy(buffer, fieldPtr, bufferSize); fieldRemainingLength -= bufferSize; fieldPtr += bufferSize; return bufferSize; } - -} \ No newline at end of file +} /* namespace httpsserver */ From b1dcfd47eb15c48c34163472d09bb46716ba8f94 Mon Sep 17 00:00:00 2001 From: Jack Jansen Date: Mon, 9 Mar 2020 21:18:46 +0100 Subject: [PATCH 4/9] Read the whole body. Also cater for not knowing the ContentLength. --- src/HTTPURLEncodedBodyParser.cpp | 51 +++++++++++++++++++++++++++++--- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/src/HTTPURLEncodedBodyParser.cpp b/src/HTTPURLEncodedBodyParser.cpp index 9e507a2..71d58e7 100644 --- a/src/HTTPURLEncodedBodyParser.cpp +++ b/src/HTTPURLEncodedBodyParser.cpp @@ -1,5 +1,8 @@ #include "HTTPURLEncodedBodyParser.hpp" +#define CHUNKSIZE 512 +#define MINCHUNKSIZE 64 + namespace httpsserver { HTTPURLEncodedBodyParser::HTTPURLEncodedBodyParser(HTTPRequest * req): @@ -13,16 +16,56 @@ HTTPURLEncodedBodyParser::HTTPURLEncodedBodyParser(HTTPRequest * req): { bodyLength = _request->getContentLength(); if (bodyLength) { - bodyBuffer = new char[bodyLength+1]; - _request->readChars(bodyBuffer, bodyLength); + // We know the body length. We try to read that much and give an error if it fails. + bodyBuffer = (char *)malloc(bodyLength+1); + if (bodyBuffer == NULL) { + HTTPS_LOGE("HTTPURLEncodedBodyParser: out of memory"); + return; + } + bodyPtr = bodyBuffer; + size_t toRead = bodyLength; + while(toRead > 0) { + size_t didRead = _request->readChars(bodyPtr, toRead); + if (didRead == 0) { + HTTPS_LOGE("HTTPURLEncodedBodyParser: short read"); + bodyLength = bodyPtr - bodyBuffer; + break; + } + bodyPtr += didRead; + toRead -= didRead; + } + } else { + // We don't know the length. Read as much as possible. + bodyBuffer = (char *)malloc(CHUNKSIZE+1); + if (bodyBuffer == NULL) { + HTTPS_LOGE("HTTPURLEncodedBodyParser: out of memory"); + return; + } bodyPtr = bodyBuffer; - bodyBuffer[bodyLength] = '\0'; + size_t bufferUsed = 0; + size_t bufferAvailable = CHUNKSIZE; + while(!_request->requestComplete()) { + if (bufferAvailable < MINCHUNKSIZE) { + bodyBuffer = (char *)realloc(bodyBuffer, bufferUsed + CHUNKSIZE+1); + if (bodyBuffer == NULL) { + HTTPS_LOGE("HTTPURLEncodedBodyParser: out of memory"); + return; + } + bufferAvailable = CHUNKSIZE; + } + size_t didRead = _request->readChars(bodyBuffer+bufferUsed, bufferAvailable); + bufferUsed += didRead; + bufferAvailable -= didRead; + } + bodyLength = bufferUsed; } + bodyPtr = bodyBuffer; + bodyBuffer[bodyLength] = '\0'; } HTTPURLEncodedBodyParser::~HTTPURLEncodedBodyParser() { if (bodyBuffer) { - delete[] bodyBuffer; + free(bodyBuffer); } bodyBuffer = NULL; } From 250decda41707db5a9ee6ae5ad8ca09debdb174b Mon Sep 17 00:00:00 2001 From: Jack Jansen Date: Mon, 23 Mar 2020 21:50:27 +0100 Subject: [PATCH 5/9] Update src/HTTPMultipartBodyParser.cpp Co-Authored-By: Frank Hessel --- src/HTTPMultipartBodyParser.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/HTTPMultipartBodyParser.cpp b/src/HTTPMultipartBodyParser.cpp index c267447..15eeda1 100644 --- a/src/HTTPMultipartBodyParser.cpp +++ b/src/HTTPMultipartBodyParser.cpp @@ -26,8 +26,7 @@ HTTPMultipartBodyParser::HTTPMultipartBodyParser(HTTPRequest * req): discardBody(); return; } - //TODO: remove all magic constants - boundary = contentType.substr(boundaryIndex + 9); + boundary = contentType.substr(boundaryIndex + 9); // "boundary=" auto commaIndex = boundary.find(';'); boundary = "--" + boundary.substr(0, commaIndex); if(boundary.size() > 72) { From 0c392ed22a2cc2ff75e659eba198bbb5631824f1 Mon Sep 17 00:00:00 2001 From: Jack Jansen Date: Mon, 23 Mar 2020 21:58:35 +0100 Subject: [PATCH 6/9] Update src/HTTPURLEncodedBodyParser.cpp Co-Authored-By: Frank Hessel --- src/HTTPURLEncodedBodyParser.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/HTTPURLEncodedBodyParser.cpp b/src/HTTPURLEncodedBodyParser.cpp index 71d58e7..d1ab73a 100644 --- a/src/HTTPURLEncodedBodyParser.cpp +++ b/src/HTTPURLEncodedBodyParser.cpp @@ -46,11 +46,14 @@ HTTPURLEncodedBodyParser::HTTPURLEncodedBodyParser(HTTPRequest * req): size_t bufferAvailable = CHUNKSIZE; while(!_request->requestComplete()) { if (bufferAvailable < MINCHUNKSIZE) { - bodyBuffer = (char *)realloc(bodyBuffer, bufferUsed + CHUNKSIZE+1); - if (bodyBuffer == NULL) { + char *pBuf = (char *)realloc(bodyBuffer, bufferUsed + CHUNKSIZE+1); + if (pBuf == NULL) { HTTPS_LOGE("HTTPURLEncodedBodyParser: out of memory"); + free(bodyBuffer); + bodyBuffer = NULL; return; } + bodyBuffer = pBuf; bufferAvailable = CHUNKSIZE; } size_t didRead = _request->readChars(bodyBuffer+bufferUsed, bufferAvailable); From bf49c777760c6ba7c94f323dd4bd43f15148dd2b Mon Sep 17 00:00:00 2001 From: Jack Jansen Date: Mon, 23 Mar 2020 22:10:25 +0100 Subject: [PATCH 7/9] Removed outdated file --- examples/Static-Page/Static-Page.cpp | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 examples/Static-Page/Static-Page.cpp diff --git a/examples/Static-Page/Static-Page.cpp b/examples/Static-Page/Static-Page.cpp deleted file mode 100644 index 8efb357..0000000 --- a/examples/Static-Page/Static-Page.cpp +++ /dev/null @@ -1,3 +0,0 @@ -#include -#include "Static-Page.ino" - From 9a26346aef6f5382e3a1bc0827374f7fca1fcfdd Mon Sep 17 00:00:00 2001 From: Jack Jansen Date: Tue, 24 Mar 2020 01:03:40 +0100 Subject: [PATCH 8/9] Check for 2 more out-of-memory conditions --- src/HTTPMultipartBodyParser.cpp | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/HTTPMultipartBodyParser.cpp b/src/HTTPMultipartBodyParser.cpp index 15eeda1..2211419 100644 --- a/src/HTTPMultipartBodyParser.cpp +++ b/src/HTTPMultipartBodyParser.cpp @@ -66,11 +66,22 @@ void HTTPMultipartBodyParser::fillBuffer(size_t maxLen) { if (peekBuffer == NULL) { // Nothing in the buffer. Allocate one of the wanted size peekBuffer = (char *)malloc(maxLen); + if (peekBuffer == NULL) { + HTTPS_LOGE("Multipart: out of memory"); + discardBody(); + return; + } bufPtr = peekBuffer; peekBufferSize = 0; } else if (peekBufferSize < maxLen) { // Something in the buffer, but not enough - peekBuffer = (char *)realloc(peekBuffer, maxLen); + char *newPeekBuffer = (char *)realloc(peekBuffer, maxLen); + if (newPeekBuffer == NULL) { + HTTPS_LOGE("Multipart: out of memory"); + discardBody(); + return; + } + peekBuffer = newPeekBuffer; bufPtr = peekBuffer + peekBufferSize; } else { // We already have enough data in the buffer. From 46819806fec86ea23c3937c0aad0b03d4e33c3ad Mon Sep 17 00:00:00 2001 From: Jack Jansen Date: Tue, 24 Mar 2020 01:07:10 +0100 Subject: [PATCH 9/9] Build HTML-Forms example during ci --- .github/workflows/build-examples.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build-examples.yml b/.github/workflows/build-examples.yml index 92fb7ca..6728131 100644 --- a/.github/workflows/build-examples.yml +++ b/.github/workflows/build-examples.yml @@ -14,6 +14,7 @@ jobs: example: - Async-Server - Authentication + - HTML-Forms - HTTPS-and-HTTP - Middleware - Parameters