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/httpdespfs.c

409 lines
13 KiB

/*
Connector to let httpd use the espfs filesystem to serve the files in it.
*/
/*
* ----------------------------------------------------------------------------
* "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 <stddef.h>
#include <string.h>
#include "httpd.h"
#include "httpd-platform.h"
#include "httpdespfs.h"
#include "espfs.h"
#include "espfsformat.h"
#include "logging.h"
#define FILE_CHUNK_LEN 1024
// 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
}
httpd_cgi_state
serveStaticFile(HttpdConnData *connData, const char *filepath)
{
EspFsFile *file = connData->cgiData;
int len;
uint8_t buff[FILE_CHUNK_LEN + 1];
char acceptEncodingBuffer[64 + 1];
int isGzip;
if (connData->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
isGzip = espFsFlags(file) & 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(connData, "Accept-Encoding", acceptEncodingBuffer, 64);
if (strstr(acceptEncodingBuffer, "gzip") == NULL) {
//No Accept-Encoding: gzip header present
httpdSendStr(connData, gzipNonSupportedMessage);
espFsClose(file);
return HTTPD_CGI_DONE;
}
}
connData->cgiData = file;
httpdStartResponse(connData, 200);
const char *mime = httpdGetMimetype(filepath);
httpdHeader(connData, "Content-Type", mime);
if (isGzip) {
httpdHeader(connData, "Content-Encoding", "gzip");
}
httpdAddCacheHeaders(connData, mime);
httpdEndHeaders(connData);
return HTTPD_CGI_MORE;
}
len = espFsRead(file, buff, FILE_CHUNK_LEN);
if (len > 0) {
espfs_dbg("[EspFS] Read file chunk: %d bytes", len);
httpdSend(connData, 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 cgiEspFsHook(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;
void *tplArg;
char token[64];
int tokenPos;
char buff[FILE_CHUNK_LEN + 1];
bool chunk_resume;
int buff_len;
int buff_x;
int buff_sp;
char *buff_e;
TplEncode tokEncode;
} TplData;
int
tplSend(HttpdConnData *conn, const char *str, int len)
{
if (conn == NULL) { return 0; }
TplData *tpd = conn->cgiData;
if (tpd == NULL || tpd->tokEncode == ENCODE_PLAIN) {
return httpdSendStrN(conn, str, len);
} else if (tpd->tokEncode == ENCODE_HTML) {
return httpdSend_html(conn, (const uint8_t *) str, len);
} else if (tpd->tokEncode == ENCODE_JS) {
return httpdSend_js(conn, (const uint8_t *) str, len);
}
return 0;
}
httpd_cgi_state cgiEspFsTemplate(HttpdConnData *connData)
{
TplData *tpd = connData->cgiData;
int len;
int x, sp = 0;
char *e = NULL;
int tokOfs;
if (connData->conn == NULL) {
//Connection aborted. Clean up.
((TplCallback) (connData->cgiArg))(connData, NULL, &tpd->tplArg);
espFsClose(tpd->file);
httpdPlatFree(tpd);
return HTTPD_CGI_DONE;
}
if (tpd == NULL) {
//First call to this cgi. Open the file so we can read it.
tpd = (TplData *) httpdPlatMalloc(sizeof(TplData));
if (tpd == NULL) {
espfs_error("Failed to malloc tpl struct");
return HTTPD_CGI_NOTFOUND;
}
tpd->chunk_resume = false;
const char *filepath = connData->url;
// check for custom template URL
if (connData->cgiArg2 != NULL) {
filepath = connData->cgiArg2;
espfs_dbg("Using filepath %s", filepath);
}
tpd->file = espFsOpen(filepath);
if (tpd->file == NULL) {
// maybe a folder, look for index file
tpd->file = tryOpenIndex(filepath);
if (tpd->file == NULL) {
httpdPlatFree(tpd);
return HTTPD_CGI_NOTFOUND;
}
}
tpd->tplArg = NULL;
tpd->tokenPos = -1;
if (espFsFlags(tpd->file) & FLAG_GZIP) {
espfs_error("cgiEspFsTemplate: Trying to use gzip-compressed file %s as template!", connData->url);
espFsClose(tpd->file);
httpdPlatFree(tpd);
return HTTPD_CGI_NOTFOUND;
}
connData->cgiData = tpd;
httpdStartResponse(connData, 200);
const char *mime = httpdGetMimetype(connData->url);
httpdHeader(connData, "Content-Type", mime);
httpdAddCacheHeaders(connData, mime);
httpdEndHeaders(connData);
return HTTPD_CGI_MORE;
}
char *buff = tpd->buff;
// resume the parser state from the last token,
// if subst. func wants more data to be sent.
if (tpd->chunk_resume) {
//espfs_dbg("Resuming tpl parser for multi-part subst");
len = tpd->buff_len;
e = tpd->buff_e;
sp = tpd->buff_sp;
x = tpd->buff_x;
} else {
len = espFsRead(tpd->file, (uint8_t *) buff, FILE_CHUNK_LEN);
tpd->buff_len = len;
e = buff;
sp = 0;
x = 0;
}
if (len > 0) {
for (; x < len; x++) {
if (tpd->tokenPos == -1) {
//Inside ordinary text.
if (buff[x] == '%') {
//Send raw data up to now
if (sp != 0) { httpdSendStrN(connData, e, sp); }
sp = 0;
//Go collect token chars.
tpd->tokenPos = 0;
} else {
sp++;
}
} else {
if (buff[x] == '%') {
if (tpd->tokenPos == 0) {
//This is the second % of a %% escape string.
//Send a single % and resume with the normal program flow.
httpdSendStrN(connData, "%", 1);
} else {
if (!tpd->chunk_resume) {
//This is an actual token.
tpd->token[tpd->tokenPos++] = 0; //zero-terminate token
tokOfs = 0;
tpd->tokEncode = ENCODE_PLAIN;
if (strneq(tpd->token, "html:", 5)) {
tokOfs = 5;
tpd->tokEncode = ENCODE_HTML;
} else if (strneq(tpd->token, "h:", 2)) {
tokOfs = 2;
tpd->tokEncode = ENCODE_HTML;
} else if (strneq(tpd->token, "js:", 3)) {
tokOfs = 3;
tpd->tokEncode = ENCODE_JS;
} else if (strneq(tpd->token, "j:", 2)) {
tokOfs = 2;
tpd->tokEncode = ENCODE_JS;
}
// do the shifting
if (tokOfs > 0) {
for (int i = tokOfs; i <= tpd->tokenPos; i++) {
tpd->token[i - tokOfs] = tpd->token[i];
}
}
}
tpd->chunk_resume = false;
httpd_cgi_state status = ((TplCallback) (connData->cgiArg))(connData, tpd->token, &tpd->tplArg);
if (status == HTTPD_CGI_MORE) {
// espfs_dbg("Multi-part tpl subst, saving parser state");
// wants to send more in this token's place.....
tpd->chunk_resume = true;
tpd->buff_len = len;
tpd->buff_e = e;
tpd->buff_sp = sp;
tpd->buff_x = x;
break;
}
}
//Go collect normal chars again.
e = &buff[x + 1];
tpd->tokenPos = -1;
} else {
// Add char to the token buf
char c = buff[x];
bool outOfSpace = tpd->tokenPos >= (sizeof(tpd->token) - 1);
if (outOfSpace ||
(!(c >= 'a' && c <= 'z') &&
!(c >= 'A' && c <= 'Z') &&
!(c >= '0' && c <= '9') &&
c != '.' && c != '_' && c != '-' && c != ':'
)) {
// looks like we collected some garbage
httpdSendStrN(connData, "%", 1);
if (tpd->tokenPos > 0) {
httpdSendStrN(connData, tpd->token, tpd->tokenPos);
}
// the bad char
httpdSendStrN(connData, &c, 1);
//Go collect normal chars again.
e = &buff[x + 1];
tpd->tokenPos = -1;
} else {
// collect it
tpd->token[tpd->tokenPos++] = c;
}
}
}
}
}
if (tpd->chunk_resume) {
return HTTPD_CGI_MORE;
}
//Send remaining bit.
if (sp != 0) { httpdSendStrN(connData, e, sp); }
if (len != FILE_CHUNK_LEN) {
//We're done.
((TplCallback) (connData->cgiArg))(connData, NULL, &tpd->tplArg);
espfs_info("Template sent.");
espFsClose(tpd->file);
httpdPlatFree(tpd);
return HTTPD_CGI_DONE;
} else {
//Ok, till next time.
return HTTPD_CGI_MORE;
}
}