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.
430 lines
14 KiB
430 lines
14 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 "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;
|
|
}
|
|
}
|
|
|
|
|