From 0f686440643ee1ac188a6f47fca3c8f644d6b5d4 Mon Sep 17 00:00:00 2001 From: Th3maz1ng Date: Sat, 22 Oct 2022 19:25:20 +0200 Subject: [PATCH] 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 --- src/app/WEBClient.cpp | 1 + src/app/WEBServer.h | 348 +++++++++++++++++++++++++++--------------- 2 files changed, 230 insertions(+), 119 deletions(-) diff --git a/src/app/WEBClient.cpp b/src/app/WEBClient.cpp index e0d104f..fe127fb 100644 --- a/src/app/WEBClient.cpp +++ b/src/app/WEBClient.cpp @@ -32,6 +32,7 @@ WEBClient::~WEBClient() void WEBClient::clearHttpRequestData() { free(_httpRequestData.httpResource);free(_httpRequestData.httpBody); + _httpRequestData.cookies.dispose(); _httpRequestData.getParams.dispose(); _httpRequestData.postParams.dispose(); } diff --git a/src/app/WEBServer.h b/src/app/WEBServer.h index fab9aa7..b317a6f 100644 --- a/src/app/WEBServer.h +++ b/src/app/WEBServer.h @@ -20,17 +20,14 @@ class WEBServer : public TCPServer, 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, public HttpConstants HttpMIMEType HMT; size_t contentLength; + Dictionary cookies; Dictionary getParams; Dictionary postParams; char *postParamsDataPointer; //Used in the postParams algorithm @@ -100,9 +98,9 @@ class WEBServer : public TCPServer, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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 _apiDictionary; Dictionary _httpHeadersDictionary; - Dictionary _setCookieDictionary; + Dictionary _setCookieDictionary; SDClass *_sdClass; char *_WWWDir = nullptr; //Website root folder };