diff --git a/examples/HTTPS-Workers/HTTPS-Workers.ino b/examples/HTTPS-Workers/HTTPS-Workers.ino new file mode 100644 index 0000000..4b5d223 --- /dev/null +++ b/examples/HTTPS-Workers/HTTPS-Workers.ino @@ -0,0 +1,239 @@ +/** + * Example for the ESP32 HTTP(S) Webserver + * + * IMPORTANT NOTE: + * To run this script, your 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: + * - Use of TLS Tickets (RFC 5077) + * - Use of server workers in separate FreeRTOS tasks + * - Show simple page on web server root + * - Paralell serving of multiple images (cats.html) + * + */ + +/** + * NOTE: You need to upload the data directory to SPIFFS + * + * Cat images in data folder were taken from: + * https://unsplash.com/search/photos/cat + * License: https://unsplash.com/license + */ + +// TODO: Configure your WiFi here +#define WIFI_SSID "" +#define WIFI_PSK "" + +// Adjust these for available memory +#define MAX_CONNECTIONS 2 +#define NUM_WORKERS 2 + +//Delay image sending, milisecods between 256 bytes blocks. +//Can be used to visualize parallel connection handling +#define IMAGE_SLOWDOWN 10 + +// Include certificate data (see note above) +#include "cert.h" +#include "private_key.h" + +// We will use wifi +#include + +// And images stored in SPIFFS +#include "FS.h" +#include "SPIFFS.h" + +// Includes for the server +#include +#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 TLS-enabled server that uses the certificate +HTTPSServer secureServer = HTTPSServer(&cert, 443, MAX_CONNECTIONS); + +// Declare some handler functions for the various URLs on the server +void handleRoot(HTTPRequest * req, HTTPResponse * res); +void handleCatIndex(HTTPRequest * req, HTTPResponse * res); +void handleDefault(HTTPRequest * req, HTTPResponse * res); + +void setup() { + // For logging + Serial.begin(115200); + + // Check SPIFFS data + if (!SPIFFS.begin(false)) { + Serial.println("Please upload SPIFFS data for this sketch."); + while(1); + } + + // 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()); + + Serial.println("Memory before server start:"); + Serial.print(" heap size = "); Serial.println(heap_caps_get_free_size(MALLOC_CAP_8BIT)); + Serial.print(" largest free block = "); Serial.println(heap_caps_get_largest_free_block(MALLOC_CAP_8BIT)); + + // Add nodes to the server + secureServer.registerNode(new ResourceNode("/", "GET", &handleRoot)); + secureServer.registerNode(new ResourceNode("/cats.html", "GET", &handleCatIndex)); + secureServer.setDefaultNode(new ResourceNode("", "GET", &handleDefault)); + + Serial.println("Starting server..."); + + // We want to use RFC5077 TLS ticket for faster + // TLS connenction negotiation once one connection + // was established. Tickets are by default valid + // for one day + // Must be configure before you start the server + secureServer.enableTLSTickets(); + + // Enable two FreeRTOS task that will independedntly + // handle all server functions and resource callbacks + // Must be configure before you start the server + secureServer.enableWorkers(NUM_WORKERS); + + // And finally start the HTTPS server + secureServer.start(); + + // We are done. You are free to use the loop() function + // without minding the server... that is if you still have + // RAM available... + + if (secureServer.isRunning()) { + Serial.println("Server is running."); + } + + Serial.println("Memory after server start:"); + Serial.print(" heap size = "); Serial.println(heap_caps_get_free_size(MALLOC_CAP_8BIT)); + Serial.print(" largest free block = "); Serial.println(heap_caps_get_largest_free_block(MALLOC_CAP_8BIT)); +} + +void loop() { + Serial.println("main loop()"); + delay(5000); +} + +// HTTPS Server handlers + +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("

Hello World!

"); + res->print("

Your server is running for "); + // A bit of dynamic data: Show the uptime + res->print((int)(millis()/1000), DEC); + res->println(" seconds.

"); + res->print("

Task servicing this connection is: "); + res->print((uint32_t)xTaskGetCurrentTaskHandle(), HEX); + res->println("

"); + res->println("

Here you can find some random cats

"); + res->println(""); + res->println(""); +} + +/** + * Generate page with links to images in SPIFFS:/cats directory + */ +void handleCatIndex(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("Random cats"); + res->println(""); + File dir = SPIFFS.open("/cats"); + if (dir.isDirectory()) { + File file = dir.openNextFile(); + while (file) { + String filename = file.name(); + if (filename.endsWith(".jpg")) { + res->print(""); + } + file = dir.openNextFile(); + } + } + res->println(""); + res->println(""); +} + +// Default handler looks for matching file in SPIFFS +// and sends it, otherwise retuns 404 +void handleDefault(HTTPRequest * req, HTTPResponse * res) { + // Discard request body, if we received any + req->discardRequestBody(); + + // Find the file in SPIFFS + String filename = String(req->getRequestString().c_str()); + if (!SPIFFS.exists(filename)) { + // File doesn't exist, return 404 + res->setStatusCode(404); + res->setStatusText("Not found"); + // Write a tiny HTTP page + res->setHeader("Content-Type", "text/html"); + res->println(""); + res->println(""); + res->println("Not Found"); + res->println("

404 Not Found

The requested resource was not found on this server.

