diff --git a/.gitignore b/.gitignore index 600d2d3..42f8df3 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -.vscode \ No newline at end of file +.vscode +.pio \ No newline at end of file diff --git a/README.md b/README.md index 4905f01..4d6b049 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# esp32_https_server_compat +# esp32\_https\_server\_compat This library is a wrapper around the [TLS-enabled web server for the ESP32 using the Arduino core](https://github.com/fhessel/esp32_https_server), to make it compatible with the [default Webserver API](https://github.com/espressif/arduino-esp32/tree/master/libraries/WebServer). @@ -8,7 +8,7 @@ The setup depends on the IDE that you're using. ### Using PlatformIO (recommended) -If you're using PlatformIO, just add esp32_https_server_compat to the library depenendencies in your platform.ini: +If you're using PlatformIO, just add `esp32\_https\_server\_compat` to the library depenendencies in your `platform.ini`: ```ini [env:myenv] @@ -37,24 +37,18 @@ void loop() { } ``` -More information and examples can be found in the default WebServer's [repository](https://github.com/espressif/arduino-esp32/tree/master/libraries/WebServer). +To use the HTTPS server use `` and `ESPWebServerSecure` in stead of `ESPWebServer`. + +More information and examples can be found in the default WebServer's [repository](https://github.com/espressif/arduino-esp32/tree/master/libraries/WebServer). There are two minimal examples (more test programs, really) in the [examples](examples) directory. ## State of Development -This wrapper is still very WIP. - -| Function | State | Comment | -| -------- | ----- | ------- | -| Starting and stopping the server | ✅ | (but not tested) | -| Handling basic requests | ✅ | `on(...)` | -| Handling 404 | ✅ | `onNotFound(...)` | -| Providing access to request properties | ✅ | `uri()`, `method()` | -| Handling file uploads | ❌ | `onFileUpload(...)`, `upload()`, and `on()` with 4 parameters | -| Handling headers | ❌ | `header()`, `headerName()`, `headers()` etc. | -| Handling arguments | ❌ | `arg()`, `argName()`, `hasArg()` etc. | -| Handling forms | ❌ | Needs [esp32_https_server#29](https://github.com/fhessel/esp32_https_server/issues/29) first. | -| Sending responses | ❌ | `send()` etc. | -| CORS and cross-origin | ❌ | Needs headers first | -| Streaming files | ❌ | `streamFile()` | -| `FS` support | ❌ | | -| TLS | ❌ | Needs `ESPWebServerSecure` that extends `ESPWebServer` | +The following issues are known: + +- `serveStatic()` will serve only a single file or a single directory (as opposed to serving a whole subtree in the default WebServer). +- `serveStatic()` does not implement automatic gzip support. +- `serveStatic()` knows about only a limited set of mimetypes for file extensions. +- `authenticate()` and `requestAuthentication()` handle only `Basic` authentication, not `Digest` authentication. +- `sendHeader()` ignores the `first=true` parameter. +- `collectHeaders()` is not implemented. +- Handling of `POST` forms with mimetype `application/x-www-form-urlencoded` is memory-inefficient: the whole POST body is loaded into memory twice. \ No newline at end of file diff --git a/examples/FormServer/FormServer.ino b/examples/FormServer/FormServer.ino new file mode 100644 index 0000000..c20c661 --- /dev/null +++ b/examples/FormServer/FormServer.ino @@ -0,0 +1,223 @@ +#include +#include +#include +#include +#ifdef USE_DEFAULT_WEBSERVER +#include +typedef WebServer ESPWebServer; +#else +#include +#endif + +const char* ssid = "........"; +const char* password = "........"; + +ESPWebServer server(80); + +static std::string htmlEncode(std::string data) { + 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 handleRoot() { + String rv; + rv += ""; + rv += ""; + rv += "Very simple file server"; + rv += ""; + rv += "

Very simple file server

"; + rv += "

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

"; + rv += "

List existing files

"; + rv += "

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

"; + rv += "

Upload new file

"; + rv += "

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

"; + rv += "
"; + rv += "file:
"; + rv += ""; + rv += "
"; + rv += ""; + rv += ""; + server.send(200, "text/html", rv); +} + +void handleFormEdit() { + if (!server.authenticate("admin", "admin")) return server.requestAuthentication(); + bool hasFilename = server.hasArg("filename");; + String filename = server.arg("filename"); + String pathname = String("/public/") + filename; + if (server.hasArg("content") ){ + // Content has been edited. Save. + File file = SPIFFS.open(pathname.c_str(), "w"); + String content = server.arg("content"); + file.write((uint8_t *)content.c_str(), content.length()); + file.close(); + } + String content = server.arg("content"); + String rv; + rv += "Edit File\n"; + File file = SPIFFS.open(pathname.c_str()); + if (!hasFilename) { + rv += "

No filename specified.

\n"; + } else if (!file.available()) { + rv += "

File not found:"; + rv += pathname; + rv += "

\n"; + } else { + rv += "

