lua-users home
lua-l archive

[Date Prev][Date Next][Thread Prev][Thread Next] [Date Index] [Thread Index]


I forgot to mention, my shttpd dispatcher of course uses the SAPI CGI launcher from Kepler as well as CGILua. The entire system is actually a layering of technologies:

daemontools and tcpserver (ucspi-tcp) from D. J. Bernstein
http://cr.yp.to/daemontools.html
http://cr.yp.to/ucspi-tcp.html

shttpd from SuperScript Technology
http://www.superscript.com/shttpd/intro.html

cgi-1.1-dispatch and cgi-1.1-fixup from me
(attached)

SAPI CGI launcher and CGILua from Kepler
http://www.keplerproject.org/
http://www.keplerproject.org/cgilua

slslpp.lua from Rici Lake
http://lua-users.org/wiki/SlightlyLessSimpleLuaPreprocessor

I will write up a wiki page on how to set it all up, if there is any interest.

					-Mark
/* cgi-1.1-dispatch.c  Mark Edgar
 *
 * This program is intended to be run by cgi-httpd
 * http://www.superscript.com/shttpd/intro.html
 * It translates the environment variables PATH_INFO and SCRIPT_NAME:
 *
 * PATH_INFO=/path/to/executable/extra/info
 * SCRIPT_NAME=
 *
 * to
 *
 * PATH_INFO=/extra/info
 * SCRIPT_NAME=/path/to/executable
 *
 * and then executes ./bin/path/to/executable
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <limits.h>
#include <assert.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>

const char *progname;

#define debug(...)

static void
Perror(const char *message)
{
	char wd[PATH_MAX];
	debug("%s: cwd >>%s<<\n",
		progname, getcwd(wd, sizeof wd));
	fprintf(stderr, "%s: ``%s'': %s\n",
		progname, message, strerror(errno));
}

/*
 * Replace /. with /: and // with /
 */
static void
sanitize_path(char *path)
{
	size_t s, d;
	int c;
	for (s = d = 1; (c = path[s]); s++) {
		if (path[s - 1] == '/') {
			if (c == '.') c = ':';
			if (c == '/') continue;
		}
		path[d++] = c;
	}
	path[d] = 0;
}

/*
 * Copy a string; return a pointer to the null terminator written.
 * The buffers may overlap as long as src > dst.
 */
char *
stpcpy(char *dst, const char *src)
{
	while ((*dst = *src++)) dst++;
	return dst;
}

/*
 * Copy path components from path to name until name contains the path to a
 * file, or a non-existent path.  A pointer to the remainder of path is
 * returned.  name must contain a path name (or empty string) and must be large
 * enough to store this and all the path elements from path which may be
 * copied.
 */
static const char *
find_program(const char *path, char *name)
{
	const char *p;
	char *e;
	struct stat st;

	assert(path != 0);

	e = strchr(name, '\0');
	p = path;
	do {
		assert(*p == '/');
		do *e++ = *p++;
		while (*p && *p != '/');
		if (lstat(name, &st) != 0 || S_ISLNK(st.st_mode)) {
			Perror(name);
			return 0;
		}
		if (!S_ISDIR(st.st_mode))
			return p;
	} while (*p);

	/* This is where to add index.cgi support. */
	Perror(name);

	return 0;
}


static void
page(int status_code, const char *status_message)
{
	const char *prot;
	if ((prot = getenv("SERVER_PROTOCOL")) && 0 != strcmp(prot, "HTTP/0.9"))
		printf(
			"%s %d %s\r\n"
			"Content-Type: text/plain\r\n"
			"Content-Length: %u\r\n"
			"\r\n",
			status_code > 0 ? prot : "Status:",
			abs(status_code), status_message,
			strlen(status_message) + 2);
	else
		printf("%u ", abs(status_code));
	printf("%s\r\n", status_message);
}


static void
E(int e)
{
	if (e == -1) {
		Perror("Error");
		page(500, "Internal server error: Failed to execute CGI program");
		exit(21);
	}
}


