diff --git a/README.md b/README.md index 63fd91d..4c91be5 100644 --- a/README.md +++ b/README.md @@ -173,11 +173,13 @@ SoftwareSerial | ❌ SPI | ❌ Wire | ❌ **OTHER LIBRARIES** | -Wi-Fi Station | ✔️ -Wi-Fi Access Point | ✔️ -Wi-Fi Events | ❌ +Wi-Fi STA/AP/Mixed | ✔️ Wi-Fi Client (SSL) | ✔️ (❌) Wi-Fi Server | ✔️ +Wi-Fi Events | ❌ +IPv6 | ❌ +HTTP Client (SSL) | ✔️ (❌) +HTTP Server | ✔️ NVS / Preferences | ❌ SPIFFS | ❌ BLE | - diff --git a/arduino/libretuya/libraries/WebServer/HTTP_Method.h b/arduino/libretuya/libraries/WebServer/HTTP_Method.h new file mode 100644 index 0000000..b739a2f --- /dev/null +++ b/arduino/libretuya/libraries/WebServer/HTTP_Method.h @@ -0,0 +1,56 @@ +#pragma once + +/* Request Methods */ +#define HTTP_METHOD_MAP(XX) \ + XX(0, DELETE, DELETE) \ + XX(1, GET, GET) \ + XX(2, HEAD, HEAD) \ + XX(3, POST, POST) \ + XX(4, PUT, PUT) \ + /* pathological */ \ + XX(5, CONNECT, CONNECT) \ + XX(6, OPTIONS, OPTIONS) \ + XX(7, TRACE, TRACE) \ + /* WebDAV */ \ + XX(8, COPY, COPY) \ + XX(9, LOCK, LOCK) \ + XX(10, MKCOL, MKCOL) \ + XX(11, MOVE, MOVE) \ + XX(12, PROPFIND, PROPFIND) \ + XX(13, PROPPATCH, PROPPATCH) \ + XX(14, SEARCH, SEARCH) \ + XX(15, UNLOCK, UNLOCK) \ + XX(16, BIND, BIND) \ + XX(17, REBIND, REBIND) \ + XX(18, UNBIND, UNBIND) \ + XX(19, ACL, ACL) \ + /* subversion */ \ + XX(20, REPORT, REPORT) \ + XX(21, MKACTIVITY, MKACTIVITY) \ + XX(22, CHECKOUT, CHECKOUT) \ + XX(23, MERGE, MERGE) \ + /* upnp */ \ + XX(24, MSEARCH, M - SEARCH) \ + XX(25, NOTIFY, NOTIFY) \ + XX(26, SUBSCRIBE, SUBSCRIBE) \ + XX(27, UNSUBSCRIBE, UNSUBSCRIBE) \ + /* RFC-5789 */ \ + XX(28, PATCH, PATCH) \ + XX(29, PURGE, PURGE) \ + /* CalDAV */ \ + XX(30, MKCALENDAR, MKCALENDAR) \ + /* RFC-2068, section 19.6.1.2 */ \ + XX(31, LINK, LINK) \ + XX(32, UNLINK, UNLINK) \ + /* icecast */ \ + XX(33, SOURCE, SOURCE) + +enum http_method { + +#define XX(num, name, string) HTTP_##name = num, + HTTP_METHOD_MAP(XX) +#undef XX +}; + +typedef enum http_method HTTPMethod; +#define HTTP_ANY (HTTPMethod)(255) diff --git a/arduino/libretuya/libraries/WebServer/Parsing.cpp b/arduino/libretuya/libraries/WebServer/Parsing.cpp new file mode 100644 index 0000000..1fc4a7e --- /dev/null +++ b/arduino/libretuya/libraries/WebServer/Parsing.cpp @@ -0,0 +1,605 @@ +/* + Parsing.cpp - HTTP request parsing. + + Copyright (c) 2015 Ivan Grokhotkov. All rights reserved. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + Modified 8 May 2015 by Hristo Gochkov (proper post and file upload handling) +*/ + +#include + +#include "WebServer.h" +#include "WiFiClient.h" +#include "WiFiServer.h" +#include "detail/mimetable.h" + +#ifndef WEBSERVER_MAX_POST_ARGS +#define WEBSERVER_MAX_POST_ARGS 32 +#endif + +#define __STR(a) #a +#define _STR(a) __STR(a) +const char *_http_method_str[] = { +#define XX(num, name, string) _STR(name), + HTTP_METHOD_MAP(XX) +#undef XX +}; + +static const char Content_Type[] PROGMEM = "Content-Type"; +static const char filename[] PROGMEM = "filename"; + +static char *readBytesWithTimeout(WiFiClient &client, size_t maxLength, size_t &dataLength, int timeout_ms) { + char *buf = nullptr; + dataLength = 0; + while (dataLength < maxLength) { + int tries = timeout_ms; + size_t newLength; + while (!(newLength = client.available()) && tries--) + delay(1); + if (!newLength) { + break; + } + if (!buf) { + buf = (char *)malloc(newLength + 1); + if (!buf) { + return nullptr; + } + } else { + char *newBuf = (char *)realloc(buf, dataLength + newLength + 1); + if (!newBuf) { + free(buf); + return nullptr; + } + buf = newBuf; + } + client.readBytes(buf + dataLength, newLength); + dataLength += newLength; + buf[dataLength] = '\0'; + } + return buf; +} + +bool 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(); + } + + // First line of HTTP request looks like "GET /path HTTP/1.1" + // Retrieve the "/path" part by finding the spaces + int addr_start = req.indexOf(' '); + 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; + } + + String methodStr = req.substring(0, addr_start); + String url = req.substring(addr_start + 1, addr_end); + String versionEnd = req.substring(addr_end + 8); + _currentVersion = atoi(versionEnd.c_str()); + String searchStr = ""; + int hasSearch = url.indexOf('?'); + if (hasSearch != -1) { + searchStr = url.substring(hasSearch + 1); + url = url.substring(0, hasSearch); + } + _currentUri = url; + _chunked = false; + + HTTPMethod method = HTTP_ANY; + size_t num_methods = sizeof(_http_method_str) / sizeof(const char *); + for (size_t i = 0; i < num_methods; i++) { + if (methodStr == _http_method_str[i]) { + method = (HTTPMethod)i; + break; + } + } + if (method == HTTP_ANY) { + log_e("Unknown HTTP Method: %s", methodStr.c_str()); + return false; + } + _currentMethod = method; + + log_v("method: %s url: %s search: %s", methodStr.c_str(), url.c_str(), searchStr.c_str()); + + // attach handler + RequestHandler *handler; + for (handler = _firstHandler; handler; handler = handler->next()) { + if (handler->canHandle(_currentMethod, _currentUri)) + break; + } + _currentHandler = handler; + + String formData; + // below is needed only when POST type request + if (method == HTTP_POST || method == HTTP_PUT || method == HTTP_PATCH || method == HTTP_DELETE) { + String boundaryStr; + String headerName; + String headerValue; + bool isForm = false; + bool isEncoded = false; + uint32_t contentLength = 0; + // parse headers + while (1) { + req = client.readStringUntil('\r'); + client.readStringUntil('\n'); + if (req == "") + break; // no moar headers + int headerDiv = req.indexOf(':'); + if (headerDiv == -1) { + break; + } + headerName = req.substring(0, headerDiv); + headerValue = req.substring(headerDiv + 1); + headerValue.trim(); + _collectHeader(headerName.c_str(), headerValue.c_str()); + + log_v("headerName: %s", headerName.c_str()); + log_v("headerValue: %s", headerValue.c_str()); + + if (headerName.equalsIgnoreCase(FPSTR(Content_Type))) { + using namespace mime; + if (headerValue.startsWith(FPSTR(mimeTable[txt].mimeType))) { + isForm = false; + } else if (headerValue.startsWith(F("application/x-www-form-urlencoded"))) { + isForm = false; + isEncoded = true; + } else if (headerValue.startsWith(F("multipart/"))) { + boundaryStr = headerValue.substring(headerValue.indexOf('=') + 1); + boundaryStr.replace("\"", ""); + isForm = true; + } + } else if (headerName.equalsIgnoreCase(F("Content-Length"))) { + contentLength = headerValue.toInt(); + } else if (headerName.equalsIgnoreCase(F("Host"))) { + _hostHeader = headerValue; + } + } + + if (!isForm) { + size_t plainLength; + char *plainBuf = readBytesWithTimeout(client, contentLength, plainLength, HTTP_MAX_POST_WAIT); + if (plainLength < contentLength) { + free(plainBuf); + return false; + } + if (contentLength > 0) { + if (isEncoded) { + // url encoded form + if (searchStr != "") + searchStr += '&'; + searchStr += plainBuf; + } + _parseArguments(searchStr); + if (!isEncoded) { + // plain post json or other data + RequestArgument &arg = _currentArgs[_currentArgCount++]; + arg.key = F("plain"); + arg.value = String(plainBuf); + } + + log_v("Plain: %s", plainBuf); + free(plainBuf); + } else { + // No content - but we can still have arguments in the URL. + _parseArguments(searchStr); + } + } + + if (isForm) { + _parseArguments(searchStr); + if (!_parseForm(client, boundaryStr, contentLength)) { + return false; + } + } + } else { + String headerName; + String headerValue; + // parse headers + while (1) { + req = client.readStringUntil('\r'); + client.readStringUntil('\n'); + if (req == "") + break; // no moar headers + int headerDiv = req.indexOf(':'); + if (headerDiv == -1) { + break; + } + headerName = req.substring(0, headerDiv); + headerValue = req.substring(headerDiv + 2); + _collectHeader(headerName.c_str(), headerValue.c_str()); + + log_v("headerName: %s", headerName.c_str()); + log_v("headerValue: %s", headerValue.c_str()); + + if (headerName.equalsIgnoreCase("Host")) { + _hostHeader = headerValue; + } + } + _parseArguments(searchStr); + } + client.flush(); + + log_v("Request: %s", url.c_str()); + log_v(" Arguments: %s", searchStr.c_str()); + + return true; +} + +bool WebServer::_collectHeader(const char *headerName, const char *headerValue) { + for (int i = 0; i < _headerKeysCount; i++) { + if (_currentHeaders[i].key.equalsIgnoreCase(headerName)) { + _currentHeaders[i].value = headerValue; + return true; + } + } + return false; +} + +void WebServer::_parseArguments(String data) { + log_v("args: %s", data.c_str()); + if (_currentArgs) + delete[] _currentArgs; + _currentArgs = 0; + if (data.length() == 0) { + _currentArgCount = 0; + _currentArgs = new RequestArgument[1]; + return; + } + _currentArgCount = 1; + + for (int i = 0; i < (int)data.length();) { + i = data.indexOf('&', i); + if (i == -1) + break; + ++i; + ++_currentArgCount; + } + log_v("args count: %d", _currentArgCount); + + _currentArgs = new RequestArgument[_currentArgCount + 1]; + int pos = 0; + int iarg; + for (iarg = 0; iarg < _currentArgCount;) { + int equal_sign_index = data.indexOf('=', pos); + int next_arg_index = data.indexOf('&', pos); + log_v("pos %d =@%d &@%d", pos, equal_sign_index, next_arg_index); + if ((equal_sign_index == -1) || ((equal_sign_index > next_arg_index) && (next_arg_index != -1))) { + log_e("arg missing value: %d", iarg); + if (next_arg_index == -1) + break; + pos = next_arg_index + 1; + continue; + } + RequestArgument &arg = _currentArgs[iarg]; + arg.key = urlDecode(data.substring(pos, equal_sign_index)); + arg.value = urlDecode(data.substring(equal_sign_index + 1, next_arg_index)); + log_v("arg %d key: %s value: %s", iarg, arg.key.c_str(), arg.value.c_str()); + ++iarg; + if (next_arg_index == -1) + break; + pos = next_arg_index + 1; + } + _currentArgCount = iarg; + log_v("args count: %d", _currentArgCount); +} + +void WebServer::_uploadWriteByte(uint8_t b) { + if (_currentUpload->currentSize == HTTP_UPLOAD_BUFLEN) { + if (_currentHandler && _currentHandler->canUpload(_currentUri)) + _currentHandler->upload(*this, _currentUri, *_currentUpload); + _currentUpload->totalSize += _currentUpload->currentSize; + _currentUpload->currentSize = 0; + } + _currentUpload->buf[_currentUpload->currentSize++] = b; +} + +int WebServer::_uploadReadByte(WiFiClient &client) { + int res = client.read(); + if (res < 0) { + // keep trying until you either read a valid byte or timeout + unsigned long startMillis = millis(); + long timeoutIntervalMillis = client.getTimeout(); + boolean timedOut = false; + for (;;) { + if (!client.connected()) + return -1; + // loosely modeled after blinkWithoutDelay pattern + while (!timedOut && !client.available() && client.connected()) { + delay(2); + timedOut = millis() - startMillis >= timeoutIntervalMillis; + } + + res = client.read(); + if (res >= 0) { + return res; // exit on a valid read + } + // NOTE: it is possible to get here and have all of the following + // assertions hold true + // + // -- client.available() > 0 + // -- client.connected == true + // -- res == -1 + // + // a simple retry strategy overcomes this which is to say the + // assertion is not permanent, but the reason that this works + // is elusive, and possibly indicative of a more subtle underlying + // issue + + timedOut = millis() - startMillis >= timeoutIntervalMillis; + if (timedOut) { + return res; // exit on a timeout + } + } + } + + return res; +} + +bool WebServer::_parseForm(WiFiClient &client, String boundary, uint32_t len) { + (void)len; + log_v("Parse Form: Boundary: %s Length: %d", boundary.c_str(), len); + String line; + int retry = 0; + do { + line = client.readStringUntil('\r'); + ++retry; + } while (line.length() == 0 && retry < 3); + + client.readStringUntil('\n'); + // start reading the form + if (line == ("--" + boundary)) { + if (_postArgs) + delete[] _postArgs; + _postArgs = new RequestArgument[WEBSERVER_MAX_POST_ARGS]; + _postArgsLen = 0; + while (1) { + String argName; + String argValue; + String argType; + String argFilename; + bool argIsFile = false; + + line = client.readStringUntil('\r'); + client.readStringUntil('\n'); + if (line.length() > 19 && line.substring(0, 19).equalsIgnoreCase(F("Content-Disposition"))) { + int nameStart = line.indexOf('='); + if (nameStart != -1) { + argName = line.substring(nameStart + 2); + nameStart = argName.indexOf('='); + if (nameStart == -1) { + argName = argName.substring(0, argName.length() - 1); + } else { + argFilename = argName.substring(nameStart + 2, argName.length() - 1); + argName = argName.substring(0, argName.indexOf('"')); + argIsFile = true; + log_v("PostArg FileName: %s", argFilename.c_str()); + // use GET to set the filename if uploading using blob + if (argFilename == F("blob") && hasArg(FPSTR(filename))) + argFilename = arg(FPSTR(filename)); + } + log_v("PostArg Name: %s", argName.c_str()); + using namespace mime; + argType = FPSTR(mimeTable[txt].mimeType); + line = client.readStringUntil('\r'); + client.readStringUntil('\n'); + if (line.length() > 12 && line.substring(0, 12).equalsIgnoreCase(FPSTR(Content_Type))) { + argType = line.substring(line.indexOf(':') + 2); + // skip next line + client.readStringUntil('\r'); + client.readStringUntil('\n'); + } + log_v("PostArg Type: %s", argType.c_str()); + if (!argIsFile) { + while (1) { + line = client.readStringUntil('\r'); + client.readStringUntil('\n'); + if (line.startsWith("--" + boundary)) + break; + if (argValue.length() > 0) + argValue += "\n"; + argValue += line; + } + log_v("PostArg Value: %s", argValue.c_str()); + + RequestArgument &arg = _postArgs[_postArgsLen++]; + arg.key = argName; + arg.value = argValue; + + if (line == ("--" + boundary + "--")) { + log_v("Done Parsing POST"); + break; + } else if (_postArgsLen >= WEBSERVER_MAX_POST_ARGS) { + log_e("Too many PostArgs (max: %d) in request.", WEBSERVER_MAX_POST_ARGS); + return false; + } + } else { + _currentUpload.reset(new HTTPUpload()); + _currentUpload->status = UPLOAD_FILE_START; + _currentUpload->name = argName; + _currentUpload->filename = argFilename; + _currentUpload->type = argType; + _currentUpload->totalSize = 0; + _currentUpload->currentSize = 0; + log_v( + "Start File: %s Type: %s", + _currentUpload->filename.c_str(), + _currentUpload->type.c_str() + ); + if (_currentHandler && _currentHandler->canUpload(_currentUri)) + _currentHandler->upload(*this, _currentUri, *_currentUpload); + _currentUpload->status = UPLOAD_FILE_WRITE; + int argByte = _uploadReadByte(client); + readfile: + + while (argByte != 0x0D) { + if (argByte < 0) + return _parseFormUploadAborted(); + _uploadWriteByte(argByte); + argByte = _uploadReadByte(client); + } + + argByte = _uploadReadByte(client); + if (argByte < 0) + return _parseFormUploadAborted(); + if (argByte == 0x0A) { + argByte = _uploadReadByte(client); + if (argByte < 0) + return _parseFormUploadAborted(); + if ((char)argByte != '-') { + // continue reading the file + _uploadWriteByte(0x0D); + _uploadWriteByte(0x0A); + goto readfile; + } else { + argByte = _uploadReadByte(client); + if (argByte < 0) + return _parseFormUploadAborted(); + if ((char)argByte != '-') { + // continue reading the file + _uploadWriteByte(0x0D); + _uploadWriteByte(0x0A); + _uploadWriteByte((uint8_t)('-')); + goto readfile; + } + } + + uint8_t endBuf[boundary.length()]; + uint32_t i = 0; + while (i < boundary.length()) { + argByte = _uploadReadByte(client); + if (argByte < 0) + return _parseFormUploadAborted(); + if ((char)argByte == 0x0D) { + _uploadWriteByte(0x0D); + _uploadWriteByte(0x0A); + _uploadWriteByte((uint8_t)('-')); + _uploadWriteByte((uint8_t)('-')); + uint32_t j = 0; + while (j < i) { + _uploadWriteByte(endBuf[j++]); + } + goto readfile; + } + endBuf[i++] = (uint8_t)argByte; + } + + if (strstr((const char *)endBuf, boundary.c_str()) != NULL) { + if (_currentHandler && _currentHandler->canUpload(_currentUri)) + _currentHandler->upload(*this, _currentUri, *_currentUpload); + _currentUpload->totalSize += _currentUpload->currentSize; + _currentUpload->status = UPLOAD_FILE_END; + if (_currentHandler && _currentHandler->canUpload(_currentUri)) + _currentHandler->upload(*this, _currentUri, *_currentUpload); + log_v( + "End File: %s Type: %s Size: %d", + _currentUpload->filename.c_str(), + _currentUpload->type.c_str(), + _currentUpload->totalSize + ); + line = client.readStringUntil(0x0D); + client.readStringUntil(0x0A); + if (line == "--") { + log_v("Done Parsing POST"); + break; + } + continue; + } else { + _uploadWriteByte(0x0D); + _uploadWriteByte(0x0A); + _uploadWriteByte((uint8_t)('-')); + _uploadWriteByte((uint8_t)('-')); + uint32_t i = 0; + while (i < boundary.length()) { + _uploadWriteByte(endBuf[i++]); + } + argByte = _uploadReadByte(client); + goto readfile; + } + } else { + _uploadWriteByte(0x0D); + goto readfile; + } + break; + } + } + } + } + + int iarg; + int totalArgs = ((WEBSERVER_MAX_POST_ARGS - _postArgsLen) < _currentArgCount) + ? (WEBSERVER_MAX_POST_ARGS - _postArgsLen) + : _currentArgCount; + for (iarg = 0; iarg < totalArgs; iarg++) { + RequestArgument &arg = _postArgs[_postArgsLen++]; + arg.key = _currentArgs[iarg].key; + arg.value = _currentArgs[iarg].value; + } + if (_currentArgs) + delete[] _currentArgs; + _currentArgs = new RequestArgument[_postArgsLen]; + for (iarg = 0; iarg < _postArgsLen; iarg++) { + RequestArgument &arg = _currentArgs[iarg]; + arg.key = _postArgs[iarg].key; + arg.value = _postArgs[iarg].value; + } + _currentArgCount = iarg; + if (_postArgs) { + delete[] _postArgs; + _postArgs = nullptr; + _postArgsLen = 0; + } + return true; + } + log_e("Error: line: %s", line.c_str()); + return false; +} + +String WebServer::urlDecode(const String &text) { + String decoded = ""; + char temp[] = "0x00"; + unsigned int len = text.length(); + unsigned int i = 0; + while (i < len) { + char decodedChar; + char encodedChar = text.charAt(i++); + if ((encodedChar == '%') && (i + 1 < len)) { + temp[2] = text.charAt(i++); + temp[3] = text.charAt(i++); + + decodedChar = strtol(temp, NULL, 16); + } else { + if (encodedChar == '+') { + decodedChar = ' '; + } else { + decodedChar = encodedChar; // normal ascii char + } + } + decoded += decodedChar; + } + return decoded; +} + +bool WebServer::_parseFormUploadAborted() { + _currentUpload->status = UPLOAD_FILE_ABORTED; + if (_currentHandler && _currentHandler->canUpload(_currentUri)) + _currentHandler->upload(*this, _currentUri, *_currentUpload); + return false; +} diff --git a/arduino/libretuya/libraries/WebServer/Uri.h b/arduino/libretuya/libraries/WebServer/Uri.h new file mode 100644 index 0000000..67602fa --- /dev/null +++ b/arduino/libretuya/libraries/WebServer/Uri.h @@ -0,0 +1,29 @@ +#pragma once + +#include +#include + +class Uri { + + protected: + const String _uri; + + public: + Uri(const char *uri) : _uri(uri) {} + + Uri(const String &uri) : _uri(uri) {} + + Uri(const __FlashStringHelper *uri) : _uri(String(uri)) {} + + virtual ~Uri() {} + + virtual Uri *clone() const { + return new Uri(_uri); + }; + + virtual void initPathArgs(__attribute__((unused)) std::vector &pathArgs) {} + + virtual bool canHandle(const String &requestUri, __attribute__((unused)) std::vector &pathArgs) { + return _uri == requestUri; + } +}; diff --git a/arduino/libretuya/libraries/WebServer/WebServer.cpp b/arduino/libretuya/libraries/WebServer/WebServer.cpp new file mode 100644 index 0000000..981f6ec --- /dev/null +++ b/arduino/libretuya/libraries/WebServer/WebServer.cpp @@ -0,0 +1,717 @@ +/* + WebServer.cpp - Dead simple web-server. + Supports only one simultaneous client, knows how to handle GET and POST. + + Copyright (c) 2014 Ivan Grokhotkov. All rights reserved. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + Modified 8 May 2015 by Hristo Gochkov (proper post and file upload handling) +*/ + +#include + +#include "FS.h" +#include "WebServer.h" +#include "WiFiClient.h" +#include "WiFiServer.h" +#include "detail/RequestHandlersImpl.h" +// #include "mbedtls/md5.h" +#include + +static const char AUTHORIZATION_HEADER[] = "Authorization"; +static const char qop_auth[] PROGMEM = "qop=auth"; +static const char qop_auth_quoted[] PROGMEM = "qop=\"auth\""; +static const char WWW_Authenticate[] = "WWW-Authenticate"; +static const char Content_Length[] = "Content-Length"; + +WebServer::WebServer(IPAddress addr, int port) + : _corsEnabled(false), _server(addr, port), _currentMethod(HTTP_ANY), _currentVersion(0), _currentStatus(HC_NONE), + _statusChange(0), _nullDelay(true), _currentHandler(nullptr), _firstHandler(nullptr), _lastHandler(nullptr), + _currentArgCount(0), _currentArgs(nullptr), _postArgsLen(0), _postArgs(nullptr), _headerKeysCount(0), + _currentHeaders(nullptr), _contentLength(0), _chunked(false) { + log_v("WebServer::Webserver(addr=%s, port=%d)", addr.toString().c_str(), port); +} + +WebServer::WebServer(int port) + : _corsEnabled(false), _server(port), _currentMethod(HTTP_ANY), _currentVersion(0), _currentStatus(HC_NONE), + _statusChange(0), _nullDelay(true), _currentHandler(nullptr), _firstHandler(nullptr), _lastHandler(nullptr), + _currentArgCount(0), _currentArgs(nullptr), _postArgsLen(0), _postArgs(nullptr), _headerKeysCount(0), + _currentHeaders(nullptr), _contentLength(0), _chunked(false) { + log_v("WebServer::Webserver(port=%d)", port); +} + +WebServer::~WebServer() { + _server.close(); + if (_currentHeaders) + delete[] _currentHeaders; + RequestHandler *handler = _firstHandler; + while (handler) { + RequestHandler *next = handler->next(); + delete handler; + handler = next; + } +} + +void WebServer::begin() { + close(); + _server.begin(); + _server.setNoDelay(true); +} + +void WebServer::begin(uint16_t port) { + close(); + _server.begin(port); + _server.setNoDelay(true); +} + +String WebServer::_extractParam(String &authReq, const String ¶m, const char delimit) { + int _begin = authReq.indexOf(param); + if (_begin == -1) + return ""; + return authReq.substring(_begin + param.length(), authReq.indexOf(delimit, _begin + param.length())); +} + +static String md5str(String &in) { + /* char out[33] = {0}; + mbedtls_md5_context _ctx; + uint8_t i; + uint8_t *_buf = (uint8_t *)malloc(16); + if (_buf == NULL) + return String(out); + memset(_buf, 0x00, 16); + mbedtls_md5_init(&_ctx); + mbedtls_md5_starts_ret(&_ctx); + mbedtls_md5_update_ret(&_ctx, (const uint8_t *)in.c_str(), in.length()); + mbedtls_md5_finish_ret(&_ctx, _buf); + for (i = 0; i < 16; i++) { + sprintf(out + (i * 2), "%02x", _buf[i]); + } + out[32] = 0; + free(_buf); + return String(out); */ + return ""; +} + +bool WebServer::authenticate(const char *username, const char *password) { + if (hasHeader(FPSTR(AUTHORIZATION_HEADER))) { + String authReq = header(FPSTR(AUTHORIZATION_HEADER)); + if (authReq.startsWith(F("Basic"))) { + authReq = authReq.substring(6); + authReq.trim(); + char toencodeLen = strlen(username) + strlen(password) + 1; + char *toencode = new char[toencodeLen + 1]; + if (toencode == NULL) { + authReq = ""; + return false; + } + char *encoded = new char[base64_encode_expected_len(toencodeLen) + 1]; + if (encoded == NULL) { + authReq = ""; + delete[] toencode; + return false; + } + sprintf(toencode, "%s:%s", username, password); + if (base64_encode_chars(toencode, toencodeLen, encoded) > 0 && authReq.equals(encoded)) { + authReq = ""; + delete[] toencode; + delete[] encoded; + return true; + } + delete[] toencode; + delete[] encoded; + } else if (authReq.startsWith(F("Digest"))) { + authReq = authReq.substring(7); + log_v("%s", authReq.c_str()); + String _username = _extractParam(authReq, F("username=\""), '\"'); + if (!_username.length() || _username != String(username)) { + authReq = ""; + return false; + } + // extracting required parameters for RFC 2069 simpler Digest + String _realm = _extractParam(authReq, F("realm=\""), '\"'); + String _nonce = _extractParam(authReq, F("nonce=\""), '\"'); + String _uri = _extractParam(authReq, F("uri=\""), '\"'); + String _response = _extractParam(authReq, F("response=\""), '\"'); + String _opaque = _extractParam(authReq, F("opaque=\""), '\"'); + + if ((!_realm.length()) || (!_nonce.length()) || (!_uri.length()) || (!_response.length()) || + (!_opaque.length())) { + authReq = ""; + return false; + } + if ((_opaque != _sopaque) || (_nonce != _snonce) || (_realm != _srealm)) { + authReq = ""; + return false; + } + // parameters for the RFC 2617 newer Digest + String _nc, _cnonce; + if (authReq.indexOf(FPSTR(qop_auth)) != -1 || authReq.indexOf(FPSTR(qop_auth_quoted)) != -1) { + _nc = _extractParam(authReq, F("nc="), ','); + _cnonce = _extractParam(authReq, F("cnonce=\""), '\"'); + } + String _H1 = md5str(String(username) + ':' + _realm + ':' + String(password)); + log_v("Hash of user:realm:pass=%s", _H1.c_str()); + String _H2 = ""; + if (_currentMethod == HTTP_GET) { + _H2 = md5str(String(F("GET:")) + _uri); + } else if (_currentMethod == HTTP_POST) { + _H2 = md5str(String(F("POST:")) + _uri); + } else if (_currentMethod == HTTP_PUT) { + _H2 = md5str(String(F("PUT:")) + _uri); + } else if (_currentMethod == HTTP_DELETE) { + _H2 = md5str(String(F("DELETE:")) + _uri); + } else { + _H2 = md5str(String(F("GET:")) + _uri); + } + log_v("Hash of GET:uri=%s", _H2.c_str()); + String _responsecheck = ""; + if (authReq.indexOf(FPSTR(qop_auth)) != -1 || authReq.indexOf(FPSTR(qop_auth_quoted)) != -1) { + _responsecheck = md5str(_H1 + ':' + _nonce + ':' + _nc + ':' + _cnonce + F(":auth:") + _H2); + } else { + _responsecheck = md5str(_H1 + ':' + _nonce + ':' + _H2); + } + log_v("The Proper response=%s", _responsecheck.c_str()); + if (_response == _responsecheck) { + authReq = ""; + return true; + } + } + authReq = ""; + } + return false; +} + +String WebServer::_getRandomHexString() { + char buffer[33]; // buffer to hold 32 Hex Digit + /0 + int i; + for (i = 0; i < 4; i++) { + sprintf(buffer + (i * 8), "%08x", rand()); + } + return String(buffer); +} + +void WebServer::requestAuthentication(HTTPAuthMethod mode, const char *realm, const String &authFailMsg) { + if (realm == NULL) { + _srealm = String(F("Login Required")); + } else { + _srealm = String(realm); + } + if (mode == BASIC_AUTH) { + sendHeader(String(FPSTR(WWW_Authenticate)), String(F("Basic realm=\"")) + _srealm + String(F("\""))); + } else { + _snonce = _getRandomHexString(); + _sopaque = _getRandomHexString(); + sendHeader( + String(FPSTR(WWW_Authenticate)), + String(F("Digest realm=\"")) + _srealm + String(F("\", qop=\"auth\", nonce=\"")) + _snonce + + String(F("\", opaque=\"")) + _sopaque + String(F("\"")) + ); + } + using namespace mime; + send(401, String(FPSTR(mimeTable[html].mimeType)), authFailMsg); +} + +void WebServer::on(const Uri &uri, WebServer::THandlerFunction handler) { + on(uri, HTTP_ANY, handler); +} + +void WebServer::on(const Uri &uri, HTTPMethod method, WebServer::THandlerFunction fn) { + on(uri, method, fn, _fileUploadHandler); +} + +void WebServer::on(const Uri &uri, HTTPMethod method, WebServer::THandlerFunction fn, WebServer::THandlerFunction ufn) { + _addRequestHandler(new FunctionRequestHandler(fn, ufn, uri, method)); +} + +void WebServer::addHandler(RequestHandler *handler) { + _addRequestHandler(handler); +} + +void WebServer::_addRequestHandler(RequestHandler *handler) { + if (!_lastHandler) { + _firstHandler = handler; + _lastHandler = handler; + } else { + _lastHandler->next(handler); + _lastHandler = handler; + } +} + +void WebServer::serveStatic(const char *uri, FS &fs, const char *path, const char *cache_header) { + _addRequestHandler(new StaticRequestHandler(fs, path, uri, cache_header)); +} + +void WebServer::handleClient() { + if (_currentStatus == HC_NONE) { + WiFiClient client = _server.available(); + if (!client) { + if (_nullDelay) { + delay(1); + } + return; + } + + log_v("New client: client.localIP()=%s", client.localIP().toString().c_str()); + + _currentClient = client; + _currentStatus = HC_WAIT_READ; + _statusChange = millis(); + } + + bool keepCurrentClient = false; + bool callYield = false; + + if (_currentClient.connected()) { + switch (_currentStatus) { + case HC_NONE: + // No-op to avoid C++ compiler warning + break; + case HC_WAIT_READ: + // Wait for data from client to become available + if (_currentClient.available()) { + if (_parseRequest(_currentClient)) { + // 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; + // } + } + } else { // !_currentClient.available() + if (millis() - _statusChange <= HTTP_MAX_DATA_WAIT) { + keepCurrentClient = true; + } + callYield = true; + } + break; + case HC_WAIT_CLOSE: + // Wait for client to close the connection + if (millis() - _statusChange <= HTTP_MAX_CLOSE_WAIT) { + keepCurrentClient = true; + callYield = true; + } + } + } + + if (!keepCurrentClient) { + _currentClient = WiFiClient(); + _currentStatus = HC_NONE; + _currentUpload.reset(); + } + + if (callYield) { + yield(); + } +} + +void WebServer::close() { + _server.close(); + _currentStatus = HC_NONE; + if (!_headerKeysCount) + collectHeaders(0, 0); +} + +void WebServer::stop() { + close(); +} + +void WebServer::sendHeader(const String &name, const String &value, bool first) { + String headerLine = name; + headerLine += F(": "); + headerLine += value; + headerLine += "\r\n"; + + if (first) { + _responseHeaders = headerLine + _responseHeaders; + } else { + _responseHeaders += headerLine; + } +} + +void WebServer::setContentLength(const size_t contentLength) { + _contentLength = contentLength; +} + +void WebServer::enableDelay(boolean value) { + _nullDelay = value; +} + +void WebServer::enableCORS(boolean value) { + _corsEnabled = value; +} + +void WebServer::enableCrossOrigin(boolean value) { + enableCORS(value); +} + +void WebServer::_prepareHeader(String &response, int code, const char *content_type, size_t contentLength) { + response = String(F("HTTP/1.")) + String(_currentVersion) + ' '; + response += String(code); + response += ' '; + response += _responseCodeToString(code); + response += "\r\n"; + + using namespace mime; + if (!content_type) + content_type = mimeTable[html].mimeType; + + sendHeader(String(F("Content-Type")), String(FPSTR(content_type)), true); + if (_contentLength == CONTENT_LENGTH_NOT_SET) { + sendHeader(String(FPSTR(Content_Length)), String(contentLength)); + } else if (_contentLength != CONTENT_LENGTH_UNKNOWN) { + sendHeader(String(FPSTR(Content_Length)), String(_contentLength)); + } else if (_contentLength == CONTENT_LENGTH_UNKNOWN && _currentVersion) { // HTTP/1.1 or above client + // let's do chunked + _chunked = true; + sendHeader(String(F("Accept-Ranges")), String(F("none"))); + sendHeader(String(F("Transfer-Encoding")), String(F("chunked"))); + } + if (_corsEnabled) { + sendHeader(String(FPSTR("Access-Control-Allow-Origin")), String("*")); + sendHeader(String(FPSTR("Access-Control-Allow-Methods")), String("*")); + sendHeader(String(FPSTR("Access-Control-Allow-Headers")), String("*")); + } + sendHeader(String(F("Connection")), String(F("close"))); + + response += _responseHeaders; + response += "\r\n"; + _responseHeaders = ""; +} + +void WebServer::send(int code, const char *content_type, const String &content) { + String header; + // Can we asume the following? + // if(code == 200 && content.length() == 0 && _contentLength == CONTENT_LENGTH_NOT_SET) + // _contentLength = CONTENT_LENGTH_UNKNOWN; + _prepareHeader(header, code, content_type, content.length()); + _currentClientWrite(header.c_str(), header.length()); + if (content.length()) + sendContent(content); +} + +void WebServer::send_P(int code, PGM_P content_type, PGM_P content) { + size_t contentLength = 0; + + if (content != NULL) { + contentLength = strlen_P(content); + } + + String header; + char type[64]; + strncpy_P(type, (PGM_P)content_type, sizeof(type)); + _prepareHeader(header, code, (const char *)type, contentLength); + _currentClientWrite(header.c_str(), header.length()); + sendContent_P(content); +} + +void WebServer::send_P(int code, PGM_P content_type, PGM_P content, size_t contentLength) { + String header; + char type[64]; + strncpy_P(type, (PGM_P)content_type, sizeof(type)); + _prepareHeader(header, code, (const char *)type, contentLength); + sendContent(header); + sendContent_P(content, contentLength); +} + +void WebServer::send(int code, char *content_type, const String &content) { + send(code, (const char *)content_type, content); +} + +void WebServer::send(int code, const String &content_type, const String &content) { + send(code, (const char *)content_type.c_str(), content); +} + +void WebServer::sendContent(const String &content) { + sendContent(content.c_str(), content.length()); +} + +void WebServer::sendContent(const char *content, size_t contentLength) { + const char *footer = "\r\n"; + if (_chunked) { + char *chunkSize = (char *)malloc(11); + if (chunkSize) { + sprintf(chunkSize, "%x%s", contentLength, footer); + _currentClientWrite(chunkSize, strlen(chunkSize)); + free(chunkSize); + } + } + _currentClientWrite(content, contentLength); + if (_chunked) { + _currentClient.write(footer, 2); + if (contentLength == 0) { + _chunked = false; + } + } +} + +void WebServer::sendContent_P(PGM_P content) { + sendContent_P(content, strlen_P(content)); +} + +void WebServer::sendContent_P(PGM_P content, size_t size) { + const char *footer = "\r\n"; + if (_chunked) { + char *chunkSize = (char *)malloc(11); + if (chunkSize) { + sprintf(chunkSize, "%x%s", size, footer); + _currentClientWrite(chunkSize, strlen(chunkSize)); + free(chunkSize); + } + } + _currentClientWrite_P(content, size); + if (_chunked) { + _currentClient.write(footer, 2); + if (size == 0) { + _chunked = false; + } + } +} + +void WebServer::_streamFileCore(const size_t fileSize, const String &fileName, const String &contentType) { + using namespace mime; + setContentLength(fileSize); + if (fileName.endsWith(String(FPSTR(mimeTable[gz].endsWith))) && + contentType != String(FPSTR(mimeTable[gz].mimeType)) && + contentType != String(FPSTR(mimeTable[none].mimeType))) { + sendHeader(F("Content-Encoding"), F("gzip")); + } + send(200, contentType, ""); +} + +String WebServer::pathArg(unsigned int i) { + if (_currentHandler != nullptr) + return _currentHandler->pathArg(i); + return ""; +} + +String WebServer::arg(String name) { + for (int j = 0; j < _postArgsLen; ++j) { + if (_postArgs[j].key == name) + return _postArgs[j].value; + } + for (int i = 0; i < _currentArgCount; ++i) { + if (_currentArgs[i].key == name) + return _currentArgs[i].value; + } + return ""; +} + +String WebServer::arg(int i) { + if (i < _currentArgCount) + return _currentArgs[i].value; + return ""; +} + +String WebServer::argName(int i) { + if (i < _currentArgCount) + return _currentArgs[i].key; + return ""; +} + +int WebServer::args() { + return _currentArgCount; +} + +bool WebServer::hasArg(String name) { + for (int j = 0; j < _postArgsLen; ++j) { + if (_postArgs[j].key == name) + return true; + } + for (int i = 0; i < _currentArgCount; ++i) { + if (_currentArgs[i].key == name) + return true; + } + return false; +} + +String WebServer::header(String name) { + for (int i = 0; i < _headerKeysCount; ++i) { + if (_currentHeaders[i].key.equalsIgnoreCase(name)) + return _currentHeaders[i].value; + } + return ""; +} + +void WebServer::collectHeaders(const char *headerKeys[], const size_t headerKeysCount) { + _headerKeysCount = headerKeysCount + 1; + if (_currentHeaders) + delete[] _currentHeaders; + _currentHeaders = new RequestArgument[_headerKeysCount]; + _currentHeaders[0].key = FPSTR(AUTHORIZATION_HEADER); + for (int i = 1; i < _headerKeysCount; i++) { + _currentHeaders[i].key = headerKeys[i - 1]; + } +} + +String WebServer::header(int i) { + if (i < _headerKeysCount) + return _currentHeaders[i].value; + return ""; +} + +String WebServer::headerName(int i) { + if (i < _headerKeysCount) + return _currentHeaders[i].key; + return ""; +} + +int WebServer::headers() { + return _headerKeysCount; +} + +bool WebServer::hasHeader(String name) { + for (int i = 0; i < _headerKeysCount; ++i) { + if ((_currentHeaders[i].key.equalsIgnoreCase(name)) && (_currentHeaders[i].value.length() > 0)) + return true; + } + return false; +} + +String WebServer::hostHeader() { + return _hostHeader; +} + +void WebServer::onFileUpload(THandlerFunction fn) { + _fileUploadHandler = fn; +} + +void WebServer::onNotFound(THandlerFunction fn) { + _notFoundHandler = fn; +} + +void WebServer::_handleRequest() { + bool handled = false; + if (!_currentHandler) { + log_e("request handler not found"); + } else { + handled = _currentHandler->handle(*this, _currentMethod, _currentUri); + if (!handled) { + log_e("request handler failed to handle request"); + } + } + if (!handled && _notFoundHandler) { + _notFoundHandler(); + handled = true; + } + if (!handled) { + using namespace mime; + send(404, String(FPSTR(mimeTable[html].mimeType)), String(F("Not found: ")) + _currentUri); + handled = true; + } + if (handled) { + _finalizeResponse(); + } + _currentUri = ""; +} + +void WebServer::_finalizeResponse() { + if (_chunked) { + sendContent(""); + } +} + +String WebServer::_responseCodeToString(int code) { + switch (code) { + case 100: + return F("Continue"); + case 101: + return F("Switching Protocols"); + case 200: + return F("OK"); + case 201: + return F("Created"); + case 202: + return F("Accepted"); + case 203: + return F("Non-Authoritative Information"); + case 204: + return F("No Content"); + case 205: + return F("Reset Content"); + case 206: + return F("Partial Content"); + case 300: + return F("Multiple Choices"); + case 301: + return F("Moved Permanently"); + case 302: + return F("Found"); + case 303: + return F("See Other"); + case 304: + return F("Not Modified"); + case 305: + return F("Use Proxy"); + case 307: + return F("Temporary Redirect"); + case 400: + return F("Bad Request"); + case 401: + return F("Unauthorized"); + case 402: + return F("Payment Required"); + case 403: + return F("Forbidden"); + case 404: + return F("Not Found"); + case 405: + return F("Method Not Allowed"); + case 406: + return F("Not Acceptable"); + case 407: + return F("Proxy Authentication Required"); + case 408: + return F("Request Time-out"); + case 409: + return F("Conflict"); + case 410: + return F("Gone"); + case 411: + return F("Length Required"); + case 412: + return F("Precondition Failed"); + case 413: + return F("Request Entity Too Large"); + case 414: + return F("Request-URI Too Large"); + case 415: + return F("Unsupported Media Type"); + case 416: + return F("Requested range not satisfiable"); + case 417: + return F("Expectation Failed"); + case 500: + return F("Internal Server Error"); + case 501: + return F("Not Implemented"); + case 502: + return F("Bad Gateway"); + case 503: + return F("Service Unavailable"); + case 504: + return F("Gateway Time-out"); + case 505: + return F("HTTP Version not supported"); + default: + return F(""); + } +} diff --git a/arduino/libretuya/libraries/WebServer/WebServer.h b/arduino/libretuya/libraries/WebServer/WebServer.h new file mode 100644 index 0000000..f89502a --- /dev/null +++ b/arduino/libretuya/libraries/WebServer/WebServer.h @@ -0,0 +1,224 @@ +/* + WebServer.h - Dead simple web-server. + Supports only one simultaneous client, knows how to handle GET and POST. + + Copyright (c) 2014 Ivan Grokhotkov. All rights reserved. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + Modified 8 May 2015 by Hristo Gochkov (proper post and file upload handling) +*/ + +#pragma once + +#include "HTTP_Method.h" +#include "Uri.h" +#include +#include +#include + +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 }; + +#define HTTP_DOWNLOAD_UNIT_SIZE 1436 + +#ifndef HTTP_UPLOAD_BUFLEN +#define HTTP_UPLOAD_BUFLEN 1436 +#endif + +#define HTTP_MAX_DATA_WAIT 5000 // ms to wait for the client to send the request +#define HTTP_MAX_POST_WAIT 5000 // ms to wait for POST data to arrive +#define HTTP_MAX_SEND_WAIT 5000 // ms to wait for data chunk to be ACKed +#define HTTP_MAX_CLOSE_WAIT 2000 // ms to wait for the client to close the connection + +#define CONTENT_LENGTH_UNKNOWN ((size_t)-1) +#define CONTENT_LENGTH_NOT_SET ((size_t)-2) + +class WebServer; + +typedef struct { + HTTPUploadStatus status; + String filename; + String name; + String type; + size_t totalSize; // file size + size_t currentSize; // size of data currently in buf + uint8_t buf[HTTP_UPLOAD_BUFLEN]; +} HTTPUpload; + +#include "detail/RequestHandler.h" + +namespace fs { +class FS; +} + +class WebServer { + public: + WebServer(IPAddress addr, int port = 80); + WebServer(int port = 80); + virtual ~WebServer(); + + virtual void begin(); + virtual void begin(uint16_t port); + virtual void handleClient(); + + virtual void close(); + void stop(); + + bool authenticate(const char *username, const char *password); + void requestAuthentication( + HTTPAuthMethod mode = BASIC_AUTH, const char *realm = NULL, const String &authFailMsg = String("") + ); + + typedef std::function THandlerFunction; + void on(const Uri &uri, THandlerFunction fn); + void on(const Uri &uri, HTTPMethod method, THandlerFunction fn); + void on(const Uri &uri, HTTPMethod method, THandlerFunction fn, THandlerFunction ufn); // ufn handles file uploads + void addHandler(RequestHandler *handler); + void serveStatic(const char *uri, fs::FS &fs, const char *path, const char *cache_header = NULL); + void onNotFound(THandlerFunction fn); // called when handler is not assigned + void onFileUpload(THandlerFunction ufn); // handle file uploads + + String uri() { + return _currentUri; + } + + HTTPMethod method() { + return _currentMethod; + } + + virtual WiFiClient client() { + return _currentClient; + } + + HTTPUpload &upload() { + return *_currentUpload; + } + + String pathArg(unsigned int i); // get request path argument by number + String arg(String name); // get request argument value by name + String arg(int i); // get request argument value by number + String argName(int i); // get request argument name by number + int args(); // get arguments count + bool hasArg(String name); // check if argument exists + void collectHeaders(const char *headerKeys[], const size_t headerKeysCount); // set the request headers to collect + String header(String name); // get request header value by name + String header(int i); // get request header value by number + String headerName(int i); // get request header name by number + int headers(); // get header count + bool hasHeader(String name); // check if header exists + + String hostHeader(); // get request host header if available or empty String if not + + // send response to the client + // code - HTTP response code, can be 200 or 404 + // content_type - HTTP content type, like "text/plain" or "image/png" + // content - actual content body + void send(int code, const char *content_type = NULL, const String &content = String("")); + void send(int code, char *content_type, const String &content); + void send(int code, const String &content_type, const String &content); + void send_P(int code, PGM_P content_type, PGM_P content); + void send_P(int code, PGM_P content_type, PGM_P content, size_t contentLength); + + void enableDelay(boolean value); + void enableCORS(boolean value = true); + void enableCrossOrigin(boolean value = true); + + void setContentLength(const size_t contentLength); + void sendHeader(const String &name, const String &value, bool first = false); + void sendContent(const String &content); + void sendContent(const char *content, size_t contentLength); + void sendContent_P(PGM_P content); + void sendContent_P(PGM_P content, size_t size); + + static String urlDecode(const String &text); + + template + size_t streamFile(T &file, const String &contentType) { + _streamFileCore(file.size(), file.name(), contentType); + return _currentClient.write(file); + } + + 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); + void _parseArguments(String data); + static String _responseCodeToString(int code); + bool _parseForm(WiFiClient &client, String boundary, uint32_t len); + bool _parseFormUploadAborted(); + void _uploadWriteByte(uint8_t b); + int _uploadReadByte(WiFiClient &client); + void _prepareHeader(String &response, int code, const char *content_type, size_t contentLength); + 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 ¶m, const char delimit = '"'); + + struct RequestArgument { + String key; + String value; + }; + + boolean _corsEnabled; + WiFiServer _server; + + WiFiClient _currentClient; + HTTPMethod _currentMethod; + String _currentUri; + uint8_t _currentVersion; + HTTPClientStatus _currentStatus; + unsigned long _statusChange; + boolean _nullDelay; + + RequestHandler *_currentHandler; + RequestHandler *_firstHandler; + RequestHandler *_lastHandler; + THandlerFunction _notFoundHandler; + THandlerFunction _fileUploadHandler; + + int _currentArgCount; + RequestArgument *_currentArgs; + int _postArgsLen; + RequestArgument *_postArgs; + + std::unique_ptr _currentUpload; + + int _headerKeysCount; + RequestArgument *_currentHeaders; + size_t _contentLength; + String _responseHeaders; + + String _hostHeader; + bool _chunked; + + String _snonce; // Store noance and opaque for future comparison + String _sopaque; + String _srealm; // Store the Auth realm between Calls +}; diff --git a/arduino/libretuya/libraries/WebServer/detail/RequestHandler.h b/arduino/libretuya/libraries/WebServer/detail/RequestHandler.h new file mode 100644 index 0000000..7b6279a --- /dev/null +++ b/arduino/libretuya/libraries/WebServer/detail/RequestHandler.h @@ -0,0 +1,53 @@ +#pragma once + +#include +#include + +class RequestHandler { + public: + virtual ~RequestHandler() {} + + virtual bool canHandle(HTTPMethod method, String uri) { + (void)method; + (void)uri; + return false; + } + + virtual bool canUpload(String uri) { + (void)uri; + return false; + } + + virtual bool handle(WebServer &server, HTTPMethod requestMethod, String requestUri) { + (void)server; + (void)requestMethod; + (void)requestUri; + return false; + } + + virtual void upload(WebServer &server, String requestUri, HTTPUpload &upload) { + (void)server; + (void)requestUri; + (void)upload; + } + + RequestHandler *next() { + return _next; + } + + void next(RequestHandler *r) { + _next = r; + } + + private: + RequestHandler *_next = nullptr; + + protected: + std::vector pathArgs; + + public: + const String &pathArg(unsigned int i) { + assert(i < pathArgs.size()); + return pathArgs[i]; + } +}; diff --git a/arduino/libretuya/libraries/WebServer/detail/RequestHandlersImpl.h b/arduino/libretuya/libraries/WebServer/detail/RequestHandlersImpl.h new file mode 100644 index 0000000..8d0cd9d --- /dev/null +++ b/arduino/libretuya/libraries/WebServer/detail/RequestHandlersImpl.h @@ -0,0 +1,149 @@ +#pragma once + +#include "RequestHandler.h" +#include "Uri.h" +#include "WString.h" +#include "mimetable.h" + +using namespace mime; + +class FunctionRequestHandler : public RequestHandler { + public: + FunctionRequestHandler( + WebServer::THandlerFunction fn, WebServer::THandlerFunction ufn, const Uri &uri, HTTPMethod method + ) + : _fn(fn), _ufn(ufn), _uri(uri.clone()), _method(method) { + _uri->initPathArgs(pathArgs); + } + + ~FunctionRequestHandler() { + delete _uri; + } + + bool canHandle(HTTPMethod requestMethod, String requestUri) override { + if (_method != HTTP_ANY && _method != requestMethod) + return false; + + return _uri->canHandle(requestUri, pathArgs); + } + + bool canUpload(String requestUri) override { + if (!_ufn || !canHandle(HTTP_POST, requestUri)) + return false; + + return true; + } + + bool handle(WebServer &server, HTTPMethod requestMethod, String requestUri) override { + (void)server; + if (!canHandle(requestMethod, requestUri)) + return false; + + _fn(); + return true; + } + + void upload(WebServer &server, String requestUri, HTTPUpload &upload) override { + (void)server; + (void)upload; + if (canUpload(requestUri)) + _ufn(); + } + + protected: + WebServer::THandlerFunction _fn; + WebServer::THandlerFunction _ufn; + Uri *_uri; + HTTPMethod _method; +}; + +class StaticRequestHandler : public RequestHandler { + public: + StaticRequestHandler(FS &fs, const char *path, const char *uri, const char *cache_header) + : _fs(fs), _uri(uri), _path(path), _cache_header(cache_header) { + File f = fs.open(path); + _isFile = (f && (!f.isDirectory())); + log_v( + "StaticRequestHandler: path=%s uri=%s isFile=%d, cache_header=%s\r\n", + path, + uri, + _isFile, + cache_header ? cache_header : "" + ); // issue 5506 - cache_header can be nullptr + _baseUriLength = _uri.length(); + } + + bool canHandle(HTTPMethod requestMethod, String requestUri) override { + if (requestMethod != HTTP_GET) + return false; + + if ((_isFile && requestUri != _uri) || !requestUri.startsWith(_uri)) + return false; + + return true; + } + + bool handle(WebServer &server, HTTPMethod requestMethod, String requestUri) override { + if (!canHandle(requestMethod, requestUri)) + return false; + + log_v("StaticRequestHandler::handle: request=%s _uri=%s\r\n", requestUri.c_str(), _uri.c_str()); + + String path(_path); + + if (!_isFile) { + // Base URI doesn't point to a file. + // If a directory is requested, look for index file. + if (requestUri.endsWith("/")) + requestUri += "index.htm"; + + // Append whatever follows this URI in request to get the file path. + path += requestUri.substring(_baseUriLength); + } + log_v("StaticRequestHandler::handle: path=%s, isFile=%d\r\n", path.c_str(), _isFile); + + String contentType = 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... + if (!path.endsWith(FPSTR(mimeTable[gz].endsWith)) && !_fs.exists(path)) { + String pathWithGz = path + FPSTR(mimeTable[gz].endsWith); + if (_fs.exists(pathWithGz)) + path += FPSTR(mimeTable[gz].endsWith); + } + + File f = _fs.open(path, "r"); + if (!f || !f.available()) + return false; + + if (_cache_header.length() != 0) + server.sendHeader("Cache-Control", _cache_header); + + server.streamFile(f, contentType); + 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; + String _path; + String _cache_header; + bool _isFile; + size_t _baseUriLength; +}; diff --git a/arduino/libretuya/libraries/WebServer/detail/mimetable.cpp b/arduino/libretuya/libraries/WebServer/detail/mimetable.cpp new file mode 100644 index 0000000..7790950 --- /dev/null +++ b/arduino/libretuya/libraries/WebServer/detail/mimetable.cpp @@ -0,0 +1,33 @@ +#include "mimetable.h" +#include "pgmspace.h" + +namespace mime { + +// Table of extension->MIME strings stored in PROGMEM, needs to be global due to GCC section typing rules +const Entry mimeTable[maxType] = { + {".html", "text/html" }, + {".htm", "text/html" }, + {".css", "text/css" }, + {".txt", "text/plain" }, + {".js", "application/javascript" }, + {".json", "application/json" }, + {".png", "image/png" }, + {".gif", "image/gif" }, + {".jpg", "image/jpeg" }, + {".ico", "image/x-icon" }, + {".svg", "image/svg+xml" }, + {".ttf", "application/x-font-ttf" }, + {".otf", "application/x-font-opentype" }, + {".woff", "application/font-woff" }, + {".woff2", "application/font-woff2" }, + {".eot", "application/vnd.ms-fontobject"}, + {".sfnt", "application/font-sfnt" }, + {".xml", "text/xml" }, + {".pdf", "application/pdf" }, + {".zip", "application/zip" }, + {".gz", "application/x-gzip" }, + {".appcache", "text/cache-manifest" }, + {"", "application/octet-stream" } +}; + +} // namespace mime diff --git a/arduino/libretuya/libraries/WebServer/detail/mimetable.h b/arduino/libretuya/libraries/WebServer/detail/mimetable.h new file mode 100644 index 0000000..9e5dd8b --- /dev/null +++ b/arduino/libretuya/libraries/WebServer/detail/mimetable.h @@ -0,0 +1,38 @@ +#pragma once + +namespace mime { + +enum type { + html, + htm, + css, + txt, + js, + json, + png, + gif, + jpg, + ico, + svg, + ttf, + otf, + woff, + woff2, + eot, + sfnt, + xml, + pdf, + zip, + gz, + appcache, + none, + maxType +}; + +struct Entry { + const char endsWith[16]; + const char mimeType[32]; +}; + +extern const Entry mimeTable[maxType]; +} // namespace mime diff --git a/arduino/libretuya/libraries/WebServer/uri/UriBraces.h b/arduino/libretuya/libraries/WebServer/uri/UriBraces.h new file mode 100644 index 0000000..b641f4b --- /dev/null +++ b/arduino/libretuya/libraries/WebServer/uri/UriBraces.h @@ -0,0 +1,61 @@ +#pragma once + +#include "Uri.h" + +class UriBraces : public Uri { + + public: + explicit UriBraces(const char *uri) : Uri(uri){}; + explicit UriBraces(const String &uri) : Uri(uri){}; + + Uri *clone() const override final { + return new UriBraces(_uri); + }; + + void initPathArgs(std::vector &pathArgs) override final { + int numParams = 0, start = 0; + do { + start = _uri.indexOf("{}", start); + if (start > 0) { + numParams++; + start += 2; + } + } while (start > 0); + pathArgs.resize(numParams); + } + + bool canHandle(const String &requestUri, std::vector &pathArgs) override final { + if (Uri::canHandle(requestUri, pathArgs)) + return true; + + size_t uriLength = _uri.length(); + unsigned int pathArgIndex = 0; + unsigned int requestUriIndex = 0; + for (unsigned int i = 0; i < uriLength; i++, requestUriIndex++) { + char uriChar = _uri[i]; + char requestUriChar = requestUri[requestUriIndex]; + + if (uriChar == requestUriChar) + continue; + if (uriChar != '{') + return false; + + i += 2; // index of char after '}' + if (i >= uriLength) { + // there is no char after '}' + pathArgs[pathArgIndex] = requestUri.substring(requestUriIndex); + return pathArgs[pathArgIndex].indexOf("/") == -1; // path argument may not contain a '/' + } else { + char charEnd = _uri[i]; + int uriIndex = requestUri.indexOf(charEnd, requestUriIndex); + if (uriIndex < 0) + return false; + pathArgs[pathArgIndex] = requestUri.substring(requestUriIndex, uriIndex); + requestUriIndex = (unsigned int)uriIndex; + } + pathArgIndex++; + } + + return requestUriIndex >= requestUri.length(); + } +}; diff --git a/arduino/libretuya/libraries/WebServer/uri/UriGlob.h b/arduino/libretuya/libraries/WebServer/uri/UriGlob.h new file mode 100644 index 0000000..243a98d --- /dev/null +++ b/arduino/libretuya/libraries/WebServer/uri/UriGlob.h @@ -0,0 +1,19 @@ +#pragma once + +#include "Uri.h" +#include + +class UriGlob : public Uri { + + public: + explicit UriGlob(const char *uri) : Uri(uri){}; + explicit UriGlob(const String &uri) : Uri(uri){}; + + Uri *clone() const override final { + return new UriGlob(_uri); + }; + + bool canHandle(const String &requestUri, __attribute__((unused)) std::vector &pathArgs) override final { + return fnmatch(_uri.c_str(), requestUri.c_str(), 0) == 0; + } +}; diff --git a/arduino/libretuya/libraries/WebServer/uri/UriRegex.h b/arduino/libretuya/libraries/WebServer/uri/UriRegex.h new file mode 100644 index 0000000..176300d --- /dev/null +++ b/arduino/libretuya/libraries/WebServer/uri/UriRegex.h @@ -0,0 +1,41 @@ +#pragma once + +#include "Uri.h" +#include + +class UriRegex : public Uri { + + public: + explicit UriRegex(const char *uri) : Uri(uri){}; + explicit UriRegex(const String &uri) : Uri(uri){}; + + Uri *clone() const override final { + return new UriRegex(_uri); + }; + + void initPathArgs(std::vector &pathArgs) override final { + std::regex rgx((_uri + "|").c_str()); + std::smatch matches; + std::string s{""}; + std::regex_search(s, matches, rgx); + pathArgs.resize(matches.size() - 1); + } + + bool canHandle(const String &requestUri, std::vector &pathArgs) override final { + if (Uri::canHandle(requestUri, pathArgs)) + return true; + + unsigned int pathArgIndex = 0; + std::regex rgx(_uri.c_str()); + std::smatch matches; + std::string s(requestUri.c_str()); + if (std::regex_search(s, matches, rgx)) { + for (size_t i = 1; i < matches.size(); ++i) { // skip first + pathArgs[pathArgIndex] = String(matches[i].str().c_str()); + pathArgIndex++; + } + return true; + } + return false; + } +}; diff --git a/builder/arduino-common.py b/builder/arduino-common.py index 1b1cfd1..5185466 100644 --- a/builder/arduino-common.py +++ b/builder/arduino-common.py @@ -21,6 +21,7 @@ env.Prepend( join(LT_API_DIR, "compat"), join(LT_API_DIR, "libraries", "base64"), join(LT_API_DIR, "libraries", "HTTPClient"), + join(LT_API_DIR, "libraries", "WebServer"), join(LT_API_DIR, "libraries", "WiFiMulti"), # fmt: on ],