SpriteHTTPD - embedded HTTP server with read-only filesystem and templating, originally developed for ESP8266, now stand-alone and POSIX compatible.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
spritehttpd/spritehttpd/src/httpd.c

936 lines
32 KiB

/*
Esp8266 http server - core routines
*/
/*
* ----------------------------------------------------------------------------
* "THE BEER-WARE LICENSE" (Revision 42):
* Jeroen Domburg <jeroen@spritesmods.com> wrote this file. As long as you retain
* this notice you can do whatever you want with this stuff. If we meet some day,
* and you think this stuff is worth it, you can buy me a beer in return.
* ----------------------------------------------------------------------------
*/
#include <string.h>
#include <stdlib.h>
#include "httpd.h"
#include "httpd-platform.h"
#include "httpd-utils.h"
#include "httpd-logging.h"
static void cleanupCgiAndUserData(HttpdConnData *hconn);
static void httpdRetireConn(HttpdConnData *hconn);
_Static_assert(HTTPD_MAX_CONNECTIONS < 256, "HTTPD_MAX_CONNECTIONS must be at most 255");
//This gets set at init time.
static const HttpdBuiltInUrl *s_builtInUrls;
static const char *s_serverName = HTTPD_SERVERNAME;
typedef struct HttpSendBacklogItem HttpSendBacklogItem;
struct HttpSendBacklogItem {
size_t len;
HttpSendBacklogItem *next;
uint8_t data[];
};
//Flags (1 byte)
#define HFL_HTTP11 (1<<0)
#define HFL_CHUNKED (1<<1)
#define HFL_SENDINGBODY (1<<2)
#define HFL_DISCONAFTERSENT (1<<3)
#define HFL_NOCONNECTIONSTR (1<<4)
#define HFL_NOCORS (1<<5)
//Private data for http connection
struct HttpdPriv {
char head[HTTPD_MAX_HEAD_LEN];
char corsToken[HTTPD_MAX_CORS_TOKEN_LEN];
size_t headPos;
uint8_t *sendBuff;
size_t sendBuffLen;
char *chunkHdr;
HttpSendBacklogItem *sendBacklog;
size_t sendBacklogSize;
uint8_t flags;
};
//Connection pool
HttpdConnData *s_connData[HTTPD_MAX_CONNECTIONS];
void httpdInternalCloseAllSockets()
{
httpdPlatLock();
/*release data connection*/
for (int i = 0; i < HTTPD_MAX_CONNECTIONS; i++) {
//find all valid handle
if (s_connData[i]->conn == NULL) {
continue;
}
if (s_connData[i]->cgi != NULL) {
//flush cgi data
s_connData[i]->cgi(s_connData[i]);
s_connData[i]->cgi = NULL;
}
httpdConnRelease(s_connData[i]->conn);
httpdRetireConn(s_connData[i]);
s_connData[i] = NULL;
}
httpdPlatUnlock();
}
void httpdAddCacheHeaders(HttpdConnData *connData, const char *mime)
{
// TODO make this extensible
if (streq(mime, "text/html")
|| streq(mime, "text/plain")
|| streq(mime, "text/csv")
|| streq(mime, "application/json")
) {
return;
}
httpdHeader(connData, "Cache-Control", "max-age=7200, public, must-revalidate");
}
const char *httpdGetVersion(void)
{
return HTTPDVER;
}
size_t httpGetBacklogSize(const HttpdConnData *conn)
{
HttpSendBacklogItem *bl = conn->priv->sendBacklog;
if (!bl) { return 0; }
size_t bytes = 0;
while (bl != NULL) {
bytes += bl->len;
bl = bl->next;
}
return bytes;
}
//Looks up the connData info for a specific connection
static HttpdConnData *httpdFindConnData(ConnTypePtr conn, const uint8_t *remIp, int remPort)
{
for (int i = 0; i < HTTPD_MAX_CONNECTIONS; i++) {
if (s_connData[i] && s_connData[i]->remote_port == remPort &&
memcmp(s_connData[i]->remote_ip, remIp, 4) == 0) {
s_connData[i]->conn = conn;
return s_connData[i];
}
}
//Shouldn't happen.
http_error("*** Unknown connection %d.%d.%d.%d:%d", remIp[0] & 0xff, remIp[1] & 0xff, remIp[2] & 0xff, remIp[3] & 0xff, remPort);
httpdConnDisconnect(conn);
return NULL;
}
//Retires a connection for re-use
static void httpdRetireConn(HttpdConnData *hconn)
{
if (!hconn) {
return;
}
http_info("Pool slot %d: socket closed.", hconn->slot);
// this sets the `conn` pointer to NULL to indicate we are cleaning up to CGI
cleanupCgiAndUserData(hconn);
// Free any memory allocated for backlog - walk the linked list
if (hconn->priv->sendBacklog != NULL) {
HttpSendBacklogItem *i, *j;
i = hconn->priv->sendBacklog;
do {
j = i;
i = i->next;
httpdPlatFree(j);
} while (i != NULL);
}
if (hconn->post->buff != NULL) { httpdPlatFree(hconn->post->buff); }
if (hconn->post != NULL) { httpdPlatFree(hconn->post); }
if (hconn->priv != NULL) { httpdPlatFree(hconn->priv); }
// Unlink from the connection list
s_connData[hconn->slot] = NULL;
// release memory
httpdPlatFree(hconn);
}
//Get the value of a certain header in the HTTP client head
//Returns true when found, false when not found.
int httpdGetHeader(HttpdConnData *conn, const char *header, char *buff, size_t buffLen)
{
char *p = conn->priv->head;
p = p + strlen(p) + 1; //skip GET/POST part
p = p + strlen(p) + 1; //skip HTTP part
while (p < (conn->priv->head + conn->priv->headPos)) {
while (*p <= 32 && *p != 0) { p++; } //skip crap at start
//See if this is the header
if (strstarts(p, header) && p[strlen(header)] == ':') {
//Skip 'key:' bit of header line
p = p + strlen(header) + 1;
//Skip past spaces after the colon
while (*p == ' ') { p++; }
//Copy from p to end
while (*p != 0 && *p != '\r' && *p != '\n' && buffLen > 1) {
*buff++ = *p++;
buffLen--;
}
//Zero-terminate string
*buff = 0;
//All done :)
return 1;
}
p += strlen(p) + 1; //Skip past end of string and \0 terminator
}
return 0;
}
void httdSetTransferMode(HttpdConnData *conn, httpd_transfer_opt mode)
{
if (mode == HTTPD_TRANSFER_CLOSE) {
conn->priv->flags &= (uint8_t) ~HFL_CHUNKED;
conn->priv->flags &= (uint8_t) ~HFL_NOCONNECTIONSTR;
} else if (mode == HTTPD_TRANSFER_CHUNKED) {
conn->priv->flags |= HFL_CHUNKED;
conn->priv->flags &= (uint8_t) ~HFL_NOCONNECTIONSTR;
} else if (mode == HTTPD_TRANSFER_NONE) {
conn->priv->flags &= (uint8_t) ~HFL_CHUNKED;
conn->priv->flags |= HFL_NOCONNECTIONSTR;
}
}
void httdResponseOptions(HttpdConnData *conn, int cors)
{
if (cors == 0) { conn->priv->flags |= HFL_NOCORS; }
}
//Start the response headers.
void httpdStartResponse(HttpdConnData *conn, int code)
{
char buff[256];
const char *connStr = "";
if (!(conn->priv->flags & HFL_NOCONNECTIONSTR)) {
if (conn->priv->flags & HFL_CHUNKED) {
connStr = "Transfer-Encoding: chunked\r\n";
} else {
connStr = "Connection: close\r\n";
}
}
size_t l = (size_t)sprintf(buff, "HTTP/1.%d %d %s\r\nServer: %s\r\n%s",
((conn->priv->flags & HFL_HTTP11) ? 1 : 0),
code,
httpdStatusName(code),
s_serverName,
connStr);
httpdSendStrN(conn, buff, l);
if (!(conn->priv->flags & HFL_NOCORS)) {
// CORS headers
httpdSendStr(conn, "Access-Control-Allow-Origin: *\r\n");
httpdSendStr(conn, "Access-Control-Allow-Methods: GET,POST,OPTIONS\r\n");
}
}
//Send a http header.
void httpdHeader(HttpdConnData *conn, const char *field, const char *val)
{
httpdSendStr(conn, field);
httpdSendStr(conn, ": ");
httpdSendStr(conn, val);
httpdSendStr(conn, "\r\n");
}
//Finish the headers.
void httpdEndHeaders(HttpdConnData *conn)
{
httpdSendStr(conn, "\r\n");
conn->priv->flags |= HFL_SENDINGBODY;
}
//Redirect to the given URL.
void httpdRedirect(HttpdConnData *conn, const char *newUrl)
{
http_dbg("Redirecting to %s", newUrl);
httpdStartResponse(conn, 302);
httpdHeader(conn, "Location", newUrl);
httpdEndHeaders(conn);
httpdSendStr(conn, "Moved to ");
httpdSendStr(conn, newUrl);
}
//Add data to the send buffer. len is the length of the data. If len is -1
//the data is seen as a C-string.
//Returns 1 for success, 0 for out-of-memory.
int httpdSend(HttpdConnData *conn, const uint8_t *data, size_t len)
{
if (conn->conn == NULL) { return 0; }
if (len == 0) { return 0; }
if (conn->priv->flags & HFL_CHUNKED && conn->priv->flags & HFL_SENDINGBODY && conn->priv->chunkHdr == NULL) {
if (conn->priv->sendBuffLen + len + 6 > HTTPD_MAX_SENDBUFF_LEN) { return 0; }
//Establish start of chunk
conn->priv->chunkHdr = (char *) &conn->priv->sendBuff[conn->priv->sendBuffLen];
strcpy(conn->priv->chunkHdr, "0000\r\n");
conn->priv->sendBuffLen += 6;
}
if (conn->priv->sendBuffLen + len > HTTPD_MAX_SENDBUFF_LEN) { return 0; }
memcpy(conn->priv->sendBuff + conn->priv->sendBuffLen, data, len);
conn->priv->sendBuffLen += len;
return 1;
}
#define httpdSend_orDie(conn, data, len) do { if (!httpdSend((conn), (const uint8_t *)(data), (size_t)(len))) return false; } while (0)
#define httpdSendStr_orDie(conn, data) do { if (!httpdSendStr((conn), (data))) return false; } while (0)
/* encode for HTML. returns 0 or 1 - 1 = success */
int httpdSend_html(HttpdConnData *conn, const char *data, ssize_t len)
{
int start = 0, end = 0;
char c;
if (conn->conn == NULL) { return 0; }
if (len < 0) { len = (int) strlen((const char *) data); }
if (len == 0) { return 0; }
for (end = 0; end < len; end++) {
c = data[end];
if (c == 0) {
// we found EOS
break;
}
if (c == '"' || c == '\'' || c == '<' || c == '>') {
if (start < end) httpdSend_orDie(conn, data + start, end - start);
start = end + 1;
}
if (c == '"') httpdSendStr_orDie(conn, "&#34;");
else if (c == '\'') httpdSendStr_orDie(conn, "&#39;");
else if (c == '<') httpdSendStr_orDie(conn, "&lt;");
else if (c == '>') httpdSendStr_orDie(conn, "&gt;");
}
if (start < end) httpdSend_orDie(conn, data + start, end - start);
return 1;
}
/* encode for JS. returns 0 or 1 - 1 = success */
int httpdSend_js(HttpdConnData *conn, const char *data, ssize_t len)
{
int start = 0, end = 0;
char c;
if (conn->conn == NULL) { return 0; }
if (len < 0) { len = (int) strlen((const char *) data); }
if (len == 0) { return 0; }
for (end = 0; end < len; end++) {
c = data[end];
if (c == 0) {
// we found EOS
break;
}
if (c == '"' || c == '\\' || c == '\'' || c == '<' || c == '>' || c == '\n' || c == '\r') {
if (start < end) httpdSend_orDie(conn, data + start, end - start);
start = end + 1;
}
if (c == '"') httpdSendStr_orDie(conn, "\\\"");
else if (c == '\'') httpdSendStr_orDie(conn, "\\'");
else if (c == '\\') httpdSendStr_orDie(conn, "\\\\");
else if (c == '<') httpdSendStr_orDie(conn, "\\u003C");
else if (c == '>') httpdSendStr_orDie(conn, "\\u003E");
else if (c == '\n') httpdSendStr_orDie(conn, "\\n");
else if (c == '\r') httpdSendStr_orDie(conn, "\\r");
}
if (start < end) httpdSend_orDie(conn, data + start, end - start);
return 1;
}
//Function to send any data in conn->priv->sendBuff. Do not use in CGIs unless you know what you
//are doing! Also, if you do set conn->cgi to NULL to indicate the connection is closed, do it BEFORE
//calling this.
//Returns false if data could not be sent nor put in backlog.
bool httpdFlushSendBuffer(HttpdConnData *conn)
{
int r;
size_t len;
if (conn->conn == NULL) { return false; }
if (conn->priv->chunkHdr != NULL) {
//We're sending chunked data, and the chunk needs fixing up.
//Finish chunk with cr/lf
httpdSendStr(conn, "\r\n");
//Calculate length of chunk
len = (size_t) ((char *)(&conn->priv->sendBuff[conn->priv->sendBuffLen]) - conn->priv->chunkHdr) - 8;
//Fix up chunk header to correct value
conn->priv->chunkHdr[0] = httpdHexNibble((uint8_t) (len >> 12));
conn->priv->chunkHdr[1] = httpdHexNibble((uint8_t) (len >> 8));
conn->priv->chunkHdr[2] = httpdHexNibble((uint8_t) (len >> 4));
conn->priv->chunkHdr[3] = httpdHexNibble((uint8_t) (len >> 0));
//Reset chunk hdr for next call
conn->priv->chunkHdr = NULL;
}
if (conn->priv->flags & HFL_CHUNKED && conn->priv->flags & HFL_SENDINGBODY && conn->cgi == NULL) {
//Connection finished sending whatever needs to be sent. Add NULL chunk to indicate this.
strcpy((char *) &conn->priv->sendBuff[conn->priv->sendBuffLen], "0\r\n\r\n");
conn->priv->sendBuffLen += 5;
}
if (conn->priv->sendBuffLen != 0) {
r = httpdConnSendData(conn->conn, conn->priv->sendBuff, conn->priv->sendBuffLen);
if (!r) {
//Can't send this for some reason. Dump packet in backlog, we can send it later.
if (conn->priv->sendBacklogSize + conn->priv->sendBuffLen > HTTPD_MAX_BACKLOG_SIZE) {
http_error("Httpd: Backlog overrun, dropped %dB", (int) conn->priv->sendBuffLen);
conn->priv->sendBuffLen = 0;
return false;
}
HttpSendBacklogItem *i = httpdPlatMalloc(sizeof(HttpSendBacklogItem) + conn->priv->sendBuffLen);
if (i == NULL) {
http_error("Httpd: Backlog: malloc failed, out of memory!");
return false;
}
memcpy(i->data, conn->priv->sendBuff, conn->priv->sendBuffLen);
i->len = conn->priv->sendBuffLen;
i->next = NULL;
if (conn->priv->sendBacklog == NULL) {
conn->priv->sendBacklog = i;
} else {
HttpSendBacklogItem *e = conn->priv->sendBacklog;
while (e->next != NULL) { e = e->next; }
e->next = i;
}
conn->priv->sendBacklogSize += conn->priv->sendBuffLen;
}
conn->priv->sendBuffLen = 0;
}
return true;
}
void httpdCgiIsDone(HttpdConnData *conn)
{
conn->cgi = NULL; //no need to call this anymore
if (conn->priv->flags & HFL_CHUNKED) {
http_dbg("Pool slot %d is done. Cleaning up for next req", conn->slot);
httpdFlushSendBuffer(conn);
//Note: Do not clean up sendBacklog, it may still contain data at this point.
conn->priv->headPos = 0;
conn->post->len = -1;
conn->priv->flags = 0;
if (conn->post->buff) { httpdPlatFree(conn->post->buff); }
conn->post->buff = NULL;
conn->post->buffLen = 0;
conn->post->received = 0;
conn->hostName = NULL;
} else {
//Cannot re-use this connection. Mark to get it killed after all data is sent.
conn->priv->flags |= HFL_DISCONAFTERSENT;
}
}
//Callback called when the data on a socket has been successfully
//sent.
void httpdSentCb(ConnTypePtr rconn, const uint8_t *remIp, uint16_t remPort)
{
HttpdConnData *conn = httpdFindConnData(rconn, remIp, remPort);
httpdContinue(conn);
}
//Can be called after a CGI function has returned HTTPD_CGI_MORE to
//resume handling an open connection asynchronously
void httpdContinue(HttpdConnData *conn)
{
int r;
httpdPlatLock();
uint8_t *sendBuff;
if (conn == NULL) { return; }
if (conn->priv->sendBacklog != NULL) {
//We have some backlog to send first.
HttpSendBacklogItem *next = conn->priv->sendBacklog->next;
httpdConnSendData(conn->conn, (uint8_t *) conn->priv->sendBacklog->data, conn->priv->sendBacklog->len);
conn->priv->sendBacklogSize -= conn->priv->sendBacklog->len;
httpdPlatFree(conn->priv->sendBacklog);
conn->priv->sendBacklog = next;
httpdPlatUnlock();
return;
}
if (conn->priv->flags & HFL_DISCONAFTERSENT) { //Marked for destruction?
http_dbg("Pool slot %d is done. Closing.", conn->slot);
httpdConnDisconnect(conn->conn);
httpdPlatUnlock();
return; //No need to call httpdFlushSendBuffer.
}
//If we don't have a CGI function, there's nothing to do but wait for something from the client.
if (conn->cgi == NULL) {
httpdPlatUnlock();
return;
}
sendBuff = httpdPlatMalloc(HTTPD_MAX_SENDBUFF_LEN);
if (sendBuff == NULL) {
http_error("Malloc of sendBuff failed!");
httpdPlatUnlock();
return;
}
conn->priv->sendBuff = sendBuff;
conn->priv->sendBuffLen = 0;
r = conn->cgi(conn); //Execute cgi fn.
if (r == HTTPD_CGI_DONE) {
httpdCgiIsDone(conn);
}
if (r == HTTPD_CGI_NOTFOUND || r == HTTPD_CGI_AUTHENTICATED) {
http_error("ERROR! CGI fn returns code %d after sending data! Bad CGI!", r);
httpdCgiIsDone(conn);
}
httpdFlushSendBuffer(conn);
httpdPlatFree(sendBuff);
httpdPlatUnlock();
}
//This is called when the headers have been received and the connection is ready to send
//the result headers and data.
//We need to find the CGI function to call, call it, and dependent on what it returns either
//find the next cgi function, wait till the cgi data is sent or close up the connection.
static void httpdProcessRequest(HttpdConnData *conn)
{
int r;
int i = 0;
if (conn->url == NULL) {
router_warn("WtF? url = NULL");
return; //Shouldn't happen
}
// CORS preflight, allow the token we received before
if (conn->requestType == HTTPD_METHOD_OPTIONS) {
httpdStartResponse(conn, 200);
httpdHeader(conn, "Access-Control-Allow-Headers", conn->priv->corsToken);
httpdEndHeaders(conn);
httpdCgiIsDone(conn);
router_dbg("CORS preflight resp sent.");
return;
}
//See if we can find a CGI that's happy to handle the request.
while (1) {
//Look up URL in the built-in URL table.
while (s_builtInUrls[i].url != NULL) {
int match = 0;
const char *route = s_builtInUrls[i].url;
//See if there's a literal match
if (streq(route, conn->url)) { match = 1; }
//See if there's a wildcard match (*)
if (!match && last_char(route) == '*' &&
strneq(route, conn->url, strlen(route) - 1)) {
match = 1;
}
// Optional slash (/?)
if (!match && last_char(route) == '?' && last_char_n(route, 2) == '/' &&
strneq(route, conn->url, strlen(route) - 2) &&
strlen(conn->url) <= strlen(route) - 1) {
match = 1;
}
if (match) {
router_dbg("Matched route #%d, url=%s", i, route);
conn->cgiData = NULL;
conn->cgi = s_builtInUrls[i].cgiCb;
conn->cgiArg = s_builtInUrls[i].cgiArg;
conn->cgiArg2 = s_builtInUrls[i].cgiArg2;
break;
}
i++;
}
if (s_builtInUrls[i].url == NULL) {
//Drat, we're at the end of the URL table. This usually shouldn't happen. Well, just
//generate a built-in 404 to handle this.
router_warn("%s not found. 404!", conn->url);
conn->cgi = cgiNotFound;
}
//Okay, we have a CGI function that matches the URL. See if it wants to handle the
//particular URL we're supposed to handle.
r = conn->cgi(conn);
if (r == HTTPD_CGI_MORE) {
//Yep, it's happy to do so and has more data to send.
if (conn->recvHdl) {
//Seems the CGI is planning to do some long-term communications with the socket.
//Disable the timeout on it, so we won't run into that.
httpdPlatDisableTimeout(conn->conn);
}
httpdFlushSendBuffer(conn);
return;
} else if (r == HTTPD_CGI_DONE) {
//Yep, it's happy to do so and already is done sending data.
httpdCgiIsDone(conn);
return;
} else if (r == HTTPD_CGI_NOTFOUND || r == HTTPD_CGI_AUTHENTICATED) {
//URL doesn't want to handle the request: either the data isn't found or there's no
//need to generate a login screen.
i++; //look at next url the next iteration of the loop.
}
}
}
//Parse a line of header data and modify the connection data accordingly.
static void httpdParseHeader(char *h, HttpdConnData *conn)
{
int i;
char firstLine = 0;
if (strstarts(h, "GET ")) {
conn->requestType = HTTPD_METHOD_GET;
firstLine = 1;
} else if (strstarts(h, "Host:")) {
i = 5;
while (h[i] == ' ') { i++; }
conn->hostName = &h[i];
} else if (strstarts(h, "POST ")) {
conn->requestType = HTTPD_METHOD_POST;
firstLine = 1;
} else if (strstarts(h, "PUT ")) {
conn->requestType = HTTPD_METHOD_PUT;
firstLine = 1;
} else if (strstarts(h, "PATCH ")) {
conn->requestType = HTTPD_METHOD_PATCH;
firstLine = 1;
} else if (strstarts(h, "OPTIONS ")) {
conn->requestType = HTTPD_METHOD_OPTIONS;
firstLine = 1;
} else if (strstarts(h, "DELETE ")) {
conn->requestType = HTTPD_METHOD_DELETE;
firstLine = 1;
}
if (firstLine) {
char *e;
//Skip past the space after POST/GET
i = 0;
while (h[i] != ' ') { i++; }
conn->url = h + i + 1;
//Figure out end of url.
e = strstr(conn->url, " ");
if (e == NULL) { return; } //wtf?
*e = 0; //terminate url part
e++; //Skip to protocol indicator
while (*e == ' ') { e++; } //Skip spaces.
//If HTTP/1.1, note that and set chunked encoding
if (strcasecmp(e, "HTTP/1.1") == 0) {
conn->priv->flags |= HFL_HTTP11 | HFL_CHUNKED;
}
http_info("URL = %s", conn->url);
//Parse out the URL part before the GET parameters.
conn->getArgs = strstr(conn->url, "?");
if (conn->getArgs != 0) {
*conn->getArgs = 0;
conn->getArgs++;
http_dbg("GET args = %s", conn->getArgs);
} else {
conn->getArgs = NULL;
}
} else if (strstarts(h, "Connection:")) {
i = 11;
//Skip trailing spaces
while (h[i] == ' ') { i++; }
if (strstarts(&h[i], "close")) { conn->priv->flags &= (uint8_t) ~HFL_CHUNKED; } //Don't use chunked conn
} else if (strstarts(h, "Content-Length:")) {
i = 15;
//Skip trailing spaces
while (h[i] == ' ') { i++; }
//Get POST data length
conn->post->len = (int) strtol(h + i, NULL, 10);
// Allocate the buffer
if (conn->post->len > HTTPD_MAX_POST_LEN) {
// we'll stream this in in chunks
conn->post->buffSize = HTTPD_MAX_POST_LEN;
} else {
conn->post->buffSize = (size_t) conn->post->len;
}
http_dbg("Mallocced buffer for %d + 1 bytes of post data.", (int) conn->post->buffSize);
conn->post->buff = (char *) httpdPlatMalloc(conn->post->buffSize + 1);
if (conn->post->buff == NULL) {
http_error("...failed!");
return;
}
conn->post->buffLen = 0;
} else if (strstarts(h, "Content-Type:")) {
if (strstr(h, "multipart/form-data")) {
// It's multipart form data so let's pull out the boundary for future use
char *b;
if ((b = strstr(h, "boundary=")) != NULL) {
conn->post->multipartBoundary = b + 7; // move the pointer 2 chars before boundary then fill them with dashes
conn->post->multipartBoundary[0] = '-';
conn->post->multipartBoundary[1] = '-';
http_dbg("boundary = %s", conn->post->multipartBoundary);
}
}
} else if (strstarts(h, "Access-Control-Request-Headers: ")) {
// CORS crap that needs to be repeated in the response
http_info("CORS preflight request.");
strncpy(conn->priv->corsToken, h + strlen("Access-Control-Request-Headers: "), HTTPD_MAX_CORS_TOKEN_LEN);
}
}
//Make a connection 'live' so we can do all the things a cgi can do to it.
//ToDo: Also make httpdRecvCb/httpdContinue use these?
//ToDo: Fail if malloc fails?
void httpdConnSendStart(HttpdConnData *conn)
{
httpdPlatLock();
uint8_t *sendBuff = httpdPlatMalloc(HTTPD_MAX_SENDBUFF_LEN);
if (sendBuff == NULL) {
http_error("Malloc sendBuff failed!");
return;
}
conn->priv->sendBuff = sendBuff;
conn->priv->sendBuffLen = 0;
}
//Finish the live-ness of a connection. Always call this after httpdConnStart
void httpdConnSendFinish(HttpdConnData *conn)
{
if (conn->conn) { httpdFlushSendBuffer(conn); }
httpdPlatFree(conn->priv->sendBuff);
httpdPlatUnlock();
}
//Callback called when there's data available on a socket.
void httpdRecvCb(ConnTypePtr rconn, const uint8_t *remIp, uint16_t remPort, uint8_t *data, size_t len)
{
httpd_cgi_state r;
size_t x;
char *p, *e;
httpdPlatLock();
HttpdConnData *conn = httpdFindConnData(rconn, remIp, remPort);
if (conn == NULL) {
httpdPlatUnlock();
return;
}
uint8_t *sendBuff = httpdPlatMalloc(HTTPD_MAX_SENDBUFF_LEN);
if (sendBuff == NULL) {
http_error("Malloc sendBuff failed!");
httpdPlatUnlock();
return;
}
conn->priv->sendBuff = sendBuff;
conn->priv->sendBuffLen = 0;
conn->priv->corsToken[0] = 0;
//This is slightly evil/dirty: we abuse conn->post->len as a state variable for where in the http communications we are:
//<0 (-1): Post len unknown because we're still receiving headers
//==0: No post data
//>0: Need to receive post data
//ToDo: See if we can use something more elegant for this.
for (x = 0; x < len; x++) {
if (conn->post->len < 0) {
//This byte is a header byte.
if (data[x] == '\n') {
//Compatibility with clients that send \n only: fake a \r in front of this.
if (conn->priv->headPos != 0 && conn->priv->head[conn->priv->headPos - 1] != '\r') {
conn->priv->head[conn->priv->headPos++] = '\r';
}
}
//ToDo: return http error code 431 (request header too long) if this happens
if (conn->priv->headPos != HTTPD_MAX_HEAD_LEN) { conn->priv->head[conn->priv->headPos++] = (char) data[x]; }
conn->priv->head[conn->priv->headPos] = 0;
//Scan for /r/n/r/n. Receiving this indicates the headers end.
if (data[x] == '\n' && strstr(conn->priv->head, "\r\n\r\n") != NULL) {
//Indicate we're done with the headers.
conn->post->len = 0;
//Reset url data
conn->url = NULL;
//Iterate over all received headers and parse them.
p = conn->priv->head;
while (p < (&conn->priv->head[conn->priv->headPos - 4])) {
e = strstr(p, "\r\n"); //Find end of header line
if (e == NULL) { break; } //Shouldn't happen.
e[0] = 0; //Zero-terminate header
httpdParseHeader(p, conn); //and parse it.
p = e + 2; //Skip /r/n (now /0/n)
}
//If we don't need to receive post data, we can send the response now.
if (conn->post->len == 0) {
httpdProcessRequest(conn);
}
}
} else if (conn->post->len != 0) {
//This byte is a POST byte.
conn->post->buff[conn->post->buffLen++] = (char) data[x];
conn->post->received++;
conn->hostName = NULL;
if (conn->post->buffLen >= conn->post->buffSize || (int) conn->post->received == conn->post->len) {
//Received a chunk of post data
conn->post->buff[conn->post->buffLen] = 0; //zero-terminate, in case the cgi handler knows it can use strings
//Process the data
if (conn->cgi) {
r = conn->cgi(conn);
if (r == HTTPD_CGI_DONE) {
httpdCgiIsDone(conn);
}
} else {
//No CGI fn set yet: probably first call. Allow httpdProcessRequest to choose CGI and
//call it the first time.
httpdProcessRequest(conn);
}
conn->post->buffLen = 0;
conn->post->len = 0; // this causes transfer to the recvHdl branch
}
} else {
//Let cgi handle data if it registered a recvHdl callback. If not, ignore.
if (conn->recvHdl) {
r = conn->recvHdl(conn, data + x, len - x);
if (r == HTTPD_CGI_DONE) {
http_dbg("Recvhdl returned DONE");
httpdCgiIsDone(conn);
//We assume the recvhdlr has sent something; we'll kill the sock in the sent callback.
}
break; //ignore rest of data, recvhdl has parsed it.
} else {
http_warn("Eh? Got unexpected data from client. %s", data);
}
}
}
if (conn->conn) {
httpdFlushSendBuffer(conn);
}
httpdPlatFree(sendBuff);
httpdPlatUnlock();
}
//The platform layer should ALWAYS call this function, regardless if the connection is closed by the server
//or by the client.
void httpdDisconCb(ConnTypePtr rconn, const uint8_t *remIp, uint16_t remPort)
{
httpdPlatLock();
HttpdConnData *hconn = httpdFindConnData(rconn, remIp, remPort);
if (hconn == NULL) {
httpdPlatUnlock();
return;
}
httpdRetireConn(hconn);
httpdPlatUnlock();
}
/**
* Clean up cgiData and userData and do any cgi-specific finalizing.
*
* @param hconn
*/
static void cleanupCgiAndUserData(HttpdConnData *hconn)
{
hconn->conn = NULL; //indicate cgi the connection is gone
if (hconn->cgi) {
//Execute cgi fn if needed
hconn->cgi(hconn);
hconn->cgi = NULL;
}
if (hconn->userDataCleanupCb && hconn->userData) {
hconn->userDataCleanupCb(hconn->userData);
hconn->userData = NULL;
hconn->userDataCleanupCb = NULL;
}
}
int httpdConnectCb(ConnTypePtr conn, const uint8_t *remIp, uint16_t remPort)
{
uint8_t i;
httpdPlatLock();
//Find empty conndata in pool
for (i = 0; i < HTTPD_MAX_CONNECTIONS; i++) {
if (s_connData[i] == NULL) {
break;
}
}
http_info("Conn req from %d.%d.%d.%d:%d, using pool slot %d", remIp[0], remIp[1], remIp[2], remIp[3], remPort, i);
if (i >= HTTPD_MAX_CONNECTIONS) {
http_error("Aiee, conn pool overflow!");
httpdPlatUnlock();
return 0;
}
s_connData[i] = httpdPlatMalloc(sizeof(HttpdConnData));
if (s_connData[i] == NULL) {
http_warn("Out of memory allocating connData!");
httpdPlatUnlock();
return 0;
}
memset(s_connData[i], 0, sizeof(HttpdConnData));
s_connData[i]->priv = httpdPlatMalloc(sizeof(HttpdPriv));
memset(s_connData[i]->priv, 0, sizeof(HttpdPriv));
s_connData[i]->conn = conn;
s_connData[i]->slot = i;
s_connData[i]->priv->headPos = 0;
s_connData[i]->post = httpdPlatMalloc(sizeof(HttpdPostData));
if (s_connData[i]->post == NULL) {
http_error("Out of memory allocating connData post struct!");
httpdPlatUnlock();
return 0;
}
memset(s_connData[i]->post, 0, sizeof(HttpdPostData));
s_connData[i]->post->buff = NULL;
s_connData[i]->post->buffLen = 0;
s_connData[i]->post->received = 0;
s_connData[i]->post->len = -1;
s_connData[i]->hostName = NULL;
s_connData[i]->remote_port = remPort;
s_connData[i]->priv->sendBacklog = NULL;
s_connData[i]->priv->sendBacklogSize = 0;
memcpy(s_connData[i]->remote_ip, remIp, 4);
httpdPlatUnlock();
return 1;
}
//Httpd initialization routine. Call this to kick off webserver functionality.
httpd_thread_handle_t *httpdStart(const HttpdBuiltInUrl *fixedUrls, struct httpd_options *options)
{
int i;
for (i = 0; i < HTTPD_MAX_CONNECTIONS; i++) {
s_connData[i] = NULL;
}
s_builtInUrls = fixedUrls;
httpdPlatInit();
http_info("Httpd init");
return httpdPlatStart(options);
}
void httpdJoin(httpd_thread_handle_t *handle)
{
if (handle) {
httpdPlatJoin(handle);
}
}
void httpdSetName(const char *name)
{
s_serverName = name;
}