Browse Source

Switched to mmap for loading files. Added POST variable handling. Added

sandboxed loadstring, loadfile and  dofile functions for Lua. Fixed bug
where errors generated during request parsing may cause memory leaks
on certain platforms. Fixed bug where non-string errors generated by
Lua would cause a segmentation fault. Corrected various typos and
unclear code comments.
master
Christopher Ramey 10 years ago
committed by cdramey
parent
commit
76d187e568
  1. 2
      LICENCE
  2. 1
      Makefile
  3. 2
      README.md
  4. 10
      TODO
  5. 2
      lua-fastcgi.lua
  6. 154
      src/lfuncs.c
  7. 10
      src/lfuncs.h
  8. 48
      src/lua-fastcgi.c
  9. 190
      src/lua.c
  10. 19
      src/lua.h

2
LICENCE

@ -9,7 +9,7 @@ are met:
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.
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

1
Makefile

@ -8,6 +8,7 @@ LDFLAGS=-O2 -Wl,-Bstatic -lfcgi -llua5.1 -Wl,-Bdynamic -lm -lpthread
all: lua-fastcgi
debug: CFLAGS+=-g -DDEBUG
debug: LDFLAGS+=-lrt
debug: lua-fastcgi
lua-fastcgi: src/lua-fastcgi.o src/lfuncs.o src/lua.o src/config.o

2
README.md

@ -4,7 +4,7 @@ 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
for each request. While sandboxed, lua-fastcgi supports a limited set
of functions. If sandboxing is disabled, lua-fastcgi loads the standard
libraries and users may load modules as needed.

10
TODO

@ -1,17 +1,17 @@
High Priority
-------------
POST variable parsing
Basic sandboxed database support (e.g. GET/PUT)
Sandboxed dofile() / loadfile() / loadstring()
Function for reading files into string
Medium Priority
---------------
File Upload Support
Database error logging
Consider switching to mmap() for file reads
File upload support
Logging of errors to database
Low Priority
------------
Database file adapter
Session scoped variables
Application scoped variables

2
lua-fastcgi.lua

