/* Esp8266 http server - core routines */ /* * ---------------------------------------------------------------------------- * "THE BEER-WARE LICENSE" (Revision 42): * Jeroen Domburg 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 #include #include "httpd.h" #include "httpd-platform.h" #include "httpd-utils.h" #include "httpd-logging.h" #include "httpd-heap.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; //Connection pool HttpdConnData s_connData[HTTPD_MAX_CONNECTIONS]; void httpdInternalCloseAllSockets(void) { httpdPlatLock(); /*release data connection*/ for (int i = 0; i < HTTPD_MAX_CONNECTIONS; i++) { HttpdConnData *hconn = &s_connData[i]; //find all valid handle if (!hconn->occupied) { continue; } httpdConnRelease(hconn->conn); httpdRetireConn(hconn); } 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) { HttpdSendBacklogItem *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, httpd_ipaddr_t remIp, int remPort) { for (int i = 0; i < HTTPD_MAX_CONNECTIONS; i++) { if (s_connData[i].occupied && s_connData[i].remote_port == remPort && s_connData[i].remote_ip == remIp) { s_connData[i].conn = conn; return &s_connData[i]; } } //Shouldn't happen. http_error("Unknown connection"); httpdConnDisconnect(conn); return NULL; } //Retires a connection for re-use static void httpdRetireConn(HttpdConnData *hconn) { if (!hconn || !hconn->occupied) { 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) { HttpdSendBacklogItem *backlog, *next; backlog = hconn->priv.sendBacklog; do { next = backlog->next; httpdFree(backlog); backlog = next; } while (backlog != NULL); } // Free post data buffer if (hconn->post.buff) { httpdFree(hconn->post.buff); hconn->post.buff = NULL; } // Free left-over queued headers - can happen if the client disconnects before headers were sent HttpdQueuedHeader *hdr = hconn->priv.headersToSend; hconn->priv.headersToSend = NULL; while (hdr) { HttpdQueuedHeader *next = hdr->next; httpdFree(hdr); hdr = next; } s_connData[hconn->slot].occupied = false; } //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) { const char *p = httpdGetHeaderPtr(conn, header); if (!p) { return 0; } while (*p != 0 && buffLen > 1) { // && *p != '\r' && *p != '\n' *buff++ = *p++; buffLen--; } //Zero-terminate string *buff = 0; //All done :) return 1; } const char * httpdGetHeaderPtr(HttpdConnData *conn, const char *header) { 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++; } return p; } p += strlen(p) + 1; //Skip past end of string and \0 terminator } return NULL; } void httpdQueueHeader(HttpdConnData *conn, const char *header, const char *value) { if (!conn || !header || !value) { return; } if (conn->priv.flags & HFL_SENDINGBODY) { http_error("Headers already sent."); return; } HttpdQueuedHeader *queEntry = httpdMalloc(sizeof(void *) + strlen(header) + strlen(value) + 5); if (!queEntry) { http_error("httpdQueueHeader - no mem"); return; } queEntry->next = NULL; queEntry->headerLine[0] = 0; strcat(queEntry->headerLine, header); strcat(queEntry->headerLine, ": "); strcat(queEntry->headerLine, value); strcat(queEntry->headerLine, "\r\n"); if (!httpdQueueHeaderRaw(conn, queEntry)) { httpdFree(queEntry); } } bool httpdQueueHeaderRaw(HttpdConnData *conn, HttpdQueuedHeader *queEntry) { if (!conn || !queEntry) { return false; } if (conn->priv.flags & HFL_SENDINGBODY) { http_error("Headers already sent."); return false; } queEntry->next = NULL; if (!conn->priv.headersToSend) { conn->priv.headersToSend = queEntry; } else { HttpdQueuedHeader *ph = conn->priv.headersToSend; // Go to the end of the linked list while (ph->next) { ph = ph->next; } ph->next = queEntry; } return true; } bool httpdSetCookie(HttpdConnData *conn, const SetCookie *parm) { if (!conn || !parm) { return false; } if (!parm->name || !parm->value) { return false; } if (conn->priv.flags & HFL_SENDINGBODY) { http_error("Headers already sent."); return false; } #define COOKIE_PART_SET_COOKIE "Set-Cookie: " #define COOKIE_PART_EXPIRES "; Expires=" #define COOKIE_PART_MAX_AGE "; Max-Age=" #define COOKIE_PART_DOMAIN "; Domain=" #define COOKIE_PART_PATH "; Path=" #define COOKIE_PART_SAME_SITE "; SameSite=" #define COOKIE_PART_SECURE "; Secure" #define COOKIE_PART_HTTP_ONLY "; HttpOnly" // "Set-Cookie: " size_t buflen = strlen(COOKIE_PART_SET_COOKIE) + strlen(parm->name) + 1 + strlen(parm->value) + 5; // cr lf + nul + some spare space if (parm->expires) { buflen += strlen(COOKIE_PART_EXPIRES) + strlen(parm->expires); } if (parm->maxAge != 0) { // 10 chars are needed for 32bit integer buflen += strlen(COOKIE_PART_MAX_AGE) + 10; } if (parm->domain) { buflen += strlen(COOKIE_PART_DOMAIN) + strlen(parm->domain); } if (parm->path) { buflen += strlen(COOKIE_PART_PATH) + strlen(parm->path); } if (parm->sameSite) { // Strict, Lax, None buflen += strlen(COOKIE_PART_SAME_SITE) + strlen(parm->sameSite); } if (parm->secure) { buflen += strlen(COOKIE_PART_SECURE); } if (parm->httponly) { buflen += strlen(COOKIE_PART_HTTP_ONLY); } HttpdQueuedHeader *queEntry = httpdMalloc(sizeof(void*) + buflen); if (!queEntry) { http_error("httpdSetCookie - no mem"); return false; } queEntry->next = NULL; queEntry->headerLine[0] = 0; strcat(queEntry->headerLine, COOKIE_PART_SET_COOKIE); strcat(queEntry->headerLine, parm->name); strcat(queEntry->headerLine, "="); strcat(queEntry->headerLine, parm->value); if (parm->expires) { strcat(queEntry->headerLine, COOKIE_PART_EXPIRES); strcat(queEntry->headerLine, parm->expires); } if (parm->maxAge != 0) { strcat(queEntry->headerLine, COOKIE_PART_MAX_AGE); sprintf(queEntry->headerLine + strlen(queEntry->headerLine), "%d", parm->maxAge); } if (parm->domain) { strcat(queEntry->headerLine, COOKIE_PART_DOMAIN); strcat(queEntry->headerLine, parm->domain); } if (parm->path) { strcat(queEntry->headerLine, COOKIE_PART_PATH); strcat(queEntry->headerLine, parm->path); } if (parm->sameSite) { strcat(queEntry->headerLine, COOKIE_PART_SAME_SITE); strcat(queEntry->headerLine, parm->sameSite); } if (parm->secure) { strcat(queEntry->headerLine, COOKIE_PART_SECURE); } if (parm->httponly) { strcat(queEntry->headerLine, COOKIE_PART_HTTP_ONLY); } strcat(queEntry->headerLine, "\r\n"); if (!httpdQueueHeaderRaw(conn, queEntry)) { // should not be possible httpdFree(queEntry); } return true; } bool httpdGetCookie(HttpdConnData *conn, const char *name, char *buff, size_t buffLen) { const char *p = httpdGetHeaderPtr(conn, "Cookie"); if (!p) return 0; size_t namelen = strlen(name); while (*p != 0 && *p != '\r' && *p != '\n') { if (*p == ' ') { // skip whitespace p++; continue; } const char *next_semi = strchr(p, ';'); if (!next_semi) { next_semi = p + strlen(p); // end of string } if (strstarts(p, name) && p[namelen] == '=') { const char * valuestart = p + namelen + 1; size_t valuelen = (size_t) (next_semi - valuestart); if (valuelen >= buffLen - 1) { valuelen = buffLen - 1; } strncpy(buff, valuestart, valuelen); return true; } p = next_semi + 1; } return false; } 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) { // Add queued headers & dealloc the struct HttpdQueuedHeader *qh = conn->priv.headersToSend; conn->priv.headersToSend = NULL; while (qh) { httpdSendStr(conn, qh->headerLine); HttpdQueuedHeader *next = qh->next; httpdFree(qh); qh = next; } 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, """); } else if (c == '\'') { httpdSendStr_orDie(conn, "'"); } else if (c == '<') { httpdSendStr_orDie(conn, "<"); } else if (c == '>') { httpdSendStr_orDie(conn, ">"); } } 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; } HttpdSendBacklogItem *backlogItem = httpdMalloc(sizeof(HttpdSendBacklogItem) + conn->priv.sendBuffLen); if (backlogItem == NULL) { http_error("Httpd: Backlog: malloc failed, out of memory!"); return false; } memcpy(backlogItem->data, conn->priv.sendBuff, conn->priv.sendBuffLen); backlogItem->len = conn->priv.sendBuffLen; backlogItem->next = NULL; if (conn->priv.sendBacklog == NULL) { conn->priv.sendBacklog = backlogItem; } else { HttpdSendBacklogItem *e = conn->priv.sendBacklog; while (e->next != NULL) { e = e->next; } e->next = backlogItem; } conn->priv.sendBacklogSize += conn->priv.sendBuffLen; } conn->priv.sendBuffLen = 0; } return true; } static 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) { httpdFree(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, httpd_ipaddr_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) { httpdPlatLock(); if (conn == NULL) { return; } if (conn->priv.sendBacklog != NULL) { //We have some backlog to send first. HttpdSendBacklogItem *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; httpdFree(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; } conn->priv.sendBuffLen = 0; httpd_cgi_state 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); 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 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. httpd_cgi_state 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 *) httpdMalloc(conn->post.buffSize + 1); if (conn->post.buff == NULL) { http_error("post buf alloc 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(); 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); } httpdPlatUnlock(); } //Callback called when there's data available on a socket. void httpdRecvCb(ConnTypePtr rconn, httpd_ipaddr_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; } 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); } 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, httpd_ipaddr_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, httpd_ipaddr_t remIp, uint16_t remPort) { uint8_t ci; // connection index httpdPlatLock(); //Find empty conndata in pool for (ci = 0; ci < HTTPD_MAX_CONNECTIONS; ci++) { if (!s_connData[ci].occupied) { break; } } http_info("Conn req from %d.%d.%d.%d:%d, using pool slot %d", IP_SPLIT(remIp), remPort, ci); if (ci >= HTTPD_MAX_CONNECTIONS) { http_error("Aiee, conn pool overflow!"); httpdPlatUnlock(); return 0; } memset(&s_connData[ci], 0, sizeof(HttpdConnData)); s_connData[ci].slot = ci; s_connData[ci].occupied = true; s_connData[ci].conn = conn; s_connData[ci].remote_ip = remIp; s_connData[ci].remote_port = remPort; s_connData[ci].post.len = -1; httpdPlatUnlock(); return 1; } //Httpd initialization routine. Call this to kick off webserver functionality. httpd_thread_handle_t *httpdStart(const HttpdBuiltInUrl *fixedUrls, struct httpd_init_options *options) { memset(s_connData, 0, sizeof(s_connData)); 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; }