#ifndef WEBSERVER_H #define WEBSERVER_H #include "TCPServer.h" #include "Dictionary.h" #include "SDCardManager.h" #include "HttpConstants.h" //#define DEBUG_WEBS #define READ_WRITE_BUFFER_SIZE 2000 template class WEBServer : public TCPServer, public HttpConstants { public: enum HttpParserStatus {HTTP_VERB, HTTP_RESSOURCE, HTTP_VERSION, HTTP_PARAMS, POST_DATA, HEADER_PARAMS}; enum WEBClientState {ACCEPTED, PARSING, QUERY_PARSED, RESPONSE_SENT, DONE}; struct HttpRequestData { HttpRequestMethod HRM; HttpVersion HV; HttpMIMEType HMT; Dictionary getParams; char *getParamsDataPointer; //Used in the getParams algorithm Dictionary postParams; char *postParamsDataPointer; //Used in the postParams algorithm char *httpResource; uint16_t maxResourceBuffer; char *httpBody; uint16_t maxBodyBuffer; }; WEBServer(uint16_t port = 80, SDCardManager *sdCardManager = NULL, uint8_t maxClient = MAX_CLIENT, uint16_t clientDataBufferSize = 255) : TCPServer(port, maxClient, clientDataBufferSize), _sdCardManager(sdCardManager) {} 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 injectApiHeader(char *header, const char *contentType, char *content) { char *buffer = (char *)malloc(sizeof(char) * strlen(content) + 1); if(buffer != NULL) { strcpy(buffer, content); sprintf(header,"HTTP/1.1 200 OK\r\nContent-Type: %s\r\nContent-Length: %d\r\n\r\n%s",contentType,strlen(buffer), buffer); free(buffer); } } protected: private: virtual T* createNewClient(WiFiClient wc) { return new T(wc, TCPServer::freeClientId(), TCPServer::_clientDataBufferSize); } virtual void greetClient(T *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::HTTP_VERB: { #ifdef DEBUG_WEBS Serial.println((char *)client->_data); #endif char *pVerb = strstr((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::HTTP_RESSOURCE; } 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::HTTP_RESSOURCE: { char *pRsrc = strstr((char *)client->_data, " "); if(pRsrc != NULL) { *pRsrc = '\0'; uint16_t safeLength = pRsrc - (char *)client->_data <= client->_httpRequestData.maxResourceBuffer ? pRsrc - (char *)client->_data : client->_httpRequestData.maxResourceBuffer; #ifdef DEBUG_WEBS Serial.print("Resrc length : ");Serial.println(safeLength); #endif client->_httpRequestData.httpResource = (char *) malloc(sizeof(char) * (safeLength+1) ); //for \0 if(client->_httpRequestData.httpResource != NULL) { strncpy(client->_httpRequestData.httpResource, (char *)client->_data, safeLength); client->_httpRequestData.httpResource[safeLength] = '\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(safeLength + 1); #ifdef DEBUG_WEBS Serial.print("Resrc : ");Serial.println(client->_httpRequestData.httpResource); Serial.println((char *)client->_data); #endif } else //Resource is probably too long, so we truncate it { client->_httpRequestData.httpResource = (char *) malloc(sizeof(char) * (client->_httpRequestData.maxResourceBuffer+1) ); //for \0 if(client->_httpRequestData.httpResource != NULL) { strncpy(client->_httpRequestData.httpResource, (char *)client->_data, client->_httpRequestData.maxResourceBuffer); client->_httpRequestData.httpResource[client->_httpRequestData.maxResourceBuffer] = '\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(client->_httpRequestData.maxResourceBuffer + 1); } client->_httpParserState = HttpParserStatus::HTTP_PARAMS; } break; case HttpParserStatus::HTTP_VERSION: { char *pEndline = strstr((char *)client->_data, "\r\n"); if(pEndline == NULL) pEndline = strstr((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::HEADER_PARAMS; } } break; case HttpParserStatus::HTTP_PARAMS: //index.htm?var1=1&var2=2... if(!httpRsrcParamParser(client)) { #ifdef DEBUG_WEBS Serial.print("Resrc : ");Serial.println(client->_httpRequestData.httpResource); Serial.println("Get params :"); for(int i = 0; i < client->_httpRequestData.getParams.count(); i++) { Serial.print(client->_httpRequestData.getParams.getParameter(i));Serial.print(" : ");Serial.println(client->_httpRequestData.getParams.getAt(i)->getString()); } #endif client->_httpParserState = HttpParserStatus::HTTP_VERSION; } break; case HttpParserStatus::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 { client->_httpParserState = HttpParserStatus::POST_DATA; client->freeDataBuffer((pEndLine - (char *)client->_data) +3); //client->_data must not be empty... break; } //Before in the buffer : key1: value1\0\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::POST_DATA; } } break; case HttpParserStatus::POST_DATA: switch(client->_httpRequestData.HMT) { case APPLICATION_X_WWW_FORM_URLENCODED: #ifdef DEBUG_WEBS Serial.printf("Post data : APPLICATION_X_WWW_FORM_URLENCODED\n"); Serial.printf("Post data : %s\n", client->_data); #endif //we parse it ! if(!httpPostParamParser(client)) { //Parsing done! #ifdef DEBUG_WEBS Serial.println("Post params :"); for(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 *contentTypeP = strstr((char *)client->_data, "t-Type: application/x-www-form-urlen"); if(contentTypeP != NULL) { #ifdef DEBUG_WEBS Serial.printf("Data is of type : APPLICATION_X_WWW_FORM_URLENCODED\n"); #endif client->_httpRequestData.HMT = APPLICATION_X_WWW_FORM_URLENCODED; } //Range part for file downloads /*char *startingP = strstr((char *)client->_data, "nge: bytes="), *endP = strchr((char *)client->_data, '-'); if(startingP != NULL && endP != NULL) { endP = '\0'; client->_range = strtoul(startingP + 11,NULL, 10); #ifdef DEBUG_WEBS Serial.printf("Range : %d\n", client->_range); #endif }*/ } /* * This function is here to parse resources parameters */ boolean httpRsrcParamParser(T *client) { char *pGetParam = strchr((char *)client->_httpRequestData.httpResource, '?'); if(pGetParam != NULL) //There are some params to be parsed { if(client->_httpRequestData.getParamsDataPointer == NULL) { client->_httpRequestData.getParamsDataPointer = pGetParam +1;//We save the starting position of the string to parse } char *key = strchr(client->_httpRequestData.getParamsDataPointer, '='); char *value = strchr(client->_httpRequestData.getParamsDataPointer, '&'); if(key == NULL && value == NULL) //Only the key is present { client->_httpRequestData.getParams.add(client->_httpRequestData.getParamsDataPointer, new DictionaryHelper::StringEntity(NULL)); *pGetParam = '\0'; return false; } else if(key != NULL && value != NULL) { if(key < value)*key = '\0'; *value = '\0'; client->_httpRequestData.getParams.add(client->_httpRequestData.getParamsDataPointer, new DictionaryHelper::StringEntity(key > value ? NULL : key + 1)); memmove(client->_httpRequestData.getParamsDataPointer, value+1, strlen(value+1)+1); #ifdef DEBUG_WEBS Serial.print("Params pointer : ");Serial.println(client->_httpRequestData.getParamsDataPointer); #endif } else if(key != NULL && value == NULL) //Only one key/value pair present { *key = '\0'; client->_httpRequestData.getParams.add(client->_httpRequestData.getParamsDataPointer, new DictionaryHelper::StringEntity(key+1)); *pGetParam = '\0'; return false; } else if(key == NULL && value != NULL) { *value = '\0'; client->_httpRequestData.getParams.add(client->_httpRequestData.getParamsDataPointer, new DictionaryHelper::StringEntity(NULL)); memmove(client->_httpRequestData.getParamsDataPointer, value+1, strlen(value+1)+1); } } else //nothing to parse or done { #ifdef DEBUG_WEBS Serial.println("Nothing to parse or done"); #endif return false; } return true; } boolean httpPostParamParser(T* client) { if(client->_httpRequestData.postParamsDataPointer == NULL) { 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; 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(_sdCardManager != NULL) { File pageToSend; char *filePath(NULL), *header(NULL); uint8_t sendBuffer[READ_WRITE_BUFFER_SIZE]; int readBytes(0); //We check what kind of http verb it is switch(client->_httpRequestData.HRM) { case GET: filePath = getFilePathByHttpResource(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.print("FILE PATH : "); Serial.println(filePath); #endif pageToSend = _sdCardManager->open(filePath); free(filePath);filePath = NULL; //If we couldn't open the file if(!pageToSend) { char *response(NULL); response = (char *) malloc(sizeof(char) * (strlen_P((PGM_P)F("Resource : not found on this server")) + 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()) //To DO : List the files present in the directory { pageToSend.close(); sendInfoResponse(HTTP_CODE::HTTP_CODE_FORBIDDEN, client, "The file you want to access is a folder"); return false; } if(client->_fileSentBytes == 0 /*&& client->_range == 0*/) { char *fileName = (char *) malloc(sizeof(char) * strlen(pageToSend.name()) + 1); if(fileName != NULL)strcpy(fileName, pageToSend.name()); header = getHTTPHeader(getMIMETypeByExtension(strlwr(getFileExtension(fileName))), pageToSend.size()); #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 if(client->_fileSentBytes == 0 && (!client->_range == 0)) { client->_fileSentBytes = client->_range-500; Serial.println("RANGE SET"); }*/ else { pageToSend.seek(client->_fileSentBytes); } if(pageToSend.available()) { readBytes = pageToSend.read(sendBuffer,READ_WRITE_BUFFER_SIZE); client->_client.write(sendBuffer, readBytes); #ifdef DEBUG_WEBS Serial.print("BYTES SENT : "); Serial.println(readBytes); #endif client->_fileSentBytes += readBytes; //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; } /*Static helper methods*/ static void sendInfoResponse(HTTP_CODE http_code, T *client, const char *message) { char codeLiteral[100]; switch(http_code) { case HTTP_CODE_BAD_REQUEST: strcpy_P(codeLiteral,PSTR("Bad Request")); break; case HTTP_CODE_NOT_FOUND: strcpy_P(codeLiteral,PSTR("Not Found")); break; case HTTP_CODE_FORBIDDEN: strcpy_P(codeLiteral,PSTR("Forbidden")); break; case HTTP_CODE_METHOD_NOT_ALLOWED: strcpy_P(codeLiteral,PSTR("Method Not Allowed")); 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\nContent-Type: text/html\r\nContent-Length: %d\r\n\r\n\r\n\r\n