@ -30,7 +30,7 @@ return {
-- Limit page output to x bytes or 0 for unlimited
-- Default: 65536
output_max = 0,
output_max = 65536,
-- Default content type returned in header
content_type = "text/html; charset=iso-8859-1"

154
src/lfuncs.c

@ -17,9 +17,9 @@ static int LF_pprint(lua_State *l, int cr)
int args = lua_gettop(l);
// Fetch the response
lua_pushstring(l, "RESPONSE");
lua_pushstring(l, "STATE");
lua_rawget(l, LUA_REGISTRYINDEX);
LF_response *response = lua_touserdata(l, args+1);
LF_state *state = lua_touserdata(l, args+1);
lua_pop(l, 1);
// fetch limits
@ -28,9 +28,8 @@ static int LF_pprint(lua_State *l, int cr)
size_t *limit = lua_touserdata(l, args+1);
lua_pop(l, 1);
// If the response isn't committed, send the header
if(!response->committed){
if(!state->committed){
lua_getglobal(l, "HEADER");
if(!lua_istable(l, args+1)){ luaL_error(l, "Invalid HEADER (Not table)."); }
@ -51,10 +50,10 @@ static int LF_pprint(lua_State *l, int cr)
*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;
FCGX_PutStr("Status: ", 8, state->response);
FCGX_PutStr(str, len, state->response);
FCGX_PutStr("\r\n", 2, state->response);
state->committed = 1;
}
lua_pop(l, 1); // Pop the status
@ -82,12 +81,12 @@ static int LF_pprint(lua_State *l, int cr)
*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);
FCGX_PutStr(key, keylen, state->response);
FCGX_PutStr(": ", 2, state->response);
FCGX_PutStr(val, vallen, state->response);
FCGX_PutStr("\r\n", 2, state->response);
response->committed = 1;
state->committed = 1;
lua_pop(l, 1); // Clear the last value out
}
lua_pop(l, 1); // Clear the table out
@ -97,8 +96,8 @@ static int LF_pprint(lua_State *l, int cr)
*limit -= 2;
}
FCGX_PutS("\r\n", response->out);
response->committed = 1;
FCGX_PutS("\r\n", state->response);
state->committed = 1;
}
size_t strlen;
@ -114,7 +113,7 @@ static int LF_pprint(lua_State *l, int cr)
*limit -= strlen;
}
FCGX_PutStr(str, strlen, response->out);
FCGX_PutStr(str, strlen, state->response);
break;
default: /* Ignore other types */ break;
@ -127,7 +126,7 @@ static int LF_pprint(lua_State *l, int cr)
(*limit)--;
}
FCGX_PutChar('\n', response->out);
FCGX_PutChar('\n', state->response);
}
return 0;
}
@ -135,3 +134,124 @@ static int LF_pprint(lua_State *l, int cr)
int LF_print(lua_State *l){ return LF_pprint(l, 1); }
int LF_write(lua_State *l){ return LF_pprint(l, 0); }
int LF_loadstring(lua_State *l)
{
size_t sz;
const char *s = luaL_checklstring(l, 1, &sz);
if(sz > 3 && memcmp(s, LUA_SIGNATURE, 4) == 0){
lua_pushnil(l);
lua_pushstring(l, "Compiled bytecode not supported.");
return 2;
}
if(luaL_loadbuffer(l, s, sz, luaL_optstring(l, 2, s)) == 0){
return 1;
} else {
lua_pushnil(l);
lua_insert(l, -2);
return 2;
}
}
int LF_loadfile(lua_State *l)
{
size_t sz;
const char *spath = luaL_checklstring(l, 1, &sz);
lua_pushstring(l, "DOCUMENT_ROOT");
lua_rawget(l, LUA_REGISTRYINDEX);
char *document_root = lua_touserdata(l, -1);
lua_pop(l, 1);
if(document_root == NULL){
lua_pushnil(l);
lua_pushstring(l, "DOCUMENT_ROOT not defined.");
return 2;
}
size_t dz = strlen(document_root);
if(dz == 0){
lua_pushnil(l);
lua_pushstring(l, "DOCUMENT_ROOT empty.");
return 2;
}
size_t hz = dz + sz;
if((hz + 2) > 4096){
lua_pushnil(l);
lua_pushstring(l, "Path too large.");
return 2;
}
char hpath[4096];
memcpy(&hpath[0], document_root, dz);
if(hpath[dz-1] != '/' && spath[0] != '/'){ hpath[dz] = '/'; }
memcpy(&hpath[dz+1], spath, sz);
hpath[hz+1] = 0;
char rpath[4096];
char *ptr = realpath(hpath, rpath);
if(ptr == NULL || memcmp(document_root, rpath, dz) != 0){
lua_pushnil(l);
lua_pushstring(l, "Invalid path.");
return 2;
}
switch(LF_fileload(l, &spath[0], &hpath[0])){
case 0:
return 1;
break;
case LF_ERRACCESS:
lua_pushnil(l);
lua_pushstring(l, "Access denied.");
break;
case LF_ERRMEMORY:
lua_pushnil(l);
lua_pushstring(l, "Not enough memory.");
break;
case LF_ERRNOTFOUND:
lua_pushnil(l);
lua_pushstring(l, "No such file or directory.");
break;
case LF_ERRSYNTAX:
lua_pushnil(l);
lua_insert(l, -2);
break;
case LF_ERRBYTECODE:
lua_pushnil(l);
lua_pushstring(l, "Compiled bytecode not supported.");
break;
case LF_ERRNOPATH:
case LF_ERRNONAME:
lua_pushnil(l);
lua_pushstring(l, "Invalid path.");
break;
}
return 2;
}
int LF_dofile(lua_State *l)
{
int r = LF_loadfile(l);
if(r == 1 && lua_isfunction(l, -1)){
lua_call(l, 0, LUA_MULTRET);
return lua_gettop(l) - 1;
} else {
lua_error(l);
}
return 0;
}

