#ifndef WEBSERVER_H #define WEBSERVER_H #include #include "TCPServer.h" #include "Dictionary.h" #include "HttpConstants.h" #include "utilities.h" //#define DEBUG_WEBS #define READ_WRITE_BUFFER_SIZE 5000 template class WEBServer : public TCPServer, 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 }; enum WEBClientState {ACCEPTED, PARSING, QUERY_PARSED, RESPONSE_SENT, DONE}; struct HttpRequestData { HttpRequestMethod HRM; HttpVersion HV; HttpMIMEType HMT; size_t contentLength; Dictionary getParams; Dictionary 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(port, maxClient, clientDataBufferSize), _sdClass(sdClass) {} virtual ~WEBServer() { free(_WWWDir); } boolean addApiRoutine(const char *uri, boolean (*apiRoutine)(HttpRequestData&, WiFiClient*, void*), void *pData, HttpRequestMethod HRM = UNDEFINED) { return _apiDictionary.add(uri, new ApiRoutine({apiRoutine, pData, HRM})); } void clearApiRoutine() { _apiDictionary.clear(); }; boolean removeApiRoutine(const char* uri) { return _apiDictionary.remove(uri); } //Helper function used for the webApi static void sendHTTPHeader(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); } client->print("\r\nAccess-Control-Allow-Origin: *"); 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; } } protected: private: virtual T* createNewClient(WiFiClient wc) { return new T(wc, TCPServer::freeClientId(), TCPServer::_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, "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, "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¶m2 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, "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, "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; case HttpParserStatus::PARSE_HTTP_HEADER_PARAMS: //Here we parse the different header params until we arrive to \r\n\r\n { 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 { client->_httpParserState = HttpParserStatus::PARSE_HTTP_POST_DATA; } } break; case HttpParserStatus::PARSE_HTTP_POST_DATA: switch(client->_httpRequestData.HMT) { case APPLICATION_X_WWW_FORM_URLENCODED: #if 1//def 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! #if 1//def 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()); } #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) { #ifdef DEBUG_WEBS Serial.printf("Header param : %s\n",(char *)client->_data); #endif //Here we check if we have interesting params char *search = strstr((char *)client->_data, "t-Type: application/x-www-form-urlen"); if(search != nullptr) { #ifdef DEBUG_WEBS Serial.printf("Content-Type : APPLICATION_X_WWW_FORM_URLENCODED\n"); #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 } } /* * This function fills the client's _rangeData struct with proper values */ bool fillRangeByteStruct(T *client) { char *rangeStart = strchr((char *)client->_data, '='), *delimiter = strchr((char *)client->_data, '-'), *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; 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 */ 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; } 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))(client->_httpRequestData, &(client->_client), ref->pData); } else if(ref->HRM == client->_httpRequestData.HRM) { return (*(ref->apiRoutine))(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::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) { sendHTTPHeader(&client->_client, HttpConstants::httpMIMETypeToString(HttpConstants::TEXT_HTML)); client->_client.printf_P(PSTR( "\r\n\ \r\n\ \r\n\ Index of %s\r\n\ \r\n\ \r\n\

Index of %s

\r\n\ \r\n\ \r\n\ \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("\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("\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( "\r\n\
TypeNameCreatedLast modifiedSize

[DIR]Parent Directory - -
%s%s%d-%s-%s %s:%s%d-%s-%s %s:%s%.1fK

\r\n\
SAB WEBServer, Version %s at %s Port %u
\r\n\ \r\n\ "), "1.0.0", client->_client.localIP().toString().c_str(), TCPServer::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\r\n\r\n

Error %d

%s

\r\n"), 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 { boolean (*apiRoutine)(HttpRequestData&, WiFiClient*, void*); void *pData; HttpRequestMethod HRM; }; Dictionary _apiDictionary; SDClass *_sdClass; char *_WWWDir = nullptr; //Website root folder }; #endif //WEBSERVER_H