"); + res->println(""); + return; + } + File file = SPIFFS.open(filename); + + // Set headers, "Content-Length" is important! + res->setHeader("Content-Length", httpsserver::intToString(file.size())); + res->setHeader("Content-Type", "image/jpg"); + // Informational only, if you look at developer console in your browser + char taskHandle[11]; + sprintf(taskHandle, "0x%08x", (uint32_t)xTaskGetCurrentTaskHandle()); + res->setHeader("X-Task-ID", taskHandle); + + // Allocate buffer in the task stack as this may run in parallel + uint8_t buffer[256]; + // Send file contents + size_t length = 0; + do { + length = file.read(buffer, sizeof(buffer)); + res->write(buffer, length); + #if IMAGE_SLOWDOWN > 0 + delay(IMAGE_SLOWDOWN); + #endif + } while (length > 0); + file.close(); +} diff --git a/examples/HTTPS-Workers/data/cats/cat_sm_1.jpg b/examples/HTTPS-Workers/data/cats/cat_sm_1.jpg new file mode 100644 index 0000000..9a534f5 Binary files /dev/null and b/examples/HTTPS-Workers/data/cats/cat_sm_1.jpg differ diff --git a/examples/HTTPS-Workers/data/cats/cat_sm_10.jpg b/examples/HTTPS-Workers/data/cats/cat_sm_10.jpg new file mode 100644 index 0000000..6853328 Binary files /dev/null and b/examples/HTTPS-Workers/data/cats/cat_sm_10.jpg differ diff --git a/examples/HTTPS-Workers/data/cats/cat_sm_11.jpg b/examples/HTTPS-Workers/data/cats/cat_sm_11.jpg new file mode 100644 index 0000000..f54f33d Binary files /dev/null and b/examples/HTTPS-Workers/data/cats/cat_sm_11.jpg differ diff --git a/examples/HTTPS-Workers/data/cats/cat_sm_12.jpg b/examples/HTTPS-Workers/data/cats/cat_sm_12.jpg new file mode 100644 index 0000000..ebf919b Binary files /dev/null and b/examples/HTTPS-Workers/data/cats/cat_sm_12.jpg differ diff --git a/examples/HTTPS-Workers/data/cats/cat_sm_13.jpg b/examples/HTTPS-Workers/data/cats/cat_sm_13.jpg new file mode 100644 index 0000000..488df61 Binary files /dev/null and b/examples/HTTPS-Workers/data/cats/cat_sm_13.jpg differ diff --git a/examples/HTTPS-Workers/data/cats/cat_sm_14.jpg b/examples/HTTPS-Workers/data/cats/cat_sm_14.jpg new file mode 100644 index 0000000..0544199 Binary files /dev/null and b/examples/HTTPS-Workers/data/cats/cat_sm_14.jpg differ diff --git a/examples/HTTPS-Workers/data/cats/cat_sm_15.jpg b/examples/HTTPS-Workers/data/cats/cat_sm_15.jpg new file mode 100644 index 0000000..0c2b18a Binary files /dev/null and b/examples/HTTPS-Workers/data/cats/cat_sm_15.jpg differ diff --git a/examples/HTTPS-Workers/data/cats/cat_sm_16.jpg b/examples/HTTPS-Workers/data/cats/cat_sm_16.jpg new file mode 100644 index 0000000..8b03b35 Binary files /dev/null and b/examples/HTTPS-Workers/data/cats/cat_sm_16.jpg differ diff --git a/examples/HTTPS-Workers/data/cats/cat_sm_2.jpg b/examples/HTTPS-Workers/data/cats/cat_sm_2.jpg new file mode 100644 index 0000000..2299a19 Binary files /dev/null and b/examples/HTTPS-Workers/data/cats/cat_sm_2.jpg differ diff --git a/examples/HTTPS-Workers/data/cats/cat_sm_3.jpg b/examples/HTTPS-Workers/data/cats/cat_sm_3.jpg new file mode 100644 index 0000000..86143c2 Binary files /dev/null and b/examples/HTTPS-Workers/data/cats/cat_sm_3.jpg differ diff --git a/examples/HTTPS-Workers/data/cats/cat_sm_4.jpg b/examples/HTTPS-Workers/data/cats/cat_sm_4.jpg new file mode 100644 index 0000000..1d25fa6 Binary files /dev/null and b/examples/HTTPS-Workers/data/cats/cat_sm_4.jpg differ diff --git a/examples/HTTPS-Workers/data/cats/cat_sm_5.jpg b/examples/HTTPS-Workers/data/cats/cat_sm_5.jpg new file mode 100644 index 0000000..562e4c8 Binary files /dev/null and b/examples/HTTPS-Workers/data/cats/cat_sm_5.jpg differ diff --git a/examples/HTTPS-Workers/data/cats/cat_sm_6.jpg b/examples/HTTPS-Workers/data/cats/cat_sm_6.jpg new file mode 100644 index 0000000..830238c Binary files /dev/null and b/examples/HTTPS-Workers/data/cats/cat_sm_6.jpg differ diff --git a/examples/HTTPS-Workers/data/cats/cat_sm_7.jpg b/examples/HTTPS-Workers/data/cats/cat_sm_7.jpg new file mode 100644 index 0000000..73f50d8 Binary files /dev/null and b/examples/HTTPS-Workers/data/cats/cat_sm_7.jpg differ diff --git a/examples/HTTPS-Workers/data/cats/cat_sm_8.jpg b/examples/HTTPS-Workers/data/cats/cat_sm_8.jpg new file mode 100644 index 0000000..07f5e27 Binary files /dev/null and b/examples/HTTPS-Workers/data/cats/cat_sm_8.jpg differ diff --git a/examples/HTTPS-Workers/data/cats/cat_sm_9.jpg b/examples/HTTPS-Workers/data/cats/cat_sm_9.jpg new file mode 100644 index 0000000..e97edaa Binary files /dev/null and b/examples/HTTPS-Workers/data/cats/cat_sm_9.jpg differ diff --git a/src/HTTPConnection.cpp b/src/HTTPConnection.cpp index 0786768..e449593 100644 --- a/src/HTTPConnection.cpp +++ b/src/HTTPConnection.cpp @@ -26,23 +26,28 @@ HTTPConnection::~HTTPConnection() { } /** - * Initializes the connection from a server socket. + * Initializes the connection + */ +void HTTPConnection::initialize(int serverSocketID, HTTPHeaders *defaultHeaders) { + _defaultHeaders = defaultHeaders; + _serverSocket = serverSocketID; +} + +/** + * Accepts the connection from a server socket. * * The call WILL BLOCK if accept(serverSocketID) blocks. So use select() to check for that in advance. */ -int HTTPConnection::initialize(int serverSocketID, HTTPHeaders *defaultHeaders) { +int HTTPConnection::initialAccept() { if (_connectionState == STATE_UNDEFINED) { - _defaultHeaders = defaultHeaders; - _socket = accept(serverSocketID, (struct sockaddr * )&_sockAddr, &_addrLen); + _socket = accept(_serverSocket, (struct sockaddr * )&_sockAddr, &_addrLen); - // Build up SSL Connection context if the socket has been created successfully if (_socket >= 0) { HTTPS_LOGI("New connection. Socket FID=%d", _socket); - _connectionState = STATE_INITIAL; + _connectionState = STATE_ACCEPTED; _httpHeaders = new HTTPHeaders(); refreshTimeout(); return _socket; - } HTTPS_LOGE("Could not accept() new connection"); @@ -58,6 +63,23 @@ int HTTPConnection::initialize(int serverSocketID, HTTPHeaders *defaultHeaders) return -1; } +int HTTPConnection::fullyAccept() { + if (_connectionState == STATE_UNDEFINED) { + initialAccept(); + } + if (_connectionState == STATE_ACCEPTED) { + _connectionState = STATE_INITIAL; + return _socket; + } + return -1; +} + +/** + * Get connection socket + */ +int HTTPConnection::getSocket() { + return _socket; +} /** * True if the connection is timed out. @@ -68,6 +90,17 @@ bool HTTPConnection::isTimeoutExceeded() { return _lastTransmissionTS + HTTPS_CONNECTION_TIMEOUT < millis(); } +/** + * Return remaining milliseconds until timeout + * + * (Should return 0 or negative value if connection is timed-out or closed) + */ +long int HTTPConnection::remainingMsUntilTimeout() { + if (isClosed()) return -1; + unsigned long remain = _lastTransmissionTS + HTTPS_CONNECTION_TIMEOUT - millis(); + return (long int)remain; +} + /** * Resets the timeout to allow again the full HTTPS_CONNECTION_TIMEOUT milliseconds */ @@ -89,6 +122,14 @@ bool HTTPConnection::isError() { return (_connectionState == STATE_ERROR); } +bool HTTPConnection::isIdle() { + if (_connectionState == STATE_INITIAL) { + uint32_t delta = millis() - _lastTransmissionTS; + return (int32_t)delta > HTTPS_CONNECTION_IDLE_TIMEOUT; + } + return false; +} + bool HTTPConnection::isSecure() { return false; } @@ -129,6 +170,7 @@ void HTTPConnection::closeConnection() { if (_wsHandler != nullptr) { HTTPS_LOGD("Free WS Handler"); delete _wsHandler; + _wsHandler = NULL; } } @@ -258,20 +300,19 @@ size_t HTTPConnection::readBytesToBuffer(byte* buffer, size_t length) { return recv(_socket, buffer, length, MSG_WAITALL | MSG_DONTWAIT); } -void HTTPConnection::serverError() { - _connectionState = STATE_ERROR; - - char staticResponse[] = "HTTP/1.1 500 Internal Server Error\r\nServer: esp32https\r\nConnection:close\r\nContent-Type: text/html\r\nContent-Length:34\r\n\r\n

500 Internal Server Error

"; - writeBuffer((byte*)staticResponse, strlen(staticResponse)); - closeConnection(); -} - - -void HTTPConnection::clientError() { +void HTTPConnection::raiseError(uint16_t code, std::string reason) { _connectionState = STATE_ERROR; - - char staticResponse[] = "HTTP/1.1 400 Bad Request\r\nServer: esp32https\r\nConnection:close\r\nContent-Type: text/html\r\nContent-Length:26\r\n\r\n

400 Bad Request