int
main(int argc, char **argv)
{
	char *pi;
	const char *new_pi;
	char command[PATH_MAX], *script_name;

	progname = argv[0];

	if (!(pi = getenv("PATH_INFO")) || pi[0] != '/') {
		page(500, "Internal server error: Missing or invalid PATH_INFO");
		return 21;
	}
	sanitize_path(pi);

	script_name = stpcpy(command, "./bin");
	if (!(new_pi = find_program(pi, command))) {
		page(404, "Not Found");
		return 0;
	}

	/* modify PATH_INFO */
	stpcpy(pi, new_pi);

	/* create PATH_TRANSLATED */
	setenv("PATH_TRANSLATED", pi + !!*pi, !0);

	/* create SCRIPT_NAME */
	setenv("SCRIPT_NAME", script_name, !0);

	/* pipe output to fixup program, if specified. */
	if (argv[1]) {
		pid_t pid;
		int p[2];
		E(pipe(p));
		switch (pid = fork()) {
		case -1:
			E(-1);
		case 0:
			dup2(p[0], STDIN_FILENO);
			close(p[0]);
			close(p[1]);
			execvp(argv[1], argv + 1);
			Perror("exec");
			page(500, "Internal server error: Failed to execute CGI program");
			_exit(111);
		default:
			dup2(p[1], STDOUT_FILENO);
			close(p[0]);
			close(p[1]);
			break;
		}
	}

	execl(command, script_name + 1, (char *)0);
	Perror("exec failed");
	page(-500, "Internal server error: Failed to execute CGI program");
	return 21;
}
#!/usr/bin/env lua

--[[
  This program implements the "backend" of the CGI/1.1 protocol.
  It reads the output of a CGI program from its standard input, provides
  an appropriate status line, supplies missing header fields (Server,
  Date, Content-Length), and writes the result to standard output
  using proper CR LF line endings in the header.  The program correctly
  handles HTTP/0.9.

  The returned status code is taken from the Status header field if
  it is output by the CGI program, or is 302 if the Location header
  field is output, or 200 if a valid Content-Type header field exists,
  otherwise the program returns a 500 error.

  If the input begins with a valid HTTP status line, the entire input
  is passed through to the output.  This implements non-parsed-header
  mode without the nph- filename rudeness.  This is an extension to the
  CGI protocol.

  It is an error if the input contains neither a Status header field,
  nor a Location header field nor a Content-Type header field.

  If the SERVER_PROTOCOL environment variable does not exist, HTTP/0.9
  is assumed.

  If the Server header field is missing, the value is taken from the
  SERVER_SOFTWARE environment variable if it exists, otherwise the value
  "Unknown" is used.

  If the Date header field is missing, its value is the current time in
  RFC 2822 (RFC 822) format.

  If the Content-Length header field is missing, its value is calculated
  from the response body length.

  If a response body is not provided by the CGI program, then one will be
  provided only for a Location redirect request or in the presence of a Status
  header field.
--]]

local function debug(...) io.stderr:write(unpack(arg)) end

local function find_all(str, pat)
	return
		function(state,_)
			return (function(a, b, ...)
				if not a then
					return nil
				end
				state.a = a
				state.b = b
				return unpack(arg)
			end)(string.find(state.str, state.pat, state.b + 1))
		end,
		{ str = str, pat = pat, b = 0 }
end

-- Are we at least HTTP/1.0?
local http1
do
	local protocol = os.getenv("SERVER_PROTOCOL")
	http1 = protocol and protocol ~= "HTTP/0.9"
end

local function http_status_line(code, message)
	return "HTTP/1.1 " .. code .. " " .. message .. "\r\n"
end

local function write_header(field)
	if http1 then
		io.write(field)
	end
end