10
src/lfuncs.h

@ -1,4 +1,14 @@
// 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 *);
// loadstring() function with anti-bytecode security measures
int LF_loadstring(lua_State *);
// loadfile() function with sandboxing security measures
int LF_loadfile(lua_State *);
// dofile() function with sandboxing security measures
int LF_dofile(lua_State *);

48
src/lua-fastcgi.c

@ -25,12 +25,12 @@ static char *http_status_strings[] = {
#define senderror(status_code,error_string) \
if(!response.committed){ \
if(!state.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; \
state.committed = 1; \
} \
FCGX_PutS(error_string, response.out);
FCGX_PutS(error_string, state.response);
#ifdef DEBUG
@ -64,7 +64,7 @@ void *thread_run(void *arg)
LF_params *params = arg;
LF_config *config = params->config;
LF_limits *limits = LF_newlimits();
LF_response response;
LF_state state;
lua_State *l;
FCGX_Request request;
@ -86,31 +86,43 @@ void *thread_run(void *arg)
#ifdef DEBUG
printvars(&request);
struct timespec rstart, rend;
clock_gettime(CLOCK_MONOTONIC, &rstart);
#endif
LF_parserequest(l, &request, &state);
#ifdef DEBUG
clock_gettime(CLOCK_MONOTONIC, &rend);
// Assumes the request returns in less than a second (which it should)
printf("Request parsed in %luns\n", (rend.tv_nsec-rstart.tv_nsec));
#endif
LF_parserequest(l, &request, &response);
LF_enablelimits(l, limits);
switch(LF_loadfile(l)){
switch(LF_loadscript(l)){
case 0:
if(lua_pcall(l, 0, 0, 0)){
senderror(500, lua_tostring(l, -1));
} else if(!response.committed){
if(lua_isstring(l, -1)){
senderror(500, lua_tostring(l, -1));
} else {
senderror(500, "unspecified lua error");
}
} else if(!state.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));
case LF_ERRACCESS: senderror(403, "access denied"); break;
case LF_ERRMEMORY: senderror(500, "not enough memory"); break;
case LF_ERRNOTFOUND:
printf("404\n");
senderror(404, "no such file or directory");
break;
case LF_ERRSYNTAX: senderror(500, lua_tostring(l, -1)); break;
case LF_ERRBYTECODE: senderror(403, "compiled bytecode not supported"); break;
case LF_ERRNOPATH: senderror(500, "SCRIPT_FILENAME not provided"); break;
case LF_ERRNONAME: senderror(500, "SCRIPT_NAME not provided"); break;
}
FCGX_Finish_r(&request);

190
src/lua.c

@ -1,10 +1,13 @@
#include <stdlib.h>
#include <inttypes.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
#include <ctype.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <sys/time.h>
#include <sys/resource.h>
@ -17,14 +20,6 @@
#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)
@ -119,7 +114,6 @@ void LF_setlimits(LF_limits *limits, size_t memory, size_t output, uint32_t cpu_
}
void LF_enablelimits(lua_State *l, LF_limits *limits)
{
if(limits->cpu.tv_usec > 0 || limits->cpu.tv_sec > 0){
@ -142,7 +136,13 @@ void LF_enablelimits(lua_State *l, LF_limits *limits)
lua_rawset(l, LUA_REGISTRYINDEX);
}
if(limits->memory){ lua_setallocf(l, &LF_limit_alloc, &limits->memory); }
if(limits->memory){
lua_pushstring(l, "MEMORY_LIMIT");
lua_pushlightuserdata(l, &limits->memory);
lua_rawset(l, LUA_REGISTRYINDEX);
lua_setallocf(l, &LF_limit_alloc, &limits->memory);
}
}
@ -174,13 +174,16 @@ lua_State *LF_newstate(int sandbox, char *content_type)
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");
// Override unsafe functions
lua_register(l, "loadstring", &LF_loadstring);
lua_register(l, "loadfile", &LF_loadfile);
lua_register(l, "dofile", &LF_dofile);
}
// Register the print function
@ -202,7 +205,7 @@ lua_State *LF_newstate(int sandbox, char *content_type)
// Set GET variables
static void LF_parsequerystring(lua_State *l, char *query_string)
static void LF_parsequerystring(lua_State *l, char *query_string, char *table)
{
lua_newtable(l);
@ -262,7 +265,7 @@ static void LF_parsequerystring(lua_State *l, char *query_string)
if(lua_gettop(l) == (stack+2)){ lua_rawset(l, stack); }
// Finally, set the table
lua_setglobal(l, "GET");
lua_setglobal(l, table);
return;
break;
@ -275,8 +278,17 @@ static void LF_parsequerystring(lua_State *l, char *query_string)
// Parses fastcgi request
void LF_parserequest(lua_State *l, FCGX_Request *request, LF_response *response)
void LF_parserequest(lua_State *l, FCGX_Request *request, LF_state *state)
{
uintmax_t content_length = 0;
char *content_type = NULL;
state->committed = 0;
state->response = request->out;
lua_pushstring(l, "STATE");
lua_pushlightuserdata(l, state);
lua_rawset(l, LUA_REGISTRYINDEX);
lua_newtable(l);
for(char **p = request->envp; *p; ++p){
char *vptr = strchr(*p, '=');
@ -285,77 +297,129 @@ void LF_parserequest(lua_State *l, FCGX_Request *request, LF_response *response)
lua_pushlstring(l, *p, keylen); // Push Key
lua_pushstring(l, (vptr+1)); // Push Value
lua_rawset(l, 1); // Set key/value into table
switch(keylen){
case 11:
if(memcmp(*p, "SCRIPT_NAME", 11) == 0){
lua_pushstring(l, "SCRIPT_NAME");
lua_pushlightuserdata(l, (vptr+1));
lua_rawset(l, LUA_REGISTRYINDEX);
}
break;
if(keylen == 12 && memcmp(*p, "QUERY_STRING", 12) == 0){
LF_parsequerystring(l, (vptr+1));
case 12:
if(memcmp(*p, "QUERY_STRING", 12) == 0){
LF_parsequerystring(l, (vptr+1), "GET");
} if(memcmp(*p, "CONTENT_TYPE", 12) == 0){
content_type = (vptr+1);
}
break;
case 13:
if(memcmp(*p, "DOCUMENT_ROOT", 13) == 0){
lua_pushstring(l, "DOCUMENT_ROOT");
lua_pushlightuserdata(l, (vptr+1));
lua_rawset(l, LUA_REGISTRYINDEX);
}
break;
case 14:
if(memcmp(*p, "CONTENT_LENGTH", 14) == 0){
content_length = strtoumax((vptr+1), NULL, 10);
}
break;
case 15:
if(memcmp(*p, "SCRIPT_FILENAME", 15) == 0){
lua_pushstring(l, "SCRIPT_FILENAME");
lua_pushlightuserdata(l, (vptr+1));
lua_rawset(l, LUA_REGISTRYINDEX);
}
break;
}
}
lua_setglobal(l, "REQUEST");
response->committed = 0;
response->out = request->out;
lua_pushstring(l, "RESPONSE");
lua_pushlightuserdata(l, response);
lua_rawset(l, LUA_REGISTRYINDEX);
if(content_length > 0 && content_type != NULL && memcmp(content_type, "application/x-www-form-urlencoded", 33) == 0){
char *content = lua_newuserdata(l, content_length+1);
int r = FCGX_GetStr(
content, (content_length > INT_MAX ? INT_MAX : content_length),
request->in
);
*(content + r) = 0; // Add NUL byte at end for proper string
LF_parsequerystring(l, content, "POST");
lua_pop(l, 1);
}
}
static const char *LF_filereader(lua_State *l, void *data, size_t *size)
// Load script by name and path
int LF_fileload(lua_State *l, const char *name, char *scriptpath)
{
LF_loaderdata *ld = data;
char *script = NULL;
int fd = -1, r = 0;
struct stat sb;
if(scriptpath == NULL){ return LF_ERRNOPATH; }
if(name == NULL){ return LF_ERRNONAME; }
*size = read(ld->fd, ld->buffer, LF_BUFFERSIZE);
// Generate a string with an '=' followed by the script name
// this ensures lua will generation a reasonable error
size_t namelen = strlen(name);
char scriptname[namelen+2];
scriptname[0] = '=';
memcpy(&scriptname[1], name, namelen+1);
if(ld->total == 0 && *size > 3){
if(memcmp(ld->buffer, LUA_SIGNATURE, 4) == 0){
luaL_error(l, "Compiled bytecode not supported.");
if((fd = open(scriptpath, O_RDONLY)) == -1){ goto errorL; }
if(fstat(fd, &sb) == -1){ goto errorL; }
if((script = mmap(NULL, sb.st_size, PROT_READ, MAP_SHARED, fd, 0)) == NULL){
goto errorL;
}
if(madvise(script, sb.st_size, MADV_SEQUENTIAL) == -1){ goto errorL; }
if(sb.st_size > 3 && memcmp(script, LUA_SIGNATURE, 4) == 0){
r = LF_ERRBYTECODE;
} else {
switch(luaL_loadbuffer(l, script, sb.st_size, scriptname)){
case LUA_ERRSYNTAX: r = LF_ERRSYNTAX; break;
case LUA_ERRMEM: r = LF_ERRMEMORY; break;
}
}
switch(*size){
case 0: return NULL;
case -1: luaL_error(l, strerror(errno));
default:
ld->total += *size;
return ld->buffer;
if(script != NULL){ munmap(script, sb.st_size); }
if(fd != -1){ close(fd); }
return r;
errorL:
if(script != NULL){ munmap(script, sb.st_size); }
if(fd != -1){ close(fd); }
switch(errno){
case EACCES: return r = LF_ERRACCESS;
case ENOENT: return r = LF_ERRNOTFOUND;
case ENOMEM: return r = LF_ERRMEMORY;
default: return r = LF_ERRANY;
}
return r;
}
// Loads a lua file into a state
int LF_loadfile(lua_State *l)
// Loads script specified in registryindex into lua state
int LF_loadscript(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_rawget(l, LUA_REGISTRYINDEX);
char *scriptpath = lua_touserdata(l, 1);
lua_pop(l, 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);
lua_rawget(l, LUA_REGISTRYINDEX);
char *name = lua_touserdata(l, 1);
lua_pop(l, 1);
close(ld.fd);
return (r == 0 ? 0 : ENOMSG);
return LF_fileload(l, name, scriptpath);
}

19
src/lua.h

@ -1,7 +1,17 @@
#define LF_ERRNONE 0
#define LF_ERRANY 1
#define LF_ERRACCESS 2
#define LF_ERRMEMORY 3
#define LF_ERRNOTFOUND 4
#define LF_ERRSYNTAX 5
#define LF_ERRBYTECODE 6
#define LF_ERRNOPATH 7
#define LF_ERRNONAME 8
typedef struct {
FCGX_Stream *response;
int committed;
FCGX_Stream *out;
} LF_response;
} LF_state;
typedef struct {
size_t memory;
@ -14,7 +24,8 @@ 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_parserequest(lua_State *l, FCGX_Request *, LF_state *);
void LF_emptystack(lua_State *);
int LF_loadfile(lua_State *);
int LF_fileload(lua_State *, const char *, char *);
int LF_loadscript(lua_State *);
void LF_closestate(lua_State *);