ESP8266_swiss_army_board/src/app/WEBServer.h

1231 lines
48 KiB
C++

#ifndef WEBSERVER_H
#define WEBSERVER_H
#include <SD.h>
#include "TCPServer.h"
#include "Dictionary.h"
#include "HttpConstants.h"
#include "utilities.h"
//#define DEBUG_WEBS
#define READ_WRITE_BUFFER_SIZE 5000
template <typename T>
class WEBServer : public TCPServer<T>, public HttpConstants
{
public:
enum HttpParserStatus
{
PARSE_HTTP_VERB,
PARSE_HTTP_RESOURCE,
PARSE_HTTP_VERSION,
PARSE_HTTP_RESOURCE_QUERY,
PARSE_HTTP_POST_DATA,
PARSE_HTTP_HEADER_PARAMS,
PARSE_HTTP_COOKIES
};
enum WEBClientState {ACCEPTED, PARSING, QUERY_PARSED, RESPONSE_SENT, DONE};
struct HttpCookie
{
DictionaryHelper::StringEntity value;
};
struct HttpRequestData
{
HttpRequestMethod HRM;
HttpVersion HV;
HttpMIMEType HMT;
size_t contentLength;
Dictionary<HttpCookie> cookies;
Dictionary<DictionaryHelper::StringEntity> getParams;
Dictionary<DictionaryHelper::StringEntity> postParams;
char *postParamsDataPointer; //Used in the postParams algorithm
char *httpResource;
uint16_t maxResourceBuffer;
char *httpBody;
uint16_t maxBodyBuffer;
};
WEBServer(uint16_t port = 80, SDClass *sdClass = NULL, uint8_t maxClient = MAX_CLIENT, uint16_t clientDataBufferSize = 256) : TCPServer<T>(port, maxClient, clientDataBufferSize), _sdClass(sdClass) {}
virtual ~WEBServer()
{
free(_WWWDir);
}
using ApiRoutineCallback = boolean (*)(WEBServer<T>*, HttpRequestData&, WiFiClient*, void*);
boolean addApiRoutine(const char *uri, ApiRoutineCallback apiRoutine, void *pData, HttpRequestMethod HRM = UNDEFINED)
{
return _apiDictionary.add(uri, new ApiRoutine({apiRoutine, pData, HRM}));
}
void clearApiRoutine(void) { _apiDictionary.clear(); };
boolean removeApiRoutine(const char *uri)
{
return _apiDictionary.remove(uri);
}
unsigned int apiRoutineCount(void) const
{
return _apiDictionary.count();
}
boolean addHttpHeader(const char *name, const char *value)
{
return _httpHeadersDictionary.add(name, DictionaryHelper::StringEntity(value));
}
void clearHttpHeaders(void)
{
_httpHeadersDictionary.clear();
}
boolean removeHttpHeader(const char *name)
{
return _httpHeadersDictionary.remove(name);
}
unsigned int httpHeadersCount(void) const
{
return _httpHeadersDictionary.count();
}
/**
* The addCookie method adds cookies to the cookie dictionary which will be used when calling the sendHTTPResponse method.
* Once the cookies are sent, the dictionary will be emptied.
**/
boolean addCookies(const char *cookieName, const char *cookieValue = nullptr, int32_t maxAge = -1, const char *cookiePath = nullptr, const char *cookieDomain = nullptr, boolean httpOnly = false, boolean sameSite = false)
{
return _setCookieDictionary.add(cookieName, new ServerHttpCookie({cookieValue, cookieDomain, cookiePath, sameSite, httpOnly, maxAge}));
}
void clearCookies(void)
{
_setCookieDictionary.clear();
}
boolean removeCookie(const char *cookieName)
{
return _setCookieDictionary.remove(cookieName);
}
void sendHTTPResponse(WiFiClient *client, const char *contentType, const size_t contentLength = 0, HttpVersion version = HttpVersion::HTTP_1_1, HTTP_CODE HTTPCode = HTTP_CODE::HTTP_CODE_OK)
{
if(!client) return;
(void)HTTPCode;
client->printf("%s 200 OK\r\nContent-Type: %s", httpVersionToString(version), contentType);
if(contentLength)
{
client->printf("\r\nContent-Length: %d", contentLength);
}
//We here send user defined HTTP headers :)
for(unsigned int i(0); i < _httpHeadersDictionary.count(); i++)
{
client->printf("\r\n%s: %s", _httpHeadersDictionary.getParameter(i), _httpHeadersDictionary.getAt(i) ? _httpHeadersDictionary.getAt(i)->getString() : "");
}
//We here send the user defined cookies :)
for(unsigned int i(0); i < _setCookieDictionary.count(); i++)
{
ServerHttpCookie *pCookie = _setCookieDictionary.getAt(i);
if(pCookie)
{
client->printf_P(PSTR("\r\nSet-Cookie: %s=%s"), _setCookieDictionary.getParameter(i), pCookie->value.getString());
if(pCookie->maxAge != -1)
client->printf_P(PSTR("; Max-Age=%d"), pCookie->maxAge);
if(pCookie->sameSite)
client->printf_P(PSTR("; SameSite=Strict"));
if(pCookie->domain.getString()[0] != '\0')
client->printf_P(PSTR("; Domain=%s"), pCookie->domain.getString());
if(pCookie->path.getString()[0] != '\0')
client->printf_P(PSTR("; Path=%s"), pCookie->path.getString());
if(pCookie->httpOnly)
client->printf_P(PSTR("; HttpOnly"));
}
}
//We do not forget to clear the cookie dictionary after
clearCookies();
client->print("\r\n\r\n");
}
void setWWWDir(const char *WWWDir)
{
if(WWWDir)
{
free(_WWWDir);
_WWWDir = (char *)malloc((strlen(WWWDir) * sizeof(char)) + 1);
strcpy(_WWWDir, WWWDir);
}
else
{
free(_WWWDir);
_WWWDir = nullptr;
}
}
const char * getWWWDir(void) const
{
return _WWWDir;
}
void setSDClass(SDClass *sdClass)
{
_sdClass = sdClass;
}
protected:
private:
virtual T* createNewClient(WiFiClient wc)
{
return new T(wc, TCPServer<T>::freeClientId(), TCPServer<T>::_clientDataBufferSize);
}
/**
* The fillDataBuffer method from the TCPServer base class was overloaded in order to read from socket
* until the \r\n\r\n body delimiter.
* The remaining data should NOT BE READ if the MIME TYPE is not APPLICATION_X_WWW_FORM_URLENCODED
* so that we can consume the full request body later.
*/
#if 0
virtual void fillDataBuffer(T *client)
{
uint16_t freeSpace = (client->_dataBufferSize-1/*for \0*/ - client->_dataSize);
uint32_t bytesAvailable(client->_client.available());
void *bodyDelimiter(memmem((client->_client).peekBuffer(), (client->_client).peekAvailable(), "\r\n\r\n", 4));
if(bodyDelimiter && client->_httpRequestData.HMT != HttpMIMEType::APPLICATION_X_WWW_FORM_URLENCODED)
{
bytesAvailable = (const char *)bodyDelimiter - (client->_client).peekBuffer();
}
if(freeSpace > 0)
{
int amountToBeRead = bytesAvailable < freeSpace ? bytesAvailable : freeSpace, read(0);
read = (client->_client).read(client->_data + client->_dataSize, amountToBeRead);
client->_dataSize += read;
client->_data[client->_dataSize] = '\0';
client->_newDataAvailable = true;
}
}
#endif
virtual void greetClient(T *client)
{
(void)client;
}
virtual void processClientData(T *client)
{
if(client->_dataSize > 0)
{
switch(client->_WEBClientState)
{
case ACCEPTED:
#ifdef DEBUG_WEBS
Serial.println("WEBServer : ACCEPTED");
#endif
client->_WEBClientState = WEBClientState::PARSING;
break;
case PARSING:
queryParser(client);
break;
case QUERY_PARSED:
#ifdef DEBUG_WEBS
Serial.println("WEBServer : QUERY_PARSED");
#endif
sendDataToClient(client);
break;
case RESPONSE_SENT:
#ifdef DEBUG_WEBS
Serial.println("WEBServer : RESPONSE_SENT");
#endif
client->_WEBClientState = WEBClientState::DONE;
break;
case DONE:
#ifdef DEBUG_WEBS
Serial.println("WEBServer : DONE");
#endif
client->_clientState = TCPClient::ClientState::DISCARDED;
break;
}
}
}
void queryParser(T *client)
{
switch(client->_httpParserState)
{
case HttpParserStatus::PARSE_HTTP_VERB:
{
#ifdef DEBUG_WEBS
Serial.println((char *)client->_data);
#endif
char *pVerb(strchr((char *)client->_data, ' '));
if(pVerb != NULL)
{
*pVerb = '\0';
client->_httpRequestData.HRM = getHttpVerbEnumValue((char *)client->_data);
client->freeDataBuffer((pVerb - (char *)client->_data) +1);
if(client->_httpRequestData.HRM == HttpRequestMethod::UNDEFINED) //Error 400
{
sendInfoResponse(HTTP_CODE::HTTP_CODE_BAD_REQUEST, client, PSTR("The server could not understand the request due to invalid syntax"));
client->_clientState = TCPClient::ClientState::DISCARDED;
break;
}
#ifdef DEBUG_WEBS
Serial.print("Verb : ");Serial.println(client->_httpRequestData.HRM);
Serial.println((char *)client->_data);
#endif
client->_httpParserState = HttpParserStatus::PARSE_HTTP_RESOURCE;
}
else
{
sendInfoResponse(HTTP_CODE::HTTP_CODE_BAD_REQUEST, client, PSTR("The server could not understand the request due to invalid syntax"));
client->_clientState = TCPClient::ClientState::DISCARDED;
}
}
break;
case HttpParserStatus::PARSE_HTTP_RESOURCE:
{
char *pRsrc(strchr((char *)client->_data, ' ')), *pRsrcQuery(strchr((char *)client->_data, '?'));
//!\ the ? should be present before ' ' if ' ' is found !!!!
if(pRsrc && pRsrcQuery)
if(pRsrcQuery > pRsrc)pRsrcQuery = nullptr;
//The case where we have the resource complete or not complete with query parameters like : GET /some/path/resource.rsrc?param1=one&param2 HTTP/1.1
if(pRsrc || pRsrcQuery)
{
uint16_t rawLengthOfResource(0);
if(pRsrcQuery )
{
*pRsrcQuery = '\0'; // The ? is the end of the resource string
rawLengthOfResource = pRsrcQuery - (char *)client->_data;
#ifdef DEBUG_WEBS
Serial.printf("Resource w/ query\n");
#endif
}
else
{
*pRsrc = '\0';
rawLengthOfResource = pRsrc - (char *)client->_data;
#ifdef DEBUG_WEBS
Serial.printf("Resource w/o query\nRaw length : %u\n",rawLengthOfResource);
#endif
}
if(rawLengthOfResource >= client->_httpRequestData.maxResourceBuffer) //Then we cannot handle the full resource and there is no point of truncating it
//better tell the client about it ...
{
#ifdef DEBUG_WEBS
Serial.printf("Resource too long\nResource raw length is : %u (\\0 included)\nMax length is : %u (\\0 included)\nclient->_data : #%s#\n", rawLengthOfResource + 1, client->_httpRequestData.maxResourceBuffer, (char *)client->_data);
#endif
sendInfoResponse(HTTP_CODE::HTTP_CODE_URI_TOO_LONG, client, PSTR("Resource too long"));
client->_clientState = TCPClient::ClientState::DISCARDED;
break;
}
client->_httpRequestData.httpResource = (char *) malloc(sizeof(char) * (rawLengthOfResource + 1)); // +1 for the \0
if(client->_httpRequestData.httpResource != nullptr)
{
strncpy(client->_httpRequestData.httpResource, (char *)client->_data, rawLengthOfResource);
client->_httpRequestData.httpResource[rawLengthOfResource] = '\0';
}
else //Error 500
{
sendInfoResponse(HTTP_CODE::HTTP_CODE_INTERNAL_SERVER_ERROR, client, "Failed to allocate memory for resources");
client->_clientState = TCPClient::ClientState::DISCARDED;
break;
}
client->freeDataBuffer(rawLengthOfResource + 1); //+1 to go past the \0, only the query parameters left in the buffer '?' excluded
#ifdef DEBUG_WEBS
Serial.printf("Resource length : %u\nRsrc : %s\nclient->_data : #%s#\n",
rawLengthOfResource,
client->_httpRequestData.httpResource,
(char *)client->_data);
#endif
if(pRsrcQuery)
client->_httpParserState = HttpParserStatus::PARSE_HTTP_RESOURCE_QUERY;
else
client->_httpParserState = HttpParserStatus::PARSE_HTTP_VERSION;
}
else //The URL is too long to fit in the buffer, we dont know it's length nor we now if it has query parameters.
//TODO : Maybe the query is incomplete and the client will send more data, case to handle.
{
#ifdef DEBUG_WEBS
Serial.printf("Could not find ' ' or '?' delimiter\nclient->_data : #%s#\n",
(char *)client->_data);
#endif
sendInfoResponse(HTTP_CODE::HTTP_CODE_URI_TOO_LONG, client, PSTR("Resource too long"));
client->_clientState = TCPClient::ClientState::DISCARDED;
break;
}
}
break;
case HttpParserStatus::PARSE_HTTP_VERSION:
{
char *pEndline = strstr((char *)client->_data, "\r\n");
if(pEndline == NULL) pEndline = strchr((char *)client->_data, '\n');
char *pVers = strstr((char *)client->_data, "HTTP/");
if(pEndline != NULL && pVers!= NULL)
{
*pEndline = '\0';
client->_httpRequestData.HV = getHttpVersionEnumValue(pVers+5);
#ifdef DEBUG_WEBS
Serial.print("Vers : ");Serial.println(pVers+5);
Serial.print("Vers : ");Serial.println(client->_httpRequestData.HV);
#endif
client->freeDataBuffer((pEndline - (char *)client->_data)+2);
#ifdef DEBUG_WEBS
Serial.println((char *)client->_data);
#endif
client->_httpParserState = HttpParserStatus::PARSE_HTTP_HEADER_PARAMS;
}
}
break;
//index.htm?var1=1&var2=2...
//----------^^^^^^^^^^^^^^
case HttpParserStatus::PARSE_HTTP_RESOURCE_QUERY:
//If we are here, it means we are sure that there is at least one parameter
if(!httpRsrcParamParser(client))
{
#ifdef DEBUG_WEBS
Serial.println("Get params :");
for(unsigned int i = 0; i < client->_httpRequestData.getParams.count(); i++)
{
Serial.printf("%s : %s\n", client->_httpRequestData.getParams.getParameter(i), client->_httpRequestData.getParams.getAt(i)->getString());
}
Serial.printf("client->_data : #%s#\n", client->_data);
#endif
client->_httpParserState = HttpParserStatus::PARSE_HTTP_VERSION;
}
break;
//Here we parse the different header params until we arrive to \r\n\r\n
//We also know that header params are of the form : Param1: value1\r\nParam2: value2\r\n etc
//So we look for the delimitor which is ":"
case HttpParserStatus::PARSE_HTTP_HEADER_PARAMS:
{
char *pDelimiter(strchr((char *)client->_data, ':'));
char *endHeaderDelimiter(strstr((char *)client->_data, "\r\n"));
//If we found the delimiter, this means there is at least one parameter
if(pDelimiter)
{
*pDelimiter = '\0';
httpHeaderParamParser(client);
}
//There is maybe not headers so we go to the http body part
else if(endHeaderDelimiter)
{
if(client->_httpRequestData.contentLength) //If a body is expected
client->_httpParserState = HttpParserStatus::PARSE_HTTP_POST_DATA;
else //Else we are done
client->_WEBClientState = WEBClientState::QUERY_PARSED;
client->freeDataBuffer((endHeaderDelimiter - (char *)client->_data) +1); //client->_data must not be empty so we keep the last \n...
break;
}
else
{
#ifdef DEBUG_WEBS
Serial.println("Error, should have found \\r\\n\\r\\n");
#endif
sendInfoResponse(HTTP_CODE::HTTP_CODE_BAD_REQUEST, client, PSTR("The server could not understand the request due to invalid syntax"));
client->_clientState = TCPClient::ClientState::DISCARDED;
}
}
break;
case HttpParserStatus::PARSE_HTTP_COOKIES:
if(!httpCookiesParser(client))
{
#ifdef DEBUG_WEBS
Serial.println("Cookies :");
for(unsigned int i = 0; i < client->_httpRequestData.cookies.count(); i++)
{
Serial.printf("#%s# : #%s#\n", client->_httpRequestData.cookies.getParameter(i), client->_httpRequestData.cookies.getAt(i)->value.getString());
}
Serial.printf("phc client->_data : #%s#\n", client->_data);
#endif
//Once we are done parsing the cookies, we go back to the HTTP HEADER PARAMS PARSER as there may be still some more after the cookies ?
client->_httpParserState = HttpParserStatus::PARSE_HTTP_HEADER_PARAMS;
}
break;
case HttpParserStatus::PARSE_HTTP_POST_DATA:
switch(client->_httpRequestData.HMT)
{
case APPLICATION_X_WWW_FORM_URLENCODED:
#ifdef DEBUG_WEBS
Serial.printf("Post data : APPLICATION_X_WWW_FORM_URLENCODED\nPost data : #%s#\n", client->_data);
#endif
//we parse it !
if(!httpPostParamParser(client))
{
//Parsing done!
#ifdef DEBUG_WEBS
Serial.println("Post params :");
for(unsigned int i = 0; i < client->_httpRequestData.postParams.count(); i++)
{
Serial.printf("#%s# : #%s#\n", client->_httpRequestData.postParams.getParameter(i), client->_httpRequestData.postParams.getAt(i)->getString());
}
#endif
client->_WEBClientState = WEBClientState::QUERY_PARSED;
}
break;
default :
client->_WEBClientState = WEBClientState::QUERY_PARSED;
}
break;
default :
sendInfoResponse(HTTP_CODE::HTTP_CODE_INTERNAL_SERVER_ERROR, client, "WEB server error");
client->_clientState = TCPClient::ClientState::DISCARDED;
break;
}
}
/*
* This function parses the header parameters in order to find particular parameters.
* For example we look for the "Content-Type" header or for the "Range: bytes=" header
*/
void httpHeaderParamParser(T *client)
{
char *pHeaderParam((char *)client->_data);
char *pHeaderValue((char *)client->_data + strlen((char *)client->_data) + 2); //+2 is to discard the ": "
char *pHeaderEndOfValue(strstr((char *)pHeaderValue, "\r\n"));
if(pHeaderEndOfValue)
{
*pHeaderEndOfValue = '\0';
#ifdef DEBUG_WEBS
Serial.printf("Header param->value : #%s# -> #%s#\n", pHeaderParam, pHeaderValue);
#endif
char *searchParam(strstr(pHeaderParam, "t-Type"));
char *searchValue(strstr(pHeaderValue, "ion/x-www-for"));
if(searchParam && searchValue)
{
client->_httpRequestData.HMT = APPLICATION_X_WWW_FORM_URLENCODED;
client->freeDataBuffer((pHeaderEndOfValue - (char *)client->_data) + 2);
#ifdef DEBUG_WEBS
Serial.println("Content-Type is APPLICATION_X_WWW_FORM_URLENCODED");
#endif
return;
}
if((searchParam = strstr(pHeaderParam, "ion")) && (searchValue = strstr(pHeaderValue, "keep-al")))
{
client->_keepAlive = true;
client->freeDataBuffer((pHeaderEndOfValue - (char *)client->_data) + 2);
#ifdef DEBUG_WEBS
Serial.println("Connection: keep-alive");
#endif
return;
}
if((searchParam = strstr(pHeaderParam, "ent-Len")))
{
char *check(nullptr);
client->_httpRequestData.contentLength = strtoul(pHeaderValue, &check, 10);
if(*check != '\0') //Failed to parse the content length !
{
client->_httpRequestData.contentLength = 0;
#ifdef DEBUG_WEBS
Serial.println("Failed to parse Content-Length");
#endif
}
#ifdef DEBUG_WEBS
else
{
Serial.printf("Content-Length: %u\n", client->_httpRequestData.contentLength);
}
#endif
client->freeDataBuffer((pHeaderEndOfValue - (char *)client->_data) + 2);
return;
}
//Range part for file downloads and media playback
if((searchParam = strstr(pHeaderParam, "nge")) && (searchValue = strstr(pHeaderValue, "ytes=")))
{
//We need to parse the range values
if(fillRangeByteStruct(client, pHeaderValue))
{
#ifdef DEBUG_WEBS
Serial.printf("Range (bytes) data : start -> %u ; end -> %u\n", client->_rangeData._rangeStart, client->_rangeData._rangeEnd);
#endif
}
#ifdef DEBUG_WEBS
else
{
Serial.printf("Range (bytes) data parse error : start -> %u ; end -> %u\n", client->_rangeData._rangeStart, client->_rangeData._rangeEnd);
}
#endif
client->freeDataBuffer((pHeaderEndOfValue - (char *)client->_data) + 2);
return;
}
//Here we check if there are some cookies to be parsed
if((searchParam = strstr(pHeaderParam, "okie")))
{
client->_httpParserState = HttpParserStatus::PARSE_HTTP_COOKIES;
client->freeDataBuffer(pHeaderValue - (char *)client->_data);
return;
}
//We still need to remove
client->freeDataBuffer((pHeaderEndOfValue - (char *)client->_data) + 2);
}
//Error, we did not find the \r\n, maybe the parameter value is too long, error needs to be handled !
else
{
#ifdef DEBUG_WEBS
Serial.println("Error : header value \\r\\n not found!");
#endif
sendInfoResponse(HTTP_CODE::HTTP_CODE_BAD_REQUEST, client, PSTR("The server could not understand the request due to invalid syntax"));
client->_clientState = TCPClient::ClientState::DISCARDED;
}
}
/*
* This function fills the client's _rangeData struct with proper values
*/
bool fillRangeByteStruct(T *client, char *rangeBytesData)
{
if(!rangeBytesData)return false;
char *rangeStart = strchr(rangeBytesData, '='), *delimiter = strchr(rangeBytesData, '-'), *check(nullptr);
if(!rangeStart)return false;
rangeStart++; //We move one char forward
//We parse the 1st part of the range byte
if(!delimiter) // If only one part (ill-formed)
{
client->_rangeData._rangeStart = strtoull(rangeStart, &check, 10);
if(*check != '\0')return false;
return true;
}
else
{
*delimiter = '\0';
client->_rangeData._rangeStart = strtoull(rangeStart, &check, 10);
if(*check != '\0')return false;
}
rangeStart = delimiter+1;
//We parse the 2nd part of the range byte
client->_rangeData._rangeEnd = strtoull(rangeStart, &check, 10);
if(*check != '\0')return false;
client->_rangeData._rangeRequest = true;
return true;
}
/*
* This function parses resources query parameters
*/
boolean httpRsrcParamParser(T *client)
{
char *end(strchr((char *)client->_data, ' '));
//If we find the end we mark it, this is needed for subsequent strchr
if(end)*end = '\0';
char *key(strchr((char *)client->_data, '=')), *value(strchr((char *)client->_data, '&'));
if(key == nullptr && value == nullptr) //Only the key is present
{
client->_httpRequestData.getParams.add((char *)client->_data, new DictionaryHelper::StringEntity(NULL));
client->freeDataBuffer(strlen((char *)client->_data) + 1);
#ifdef DEBUG_WEBS
Serial.printf("client->_data : #%s#\n", client->_data);
#endif
return false;
}
else if(key != nullptr && value != nullptr)
{
if(key < value)*key = '\0';
*value = '\0';
client->_httpRequestData.getParams.add((char *)client->_data, new DictionaryHelper::StringEntity(key > value ? NULL : key + 1));
client->freeDataBuffer((value - (char *)client->_data) + 1);
#ifdef DEBUG_WEBS
Serial.printf("client->_data : #%s#\n", client->_data);
#endif
}
else if(key != nullptr && value == nullptr) //Only one key/value pair present
{
*key = '\0';
client->_httpRequestData.getParams.add((char *)client->_data, new DictionaryHelper::StringEntity(key+1));
client->freeDataBuffer((key - (char *)client->_data) + strlen(key+1) + 2);
#ifdef DEBUG_WEBS
Serial.printf("client->_data : #%s#\n", client->_data);
#endif
return false;
}
else if(key == nullptr && value != nullptr)
{
*value = '\0';
client->_httpRequestData.getParams.add((char *)client->_data, new DictionaryHelper::StringEntity(NULL));
client->freeDataBuffer((value - (char *)client->_data) + 1);
#ifdef DEBUG_WEBS
Serial.printf("client->_data : #%s#\n", client->_data);
#endif
}
return true;
}
boolean httpPostParamParser(T* client)
{
if(client->_httpRequestData.postParamsDataPointer == NULL)
{
if(strlen((char *)client->_data + 1) != client->_httpRequestData.contentLength)
return true;
client->_httpRequestData.postParamsDataPointer = (char *)client->_data + 1;//We save the starting position of the string to parse and we ignore the \n
}
char *key = strchr(client->_httpRequestData.postParamsDataPointer, '=');
char *value = strchr(client->_httpRequestData.postParamsDataPointer, '&');
if(key == NULL && value == NULL) //Nothing to parse or done
{
return false;
}
else if(key != NULL && value == NULL) //Only one key is present
{
*key = '\0';
client->_httpRequestData.postParams.add(client->_httpRequestData.postParamsDataPointer, new DictionaryHelper::StringEntity(key+1));
return false;
}
else if(key != NULL && value != NULL)
{
*key = '\0';
*value = '\0';
client->_httpRequestData.postParams.add(client->_httpRequestData.postParamsDataPointer, new DictionaryHelper::StringEntity(key+1));
memmove(client->_httpRequestData.postParamsDataPointer, value +1, strlen(value+1) + 1);
}
else if(key == NULL && value != NULL)//Should never happen
return false;
return true;
}
boolean httpCookiesParser(T *client)
{
//He we have the the Cookie field value like : TestCookie=TestValue; TestCookie2=; TestCookie3=TestValue3
//It is what we need to parse.
char *endOfKey(strchr((char *)client->_data, '='));
char *endOfValue(strchr((char *)client->_data, ';'));
char *failSafe(strstr((char *)client->_data, "\r\n"));
boolean notFinished(true);
//In the case where we have cookies followed by post data, there was an issue were we mistaken the = from the cookie separator with the = of the post data separator.
//Here just in case of the last cookie value finishing with a ';'
if(failSafe)
return false;
//There is at least one key/value pair
if(endOfKey)
{
*endOfKey = '\0';
endOfKey++;
if(endOfValue)
{
*endOfValue = '\0';
}
else
{
endOfValue = endOfKey + strlen(endOfKey) + 1;
notFinished = false;
}
#ifdef DEBUG_WEBS
Serial.printf("Key -> Value : #%s# Value : #%s#\n", ((char)*client->_data == ' ') ? (char*)client->_data + 1 : (char*)client->_data , endOfKey);
#endif
client->_httpRequestData.cookies.add(((char)*client->_data == ' ') ? (char*)client->_data + 1 : (char*)client->_data, new HttpCookie({endOfKey}));
//We dont forget to free the parsed data
client->freeDataBuffer((endOfValue + 1 - (char *)client->_data));
return notFinished;
}
return false;
}
void sendDataToClient(T *client)
{
if(!sendPageToClientFromApiDictio(client)) //Then we check if it is not a file that is requested
{
if(!sendPageToClientFromSdCard(client)) //If this function returns false, we close the connection with the client. An error occured (An error message has already been sent) or the whole file has been sent.
{
client->_WEBClientState = WEBClientState::RESPONSE_SENT;
}
}
else //If we found the api endpoint, we can close the connection with the client after the data has been sent.
client->_WEBClientState = WEBClientState::RESPONSE_SENT;
}
boolean sendPageToClientFromApiDictio(T *client)
{
if(_apiDictionary.count() == 0 || client->_httpRequestData.httpResource == NULL)
return false;
ApiRoutine *ref = _apiDictionary(client->_httpRequestData.httpResource);
if(ref == NULL)
return false;
// We consume the body's \r\n\r\n
uint8_t discard[4];
client->_client.read(discard, 4);
if(ref->HRM == UNDEFINED)
{
return (*(ref->apiRoutine))(this, client->_httpRequestData, &(client->_client), ref->pData);
}
else if(ref->HRM == client->_httpRequestData.HRM)
{
return (*(ref->apiRoutine))(this, client->_httpRequestData, &(client->_client), ref->pData);
}
else
return false;
}
boolean sendPageToClientFromSdCard(T *client)
{
if(_sdClass != NULL)
{
File pageToSend;
char *filePath(NULL), *header(NULL);
size_t readBytesFromFS(0), sentBytesFromSocket(0);
//We check what kind of http verb it is
switch(client->_httpRequestData.HRM)
{
case GET:
filePath = getFilePathByHttpResource(_WWWDir, client->_httpRequestData.httpResource);
if(filePath == NULL)
{
sendInfoResponse(HTTP_CODE::HTTP_CODE_INTERNAL_SERVER_ERROR, client, "Failed to allocate memory for the filePath");
return false;
}
#ifdef DEBUG_WEBS
Serial.printf("File path : #%s#\n",filePath);
#endif
pageToSend = _sdClass->open(filePath);
free(filePath);filePath = NULL;
//If we couldn't open the file
if(!pageToSend)
{
char *response(NULL);
response = (char *) malloc(sizeof(char) * (36 + strlen(client->_httpRequestData.httpResource) + 1));
if(response == NULL)
{
sendInfoResponse(HTTP_CODE::HTTP_CODE_INTERNAL_SERVER_ERROR, client, "Failed to allocate memory for the response");
return false;
}
sprintf(response, "Resource : %s not found on this server", client->_httpRequestData.httpResource);
sendInfoResponse(HTTP_CODE::HTTP_CODE_NOT_FOUND, client, response);
free(response);response = NULL;
return false;
}
#ifdef DEBUG_WEBS
Serial.print("FILE SIZE : ");
Serial.println(pageToSend.size());
Serial.print("FILE NAME : ");
Serial.println(pageToSend.name());
#endif
if(pageToSend.isDirectory())
{
//If the ressource wasn't terminated by a '/' then we issue a 301 Moved Permanently, else all's good
if(client->_httpRequestData.httpResource[strlen(client->_httpRequestData.httpResource)-1] != '/')
{
uint16_t locationLength(strlen(client->_httpRequestData.httpResource) + 7/*http://*/ + 16/*ip addr + :*/ + 5/*port*/ + 2);
char *location = (char *)malloc(locationLength * sizeof(char));
char *message = (char *)malloc((27 + locationLength + 1) * sizeof(char));
if(!location || !message)
{
sendInfoResponse(HTTP_CODE::HTTP_CODE_INTERNAL_SERVER_ERROR, client, "Failed to allocate memory for the location or message");
pageToSend.close();
return false;
}
sprintf(location, "http://%s:%u%s/",client->_client.localIP().toString().c_str(), TCPServer<T>::getPort(), client->_httpRequestData.httpResource);
sprintf(message, "The document has moved to %s.", location);
sendInfoResponse(HTTP_CODE::HTTP_CODE_MOVED_PERMANENTLY, client, message, location);
free(location);free(message);
}
else
sendDirectoryListing(client, pageToSend); //Sends the content of the directory like what apache does by default.
pageToSend.close();
return false;
}
if(client->_fileSentBytes == 0)
{
size_t pageToSendSize(pageToSend.size());
char *fileName = (char *) malloc(sizeof(char) * strlen(pageToSend.name()) + 1);
if(fileName == NULL)
{
sendInfoResponse(HTTP_CODE::HTTP_CODE_INTERNAL_SERVER_ERROR, client, "Failed to allocate memory for the response");
pageToSend.close();
return false;
}
strcpy(fileName, pageToSend.name());
if(client->_rangeData._rangeRequest)
{
client->_fileSentBytes = client->_rangeData._rangeStart;
client->_rangeData._rangeEnd = !client->_rangeData._rangeEnd ? pageToSendSize - 1 : client->_rangeData._rangeEnd;
pageToSend.seek(client->_fileSentBytes);
}
else
client->_rangeData._rangeEnd = pageToSendSize - 1; //Needed to carry on sending when not a partial file
header = getHTTPHeader(getMIMETypeByExtension(strlwr(getFileExtension(fileName))), pageToSendSize, client->_rangeData._rangeRequest, client->_rangeData._rangeStart, client->_rangeData._rangeEnd);
#ifdef DEBUG_WEBS
Serial.print("FILE EXTENSION : ");
Serial.println(getFileExtension(fileName));
#endif
free(fileName);
if(header == NULL)
{
sendInfoResponse(HTTP_CODE::HTTP_CODE_INTERNAL_SERVER_ERROR, client, "Failed to allocate memory for the header");
pageToSend.close();
return false;
}
client->_client.print(header);
free(header);header = NULL;
}
else
{
pageToSend.seek(client->_fileSentBytes);
}
if(pageToSend.available() && client->_fileSentBytes < client->_rangeData._rangeEnd + 1) // File is done sending once the whole file was sent or the partial part is done sending
{
size_t mallocAcceptedSize(0);
/*
The original issue was that a buffer of 2048 bytes was allocated on the stack causing a hard to track down stack overflow.
Possible solutions I cam up with :
1) Create a statically allocated buffer which would live in the BSS (Block Started by a Symbol) RAM segment.
Advantage : buffer is allocated once and ready to be used immediately.
Drawback : takes space in RAM (lost space) even if the buffer isn't used.
2) Create a dynamically allocated buffer using malloc and friends which would live in the heap RAM segment - SOLUTION I IMPLEMENTED
Advantage : buffer is taking RAM only when needed and can be freed afterwards.
Drawback : Allocating and deallocating heap memory a lot is costly in MCU time, leads to RAM fragmentation and could potentially fail...
*/
uint8_t *sendBuffer = (uint8_t*)mallocWithFallback(READ_WRITE_BUFFER_SIZE * sizeof(uint8_t), &mallocAcceptedSize);
if(!sendBuffer)
{
pageToSend.close();
return false; //Not clean but what else can I do. Should never happen anyway.
}
readBytesFromFS = pageToSend.read(sendBuffer,mallocAcceptedSize);
sentBytesFromSocket = client->_client.write(sendBuffer, readBytesFromFS);
free(sendBuffer);
#ifdef DEBUG_WEBS
Serial.printf("Bytes read from FS : %u, Bytes sent : %u, got allocated buffer size : %u - free stack : %u\n", readBytesFromFS, sentBytesFromSocket, mallocAcceptedSize, ESP.getFreeContStack());
#endif
client->_fileSentBytes += sentBytesFromSocket; //We save the number of bytes sent so that we can reopen the file to this position later on.
}
else
{
pageToSend.close();
return false;
}
pageToSend.close();
break;
default: //If not supported
sendInfoResponse(HTTP_CODE::HTTP_CODE_METHOD_NOT_ALLOWED, client, "The method used is not allowed");
return false;
break;
}
}
else
{
sendInfoResponse(HTTP_CODE::HTTP_CODE_INTERNAL_SERVER_ERROR, client, "Api endpoint does not exist");
return false;
}
return true;
}
void sendDirectoryListing(T *client, File& pageToSend)
{
sendHTTPResponse(&client->_client, HttpConstants::httpMIMETypeToString(HttpConstants::TEXT_HTML));
client->_client.printf_P(PSTR( "<!DOCTYPE HTML>\r\n\
<html>\r\n\
<head>\r\n\
<title>Index of %s</title>\r\n\
</head>\r\n\
<body>\r\n\
<h1>Index of %s</h1>\r\n\
<table>\r\n\
<tr><th>Type</th><th>Name</th><th>Created</th><th>Last modified</th><th>Size</th></tr>\r\n\
<tr><th colspan=\"5\"><hr></th></tr>\r\n")
, client->_httpRequestData.httpResource, client->_httpRequestData.httpResource);
if(strlen(client->_httpRequestData.httpResource) > 1) //Then we are not at the root of the WEBServer's directory.
{
char *rsrcCopy = strdup(client->_httpRequestData.httpResource);
if(rsrcCopy)
{
char *lastSlash(strrchr(rsrcCopy, '/'));
if(lastSlash)*lastSlash = '\0';
lastSlash = strrchr(rsrcCopy, '/');
if(lastSlash)*lastSlash = '\0';
client->_client.printf_P(PSTR("<tr><td>[DIR]</td><td><a href=\"%s/\">Parent Directory</a></td><td> - </td><td align=\"right\"> - </td><td> </td></tr>\r\n"), rsrcCopy);
free(rsrcCopy);
}
}
File nextFile;
for(;;)
{
if(!(nextFile = pageToSend.openNextFile()))break;
char zero_prepended[8][3] = {"","","","","","","",""};
time_t rawCreationTime(nextFile.getCreationTime()), rawLastModifiedTime(nextFile.getLastWrite());
tm creationTime(*localtime(&rawCreationTime)), lastModifiedTime(*localtime(&rawLastModifiedTime));
client->_client.printf_P(PSTR("<tr><td>%s</td><td><a href=\"%s\">%s</a></td><td align=\"right\">%d-%s-%s %s:%s</td><td align=\"right\">%d-%s-%s %s:%s</td><td align=\"right\">%.1fK</td></tr>\r\n"),
nextFile.isDirectory() ? "[DIR]":"[FILE]",
nextFile.name(),
nextFile.name(),
creationTime.tm_year + 1900, dateTimeFormater(zero_prepended[0], creationTime.tm_mon + 1, '0'), dateTimeFormater(zero_prepended[1], creationTime.tm_mday, '0'), dateTimeFormater(zero_prepended[2], creationTime.tm_hour, '0'), dateTimeFormater(zero_prepended[3], creationTime.tm_min, '0'),
lastModifiedTime.tm_year + 1900, dateTimeFormater(zero_prepended[4], lastModifiedTime.tm_mon + 1, '0'), dateTimeFormater(zero_prepended[5], lastModifiedTime.tm_mday, '0'), dateTimeFormater(zero_prepended[6], lastModifiedTime.tm_hour, '0'), dateTimeFormater(zero_prepended[7], lastModifiedTime.tm_min, '0'),
nextFile.size() / 1024.0
);
#ifdef DEBUG_WEBS
Serial.printf("File name : %s\nFile size : %u\nFree stack : %u\n", nextFile.name(), nextFile.size(), ESP.getFreeContStack());
#endif
nextFile.close();
delay(5);
}
client->_client.printf_P(PSTR( "<tr><th colspan=\"5\"><hr></th></tr>\r\n\
</table>\r\n\
<address>SAB WEBServer, Version %s at %s Port %u</address>\r\n\
</body>\r\n\
</html>"), "1.0.0", client->_client.localIP().toString().c_str(), TCPServer<T>::getPort());
}
/*Static helper methods*/
static void sendInfoResponse(HTTP_CODE http_code, T *client, const char *message, const char *location = nullptr)
{
char codeLiteral[100];
switch(http_code)
{
case HTTP_CODE_MOVED_PERMANENTLY:
strcpy_P(codeLiteral,PSTR("Moved Permanently"));
break;
case HTTP_CODE_BAD_REQUEST:
strcpy_P(codeLiteral,PSTR("Bad Request"));
break;
case HTTP_CODE_FORBIDDEN:
strcpy_P(codeLiteral,PSTR("Forbidden"));
break;
case HTTP_CODE_NOT_FOUND:
strcpy_P(codeLiteral,PSTR("Not Found"));
break;
case HTTP_CODE_METHOD_NOT_ALLOWED:
strcpy_P(codeLiteral,PSTR("Method Not Allowed"));
break;
case HTTP_CODE_URI_TOO_LONG:
strcpy_P(codeLiteral,PSTR("URI Too Long"));
break;
case HTTP_CODE_INTERNAL_SERVER_ERROR:
strcpy_P(codeLiteral,PSTR("Internal Server Error"));
break;
default:
strcpy_P(codeLiteral,PSTR("Error Not Defined"));
break;
}
client->_client.printf_P(PSTR("HTTP/1.1 %d %s\r\n"), http_code, codeLiteral);
if(http_code == HTTP_CODE_MOVED_PERMANENTLY)
client->_client.printf_P(PSTR("Location: %s\r\n"), location);
client->_client.printf_P(PSTR("Content-Type: text/html\r\nContent-Length: %d\r\n\r\n<!DOCTYPE HTML>\r\n<html>\r\n<h1>Error %d</h1><p>%s</p>\r\n</html>"), strlen(message) + 56 + (http_code != 0 ? 3:1), http_code , message);
}
static HttpRequestMethod getHttpVerbEnumValue(const char *parseBuffer)
{
if(parseBuffer == NULL)return HttpRequestMethod::UNDEFINED;
//UNDEFINED, GET, HEAD, POST, PUT, DELETE, CONNECT, OPTIONS, TRACE, PATCH
if(strcmp(parseBuffer,"GET") == 0){return HttpRequestMethod::GET;}
else if(strcmp(parseBuffer,"POST") == 0){return HttpRequestMethod::POST;}
else if(strcmp(parseBuffer,"HEAD") == 0){return HttpRequestMethod::HEAD;}
else if(strcmp(parseBuffer,"PUT") == 0){return HttpRequestMethod::PUT;}
else if(strcmp(parseBuffer,"DELETE") == 0){return HttpRequestMethod::DELETE;}
else if(strcmp(parseBuffer,"CONNECT") == 0){return HttpRequestMethod::CONNECT;}
else if(strcmp(parseBuffer,"TRACE") == 0){return HttpRequestMethod::TRACE;}
else if(strcmp(parseBuffer,"PATCH") == 0){return HttpRequestMethod::PATCH;}
else if(strcmp(parseBuffer,"OPTIONS") == 0){return HttpRequestMethod::OPTIONS;}
else
return HttpRequestMethod::UNDEFINED;
}
static HttpMIMEType getMIMETypeByExtension(const char *extension)
{
if(extension == NULL)return UNKNOWN_MIME;
//TEXT_PLAIN, TEXT_CSS, TEXT_HTML, TEXT_JAVASCRIPT
if(strcmp(extension, "web") == 0) return TEXT_HTML;
else if(strcmp(extension, "htm") == 0) return TEXT_HTML;
else if(strcmp(extension, "css") == 0) return TEXT_CSS;
else if(strcmp(extension, "js") == 0) return TEXT_JAVASCRIPT;
else if(strcmp(extension, "png") == 0) return IMAGE_PNG;
else if(strcmp(extension, "jpg") == 0) return IMAGE_JPEG;
else if(strcmp(extension, "ico") == 0) return IMAGE_X_ICO;
else if(strcmp(extension, "mp3") == 0) return AUDIO_MPEG;
else if(strcmp(extension, "txt") == 0) return TEXT_PLAIN;
else return UNKNOWN_MIME;
}
static char *getHTTPHeader(HttpMIMEType httpMIMEType, const size_t contentLength, bool acceptRanges = false, size_t rangeStart = 0, size_t rangeEnd = 0)
{
size_t headerToAllocSize(/*strlen("HTTP/1.1 200 OK\r\nContent-Type: \r\nContent-Length: \r\nCache-Control: max-age=31536000\r\n\r\n")*/86 + 255/*Longest MIME-TYPE that RFC allows*/ + 10 /*Max unsigned long footprint*/ + 1 /*\0 character*/);
if(acceptRanges)headerToAllocSize += (22 + 25 + 10*3 + 13); //"Accept-Ranges: bytes\r\n" is 22 characters + "Content-Range: bytes %u-%u/%u\r\n" + 3*Max unsigned long footprint + space for 206 Partial Content
char *header = (char *) malloc(sizeof(char) * headerToAllocSize);
if(!header)return NULL;
if(!acceptRanges)
sprintf(header,"HTTP/1.1 200 OK\r\nContent-Type: %s\r\nContent-Length: %u\r\nCache-Control: max-age=31536000\r\n\r\n", httpMIMETypeToString(httpMIMEType), contentLength);
else
sprintf(header,"HTTP/1.1 206 Partial Content\r\nContent-Type: %s\r\nAccept-Ranges: bytes\r\nContent-Length: %u\r\nContent-Range: bytes %u-%u/%u\r\nCache-Control: max-age=31536000\r\n\r\n", httpMIMETypeToString(httpMIMEType), rangeEnd-rangeStart+1,rangeStart ,rangeEnd, contentLength);
return header;
}
static char *getFileExtension(char *name)
{
char *p(strrchr(name, '.'));
return p != NULL ? p+1 : NULL;
}
static char *getFilePathByHttpResource(const char *WWWDir, char *res)
{
uint16_t buffSize = (WWWDir ? strlen(WWWDir) : 0 /*default is / */) + (strcmp(res, "/") == 0 ? 10:strlen(res)) + 1;//10 for /index.htm +1 for \0
char *filePath = (char*) malloc( sizeof(char) * buffSize);
if(filePath == NULL)
return NULL;
WWWDir ? strcpy(filePath, WWWDir) : strcpy(filePath, "");
strcat(filePath, (strcmp(res, "/") == 0) ? "/index.htm":res);
#ifdef DEBUG_FILEPATH
Serial.println(res);
Serial.print("Reserved space : ");Serial.println(buffSize);
Serial.print("Actual size : ");Serial.println(strlen(filePath));
Serial.println(filePath);
#endif
return filePath;
}
static HttpVersion getHttpVersionEnumValue(const char *parseBuffer)
{
//HTTP_0_9, HTTP_1_1, HTTP_1_0, HTTP_2_0
if(strcmp(parseBuffer,"1.1") == 0){return HttpVersion::HTTP_1_1;}
else if(strcmp(parseBuffer,"2.0") == 0){return HttpVersion::HTTP_2_0;}
else if(strcmp(parseBuffer,"1.0") == 0){return HttpVersion::HTTP_1_0;}
else if(strcmp(parseBuffer,"0.9") == 0){return HttpVersion::HTTP_0_9;}
else
return HttpVersion::UNKNOWN;
}
struct ApiRoutine
{
ApiRoutineCallback apiRoutine;
void *pData;
HttpRequestMethod HRM;
};
//Server side http cookie handling
struct ServerHttpCookie : public HttpCookie
{
DictionaryHelper::StringEntity domain;
DictionaryHelper::StringEntity path;
int32_t sameSite : 1, httpOnly : 1, maxAge : 30;
//Need to add the expires field as well. Thinking about the best way of doing it.
};
Dictionary<ApiRoutine> _apiDictionary;
Dictionary<DictionaryHelper::StringEntity> _httpHeadersDictionary;
Dictionary<ServerHttpCookie> _setCookieDictionary;
SDClass *_sdClass;
char *_WWWDir = nullptr; //Website root folder
};
#endif //WEBSERVER_H