Added full HTTP cookie support to the WEBServer, did some cleanup and refactoring around the HTTP Header Parsing algorithm to ease the cookie parser implementation

This commit is contained in:
Th3maz1ng 2022-10-22 19:25:20 +02:00
parent bbebac6212
commit 0f68644064
2 changed files with 230 additions and 119 deletions

View File

@ -32,6 +32,7 @@ WEBClient::~WEBClient()
void WEBClient::clearHttpRequestData()
{
free(_httpRequestData.httpResource);free(_httpRequestData.httpBody);
_httpRequestData.cookies.dispose();
_httpRequestData.getParams.dispose();
_httpRequestData.postParams.dispose();
}

View File

@ -20,17 +20,14 @@ class WEBServer : public TCPServer<T>, public HttpConstants
PARSE_HTTP_VERSION,
PARSE_HTTP_RESOURCE_QUERY,
PARSE_HTTP_POST_DATA,
PARSE_HTTP_HEADER_PARAMS
PARSE_HTTP_HEADER_PARAMS,
PARSE_HTTP_COOKIES
};
enum WEBClientState {ACCEPTED, PARSING, QUERY_PARSED, RESPONSE_SENT, DONE};
struct HttpCookie
{
DictionaryHelper::StringEntity value;
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.
};
struct HttpRequestData
@ -40,6 +37,7 @@ class WEBServer : public TCPServer<T>, public HttpConstants
HttpMIMEType HMT;
size_t contentLength;
Dictionary<HttpCookie> cookies;
Dictionary<DictionaryHelper::StringEntity> getParams;
Dictionary<DictionaryHelper::StringEntity> postParams;
char *postParamsDataPointer; //Used in the postParams algorithm
@ -100,9 +98,9 @@ class WEBServer : public TCPServer<T>, public HttpConstants
* 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, int32_t maxAge = -1, const char *cookiePath = nullptr, const char *cookieDomain = nullptr, boolean httpOnly = false, boolean sameSite = false)
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 HttpCookie({cookieValue, cookieDomain, cookiePath, sameSite, httpOnly, maxAge}));
return _setCookieDictionary.add(cookieName, new ServerHttpCookie({cookieValue, cookieDomain, cookiePath, sameSite, httpOnly, maxAge}));
}
void clearCookies(void)
@ -136,10 +134,26 @@ class WEBServer : public TCPServer<T>, public HttpConstants
//We here send the user defined cookies :)
for(unsigned int i(0); i < _setCookieDictionary.count(); i++)
{
//client
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 them after
//We do not forget to clear the cookie dictionary after
clearCookies();
client->print("\r\n\r\n");
@ -270,7 +284,7 @@ class WEBServer : public TCPServer<T>, public HttpConstants
if(client->_httpRequestData.HRM == HttpRequestMethod::UNDEFINED) //Error 400
{
sendInfoResponse(HTTP_CODE::HTTP_CODE_BAD_REQUEST, client, "The server could not understand the request due to invalid syntax");
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;
}
@ -284,7 +298,7 @@ class WEBServer : public TCPServer<T>, public HttpConstants
}
else
{
sendInfoResponse(HTTP_CODE::HTTP_CODE_BAD_REQUEST, client, "The server could not understand the request due to invalid syntax");
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;
}
}
@ -322,7 +336,7 @@ class WEBServer : public TCPServer<T>, public HttpConstants
#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, "Resource too long");
sendInfoResponse(HTTP_CODE::HTTP_CODE_URI_TOO_LONG, client, PSTR("Resource too long"));
client->_clientState = TCPClient::ClientState::DISCARDED;
break;
}
@ -361,7 +375,7 @@ class WEBServer : public TCPServer<T>, public HttpConstants
Serial.printf("Could not find ' ' or '?' delimiter\nclient->_data : #%s#\n",
(char *)client->_data);
#endif
sendInfoResponse(HTTP_CODE::HTTP_CODE_URI_TOO_LONG, client, "Resource too long");
sendInfoResponse(HTTP_CODE::HTTP_CODE_URI_TOO_LONG, client, PSTR("Resource too long"));
client->_clientState = TCPClient::ClientState::DISCARDED;
break;
}
@ -413,43 +427,63 @@ class WEBServer : public TCPServer<T>, public HttpConstants
client->_httpParserState = HttpParserStatus::PARSE_HTTP_VERSION;
}
break;
case HttpParserStatus::PARSE_HTTP_HEADER_PARAMS: //Here we parse the different header params until we arrive to \r\n\r\n
//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)
{
char *pEndLine = strstr((char *)client->_data, "\r\n");
if( pEndLine != NULL )
{
*pEndLine = '\0';
httpHeaderParamParser(client);
if(*(pEndLine+2) == '\r') //We got \r\n\r\n -> so we go to the post data section
{
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((pEndLine - (char *)client->_data) +3); //client->_data must not be empty...
break;
}
//Before in the buffer : key1: value1\r\nkey2: value2
//After in the buffer : key2: value2\r\n
client->freeDataBuffer((pEndLine - (char *)client->_data) + 2);
}
else //Error : indeed, we should at least have : \r\n. We go to the next step anyway
{
*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:
#if 1//def DEBUG_WEBS
#ifdef DEBUG_WEBS
Serial.printf("Post data : APPLICATION_X_WWW_FORM_URLENCODED\nPost data : #%s#\n", client->_data);
#endif
@ -457,11 +491,11 @@ class WEBServer : public TCPServer<T>, public HttpConstants
if(!httpPostParamParser(client))
{
//Parsing done!
#if 1//def DEBUG_WEBS
#ifdef DEBUG_WEBS
Serial.println("Post params :");
for(unsigned int i = 0; i < client->_httpRequestData.postParams.count(); i++)
{
Serial.print(client->_httpRequestData.postParams.getParameter(i));Serial.print(" : ");Serial.println(client->_httpRequestData.postParams.getAt(i)->getString());
Serial.printf("#%s# : #%s#\n", client->_httpRequestData.postParams.getParameter(i), client->_httpRequestData.postParams.getAt(i)->getString());
}
#endif
client->_WEBClientState = WEBClientState::QUERY_PARSED;
@ -486,79 +520,116 @@ class WEBServer : public TCPServer<T>, public HttpConstants
*/
void httpHeaderParamParser(T *client)
{
#ifdef DEBUG_WEBS
Serial.printf("Header param : %s\n",(char *)client->_data);
#endif
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"));
//Here we check if we have interesting params
char *search = strstr((char *)client->_data, "t-Type: application/x-www-form-urlen");
if(search != nullptr)
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.printf("Content-Type : APPLICATION_X_WWW_FORM_URLENCODED\n");
Serial.println("Error : header value \\r\\n not found!");
#endif
client->_httpRequestData.HMT = APPLICATION_X_WWW_FORM_URLENCODED;
return; //No need to look further
}
search = strstr((char *)client->_data, "ion: keep-al");
if(search != nullptr)
{
#ifdef DEBUG_WEBS
Serial.printf("Connection : keep-alive\n");
#endif
client->_keepAlive = true;
return; //No need to look further
}
//Range part for file downloads and media playback
search = strstr((char *)client->_data, "nge: bytes=");
if(search != nullptr)
{
//We parse the range byte data
if(fillRangeByteStruct(client))
{
#ifdef DEBUG_WEBS
Serial.printf("Range (bytes) data : start -> %u ; end -> %u\n", client->_rangeData._rangeStart, client->_rangeData._rangeEnd);
#endif
client->_rangeData._rangeRequest = true;
}
else
{
#ifdef DEBUG_WEBS
Serial.printf("Range (bytes) data parse error : start -> %u ; end -> %u\n", client->_rangeData._rangeStart, client->_rangeData._rangeEnd);
#endif
}
return; //No need to look further
}
//Content-length header
search = strstr((char *)client->_data, "ent-Length: ");
if(search != nullptr)
{
if(!fillContentLength(client))
{
#if 1//def DEBUG_WEBS
Serial.printf("Failed to parse content length\n");
#endif
}
else
{
#if 1//def DEBUG_WEBS
Serial.printf("Parsed content length is :%u\n", client->_httpRequestData.contentLength);
#endif
}
return; //No need to look further
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)
bool fillRangeByteStruct(T *client, char *rangeBytesData)
{
char *rangeStart = strchr((char *)client->_data, '='), *delimiter = strchr((char *)client->_data, '-'), *check(nullptr);
if(!rangeBytesData)return false;
char *rangeStart = strchr(rangeBytesData, '='), *delimiter = strchr(rangeBytesData, '-'), *check(nullptr);
if(!rangeStart)return false;
@ -583,24 +654,14 @@ class WEBServer : public TCPServer<T>, public HttpConstants
//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 fills the client's _httpRequestData.contentLength attribut
*/
bool fillContentLength(T *client)
{
char *start(strchr((char *)client->_data, ':')), *check(nullptr);
if(!start)return false;
start++;
client->_httpRequestData.contentLength = strtoul(start, &check, 10);
if(*check != '\0')return false;
return true;
}
/*
* This function is here to parse resources query parameters
* This function parses resources query parameters
*/
boolean httpRsrcParamParser(T *client)
{
@ -691,6 +752,46 @@ class WEBServer : public TCPServer<T>, public HttpConstants
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
@ -1109,10 +1210,19 @@ class WEBServer : public TCPServer<T>, public HttpConstants
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<HttpCookie> _setCookieDictionary;
Dictionary<ServerHttpCookie> _setCookieDictionary;
SDClass *_sdClass;
char *_WWWDir = nullptr; //Website root folder
};