diff --git a/libraries/WebServer/examples/HelloServer/HelloServer.ino b/libraries/WebServer/examples/HelloServer/HelloServer.ino index 1a1180c2593..9facbba3315 100644 --- a/libraries/WebServer/examples/HelloServer/HelloServer.ino +++ b/libraries/WebServer/examples/HelloServer/HelloServer.ino @@ -63,7 +63,64 @@ void setup(void) { }); server.onNotFound(handleNotFound); + ///////////////////////////////////////////////////////// + // Hook examples + server.addHook([](const String & method, const String & url, WiFiClient * client, WebServer::ContentTypeFunction contentType) { + (void)method; // GET, PUT, ... + (void)url; // example: /root/myfile.html + (void)client; // the webserver tcp client connection + (void)contentType; // contentType(".html") => "text/html" + Serial.printf("A useless web hook has passed\n"); + return CLIENT_REQUEST_CAN_CONTINUE; + }); + + server.addHook([](const String&, const String & url, WiFiClient*, WebServer::ContentTypeFunction) { + if (url.startsWith("/fail")) { + Serial.printf("An always failing web hook has been triggered\n"); + return CLIENT_MUST_STOP; + } + return CLIENT_REQUEST_CAN_CONTINUE; + }); + + server.addHook([](const String&, const String & url, WiFiClient * client, WebServer::ContentTypeFunction) { + if (url.startsWith("/dump")) { + Serial.printf("The dumper web hook is on the run\n"); + + // Here the request is not interpreted, so we cannot for sure + // swallow the exact amount matching the full request+content, + // hence the tcp connection cannot be handled anymore by the + // webserver. +#ifdef STREAMTO_API + // we are lucky + client->toWithTimeout(Serial, 500); +#else + auto last = millis(); + while ((millis() - last) < 500) { + char buf[32]; + size_t len = client->read((uint8_t*)buf, sizeof(buf)); + if (len > 0) { + Serial.printf("(<%d> chars)", (int)len); + Serial.write(buf, len); + last = millis(); + } + } +#endif + // Two choices: return MUST STOP and webserver will close it + // (we already have the example with '/fail' hook) + // or IS GIVEN and webserver will forget it + // trying with IS GIVEN and storing it on a dumb WiFiClient. + // check the client connection: it should not immediately be closed + // (make another '/dump' one to close the first) + Serial.printf("\nTelling server to forget this connection\n"); + static WiFiClient forgetme = *client; // stop previous one if present and transfer client refcounter + return CLIENT_IS_GIVEN; + } + return CLIENT_REQUEST_CAN_CONTINUE; + }); + + // Hook examples + ///////////////////////////////////////////////////////// server.begin(); Serial.println("HTTP server started"); } diff --git a/libraries/WebServer/src/Parsing.cpp b/libraries/WebServer/src/Parsing.cpp index 1db2aef5541..5b1a6e49a8a 100644 --- a/libraries/WebServer/src/Parsing.cpp +++ b/libraries/WebServer/src/Parsing.cpp @@ -65,13 +65,13 @@ static char* readBytesWithTimeout(WiFiClient& client, size_t maxLength, size_t& return buf; } -bool WebServer::_parseRequest(WiFiClient& client) { +ClientFuture WebServer::_parseRequest(WiFiClient& client) { // Read the first line of HTTP request String req = client.readStringUntil('\r'); client.readStringUntil('\n'); //reset header value for (int i = 0; i < _headerKeysCount; ++i) { - _currentHeaders[i].value =String(); + _currentHeaders[i].value.clear(); } // First line of HTTP request looks like "GET /path HTTP/1.1" @@ -80,7 +80,7 @@ bool WebServer::_parseRequest(WiFiClient& client) { int addr_end = req.indexOf(' ', addr_start + 1); if (addr_start == -1 || addr_end == -1) { log_e("Invalid request: %s", req.c_str()); - return false; + return CLIENT_MUST_STOP; } String methodStr = req.substring(0, addr_start); @@ -95,7 +95,12 @@ bool WebServer::_parseRequest(WiFiClient& client) { } _currentUri = url; _chunked = false; - + if (_hook) + { + auto whatNow = _hook(methodStr, url, &client, mime::getContentType); + if (whatNow != CLIENT_REQUEST_CAN_CONTINUE) + return whatNow; + } HTTPMethod method = HTTP_GET; if (methodStr == F("POST")) { method = HTTP_POST; @@ -170,7 +175,7 @@ bool WebServer::_parseRequest(WiFiClient& client) { char* plainBuf = readBytesWithTimeout(client, contentLength, plainLength, HTTP_MAX_POST_WAIT); if (plainLength < contentLength) { free(plainBuf); - return false; + return CLIENT_MUST_STOP; } if (contentLength > 0) { if(isEncoded){ @@ -197,7 +202,7 @@ bool WebServer::_parseRequest(WiFiClient& client) { if (isForm){ _parseArguments(searchStr); if (!_parseForm(client, boundaryStr, contentLength)) { - return false; + return CLIENT_MUST_STOP; } } } else { @@ -230,7 +235,7 @@ bool WebServer::_parseRequest(WiFiClient& client) { log_v("Request: %s", url.c_str()); log_v(" Arguments: %s", searchStr.c_str()); - return true; + return CLIENT_REQUEST_CAN_CONTINUE; } bool WebServer::_collectHeader(const char* headerName, const char* headerValue) { diff --git a/libraries/WebServer/src/WebServer.cpp b/libraries/WebServer/src/WebServer.cpp index 35d316308df..f4c901d4436 100644 --- a/libraries/WebServer/src/WebServer.cpp +++ b/libraries/WebServer/src/WebServer.cpp @@ -306,34 +306,53 @@ void WebServer::handleClient() { case HC_WAIT_READ: // Wait for data from client to become available if (_currentClient.available()) { - if (_parseRequest(_currentClient)) { + switch (_parseRequest(_currentClient)) + { + case CLIENT_REQUEST_CAN_CONTINUE: // because HTTP_MAX_SEND_WAIT is expressed in milliseconds, // it must be divided by 1000 - _currentClient.setTimeout(HTTP_MAX_SEND_WAIT / 1000); - _contentLength = CONTENT_LENGTH_NOT_SET; - _handleRequest(); - -// Fix for issue with Chrome based browsers: https://github.com/espressif/arduino-esp32/issues/3652 -// if (_currentClient.connected()) { -// _currentStatus = HC_WAIT_CLOSE; -// _statusChange = millis(); -// keepCurrentClient = true; -// } - } + _currentClient.setTimeout(HTTP_MAX_SEND_WAIT / 1000); + _contentLength = CONTENT_LENGTH_NOT_SET; + _handleRequest(); + /* fallthrough */ + case CLIENT_REQUEST_IS_HANDLED://log_v + if (_currentClient.connected() || _currentClient.available()) { + _currentStatus = HC_WAIT_CLOSE; + _statusChange = millis(); + keepCurrentClient = true; + } else + log_v("webserver: peer has closed after served"); + break; + case CLIENT_MUST_STOP: + log_v("Close client\n"); + _currentClient.stop(); + break; + case CLIENT_IS_GIVEN: + // client must not be stopped but must not be handled here anymore + // (example: tcp connection given to websocket) + log_v("Give client\n"); + break; + } // switch _parseRequest() } else { // !_currentClient.available() if (millis() - _statusChange <= HTTP_MAX_DATA_WAIT) { keepCurrentClient = true; } - callYield = true; + else + log_v("webserver: closing after read timeout\n"); + callYield = true; } break; case HC_WAIT_CLOSE: // Wait for client to close the connection - if (millis() - _statusChange <= HTTP_MAX_CLOSE_WAIT) { + if (!_server.available() && (millis() - _statusChange <= HTTP_MAX_CLOSE_WAIT)) { keepCurrentClient = true; callYield = true; + if (_currentClient.available()) + // continue serving current client + _currentStatus = HC_WAIT_READ; } - } + break; + } // switch _currentStatus } if (!keepCurrentClient) { diff --git a/libraries/WebServer/src/WebServer.h b/libraries/WebServer/src/WebServer.h index c169da82229..8278b8de90a 100644 --- a/libraries/WebServer/src/WebServer.h +++ b/libraries/WebServer/src/WebServer.h @@ -34,6 +34,7 @@ enum HTTPUploadStatus { UPLOAD_FILE_START, UPLOAD_FILE_WRITE, UPLOAD_FILE_END, UPLOAD_FILE_ABORTED }; enum HTTPClientStatus { HC_NONE, HC_WAIT_READ, HC_WAIT_CLOSE }; enum HTTPAuthMethod { BASIC_AUTH, DIGEST_AUTH }; +enum ClientFuture { CLIENT_REQUEST_CAN_CONTINUE, CLIENT_REQUEST_IS_HANDLED, CLIENT_MUST_STOP, CLIENT_IS_GIVEN }; #define HTTP_DOWNLOAD_UNIT_SIZE 1436 @@ -49,6 +50,8 @@ enum HTTPAuthMethod { BASIC_AUTH, DIGEST_AUTH }; #define CONTENT_LENGTH_UNKNOWN ((size_t) -1) #define CONTENT_LENGTH_NOT_SET ((size_t) -2) +#define WEBSERVER_HAS_HOOK 1 + class WebServer; typedef struct { @@ -73,7 +76,8 @@ class WebServer WebServer(IPAddress addr, int port = 80); WebServer(int port = 80); virtual ~WebServer(); - + typedef String (*ContentTypeFunction) (const String&); + using HookFunction = std::function; virtual void begin(); virtual void begin(uint16_t port); virtual void handleClient(); @@ -141,14 +145,26 @@ class WebServer _streamFileCore(file.size(), file.name(), contentType); return _currentClient.write(file); } - + void addHook (HookFunction hook) { + if (_hook) { + auto previousHook = _hook; + _hook = [previousHook, hook](const String& method, const String& url, WiFiClient* client, ContentTypeFunction contentType) { + auto whatNow = previousHook(method, url, client, contentType); + if (whatNow == CLIENT_REQUEST_CAN_CONTINUE) + return hook(method, url, client, contentType); + return whatNow; + }; + } else { + _hook = hook; + } + } protected: virtual size_t _currentClientWrite(const char* b, size_t l) { return _currentClient.write( b, l ); } virtual size_t _currentClientWrite_P(PGM_P b, size_t l) { return _currentClient.write_P( b, l ); } void _addRequestHandler(RequestHandler* handler); void _handleRequest(); void _finalizeResponse(); - bool _parseRequest(WiFiClient& client); + ClientFuture _parseRequest(WiFiClient& client); void _parseArguments(String data); static String _responseCodeToString(int code); bool _parseForm(WiFiClient& client, String boundary, uint32_t len); @@ -159,7 +175,7 @@ class WebServer bool _collectHeader(const char* headerName, const char* headerValue); void _streamFileCore(const size_t fileSize, const String & fileName, const String & contentType); - + String _getRandomHexString(); // for extracting Auth parameters String _extractParam(String& authReq,const String& param,const char delimit = '"'); @@ -204,7 +220,7 @@ class WebServer String _snonce; // Store noance and opaque for future comparison String _sopaque; String _srealm; // Store the Auth realm between Calls - + HookFunction _hook; }; diff --git a/libraries/WebServer/src/detail/RequestHandlersImpl.h b/libraries/WebServer/src/detail/RequestHandlersImpl.h index 27a2a8d328a..4c16b2ffe19 100644 --- a/libraries/WebServer/src/detail/RequestHandlersImpl.h +++ b/libraries/WebServer/src/detail/RequestHandlersImpl.h @@ -102,7 +102,7 @@ class StaticRequestHandler : public RequestHandler { } log_v("StaticRequestHandler::handle: path=%s, isFile=%d\r\n", path.c_str(), _isFile); - String contentType = getContentType(path); + String contentType = mime::getContentType(path); // look for gz file, only if the original specified path is not a gz. So part only works to send gzip via content encoding when a non compressed is asked for // if you point the the path to gzip you will serve the gzip as content type "application/x-gzip", not text or javascript etc... @@ -123,21 +123,6 @@ class StaticRequestHandler : public RequestHandler { return true; } - static String getContentType(const String& path) { - char buff[sizeof(mimeTable[0].mimeType)]; - // Check all entries but last one for match, return if found - for (size_t i=0; i < sizeof(mimeTable)/sizeof(mimeTable[0])-1; i++) { - strcpy_P(buff, mimeTable[i].endsWith); - if (path.endsWith(buff)) { - strcpy_P(buff, mimeTable[i].mimeType); - return String(buff); - } - } - // Fall-through and just return default type - strcpy_P(buff, mimeTable[sizeof(mimeTable)/sizeof(mimeTable[0])-1].mimeType); - return String(buff); - } - protected: FS _fs; String _uri; diff --git a/libraries/WebServer/src/detail/mimetable.cpp b/libraries/WebServer/src/detail/mimetable.cpp index 563556a6358..b4957943fc6 100644 --- a/libraries/WebServer/src/detail/mimetable.cpp +++ b/libraries/WebServer/src/detail/mimetable.cpp @@ -1,5 +1,6 @@ #include "mimetable.h" #include "pgmspace.h" +#include "WString.h" namespace mime { @@ -32,4 +33,19 @@ const Entry mimeTable[maxType] = { "", "application/octet-stream" } }; + String getContentType(const String& path) { + char buff[sizeof(mimeTable[0].mimeType)]; + // Check all entries but last one for match, return if found + for (size_t i=0; i < sizeof(mimeTable)/sizeof(mimeTable[0])-1; i++) { + strcpy_P(buff, mimeTable[i].endsWith); + if (path.endsWith(buff)) { + strcpy_P(buff, mimeTable[i].mimeType); + return String(buff); + } + } + // Fall-through and just return default type + strcpy_P(buff, mimeTable[sizeof(mimeTable)/sizeof(mimeTable[0])-1].mimeType); + return String(buff); + } + } diff --git a/libraries/WebServer/src/detail/mimetable.h b/libraries/WebServer/src/detail/mimetable.h index 191356c489a..63564bae29b 100644 --- a/libraries/WebServer/src/detail/mimetable.h +++ b/libraries/WebServer/src/detail/mimetable.h @@ -1,6 +1,6 @@ #ifndef __MIMETABLE_H__ #define __MIMETABLE_H__ - +class String; namespace mime { @@ -41,6 +41,7 @@ struct Entry extern const Entry mimeTable[maxType]; +String getContentType(const String& path); }