Files
Last update 6 years 10 months
by
Nick
webcc.h/* * Copyright (C) 2017 Nick Naumenko (https://github.com/nnaumenko) * All rights reserved * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ /** * @file * @brief Provides webserver infrastructure. */ #ifndef WEBCC_H #define WEBCC_H #include <Arduino.h> #include <ESP8266WiFi.h> #include "module.h" #include "util_comm.h" namespace webcc { ////////////////////////////////////////////////////////////////////// // Texts & TextsUI ////////////////////////////////////////////////////////////////////// #define MODULE_TEXT(name,value) public: const char name [sizeof(value)] = value /// String literals used internally by WebCC module class Texts { MODULE_TEXT(moduleName, "WebCC"); MODULE_TEXT(rootPath, "/"); MODULE_TEXT(indexPath, "/index"); MODULE_TEXT(crlf, "\r\n"); } __attribute__((packed)); /// String literals used in UI and visible to user class TextsUI { MODULE_TEXT(errorNone, "No error"); MODULE_TEXT(errorUnknown, "Unknown error"); MODULE_TEXT(errorServerNotInitialised, "WiFiServer not initialised"); MODULE_TEXT(errorParserError, "HTTP Request parser reported an error"); MODULE_TEXT(errorMethodNotAccepted, "HTTP method not accepted"); MODULE_TEXT(errorPathNotAccepted, "Path not found"); MODULE_TEXT(errorPathConflict, "Module path conflict"); MODULE_TEXT(parseErrorNone, "No HTTP request parser error"); MODULE_TEXT(parseErrorUnknown, "Unknown HTTP request parser error"); MODULE_TEXT(parseErrorInternal, "HTTP request parser internal error"); MODULE_TEXT(parseErrorRequestPartTooLong, "Request part is too long"); MODULE_TEXT(parseErrorRequestStructure, "Bad request"); MODULE_TEXT(parseErrorRequestSemantics, "Request contains conflicting information"); MODULE_TEXT(webClientConnected, "Web client connected: "); MODULE_TEXT(beginParsing, "Begin parsing request"); MODULE_TEXT(endParsing, "End parsing request"); MODULE_TEXT(printMethod, "Method: "); MODULE_TEXT(printPath, "Path: "); MODULE_TEXT(parsingError, "Error parsing request"); MODULE_TEXT(httpStatusCode, "HTTP status code: "); MODULE_TEXT(redirectTo, "Redirecting to: "); MODULE_TEXT(sendModuleIndexBegin, "Sending module index"); MODULE_TEXT(sendModuleIndexEnd, "Sending module sent"); MODULE_TEXT(rootCaption, "WebConfigControl"); MODULE_TEXT(moduleIndex, "Modules"); MODULE_TEXT(moduleIndexNameCaption, "Name"); MODULE_TEXT(moduleIndexLinkCaption, "Link"); } __attribute__((packed)); extern const Texts PROGMEM texts; extern const TextsUI PROGMEM textsUI; #undef MODULE_TEXT ////////////////////////////////////////////////////////////////////// // HTTPRequestPart ////////////////////////////////////////////////////////////////////// /// Parts of the HTTP request which are passed from HTTP Request Parser enum class HTTPRequestPart { NONE, ///< No request part passed by parser METHOD, ///< Method from request header (e.g. GET or POST) PATH, ///< Path from request header (e.g. /index.html) URL_QUERY_NAME, ///< Name from the URL query string item (e.g. name1 or name2 for URL /index?name1=value1&name2=value2) URL_QUERY_VALUE, ///< Value from the URL query string item (e.g. value1 or value2 for URL /index?name1=value1&name2=value2) HTTP_VERSION, ///< HTTP version from request header (e.g. HTTP/1.1) FIELD_NAME, ///< Name of the header field (e.g. Field-Name for field Field-Name:value1=value2;value3=value4) FIELD_VALUE_PART1, ///< First part of the header field value (e.g. value1 or value3 for field Field-Name:value1=value2;value3=value4) FIELD_VALUE_PART2, ///< Second part of the header field value (e.g. value2 or value4 for field Field-Name:value1=value2;value3=value4) POST_QUERY_NAME, ///< Name from the POST query string item (e.g. name1 or name2 for POST request body name1=value1&name2=value2) POST_QUERY_VALUE, ///< Value from the POST query string item (e.g. value1 or value2 for POST request body name1=value1&name2=value2) }; ////////////////////////////////////////////////////////////////////// // ParseError ////////////////////////////////////////////////////////////////////// /// Errors generated by HTTP request parser enum class ParseError { NONE, ///< No error INTERNAL_ERROR, ///< Parser's internal error REQUEST_PART_TOO_LONG, ///< Request part is too long to fit into buffer REQUEST_STRUCTURE, ///< Malformed request REQUEST_SEMANTICS ///< Request information is semantically invalid }; ////////////////////////////////////////////////////////////////////// // WebConfigControl ////////////////////////////////////////////////////////////////////// /// @brief Provides webserver infrastructure for software modules /// @details WebConfigControl processes HTTP requests and calls corresponding /// methods of the software modules listed under parameter pack WebModules. /// See ModuleWebServer for details. /// @tparam Diag diagnostic output Policy to direct messages to. Required /// to contain Severity enum member (see diag::DiagLog::Severity for details) /// and log() method which accepts Severity and then variable number of /// parameters (see diag::DiagLog::log() for details). If these requirements /// are not met, compilation will fail. /// @tparam Parser HTTP request parser Policy. Required to contain methods /// begin(), parse(), finished(), error() and getError() (see /// HTTPReqParserStateMachine for details). If these requirements are not met, /// compilation will fail. /// @tparam OutputStream Policy for data output. Output Stream is integrated /// between the software modules and web-client. All caching, buffering, /// post-processing before data is sent to web-client is performed by /// OutputStream. OutputStream must be derived from Print class. If these /// requirements are not met, compilation will fail. /// @tparam WebForm Used to generate HTML UI forms. See WebccForm for details. template <class Diag, class Parser, class OutputStream, class WebForm, class... WebModules> class WebConfigControl : public Module<WebConfigControl<Diag, Parser, OutputStream, WebForm, WebModules...>> { public: inline void setServer(WiFiServer &server); inline WiFiServer * getServer(void); public: inline const char * PROGMEM moduleName (void); inline const char * PROGMEM getMainPath(void); public: void onBegin(void); void onRun(void); public: inline void setRootRedirect(const char * redirectURL); inline const char * PROGMEM getRootRedirect(void) const; public: boolean onHTTPReqPath(const char * path); boolean onHTTPReqMethod(const char * method); boolean onRespond(Print &client); private: boolean pathRoot = false; ///< Set to true if root path ("/") is included in HTTP request private: WiFiServer * server = NULL; private: inline void callWebModulesOnStart(void); inline int callWebModulesOnPath(const char * path); inline boolean callWebModulesOnMethod(size_t index, const char *method); inline boolean callWebModulesOnURLQuery(size_t index, const char * name, const char * value); inline boolean callWebModulesOnPOSTQuery(size_t index, const char * name, const char * value); inline boolean callWebModulesOnRespond(size_t index, Print &client); inline boolean callWebModulesOnEnd(size_t index, boolean error); private: enum class WebccError { NONE, ///< No error SERVER_NOT_INITIALISED, ///< No WiFiServer was set with setServer PARSER_ERROR, ///< Error occured while parsing HTTP request (see ParseError) METHOD_NOT_ACCEPTED, ///< Method specified in HTTP request header was not accepted by module PATH_NOT_ACCEPTED, ///< Path specified in HTTP request header was not accepted by any of the modules PATH_CONFLICT ///< Path specified in HTTP request header was accepted by more than one module }; const __FlashStringHelper * getErrorMessage(WebccError error); const __FlashStringHelper * getParseErrorMessage(ParseError error); util::http::HTTPStatusCode getHTTPStatusCode(WebccError error, ParseError parseError); void handleErrors(OutputStream &output, WebccError error, const Parser &parser); private: static const int webModulesCallThisAccepted = -1; static const int webModulesCallNoneAccepted = -2; static const int webModulesCallMultipleAccepted = -3; const size_t httpRequestPartMaxSize = 33; private: const char * rootRedirect = NULL; private: static const size_t outputBufferSize = WIFICLIENT_MAX_PACKET_SIZE; uint8_t outputBuffer[outputBufferSize]; }; template <class Diag, class Parser, class OutputStream, class WebForm, class... WebModules> void WebConfigControl<Diag, Parser, OutputStream, WebForm, WebModules...>::setServer(WiFiServer &server) { this->server = &server; } template <class Diag, class Parser, class OutputStream, class WebForm, class... WebModules> WiFiServer * WebConfigControl<Diag, Parser, OutputStream, WebForm, WebModules...>::getServer(void) { return (server); } template <class Diag, class Parser, class OutputStream, class WebForm, class... WebModules> const char * PROGMEM WebConfigControl<Diag, Parser, OutputStream, WebForm, WebModules...>::moduleName (void) { /// Returns human-readable module name, implements interface method ModuleBase::moduleName(). return (texts.moduleName); } template <class Diag, class Parser, class OutputStream, class WebForm, class... WebModules> const char * PROGMEM WebConfigControl<Diag, Parser, OutputStream, WebForm, WebModules...>::getMainPath (void) { /// Returns default webserver path for this module, implements interface method ModuleWebServer::getMainPath(). return (texts.indexPath); } template <class Diag, class Parser, class OutputStream, class WebForm, class... WebModules> void WebConfigControl<Diag, Parser, OutputStream, WebForm, WebModules...>::onBegin(void) { /// Initialises webserver if (!server) { Diag::instance()->log(Diag::Severity::CRITICAL, getErrorMessage(WebccError::SERVER_NOT_INITIALISED)); } } template <class Diag, class Parser, class OutputStream, class WebForm, class... WebModules> void WebConfigControl<Diag, Parser, OutputStream, WebForm, WebModules...>::onRun(void) { /// @brief Processes incoming connections, passes HTTP Requests to the parser /// and calls ModuleWebServer methods implemented by software modules Diag * diagLog = Diag::instance(); if (!server) return; WiFiClient client = this->server->available(); if (!client) return; diagLog->log(Diag::Severity::INFORMATIONAL, FPSTR(textsUI.webClientConnected), client.remoteIP(), ':', client.remotePort()); while (!client.available()) { delay(1); yield(); } OutputStream outputClient(client, outputBuffer, outputBufferSize); Parser parser; parser.begin(client); diagLog->log(Diag::Severity::DEBUG, FPSTR(textsUI.beginParsing)); char tempBuffer[httpRequestPartMaxSize + 1];//+1 char for \0 tempBuffer[0] = '\0'; int indexModuleAccepted = webModulesCallNoneAccepted; WebccError error = WebccError::NONE; callWebModulesOnStart(); do { char readBuffer[httpRequestPartMaxSize + 1];//+1 char for \0 readBuffer[0] = '\0'; HTTPRequestPart reqPart = HTTPRequestPart::NONE; parser.parse(readBuffer, sizeof(readBuffer), &reqPart); yield(); if (parser.error()) { error = WebccError::PARSER_ERROR; break; } switch (reqPart) { case HTTPRequestPart::METHOD: diagLog->log(Diag::Severity::INFORMATIONAL, FPSTR(textsUI.printMethod), readBuffer); strncpy(tempBuffer, readBuffer, sizeof(tempBuffer) - 1); tempBuffer[sizeof(tempBuffer) - 1] = '\0'; break; case HTTPRequestPart::PATH: //In case of empty path, control will not be passed here, thus path check is needed in subsequent HTTP request parts diagLog->log(Diag::Severity::INFORMATIONAL, FPSTR(textsUI.printPath), readBuffer); indexModuleAccepted = callWebModulesOnPath(readBuffer); if (indexModuleAccepted == webModulesCallNoneAccepted) { error = WebccError::PATH_NOT_ACCEPTED; break; } if (indexModuleAccepted == webModulesCallMultipleAccepted) { error = WebccError::PATH_CONFLICT; diagLog->log(Diag::Severity::CRITICAL, getErrorMessage(error), ' ', readBuffer); //TODO: print list of the modules which accepted the same path break; } if (!callWebModulesOnMethod(indexModuleAccepted, tempBuffer)) { //tempBuffer at this point contains method error = WebccError::METHOD_NOT_ACCEPTED; break; } tempBuffer[0] = '\0'; case HTTPRequestPart::URL_QUERY_NAME: if (indexModuleAccepted == webModulesCallNoneAccepted) { error = WebccError::PATH_NOT_ACCEPTED; break; } strncpy(tempBuffer, readBuffer, sizeof(tempBuffer) - 1); tempBuffer[sizeof(tempBuffer) - 1] = '\0'; break; case HTTPRequestPart::URL_QUERY_VALUE: if (indexModuleAccepted == webModulesCallNoneAccepted) { error = WebccError::PATH_NOT_ACCEPTED; break; } callWebModulesOnURLQuery(indexModuleAccepted, tempBuffer, readBuffer); break; case HTTPRequestPart::POST_QUERY_NAME: if (indexModuleAccepted == webModulesCallNoneAccepted) { error = WebccError::PATH_NOT_ACCEPTED; break; } strncpy(tempBuffer, readBuffer, sizeof(tempBuffer) - 1); tempBuffer[sizeof(tempBuffer) - 1] = '\0'; break; case HTTPRequestPart::POST_QUERY_VALUE: if (indexModuleAccepted == webModulesCallNoneAccepted) { error = WebccError::PATH_NOT_ACCEPTED; break; } callWebModulesOnPOSTQuery(indexModuleAccepted, tempBuffer, readBuffer); break; default: break; } } while (!parser.finished()); diagLog->log(Diag::Severity::DEBUG, FPSTR(textsUI.endParsing)); if (error == WebccError::NONE) { callWebModulesOnRespond(indexModuleAccepted, outputClient); } else { diagLog->log(Diag::Severity::NOTICE, FPSTR(textsUI.parsingError)); handleErrors(outputClient, error, parser); } callWebModulesOnEnd(indexModuleAccepted, parser.error()); client.flush(); } template <class Diag, class Parser, class OutputStream, class WebForm, class... WebModules> void WebConfigControl<Diag, Parser, OutputStream, WebForm, WebModules...>::callWebModulesOnStart(void) { /// @brief Calls onHttpReqPath method of all modules in WebModules parameter pack this->onHTTPReqStart(); boolean callResult[] = { WebModules::instance()->onHTTPReqStart()... }; static_cast<void>(callResult); } template <class Diag, class Parser, class OutputStream, class WebForm, class... WebModules> int WebConfigControl<Diag, Parser, OutputStream, WebForm, WebModules...>::callWebModulesOnPath(const char * path) { /// @brief Calls onHTTPReqPath method of all modules in WebModules parameter pack and checks which of the /// WebModules did accept the paramteter pack /// @param path Path from HTTP request /// @return Index in WebModules parameter pack of the module which accepted parameter pack. If no module accepted /// the current path, this method returns WebCC::webModulesCallNoneAccepted. If more than one module accepted /// the current path, this method returns WebCC::webModulesCallMultipleAccepted. If this module (WebCC) did ' /// accept the current path, this method returns WebCC::webModulesCallThisAccepted; boolean thisModuleAccepted = this->onHTTPReqPath(path); boolean callResult[] = { WebModules::instance()->onHTTPReqPath(path)... }; int acceptCount = static_cast<int>(thisModuleAccepted), acceptedModuleIndex = webModulesCallNoneAccepted; for (size_t i = 0; i < sizeof(callResult); i++) { if (callResult[i]) { acceptCount++; acceptedModuleIndex = i; } } if (acceptCount > 1) return (webModulesCallMultipleAccepted); if (thisModuleAccepted) return (webModulesCallThisAccepted); return (acceptedModuleIndex); } template <class Diag, class Parser, class OutputStream, class WebForm, class... WebModules> boolean WebConfigControl<Diag, Parser, OutputStream, WebForm, WebModules...>::callWebModulesOnMethod(size_t index, const char *method) { /// @brief Calls onHttpReqMethod method of one module from WebModules parameter pack. /// @param index Index of the module in the parameter pack or WebCC::webModulesCallThisAccepted if this module /// previously accepted the path from HTTP request /// @param Dethod HTTP Method from the HTTP request /// @return Value returned by onHTTPReqMethod of the corresponding module if (index == webModulesCallThisAccepted) { return (this->onHTTPReqMethod(method)); } if (index < 0) return (false); size_t i = 0; int callResult[] = { ((i++ == index) ? WebModules::instance()->onHTTPReqMethod(method) : false)... }; return (callResult[index]); } template <class Diag, class Parser, class OutputStream, class WebForm, class... WebModules> boolean WebConfigControl<Diag, Parser, OutputStream, WebForm, WebModules...>::callWebModulesOnURLQuery(size_t index, const char * name, const char * value) { /// @brief Calls onHttpReqURLQuery method of one module from WebModules parameter pack. /// @param index Index of the module in the parameter pack or webModulesCallThisAccepted /// if this module previously accepted the path from HTTP request /// @param name Name of URL Query String Item from the HTTP request /// @param name Value of URL Query String Item from the HTTP request /// @return Value returned by onHttpReqURLQuery of the corresponding module if (index == webModulesCallThisAccepted) { return (this->onHTTPReqURLQuery(name, value)); } if (index < 0) return (false); int i = 0; int callResult[] = { ((i++ == index) ? WebModules::instance()->onHTTPReqURLQuery(name, value) : false)... }; return (callResult[index]); } template <class Diag, class Parser, class OutputStream, class WebForm, class... WebModules> boolean WebConfigControl<Diag, Parser, OutputStream, WebForm, WebModules...>::callWebModulesOnPOSTQuery(size_t index, const char * name, const char * value) { /// @brief Calls onHttpReqPOSTQuery method of one module from WebModules parameter pack. /// @param index Index of the module in the parameter pack or WebCC::webModulesCallThisAccepted if this module /// previously accepted the path from HTTP request /// @param name Name of POST Query String Item from the HTTP request /// @param name Value of POST Query String Item from the HTTP request /// @return Value returned by onHttpReqPOSTQuery of the corresponding module if (index == webModulesCallThisAccepted) { return (this->onHTTPReqPOSTQuery(name, value)); } if (index < 0) return (false); int i = 0; int callResult[] = { ((i++ == index) ? WebModules::instance()->onHTTPReqPOSTQuery(name, value) : false)... }; return (callResult[index]); } template <class Diag, class Parser, class OutputStream, class WebForm, class... WebModules> boolean WebConfigControl<Diag, Parser, OutputStream, WebForm, WebModules...>::callWebModulesOnRespond(size_t index, Print &client) { /// @brief Calls onRespond method of one module from WebModules parameter pack. /// @param index Index of the module in the parameter pack or WebCC::webModulesCallThisAccepted if this module /// previously accepted the path from HTTP request /// @param client Destination for the response to send to /// @return Value returned by onRespond method of the corresponding module if (index == webModulesCallThisAccepted) { return (this->onRespond(client)); } if (index < 0) return (false); int i = 0; int callResult[] = { ((i++ == index) ? WebModules::instance()->onRespond(client) : false)... }; return (callResult[index]); } template <class Diag, class Parser, class OutputStream, class WebForm, class... WebModules> boolean WebConfigControl<Diag, Parser, OutputStream, WebForm, WebModules...>::callWebModulesOnEnd(size_t index, boolean error) { /// @brief Calls onHTTPReqEnd method of all modules in WebModules parameter pack /// @param index Index of the module in the parameter pack or WebCC::webModulesCallThisAccepted if this module /// previously accepted the path from HTTP request /// @param error True if an error occured during HTTP request processing, false if request was processed with /// no errors /// @return Value returned by onHTTPReqEnd of the module boolean thisModuleReturnValue = this->onHTTPReqEnd(error); // if (index == webModulesCallThisAccepted) { // return (this->onHTTPReqEnd(error)); // } if (index == webModulesCallThisAccepted) return (thisModuleReturnValue); if (index < 0) return (false); //int i = 0; //int callResult[] = { ((i++ == index) ? WebModules::instance()->onHTTPReqEnd(error) : false)... }; int callResult[] = { WebModules::instance()->onHTTPReqEnd(error)... }; return (callResult[index]); } template <class Diag, class Parser, class OutputStream, class WebForm, class... WebModules> void WebConfigControl<Diag, Parser, OutputStream, WebForm, WebModules...>::setRootRedirect(const char * redirectURL) { /// @brief Sets path to redirect to when root URL path ("/") is provided in HTTP Request /// @param redirectURL URL path to redirect to (must be located in PROGMEM) rootRedirect = redirectURL; } template <class Diag, class Parser, class OutputStream, class WebForm, class... WebModules> const char * PROGMEM WebConfigControl<Diag, Parser, OutputStream, WebForm, WebModules...>::getRootRedirect(void) const { /// @brief Returns path to redirect to when root URL path ("/") is provided in HTTP Request /// @return URL path to redirect to (located in PROGMEM) return (rootRedirect); } template <class Diag, class Parser, class OutputStream, class WebForm, class... WebModules> boolean WebConfigControl<Diag, Parser, OutputStream, WebForm, WebModules...>::onHTTPReqPath(const char * path) { /// @brief Interface to integrate into webserver, implements interface method ModuleWebServer::onHTTPReqPath() if (!strcmp_P(path, texts.rootPath)) { pathRoot = true; return (true); } if (!strcmp_P(path, texts.indexPath)) { pathRoot = false; return (true); } return (false); } template <class Diag, class Parser, class OutputStream, class WebForm, class... WebModules> boolean WebConfigControl<Diag, Parser, OutputStream, WebForm, WebModules...>::onHTTPReqMethod(const char * method) { /// @brief Interface to integrate into webserver, implements interface method ModuleWebServer::onHTTPReqMethod() if (util::http::HTTPRequestHelper::getMethod(method) == util::http::HTTPRequestMethod::GET) return (true); return (false); } template <class Diag, class Parser, class OutputStream, class WebForm, class... WebModules> boolean WebConfigControl<Diag, Parser, OutputStream, WebForm, WebModules...>::onRespond(Print &client) { /// @brief Interface to integrate into webserver, implements interface method ModuleWebServer::onRespond() /// @details Produces HTML page with the list of the modules passed as a template parameter pack WebModules Diag * diagLog = Diag::instance(); if (pathRoot && rootRedirect) { util::http::HTTPResponseHeader::redirect(client, (__FlashStringHelper *)rootRedirect); diagLog->log(Diag::Severity::INFORMATIONAL, FPSTR(textsUI.redirectTo), (__FlashStringHelper *)rootRedirect); return (true); } diagLog->log(Diag::Severity::INFORMATIONAL, FPSTR(textsUI.sendModuleIndexBegin)); const char * paths[] = { (WebModules::instance()->getMainPath())... }; const char * names[] = { (WebModules::instance()->moduleName())... }; int moduleCount = sizeof...(WebModules); util::http::HTTPResponseHeader::contentHeader(client, util::http::HTTPContentType::HTML); WebForm index(client); index.bodyBegin(NULL, textsUI.rootCaption); index.sectionBegin(textsUI.moduleIndex); index.subsectionBegin(textsUI.moduleIndex); index.plaintext(textsUI.moduleIndexNameCaption, textsUI.moduleIndexLinkCaption); index.link(this->moduleName(), this->getMainPath(), this->getMainPath()); for (int i = 0; i < moduleCount; i++) { index.link(names[i], paths[i], paths[i]); } index.bodyEnd(); diagLog->log(Diag::Severity::DEBUG, FPSTR(textsUI.sendModuleIndexEnd)); return (true); } template <class Diag, class Parser, class OutputStream, class WebForm, class... WebModules> void WebConfigControl<Diag, Parser, OutputStream, WebForm, WebModules...>::handleErrors(OutputStream &output, WebccError error, const Parser &parser) { /// @brief Handles HTTP Request Parser errors and WebCC errors /// @param output Output which receives error data in form of HTTP Responses /// @param error Error generated by WebCC itself /// @param parser HTTP Request Parser to provide information on parsing errors Diag * diagLog = Diag::instance(); if (error != WebccError::NONE) { diagLog->log(Diag::Severity::NOTICE, FPSTR(textsUI.httpStatusCode), static_cast<int>(getHTTPStatusCode(error, parser.getError())), ' ', getErrorMessage(error)); } if (error == WebccError::PARSER_ERROR) { diagLog->log(Diag::Severity::NOTICE, getParseErrorMessage(parser.getError())); } util::http::HTTPResponseHeader::statusLine(output, getHTTPStatusCode(error, parser.getError())); output.print(FPSTR(texts.crlf)); output.print(getErrorMessage(error)); output.print(FPSTR(texts.crlf)); if (error == WebccError::PARSER_ERROR) { output.print(getParseErrorMessage(parser.getError())); output.print(FPSTR(texts.crlf)); } output.print(FPSTR(texts.crlf)); output.print(FPSTR(texts.crlf)); } template <class Diag, class Parser, class OutputStream, class WebForm, class... WebModules> const __FlashStringHelper * WebConfigControl<Diag, Parser, OutputStream, WebForm, WebModules...>::getErrorMessage(WebccError error) { /// @brief Returns WebCC error message in human-readable form /// @param error Error to produce a message from /// @return C-String with human-readable error message (located in PROGMEM) switch (error) { case WebccError::NONE: return (FPSTR(textsUI.errorNone)); case WebccError::SERVER_NOT_INITIALISED: return (FPSTR(textsUI.errorServerNotInitialised)); case WebccError::PARSER_ERROR: return (FPSTR(textsUI.errorParserError)); case WebccError::METHOD_NOT_ACCEPTED: return (FPSTR(textsUI.errorMethodNotAccepted)); case WebccError::PATH_NOT_ACCEPTED: return (FPSTR(textsUI.errorPathNotAccepted)); case WebccError::PATH_CONFLICT: return (FPSTR(textsUI.errorPathConflict)); default: return (FPSTR(textsUI.errorUnknown)); } } template <class Diag, class Parser, class OutputStream, class WebForm, class... WebModules> const __FlashStringHelper * WebConfigControl<Diag, Parser, OutputStream, WebForm, WebModules...>::getParseErrorMessage(ParseError error) { /// @brief Returns HTTP Request Parser's error message in human-readable form /// @param error Error to produce a message from /// @return C-String with human-readable error message (located in PROGMEM) switch (error) { case ParseError::NONE: return (FPSTR(textsUI.parseErrorNone)); case ParseError::INTERNAL_ERROR: return (FPSTR(textsUI.parseErrorInternal)); case ParseError::REQUEST_PART_TOO_LONG: return (FPSTR(textsUI.parseErrorRequestPartTooLong)); case ParseError::REQUEST_STRUCTURE: return (FPSTR(textsUI.parseErrorRequestStructure)); case ParseError::REQUEST_SEMANTICS: return (FPSTR(textsUI.parseErrorRequestSemantics)); default: return (FPSTR(textsUI.parseErrorUnknown)); } } template <class Diag, class Parser, class OutputStream, class WebForm, class... WebModules> util::http::HTTPStatusCode WebConfigControl<Diag, Parser, OutputStream, WebForm, WebModules...>::getHTTPStatusCode(WebccError error, ParseError parseError) { /// @brief Generates HTTP Status Code (to be used in HTTP Response) based on WebCC error /// and HTTP Request parser error /// @param error WebCC error /// @param parseError HTTP Request Parser error switch (error) { case WebccError::NONE: return (util::http::HTTPStatusCode::OK); case WebccError::SERVER_NOT_INITIALISED: return (util::http::HTTPStatusCode::INTERNAL_SERVER_ERROR); case WebccError::PARSER_ERROR: switch (parseError) { case ParseError::INTERNAL_ERROR: return (util::http::HTTPStatusCode::INTERNAL_SERVER_ERROR); case ParseError::REQUEST_PART_TOO_LONG: return (util::http::HTTPStatusCode::PAYLOAD_TOO_LARGE); case ParseError::REQUEST_STRUCTURE: return (util::http::HTTPStatusCode::BAD_REQUEST); case ParseError::REQUEST_SEMANTICS: return (util::http::HTTPStatusCode::CONFLICT); default: return (util::http::HTTPStatusCode::INTERNAL_SERVER_ERROR); } return (util::http::HTTPStatusCode::INTERNAL_SERVER_ERROR); case WebccError::METHOD_NOT_ACCEPTED: return (util::http::HTTPStatusCode::NOT_IMPLEMENTED); case WebccError::PATH_NOT_ACCEPTED: return (util::http::HTTPStatusCode::NOT_FOUND); case WebccError::PATH_CONFLICT: return (util::http::HTTPStatusCode::INTERNAL_SERVER_ERROR); default: return (util::http::HTTPStatusCode::INTERNAL_SERVER_ERROR); } } ////////////////////////////////////////////////////////////////////// // WebccFormHTML ////////////////////////////////////////////////////////////////////// //If defined, will produce HTML UI form with formatting, i.e. with //tabs, line breaks and comments. Formatting increases size of the //produced HTML UI form but simplifies debugging #define HTML_FORMATTING #ifdef HTML_FORMATTING #define TAB "\t" #define CRLF "\r\n" #define CSSCOMMENT(comment) "/* " comment " */" #define JSCOMMENT(comment) "// " comment #else #define TAB "" #define CRLF "" #define CSSCOMMENT(comment) "" #define JSCOMMENT(comment) "" #endif #define FORM_PART(name,value) const char name [sizeof(value)] = value /// Contains string literals for HTML/CSS/JS code parts which are used to /// generate HTML forms for user interface class WebccFormHTML { public: FORM_PART(bodyBegin1, "<!DOCTYPE html>" CRLF "<html>" CRLF "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">" CRLF "<head>" CRLF "<title>" ); FORM_PART(bodyBegin2, "</title>" CRLF "<style type=\"text/css\" media=\"all\">" CRLF "html{overflow-y:scroll;}" CRLF "body{font-family:Arial;background-color:#D0FFD0;color:#000000;font-size:85%;}" CRLF "h1{text-align:center;font-size:130%;font-weight:bold;}" CRLF "h2{text-align:center;font-size:120%;font-weight:bold;margin-top:2em;}" CRLF "h3{text-align:center;font-size:110%;font-weight:bold;}" CRLF "h3:first-child{margin-top:-.25em;}" CRLF "input, select {display:block;text-align:left;float:right;width:12.5em;}" CRLF "input[type='checkbox'] {width:1em;}" CRLF "input[type='submit'] {width:auto;}" CRLF "label {text-align:left;float:left;max-width:11em;}" ".plaintext {text-align:left;float:right;width:12.5em;}" CRLF ".parameter {overflow:auto;padding-bottom:0.25em;line-height:1.65em;}" CRLF ".subsection {text-align:left;" CRLF TAB "margin:auto;" CRLF TAB "margin-bottom:1em;" CRLF TAB "width:25em;" CRLF TAB "padding:1em;" CRLF TAB "background-color:#66FF66;" CRLF TAB "border-style:solid;" CRLF TAB "border-color:black;" CRLF TAB "border-width:2px;" CRLF TAB "overflow:auto;" CRLF "}" CRLF ".submit {" CRLF TAB "text-align:left;" CRLF TAB "margin:auto;" CRLF TAB "margin-bottom:1em;" CRLF TAB "width:25em;" CRLF TAB "padding:1em;" CRLF TAB "background-color:#AAFFAA;" CRLF TAB "border-style:solid;" CRLF TAB "border-color:black;" CRLF TAB "border-width:2px;" CRLF TAB "overflow:auto;" CRLF TAB "}" CRLF CSSCOMMENT ("Tooltip styles") CRLF ".tooltip_ref:hover .tooltip_text {display:block;}" CRLF ".tooltip_ref {color:#006000;font-weight:bold;font-size:60%;}" CRLF ".tooltip_text {" CRLF TAB "display:none;" CRLF TAB "background:#A0FFA0;" CRLF TAB "margin-left:1em;" CRLF TAB "padding:1em;" CRLF TAB "position:absolute;" CRLF TAB "color:black;" CRLF TAB "font-size:75%;" CRLF TAB "tmax-width:20em;" CRLF TAB "line-height:100%;" CRLF "}" CRLF CSSCOMMENT ("Tabs styles") CRLF ".tab-content {border-top:1px solid #A0A0A0;margin:auto;width:37em;}" CRLF ".tab-header {position:relative;left:0.75em;height:3em;width:37em;margin:auto;margin-bottom:0px;padding-bottom:0px;}" CRLF ".tab-header ul.tab-list {margin:0;padding:0;position:absolute;bottom:-1px;width:37em;}" CRLF ".tab-header ul.tab-list li {display:inline;list-style:none;margin:0;}" CRLF ".tab-header ul.tab-list a,.tab-header ul.tab-list span,.tab-header ul.tab-list a.tab-active {" CRLF TAB "width:8em;" CRLF TAB "display:block;" CRLF TAB "float:left;" CRLF TAB "padding:4px 0;" CRLF TAB "margin:1px 2px 0 0;" CRLF TAB "text-align:center;" CRLF TAB "font-size:85%;" CRLF TAB "text-decoration:none;" CRLF "}" CRLF ".tab-header ul.tab-list span,.tab-header ul.tab-list a.tab-active {" CRLF TAB "border:1px solid #606060;" CRLF TAB "border-bottom:none;" CRLF TAB "background:#66FF66;" CRLF TAB "padding-bottom:7px;" CRLF TAB "margin-top:0;" CRLF "}" CRLF ".tab-header ul.tab-list a {" CRLF TAB "background:#D0D0D0;" TAB "border-top:1px solid #A0A0A0;" CRLF TAB "border-left:1px solid #A0A0A0;" CRLF TAB "border-right:1px solid #A0A0A0;" CRLF TAB "border-bottom:none;" CRLF TAB "color:black;" CRLF "}" CRLF ".tab {display: none;}" CRLF "</style>" CRLF "<script type=\"text/javascript\">" CRLF "/*<![CDATA[*/" CRLF JSCOMMENT ("HTML Tabs begin") CRLF CRLF "function getChildElementsByClassName(parentElement,className){" CRLF TAB "var i,childElements,pattern,result;" CRLF TAB "result=new Array();" CRLF TAB "pattern=new RegExp(\"\\\\b\"+className+\"\\\\b\");" CRLF CRLF TAB "childElements=parentElement.getElementsByTagName('*');" CRLF TAB "for(i=0;i<childElements.length;i++){" CRLF TAB TAB "if(childElements[i].className.search(pattern)!=-1){" CRLF TAB TAB TAB "result[result.length]=childElements[i];" CRLF TAB TAB "}" CRLF TAB "}" CRLF TAB "return result;" CRLF "}" CRLF CRLF CRLF "function BuildTabs(containerId){" CRLF TAB "var i,tabContainer,tabContents,tabHeading,title,tabElement;" CRLF TAB "var divElement,ulElement,liElement,tabLink,linkText;" CRLF CRLF TAB JSCOMMENT("assume that if document.getElementById exists, then this will work...") CRLF TAB "if(!eval('document.getElementById'))return;" CRLF TAB "tabContainer=document.getElementById(containerId);" CRLF TAB "if(tabContainer==null)return;" CRLF TAB "tabContents=getChildElementsByClassName(tabContainer,'tab-content');" CRLF TAB "if(tabContents.length==0)return;" CRLF CRLF TAB "divElement=document.createElement(\"div\");" CRLF TAB "divElement.className='tab-header';" CRLF TAB "divElement.id=containerId+'-header';" CRLF TAB "ulElement=document.createElement(\"ul\");" CRLF TAB "ulElement.className='tab-list';" CRLF CRLF TAB "tabContainer.insertBefore(divElement,tabContents[0]);" CRLF TAB "divElement.appendChild(ulElement);" CRLF CRLF TAB "for(i=0;i<tabContents.length;i++){" CRLF TAB TAB "tabHeading=getChildElementsByClassName(tabContents[i],'tab');" CRLF TAB TAB "title=tabHeading[0].childNodes[0].nodeValue;" CRLF CRLF TAB TAB JSCOMMENT("create the tabs as an unsigned list") CRLF TAB TAB "liElement=document.createElement(\"li\");" CRLF TAB TAB "liElement.id=containerId+'-tab-'+i;" CRLF CRLF TAB TAB "tabLink=document.createElement(\"a\");" CRLF TAB TAB "linkText=document.createTextNode(title);" CRLF CRLF TAB TAB "tabLink.className=\"tab-item\";" CRLF CRLF TAB TAB "tabLink.setAttribute(\"href\",\"javascript://\");" CRLF TAB TAB "tabLink.onclick=new Function (\"ActivateTab('\"+containerId+\"', \"+i+\")\");" CRLF CRLF TAB TAB "ulElement.appendChild(liElement);" CRLF TAB TAB "liElement.appendChild(tabLink);" CRLF TAB TAB "tabLink.appendChild(linkText);" CRLF CRLF TAB TAB JSCOMMENT ("remove the H1") CRLF TAB "tabContents[i].removeChild;" CRLF TAB "}" CRLF "}" CRLF CRLF CRLF "function ActivateTab(containerId,activeTabIndex){" CRLF TAB "var i,tabContainer,tabContents;" CRLF CRLF TAB "tabContainer=document.getElementById(containerId);" CRLF TAB "if(tabContainer==null) return;" CRLF CRLF TAB "tabContents=getChildElementsByClassName(tabContainer, 'tab-content');" CRLF TAB "if(tabContents.length>0) {" CRLF TAB TAB "for(i=0;i<tabContents.length;i++) {" CRLF /* TAB TAB TAB "//tabContents[i].className = \"tab-content\";" CRLF*/ TAB TAB TAB "tabContents[i].style.display=\"none\";" CRLF TAB TAB "}" CRLF CRLF TAB TAB "tabContents[activeTabIndex].style.display=\"block\";" CRLF CRLF TAB TAB "tabList=document.getElementById(containerId+'-list');" CRLF TAB TAB "tabs=getChildElementsByClassName(tabContainer,'tab-item');" CRLF TAB TAB "if(tabs.length>0) {" CRLF TAB TAB TAB "for(i=0;i<tabs.length;i++){" CRLF TAB TAB TAB TAB "tabs[i].className=\"tab-item\";" CRLF TAB TAB TAB "}" CRLF CRLF TAB TAB TAB "tabs[activeTabIndex].className=\"tab-item tab-active\";" CRLF TAB TAB TAB "tabs[activeTabIndex].blur();" CRLF TAB TAB "}" CRLF TAB "}" CRLF "}" CRLF CRLF "function InitTabs(){" CRLF TAB "BuildTabs('tab-container');" CRLF TAB "ActivateTab('tab-container',0);" CRLF "}" CRLF JSCOMMENT ("HTML Tabs end") CRLF "/*]]>*/" CRLF "</script>" CRLF "</head>" CRLF "<body onload=\"InitTabs();\">" CRLF "<form method=\"" ); FORM_PART(bodyBegin3, "\" action=\"" ); FORM_PART (bodyBegin4, "\">" CRLF "<h1>" ); FORM_PART(bodyBegin5, "</h1>" CRLF "<div id='tab-container'>" CRLF ); FORM_PART(bodyEnd, "</div>" CRLF "</form>" CRLF "</body>" CRLF "</html>" CRLF ); FORM_PART(defaultFormMethod, "POST"); public: FORM_PART(sectionBegin1, "<div class=\"tab-content\">" CRLF "<h2 class=\"tab\">" ); FORM_PART(sectionBegin2, "</h2><br>" CRLF ); FORM_PART(sectionEnd, "</div>" CRLF ); public: FORM_PART(submitButton1, "<div class=\"submit\">" CRLF "<input type=\"submit\" value=\"" ); FORM_PART(submitButton2, "\"><br>" CRLF "</div>" CRLF ); public: FORM_PART(subsectionBegin1, "<div class=\"subsection\">" CRLF "<h3>" ); FORM_PART(subsectionBegin2, "</h3>" CRLF ); FORM_PART(subsectionEnd, "</div>" CRLF ); public: FORM_PART(tooltipBegin, " <span class=\"tooltip_ref\">[?]<span class=\"tooltip_text\">"); FORM_PART(tooltipEnd, "</span></span>"); public: FORM_PART(textParameter1, "<div class=\"parameter\"><label>"); FORM_PART(textParameter2, "</label>"); FORM_PART(textParameter3, "<input type=\"text\" name=\""); FORM_PART(textParameter4, "\" value=\""); FORM_PART(textParameter5, "\"></div>" CRLF); public: FORM_PART(plaintext1, "<div class=\"parameter\"><label>"); FORM_PART(plaintext2, "</label>"); FORM_PART(plaintext3, "<div class=\"plaintext\">"); FORM_PART(plaintext4, "</div></div>" CRLF); public: FORM_PART(link1, "<div class=\"parameter\"><label>"); FORM_PART(link2, "</label>"); FORM_PART(link3, "<div class=\"plaintext\"><a href=\""); FORM_PART(link4, "\">"); FORM_PART(link5, "</a></div></div>" CRLF); public: FORM_PART(selectParameter1, "<div class=\"parameter\"><label>"); FORM_PART(selectParameter2, "</label>" CRLF); FORM_PART(selectParameter3, "<select name=\""); FORM_PART(selectParameter4, "\">" CRLF); FORM_PART(selectParameter5, "</select></div>" CRLF); public: FORM_PART(selectParameterOption1, "<option value=\""); FORM_PART(selectParameterOption2, "\">"); FORM_PART(selectParameterOption2Selected, "\" selected>"); FORM_PART(selectParameterOption3, "</option>" CRLF); public: FORM_PART(checkboxParameter1, "<div class=\"parameter\"><label>"); FORM_PART(checkboxParameter2, "</label>"); FORM_PART(checkboxParameter3, "<input type=\"checkbox\" name=\""); FORM_PART(checkboxParameter4, "\"></div>"); FORM_PART(checkboxParameter4Checked, "\" checked></div>" CRLF); } __attribute__((packed)); #undef HTML_FORMATTING #undef TAB #undef CRLF #undef CSSCOMMENT #undef JSCOMMENT #undef FORM_PART extern const WebccFormHTML PROGMEM webccFormHTML; ////////////////////////////////////////////////////////////////////// // WebccForm ////////////////////////////////////////////////////////////////////// /// @brief Provides simple framework for generating HTML forms which are /// utilised as user interface /// @details In this version HTML form consists of parameters (also /// plaintexts and links), grouped into sections and subsections. /// @details First bodyBegin() must be called to generate HTML head, /// CSS, Javascript code, etc. After that Section must be begin by /// using sectionBegin(), and then Subsection by using subsectionBegin(). /// @details In this version Section corresponds to a tab in user interface, /// and Subsection corresponds to group of parameter within a tab. /// @details Within the Subsection parameters can be generated by calling /// textParameter(), checkboxParameter() or selectParameter() to add the /// actual parameters to the form. Also plaintext() can be used to add plain /// text (rather than text box filled with some data) or link() to add the /// hyperlink to the form. /// @details When beginning new Section or Subsection, the current Section /// or Subsection will be automatically finalised. class WebccForm { public: WebccForm(Print &client); ~WebccForm(); public: void bodyBegin(const char * formURL = NULL, const char * caption = NULL, const char * submitButtonText = NULL, const char * formMethod = NULL, boolean progmemStrings = true); void bodyEnd(void); public: void sectionBegin(const char * displayName, boolean progmemStrings = true); void sectionEnd(void); void subsectionBegin(const char * displayName, boolean progmemStrings = true); void subsectionEnd(void); public: void textParameter (const char * displayName, const char * internalName, const char * value = NULL, const char * tooltipText = NULL, boolean progmemStrings = true); inline void textParameter (const char * displayName, const char * internalName, long value, const char * tooltipText = NULL, boolean progmemStrings = true); void plaintext (const char *displayName, const char * value = NULL, const char * tooltipText = NULL, boolean progmemStrings = true); inline void plaintext (const char * displayName, long value, const char * tooltipText = NULL, boolean progmemStrings = true); void link (const char * displayName, const char * linkURL, const char * value, const char * tooltipText = NULL, boolean progmemStrings = true); void checkboxParameter(const char * displayName, const char * internalName, boolean value, const char * tooltipText = NULL, boolean progmemStrings = true); template <typename... Options> void selectParameter(const char * displayName, const char * internalName, long value, const char * tooltipText, boolean progmemStrings, Options... options); private: inline void print(const char * string, boolean progmemString, const __FlashStringHelper * defaultString = NULL); private: template <typename OptionValue, typename OptionName, typename... MoreOptions> inline void selectParameterExpand(long value, boolean progmemStrings, OptionValue optionValue, OptionName optionName, MoreOptions... moreOptions); inline void selectParameterExpand(long value, boolean progmemStrings); private: inline void selectParameterBegin(const char * displayName, const char * internalName, const char * tooltipText, boolean progmemStrings); inline void selectParameterOption(const char * displayName, long optionValue, long actualValue, boolean progmemStrings); inline void selectParameterEnd (void); private: inline void tooltip(const char *tooltiptext, boolean progmemStrings); private: Print * client = NULL; private: const char * submitButtonText = NULL; boolean progmemSubmitButtonText; private: boolean isWithinBody = false; boolean isWithinSection = false; boolean isWithinSubsection = false; }; /// @brief Sends string from RAM or PROGMEM to the client, if the string is NULL, prints defaultString /// @param string CString in RAM or PROGMEM to be printed /// @param progmemString If true string is assumed to be located in PROGMEM, /// if false sting is assumed to be located in RAM /// @param defaultString This CString is printed if string parameters is NULL. /// This CString must always be located in PROGMEM void WebccForm::print(const char * string, boolean progmemString, const __FlashStringHelper * defaultString) { if (!string) { if (defaultString) client->print(defaultString); return; } progmemString ? client->print(FPSTR(string)) : client->print(string); } /// @brief Generates parameter with drop-down list /// @tparam Options parameter pack containing pairs of value and name /// (e.g. <1, "MyValue">) for drop-down list items. See also selectParameterExpand() /// @param displayName CString which contains parameter's name visible for user in /// the UI. Can be located in RAM or PROGMEM (depending on value of progmemStrings /// parameter) /// @param internalName CString which contains parameter's name referred internally /// in POST-requests, etc. Can be located in RAM or PROGMEM (depending on value of /// progmemStrings parameter) /// @param value Drop-down list parameter's current value (currently selected item) /// @param tooltipText CString which contains parameter's tooltip text (or NULL if /// no tooltip required). Can be located in RAM or PROGMEM (depending on value of /// progmemStrings parameter). /// @param progmemStrings If true cstring arguments displayName, internalName and /// tooltipText are assumed to be located in PROGMEM, if false these cstring /// arguments are assumed to be located in RAM /// @param options Contains pairs of value and name, e.g. (1, "MyValue") of /// drop-down list items. See also selectParameterExpand() template <class... Options> void WebccForm::selectParameter(const char * displayName, const char * internalName, long value, const char * tooltipText, boolean progmemStrings, Options... options) { selectParameterBegin(displayName, internalName, tooltipText, progmemStrings); selectParameterExpand(value, progmemStrings, options...); selectParameterEnd(); } /// @brief Recursively expands parameter pack of drop-down list items /// @tparam OptionValue Value of the currently processed drop-down list item /// @tparam OptionName Name of the currently processed drop-down list item /// @tparam MoreOptions Parameter pack which contains more pairs of drop-down /// list items' values & names /// @param value Drop-down list parameter's current value /// @param progmemStrings If true cstring arguments optionName and optionValue /// are assumed to be located in PROGMEM, if false these cstring arguments are /// assumed to be located in RAM /// @param optionValue Cstring, value of the currently processed drop-down /// list item. Can be located in RAM or PROGMEM (depending on value of /// progmemStrings parameter). /// @param optionName Cstring, name of the currently processed drop-down list /// item. Can be located in RAM or PROGMEM (depending on value of /// progmemStrings parameter). /// @param moreOptions Contains further pairs of value and name, e.g. /// (1, "MyValue") of drop-down list items which will be processed recursively template <class OptionValue, class OptionName, class... MoreOptions> void WebccForm::selectParameterExpand(long value, boolean progmemStrings, OptionValue optionValue, OptionName optionName, MoreOptions... moreOptions) { selectParameterOption(optionName, optionValue, value, progmemStrings); selectParameterExpand(value, progmemStrings, moreOptions...); } /// @brief Finalises recursive expansion of parameter pack of drop-down list items /// @details Used when no more arguments are present in argument pack /// @param value Drop-down list parameter's current value /// @param progmemStrings If true cstring arguments optionName and optionValue /// are assumed to be located in PROGMEM, if false these cstring arguments are /// assumed to be located in RAM void WebccForm::selectParameterExpand(long value, boolean progmemStrings) { (void)value; (void)progmemStrings; } /// @brief Generates text parameter /// @param displayName CString which contains parameter's name visible for user in /// the UI. Can be located in RAM or PROGMEM (depending on value of progmemStrings /// parameter) /// @param internalName CString which contains parameter's name referred internally /// in POST-requests, etc. Can be located in RAM or PROGMEM (depending on value of /// progmemStrings parameter) /// @param value Parameter's current value /// @param tooltipText CString which contains parameter's tooltip text (or NULL if /// no tooltip required). Can be located in RAM or PROGMEM (depending on value of /// progmemStrings parameter). /// @param progmemStrings If true cstring arguments displayName, internalName and /// tooltipText are assumed to be located in PROGMEM, if false these cstring /// arguments are assumed to be located in RAM void WebccForm::textParameter (const char * displayName, const char * internalName, long value, const char * tooltipText, boolean progmemStrings) { const size_t MAX_CHARS_PER_LONG = 12; //max 10 digits per long + optional minus sign + null character const int RADIX_DECIMAL = 10; char valueAsText[MAX_CHARS_PER_LONG] = {0}; ltoa (value, valueAsText, RADIX_DECIMAL); textParameter(displayName, internalName, valueAsText, tooltipText, progmemStrings); } /// @brief Generates plain text in the HTML form /// @details Plain text is only displayed in the HTML form and does not represent /// an user-configurable parameter /// @param displayName CString which contains text visible for user in the UI. /// Can be located in RAM or PROGMEM (depending on value of progmemStrings parameter) /// @param value Value to include in the HTML form /// @param tooltipText CString which contains parameter's tooltip text (or NULL if /// no tooltip required). Can be located in RAM or PROGMEM (depending on value of /// progmemStrings parameter). /// @param progmemStrings If true cstring arguments displayName and tooltipText /// are assumed to be located in PROGMEM, if false these cstring /// arguments are assumed to be located in RAM void WebccForm::plaintext (const char * displayName, long value, const char * tooltipText, boolean progmemStrings) { const size_t MAX_CHARS_PER_LONG = 12; //max 10 digits per long + optional minus sign + null character const int RADIX_DECIMAL = 10; char valueAsText[MAX_CHARS_PER_LONG] = {0}; ltoa (value, valueAsText, RADIX_DECIMAL); plaintext(displayName, valueAsText, tooltipText, progmemStrings); } /// @brief Output drop-down list parameter's beginning HTML code void WebccForm::selectParameterBegin(const char * displayName, const char * internalName, const char * tooltipText, boolean progmemStrings) { if (!client) return; client->print(FPSTR(webccFormHTML.selectParameter1)); print(displayName, progmemStrings); client->print(FPSTR(webccFormHTML.selectParameter2)); tooltip(tooltipText, progmemStrings); client->print(FPSTR(webccFormHTML.selectParameter3)); print(internalName, progmemStrings); client->print(FPSTR(webccFormHTML.selectParameter4)); } /// @brief Output a single list item HTML code of drop-down /// list parameter void WebccForm::selectParameterOption(const char * displayName, long optionValue, long actualValue, boolean progmemStrings) { if (!client) return; client->print(FPSTR(webccFormHTML.selectParameterOption1)); client->print(optionValue, DEC); if (optionValue == actualValue) client->print(FPSTR(webccFormHTML.selectParameterOption2Selected)); else client->print(FPSTR(webccFormHTML.selectParameterOption2)); print(displayName, progmemStrings); client->print(FPSTR(webccFormHTML.selectParameterOption3)); } /// @brief Output drop-down list parameter's ending HTML code void WebccForm::selectParameterEnd (void) { if (!client) return; client->print(FPSTR(webccFormHTML.selectParameter5)); } /// @brief Output a tooltip to HTML form /// @param tooltipText Cstring, text of the tooltip or NULL if /// tooltip not required /// @param progmemStrings If true then tooltipText cstring is /// assumed to be located in PROGMEM, if false then tooltipText /// cstring assumed to be located in RAM void WebccForm::tooltip(const char * tooltipText, boolean progmemStrings) { if (!client) return; if (!tooltipText) return; client->print(FPSTR(webccFormHTML.tooltipBegin)); print(tooltipText, progmemStrings); client->print(FPSTR(webccFormHTML.tooltipEnd)); } ////////////////////////////////////////////////////////////////////// // BufferedPrint ////////////////////////////////////////////////////////////////////// /// @brief A simple Print-derived class which will store all printed /// data in the internal buffer and only sends them to other /// Print-derived object (client) when the buffer overflows. /// @details The reason for buffering is that ESP8266 sends a packet /// and than waits for ACK before sending subsequent packets. This /// causes sending many small packets to be slow whereas sending /// single large packet is much faster. class BufferedPrint : public Print { public: inline BufferedPrint(Print & client, uint8_t * buffer, size_t bufferSize); inline ~BufferedPrint(); public: virtual size_t write(uint8_t character); public: inline size_t getBufferSize(void); private: Print * client = NULL; private: void sendBuffer(void); size_t bufferPosition = 0; uint8_t * buffer = NULL; size_t bufferSize = 0; }; /// Initialises client and buffer data /// @param client Client to send data to /// @param buffer Buffer to use for temporarily storing data /// @param bufferSize size of the buffer in bytes BufferedPrint::BufferedPrint (Print & client, uint8_t * buffer, size_t bufferSize) { this->client = &client; this->buffer = buffer; this->bufferSize = bufferSize; } /// Sends remaining buffer contents to clients before /// object is deleted BufferedPrint::~BufferedPrint () { sendBuffer(); } /// Returns internal buffer size inline size_t BufferedPrint::getBufferSize(void) { return (bufferSize); } ////////////////////////////////////////////////////////////////////// // HTTPReqParserStateMachine ////////////////////////////////////////////////////////////////////// /// @brief Parses HTTP requests and decomposes them into HTTP Request Parts /// @details Call parse() method repeatedly to parse a HTTP Request. /// Use finished() and error() method to determine whether parsing is /// completed with or without error. /// @details This implementation is based on the state machine and does /// not have a state machine stack. class HTTPReqParserStateMachine { public: inline boolean begin(Stream &client); void parse(char * buffer, size_t bufferSize, HTTPRequestPart * part); inline boolean finished(void) const; inline boolean error(void) const; inline ParseError getError(void) const; private: enum class ParserState { ///<Parser (state machine) internal state UNKNOWN, ///<Parser state unknown (internal error) BEGIN, ///<Begin parsing METHOD, ///<Expecting HTTP request method PATH, ///<Expecting HTTP request path URL_QUERY_NAME, ///<Expecting URL query item name (before '=') URL_QUERY_VALUE, ///<Expecting URL query item value (after '=') HTTP_VERSION, ///<Expecting HTTP version FIELD_OR_HEADER_END, ///<Expecting header field or empty line (request header's end) FIELD_NAME, ///<Expecting header field name FIELD_VALUE_PART1, ///<Expecting header field value part before '=' FIELD_VALUE_PART2, ///<Expecting header field value part after '=' and before ';' or end of line POST_QUERY_OR_END, ///<Expecting POST query or end of the request POST_QUERY_NAME, ///<Expecting URL query item name (before '=') POST_QUERY_VALUE, ///<Expecting URL query item value (after '=') FINISHED, ///<Request parsing completed successfully ERROR_INTERNAL, ///<Internal error occured ERROR_REQUEST_PART_TOO_LONG, ///<Request part is too long to process ERROR_REQUEST_STRUCTURE, ///<Malformed request ERROR_REQUEST_SEMANTICS, ///<Request is correctly structured but is inconsistent or contradictory }; enum class ControlCharacter { OTHER, ///< Character other than listed below SPACE, ///< Space (0x20) QUESTION, ///< Question mark (0x3F) AMPERSAND, ///< Ampersand (0x26) EQUAL, ///< Equal sign (0x3D) COLON, ///< Colon (0x3A) CRLF, ///< \r\n (0x10 & 0x13) SEMICOLON, ///< Semicolon (0x3B) UNAVAILABLE, ///< see STREAM_UNAVAILABLE (-1) }; enum class ControlCharacterSet { ALL, ///< All control characters used FIELD_VALUE ///< Only UNAVAILABLE, SEMICOLON, EQUAL and CRLF characters are used }; enum class StreamOperation { DO_NOTHING, ///< Do not read anything from stream READ_UNTIL, ///< Read from stream to buffer until one of control characters is found or buffer is full SKIP, ///< Read from stream until control character are found READ_SINGLE, ///< Read single character READ_IF_CC ///< Do not read anything from stream and peek (rather than read) next character }; private: inline void transition(ParserState newState); inline void setError(ParseError error); inline void setInternalError(size_t codeLine = 0); private: ParserState currentState = ParserState::BEGIN; Stream * inputStream = NULL; size_t internalErrorCodeLine = 0; private: class InputStreamHelper { public: static ControlCharacter readUntilControlCharacter(Stream &client, ControlCharacterSet controlChars, char *buffer, size_t bufferSize); static ControlCharacter skipUntilControlCharacter(Stream &client, ControlCharacterSet controlChars); static ControlCharacter read(Stream &client, ControlCharacterSet controlChars); static ControlCharacter readIfControlCharacter(Stream &client, ControlCharacterSet controlChars); private: static const int CC_UNAVAILABLE = -1; //returned by Stream::read and Stream::peek if no more data available static const int CC_CR = static_cast<int>('\r'); static const int CC_LF = static_cast<int>('\n'); static const int CC_SPACE = static_cast<int>(' '); static const int CC_QUESTION = static_cast<int>('?'); static const int CC_AMPERSAND = static_cast<int>('&'); static const int CC_EQUAL = static_cast<int>('='); static const int CC_COLON = static_cast<int>(':'); static const int CC_SEMICOLON = static_cast<int>(';'); private: static inline boolean isControlCharacter(int i1, int i2 = CC_UNAVAILABLE); static inline boolean isFieldValueControlCharacter(int i1, int i2 = CC_UNAVAILABLE); static inline boolean isControlCharacterInSet(ControlCharacterSet cc, int i1, int i2 = CC_UNAVAILABLE); static ControlCharacter intToControlCharacter(ControlCharacterSet cc, int first, int second = CC_UNAVAILABLE); }; class ParserTables { public: static boolean getStateProperties(ParserState state, StreamOperation *streamOp, HTTPRequestPart *reqPart, ParserState *defaultTransition, ControlCharacterSet *controlChars); static ParserState getNextState(ParserState currentState, ControlCharacter nextCharacter, ParserState defaultTransition); private: struct StateTableEntry { ParserState state; StreamOperation streamOp; HTTPRequestPart reqPart; ParserState defaultTransition; ControlCharacterSet controlChars; }; struct TransitionTableEntry { ParserState state; ControlCharacter nextCharacter; ParserState newState; }; }; }; boolean HTTPReqParserStateMachine::begin(Stream & client) { inputStream = &client; currentState = ParserState::BEGIN; internalErrorCodeLine = 0; return (true); } boolean HTTPReqParserStateMachine::finished(void) const { return (error() || (currentState == ParserState::FINISHED)); } boolean HTTPReqParserStateMachine::error(void) const { return ((currentState == ParserState::ERROR_INTERNAL) || (currentState == ParserState::ERROR_REQUEST_PART_TOO_LONG) || (currentState == ParserState::ERROR_REQUEST_STRUCTURE) || (currentState == ParserState::ERROR_REQUEST_SEMANTICS)); } ParseError HTTPReqParserStateMachine::getError(void) const { switch (currentState) { case ParserState::ERROR_INTERNAL: return (ParseError::INTERNAL_ERROR); case ParserState::ERROR_REQUEST_PART_TOO_LONG: return (ParseError::REQUEST_PART_TOO_LONG); case ParserState::ERROR_REQUEST_STRUCTURE: return (ParseError::REQUEST_STRUCTURE); case ParserState::ERROR_REQUEST_SEMANTICS: return (ParseError::REQUEST_SEMANTICS); default: return (ParseError::NONE); } } void HTTPReqParserStateMachine::transition(HTTPReqParserStateMachine::ParserState newState) { currentState = newState; } void HTTPReqParserStateMachine::setError(ParseError error) { switch (error) { case ParseError::INTERNAL_ERROR: transition(ParserState::ERROR_INTERNAL); return; case ParseError::REQUEST_PART_TOO_LONG: transition(ParserState::ERROR_REQUEST_PART_TOO_LONG); return; case ParseError::REQUEST_STRUCTURE: transition(ParserState::ERROR_REQUEST_STRUCTURE); return; case ParseError::REQUEST_SEMANTICS: transition(ParserState::ERROR_REQUEST_SEMANTICS); return; default: return; } } void HTTPReqParserStateMachine::setInternalError(size_t codeLine) { internalErrorCodeLine = codeLine; setError(ParseError::INTERNAL_ERROR); } boolean HTTPReqParserStateMachine::InputStreamHelper::isControlCharacter(int i1, int i2) { if ((i1 == CC_CR) && (i2 == CC_LF)) return (true); return ((i1 == CC_UNAVAILABLE) || (i1 == CC_SPACE) || (i1 == CC_QUESTION) || (i1 == CC_AMPERSAND) || (i1 == CC_EQUAL) || (i1 == CC_COLON) || (i1 == CC_SEMICOLON)); } boolean HTTPReqParserStateMachine::InputStreamHelper::isFieldValueControlCharacter(int i1, int i2) { if ((i1 == CC_CR) && (i2 == CC_LF)) return (true); return ((i1 == CC_UNAVAILABLE) || (i1 == CC_EQUAL) || (i1 == CC_SEMICOLON)); } boolean HTTPReqParserStateMachine::InputStreamHelper::isControlCharacterInSet(ControlCharacterSet cc, int i1, int i2) { switch (cc) { case ControlCharacterSet::ALL: return (isControlCharacter(i1, i2)); case ControlCharacterSet::FIELD_VALUE: return (isFieldValueControlCharacter(i1, i2)); } return (false); } }; //namespace webcc #endif