Edit content of "; + rv += pathname; + rv += "

\n"; + rv += "
\n"; + rv += "\n"; + rv += "
"; + rv += ""; + rv += "
"; + } + rv += ""; + server.send(200, "text/html", rv); +} + +void handleFormUpload() { + Serial.println("handleUpload() called"); + server.send(200); +} + +File fsUploadFile; + +void handleFormUploadFile() { + Serial.println("handleUploadFile() called"); + HTTPUpload& upload = server.upload(); + if(upload.status == UPLOAD_FILE_START){ + String filename = String("/public/") + upload.filename; + Serial.printf("upload filename=%s\n", filename.c_str()); + Serial.print("handleFileUpload Name: "); Serial.println(filename); + fsUploadFile = SPIFFS.open(filename, "w"); // Open the file for writing in SPIFFS (create if it doesn't exist) + } else if(upload.status == UPLOAD_FILE_WRITE){ + Serial.printf("uploaded %d bytes\n", upload.currentSize); + if(fsUploadFile) + fsUploadFile.write(upload.buf, upload.currentSize); // Write the received bytes to the file + } else if(upload.status == UPLOAD_FILE_END){ + Serial.printf("upload total %d bytes\n", upload.totalSize); + if(fsUploadFile) { // If the file was successfully created + fsUploadFile.close(); // Close the file again + Serial.print("handleFileUpload Size: "); Serial.println(upload.totalSize); + server.sendHeader("Location","/public"); // Redirect the client to the success page + server.send(303); + } else { + server.send(500, "text/plain", "500: couldn't create file"); + } + server.send(200, "text/plain", "OK"); + } +} + +void handleDirectory() { + String rv; + rv += "File Listing\n"; + File d = SPIFFS.open("/public"); + if (!d.isDirectory()) { + rv += "

No files found.

\n"; + } else { + rv += "

File Listing

\n"; + rv += "
    \n"; + File f = d.openNextFile(); + while (f) { + std::string pathname(f.name()); + std::string filename = pathname.substr(8); // Remove /public/ + rv += "
  • "; + rv += String(filename.c_str()); + rv += ""; + if (pathname.rfind(".txt") != std::string::npos) { + rv += " [edit]"; + } + rv += "
  • "; + f = d.openNextFile(); + } + rv += "
"; + } + rv += ""; + server.send(200, "text/html", rv); +} + +void handleNotFound() { + String message = "File Not Found\n\n"; + message += "URI: "; + message += server.uri(); + message += "\nMethod: "; + message += (server.method() == HTTP_GET) ? "GET" : "POST"; + message += "\nArguments: "; + message += server.args(); + message += "\n"; + for (uint8_t i = 0; i < server.args(); i++) { + message += " " + server.argName(i) + ": " + server.arg(i) + "\n"; + } + server.send(404, "text/plain", message); +} + +void setup(void) { + Serial.begin(115200); + WiFi.mode(WIFI_STA); + WiFi.begin(ssid, password); + Serial.println(""); + + // Wait for connection + while (WiFi.status() != WL_CONNECTED) { + delay(500); + Serial.print("."); + } + Serial.println(""); + Serial.print("Connected to "); + Serial.println(ssid); + Serial.print("IP address: "); + Serial.println(WiFi.localIP()); + + if (MDNS.begin("esp32")) { + Serial.println("MDNS responder started"); + } + // Setup filesystem + if (!SPIFFS.begin(true)) Serial.println("Mounting SPIFFS failed"); + + server.on("/", handleRoot); + server.on("/upload", HTTP_POST, handleFormUpload, handleFormUploadFile); + server.on("/edit", HTTP_GET, handleFormEdit); + server.on("/edit", HTTP_POST, handleFormEdit); + // Note: /public (without trailing /) gives directory listing, but /public/... retrieves static files. + server.on("/public", HTTP_GET, handleDirectory); + server.serveStatic("/public/", SPIFFS, "/public/"); + + server.onNotFound(handleNotFound); + + server.begin(); + Serial.println("HTTP server started"); +} + +void loop(void) { + server.handleClient(); +} diff --git a/examples/HelloServer/HelloServer.ino b/examples/HelloServer/HelloServer.ino index 0388358..ceefd72 100644 --- a/examples/HelloServer/HelloServer.ino +++ b/examples/HelloServer/HelloServer.ino @@ -1,7 +1,12 @@ #include #include -#include #include +#ifdef USE_DEFAULT_WEBSERVER +#include +typedef WebServer ESPWebServer; +#else +#include +#endif const char* ssid = "........"; const char* password = "........"; @@ -12,10 +17,44 @@ const int led = 13; void handleRoot() { digitalWrite(led, 1); - server.send(200, "text/plain", "hello from esp32!"); + server.send(200, "text/plain", "hello from esp32! See /form and /inline too!"); digitalWrite(led, 0); } +void handleForm() { + String line = server.arg("line"); + Serial.print("line: "); + Serial.println(line); + String multi = server.arg("multi"); + Serial.print("multi: "); + Serial.println(multi); + line.toLowerCase(); + multi.toUpperCase(); + String rv; + rv = "Test Form"; + rv += "

