From 935271510d78bd5f227c421c13832c64218fc300 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Sun, 29 Jan 2023 16:23:42 +0100 Subject: [PATCH] cleaning, add header queuing --- demo/server_demo.c | 16 ++--- spritehttpd/include/httpd-types.h | 2 +- spritehttpd/include/httpd-utils.h | 30 ++++++-- spritehttpd/include/httpd.h | 10 +++ spritehttpd/src/cgi-espfs.c | 83 +++++++++++----------- spritehttpd/src/httpd-utils.c | 31 +++++++-- spritehttpd/src/httpd.c | 110 ++++++++++++++++++++++-------- 7 files changed, 191 insertions(+), 91 deletions(-) diff --git a/demo/server_demo.c b/demo/server_demo.c index 2a69b7e..236e1c0 100644 --- a/demo/server_demo.c +++ b/demo/server_demo.c @@ -80,13 +80,13 @@ httpd_cgi_state templateReplacer(HttpdConnData *conn, const char *token) // httpdGetHeader(conn, "Cookie", ) //} -// -//httpd_cgi_state cgiStartSession(HttpdConnData *conn) -//{ -// -// -// return HTTPD_CGI_NOTFOUND; -//} +httpd_cgi_state cgiStartSession(HttpdConnData *conn) +{ + httpdQueueHeader(conn, "X-Foo", "FOO"); + httpdQueueHeader(conn, "X-Bar", "Bar"); + + return HTTPD_CGI_NOTFOUND; +} @@ -96,7 +96,7 @@ httpd_cgi_state templateReplacer(HttpdConnData *conn, const char *token) */ const HttpdBuiltInUrl routes[] = { // TODO password lock ... -// ROUTE_CGI("*", cgiStartSession), + ROUTE_CGI("*", cgiStartSession), // --- Web pages --- // ROUTE_TPL_FILE("/", tplIndex, "/index.tpl"), diff --git a/spritehttpd/include/httpd-types.h b/spritehttpd/include/httpd-types.h index 1a411db..f3366f2 100644 --- a/spritehttpd/include/httpd-types.h +++ b/spritehttpd/include/httpd-types.h @@ -30,7 +30,7 @@ typedef enum { HTTPD_CGI_NOTFOUND = 2, /// This is, in effect, identical to NOTFOUND, it's returned by auth functions when a fall-through is allowed. /// The next route in the route list will be attempted. - HTTPD_CGI_AUTHENTICATED = 3, + HTTPD_CGI_AUTHENTICATED = 3, // TODO rename to PASS? } httpd_cgi_state; /** diff --git a/spritehttpd/include/httpd-utils.h b/spritehttpd/include/httpd-utils.h index 35129fd..c6e89ca 100644 --- a/spritehttpd/include/httpd-utils.h +++ b/spritehttpd/include/httpd-utils.h @@ -9,18 +9,40 @@ #include "httpd-types.h" // Custom helpers + +/// Test string equality #define streq(a, b) (strcmp((const char*)(a), (const char*)(b)) == 0) +/// Test string equality, case-insensitive +#define strcaseeq(a, b) (strcasecmp((const char*)(a), (const char*)(b)) == 0) +/// Test string equality up to N chars #define strneq(a, b, n) (strncmp((const char*)(a), (const char*)(b), (n)) == 0) +/// Test if string A starts with string B #define strstarts(a, b) strneq((a), (b), strlen((b))) +/// Get nth char from the end of string (1 = last) #define last_char_n(str, n) ((str))[strlen((str)) - (n)] +/// Get last char of string #define last_char(str) last_char_n((str), 1) +/// The container_of macro from the linux kernel #ifndef container_of #define container_of(ptr, type, member) ({ \ const typeof( ((type *)0)->member ) *__mptr = (ptr); \ (type *)( (char *)__mptr - offsetof(type,member) );}) #endif +/// Get length of a static or stack array +#define array_len(a) (sizeof((a)) / sizeof((a)[0])) + +/** + * Strstr up to N chars into the searched string + * + * @param str - haystack + * @param substr - needle + * @param n - haystack len to search + * @return pointer to the matching substring or NULL + */ +const char *strnstr(const char *str, const char *substr, size_t n); + /** * Turn a nibble (0-15) to a hex char. * @@ -83,12 +105,10 @@ const char *httpdGetMimetype(const char *url); * @param fallback - fallback mime if none was resolved * @return mime string */ -static inline const char *httpdGetMimetypeOr(const char *url, const char *fallback) { +static inline const char *httpdGetMimetypeOr(const char *url, const char *fallback) +{ const char *mime = httpdGetMimetype(url); - if (!mime) { - mime = fallback; - } - return mime; + return mime ? mime : fallback; } /** diff --git a/spritehttpd/include/httpd.h b/spritehttpd/include/httpd.h index 60a397f..d6b53cf 100644 --- a/spritehttpd/include/httpd.h +++ b/spritehttpd/include/httpd.h @@ -149,6 +149,16 @@ void httpdEndHeaders(HttpdConnData *conn); */ int httpdGetHeader(HttpdConnData *conn, const char *header, char *buff, size_t buffLen); +/** + * Queue a header to be sent with the response. + * + * @param conn + * @param header - name + * @param value - value + * @return 1 = OK + */ +void httpdQueueHeader(HttpdConnData *conn, const char *header, const char *value); + /** * Send binary data * diff --git a/spritehttpd/src/cgi-espfs.c b/spritehttpd/src/cgi-espfs.c index b6990a7..6686ebf 100644 --- a/spritehttpd/src/cgi-espfs.c +++ b/spritehttpd/src/cgi-espfs.c @@ -21,7 +21,12 @@ Connector to let httpd use the espfs filesystem to serve the files in it. #include "espfs.h" #include "espfsformat.h" -#define FILE_CHUNK_LEN 512 +/// EspFs CGI filename len buffer size +#define ESPFS_FILENAME_LEN 100 +/// EspFs CGI file chunk buffer size +#define ESPFS_FILE_CHUNK_LEN 512 +/// EspFs CGI header buffer (used to detect gzip) +#define ESPFS_HEADER_LEN 64 // The static files marked with FLAG_GZIP are compressed and will be served with GZIP compression. // If the client does not advertise that he accepts GZIP send following warning message (telnet users for e.g.) @@ -32,29 +37,6 @@ static const char *gzipNonSupportedMessage = "HTTP/1.0 501 Not implemented\r\n" "\r\n" "Your browser does not accept gzip-compressed data.\r\n"; -/** - * Try to open a file - * @param path - path to the file, may end with slash - * @param indexname - filename at the path - * @return file pointer or NULL - */ -static EspFsFile *tryOpenIndex_do(const char *path, const char *indexname) -{ - char fname[100]; - size_t url_len = strlen(path); - strncpy(fname, path, 99); - - // Append slash if missing - if (path[url_len - 1] != '/') { - fname[url_len++] = '/'; - } - - strcpy(fname + url_len, indexname); - - // Try to open, returns NULL if failed - return espFsOpen(fname); -} - /** * Try to find index file on a path * @param path - directory @@ -67,17 +49,29 @@ EspFsFile *tryOpenIndex(const char *path) // no point in trying to look for index. if (strchr(path, '.') != NULL) { return NULL; } - file = tryOpenIndex_do(path, "index.html"); - if (file != NULL) { return file; } + char fname[ESPFS_FILENAME_LEN]; + size_t url_len = strlen(path); + if (url_len >= ESPFS_FILENAME_LEN) { + // too long to append anything + return NULL; + } + strncpy(fname, path, ESPFS_FILENAME_LEN - 1); - file = tryOpenIndex_do(path, "index.htm"); - if (file != NULL) { return file; } + // Append slash if missing + if (path[url_len - 1] != '/') { + fname[url_len++] = '/'; + } - file = tryOpenIndex_do(path, "index.tpl.html"); - if (file != NULL) { return file; } + const char *index_names[4] = { + "index.html", "index.htm", "index.tpl.html", "index.tpl" + }; - file = tryOpenIndex_do(path, "index.tpl"); - if (file != NULL) { return file; } + for (size_t i = 0; i < array_len(index_names); i++) { + strcpy(fname + url_len, index_names[i]); + // Try to open, returns NULL if failed + file = espFsOpen(fname); + if (file != NULL) { return file; } + } return NULL; // failed to guess the right name } @@ -86,8 +80,8 @@ static httpd_cgi_state serveStaticFile(HttpdConnData *hconn, const char *filepat { EspFsFile *file = hconn->cgiData; size_t len; - uint8_t buff[FILE_CHUNK_LEN + 1]; - char acceptEncodingBuffer[64 + 1]; + uint8_t buff[ESPFS_FILE_CHUNK_LEN + 1]; + char acceptEncodingBuffer[ESPFS_HEADER_LEN + 1]; if (hconn->conn == NULL) { //Connection aborted. Clean up. @@ -123,7 +117,7 @@ static httpd_cgi_state serveStaticFile(HttpdConnData *hconn, const char *filepat if (isGzip) { // Check the browser's "Accept-Encoding" header. If the client does not // advertise that he accepts GZIP send a warning message (telnet users for e.g.) - httpdGetHeader(hconn, "Accept-Encoding", acceptEncodingBuffer, 64); + httpdGetHeader(hconn, "Accept-Encoding", acceptEncodingBuffer, ESPFS_HEADER_LEN); if (strstr(acceptEncodingBuffer, "gzip") == NULL) { //No Accept-Encoding: gzip header present httpdSendStr(hconn, gzipNonSupportedMessage); @@ -144,12 +138,12 @@ static httpd_cgi_state serveStaticFile(HttpdConnData *hconn, const char *filepat return HTTPD_CGI_MORE; } - len = espFsRead(file, buff, FILE_CHUNK_LEN); + len = espFsRead(file, buff, ESPFS_FILE_CHUNK_LEN); if (len > 0) { espfs_dbg("[EspFS] Read file chunk: %d bytes", len); httpdSend(hconn, buff, len); } - if (len != FILE_CHUNK_LEN) { + if (len != ESPFS_FILE_CHUNK_LEN) { //We're done. espFsClose(file); return HTTPD_CGI_DONE; @@ -182,7 +176,7 @@ typedef struct { EspFsFile *file; ssize_t tokenPos; - char buff[FILE_CHUNK_LEN + 1]; + char buff[ESPFS_FILE_CHUNK_LEN + 1]; char token[HTTPD_ESPFS_TOKEN_LEN]; char *pToken; @@ -301,7 +295,7 @@ httpd_cgi_state cgiEspFsTemplate(HttpdConnData *conn) sp = tdi->buff_sp; x = tdi->buff_x; } else { - len = espFsRead(tdi->file, (uint8_t *) buff, FILE_CHUNK_LEN); + len = espFsRead(tdi->file, (uint8_t *) buff, ESPFS_FILE_CHUNK_LEN); tdi->buff_len = len; e = buff; @@ -337,19 +331,20 @@ httpd_cgi_state cgiEspFsTemplate(HttpdConnData *conn) int prefixLen = 0; tdi->tokEncode = ENCODE_PLAIN; - if (strneq(tdi->token, "html:", 5)) { + if (strstarts(tdi->token, "html:")) { prefixLen = 5; tdi->tokEncode = ENCODE_HTML; - } else if (strneq(tdi->token, "h:", 2)) { + } else if (strstarts(tdi->token, "h:")) { prefixLen = 2; tdi->tokEncode = ENCODE_HTML; - } else if (strneq(tdi->token, "js:", 3)) { + } else if (strstarts(tdi->token, "js:")) { prefixLen = 3; tdi->tokEncode = ENCODE_JS; - } else if (strneq(tdi->token, "j:", 2)) { + } else if (strstarts(tdi->token, "j:")) { prefixLen = 2; tdi->tokEncode = ENCODE_JS; } + // TODO implement "include" tokens? tdi->pToken = &tdi->token[prefixLen]; } @@ -410,7 +405,7 @@ httpd_cgi_state cgiEspFsTemplate(HttpdConnData *conn) httpdSendStrN(conn, e, (size_t) sp); } - if (len != FILE_CHUNK_LEN) { + if (len != ESPFS_FILE_CHUNK_LEN) { //We're done. TplCallback callback = (TplCallback) conn->cgiArg; diff --git a/spritehttpd/src/httpd-utils.c b/spritehttpd/src/httpd-utils.c index 3aa4749..376d2ec 100644 --- a/spritehttpd/src/httpd-utils.c +++ b/spritehttpd/src/httpd-utils.c @@ -1,6 +1,23 @@ #include "httpd-utils.h" #include "httpd-logging.h" +const char *strnstr(const char *str, const char *substr, size_t n) +{ + if (!str || !substr) { + return NULL; + } + size_t substr_len = strlen(substr); + if (0 == substr_len || substr_len > n) { return NULL; } + + const char *pEnd = str + n + 1 - substr_len; + for (const char *p = str; p < pEnd; p++) { + if (0 == strncmp(p, substr, substr_len)) { + return p; + } + } + return NULL; +} + char httpdHexNibble(uint8_t val) { val &= 0xf; @@ -25,7 +42,7 @@ size_t httpdUrlDecode(const char *val, size_t valLen, char *buff, size_t buffLen uint8_t escVal = 0; while (s < valLen && d < buffLen) { if (esced == 1) { - escVal = httpdHexVal(val[s]) << 4; + escVal = (uint8_t) (httpdHexVal(val[s]) << 4); esced = 2; } else if (esced == 2) { escVal |= httpdHexVal(val[s]); @@ -58,7 +75,7 @@ int httpdFindArg(const char *line, const char *arg, char *buff, size_t buffLen) e = strstr(p, "&"); if (e == NULL) { e = p + strlen(p); } router_dbg("findArg: val %s len %d", p, (int) (e - p)); - return (int) httpdUrlDecode(p, (size_t)(e - p), buff, buffLen); + return (int) httpdUrlDecode(p, (size_t) (e - p), buff, buffLen); } p = strstr(p, "&"); if (p != NULL) { p += 1; } @@ -95,21 +112,23 @@ static const MimeMap MIME_TYPES[] = { {"svg", "image/svg+xml"}, {"xml", "text/xml"}, {"json", "application/json"}, - {NULL, NULL}, //default value }; const char *httpdGetMimetype(const char *url) { - int i = 0; //Go find the extension const char *ext = url + (strlen(url) - 1); while (ext != url && *ext != '.') { ext--; } if (*ext == '.') { ext++; } - while (MIME_TYPES[i].ext != NULL && strcasecmp(ext, MIME_TYPES[i].ext) != 0) { i++; } + for (size_t i = 0; i < array_len(MIME_TYPES); i++) { + if (strcaseeq(ext, MIME_TYPES[i].ext)) { + return MIME_TYPES[i].mimetype; + } + } - return MIME_TYPES[i].mimetype; + return NULL; } diff --git a/spritehttpd/src/httpd.c b/spritehttpd/src/httpd.c index f8f199c..5a796a6 100644 --- a/spritehttpd/src/httpd.c +++ b/spritehttpd/src/httpd.c @@ -19,6 +19,7 @@ Esp8266 http server - core routines #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"); @@ -27,6 +28,14 @@ _Static_assert(HTTPD_MAX_CONNECTIONS < 256, "HTTPD_MAX_CONNECTIONS must be at mo static const HttpdBuiltInUrl *s_builtInUrls; static const char *s_serverName = HTTPD_SERVERNAME; +struct HttpdQueuedHeader; +typedef struct HttpdQueuedHeader { + /// Pointer to the next queued header + struct HttpdQueuedHeader *next; + /// Text of the header, including CRLF + char headerLine[]; +} HttpdQueuedHeader; + typedef struct HttpSendBacklogItem HttpSendBacklogItem; struct HttpSendBacklogItem { @@ -52,6 +61,7 @@ struct HttpdPriv { size_t sendBuffLen; char *chunkHdr; HttpSendBacklogItem *sendBacklog; + HttpdQueuedHeader *headersToSend; // Linked list of headers to send with the response. Will be freed with the request. size_t sendBacklogSize; uint8_t flags; }; @@ -60,7 +70,7 @@ struct HttpdPriv { //Connection pool HttpdConnData *s_connData[HTTPD_MAX_CONNECTIONS]; -void httpdInternalCloseAllSockets() +void httpdInternalCloseAllSockets(void) { httpdPlatLock(); /*release data connection*/ @@ -89,8 +99,7 @@ void httpdAddCacheHeaders(HttpdConnData *connData, const char *mime) if (streq(mime, "text/html") || streq(mime, "text/plain") || streq(mime, "text/csv") - || streq(mime, "application/json") - ) { + || streq(mime, "application/json")) { return; } @@ -191,6 +200,37 @@ int httpdGetHeader(HttpdConnData *conn, const char *header, char *buff, size_t b return 0; } +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 = httpdPlatMalloc(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"); + + // Attach it to the linked list + HttpdQueuedHeader **ph = &conn->priv->headersToSend; + while (*ph) { + ph = &(*ph)->next; + } + *ph = queEntry; +} + void httdSetTransferMode(HttpdConnData *conn, httpd_transfer_opt mode) { if (mode == HTTPD_TRANSFER_CLOSE) { @@ -224,12 +264,12 @@ void httpdStartResponse(HttpdConnData *conn, int code) } } - 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); + 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); @@ -252,6 +292,15 @@ void httpdHeader(HttpdConnData *conn, const char *field, const char *val) //Finish the headers. void httpdEndHeaders(HttpdConnData *conn) { + // Add queued headers + HttpdQueuedHeader *qh = conn->priv->headersToSend; + while (qh) { + httpdSendStr(conn, qh->headerLine); + HttpdQueuedHeader *next = qh->next; + httpdPlatFree(qh); + qh = next; + } + httpdSendStr(conn, "\r\n"); conn->priv->flags |= HFL_SENDINGBODY; } @@ -309,17 +358,21 @@ int httpdSend_html(HttpdConnData *conn, const char *data, ssize_t len) } if (c == '"' || c == '\'' || c == '<' || c == '>') { - if (start < end) httpdSend_orDie(conn, data + start, end - start); + 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 (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); + if (start < end) { + httpdSend_orDie(conn, data + start, end - start); + } return 1; } @@ -340,20 +393,24 @@ int httpdSend_js(HttpdConnData *conn, const char *data, ssize_t len) } if (c == '"' || c == '\\' || c == '\'' || c == '<' || c == '>' || c == '\n' || c == '\r') { - if (start < end) httpdSend_orDie(conn, data + start, end - start); + 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 (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); + if (start < end) { + httpdSend_orDie(conn, data + start, end - start); + } return 1; } @@ -371,7 +428,7 @@ bool httpdFlushSendBuffer(HttpdConnData *conn) //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; + 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)); @@ -507,7 +564,6 @@ void httpdContinue(HttpdConnData *conn) //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"); @@ -564,7 +620,7 @@ static void httpdProcessRequest(HttpdConnData *conn) //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); + 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) {