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 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 new file mode 100644 index 0000000..d1460ed --- /dev/null +++ b/examples/HTML-Forms/HTML-Forms.ino @@ -0,0 +1,377 @@ +/** + * 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 + +// 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; + +// 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 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); + while (WiFi.status() != WL_CONNECTED) { + Serial.print("."); + delay(500); + } + Serial.print("Connected. IP="); + Serial.println(WiFi.localIP()); + + // Setup filesystem + if (!SPIFFS.begin(true)) Serial.println("Mounting SPIFFS failed"); + + // For every resource available on the server, we need to create a ResourceNode + // The ResourceNode links URL and HTTP method to a handler function + ResourceNode * nodeRoot = new ResourceNode("/", "GET", &handleRoot); + 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(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. + 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("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; +} + +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 { // 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(""); + } +} + +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) { + // 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..6dad5b3 --- /dev/null +++ b/src/HTTPBodyParser.hpp @@ -0,0 +1,68 @@ +#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: + const size_t unknownLength = 0x7ffffffe; + + HTTPBodyParser(HTTPRequest * req): _request(req) {}; + virtual ~HTTPBodyParser() {} + + /** + * 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 filename of the current field or an empty string */ + virtual std::string getFieldFilename() = 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; + + /** + * 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 true when all field data has been read */ + virtual bool endOfField() = 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..2211419 --- /dev/null +++ b/src/HTTPMultipartBodyParser.cpp @@ -0,0 +1,288 @@ +#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; + } + boundary = contentType.substr(boundaryIndex + 9); // "boundary=" + 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); + 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 + 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. + 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() { + 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 fieldName; +} + +std::string HTTPMultipartBodyParser::getFieldFilename() { + return fieldFilename; +} + +std::string HTTPMultipartBodyParser::getFieldMimeType() { + return fieldMimeType; +} + +bool HTTPMultipartBodyParser::endOfField() { + return peekBoundary(); +} + +size_t HTTPMultipartBodyParser::read(byte* buffer, size_t bufferSize) { + 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; +} + +} /* namespace httpsserver */ diff --git a/src/HTTPMultipartBodyParser.hpp b/src/HTTPMultipartBodyParser.hpp new file mode 100644 index 0000000..c106fc2 --- /dev/null +++ b/src/HTTPMultipartBodyParser.hpp @@ -0,0 +1,39 @@ +#ifndef SRC_HTTPMULTIPARTBODYPARSER_HPP_ +#define SRC_HTTPMULTIPARTBODYPARSER_HPP_ + +#include +#include "HTTPBodyParser.hpp" + +namespace httpsserver { + +class HTTPMultipartBodyParser : public HTTPBodyParser { +public: + HTTPMultipartBodyParser(HTTPRequest * req); + ~HTTPMultipartBodyParser(); + virtual bool nextField(); + virtual std::string getFieldName(); + virtual std::string getFieldFilename(); + virtual std::string getFieldMimeType(); + 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 + +#endif \ No newline at end of file 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 new file mode 100644 index 0000000..d1ab73a --- /dev/null +++ b/src/HTTPURLEncodedBodyParser.cpp @@ -0,0 +1,128 @@ +#include "HTTPURLEncodedBodyParser.hpp" + +#define CHUNKSIZE 512 +#define MINCHUNKSIZE 64 + +namespace httpsserver { + +HTTPURLEncodedBodyParser::HTTPURLEncodedBodyParser(HTTPRequest * req): + HTTPBodyParser(req), + bodyBuffer(NULL), + bodyPtr(NULL), + bodyLength(0), + fieldBuffer(""), + fieldPtr(NULL), + fieldRemainingLength(0) +{ + bodyLength = _request->getContentLength(); + if (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; + size_t bufferUsed = 0; + size_t bufferAvailable = CHUNKSIZE; + while(!_request->requestComplete()) { + if (bufferAvailable < MINCHUNKSIZE) { + 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); + bufferUsed += didRead; + bufferAvailable -= didRead; + } + bodyLength = bufferUsed; + } + bodyPtr = bodyBuffer; + bodyBuffer[bodyLength] = '\0'; +} + +HTTPURLEncodedBodyParser::~HTTPURLEncodedBodyParser() { + if (bodyBuffer) { + free(bodyBuffer); + } + bodyBuffer = NULL; +} + +bool HTTPURLEncodedBodyParser::nextField() { + 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 fieldName; +} + +std::string HTTPURLEncodedBodyParser::getFieldFilename() { + return ""; +} + +std::string HTTPURLEncodedBodyParser::getFieldMimeType() { + return std::string("text/plain"); +} + +bool HTTPURLEncodedBodyParser::endOfField() { + return fieldRemainingLength <= 0; +} + +size_t HTTPURLEncodedBodyParser::read(byte* buffer, size_t bufferSize) { + if (bufferSize > fieldRemainingLength) { + bufferSize = fieldRemainingLength; + } + memcpy(buffer, fieldPtr, bufferSize); + fieldRemainingLength -= bufferSize; + fieldPtr += bufferSize; + return bufferSize; +} + +} /* namespace httpsserver */ diff --git a/src/HTTPURLEncodedBodyParser.hpp b/src/HTTPURLEncodedBodyParser.hpp new file mode 100644 index 0000000..f8529a7 --- /dev/null +++ b/src/HTTPURLEncodedBodyParser.hpp @@ -0,0 +1,32 @@ +#ifndef SRC_HTTPURLENCODEDBODYPARSER_HPP_ +#define SRC_HTTPURLENCODEDBODYPARSER_HPP_ + +#include +#include "HTTPBodyParser.hpp" + +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 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 + +#endif \ No newline at end of file