/* Connector to let httpd use the espfs filesystem to serve the files in it. */ /* * ---------------------------------------------------------------------------- * "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 "cgi-espfs.h" #include "httpd-logging.h" #include "httpd-utils.h" #include "espfs.h" #include "espfsformat.h" #define FILE_CHUNK_LEN 512 // 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.) static const char *gzipNonSupportedMessage = "HTTP/1.0 501 Not implemented\r\n" "Connection: close\r\n" "Content-Type: text/plain\r\n" "Content-Length: 52\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 * @return file pointer or NULL */ EspFsFile *tryOpenIndex(const char *path) { EspFsFile *file; // A dot in the filename probably means extension // 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; } file = tryOpenIndex_do(path, "index.htm"); if (file != NULL) { return file; } file = tryOpenIndex_do(path, "index.tpl.html"); if (file != NULL) { return file; } file = tryOpenIndex_do(path, "index.tpl"); if (file != NULL) { return file; } return NULL; // failed to guess the right name } static httpd_cgi_state serveStaticFile(HttpdConnData *hconn, const char *filepath) { EspFsFile *file = hconn->cgiData; size_t len; uint8_t buff[FILE_CHUNK_LEN + 1]; char acceptEncodingBuffer[64 + 1]; if (hconn->conn == NULL) { //Connection aborted. Clean up. espFsClose(file); return HTTPD_CGI_DONE; } // invalid call. if (filepath == NULL) { espfs_error("serveStaticFile called with NULL path!"); return HTTPD_CGI_NOTFOUND; } //First call to this cgi. if (file == NULL) { //First call to this cgi. Open the file so we can read it. file = espFsOpen(filepath); if (file == NULL) { // file not found // If this is a folder, look for index file file = tryOpenIndex(filepath); if (file == NULL) { return HTTPD_CGI_NOTFOUND; } } // The gzip checking code is intentionally without #ifdefs because checking // for FLAG_GZIP (which indicates gzip compressed file) is very easy, doesn't // mean additional overhead and is actually safer to be on at all times. // If there are no gzipped files in the image, the code bellow will not cause any harm. // Check if requested file was GZIP compressed bool isGzip = (0 != (espFsFlags(file) & EFS_FLAG_GZIP)); 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); if (strstr(acceptEncodingBuffer, "gzip") == NULL) { //No Accept-Encoding: gzip header present httpdSendStr(hconn, gzipNonSupportedMessage); espFsClose(file); return HTTPD_CGI_DONE; } } hconn->cgiData = file; httpdStartResponse(hconn, 200); const char *mime = httpdGetMimetypeOr(filepath, "application/octet-stream"); httpdHeader(hconn, "Content-Type", mime); if (isGzip) { httpdHeader(hconn, "Content-Encoding", "gzip"); } httpdAddCacheHeaders(hconn, mime); httpdEndHeaders(hconn); return HTTPD_CGI_MORE; } len = espFsRead(file, buff, FILE_CHUNK_LEN); if (len > 0) { espfs_dbg("[EspFS] Read file chunk: %d bytes", len); httpdSend(hconn, buff, len); } if (len != FILE_CHUNK_LEN) { //We're done. espFsClose(file); return HTTPD_CGI_DONE; } else { //Ok, till next time. return HTTPD_CGI_MORE; } } //This is a catch-all cgi function. It takes the url passed to it, looks up the corresponding //path in the filesystem and if it exists, passes the file through. This simulates what a normal //webserver would do with static files. httpd_cgi_state cgiEspFsStaticFile(HttpdConnData *connData) { const char *filepath = (connData->cgiArg == NULL) ? connData->url : (char *) connData->cgiArg; return serveStaticFile(connData, filepath); } //cgiEspFsTemplate can be used as a template. typedef enum { ENCODE_PLAIN = 0, ENCODE_HTML, ENCODE_JS, } TplEncode; typedef struct { EspFsFile *file; ssize_t tokenPos; char buff[FILE_CHUNK_LEN + 1]; char token[HTTPD_ESPFS_TOKEN_LEN]; char *pToken; bool chunk_resume; size_t buff_len; size_t buff_x; size_t buff_sp; char *buff_e; TplEncode tokEncode; } TplDataInternal; int tplSendN(HttpdConnData *conn, const char *str, size_t len) { if (conn == NULL) { return 0; } TplDataInternal *tdi = conn->cgiData; if (!tdi) { return 0; } if (tdi->tokEncode == ENCODE_PLAIN) { return httpdSendStrN(conn, str, len); } else if (tdi->tokEncode == ENCODE_HTML) { return httpdSend_html(conn, str, (ssize_t) len); } else if (tdi->tokEncode == ENCODE_JS) { return httpdSend_js(conn, str, (ssize_t) len); } return 0; } httpd_cgi_state cgiEspFsTemplate(HttpdConnData *conn) { TplDataInternal *tdi = conn->cgiData; size_t len, x, sp; char *e = NULL; if (conn->conn == NULL) { //Connection aborted. Clean up. if (tdi) { TplCallback callback = (TplCallback) conn->cgiArg; if (callback) { callback(conn, NULL); } espFsClose(tdi->file); httpdPlatFree(tdi); } return HTTPD_CGI_DONE; } if (tdi == NULL) { //First call to this cgi. Open the file so we can read it. tdi = (TplDataInternal *) httpdPlatMalloc(sizeof(TplDataInternal)); if (tdi == NULL) { espfs_error("Failed to malloc tpl struct"); return HTTPD_CGI_NOTFOUND; } tdi->chunk_resume = false; const char *filepath = conn->url; // check for custom template URL if (conn->cgiArg2 != NULL) { filepath = conn->cgiArg2; espfs_dbg("Using filepath %s", filepath); } tdi->file = espFsOpen(filepath); if (tdi->file == NULL) { // maybe a folder, look for index file tdi->file = tryOpenIndex(filepath); if (tdi->file == NULL) { espfs_error("cgiEspFsTemplate: Couldn't find template for path: %s", conn->url); httpdPlatFree(tdi); return HTTPD_CGI_NOTFOUND; } } if (espFsFlags(tdi->file) & EFS_FLAG_GZIP) { espfs_error("cgiEspFsTemplate: Trying to use gzip-compressed file %s as template!", conn->url); espFsClose(tdi->file); httpdPlatFree(tdi); return HTTPD_CGI_NOTFOUND; } tdi->tokenPos = -1; conn->cgiData = tdi; httpdStartResponse(conn, 200); // Try to extension from the URL const char *mime = httpdGetMimetype(conn->url); if (!mime) { // Get extension from the template file mime = httpdGetMimetype(filepath); } if (!mime) { // Still unresolved... mime = "text/html"; // this is generally a good fallback for templates } httpdHeader(conn, "Content-Type", mime); httpdAddCacheHeaders(conn, mime); httpdEndHeaders(conn); return HTTPD_CGI_MORE; } char *buff = tdi->buff; // resume the parser state from the last token, // if subst. func wants more data to be sent. if (tdi->chunk_resume) { //espfs_dbg("Resuming tpl parser for multi-part subst"); len = tdi->buff_len; e = tdi->buff_e; sp = tdi->buff_sp; x = tdi->buff_x; } else { len = espFsRead(tdi->file, (uint8_t *) buff, FILE_CHUNK_LEN); tdi->buff_len = len; e = buff; sp = 0; x = 0; } if (len > 0) { for (; x < len; x++) { char c = buff[x]; if (tdi->tokenPos == -1) { //Inside ordinary text. if (c == '%') { //Send raw data up to now if (sp != 0) { httpdSendStrN(conn, e, sp); } sp = 0; //Go collect token chars. tdi->tokenPos = 0; } else { sp++; } } else { if (c == '%') { if (tdi->tokenPos == 0) { //This is the second % of a %% escape string. //Send a single % and resume with the normal program flow. httpdSendStrN(conn, "%", 1); } else { if (!tdi->chunk_resume) { //This is an actual token. tdi->token[tdi->tokenPos++] = 0; //zero-terminate token int prefixLen = 0; tdi->tokEncode = ENCODE_PLAIN; if (strneq(tdi->token, "html:", 5)) { prefixLen = 5; tdi->tokEncode = ENCODE_HTML; } else if (strneq(tdi->token, "h:", 2)) { prefixLen = 2; tdi->tokEncode = ENCODE_HTML; } else if (strneq(tdi->token, "js:", 3)) { prefixLen = 3; tdi->tokEncode = ENCODE_JS; } else if (strneq(tdi->token, "j:", 2)) { prefixLen = 2; tdi->tokEncode = ENCODE_JS; } tdi->pToken = &tdi->token[prefixLen]; } tdi->chunk_resume = false; TplCallback callback = (TplCallback) conn->cgiArg; httpd_cgi_state status = callback(conn, tdi->pToken); if (status == HTTPD_CGI_MORE) { // espfs_dbg("Multi-part tpl subst, saving parser state"); // wants to send more in this token's place..... tdi->chunk_resume = true; tdi->buff_len = len; tdi->buff_e = e; tdi->buff_sp = sp; tdi->buff_x = x; break; } else if (status == HTTPD_CGI_NOTFOUND) { tdi->tokenPos--; // undo the ++ above goto put_token_back; } } //Go collect normal chars again. e = &buff[x + 1]; tdi->tokenPos = -1; } else { // Add char to the token buf bool outOfSpace = tdi->tokenPos >= ((int) sizeof(tdi->token) - 1); if (outOfSpace) { put_token_back: // looks like we collected some garbage, put it back httpdSendStrN(conn, "%", 1); if (tdi->tokenPos > 0) { httpdSendStrN(conn, tdi->token, (size_t) tdi->tokenPos); } // the bad char httpdSendStrN(conn, &c, 1); //Go collect normal chars again. e = &buff[x + 1]; tdi->tokenPos = -1; } else { // collect it tdi->token[tdi->tokenPos++] = c; } } } } } if (tdi->chunk_resume) { return HTTPD_CGI_MORE; } //Send remaining bit. if (sp != 0) { httpdSendStrN(conn, e, (size_t) sp); } if (len != FILE_CHUNK_LEN) { //We're done. TplCallback callback = (TplCallback) conn->cgiArg; if (callback) { callback(conn, NULL); } espfs_info("Template sent."); espFsClose(tdi->file); httpdPlatFree(tdi); return HTTPD_CGI_DONE; } else { //Ok, till next time. return HTTPD_CGI_MORE; } }