diff --git a/.gitignore b/.gitignore index e69de29..3b3db51 100644 --- a/.gitignore +++ b/.gitignore @@ -0,0 +1,3 @@ +*.swp +*.o +lua-fastcgi diff --git a/LICENCE b/LICENCE new file mode 100644 index 0000000..e393d1b --- /dev/null +++ b/LICENCE @@ -0,0 +1,24 @@ +Copyright (c) 2012, Christopher Ramey +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3e4920e --- /dev/null +++ b/Makefile @@ -0,0 +1,17 @@ +CFLAGS=-c -std=gnu99 -Wall +LDFLAGS=-O2 -Wl,-Bstatic -lfcgi -llua5.1 -Wl,-Bdynamic -lm -lpthread + + +.c.o: + $(CC) $(CFLAGS) $< -o $@ + +all: lua-fastcgi + +debug: CFLAGS+=-g -DDEBUG +debug: lua-fastcgi + +lua-fastcgi: src/lua-fastcgi.o src/lfuncs.o src/lua.o src/config.o + $(CC) $^ $(LDFLAGS) -o $@ + +clean: + rm -f src/*.o lua-fastcgi diff --git a/README.md b/README.md new file mode 100644 index 0000000..00ee14f --- /dev/null +++ b/README.md @@ -0,0 +1,38 @@ +lua-fastcgi +=========== + +lua-fastcgi is a sandboxed Lua backend for FastCGI. That is, you can write +Lua scripts that serve up web pages. Options exist in lua-fastcgi.lua +to configure a fixed amount of memory, cpu usage, and output bytes +for each request. While sandboxed, lua-fastcgi supports a limit set +of functions. If sandboxing is disabled, lua-fastcgi loads the standard +libraries and users may load modules as needed. + + +compiling +--------- + +lua-fastcgi requires libfcgi and liblua to successfully compile. lua-fastcgi +has been tested under Linux, specifically Ubuntu 10.10. Other versions of +Ubuntu will likely work without any changes. Other flavors of Linux should +work with little to no effort. Other Unix-like operating systems are untested +and unsupported, for now. + + +running +------- + +lua-fastcgi reads lua-fastcgi.lua in the current working directory. If it +fails to read this file, it will assume certain defaults and continue anyway. +Configuration defaults are documented in the included lua-fastcgi.lua file. + + +lua-fastcgi has been tested with nginx, but will likely work with other +FastCGI compatible web servers with little effort. lua-fastcgi relies only +on SCRIPT_NAME and SCRIPT_FILENAME FastCGI variables passed to it. Lua scripts +can be configured inside of an nginx server directive as follows: + + location ~* \.lua$ { + include /etc/nginx/fastcgi_params; + fastcgi_pass 127.0.0.1:9222; + } diff --git a/TODO b/TODO new file mode 100644 index 0000000..92f45c3 --- /dev/null +++ b/TODO @@ -0,0 +1,17 @@ +High Priority +------------- +POST variable parsing +Basic sandboxed database support (e.g. GET/PUT) +Sandboxed dofile() / loadfile() / loadstring() + + +Medium Priority +--------------- +File Upload Support +Database error logging +Consider switching to mmap() for file reads + + +Low Priority +------------ +Database file adapter diff --git a/lua-fastcgi.lua b/lua-fastcgi.lua new file mode 100644 index 0000000..37809c1 --- /dev/null +++ b/lua-fastcgi.lua @@ -0,0 +1,37 @@ +return { + -- IP/Port to listen on + -- Default: "127.0.0.1:9222" + listen = "127.0.0.1:9222", + + -- How many connections should be back-logged by each thread + -- Default: 100 + backlog = 100, + + -- Number of threads to spin off. Usually one per CPU + -- (plus hardware threads) is a good idea + -- Default: 4 + threads = 1, + + -- Indicates if states should be sandboxed (i.e. Denied access to + -- file system resources or system-level functions) + -- Default: true + sandbox = true, + + -- Maximum amount of memory a state may consume or 0 for no limit + -- Default: 65536 + mem_max = 65536, + + -- Maximum amount of CPU time, in both seconds and picoseconds for each + -- execution. If cpu_sec and cpu_usec are 0, there is no limit + -- Default: 5000000 + cpu_usec = 500000, + -- Default: 0 + cpu_sec = 0, + + -- Limit page output to x bytes or 0 for unlimited + -- Default: 65536 + output_max = 0, + + -- Default content type returned in header + content_type = "text/html; charset=iso-8859-1" +} diff --git a/src/config.c b/src/config.c new file mode 100644 index 0000000..0969bba --- /dev/null +++ b/src/config.c @@ -0,0 +1,126 @@ +#include +#include +#include + +#include +#include +#include + +#include "config.h" + + +// Checks if a file exists +static int LF_fileexists(char *path) +{ + if(access(path, F_OK) == 0){ return 1; } + else { return 0; } +} + + +// Create configuration with default settings +LF_config *LF_createconfig() +{ + LF_config *c = malloc(sizeof(LF_config)); + if(c == NULL){ return NULL; } + + // Default settings + c->listen = "127.0.0.1:9222"; + c->backlog = 100; + c->threads = 1; + c->sandbox = 1; + c->mem_max = 65536; + c->output_max = 65536; + c->cpu_usec = 500000; + c->cpu_sec = 0; + + return c; +} + + +// Load configuration +int LF_loadconfig(LF_config *cfg, char *path) +{ + if(!LF_fileexists(path)){ return 1; } + + lua_State *l = luaL_newstate(); + if(luaL_loadfile(l, path) || lua_pcall(l,0,1,0)){ + printf("%s\n", lua_tostring(l, -1)); + lua_close(l); + return 1; + } + + lua_settop(l, 1); + + if(lua_istable(l, 1)){ + lua_pushstring(l, "listen"); + lua_rawget(l, 1); + if(lua_isstring(l, 2)){ + size_t len = 0; + const char *str = lua_tolstring(l, 2, &len); + + if(len > 0){ + cfg->listen = malloc(len+1); + memcpy(cfg->listen, str, len+1); + } + } + + lua_settop(l, 1); + + lua_pushstring(l, "backlog"); + lua_rawget(l, 1); + if(lua_isnumber(l, 2)){ cfg->backlog = lua_tonumber(l, 2); } + + lua_settop(l, 1); + + lua_pushstring(l, "threads"); + lua_rawget(l, 1); + if(lua_isnumber(l, 2)){ cfg->threads = lua_tonumber(l, 2); } + + lua_settop(l, 1); + + lua_pushstring(l, "sandbox"); + lua_rawget(l, 1); + if(lua_isboolean(l, 2)){ cfg->sandbox = lua_toboolean(l, 2); } + + lua_settop(l, 1); + + lua_pushstring(l, "mem_max"); + lua_rawget(l, 1); + if(lua_isnumber(l, 2)){ cfg->mem_max = lua_tonumber(l, 2); } + + lua_settop(l, 1); + + lua_pushstring(l, "cpu_usec"); + lua_rawget(l, 1); + if(lua_isnumber(l, 2)){ cfg->cpu_usec = lua_tonumber(l, 2); } + + lua_settop(l, 1); + + lua_pushstring(l, "cpu_sec"); + lua_rawget(l, 1); + if(lua_isnumber(l, 2)){ cfg->cpu_sec = lua_tonumber(l, 2); } + + lua_settop(l, 1); + + lua_pushstring(l, "output_max"); + lua_rawget(l, 1); + if(lua_isnumber(l, 2)){ cfg->output_max = lua_tonumber(l, 2); } + + lua_settop(l, 1); + + lua_pushstring(l, "content_type"); + lua_rawget(l, 1); + if(lua_isstring(l, 2)){ + size_t len = 0; + const char *str = lua_tolstring(l, 2, &len); + + if(len > 0){ + cfg->content_type = malloc(len+1); + memcpy(cfg->content_type, str, len+1); + } + } + } + + lua_close(l); + return 0; +} diff --git a/src/config.h b/src/config.h new file mode 100644 index 0000000..9b9a8da --- /dev/null +++ b/src/config.h @@ -0,0 +1,16 @@ +typedef struct { + char *listen; + int backlog; + int threads; + + int sandbox; + size_t mem_max; + size_t output_max; + unsigned long cpu_usec; + unsigned long cpu_sec; + + char *content_type; +} LF_config; + +LF_config *LF_createconfig(); +int LF_loadconfig(LF_config *, char *); diff --git a/src/lfuncs.c b/src/lfuncs.c new file mode 100644 index 0000000..bbe8f80 --- /dev/null +++ b/src/lfuncs.c @@ -0,0 +1,137 @@ +#include +#include +#include + +#include + +#include +#include + +#include "lua.h" +#include "lfuncs.h" + + +// replacement print function, outputs to FCGI stream +static int LF_pprint(lua_State *l, int cr) +{ + int args = lua_gettop(l); + + // Fetch the response + lua_pushstring(l, "RESPONSE"); + lua_rawget(l, LUA_REGISTRYINDEX); + LF_response *response = lua_touserdata(l, args+1); + lua_pop(l, 1); + + // fetch limits + lua_pushstring(l, "RESPONSE_LIMIT"); + lua_rawget(l, LUA_REGISTRYINDEX); + size_t *limit = lua_touserdata(l, args+1); + lua_pop(l, 1); + + + // If the response isn't committed, send the header + if(!response->committed){ + lua_getglobal(l, "HEADER"); + if(!lua_istable(l, args+1)){ luaL_error(l, "Invalid HEADER (Not table)."); } + + lua_pushstring(l, "Status"); + lua_rawget(l, args+1); + + // If the status has been explicitly set, send that + if(!lua_isnil(l, args+2)){ + if(!lua_isstring(l, args+2)){ + luaL_error(l, "Invalid HEADER (Invalid Status)."); + } + + size_t len; + const char *str = lua_tolstring(l, args+2, &len); + + if(limit){ + if((len+10) > *limit){ luaL_error(l, "Output limit exceeded."); } + *limit -= (len+10); + } + + FCGX_PutStr("Status: ", 8, response->out); + FCGX_PutStr(str, len, response->out); + FCGX_PutStr("\r\n", 2, response->out); + response->committed = 1; + } + lua_pop(l, 1); // Pop the status + + // Loop over the header, ignoring status, but sending everything else + lua_pushnil(l); + while(lua_next(l, args+1)){ + // If the key or the value isn't a string (or number) throw an error + if(!lua_isstring(l, args+2) || !lua_isstring(l, args+3)){ + luaL_error(l, "Invalid HEADER (Invalid key and/or value)."); + } + + size_t keylen = 0; + const char *key = lua_tolstring(l, args+2, &keylen); + if(keylen == 6 && memcmp(key, "Status", 6) == 0){ + // Clear the last value out + lua_pop(l, 1); + continue; + } + + size_t vallen = 0; + const char *val = lua_tolstring(l, args+3, &vallen); + + if(limit){ + if((vallen+keylen+4) > *limit){ luaL_error(l, "Output limit exceeded."); } + *limit -= (vallen+keylen+4); + } + + FCGX_PutStr(key, keylen, response->out); + FCGX_PutStr(": ", 2, response->out); + FCGX_PutStr(val, vallen, response->out); + FCGX_PutStr("\r\n", 2, response->out); + + response->committed = 1; + lua_pop(l, 1); // Clear the last value out + } + lua_pop(l, 1); // Clear the table out + + if(limit){ + if(2 >= *limit){ luaL_error(l, "Output limit exceeded."); } + *limit -= 2; + } + + FCGX_PutS("\r\n", response->out); + response->committed = 1; + } + + size_t strlen; + const char *str; + for(int i=1; i <= args; i++){ + switch(lua_type(l, i)){ + case LUA_TSTRING: + case LUA_TNUMBER: + case LUA_TBOOLEAN: + str = lua_tolstring(l, i, &strlen); + if(limit){ + if(strlen > *limit){ luaL_error(l, "Output limit exceeded."); } + *limit -= strlen; + } + + FCGX_PutStr(str, strlen, response->out); + break; + + default: /* Ignore other types */ break; + } + } + + if(cr){ + if(limit){ + if(*limit == 0){ luaL_error(l, "Output limit exceeded."); } + (*limit)--; + } + + FCGX_PutChar('\n', response->out); + } + return 0; +} + + +int LF_print(lua_State *l){ return LF_pprint(l, 1); } +int LF_write(lua_State *l){ return LF_pprint(l, 0); } diff --git a/src/lfuncs.h b/src/lfuncs.h new file mode 100644 index 0000000..c587e41 --- /dev/null +++ b/src/lfuncs.h @@ -0,0 +1,4 @@ +// Writes FCGI output followed by a carriage return +int LF_print(lua_State *); +// Writes FCGI output without a carriage return +int LF_write(lua_State *); diff --git a/src/lua-fastcgi.c b/src/lua-fastcgi.c new file mode 100644 index 0000000..f3533a1 --- /dev/null +++ b/src/lua-fastcgi.c @@ -0,0 +1,171 @@ +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +#include "lua.h" +#include "config.h" +#include "lua-fastcgi.h" + + +static char *http_status_strings[] = { + [200] = "OK", + [403] = "Forbidden", + [404] = "Not Found", + [500] = "Internal Server Error" +}; + + +#define senderror(status_code,error_string) \ + if(!response.committed){ \ + FCGX_FPrintF(request.out, "Status: %d %s\r\n", status_code, http_status_strings[status_code]); \ + FCGX_FPrintF(request.out, "Content-Type: %s\r\n\r\n", config->content_type); \ + response.committed = 1; \ + } \ + FCGX_PutS(error_string, response.out); + + +#ifdef DEBUG +static void printcfg(LF_config *cfg) +{ + printf("Listen: %s\n", cfg->listen); + printf("Backlog: %d\n", cfg->backlog); + printf("Threads: %d\n", cfg->threads); + printf("Sandbox: %d\n", cfg->sandbox); + printf("Max Memory: %zu\n", cfg->mem_max); + printf("Max Output: %zu\n", cfg->output_max); + printf("CPU usec: %lu\n", cfg->cpu_usec); + printf("CPU sec: %lu\n", cfg->cpu_sec); + printf("Default Content Type: %s\n", cfg->content_type); + printf("\n"); +} + + +static void printvars(FCGX_Request *request) +{ + for(int i=0; request->envp[i] != NULL; i++){ + printf("%s\n", request->envp[i]); + } + printf("\n"); +} +#endif + + +void *thread_run(void *arg) +{ + LF_params *params = arg; + LF_config *config = params->config; + LF_limits *limits = LF_newlimits(); + LF_response response; + lua_State *l; + + FCGX_Request request; + FCGX_InitRequest(&request, params->socket, 0); + + for(;;){ + LF_setlimits( + limits, config->mem_max, config->output_max, + config->cpu_sec, config->cpu_usec + ); + + l = LF_newstate(config->sandbox, config->content_type); + + if(FCGX_Accept_r(&request)){ + printf("FCGX_Accept_r() failure\n"); + LF_closestate(l); + continue; + } + + #ifdef DEBUG + printvars(&request); + #endif + + LF_parserequest(l, &request, &response); + LF_enablelimits(l, limits); + + switch(LF_loadfile(l)){ + case 0: + if(lua_pcall(l, 0, 0, 0)){ + senderror(500, lua_tostring(l, -1)); + } else if(!response.committed){ + senderror(200, ""); + } + break; + + case EACCES: + senderror(403, lua_tostring(l, -1)); + break; + + case ENOENT: + senderror(404, lua_tostring(l, -1)); + break; + + default: + senderror(500, lua_tostring(l, -1)); + break; + } + + FCGX_Finish_r(&request); + LF_closestate(l); + } +} + + +int main() +{ + if(FCGX_Init() != 0){ + printf("FCGX_Init() failure\n"); + exit(EXIT_FAILURE); + } + + LF_config *config = LF_createconfig(); + if(config == NULL){ + printf("LF_createconfig(): memory allocation error\n"); + exit(EXIT_FAILURE); + } + + if(LF_loadconfig(config, "./lua-fastcgi.lua")){ + printf("Error loading lua-fastcgi.lua\n"); + } + + #ifdef DEBUG + printcfg(config); + #endif + + int socket = FCGX_OpenSocket(config->listen, config->backlog); + if(socket < 0){ + printf("FCGX_OpenSocket() failure: could not open %s\n", config->listen); + exit(EXIT_FAILURE); + } + + LF_params *params = malloc(sizeof(LF_params)); + params->socket = socket; + params->config = config; + + if(config->threads == 1){ + thread_run(params); + } else { + pthread_attr_t attr; + pthread_attr_init(&attr); + pthread_t threads[config->threads]; + for(int i=0; i < config->threads; i++){ + int r = pthread_create(&threads[i], &attr, &thread_run, params); + if(r){ + printf("Thread creation error: %d\n", r); + exit(EXIT_FAILURE); + } + } + + pthread_join(threads[0], NULL); + } + + return 0; +} diff --git a/src/lua-fastcgi.h b/src/lua-fastcgi.h new file mode 100644 index 0000000..0526846 --- /dev/null +++ b/src/lua-fastcgi.h @@ -0,0 +1,5 @@ +typedef struct { + LF_config *config; + int socket; +} LF_params; + diff --git a/src/lua.c b/src/lua.c new file mode 100644 index 0000000..19d7180 --- /dev/null +++ b/src/lua.c @@ -0,0 +1,366 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +#include "lua.h" +#include "lfuncs.h" + +#define LF_BUFFERSIZE 4096 + +typedef struct { + int fd; + char buffer[LF_BUFFERSIZE]; + size_t total; +} LF_loaderdata; + + +#ifdef DEBUG +void LF_printstack(lua_State *l) +{ + int ss = lua_gettop(l); + printf("Lua Stack:\n"); + for(int i=1; i <= ss; i++){ + printf("\t%d) %s\n", i, lua_typename(l, lua_type(l, i))); + } +} +#endif + + +// Gets current thread usage +static int LF_threadusage(struct timeval *tv) +{ + struct rusage usage; + if(getrusage(RUSAGE_SELF, &usage) == -1){ + return 1; + } + + // Add user to sys to get actual usage + tv->tv_usec = usage.ru_utime.tv_usec + usage.ru_stime.tv_usec; + tv->tv_sec = usage.ru_utime.tv_sec + usage.ru_stime.tv_sec; + + if(tv->tv_usec > 1000000){ + tv->tv_usec -= 1000000; + tv->tv_sec++; + } + return 0; +} + + +// limits cpu usage +static void LF_limit_hook(lua_State *l, lua_Debug *d) +{ + lua_pushstring(l, "CPU_LIMIT"); + lua_rawget(l, LUA_REGISTRYINDEX); + struct timeval *limit = lua_touserdata(l, -1); + lua_pop(l, 1); + + struct timeval tv; + if(LF_threadusage(&tv)){ luaL_error(l, "CPU usage sample error"); } + if(timercmp(&tv, limit, >)){ luaL_error(l, "CPU limit exceeded"); } +} + + +// removes global variables +static void LF_nilglobal(lua_State *l, char *var) +{ + lua_pushnil(l); + lua_setglobal(l, var); +} + + +// Limited memory allocator +static void *LF_limit_alloc(void *ud, void *ptr, size_t osize, size_t nsize) +{ + size_t *limit = ud; + + *limit += osize; + + if(nsize == 0){ + free(ptr); + return NULL; + } else { + if(*limit < nsize){ return NULL; } + *limit -= nsize; + + return realloc(ptr, nsize); + } +} + + +// Allocates and initializes a new set of limits for a lua state +LF_limits *LF_newlimits() +{ + LF_limits *limits = malloc(sizeof(LF_limits)); + if(limits == NULL){ return NULL; } + + memset(limits, 0, sizeof(LF_limits)); + return limits; +} + + +void LF_setlimits(LF_limits *limits, size_t memory, size_t output, uint32_t cpu_sec, uint32_t cpu_usec) +{ + limits->memory = memory; + limits->output = output; + limits->cpu.tv_usec = cpu_usec; + limits->cpu.tv_sec = cpu_sec; +} + + + +void LF_enablelimits(lua_State *l, LF_limits *limits) +{ + if(limits->cpu.tv_usec > 0 || limits->cpu.tv_sec > 0){ + struct timeval curr; + if(LF_threadusage(&curr)){ + printf("CPU usage sample error\n"); // FIX ME + } else { + timeradd(&limits->cpu, &curr, &limits->cpu); + lua_sethook(l, &LF_limit_hook, LUA_MASKCOUNT, 1000); + } + + lua_pushstring(l, "CPU_LIMIT"); + lua_pushlightuserdata(l, &limits->cpu); + lua_rawset(l, LUA_REGISTRYINDEX); + } + + if(limits->output){ + lua_pushstring(l, "RESPONSE_LIMIT"); + lua_pushlightuserdata(l, &limits->output); + lua_rawset(l, LUA_REGISTRYINDEX); + } + + if(limits->memory){ lua_setallocf(l, &LF_limit_alloc, &limits->memory); } +} + + +// Initialize a new lua state using specific parameters +lua_State *LF_newstate(int sandbox, char *content_type) +{ + lua_State *l = luaL_newstate(); + if(l == NULL){ return NULL; } + + // Load base + lua_pushcfunction(l, luaopen_base); + lua_pushliteral(l, ""); + lua_call(l, 1, 0); + + if(sandbox){ + // Load table + lua_pushcfunction(l, luaopen_table); + lua_pushliteral(l, LUA_TABLIBNAME); + lua_call(l, 1, 0); + + // Load string + lua_pushcfunction(l, luaopen_string); + lua_pushliteral(l, LUA_STRLIBNAME); + lua_call(l, 1, 0); + + // Load math + lua_pushcfunction(l, luaopen_math); + lua_pushliteral(l, LUA_MATHLIBNAME); + lua_call(l, 1, 0); + + // Nil out unsafe functions/objects + LF_nilglobal(l, "dofile"); + LF_nilglobal(l, "load"); + LF_nilglobal(l, "loadfile"); + LF_nilglobal(l, "xpcall"); + LF_nilglobal(l, "pcall"); + LF_nilglobal(l, "module"); + LF_nilglobal(l, "require"); + } + + // Register the print function + lua_register(l, "print", &LF_print); + // Register the write function + lua_register(l, "write", &LF_write); + + // Setup the "HEADER" value + lua_newtable(l); + + lua_pushstring(l, "Content-Type"); + lua_pushstring(l, content_type); + lua_rawset(l, 1); + + lua_setglobal(l, "HEADER"); + + return l; +} + + +// Set GET variables +static void LF_parsequerystring(lua_State *l, char *query_string) +{ + lua_newtable(l); + + int stack = lua_gettop(l); + + char *sptr, *optr, *nptr; + for(nptr = optr = sptr = &query_string[0]; 1; optr++){ + switch(*optr){ + case '+': + *nptr++ = ' '; + break; + + case '=': + // Push a key, if it's valid and there's not already one + if(lua_gettop(l) == stack){ + if((nptr-sptr) > 0){ lua_pushlstring(l, sptr, (nptr - sptr)); } + sptr = nptr; + } else { + *nptr++ = '='; + } + break; + + case '&': + // Push key or value if valid + if((nptr-sptr) > 0){ lua_pushlstring(l, sptr, (nptr - sptr)); } + + // Push value, if there is already a key + if(lua_gettop(l) == (stack+1)){ lua_pushstring(l, ""); } + + // Set key/value if they exist + if(lua_gettop(l) == (stack+2)){ lua_rawset(l, stack); } + + sptr = nptr; + break; + + case '%': { + // Decode hex percent encoded sets, if valid + char c1 = *(optr+1), c2 = *(optr+2); + if(isxdigit(c1) && isxdigit(c2)){ + char digit = 16 * (c1 >= 'A' ? (c1 & 0xdf) - '7' : (c1 - '0')); + digit += (c2 >= 'A' ? (c2 & 0xdf) - '7' : (c2 - '0')); + *nptr++ = digit; + optr += 2; + } else { + *nptr++ = '%'; + } + } break; + + case '\0': + // Push key or value if valid + if((nptr-sptr) > 0){ lua_pushlstring(l, sptr, (nptr - sptr)); } + + // Push value, if needed + if(lua_gettop(l) == (stack+1)){ lua_pushstring(l, ""); } + + // Set key/value if valid + if(lua_gettop(l) == (stack+2)){ lua_rawset(l, stack); } + + // Finally, set the table + lua_setglobal(l, "GET"); + return; + break; + + default: + *nptr++ = *optr; + break; + } + } +} + + +// Parses fastcgi request +void LF_parserequest(lua_State *l, FCGX_Request *request, LF_response *response) +{ + lua_newtable(l); + for(char **p = request->envp; *p; ++p){ + char *vptr = strchr(*p, '='); + int keylen = (vptr - *p); + + lua_pushlstring(l, *p, keylen); // Push Key + lua_pushstring(l, (vptr+1)); // Push Value + lua_rawset(l, 1); // Set key/value into table + + if(keylen == 12 && memcmp(*p, "QUERY_STRING", 12) == 0){ + LF_parsequerystring(l, (vptr+1)); + } + } + lua_setglobal(l, "REQUEST"); + + response->committed = 0; + response->out = request->out; + lua_pushstring(l, "RESPONSE"); + lua_pushlightuserdata(l, response); + lua_rawset(l, LUA_REGISTRYINDEX); +} + + +static const char *LF_filereader(lua_State *l, void *data, size_t *size) +{ + LF_loaderdata *ld = data; + + *size = read(ld->fd, ld->buffer, LF_BUFFERSIZE); + + if(ld->total == 0 && *size > 3){ + if(memcmp(ld->buffer, LUA_SIGNATURE, 4) == 0){ + luaL_error(l, "Compiled bytecode not supported."); + } + } + + switch(*size){ + case 0: return NULL; + case -1: luaL_error(l, strerror(errno)); + default: + ld->total += *size; + return ld->buffer; + } +} + + +// Loads a lua file into a state +int LF_loadfile(lua_State *l) +{ + lua_getglobal(l, "REQUEST"); + + int stack = lua_gettop(l); + + lua_pushstring(l, "SCRIPT_FILENAME"); + lua_rawget(l, stack); + const char *path = lua_tostring(l, stack+1); + + lua_pushstring(l, "SCRIPT_NAME"); + lua_rawget(l, stack); + const char *name = lua_tostring(l, stack+2); + + LF_loaderdata ld; + ld.total = 0; + ld.fd = open(path, O_RDONLY); + if(ld.fd == -1){ + lua_pushstring(l, strerror(errno)); + return errno; + } + + // Generate a string with an '=' followed by the script name + // this ensures lua will generation a reasonable error + size_t len = strlen(name) + 1; + char scriptname[len + 1]; + scriptname[0] = '='; + memcpy(&scriptname[1], name, len); + + int r = lua_load(l, &LF_filereader, &ld, scriptname); + + close(ld.fd); + return (r == 0 ? 0 : ENOMSG); +} + + +// Closes a state +void LF_closestate(lua_State *l) +{ + lua_close(l); +} diff --git a/src/lua.h b/src/lua.h new file mode 100644 index 0000000..6216443 --- /dev/null +++ b/src/lua.h @@ -0,0 +1,20 @@ +typedef struct { + int committed; + FCGX_Stream *out; +} LF_response; + +typedef struct { + size_t memory; + struct timeval cpu; + size_t output; +} LF_limits; + + +lua_State *LF_newstate(int, char *); +LF_limits *LF_newlimits(); +void LF_setlimits(LF_limits *, size_t, size_t, uint32_t, uint32_t); +void LF_enablelimits(lua_State *, LF_limits *); +void LF_parserequest(lua_State *l, FCGX_Request *, LF_response *); +void LF_emptystack(lua_State *); +int LF_loadfile(lua_State *); +void LF_closestate(lua_State *);