Single line will be converted to lowercase, multi line to uppercase. You can submit the form with three different methods.

"; + rv += "

Form using GET

"; + rv += "
"; + rv += "Single line:

"; + rv += "Multi line:

"; + rv += ""; + rv += "
"; + rv += "

Form using POST urlencoded

"; + rv += "
"; + rv += "Single line:

"; + rv += "Multi line:

"; + rv += ""; + rv += "
"; + rv += "

Form using POST with multipart

"; + rv += "
"; + rv += "Single line:

"; + rv += "Multi line:

"; + rv += ""; + rv += "
"; + server.send(200, "text/html", rv); +} + + void handleNotFound() { digitalWrite(led, 1); String message = "File Not Found\n\n"; @@ -57,7 +96,8 @@ void setup(void) { } server.on("/", handleRoot); - + server.on("/form", handleForm); + server.on("/form", HTTP_POST, handleForm); server.on("/inline", []() { server.send(200, "text/plain", "this works as well"); }); diff --git a/library.json b/library.json index b3240f7..ebfb9da 100644 --- a/library.json +++ b/library.json @@ -5,6 +5,11 @@ "name": "Frank Hessel", "email": "frank@fhessel.de", "maintainer": true + }, + { + "name": "Jack Jansen", + "email": "Jack.Jansen@cwi.nl", + "maintainer": true } ], "keywords": "communication, esp32, http, https, server, ssl, tls, webserver, websockets", @@ -17,7 +22,7 @@ "dependencies": [ { "name": "esp32_https_server", - "version": "0.3.0" + "version": "~1.0.0" } ], "license": "MIT", diff --git a/library.properties b/library.properties index cf8a48f..a6a0699 100644 --- a/library.properties +++ b/library.properties @@ -1,6 +1,6 @@ name=ESP32 HTTP(S) Webserver (Compatibility Layer) version=0.3.0 -author=Frank Hessel +author=Frank Hessel , Jack Jansen maintainer=Frank Hessel sentence=An Arduino library for an alternative ESP32 HTTP/HTTPS web server implementation paragraph=This library is a wrapper around esp32_https_server that provides the same API as the default Webserver library. diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..2ad1e78 --- /dev/null +++ b/platformio.ini @@ -0,0 +1,50 @@ +[platformio] +default_envs = lolin32-FormServer + +[env:lolin32-FormServer-minimodule] +framework = arduino +platform = espressif32@>=1.11 +board = lolin32 +lib_ldf_mode = deep+ +lib_deps = esp32_https_server@~1.0.0 +build_flags = -DHTTPS_LOGLEVEL=4 -DHTTPS_REQUEST_MAX_REQUEST_LENGTH=800 +src_filter = +<*> +<../examples/FormServer/> +debug_tool = minimodule +upload_protocol = minimodule +monitor_speed = 115200 +upload_speed = 115200 + +[env:lolin32-FormServer] +framework = arduino +platform = espressif32@>=1.11 +board = lolin32 +lib_ldf_mode = deep+ +lib_deps = esp32_https_server@~1.0.0 +build_flags = -DHTTPS_LOGLEVEL=4 -DHTTPS_REQUEST_MAX_REQUEST_LENGTH=800 +src_filter = +<*> +<../examples/FormServer/> +monitor_speed = 115200 +upload_speed = 115200 + +[env:lolin32-HelloServer-minimodule] +framework = arduino +platform = espressif32@>=1.11 +board = lolin32 +lib_ldf_mode = deep+ +lib_deps = esp32_https_server@~1.0.0 +build_flags = -DHTTPS_LOGLEVEL=4 -DHTTPS_REQUEST_MAX_REQUEST_LENGTH=800 +src_filter = +<*> +<../examples/HelloServer/> +debug_tool = minimodule +upload_protocol = minimodule +monitor_speed = 115200 +upload_speed = 115200 + +[env:lolin32-HelloServer] +framework = arduino +platform = espressif32@>=1.11 +board = lolin32 +lib_ldf_mode = deep+ +lib_deps = esp32_https_server@~1.0.0 +build_flags = -DHTTPS_LOGLEVEL=4 -DHTTPS_REQUEST_MAX_REQUEST_LENGTH=800 +src_filter = +<*> +<../examples/HelloServer/> +monitor_speed = 115200 +upload_speed = 115200 diff --git a/src/ESPWebServer.cpp b/src/ESPWebServer.cpp index 5b961b3..0d80ec8 100644 --- a/src/ESPWebServer.cpp +++ b/src/ESPWebServer.cpp @@ -1,11 +1,20 @@ #include "ESPWebServer.hpp" +#include +#include +#include +#include +#include using namespace httpsserver; /* Copy the content of Arduino String s into a newly allocated char array p */ #define ARDUINOTONEWCHARARR(s,p) {size_t sLen=s.length()+1;char *c=new char[sLen];c[sLen-1]=0;s.toCharArray(p,sLen);p=c;} +class BodyResourceParameters : public ResourceParameters { + friend class ESPWebServer; +}; + struct { int val; char text[8]; @@ -19,13 +28,35 @@ struct { {HTTP_OPTIONS, "OPTIONS"}, }; +// 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] = { + {".html", "text/html"}, + {".txt", "text/plain"}, + {".css", "text/css"}, + {".js", "application/javascript"}, + {".json", "application/json"}, + {".png", "image/png"}, + {".jpg", "image/jpg"}, + {"", ""} +}; + +ESPWebServer::ESPWebServer(HTTPServer *server) : + _server(server), + _contentLength(0) +{ + _notFoundNode = nullptr; +} + ESPWebServer::ESPWebServer(IPAddress addr, int port) : - _server(HTTPServer(port, 4, addr)) { + _server(new HTTPServer(port, 4, addr)), + _contentLength(0) +{ _notFoundNode = nullptr; } ESPWebServer::ESPWebServer(int port) : - _server(HTTPServer(port, 4)) { + _server(new HTTPServer(port, 4)) { _notFoundNode = nullptr; } @@ -36,27 +67,63 @@ ESPWebServer::~ESPWebServer() { } void ESPWebServer::begin() { - _server.start(); + _server->start(); } void ESPWebServer::handleClient() { - _server.loop(); + _server->loop(); } void ESPWebServer::close() { - // TODO + _server->stop(); } void ESPWebServer::stop() { - _server.stop(); + _server->stop(); } bool ESPWebServer::authenticate(const char * username, const char * password) { + std::string authHeader = _activeRequest->getHeader("Authorization"); + if (authHeader == "") return false; + if (authHeader.substr(0, 5) == "Basic") { + std::string reqUser = _activeRequest->getBasicAuthUser(); + std::string reqPassword = _activeRequest->getBasicAuthPassword(); + return (username == reqUser && password == reqPassword); + } else if (authHeader.substr(0, 6) == "Digest") { + HTTPS_LOGE("Only BASIC_AUTH implemented"); +#if 0 + std::string authReq = authHeader.substr(6); + std::string realm = _extractParam(authReq, "realm=\""); + std::string hash = credentialHash(username, _realm, password); + return authenticateDigest(username, hash); +#endif + } return false; } void ESPWebServer::requestAuthentication(HTTPAuthMethod mode, const char* realm, const String& authFailMsg) { - // TODO + if (realm == NULL) realm = "Login Required"; + if (mode == BASIC_AUTH) { + std::string authArg = "Basic realm=\""; + authArg += realm; + authArg += "\""; + _activeResponse->setHeader("WWW-Authenticate", authArg); + } else { + HTTPS_LOGE("Only BASIC_AUTH implemented"); +#if 0 + _snonce = _getRandomHexString(); + _sopaque = _getRandomHexString(); + std::string authArg = "Digest realm=\""; + authArg += realm; + authArg += "\", qop=\"auth\", nonce=\""; + authArg += _snonce; + authArg += "\", opaque=\""; + authArg += _sopaque; + authArg += "\""; + _activeResponse->setHeader("WWW-Authenticate", authArg); +#endif + } + send(401, "text/html", authFailMsg); } void ESPWebServer::on(const String &uri, THandlerFunction handler) { @@ -64,37 +131,45 @@ void ESPWebServer::on(const String &uri, THandlerFunction handler) { } void ESPWebServer::on(const String &uri, HTTPMethod method, THandlerFunction fn) { + on(uri, method, fn, _uploadHandler); +} + +void ESPWebServer::on(const String &uri, HTTPMethod method, THandlerFunction fn, THandlerFunction ufn) { // TODO: Handle HTTP_ANY - char *methodname = METHODNAMES[0].text; - for (size_t n = 1; n < sizeof(METHODNAMES); n++) { + // TODO: convert {} in uri to * (so pathArg() will work) + const char *methodname = "???"; + for (size_t n = 0; n < sizeof(METHODNAMES); n++) { if (METHODNAMES[n].val == method) { methodname = METHODNAMES[n].text; break; } } - ESPWebServerNode *node = new ESPWebServerNode(this,std::string(uri.c_str()), std::string(methodname),fn); - _server.registerNode(node); -} - -void ESPWebServer::on(const String &uri, HTTPMethod method, THandlerFunction fn, THandlerFunction ufn) { - // TODO - // ufn handles uploads + ESPWebServerNode *node = new ESPWebServerNode(this,std::string(uri.c_str()), std::string(methodname),fn, ufn); + _server->registerNode(node); } void ESPWebServer::serveStatic(const char* uri, fs::FS& fs, const char* path, const char* cache_header) { - // TODO + std::string wsUri(uri); + std::string wsPath(path); + if (wsUri[wsUri.length()-1] == '/') { + // serving a whole directory + wsUri += "*"; + if (wsPath[wsPath.length()-1] != '/') wsPath += "/"; + } + ESPWebServerStaticNode *node = new ESPWebServerStaticNode(this, wsUri, fs, wsPath, std::string(cache_header?cache_header:"")); + _server->registerNode(node); } void ESPWebServer::onNotFound(THandlerFunction fn) { if (_notFoundNode != nullptr) { delete _notFoundNode; } - _notFoundNode = new ESPWebServerNode(this, "", "", fn); - _server.setDefaultNode(_notFoundNode); + _notFoundNode = new ESPWebServerNode(this, "", "", fn, THandlerFunction()); + _server->setDefaultNode(_notFoundNode); } void ESPWebServer::onFileUpload(THandlerFunction fn) { - // TODO + _uploadHandler = fn; } String ESPWebServer::uri() { @@ -111,79 +186,135 @@ HTTPMethod ESPWebServer::method() { } HTTPUpload& ESPWebServer::upload() { - // TODO - HTTPUpload upload; - return upload; + return *_activeUpload; } String ESPWebServer::pathArg(unsigned int i) { - // TODO - return ""; + auto rv = _activeParams->getPathParameter(i); + return String(rv.c_str()); } String ESPWebServer::arg(String name) { - // TODO - return ""; + // Special case: arg("plain") returns the body of non-multipart requests. + if (name == "plain") { + bool isForm = false; + HTTPHeaders* headers = _activeRequest->getHTTPHeaders(); + HTTPHeader* ctHeader = headers->get("Content-Type"); + if (ctHeader && ctHeader->_value.substr(0, 10) == "multipart/") { + isForm = true; + } + if (!isForm) { + size_t bodyLength = _activeRequest->getContentLength(); + String rv; + rv.reserve(bodyLength); + char buffer[257]; + while(!_activeRequest->requestComplete()) { + size_t readLength = _activeRequest->readBytes((byte*)buffer, 256); + if (readLength <= 0) break; + buffer[readLength] = 0; + rv += buffer; + } + return rv; + } + } + std::string value; + _activeParams->getQueryParameter(std::string(name.c_str()), value); + return String(value.c_str()); } String ESPWebServer::arg(int i) { - // TODO + int idx=0; + for (auto it=_activeParams->beginQueryParameters(); it != _activeParams->endQueryParameters(); it++, idx++) { + if (idx == i) + return String(it->second.c_str()); + } return ""; } String ESPWebServer::argName(int i) { - // TODO + int idx=0; + for (auto it=_activeParams->beginQueryParameters(); it != _activeParams->endQueryParameters(); it++, idx++) { + if (idx == i) + return String(it->first.c_str()); + } return ""; } int ESPWebServer::args() { - // TODO - return 0; + return _activeParams->getQueryParameterCount(); } bool ESPWebServer::hasArg(String name) { - // TODO - return false; + bool rv = _activeParams->isQueryParameterSet(std::string(name.c_str())); + return rv; } void ESPWebServer::collectHeaders(const char* headerKeys[], const size_t headerKeysCount) { - // TODO + HTTPS_LOGE("collectHeaders() not implemented"); } String ESPWebServer::header(String name) { - // TODO + HTTPHeaders* headers = _activeRequest->getHTTPHeaders(); + HTTPHeader* header = headers->get(std::string(name.c_str())); + if (header) { + return String(header->_value.c_str()); + } return ""; } String ESPWebServer::header(int i) { - // TODO + HTTPHeaders* headers = _activeRequest->getHTTPHeaders(); + auto allHeaders = headers->getAll(); + if (i >= 0 && i < allHeaders->size()) { + HTTPHeader* header = allHeaders->at(i); + return String(header->_value.c_str()); + } return ""; } String ESPWebServer::headerName(int i) { - // TODO + HTTPHeaders* headers = _activeRequest->getHTTPHeaders(); + auto allHeaders = headers->getAll(); + if (i >= 0 && i < allHeaders->size()) { + HTTPHeader* header = allHeaders->at(i); + return String(header->_name.c_str()); + } return ""; } int ESPWebServer::headers() { - // TODO - return 0; + HTTPHeaders* headers = _activeRequest->getHTTPHeaders(); + auto allHeaders = headers->getAll(); + return allHeaders->size(); } bool ESPWebServer::hasHeader(String name) { - // TODO - return false; + HTTPHeaders* headers = _activeRequest->getHTTPHeaders(); + HTTPHeader* header = headers->get(std::string(name.c_str())); + return header != NULL; } String ESPWebServer::hostHeader() { - // TODO - return ""; + return header("Host"); +} + +void ESPWebServer::_prepareStreamFile(size_t fileSize, const String& contentType) { + _contentLength = fileSize; + _activeResponse->setStatusCode(200); + _activeResponse->setHeader("Content-Type", contentType.c_str()); + _standardHeaders(); } void ESPWebServer::send(int code, const char* content_type, const String& content) { + if (_contentLength == CONTENT_LENGTH_NOT_SET) _contentLength = content.length(); _activeResponse->setStatusCode(code); - _activeResponse->setHeader("Content-Type", content_type); - _activeResponse->print(content); + if (content_type != NULL) { + _activeResponse->setHeader("Content-Type", content_type); + } + _standardHeaders(); + if (content) { + _activeResponse->print(content); + } } void ESPWebServer::send(int code, char* content_type, const String& content) { @@ -195,55 +326,194 @@ void ESPWebServer::send(int code, const String& content_type, const String& cont } void ESPWebServer::send_P(int code, PGM_P content_type, PGM_P content) { - // TODO + if (_contentLength == CONTENT_LENGTH_NOT_SET) _contentLength = strlen_P(content); + _activeResponse->setStatusCode(code); + String memContentType(FPSTR(content_type)); + _activeResponse->setHeader("Content-Type", memContentType.c_str()); + _standardHeaders(); + _activeResponse->print(FPSTR(content)); } void ESPWebServer::send_P(int code, PGM_P content_type, PGM_P content, size_t contentLength) { - // TODO + if (_contentLength == CONTENT_LENGTH_NOT_SET) _contentLength = contentLength; + _activeResponse->setStatusCode(code); + String memContentType(FPSTR(content_type)); + _activeResponse->setHeader("Content-Type", memContentType.c_str()); + _standardHeaders(); + _activeResponse->write((const uint8_t *)content, contentLength); +} + +void ESPWebServer::_standardHeaders() { + if (_contentLength != CONTENT_LENGTH_NOT_SET && _contentLength != CONTENT_LENGTH_UNKNOWN) { + _activeResponse->setHeader("Content-Length", String(_contentLength).c_str()); + } } void ESPWebServer::enableCORS(boolean value) { - // TODO + if (value) _server->setDefaultHeader("Access-Control-Allow-Origin", "*"); } void ESPWebServer::enableCrossOrigin(boolean value) { - // TODO + enableCORS(value); } void ESPWebServer::setContentLength(const size_t contentLength) { - // TODO + _contentLength = contentLength; } void ESPWebServer::sendHeader(const String& name, const String& value, bool first) { - // TODO + if (first) { + HTTPS_LOGE("sendHeader(..., first=true) not implemented"); + } + _activeResponse->setHeader(name.c_str(), value.c_str()); } void ESPWebServer::sendContent(const String& content) { - // TODO + _activeResponse->print(content); } void ESPWebServer::sendContent_P(PGM_P content) { - // TODO + _activeResponse->print(FPSTR(content)); } void ESPWebServer::sendContent_P(PGM_P content, size_t size) { - // TODO + _activeResponse->write((const uint8_t *)content, size); } String ESPWebServer::urlDecode(const String& text) { - // TODO - return text; + auto decoded = ::urlDecode(std::string(text.c_str())); + return String(decoded.c_str()); } void ESPWebServer::_handlerWrapper( httpsserver::HTTPRequest *req, httpsserver::HTTPResponse *res) { + BodyResourceParameters *bodyParams = nullptr; ESPWebServerNode *node = (ESPWebServerNode*)req->getResolvedNode(); node->_wrapper->_activeRequest = req; node->_wrapper->_activeResponse = res; + node->_wrapper->_activeParams = req->getParams(); + node->_wrapper->_contentLength = CONTENT_LENGTH_NOT_SET; + // POST form data needs to be handled specially + if (req->getMethod() == "POST") { + HTTPBodyParser *parser = NULL; + 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); + } + if (contentType == "application/x-www-form-urlencoded") { + parser = new HTTPURLEncodedBodyParser(req); + } + // + // _activeParams should be the merger of the URL parameters and the body parameters. + // + bodyParams = new BodyResourceParameters(); + for (auto it = node->_wrapper->_activeParams->beginQueryParameters(); it != node->_wrapper->_activeParams->endQueryParameters(); it++) { + bodyParams->setQueryParameter(it->first, it->second); + } + node->_wrapper->_activeParams = bodyParams; + + while (parser && parser->nextField()) { + std::string name = parser->getFieldName(); + std::string filename = parser->getFieldFilename(); + if (filename != "") { + // This field is a file. Use the uploader + std::string mimeType = parser->getFieldMimeType(); + HTTPUpload uploader; + node->_wrapper->_activeUpload = &uploader; + uploader.status = UPLOAD_FILE_START; + uploader.name = String(name.c_str()); + uploader.filename = String(filename.c_str()); + uploader.type = String(mimeType.c_str()); + uploader.totalSize = 0; + uploader.currentSize = 0; + // First call to the uploader callback + node->_wrappedUploadHandler(); + // Now loop over the data + uploader.status = UPLOAD_FILE_WRITE; + while(!parser->endOfField()) { + uploader.currentSize = parser->read(uploader.buf, sizeof(uploader.buf)); + uploader.totalSize += uploader.currentSize; + node->_wrappedUploadHandler(); + } + uploader.status = UPLOAD_FILE_END; + node->_wrappedUploadHandler(); + node->_wrapper->_activeUpload = NULL; + } else { + // This field is not a file. Add the value + std::string value(""); + while (!parser->endOfField()) { + byte buf[512]; + size_t readLength = parser->read(buf, 512); + std::string bufString((char *)buf, readLength); + value += bufString; + } + bodyParams->setQueryParameter(name, value); + } + } + delete parser; + } node->_wrappedHandler(); node->_wrapper->_activeRequest = nullptr; node->_wrapper->_activeResponse = nullptr; + node->_wrapper->_activeParams = nullptr; +} + +/** + * This handler function will try to load the requested resource from SPIFFS's /public folder. + * + * If the method is not GET, it will throw 405, if the file is not found, it will throw 404. + */ +void ESPWebServer::_staticPageHandler(HTTPRequest * req, HTTPResponse * res) { + assert(req->getMethod() == "GET"); + ESPWebServerStaticNode *node = (ESPWebServerStaticNode*)req->getResolvedNode(); + // xxxjack remove urlpath bits from reqFile + // xxxjack add index.htm if needed + // xxxjack prepend filepath + // Redirect / to /index.html + std::string reqFile; + if (!req->getParams()->getPathParameter(0, reqFile)) reqFile = "index.html"; + + // Try to open the file + std::string filename = node->_filePath + reqFile; + + // Check if the file exists + if (!node->_fileSystem.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 = node->_fileSystem.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(reqFile.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(); } ESPWebServerNode::ESPWebServerNode( @@ -251,13 +521,31 @@ ESPWebServerNode::ESPWebServerNode( const std::string &path, const std::string &method, const THandlerFunction &handler, + const THandlerFunction &uploadHandler, const std::string &tag) : ResourceNode(path, method, &(ESPWebServer::_handlerWrapper), tag), _wrapper(server), - _wrappedHandler(handler) { + _wrappedHandler(handler), + _wrappedUploadHandler(uploadHandler) { } ESPWebServerNode::~ESPWebServerNode() { } + +ESPWebServerStaticNode::ESPWebServerStaticNode( + ESPWebServer *server, + const std::string& urlPath, + FS& fs, + const std::string& filePath, + const std::string& cache_header) : + ResourceNode(urlPath, "GET", &(ESPWebServer::_staticPageHandler), ""), + _filePath(filePath), + _fileSystem(fs), + _cache_header(cache_header) +{ +} + +ESPWebServerStaticNode::~ESPWebServerStaticNode() { +} diff --git a/src/ESPWebServer.hpp b/src/ESPWebServer.hpp index 2b4a596..dd81b1d 100644 --- a/src/ESPWebServer.hpp +++ b/src/ESPWebServer.hpp @@ -5,11 +5,13 @@ #include #include +#include #include #include #include #include +#include #include "HTTP_Method.h" @@ -22,6 +24,9 @@ enum HTTPAuthMethod { BASIC_AUTH, /* DIGEST_AUTH */ }; #define HTTP_UPLOAD_BUFLEN 1436 #endif +#define CONTENT_LENGTH_UNKNOWN ((size_t) -1) +#define CONTENT_LENGTH_NOT_SET ((size_t) -2) + typedef std::function THandlerFunction; typedef struct { @@ -42,6 +47,9 @@ class ESPWebServerNode; class ESPWebServer { + friend class ESPWebServerSecure; +protected: + ESPWebServer(httpsserver::HTTPServer* _server); public: ESPWebServer(IPAddress addr, int port = 80); ESPWebServer(int port = 80); @@ -103,23 +111,54 @@ class ESPWebServer static String urlDecode(const String& text); - //template size_t streamFile(T &file, const String& contentType); + template size_t streamFile(T &file, const String& contentType) { + size_t fileSize = file.size(); + uint8_t buffer[HTTP_UPLOAD_BUFLEN]; + _prepareStreamFile(fileSize, contentType); + size_t didWrite = 0; + while (fileSize > 0) { + size_t thisRead = file.read(buffer, fileSize > HTTP_UPLOAD_BUFLEN ? HTTP_UPLOAD_BUFLEN : fileSize); + if (thisRead == 0) break; + _activeResponse->write(buffer, thisRead); + didWrite += thisRead; + fileSize -= thisRead; + } + return didWrite; + } protected: friend class ESPWebServerNode; + friend class ESPWebServerStaticNode; /** The wrapper function that maps on() calls */ static void _handlerWrapper(httpsserver::HTTPRequest *req, httpsserver::HTTPResponse *res); + /** The wrapper function that maps on() calls */ + static void _staticPageHandler(httpsserver::HTTPRequest *req, httpsserver::HTTPResponse *res); + + /** Add standard headers */ + void _standardHeaders(); + + /** Prepare for streaming a file */ + void _prepareStreamFile(size_t fileSize, const String& contentType); + /** The backing server instance */ - httpsserver::HTTPServer _server; + httpsserver::HTTPServer* _server; /** The currently active request */ httpsserver::HTTPRequest *_activeRequest; httpsserver::HTTPResponse *_activeResponse; + HTTPUpload *_activeUpload; + httpsserver::ResourceParameters *_activeParams; /** default node */ ESPWebServerNode *_notFoundNode; + + /** Instance variables for standard headers */ + size_t _contentLength; + + /** Default file upload handler */ + THandlerFunction _uploadHandler; }; class ESPWebServerNode : public httpsserver::ResourceNode { @@ -129,6 +168,7 @@ class ESPWebServerNode : public httpsserver::ResourceNode { const std::string &path, const std::string &method, const THandlerFunction &handler, + const THandlerFunction &uploadHandler, const std::string &tag = ""); virtual ~ESPWebServerNode(); @@ -136,6 +176,26 @@ class ESPWebServerNode : public httpsserver::ResourceNode { friend class ESPWebServer; ESPWebServer *_wrapper; const THandlerFunction _wrappedHandler; + const THandlerFunction _wrappedUploadHandler; +}; + +class ESPWebServerStaticNode : public httpsserver::ResourceNode { +public: + ESPWebServerStaticNode( + ESPWebServer *server, + const std::string& urlPath, + FS& fs, + const std::string& filePath, + const std::string& cache_header); + virtual ~ESPWebServerStaticNode(); + +protected: + friend class ESPWebServer; + ESPWebServer *_wrapper; + std::string _filePath; + FS& _fileSystem; + std::string _cache_header; + }; #endif //ESPWEBSERVER_H \ No newline at end of file diff --git a/src/ESPWebServerSecure.cpp b/src/ESPWebServerSecure.cpp new file mode 100644 index 0000000..04db490 --- /dev/null +++ b/src/ESPWebServerSecure.cpp @@ -0,0 +1,30 @@ +#include "ESPWebServerSecure.hpp" + + +ESPWebServerSecure::ESPWebServerSecure(IPAddress addr, int port) +: ESPWebServer(new httpsserver::HTTPSServer(&_sslCert, port, 4, addr)), + _underlyingServer(this), + _sslCert() +{} + +ESPWebServerSecure::ESPWebServerSecure(int port) +: ESPWebServer(new httpsserver::HTTPSServer(&_sslCert, port, 4)), + _underlyingServer(this), + _sslCert() +{} + +void ESPWebServerSecure::setServerKeyAndCert(const uint8_t *key, int keyLen, const uint8_t *cert, int certLen) { + _sslCert.setPK((unsigned char *)key, keyLen); + _sslCert.setCert((unsigned char *)cert, certLen); +} + +void ESPWebServerSecure::setServerKeyAndCert_P(const uint8_t *key, int keyLen, const uint8_t *cert, int certLen) { + setServerKeyAndCert(key, keyLen, cert, certLen); +} + +void ESPWebServerUnderlyingServer::setServerKeyAndCert(const uint8_t *key, int keyLen, const uint8_t *cert, int certLen) { + _webserver->setServerKeyAndCert(key, keyLen, cert, certLen); +} +void ESPWebServerUnderlyingServer::setServerKeyAndCert_P(const uint8_t *key, int keyLen, const uint8_t *cert, int certLen) { + _webserver->setServerKeyAndCert_P(key, keyLen, cert, certLen); +} diff --git a/src/ESPWebServerSecure.hpp b/src/ESPWebServerSecure.hpp new file mode 100644 index 0000000..6299c1a --- /dev/null +++ b/src/ESPWebServerSecure.hpp @@ -0,0 +1,36 @@ +#ifndef ESPWEBSERVERSECURE_H +#define ESPWEBSERVERSECURE_H + +#include "ESPWebServer.hpp" +#include +#include + +class ESPWebServerSecure; + +// Placeholder class, to make the API conform to what Esp8266WebServerSecure provides. +class ESPWebServerUnderlyingServer { + friend class ESPWebServerSecure; +protected: + ESPWebServerUnderlyingServer(ESPWebServerSecure *webserver) + : _webserver(webserver) + {} + ESPWebServerSecure* _webserver; +public: + void setServerKeyAndCert(const uint8_t *key, int keyLen, const uint8_t *cert, int certLen); + void setServerKeyAndCert_P(const uint8_t *key, int keyLen, const uint8_t *cert, int certLen); +}; + +class ESPWebServerSecure : public ESPWebServer { + friend class ESPWebServerUnderlyingServer; +public: + ESPWebServerSecure(IPAddress addr, int port = 442); + ESPWebServerSecure(int port = 442); + void setServerKeyAndCert(const uint8_t *key, int keyLen, const uint8_t *cert, int certLen); + void setServerKeyAndCert_P(const uint8_t *key, int keyLen, const uint8_t *cert, int certLen); + ESPWebServerUnderlyingServer& getServer() { return _underlyingServer; } +protected: + ESPWebServerUnderlyingServer _underlyingServer; + httpsserver::SSLCert _sslCert; +}; + +#endif //ESPWEBSERVERSECURE_H \ No newline at end of file