1231 lines
48 KiB
C++
1231 lines
48 KiB
C++
#ifndef WEBSERVER_H
|
|
#define WEBSERVER_H
|
|
|
|
#include <SD.h>
|
|
#include "TCPServer.h"
|
|
#include "Dictionary.h"
|
|
#include "HttpConstants.h"
|
|
#include "utilities.h"
|
|
//#define DEBUG_WEBS
|
|
#define READ_WRITE_BUFFER_SIZE 5000
|
|
|
|
template <typename T>
|
|
class WEBServer : public TCPServer<T>, 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,
|
|
PARSE_HTTP_COOKIES
|
|
};
|
|
enum WEBClientState {ACCEPTED, PARSING, QUERY_PARSED, RESPONSE_SENT, DONE};
|
|
|
|
struct HttpCookie
|
|
{
|
|
DictionaryHelper::StringEntity value;
|
|
};
|
|
|
|
struct HttpRequestData
|
|
{
|
|
HttpRequestMethod HRM;
|
|
HttpVersion HV;
|
|
HttpMIMEType HMT;
|
|
size_t contentLength;
|
|
|
|
Dictionary<HttpCookie> cookies;
|
|
Dictionary<DictionaryHelper::StringEntity> getParams;
|
|
Dictionary<DictionaryHelper::StringEntity> 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<T>(port, maxClient, clientDataBufferSize), _sdClass(sdClass) {}
|
|
|
|
virtual ~WEBServer()
|
|
{
|
|
free(_WWWDir);
|
|
}
|
|
|
|
using ApiRoutineCallback = boolean (*)(WEBServer<T>*, HttpRequestData&, WiFiClient*, void*);
|
|
|
|
boolean addApiRoutine(const char *uri, ApiRoutineCallback apiRoutine, void *pData, HttpRequestMethod HRM = UNDEFINED)
|
|
{
|
|
return _apiDictionary.add(uri, new ApiRoutine({apiRoutine, pData, HRM}));
|
|
}
|
|
|
|
void clearApiRoutine(void) { _apiDictionary.clear(); };
|
|
|
|
boolean removeApiRoutine(const char *uri)
|
|
{
|
|
return _apiDictionary.remove(uri);
|
|
}
|
|
|
|
unsigned int apiRoutineCount(void) const
|
|
{
|
|
return _apiDictionary.count();
|
|
}
|
|
|
|
boolean addHttpHeader(const char *name, const char *value)
|
|
{
|
|
return _httpHeadersDictionary.add(name, DictionaryHelper::StringEntity(value));
|
|
}
|
|
|
|
void clearHttpHeaders(void)
|
|
{
|
|
_httpHeadersDictionary.clear();
|
|
}
|
|
|
|
boolean removeHttpHeader(const char *name)
|
|
{
|
|
return _httpHeadersDictionary.remove(name);
|
|
}
|
|
|
|
unsigned int httpHeadersCount(void) const
|
|
{
|
|
return _httpHeadersDictionary.count();
|
|
}
|
|
|
|
/**
|
|
* 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 = nullptr, int32_t maxAge = -1, const char *cookiePath = nullptr, const char *cookieDomain = nullptr, boolean httpOnly = false, boolean sameSite = false)
|
|
{
|
|
return _setCookieDictionary.add(cookieName, new ServerHttpCookie({cookieValue, cookieDomain, cookiePath, sameSite, httpOnly, maxAge}));
|
|
}
|
|
|
|
void clearCookies(void)
|
|
{
|
|
_setCookieDictionary.clear();
|
|
}
|
|
|
|
boolean removeCookie(const char *cookieName)
|
|
{
|
|
return _setCookieDictionary.remove(cookieName);
|
|
}
|
|
|
|
void sendHTTPResponse(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);
|
|
}
|
|
|
|
//We here send user defined HTTP headers :)
|
|
for(unsigned int i(0); i < _httpHeadersDictionary.count(); i++)
|
|
{
|
|
client->printf("\r\n%s: %s", _httpHeadersDictionary.getParameter(i), _httpHeadersDictionary.getAt(i) ? _httpHeadersDictionary.getAt(i)->getString() : "");
|
|
}
|
|
|
|
//We here send the user defined cookies :)
|
|
for(unsigned int i(0); i < _setCookieDictionary.count(); i++)
|
|
{
|
|
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 the cookie dictionary after
|
|
clearCookies();
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
const char * getWWWDir(void) const
|
|
{
|
|
return _WWWDir;
|
|
}
|
|
|
|
void setSDClass(SDClass *sdClass)
|
|
{
|
|
_sdClass = sdClass;
|
|
}
|
|
|
|
protected:
|
|
private:
|
|
virtual T* createNewClient(WiFiClient wc)
|
|
{
|
|
return new T(wc, TCPServer<T>::freeClientId(), TCPServer<T>::_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, PSTR("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, PSTR("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, PSTR("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, PSTR("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;
|
|
//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)
|
|
{
|
|
*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:
|
|
#ifdef 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!
|
|
#ifdef DEBUG_WEBS
|
|
Serial.println("Post params :");
|
|
for(unsigned int i = 0; i < client->_httpRequestData.postParams.count(); i++)
|
|
{
|
|
Serial.printf("#%s# : #%s#\n", client->_httpRequestData.postParams.getParameter(i), 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)
|
|
{
|
|
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"));
|
|
|
|
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.println("Error : header value \\r\\n not found!");
|
|
#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;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* This function fills the client's _rangeData struct with proper values
|
|
*/
|
|
bool fillRangeByteStruct(T *client, char *rangeBytesData)
|
|
{
|
|
if(!rangeBytesData)return false;
|
|
|
|
char *rangeStart = strchr(rangeBytesData, '='), *delimiter = strchr(rangeBytesData, '-'), *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;
|
|
|
|
client->_rangeData._rangeRequest = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
* This function parses 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;
|
|
}
|
|
|
|
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
|
|
{
|
|
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))(this, client->_httpRequestData, &(client->_client), ref->pData);
|
|
}
|
|
else if(ref->HRM == client->_httpRequestData.HRM)
|
|
{
|
|
return (*(ref->apiRoutine))(this, 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<T>::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)
|
|
{
|
|
sendHTTPResponse(&client->_client, HttpConstants::httpMIMETypeToString(HttpConstants::TEXT_HTML));
|
|
client->_client.printf_P(PSTR( "<!DOCTYPE HTML>\r\n\
|
|
<html>\r\n\
|
|
<head>\r\n\
|
|
<title>Index of %s</title>\r\n\
|
|
</head>\r\n\
|
|
<body>\r\n\
|
|
<h1>Index of %s</h1>\r\n\
|
|
<table>\r\n\
|
|
<tr><th>Type</th><th>Name</th><th>Created</th><th>Last modified</th><th>Size</th></tr>\r\n\
|
|
<tr><th colspan=\"5\"><hr></th></tr>\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("<tr><td>[DIR]</td><td><a href=\"%s/\">Parent Directory</a></td><td> - </td><td align=\"right\"> - </td><td> </td></tr>\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("<tr><td>%s</td><td><a href=\"%s\">%s</a></td><td align=\"right\">%d-%s-%s %s:%s</td><td align=\"right\">%d-%s-%s %s:%s</td><td align=\"right\">%.1fK</td></tr>\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( "<tr><th colspan=\"5\"><hr></th></tr>\r\n\
|
|
</table>\r\n\
|
|
<address>SAB WEBServer, Version %s at %s Port %u</address>\r\n\
|
|
</body>\r\n\
|
|
</html>"), "1.0.0", client->_client.localIP().toString().c_str(), TCPServer<T>::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<!DOCTYPE HTML>\r\n<html>\r\n<h1>Error %d</h1><p>%s</p>\r\n</html>"), 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
|
|
{
|
|
ApiRoutineCallback apiRoutine;
|
|
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<ServerHttpCookie> _setCookieDictionary;
|
|
SDClass *_sdClass;
|
|
char *_WWWDir = nullptr; //Website root folder
|
|
};
|
|
|
|
#endif //WEBSERVER_H
|