ESP8266_swiss_army_board/src/app/WEBServer.h
2019-10-16 19:21:53 +02:00

601 lines
22 KiB
C++

#ifndef WEBSERVER_H
#define WEBSERVER_H
#include "TCPServer.h"
#include "Dictionary.h"
#include "SDCardManager.h"
//#define DEBUG_WEBS
template <typename T>
class WEBServer : public TCPServer<T>
{
public:
enum HttpRequestMethod {UNDEFINED, GET, HEAD, POST, PUT, DELETE, CONNECT, OPTIONS, TRACE, PATCH};
enum HttpVersion {UNKNOWN, HTTP_0_9, HTTP_1_1, HTTP_1_0, HTTP_2_0};
enum HttpMIMEType{UNKNOWN_MIME, TEXT_PLAIN, TEXT_CSS, TEXT_HTML, TEXT_JAVASCRIPT, APPLICATION_JSON, APPLICATION_X_WWW_FORM_URLENCODED, IMAGE_PNG, IMAGE_JPEG, AUDIO_MPEG, APPLICATION_OCTET_STREAM};
enum HttpParserStatus {HTTP_VERB, HTTP_RESSOURCE, HTTP_VERSION, HTTP_PARAMS, POST_DATA};
enum WEBClientState {ACCEPTED, QUERY_PARSED, RESPONSE_SENT, DONE};
enum HTTP_CODE {_100, _101, _200, _400, _401, _403, _404, _405, _500, _501};
struct HttpRequestData
{
HttpRequestMethod HRM;
HttpVersion HV;
HttpMIMEType HMT;
Dictionary<DictionaryHelper::StringEntity> getParams;
char *getParamsDataPointer; //Used in the getParams algorithm
Dictionary<DictionaryHelper::StringEntity> postParams;
char *postParamsDataPointer; //Used in the postParams algorithm
char *httpResource;
uint16_t maxResourceBuffer;
char *httpBody;
uint16_t maxBodyBuffer;
};
WEBServer(unsigned int port = 80, SDCardManager *sdCardManager = NULL, uint8_t maxClient = MAX_CLIENT, uint16_t clientDataBufferSize = 255) : TCPServer<T>(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);
}
protected:
private:
virtual T* createNewClient(WiFiClient wc)
{
return new T(wc, TCPServer<T>::freeClientId(), TCPServer<T>::_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
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::_400, 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::_400, client, "The server could not understand the request due to invalid syntax");
client->_clientState = TCPClient::ClientState::DISCARDED;
break;
}
}
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::_500, 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::_500, 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");
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::POST_DATA;
}
}
break;
case HttpParserStatus::HTTP_PARAMS: //index.htm?var1=1&var2=2...
if(!httpParamParser(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::POST_DATA:
client->_WEBClientState = WEBClientState::QUERY_PARSED;
break;
default :
sendInfoResponse(HTTP_CODE::_500, client, "WEB server error");
client->_clientState = TCPClient::ClientState::DISCARDED;
break;
}
}
boolean httpParamParser(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));
strcpy(client->_httpRequestData.getParamsDataPointer,value+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));
strcpy(client->_httpRequestData.getParamsDataPointer,value+1);
}
}
else //nothing to parse or done
{
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;
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), sendBuffer[2048];
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::_500, 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::_500, 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::_404, 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());
Serial.print("FILE EXTENSION : ");
Serial.println(getFileExtension(pageToSend.name()));
#endif
if(pageToSend.isDirectory()) //To DO : List the files present in the directory
{
pageToSend.close();
sendInfoResponse(HTTP_CODE::_403, client, "The file you want to access is a folder");
return false;
}
if(client->_fileSentBytes == 0)
{
header = getHTTPHeader(getMIMETypeByExtension(getFileExtension(pageToSend.name())), pageToSend.size());
if(header == NULL)
{
sendInfoResponse(HTTP_CODE::_500, 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())
{
readBytes = pageToSend.read(sendBuffer,2048);
client->_client.write(sendBuffer, readBytes);
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::_500, client, "The method used is not allowed");
break;
}
}else
{
sendInfoResponse(HTTP_CODE::_500, client, "Unable to access the SDCard");
return false;
}
return true;
}
void sendInfoResponse(HTTP_CODE http_code, T *client, const char *message)
{
uint16_t code(0);
char codeLiteral[100];
switch(http_code)
{
case _400:
code = 400;
strcpy_P(codeLiteral,(PGM_P)F("Bad Request"));
break;
case _404:
code = 404;
strcpy_P(codeLiteral,(PGM_P)F("Not Found"));
break;
case _403:
code = 403;
strcpy_P(codeLiteral,(PGM_P)F("Forbidden"));
break;
case _405:
code = 405;
strcpy_P(codeLiteral,(PGM_P)F("Method Not Allowed"));
break;
case _500:
code = 500;
strcpy_P(codeLiteral,(PGM_P)F("Internal Server Error"));
break;
default:
code = 000;
strcpy_P(codeLiteral,(PGM_P)F("Error Not Defined"));
break;
}
client->_client.print(F("HTTP/1.1 "));client->_client.print(code);client->_client.print(F(" "));client->_client.print(codeLiteral);client->_client.print(F("\r\nContent-Type: text/html\r\nContent-Length: "));
client->_client.print(strlen(message) + 59);
client->_client.print(F("\r\n\r\n<!DOCTYPE HTML>\r\n<html>\r\n<h1>Error "));client->_client.print(code);client->_client.print(F("</h1><p>"));
client->_client.print(message);
client->_client.print(F("</p>\r\n</html>"));
}
/*Static helper methods*/
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;
}
char *getHTTPHeader(HttpMIMEType httpMIMEType, unsigned long size)
{
char *header = (char *) malloc(sizeof(char) + strlen("HTTP/1.1 200 OK\r\nContent-Type: \r\nContent-Length: \r\n\r\n") + 74/*Longest MIME-TYPE*/ + 10 /*Max unsigned long footprint*/ + 1);
switch(httpMIMEType)
{
case TEXT_HTML:
sprintf(header,"HTTP/1.1 200 OK\r\nContent-Type: %s\r\nContent-Length: %lu\r\n\r\n","text/html",size);
break;
case TEXT_CSS:
sprintf(header,"HTTP/1.1 200 OK\r\nContent-Type: %s\r\nContent-Length: %lu\r\n\r\n","text/css",size);
break;
case TEXT_JAVASCRIPT:
sprintf(header,"HTTP/1.1 200 OK\r\nContent-Type: %s\r\nContent-Length: %lu\r\n\r\n","text/javascript",size);
break;
case IMAGE_PNG:
sprintf(header,"HTTP/1.1 200 OK\r\nContent-Type: %s\r\nContent-Length: %lu\r\n\r\n","image/png",size);
break;
case IMAGE_JPEG:
sprintf(header,"HTTP/1.1 200 OK\r\nContent-Type: %s\r\nContent-Length: %lu\r\n\r\n","image/jpeg",size);
break;
case TEXT_PLAIN:
sprintf(header,"HTTP/1.1 200 OK\r\nContent-Type: %s\r\nContent-Length: %lu\r\n\r\n","text/plain",size);
break;
case AUDIO_MPEG:
sprintf(header,"HTTP/1.1 200 OK\r\nContent-Type: %s\r\nContent-Length: %lu\r\n\r\n","audio/mpeg",size);
break;
case APPLICATION_OCTET_STREAM:
sprintf(header,"HTTP/1.1 200 OK\r\nContent-Type: %s\r\nContent-Length: %lu\r\n\r\n","application/octet-stream",size);
break;
default:
sprintf(header,"HTTP/1.1 200 OK\r\nContent-Type: %s\r\nContent-Length: %lu\r\n\r\n","application/octet-stream",size);
break;
}
return header;
}
static char *getFileExtension(char *name)
{
if(name == NULL)return NULL;
char *p(strchr(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<ApiRoutine> _apiDictionary;
SDCardManager *_sdCardManager;
};
#endif //WEBSERVER_H