Error %d

%s

\r\n"), http_code, codeLiteral, 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, "mp3") == 0) return AUDIO_MPEG; else return UNKNOWN_MIME; } static char *getHTTPHeader(HttpMIMEType httpMIMEType, size_t size) { char *header = (char *) malloc(sizeof(char) /*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)); switch(httpMIMEType) { case TEXT_HTML: injectHeaderLayout(header,"text/html", size); break; case TEXT_CSS: injectHeaderLayout(header,"text/css", size); break; case TEXT_JAVASCRIPT: injectHeaderLayout(header,"text/javascript", size); break; case IMAGE_PNG: injectHeaderLayout(header,"image/png", size); break; case IMAGE_JPEG: injectHeaderLayout(header,"image/jpeg", size); break; case TEXT_PLAIN: injectHeaderLayout(header,"text/plain", size); break; case AUDIO_MPEG: injectHeaderLayout(header,"audio/mpeg", size); break; case APPLICATION_OCTET_STREAM: injectHeaderLayout(header,"application/octet-stream", size); break; default: injectHeaderLayout(header,"application/octet-stream", size); break; } return header; } static void injectHeaderLayout(char *header, const char *contentType, size_t size) { sprintf(header,"HTTP/1.1 200 OK\r\nContent-Type: %s\r\nContent-Length: %lu\r\nCache-Control: max-age=31536000\r\n\r\n",contentType,size); } static char *getFileExtension(char *name) { char *p(lastIndexOf(name, '.')); return p != NULL ? p+1 : NULL; } static char *getFilePathByHttpResource(char *res) { uint16_t buffSize = strlen(WWW_DIR) + (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; strcpy(filePath, WWW_DIR); 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; SDCardManager *_sdCardManager; }; #endif //WEBSERVER_H