-- Return a new response, body_index, and header table
local function cgi_error(message,extra)
	local response_body = message .. "\r\n"
	local response_header = (extra or "") .. "\r\n"
	local index = string.len(response_header) + 1
	io.write(http_status_line(500, message))
	return response_header .. response_body, index, {}
end

local function append_body(response, code, message)
	return response .. code .. " " .. message .. "\r\n"
end

local function append_redirect_body(response, location_index)
	local _, _, location = string.find(response, "^[^:]+: *([^\r\n]+)", location_index)
	return append_body(response, 302, "Moved temporarily: " .. location)
end

local function tr_eol(lines)
	return (string.gsub(lines, "\r?\n", "\r\n"))
end

-- Translate line endings to CR LF in the header.
local function write_document(doc, body_index, content_type)
	write_header(tr_eol(string.sub(doc, 1, body_index - 1)))
	io.write(string.sub(doc, body_index))
end

-- We will track the values of certain header fields
local hdr = {
	status = false,
	server = false,
	date = false,
	location = false,
	['content-length'] = false,
	['content-type'] = false }

-- Read the entire response.
local response = io.read("*a")

-- If the first line is a status line, we pass everything else through.
-- This is basically non-parsed-header mode -- it allows the CGI program
-- to do whatever it pleases.
if string.find(response, "^HTTP/[^ ]+[ \t]+%d+[ \t]+[^\r\n]*\r\n") then
	io.write(response)
	os.exit(0)
end

-- Record the index of the start of the body in the response
local body_index

-- Process the header.  Record the index of the start of the message body.
do
	local iter, state, init = find_all(response, "^([^%s:]+):[ \t]*[^\r\n]*\r?\n")
	for name in iter, state, init do
		name = string.lower(name)
		if hdr[name] == false then
			hdr[name] = state.a
		end
	end
	body_index = state.b + 1
end

-- Now we should be at the blank line which separates the header from the body.
local _, i = string.find(response, "^\r?\n", body_index)
if not _ then
	if body_index > string.len(response) then
		response, body_index, hdr = cgi_error("Missing end of header")
	else
		response, body_index, hdr = cgi_error("Invalid header")
	end
else
	body_index = i + 1
	if hdr.status then
		local _, _, code, desc = string.find(response, "^[^:]*: *(%d%d%d) ([^\r\n]*)", hdr.status)
		if not _ then
			response, body_index, hdr = cgi_error("Invalid Status header field")
		else
			write_header(http_status_line(code, desc))
			-- Remove the Status header field from the response
			local hdr_end = string.find(response, "\n", hdr.status, true)
			response = string.sub(response, 1, hdr.status - 1) .. string.sub(response, hdr_end + 1)
			body_index = body_index - (hdr_end + 1 - hdr.status)
			if body_index > string.len(response) then
				-- Generate a body if none supplied.
				response = append_body(response, code, desc)
			end
		end
	elseif hdr.location then
		-- We assume the location is a (valid) URL.  Also, if there is no response body, we add one.
		write_header(http_status_line(302, "Moved temporarily"))
		if body_index > string.len(response) then
			response = append_redirect_body(response, hdr.location)
		end
	elseif hdr['content-type'] then
		write_header(http_status_line(200, "Success"))
	else
		response, body_index, hdr = cgi_error("Missing Content-Type header field")
	end
end

-- Write missing header fields
if not hdr.server then
	write_header("Server: " .. (os.getenv("SERVER_SOFTWARE") or "Unknown") .. "\r\n")
end

if not hdr.date then
	-- RFC 2822 compliant date string
	write_header("Date: " .. os.date("%a, %d %b %Y %H:%M:%S %z") .. "\r\n")
end

if not hdr['content-type'] and body_index <= string.len(response) then
	write_header("Content-Type: text/plain; charset=US-ASCII\r\n")
end

if not hdr['content-length'] then
	local length = string.len(response) - body_index + 1
	if length > 0 then
		write_header("Content-Length: " .. length .. "\r\n")
	end
end

-- Write the rest of the response
write_document(response, body_index, hdr['content-type'])