"; - writeBuffer((byte*)staticResponse, strlen(staticResponse)); + std::string sCode = intToString(code); + + char headers[] = "\r\nConnection: close\r\nContent-Type: text/plain;charset=utf8\r\n\r\n"; + writeBuffer((byte*)"HTTP/1.1 ", 9); + writeBuffer((byte*)sCode.c_str(), sCode.length()); + writeBuffer((byte*)" ", 1); + writeBuffer((byte*)(reason.c_str()), reason.length()); + writeBuffer((byte*)headers, strlen(headers)); + writeBuffer((byte*)sCode.c_str(), sCode.length()); + writeBuffer((byte*)" ", 1); + writeBuffer((byte*)(reason.c_str()), reason.length()); closeConnection(); } @@ -289,7 +330,7 @@ void HTTPConnection::readLine(int lengthLimit) { } else { // Line has not been terminated by \r\n HTTPS_LOGW("Line without \\r\\n (got only \\r). FID=%d", _socket); - clientError(); + raiseError(400, "Bad Request"); return; } } @@ -301,7 +342,7 @@ void HTTPConnection::readLine(int lengthLimit) { // Check that the max request string size is not exceeded if (_parserLine.text.length() > lengthLimit) { HTTPS_LOGW("Header length exceeded. FID=%d", _socket); - serverError(); + raiseError(431, "Request Header Fields Too Large"); return; } } @@ -319,7 +360,7 @@ void HTTPConnection::signalClientClose() { */ void HTTPConnection::signalRequestError() { // TODO: Check that no response has been transmitted yet - serverError(); + raiseError(400, "Bad Request"); } /** @@ -331,7 +372,13 @@ size_t HTTPConnection::getCacheSize() { return (_isKeepAlive ? HTTPS_KEEPALIVE_CACHESIZE : 0); } -void HTTPConnection::loop() { +/** + * Connection main async loop method + * + * Returns true if there is more data to be processed in + * the input buffers + */ +bool HTTPConnection::loop() { // First, update the buffer // newByteCount will contain the number of new bytes that have to be processed updateBuffer(); @@ -359,7 +406,7 @@ void HTTPConnection::loop() { size_t spaceAfterMethodIdx = _parserLine.text.find(' '); if (spaceAfterMethodIdx == std::string::npos) { HTTPS_LOGW("Missing space after method"); - clientError(); + raiseError(400, "Bad Request"); break; } _httpMethod = _parserLine.text.substr(0, spaceAfterMethodIdx); @@ -368,18 +415,19 @@ void HTTPConnection::loop() { size_t spaceAfterResourceIdx = _parserLine.text.find(' ', spaceAfterMethodIdx + 1); if (spaceAfterResourceIdx == std::string::npos) { HTTPS_LOGW("Missing space after resource"); - clientError(); + raiseError(400, "Bad Request"); break; } _httpResource = _parserLine.text.substr(spaceAfterMethodIdx + 1, spaceAfterResourceIdx - _httpMethod.length() - 1); _parserLine.parsingFinished = false; _parserLine.text = ""; - HTTPS_LOGI("Request: %s %s (FID=%d)", _httpMethod.c_str(), _httpResource.c_str(), _socket); + HTTPS_LOGI("Request: %s %s (FID=%d, T=%p)", _httpMethod.c_str(), _httpResource.c_str(), _socket, xTaskGetCurrentTaskHandle()); _connectionState = STATE_REQUEST_FINISHED; } - break; + if (_connectionState != STATE_REQUEST_FINISHED) break; + case STATE_REQUEST_FINISHED: // Read headers while (_bufferProcessed < _bufferUnusedIdx && !isClosed()) { @@ -404,7 +452,7 @@ void HTTPConnection::loop() { HTTPS_LOGD("Header: %s = %s (FID=%d)", _parserLine.text.substr(0, idxColon).c_str(), _parserLine.text.substr(idxColon+2).c_str(), _socket); } else { HTTPS_LOGW("Malformed request header: %s", _parserLine.text.c_str()); - clientError(); + raiseError(400, "Bad Request"); break; } } @@ -414,7 +462,8 @@ void HTTPConnection::loop() { } } - break; + if (_connectionState != STATE_HEADERS_FINISHED) break; + case STATE_HEADERS_FINISHED: // Handle body { HTTPS_LOGD("Resolving resource..."); @@ -520,19 +569,23 @@ void HTTPConnection::loop() { // we have no chance to do so. if (!_isKeepAlive) { // No KeepAlive -> We are done. Transition to next state. + HTTPS_LOGD("No keep-alive"); if (!isClosed()) { _connectionState = STATE_BODY_FINISHED; } } else { if (res.isResponseBuffered()) { - // If the response could be buffered: + // If the response is buffered: + HTTPS_LOGD("Buffered, set keep-alive"); res.setHeader("Connection", "keep-alive"); res.finalize(); + } + if (res.correctContentLength()) { if (_clientState != CSTATE_CLOSED) { // Refresh the timeout for the new request refreshTimeout(); // Reset headers for the new connection - _httpHeaders->clearAll(); + if (_httpHeaders) _httpHeaders->clearAll(); // Go back to initial state _connectionState = STATE_INITIAL; } @@ -546,17 +599,20 @@ void HTTPConnection::loop() { } else { // No match (no default route configured, nothing does match) HTTPS_LOGW("Could not find a matching resource"); - serverError(); + raiseError(404, "Not Found"); } } - break; + if (_connectionState != STATE_BODY_FINISHED) break; + case STATE_BODY_FINISHED: // Request is complete closeConnection(); break; + case STATE_CLOSING: // As long as we are in closing state, we call closeConnection() again and wait for it to finish or timeout closeConnection(); break; + case STATE_WEBSOCKET: // Do handling of the websocket refreshTimeout(); // don't timeout websocket connection if(pendingBufferSize() > 0) { @@ -575,9 +631,10 @@ void HTTPConnection::loop() { } } + // Return true if connection has more data to process + return (!isClosed() && ((_bufferProcessed < _bufferUnusedIdx) || canReadData())); } - bool HTTPConnection::checkWebsocket() { if(_httpMethod == "GET" && !_httpHeaders->getValue("Host").empty() && diff --git a/src/HTTPConnection.hpp b/src/HTTPConnection.hpp index d99776f..7de2f42 100644 --- a/src/HTTPConnection.hpp +++ b/src/HTTPConnection.hpp @@ -39,13 +39,18 @@ class HTTPConnection : private ConnectionContext { HTTPConnection(ResourceResolver * resResolver); virtual ~HTTPConnection(); - virtual int initialize(int serverSocketID, HTTPHeaders *defaultHeaders); + virtual void initialize(int serverSocketID, HTTPHeaders *defaultHeaders); + virtual int initialAccept(); + virtual int fullyAccept(); virtual void closeConnection(); virtual bool isSecure(); - void loop(); + bool loop(); bool isClosed(); bool isError(); + bool isIdle(); + long int remainingMsUntilTimeout(); + int getSocket(); protected: friend class HTTPRequest; @@ -57,6 +62,9 @@ class HTTPConnection : private ConnectionContext { virtual bool canReadData(); virtual size_t pendingByteCount(); + // Connection socket (LWIP) + int _socket; + // Timestamp of the last transmission action unsigned long _lastTransmissionTS; @@ -82,6 +90,8 @@ class HTTPConnection : private ConnectionContext { // The connection has not been established yet STATE_UNDEFINED, + // The connection fully established (i.e. TLS) + STATE_ACCEPTED, // The connection has just been created STATE_INITIAL, // The request line has been parsed @@ -107,8 +117,7 @@ class HTTPConnection : private ConnectionContext { } _clientState; private: - void serverError(); - void clientError(); + void raiseError(uint16_t code, std::string reason); void readLine(int lengthLimit); bool isTimeoutExceeded(); @@ -134,8 +143,8 @@ class HTTPConnection : private ConnectionContext { // Socket address, length etc for the connection struct sockaddr _sockAddr; socklen_t _addrLen; - int _socket; - + int _serverSocket; + // Resource resolver used to resolve resources ResourceResolver * _resResolver; @@ -158,7 +167,6 @@ class HTTPConnection : private ConnectionContext { //Websocket connection WebsocketHandler * _wsHandler; - }; void handleWebsocketHandshake(HTTPRequest * req, HTTPResponse * res); diff --git a/src/HTTPRequest.cpp b/src/HTTPRequest.cpp index 222b0b3..617c55e 100644 --- a/src/HTTPRequest.cpp +++ b/src/HTTPRequest.cpp @@ -28,7 +28,7 @@ HTTPRequest::HTTPRequest( } HTTPRequest::~HTTPRequest() { - _headers->clearAll(); + if (_headers) _headers->clearAll(); } diff --git a/src/HTTPResponse.cpp b/src/HTTPResponse.cpp index b47e945..9c78083 100644 --- a/src/HTTPResponse.cpp +++ b/src/HTTPResponse.cpp @@ -13,16 +13,14 @@ HTTPResponse::HTTPResponse(ConnectionContext * con): _statusText = "OK"; _headerWritten = false; _isError = false; + _setLength = 0; + _sentBytesCount = 0; _responseCacheSize = con->getCacheSize(); _responseCachePointer = 0; - if (_responseCacheSize > 0) { - HTTPS_LOGD("Creating buffered response, size: %d", _responseCacheSize); - _responseCache = new byte[_responseCacheSize]; - } else { - HTTPS_LOGD("Creating non-buffered response"); - _responseCache = NULL; - } + _responseCache = NULL; + // Don't create buffer response just yet, + // wait and see if we receive Content-Length ... } HTTPResponse::~HTTPResponse() { @@ -48,8 +46,26 @@ std::string HTTPResponse::getStatusText() { return _statusText; } +void HTTPResponse::setContentLength(size_t size) { + if (isHeaderWritten()) { + HTTPS_LOGE("Setting Content-Lenght after headers sent!"); + error(); + return; + } + if ((_setLength > 0) && (size != _setLength)) { + HTTPS_LOGW("Setting Content-Lenght more than once!"); + } + HTTPS_LOGD("Set Content-Lenght: %d", size); + _setLength = size; +} + void HTTPResponse::setHeader(std::string const &name, std::string const &value) { _headers.set(new HTTPHeader(name, value)); + // Watch for "Content-Length" header + if (name.compare("Content-Length") == 0) { + setContentLength(parseUInt(value)); + return; + } } bool HTTPResponse::isHeaderWritten() { @@ -60,6 +76,14 @@ bool HTTPResponse::isResponseBuffered() { return _responseCache != NULL; } +bool HTTPResponse::correctContentLength() { + if (_setLength > 0) { + if (_sentBytesCount == _setLength) return true; + HTTPS_LOGE("Content-Lenght (%u) and data sent (%u) mismatch!", _setLength, _sentBytesCount); + } + return false; +} + void HTTPResponse::finalize() { if (isResponseBuffered()) { drainBuffer(); @@ -77,6 +101,22 @@ void HTTPResponse::printStd(const std::string &str) { * Writes bytes to the response. May be called several times. */ size_t HTTPResponse::write(const uint8_t *buffer, size_t size) { + if (_sentBytesCount < 1) { + if (_setLength > 0) { + HTTPS_LOGD("Streaming response directly, size: %d", _setLength); + } else if (size > 0) { + // Try buffering + if (_responseCacheSize > 0) { + _responseCache = new byte[_responseCacheSize]; + HTTPS_LOGD("Content-Length not set. Creating buffered response, size: %d", _responseCacheSize); + } else { + // We'll have to tear down the connection to signal end of data + setHeader("Connection", "close"); + HTTPS_LOGD("Content-Length not set. Creating non-buffered response"); + } + } + } + _sentBytesCount += size; if(!isResponseBuffered()) { printHeader(); } @@ -87,11 +127,8 @@ size_t HTTPResponse::write(const uint8_t *buffer, size_t size) { * Writes a single byte to the response. */ size_t HTTPResponse::write(uint8_t b) { - if(!isResponseBuffered()) { - printHeader(); - } byte ba[] = {b}; - return writeBytesInternal(ba, 1); + return write(ba, 1); } /** @@ -168,8 +205,8 @@ void HTTPResponse::drainBuffer(bool onOverflow) { HTTPS_LOGD("Draining response buffer"); // Check for 0 as it may be an overflow reaction without any data that has been written earlier if(_responseCachePointer > 0) { - // FIXME: Return value? - _con->writeBuffer((byte*)_responseCache, _responseCachePointer); + _setLength = _responseCachePointer; + _sentBytesCount = _con->writeBuffer((byte*)_responseCache, _responseCachePointer); } delete[] _responseCache; _responseCache = NULL; diff --git a/src/HTTPResponse.hpp b/src/HTTPResponse.hpp index 1ba0c6e..d0fcd4c 100644 --- a/src/HTTPResponse.hpp +++ b/src/HTTPResponse.hpp @@ -42,7 +42,9 @@ class HTTPResponse : public Print { void error(); + void setContentLength(size_t size); bool isResponseBuffered(); + bool correctContentLength(); void finalize(); ConnectionContext * _con; @@ -59,6 +61,10 @@ class HTTPResponse : public Print { bool _headerWritten; bool _isError; + // Response length + size_t _setLength; + size_t _sentBytesCount; + // Response cache byte * _responseCache; size_t _responseCacheSize; diff --git a/src/HTTPSConnection.cpp b/src/HTTPSConnection.cpp index e0e3dd0..7ca92ce 100644 --- a/src/HTTPSConnection.cpp +++ b/src/HTTPSConnection.cpp @@ -5,7 +5,9 @@ namespace httpsserver { HTTPSConnection::HTTPSConnection(ResourceResolver * resResolver): HTTPConnection(resResolver) { + _sslCtx = NULL; _ssl = NULL; + _TLSTickets = NULL; } HTTPSConnection::~HTTPSConnection() { @@ -18,19 +20,34 @@ bool HTTPSConnection::isSecure() { } /** - * Initializes the connection from a server socket. + * Initializes the connection with SSL context + */ +void HTTPSConnection::initialize(int serverSocketID, HTTPHeaders *defaultHeaders, SSL_CTX * sslCtx, TLSTickets * tickets) { + HTTPConnection::initialize(serverSocketID, defaultHeaders); + _sslCtx = sslCtx; + _TLSTickets = tickets; +} + +/** + * Accepts the connection from a server socket. * * The call WILL BLOCK if accept(serverSocketID) blocks. So use select() to check for that in advance. */ -int HTTPSConnection::initialize(int serverSocketID, SSL_CTX * sslCtx, HTTPHeaders *defaultHeaders) { +int HTTPSConnection::fullyAccept() { + if (_connectionState == STATE_UNDEFINED) { - // Let the base class connect the plain tcp socket - int resSocket = HTTPConnection::initialize(serverSocketID, defaultHeaders); + initialAccept(); + } + + if (_connectionState == STATE_ACCEPTED) { + int resSocket = _socket; // Build up SSL Connection context if the socket has been created successfully if (resSocket >= 0) { + HTTPS_LOGV("Before SSL accept free:%u, lfb:%u\n", heap_caps_get_free_size(MALLOC_CAP_8BIT), heap_caps_get_largest_free_block(MALLOC_CAP_8BIT)); + _ssl = SSL_new(_sslCtx); - _ssl = SSL_new(sslCtx); + if (_TLSTickets != NULL) _TLSTickets->enable(_ssl); if (_ssl) { // Bind SSL to the socket @@ -40,9 +57,13 @@ int HTTPSConnection::initialize(int serverSocketID, SSL_CTX * sslCtx, HTTPHeader // Perform the handshake success = SSL_accept(_ssl); if (success) { + HTTPS_LOGD("SSL accepted (FID=%d)", resSocket); + HTTPS_LOGV("After SSL accept free:%u, lfb:%u", heap_caps_get_free_size(MALLOC_CAP_8BIT), heap_caps_get_largest_free_block(MALLOC_CAP_8BIT)); + _connectionState = STATE_INITIAL; return resSocket; } else { HTTPS_LOGE("SSL_accept failed. Aborting handshake. FID=%d", resSocket); + HTTPS_LOGV("After fail free:%u, lfb:%u", heap_caps_get_free_size(MALLOC_CAP_8BIT), heap_caps_get_largest_free_block(MALLOC_CAP_8BIT)); } } else { HTTPS_LOGE("SSL_set_fd failed. Aborting handshake. FID=%d", resSocket); @@ -105,19 +126,21 @@ void HTTPSConnection::closeConnection() { } size_t HTTPSConnection::writeBuffer(byte* buffer, size_t length) { - return SSL_write(_ssl, buffer, length); + if (_ssl != NULL) return SSL_write(_ssl, buffer, length); + return 0; } size_t HTTPSConnection::readBytesToBuffer(byte* buffer, size_t length) { - return SSL_read(_ssl, buffer, length); + if (_ssl != NULL) return SSL_read(_ssl, buffer, length); + return 0; } size_t HTTPSConnection::pendingByteCount() { - return SSL_pending(_ssl); + return (_ssl != NULL) && SSL_pending(_ssl); } bool HTTPSConnection::canReadData() { - return HTTPConnection::canReadData() || (SSL_pending(_ssl) > 0); + return HTTPConnection::canReadData() || ((_ssl != NULL) && (SSL_pending(_ssl) > 0)); } } /* namespace httpsserver */ diff --git a/src/HTTPSConnection.hpp b/src/HTTPSConnection.hpp index 8adbce5..fcc471d 100644 --- a/src/HTTPSConnection.hpp +++ b/src/HTTPSConnection.hpp @@ -23,6 +23,7 @@ #include "ResourceNode.hpp" #include "HTTPRequest.hpp" #include "HTTPResponse.hpp" +#include "TLSTickets.hpp" namespace httpsserver { @@ -34,7 +35,8 @@ class HTTPSConnection : public HTTPConnection { HTTPSConnection(ResourceResolver * resResolver); virtual ~HTTPSConnection(); - virtual int initialize(int serverSocketID, SSL_CTX * sslCtx, HTTPHeaders *defaultHeaders); + virtual void initialize(int serverSocketID, HTTPHeaders *defaultHeaders, SSL_CTX * sslCtx, TLSTickets * tickets); + virtual int fullyAccept() override; virtual void closeConnection(); virtual bool isSecure(); @@ -49,7 +51,9 @@ class HTTPSConnection : public HTTPConnection { private: // SSL context for this connection + SSL_CTX * _sslCtx; SSL * _ssl; + TLSTickets * _TLSTickets; }; diff --git a/src/HTTPSServer.cpp b/src/HTTPSServer.cpp index 4d8352d..5881ae2 100644 --- a/src/HTTPSServer.cpp +++ b/src/HTTPSServer.cpp @@ -9,6 +9,7 @@ HTTPSServer::HTTPSServer(SSLCert * cert, const uint16_t port, const uint8_t maxC // Configure runtime data _sslctx = NULL; + _TLSTickets = NULL; } HTTPSServer::~HTTPSServer() { @@ -45,6 +46,10 @@ uint8_t HTTPSServer::setupSocket() { } } +void HTTPSServer::enableTLSTickets(uint32_t liftimeSeconds, bool useHardwareRNG) { + _TLSTickets = new TLSTickets("esp32_https_server", liftimeSeconds, useHardwareRNG); +} + void HTTPSServer::teardownSocket() { HTTPServer::teardownSocket(); @@ -54,10 +59,10 @@ void HTTPSServer::teardownSocket() { _sslctx = NULL; } -int HTTPSServer::createConnection(int idx) { +HTTPSConnection * HTTPSServer::createConnection() { HTTPSConnection * newConnection = new HTTPSConnection(this); - _connections[idx] = newConnection; - return newConnection->initialize(_socket, _sslctx, &_defaultHeaders); + newConnection->initialize(_socket, &_defaultHeaders, _sslctx, _TLSTickets); + return newConnection; } /** diff --git a/src/HTTPSServer.hpp b/src/HTTPSServer.hpp index 68596bf..29459c0 100644 --- a/src/HTTPSServer.hpp +++ b/src/HTTPSServer.hpp @@ -21,6 +21,7 @@ #include "ResolvedResource.hpp" #include "HTTPSConnection.hpp" #include "SSLCert.hpp" +#include "TLSTickets.hpp" namespace httpsserver { @@ -32,6 +33,11 @@ class HTTPSServer : public HTTPServer { HTTPSServer(SSLCert * cert, const uint16_t portHTTPS = 443, const uint8_t maxConnections = 4, const in_addr_t bindAddress = 0); virtual ~HTTPSServer(); + // RFC 5077 TLS session tickets + void enableTLSTickets(uint32_t liftimeSeconds = 86400, bool useHardwareRNG = false); + + virtual HTTPSConnection * createConnection() override; + private: // Static configuration. Port, keys, etc. ==================== // Certificate that should be used (includes private key) @@ -39,6 +45,7 @@ class HTTPSServer : public HTTPServer { //// Runtime data ============================================ SSL_CTX * _sslctx; + TLSTickets * _TLSTickets; // Status of the server: Are we running, or not? // Setup functions @@ -46,9 +53,6 @@ class HTTPSServer : public HTTPServer { virtual void teardownSocket(); uint8_t setupSSLCTX(); uint8_t setupCert(); - - // Helper functions - virtual int createConnection(int idx); }; } /* namespace httpsserver */ diff --git a/src/HTTPSServerConstants.hpp b/src/HTTPSServerConstants.hpp index d71aa3d..ca56853 100644 --- a/src/HTTPSServerConstants.hpp +++ b/src/HTTPSServerConstants.hpp @@ -7,6 +7,7 @@ // 2: Error + Warn // 3: Error + Warn + Info // 4: Error + Warn + Info + Debug +// 5: Error + Warn + Info + Debug + Verbose #ifndef HTTPS_LOGLEVEL #define HTTPS_LOGLEVEL 3 @@ -42,6 +43,13 @@ #define HTTPS_LOGD(...) do {} while (0) #endif +#if HTTPS_LOGLEVEL > 4 + #define HTTPS_LOGV(...) HTTPS_LOGTAG("D");Serial.printf(__VA_ARGS__);Serial.println() +#else + #define HTTPS_LOGV(...) {} +#endif + + // The following lines define limits of the protocol. Exceeding these limits will lead to a 500 error // Maximum of header lines that are parsed @@ -75,6 +83,12 @@ #define HTTPS_CONNECTION_TIMEOUT 20000 #endif +// Timeout for connection in STATE_INITIAL to be considered idle. +// When connection is idle, server may close it and switch to pending connection +#ifndef HTTPS_CONNECTION_IDLE_TIMEOUT +#define HTTPS_CONNECTION_IDLE_TIMEOUT 500 +#endif + // Timeout used to wait for shutdown of SSL connection (ms) // (time for the client to return notify close flag) - without it, truncation attacks might be possible #ifndef HTTPS_SHUTDOWN_TIMEOUT @@ -86,4 +100,13 @@ #define HTTPS_SHA1_LENGTH 20 #endif +// Default values for workers. +// Stack size should not be less than 4096 for TLS connections +#ifndef HTTPS_CONN_TASK_STACK_SIZE +#define HTTPS_CONN_TASK_STACK_SIZE 4096 +#endif +#ifndef HTTPS_CONN_TASK_PRIORITY +#define HTTPS_CONN_TASK_PRIORITY (tskIDLE_PRIORITY + 1) +#endif + #endif /* SRC_HTTPSSERVERCONSTANTS_HPP_ */ diff --git a/src/HTTPServer.cpp b/src/HTTPServer.cpp index d11bf6b..d74c2b2 100644 --- a/src/HTTPServer.cpp +++ b/src/HTTPServer.cpp @@ -8,12 +8,15 @@ HTTPServer::HTTPServer(const uint16_t port, const uint8_t maxConnections, const _maxConnections(maxConnections), _bindAddress(bindAddress) { - // Create space for the connections - _connections = new HTTPConnection*[maxConnections]; - for(uint8_t i = 0; i < maxConnections; i++) _connections[i] = NULL; + _connections = NULL; + _workers = NULL; // Configure runtime data _socket = -1; + _selectMutex = xSemaphoreCreateMutex(); + _numWorkers = 0; + _workQueue = NULL; + _pendingConnection = NULL; _running = false; } @@ -25,8 +28,28 @@ HTTPServer::~HTTPServer() { stop(); } - // Delete connection pointers - delete[] _connections; + // Delete allocated memory + if (_workers) delete[] _workers; + if (_connections) delete[] _connections; + if (_connectionMutex) delete[] _connectionMutex; + if (_selectMutex) vSemaphoreDelete(_selectMutex); + if (_workQueue) vQueueDelete(_workQueue); +} + +/** + * Enables workers, each running in separate FreeRTOS task + */ +void HTTPServer::enableWorkers(uint8_t numWorkers, size_t stackSize, int priority) { + if (!_running && (numWorkers > 0)) { + _numWorkers = numWorkers; + if (_workers == NULL) { + _workers = new HTTPWorker * [numWorkers]; + HTTPS_LOGD("Creating %d worker(s) (%u,%d)", numWorkers, stackSize, priority); + for(uint8_t i = 0; i < numWorkers; i++) { + _workers[i] = new HTTPWorker(this, stackSize, priority); + } + } + } } /** @@ -35,7 +58,23 @@ HTTPServer::~HTTPServer() { uint8_t HTTPServer::start() { if (!_running) { if (setupSocket()) { + // Create space for the connections if not using worker tasks + if (!_workQueue) { + _workQueue = xQueueCreate(2 * _maxConnections, sizeof(int8_t)); + } + if (!_connections) { + _connections = new HTTPConnection*[_maxConnections]; + for(uint8_t i = 0; i < _maxConnections; i++) _connections[i] = NULL; + } + if (!_connectionMutex) { + _connectionMutex = new SemaphoreHandle_t[_maxConnections]; + for(uint8_t i = 0; i < _maxConnections; i++) _connectionMutex[i] = xSemaphoreCreateMutex(); + } _running = true; + // start the workers + if (_numWorkers > 0) { + for(uint8_t i = 0; i < _numWorkers; i++) _workers[i]->start(); + } return 1; } return 0; @@ -56,29 +95,59 @@ void HTTPServer::stop() { if (_running) { // Set the flag that the server is stopped _running = false; - - // Clean up the connections - bool hasOpenConnections = true; - while(hasOpenConnections) { - hasOpenConnections = false; - for(int i = 0; i < _maxConnections; i++) { - if (_connections[i] != NULL) { - _connections[i]->closeConnection(); - - // Check if closing succeeded. If not, we need to call the close function multiple times - // and wait for the client - if (_connections[i]->isClosed()) { - delete _connections[i]; - } else { - hasOpenConnections = true; + xSemaphoreTake(_selectMutex, portMAX_DELAY); // We won't be releasing this + + if (_connections) { + // Clean up the connections + bool hasOpenConnections = true; + while(hasOpenConnections) { + hasOpenConnections = false; + for(int i = 0; i < _maxConnections; i++) { + xSemaphoreTake(_connectionMutex[i], portMAX_DELAY); + if (_connections[i] != NULL) { + _connections[i]->closeConnection(); + + // Check if closing succeeded. If not, we need to call the close function multiple times + // and wait for the client + if (_connections[i]->isClosed()) { + delete _connections[i]; + _connections[i] = NULL; + } else { + hasOpenConnections = true; + } } + xSemaphoreGive(_connectionMutex[i]); + vSemaphoreDelete(_connectionMutex[i]); } + delay(1); } - delay(1); - } + } // if (_connections) teardownSocket(); + // Server _running is false, workers should terminate themselves... + if (_workers) { + // Just give them invalid connection number if they are blocked on the work queue + int8_t noWork = -1; + for(int i = 0; i < _numWorkers; i++) xQueueSend(_workQueue, &noWork, 0); + bool workerStillActive = false; + do { + for(int i = 0; i < _maxConnections; i++) { + if (_workers[i] != NULL) { + if (_workers[i]->isRunning()) { + workerStillActive = true; + } else { + delete _workers[i]; + _workers[i] = NULL; + } + } + } + if (workerStillActive) vTaskDelay(1); + } while (workerStillActive); + delete _workers; + _workers = NULL; + } // if (_workers) + } } @@ -92,75 +161,235 @@ void HTTPServer::setDefaultHeader(std::string name, std::string value) { } /** - * The loop method can either be called by periodical interrupt or in the main loop and handles processing - * of data + * Manages server connections + * - Cleans up closed connections + * - Queues work if there is new data on existing connection + * - Checks for new connections + * - Accepts pending connections when there is space in connection pool + * - Closes idle connections when there are pending ones + * + * Returns after all needed work is done or maxTimeoutMs expires */ -void HTTPServer::loop() { - - // Only handle requests if the server is still running - if(!_running) return; +void HTTPServer::manageConnections(int maxTimeoutMs) { + fd_set readFDs, exceptFDs, timeoutFDs; + FD_ZERO(&readFDs); + FD_ZERO(&exceptFDs); + FD_ZERO(&timeoutFDs); + int maxSocket = -1; + + // The idea here is to block on something (up to maxTimeoutMs) until + // there is new data or new connection, so work can be queue up + + // Add only the server socket or the pending connection socket + // as trying to select on the server socket while we know that + // there is pending connection will return imediatelly + if (_pendingConnection) { + // If there is pending connection and we have not yet received data + if (!_lookForIdleConnection) { + int pendingSocket = _pendingConnection->getSocket(); + FD_SET(pendingSocket, &readFDs); + FD_SET(pendingSocket, &exceptFDs); + maxSocket = pendingSocket; + } + } else { + // No pending connections (that we know of), monitor server socket + FD_SET(_socket, &readFDs); + FD_SET(_socket, &exceptFDs); + maxSocket = _socket; + } - // Step 1: Process existing connections - // Process open connections and store the index of a free connection - // (we might use that later on) - int freeConnectionIdx = -1; + // Cleanup closed connections and find minimal select timeout + // Add active connections to select sets + int minRemain = maxTimeoutMs; for (int i = 0; i < _maxConnections; i++) { - // Fetch a free index in the pointer array - if (_connections[i] == NULL) { - freeConnectionIdx = i; + if (_connections[i] != NULL) { + // Try to lock connection. + if (xSemaphoreTake(_connectionMutex[i], 0)) { + // If we suceeded, connection is currently not beening worked by other task + int fd = _connections[i]->getSocket(); + if (_connections[i]->isClosed()) { + // if it's closed clean up: + HTTPS_LOGV("Deleted connection[%d], FID=%d", i, fd); + delete _connections[i]; + _connections[i] = NULL; + // We released one connection slot, don't look for idle connection + _lookForIdleConnection = false; + fd = -1; + } + if (fd > 0) { + int remain = _connections[i]->remainingMsUntilTimeout(); + if (_lookForIdleConnection) { + // There is partially accepted pending connection, check for idle connections + if ((remain < 1) || _connections[i]->isIdle()) { + HTTPS_LOGI("Closing IDLE connection[%d] FID=%d to accept FID=%d", i, fd, _pendingConnection->getSocket()); + _connections[i]->closeConnection(); + // We closed one connection, don't look for more idle connections + _lookForIdleConnection = false; + fd = _connections[i]->getSocket(); + } else { + remain = min(remain, (int)HTTPS_CONNECTION_IDLE_TIMEOUT); + } + } + if (fd > 0) { + // Add the connection to select sets + if (remain < 1) FD_SET(fd, &timeoutFDs); + FD_SET(fd, &readFDs); + FD_SET(fd, &exceptFDs); + if (fd > maxSocket) maxSocket = fd; + } else { + remain = 0; // Force imediate rescan + } + if (remain < minRemain) minRemain = remain; + } + xSemaphoreGive(_connectionMutex[i]); + } + } + } - } else { - // if there is a connection (_connections[i]!=NULL), check if its open or closed: - if (_connections[i]->isClosed()) { - // if it's closed, clean up: - delete _connections[i]; - _connections[i] = NULL; - freeConnectionIdx = i; - } else { - // if not, process it: - _connections[i]->loop(); + // Select on socket sets with minRemain (ms) timeout + if (minRemain < 0) minRemain = 0; + timeval _timeout; + _timeout.tv_sec = minRemain / 1000; + _timeout.tv_usec = (minRemain - _timeout.tv_sec * 1000) * 1000; + select(maxSocket + 1, &readFDs, NULL, &exceptFDs, &_timeout); + + // if FD_ISSET(serverSocket, &except_fds) {} // server is stopping ? + + // Assign work for connections that have data, error or timeout + // and find empty connection slot + int8_t freeIndex = -1; + for (int8_t i = 0; i < _maxConnections; i++) { + if (_connections[i] != NULL) { + int fd = _connections[i]->getSocket(); + if ((fd < 1) || (FD_ISSET(fd, &readFDs)) || (FD_ISSET(fd, &exceptFDs)) || (FD_ISSET(fd, &timeoutFDs))) { + xQueueSend(_workQueue, &i, 0); + HTTPS_LOGV("Queued work for connection[%d], FID=%d", i, fd); } + } else { + freeIndex = i; } } - - // Step 2: Check for new connections - // This makes only sense if there is space to store the connection - if (freeConnectionIdx > -1) { - - // We create a file descriptor set to be able to use the select function - fd_set sockfds; - // Out socket is the only socket in this set - FD_ZERO(&sockfds); - FD_SET(_socket, &sockfds); - - // We define a "immediate" timeout - timeval timeout; - timeout.tv_sec = 0; - timeout.tv_usec = 0; // Return immediately, if possible - - // Wait for input - // As by 2017-12-14, it seems that FD_SETSIZE is defined as 0x40, but socket IDs now - // start at 0x1000, so we need to use _socket+1 here - select(_socket + 1, &sockfds, NULL, NULL, &timeout); - - // There is input - if (FD_ISSET(_socket, &sockfds)) { - int socketIdentifier = createConnection(freeConnectionIdx); - - // If initializing did not work, discard the new socket immediately - if (socketIdentifier < 0) { - delete _connections[freeConnectionIdx]; - _connections[freeConnectionIdx] = NULL; + + // If we have known pending connection ... + if (_pendingConnection) { + int pendingSocket = _pendingConnection->getSocket(); + // ... and if it is talking to us (client speaks first for both HTTP and TLS) ... + if (_lookForIdleConnection || (FD_ISSET(pendingSocket, &readFDs))) { + // ... and if we have space to fully accept ... + if (freeIndex >= 0) { + // ... try to fully accept the connection. + if (_pendingConnection->fullyAccept() > 0) { + // Fully accepted, add to active connections + HTTPS_LOGV("Accepted connection[%d], FID=%d", freeIndex, pendingSocket); + _connections[freeIndex] = _pendingConnection; + xQueueSend(_workQueue, &freeIndex, 0); + } else { + HTTPS_LOGD("Discarded connection FID=%d", pendingSocket); + delete _pendingConnection; + } + _pendingConnection = NULL; + _lookForIdleConnection = false; + } else { + // Pending connection has data to read but we currently + // have no space in connection pool... set flag to try + // to close one of the idle connections... + _lookForIdleConnection = true; + } + } + } else { + // No pending connection, see if we have new one on the server socket + if (FD_ISSET(_socket, &readFDs)) { + // Try to initially accept the new connection + HTTPConnection * connection = createConnection(); + int newSocket = (connection) ? connection->initialAccept() : -1; + if (newSocket > 0) { + // Initial accept succeded, do we have space in the pool + if (freeIndex >= 0) { + if (connection->fullyAccept() > 0) { + _connections[freeIndex] = connection; + xQueueSend(_workQueue, &freeIndex, 0); + HTTPS_LOGV("Accepted pending connection[%d], FID=%d", freeIndex, newSocket); + } else { + HTTPS_LOGD("Discarded pending connection, FID=%d", newSocket); + delete connection; + } + } else { + // No space in the connection pool, keep it as pending connection + // until it actually sends data (HTTP request or TLS 'hello') + HTTPS_LOGV("Connection is pending, FID=%d", newSocket); + _pendingConnection = connection; + } + } else { + // Discard new connection imediatelly + delete connection; } } + } // if/else (_pendingConnection) + +} // manageConnections() +/** + * Pick up item (connection index) from work queue and + * call connection's loop method to consume the data on the socket + * + * Returns false if there was no work in the queue and timeout expired + */ +bool HTTPServer::doQueuedWork(TickType_t waitDelay) { + int8_t connIndex = -1; + if (xQueueReceive(_workQueue, &connIndex, waitDelay)) { + if ((connIndex >= 0) && xSemaphoreTake(_connectionMutex[connIndex], portMAX_DELAY)) { + HTTPConnection * connection = _connections[connIndex]; + // Work the connection until it runs out of data + while (connection && connection->loop()); + xSemaphoreGive(_connectionMutex[connIndex]); + } + return true; + } + return false; +} + +/** + * The loop method handles processing of data and should be called by periodicaly + * from the main loop when there are no workers enabled + * + * If timeout (in millisecods) is supplied, it will wait for event + * on the 'server soceket' for new connection and/or on established + * connection sockets for closing/error. + * + * Return value is remaining milliseconds if funtion returned early + * + * NOTE: When workers are enabled, calling this method periodically + * is not needed and has no effect. + */ +int HTTPServer::loop(int timeoutMs) { + + // Only handle requests if the server is still running + // and we are handling connections in async mode + if (!_running || (_numWorkers > 0)) { + delay(timeoutMs); + return 0; } + uint32_t startMs = millis(); + + // Step 1: Process existing connections + manageConnections(timeoutMs); + + // Step 2: Complete any remaining work (without waiting) + while (doQueuedWork(0)); + + // Return the remaining time from the timeoutMs requested + uint32_t deltaMs = (startMs + timeoutMs - millis()); + if (deltaMs > 0x7FFFFFFF) deltaMs = 0; + return deltaMs; } -int HTTPServer::createConnection(int idx) { +/** + * Create new connection, initialize headers and return the pointer + */ +HTTPConnection * HTTPServer::createConnection() { HTTPConnection * newConnection = new HTTPConnection(this); - _connections[idx] = newConnection; - return newConnection->initialize(_socket, &_defaultHeaders); + newConnection->initialize(_socket, &_defaultHeaders); + return newConnection; } /** @@ -207,4 +436,8 @@ void HTTPServer::teardownSocket() { _socket = -1; } +int HTTPServer::serverSocket() { + return _socket; +} + } /* namespace httpsserver */ diff --git a/src/HTTPServer.hpp b/src/HTTPServer.hpp index 47746c1..73724cf 100644 --- a/src/HTTPServer.hpp +++ b/src/HTTPServer.hpp @@ -13,6 +13,10 @@ #include "lwip/sockets.h" #include "lwip/inet.h" +// FreeRTOS +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" + // Internal includes #include "HTTPSServerConstants.hpp" #include "HTTPHeaders.hpp" @@ -21,6 +25,7 @@ #include "ResourceResolver.hpp" #include "ResolvedResource.hpp" #include "HTTPConnection.hpp" +#include "HTTPWorker.hpp" namespace httpsserver { @@ -36,11 +41,24 @@ class HTTPServer : public ResourceResolver { void stop(); bool isRunning(); - void loop(); + // Return value is remaining miliseconds if function returned early + int loop(int timeoutMs = 0); void setDefaultHeader(std::string name, std::string value); + // Enable separate FreeRTOS tasks handling for connections. + // Must be called before start() + void enableWorkers( + uint8_t numWorkers = 2, + size_t taskStackSize = HTTPS_CONN_TASK_STACK_SIZE, + int taskPriority = HTTPS_CONN_TASK_PRIORITY + ); + + HTTPHeaders * getDefaultHeaders(); + int serverSocket(); + protected: + friend class HTTPWorker; // Static configuration. Port, keys, etc. ==================== // Certificate that should be used (includes private key) const uint16_t _port; @@ -57,6 +75,18 @@ class HTTPServer : public ResourceResolver { boolean _running; // The server socket int _socket; + + // Keep state if we have pendig connections + HTTPConnection * _pendingConnection = NULL; + bool _pendingData = false; + bool _lookForIdleConnection = false; + + // HTTPWorker(s) and syncronization + uint8_t _numWorkers = 0; + SemaphoreHandle_t _selectMutex = NULL; + SemaphoreHandle_t * _connectionMutex = NULL; + QueueHandle_t _workQueue = NULL; + HTTPWorker ** _workers; // The server socket address, that our service is bound to sockaddr_in _sock_addr; @@ -67,8 +97,14 @@ class HTTPServer : public ResourceResolver { virtual uint8_t setupSocket(); virtual void teardownSocket(); + // Internal functions + void manageConnections(int maxTimeoutMs); + bool doQueuedWork(TickType_t waitDelay); + // Helper functions - virtual int createConnection(int idx); + virtual HTTPConnection * createConnection(); + //int createConnection(int idx); + }; } diff --git a/src/HTTPWorker.cpp b/src/HTTPWorker.cpp new file mode 100644 index 0000000..ccbb70f --- /dev/null +++ b/src/HTTPWorker.cpp @@ -0,0 +1,67 @@ +#include "HTTPWorker.hpp" +#include "HTTPServer.hpp" + +#include "freertos/semphr.h" + +namespace httpsserver { + +HTTPWorker::HTTPWorker(HTTPServer * server, size_t stackSize, int priority) { + _server = server; + BaseType_t taskRes = xTaskCreate(static_task, "HTTPSWorker", stackSize, this, priority, &_handle); + if (taskRes == pdTRUE) { + HTTPS_LOGI("Started connection task %p", _handle); + } else { + HTTPS_LOGE("Error starting connection task"); + _running = false; + } +} + +bool HTTPWorker::isRunning() { + return _running; +} + +void HTTPWorker::run() { + // Run while server is running + while (_server->isRunning()) { + + if (xSemaphoreTake(_server->_selectMutex, 0) == pdTRUE) { + // One worker task will manage the connections + // i.e block on select call + HTTPS_LOGV("Task %p managing connections", _handle); + _server->manageConnections(HTTPS_CONNECTION_TIMEOUT); + xSemaphoreGive(_server->_selectMutex); + } else { + // While others should wait for work from the queue + HTTPS_LOGV("Task %p waiting for work", _handle); + _server->doQueuedWork(portMAX_DELAY); + } + + // Then all tasks complete any remaining work (without waiting) + while (_server->doQueuedWork(0)); + + } // while server->isRunning() + +} // HTTPConnectionTask::run; + +void HTTPWorker::start() { + vTaskResume(_handle); +} + +void HTTPWorker::static_task(void* param) { + HTTPWorker * _this = static_cast(param); + + // Start suspended wait for server to call start() method + vTaskSuspend(NULL); + + // Run the worker + _this->run(); + + HTTPS_LOGI("Shutting down worker task %p", _this->_handle); + _this->_running = false; + // Mark the task for deltetion. + // The FreeRTOS idle task has reposnibilty to cleanup structures and memory + vTaskDelete(NULL); +} + + +} // namespace httpsserver diff --git a/src/HTTPWorker.hpp b/src/HTTPWorker.hpp new file mode 100644 index 0000000..63bc8df --- /dev/null +++ b/src/HTTPWorker.hpp @@ -0,0 +1,45 @@ +#ifndef SRC_HTTPWORKER_HPP_ +#define SRC_HTTPWORKER_HPP_ + +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +#include "HTTPConnection.hpp" + +namespace httpsserver { + +class HTTPServer; // forward declaration + +class HTTPWorker { + +public: + HTTPWorker(HTTPServer * server, size_t stackSize, int priority); + void start(); + bool isRunning(); + +protected: + // Create the instance flagged as running + // If constructor fails to start the actual task, + // or task has ended due to server shutdown, this will be set to false. + bool _running = true; + + // FreeRTOS task handle + TaskHandle_t _handle = NULL; + + // HTTP(S)Server to which this task is attached + HTTPServer * _server = NULL; + + /** + * Worker (FreeRTOS task) main loop + */ + void run(); + + /** + * Static method to start the worker in separate FreeRTOS task + */ + static void static_task(void * param); +}; + +} // end namespace httpsserver + +#endif // SRC_HTTPWORKER_HPP_ \ No newline at end of file diff --git a/src/ResourceResolver.cpp b/src/ResourceResolver.cpp index f6f86b2..588b6ad 100644 --- a/src/ResourceResolver.cpp +++ b/src/ResourceResolver.cpp @@ -58,8 +58,7 @@ void ResourceResolver::resolveNode(const std::string &method, const std::string std::string name = param.substr(0, nvSplitIdx); std::string value = ""; if (nvSplitIdx != std::string::npos) { - // TODO: There may be url encoding in here. - value = param.substr(nvSplitIdx+1); + value = urlDecode(param.substr(nvSplitIdx+1)); } // Now we finally have name and value. @@ -116,7 +115,7 @@ void ResourceResolver::resolveNode(const std::string &method, const std::string // Second step: Grab the parameter value if (nodeIdx == nodepath.length()) { // Easy case: parse until end of string - params->setUrlParameter(pIdx, resourceName.substr(urlIdx)); + params->setUrlParameter(pIdx, urlDecode(resourceName.substr(urlIdx))); } else { // parse until first char after the placeholder char terminatorChar = nodepath[nodeIdx]; @@ -124,7 +123,7 @@ void ResourceResolver::resolveNode(const std::string &method, const std::string if (terminatorPosition != std::string::npos) { // We actually found the terminator size_t dynamicLength = terminatorPosition-urlIdx; - params->setUrlParameter(pIdx, resourceName.substr(urlIdx, dynamicLength)); + params->setUrlParameter(pIdx, urlDecode(resourceName.substr(urlIdx, dynamicLength))); urlIdx = urlIdx + dynamicLength; } else { // We did not find the terminator diff --git a/src/TLSTickets.cpp b/src/TLSTickets.cpp new file mode 100644 index 0000000..8ad2493 --- /dev/null +++ b/src/TLSTickets.cpp @@ -0,0 +1,91 @@ +#include "TLSTickets.hpp" +#include "HTTPSServerConstants.hpp" + +#include "mbedtls/net_sockets.h" + +// Low level SSL implementation on ESP32 +// Copied from esp-idf/components/openssl/platform/ssl_pm.c +struct ssl_pm { + mbedtls_net_context fd; + mbedtls_net_context cl_fd; + mbedtls_ssl_config conf; + mbedtls_ctr_drbg_context ctr_drbg; + mbedtls_ssl_context ssl; + mbedtls_entropy_context entropy; +}; + +namespace httpsserver { + +int TLSTickets::hardware_random(void * p_rng, unsigned char * output, size_t output_len) { + esp_fill_random(output, output_len); + return 0; +} + +TLSTickets::TLSTickets(const char* tag, uint32_t lifetimeSeconds, bool useHWRNG) { + _initOk = false; + _useHWRNG = useHWRNG; + + // Setup TLS tickets context + int ret = -1; + if (_useHWRNG) { + mbedtls_ssl_ticket_init(&_ticketCtx); + ret = mbedtls_ssl_ticket_setup( + &_ticketCtx, + TLSTickets::hardware_random, + NULL, + MBEDTLS_CIPHER_AES_256_GCM, + lifetimeSeconds + ); + } else { + mbedtls_entropy_init(&_entropy); + mbedtls_ctr_drbg_init(&_ctr_drbg); + mbedtls_ssl_ticket_init(&_ticketCtx); + ret = mbedtls_ctr_drbg_seed( + &_ctr_drbg, + mbedtls_entropy_func, + &_entropy, + (unsigned char*)tag, + strlen(tag) + ); + if (ret == 0) { + ret = mbedtls_ssl_ticket_setup( + &_ticketCtx, + mbedtls_ctr_drbg_random, + &_ctr_drbg, + MBEDTLS_CIPHER_AES_256_GCM, + lifetimeSeconds + ); + } + } + if (ret != 0) return; + + _initOk = true; + HTTPS_LOGI("Using TLS session tickets"); +} + +TLSTickets::~TLSTickets() { + if (!_useHWRNG) { + mbedtls_ctr_drbg_free(&_ctr_drbg); + mbedtls_entropy_free(&_entropy); + } + mbedtls_ssl_ticket_free(&_ticketCtx); +} + +bool TLSTickets::enable(SSL * ssl) { + bool res = false; + if (_initOk && ssl && ssl->ssl_pm) { + // Get handle of low-level mbedtls structures for the session + struct ssl_pm * ssl_pm = (struct ssl_pm *) ssl->ssl_pm; + // Configure TLS ticket callbacks using default MbedTLS implementation + mbedtls_ssl_conf_session_tickets_cb( + &ssl_pm->conf, + mbedtls_ssl_ticket_write, + mbedtls_ssl_ticket_parse, + &_ticketCtx + ); + res = true; + } + return res; +} + +} /* namespace httpsserver */ \ No newline at end of file diff --git a/src/TLSTickets.hpp b/src/TLSTickets.hpp new file mode 100644 index 0000000..f9b4be2 --- /dev/null +++ b/src/TLSTickets.hpp @@ -0,0 +1,57 @@ +#ifndef SRC_TLSTICKETS_HPP_ +#define SRC_TLSTICKETS_HPP_ + +#include +#include "mbedtls/entropy.h" +#include "mbedtls/ctr_drbg.h" +#include "mbedtls/ssl_ticket.h" +#include "openssl/ssl.h" + +namespace httpsserver { + +/** + * Enables handling of RFC 5077 TLS session tickets + */ +class TLSTickets { + +public: + TLSTickets(const char* tag, uint32_t liftimeSecs, bool useHWRNG); + ~TLSTickets(); + + /** + * Enables TLS ticket processing for SSL session + */ + bool enable(SSL * ssl); + +protected: + bool _initOk; + bool _useHWRNG; + + /** + * Holds TLS ticket keys + */ + mbedtls_ssl_ticket_context _ticketCtx; + + /** + * mbedTLS random number generator state + */ + mbedtls_entropy_context _entropy; + mbedtls_ctr_drbg_context _ctr_drbg; + + /** + * MbedTLS Random Number Generator using ESP32's hardware RNG + * + * NOTE: Radio (WiFi/Bluetooth) MUST be running for hardware + * entropy to be gathered. Otherwise this function is PRNG! + * + * See more details about esp_random(), here: + * https://docs.espressif.com/projects/esp-idf/en/latest/api-reference/system/system.html + * + */ + static int hardware_random(void * p_rng, unsigned char * output, size_t output_len); + +}; + +} /* namespace httpsserver */ + +#endif // SRC_TLSTICKETS_HPP_ \ No newline at end of file diff --git a/src/util.cpp b/src/util.cpp index 4554f34..275671c 100644 --- a/src/util.cpp +++ b/src/util.cpp @@ -44,7 +44,7 @@ std::string intToString(int i) { return "0"; } // We need this much digits - int digits = ceil(log10(i)); + int digits = ceil(log10(i+1)); char c[digits+1]; c[digits] = '\0'; @@ -58,3 +58,34 @@ std::string intToString(int i) { } } + +std::string urlDecode(std::string input) { + std::size_t idxReplaced = 0; + std::size_t idxFound = input.find('%'); + while (idxFound != std::string::npos) { + if (idxFound <= input.length() + 3) { + char hex[2] = { input[idxFound+1], input[idxFound+2] }; + byte val = 0; + for(int n = 0; n < sizeof(hex); n++) { + val <<= 4; + if ('0' <= hex[n] && hex[n] <= '9') { + val += hex[n]-'0'; + } + else if ('A' <= hex[n] && hex[n] <= 'F') { + val += hex[n]-'A'+10; + } + else if ('a' <= hex[n] && hex[n] <= 'f') { + val += hex[n]-'a'+10; + } + else { + goto skipChar; + } + } + input.replace(idxFound, 3, 1, (char)val); + } + skipChar: + idxReplaced = idxFound + 1; + idxFound = input.find('%', idxReplaced); + } + return input; +} diff --git a/src/util.hpp b/src/util.hpp index 51241e5..07b859d 100644 --- a/src/util.hpp +++ b/src/util.hpp @@ -27,4 +27,9 @@ std::string intToString(int i); } +/** + * \brief **Utility function**: Removes URL encoding from the string (e.g. %20 -> space) + */ +std::string urlDecode(std::string input); + #endif /* SRC_UTIL_HPP_ */