/* --------------------------------------------------------------------------
* Copyright 2003-2015 (inclusive) Nathan Angelacos
* (nangel@users.sourceforge.net)
*
* This file is part of haserl.
*
* Haserl is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* Haserl is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with haserl. If not, see .
*
* -----
* The x2c() and unescape_url() routines were taken from
* http://www.jmarshall.com/easy/cgi/getcgi.c.txt
*
* The comments in that text file state:
*
*** Written in 1996 by James Marshall, james@jmarshall.com, except
*** that the x2c() and unescape_url() routines were lifted directly
*** from NCSA's sample program util.c, packaged with their HTTPD.
*** For the latest, see http://www.jmarshall.com/easy/cgi/
* -----
*
------------------------------------------------------------------------- */
#if HAVE_CONFIG_H
#include
#endif
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#if HAVE_SIGNAL_H
#include
#endif
#include "common.h"
#include "h_error.h"
#include "h_script.h"
#include "sliding_buffer.h"
#include "rfc2388.h"
#ifdef INCLUDE_BASHSHELL
#include "h_bash.h"
#endif
#ifdef USE_LUA
#include
#include
#include
#include "h_lua_common.h"
#endif
#ifdef INCLUDE_LUASHELL
#include "h_lua.h"
#endif
#ifdef INCLUDE_LUACSHELL
#include "h_luac.h"
#endif
#include "haserl.h"
#ifndef TEMPDIR
#define TEMPDIR "/tmp"
#endif
#ifndef MAX_UPLOAD_KB
#define MAX_UPLOAD_KB 2048
#endif
/* Refuse to disable the subshell */
#ifndef SUBSHELL_CMD
#define SUBSHELL_CMD "/bin/sh"
#endif
haserl_t global;
/* declare the shell_ function pointers here */
void (*shell_exec) (buffer_t * buf, char *str);
void (*shell_echo) (buffer_t * buf, char *str, size_t len);
void (*shell_eval) (buffer_t * buf, char *str, size_t len);
void (*shell_setup) (char *, list_t *);
void (*shell_doscript) (buffer_t *, char *);
void (*shell_destroy) (void);
#ifdef BASHEXTENSIONS
void (*shell_if) (buffer_t * buf, char *str, size_t len);
void (*shell_elif) (buffer_t * buf, char *str, size_t len);
void (*shell_else) (buffer_t * buf, char *str, size_t len);
void (*shell_endif) (buffer_t * buf, char *str, size_t len);
void (*shell_case) (buffer_t * buf, char *str, size_t len);
void (*shell_when) (buffer_t * buf, char *str, size_t len);
void (*shell_otherwise) (buffer_t * buf, char *str, size_t len);
void (*shell_endcase) (buffer_t * buf, char *str, size_t len);
void (*shell_while) (buffer_t * buf, char *str, size_t len);
void (*shell_endwhile) (buffer_t * buf, char *str, size_t len);
void (*shell_until) (buffer_t * buf, char *str, size_t len);
void (*shell_enduntil) (buffer_t * buf, char *str, size_t len);
void (*shell_for) (buffer_t * buf, char *str, size_t len);
void (*shell_endfor) (buffer_t * buf, char *str, size_t len);
void (*shell_unless) (buffer_t * buf, char *str, size_t len);
void (*shell_elun) (buffer_t * buf, char *str, size_t len);
void (*shell_unelse) (buffer_t * buf, char *str, size_t len);
void (*shell_endunless) (buffer_t * buf, char *str, size_t len);
#endif
/* global shell execution function pointers. These point to the actual functions
that do the job, based on the language */
/*
* Command line / Config file directives When adding a long option, make sure
* to update the short_options as well
*/
struct option ga_long_options[] = {
{"version", no_argument, 0, 'v'},
{"help", no_argument, 0, 'h'},
{"debug", no_argument, 0, 'd'},
{"upload-limit", required_argument, 0, 'u'},
{"upload-dir", required_argument, 0, 'U'},
{"upload-handler", required_argument, 0, 'H'},
{"accept-all", no_argument, 0, 'a'},
{"accept-none", no_argument, 0, 'n'},
{"shell", required_argument, 0, 's'},
{"silent", no_argument, 0, 'S'},
{0, 0, 0, 0}
};
const char *gs_short_options = "+vhdu:U:H:ans:S";
/*
* Convert 2 char hex string into char it represents
* (from http://www.jmarshall.com/easy/cgi)
*/
char
x2c (char *what)
{
char digit;
digit = (what[0] >= 'A' ? ((what[0] & 0xdf) - 'A') + 10 : (what[0] - '0'));
digit *= 16;
digit += (what[1] >= 'A' ? ((what[1] & 0xdf) - 'A') + 10 : (what[1] - '0'));
return (digit);
}
/*
* unsescape %xx to the characters they represent
*/
/* Modified by Juris Feb 2007 */
void
unescape_url (char *url)
{
int i, j;
for (i = 0, j = 0; url[j]; ++i, ++j)
{
if ((url[i] = url[j]) != '%')
continue;
if (!url[j + 1] || !url[j + 2])
break;
url[i] = x2c (&url[j + 1]);
j += 2;
}
url[i] = '\0';
}
/*
* allocate memory or die, busybox style.
*/
void *
xmalloc (size_t size)
{
void *buf;
if ((buf = malloc (size)) == NULL)
{
die_with_message (NULL, NULL, g_err_msg[E_MALLOC_FAIL]);
}
memset (buf, 0, size);
return buf;
}
/*
* realloc memory, or die xmalloc style.
*/
void *
xrealloc (void *buf, size_t size)
{
if ((buf = realloc (buf, size)) == NULL)
{
die_with_message (NULL, NULL, g_err_msg[E_MALLOC_FAIL]);
}
return buf;
}
/*
* adds or replaces the "key=value" value in the env_list chain
* prefix is appended to the key (e.g. FORM_key=value)
*/
list_t *
myputenv (list_t * cur, char *str, char *prefix)
{
list_t *prev = NULL;
size_t keylen;
char *entry = NULL;
char *temp = NULL;
int array = 0;
int len;
temp = memchr (str, '=', strlen (str));
/* if we don't have an equal sign, exit early */
if (temp == 0)
{
return (cur);
}
keylen = (size_t) (temp - str);
/* is this an array */
if (memcmp (str + keylen - 2, "[]", 2) == 0)
{
keylen = keylen - 2;
array = 1;
}
entry = xmalloc (strlen (str) + strlen (prefix) + 1);
entry[0] = '\0';
if (strlen (prefix))
{
strncat (entry, prefix, strlen (prefix));
}
if (array == 1)
{
strncat (entry, str, keylen);
strcat (entry, str + keylen + 2);
}
else
{
strcat (entry, str);
}
/* does the value already exist? */
len = keylen + strlen (prefix) + 1;
while (cur != NULL)
{
if (memcmp (cur->buf, entry, len) == 0)
{
if (array == 1)
{
/* if an array, create a new string with this
* value added to the end of the old value(s)
*/
temp = xmalloc (strlen (cur->buf) + strlen (entry) - len + 2);
memmove (temp, cur->buf, strlen (cur->buf) + 1);
strcat (temp, "\n");
strcat (temp, str + keylen + 3);
free (entry);
entry = temp;
}
/* delete the old entry */
free (cur->buf);
if (prev != NULL)
prev->next = cur->next;
free (cur);
cur = prev;
} /* end if found a matching key */
prev = cur;
if (cur)
{
cur = (list_t *) cur->next;
}
} /* end if matching key */
/* add the value to the end of the chain */
cur = xmalloc (sizeof (list_t));
cur->buf = entry;
if (prev != NULL)
prev->next = cur;
return (cur);
}
/* free list_t chain */
void
free_list_chain (list_t * list)
{
list_t *next;
while (list)
{
next = list->next;
free (list->buf);
free (list);
list = next;
}
}
/* readenv
* reads the current environment and popluates our environment chain
*/
void
readenv (list_t * env)
{
extern char **environ;
int count = 0;
while (environ[count] != NULL)
{
myputenv (env, environ[count], global.nul_prefix);
count++;
}
}
/* CookieVars ()
* if HTTP_COOKIE is passed as an environment variable,
* attempt to parse its values into environment variables
*/
void
CookieVars (list_t * env)
{
char *qs;
char *token;
if (getenv ("HTTP_COOKIE") != NULL)
{
qs = strdup (getenv ("HTTP_COOKIE"));
}
else
{
return;
}
/** split on; to extract name value pairs */
token = strtok (qs, ";");
while (token)
{
// skip leading spaces
while (token[0] == ' ')
{
token++;
}
myputenv (env, token, global.var_prefix);
myputenv (env, token, global.cookie_prefix);
token = strtok (NULL, ";");
}
free (qs);
}
/* SessionID
* Makes a uniqe SESSIONID environment variable for this script
*/
void
sessionid (list_t * env)
{
char session[29];
sprintf (session, "SESSIONID=%x%x", getpid (), (int) time (NULL));
myputenv (env, session, global.nul_prefix);
}
list_t *
wcversion (list_t * env)
{
char version[200];
sprintf (version, "HASERLVER=%s", PACKAGE_VERSION);
return (myputenv (env, version, global.nul_prefix));
}
void
haserlflags (list_t * env)
{
char buf[200];
snprintf (buf, 200, "UPLOAD_DIR=%s", global.uploaddir);
myputenv (env, buf, global.haserl_prefix);
snprintf (buf, 200, "UPLOAD_LIMIT=%lu", global.uploadkb);
myputenv (env, buf, global.haserl_prefix);
snprintf (buf, 200, "ACCEPT_ALL=%d", global.acceptall);
myputenv (env, buf, global.haserl_prefix);
snprintf (buf, 200, "SHELL=%s", global.shell);
myputenv (env, buf, global.haserl_prefix);
}
/*
* Read cgi variables from query string, and put in environment
*/
int
ReadCGIQueryString (list_t * env)
{
char *qs;
char *token;
int i;
if (getenv ("QUERY_STRING") != NULL)
{
qs = strdup (getenv ("QUERY_STRING"));
}
else
{
return (0);
}
/* change plusses into spaces */
for (i = 0; qs[i]; i++)
{
if (qs[i] == '+')
{
qs[i] = ' ';
}
};
/** split on & and ; to extract name value pairs */
token = strtok (qs, "&;");
while (token)
{
unescape_url (token);
myputenv (env, token, global.var_prefix);
myputenv (env, token, global.get_prefix);
token = strtok (NULL, "&;");
}
free (qs);
return (0);
}
/*
* Read cgi variables from stdin (for POST queries)
*/
int
ReadCGIPOSTValues (list_t * env )
{
size_t content_length = 0;
size_t max_len;
int urldecoding = 0;
char *matchstr = "";
size_t i, j, x;
sliding_buffer_t sbuf;
buffer_t token;
unsigned char *data;
const char *CONTENT_LENGTH = "CONTENT_LENGTH";
const char *CONTENT_TYPE = "CONTENT_TYPE";
char *content_type = NULL;
if ((getenv (CONTENT_LENGTH) == NULL) ||
(strtoul (getenv (CONTENT_LENGTH), NULL, 10) == 0))
return (0);
content_type = getenv(CONTENT_TYPE);
if ( ( content_type != NULL ) &&
( strncasecmp ( content_type , "multipart/form-data", 19)) == 0 )
{
/* This is a mime request, we need to go to the mime handler */
i = rfc2388_handler (env);
return (i);
}
/* at this point its either urlencoded or some other blob */
if ( (content_type == NULL ) ||
( strncasecmp (getenv (CONTENT_TYPE), "application/x-www-form-urlencoded", 33) == 0 ) )
{
urldecoding = 1;
matchstr = "&";
}
/* Allow 2MB content, unless they have a global upload set */
max_len = ((global.uploadkb == 0) ? 2048 : global.uploadkb) *1024;
s_buffer_init (&sbuf, 32768);
sbuf.fh = STDIN;
if (getenv (CONTENT_LENGTH))
{
sbuf.maxread = strtoul (getenv (CONTENT_LENGTH), NULL, 10);
}
haserl_buffer_init (&token);
if ( urldecoding == 0 ) {
buffer_add( &token, "body=", 5 );
}
do
{
/* x is true if this token ends with a matchstr or is at the end of stream */
x = s_buffer_read (&sbuf, matchstr);
content_length += sbuf.len;
if (content_length > max_len)
{
die_with_message (NULL, NULL,
"Attempted to send content larger than allowed limits.");
}
if ((x == 0) || (token.data))
{
buffer_add (&token, (char *) sbuf.segment, sbuf.len);
}
if (x)
{
data = sbuf.segment;
sbuf.segment[sbuf.len] = '\0';
if (token.data)
{
/* add the ASCIIZ */
buffer_add (&token, sbuf.segment + sbuf.len, 1);
data = token.data;
}
if (urldecoding) {
/* change plusses into spaces */
j = strlen ((char *) data);
for (i = 0; i <= j; i++)
{
if (data[i] == '+')
{
data[i] = ' ';
}
}
unescape_url ((char *) data);
}
myputenv (env, (char *) data, global.var_prefix);
myputenv (env, (char *) data, global.post_prefix);
if (token.data)
{
buffer_reset (&token);
}
}
}
while (!sbuf.eof);
s_buffer_destroy (&sbuf);
buffer_destroy (&token);
return (0);
}
int
parseCommandLine (int argc, char *argv[])
{
int c;
int option_index = 0;
/* set optopt and optind to 0 to reset getopt_long -
* we may call it multiple times
*/
optopt = 0;
optind = 0;
while ((c = getopt_long (argc, argv, gs_short_options,
ga_long_options, &option_index)) != -1)
{
switch (c)
{
case 'd':
global.debug = TRUE;
break;
case 's':
global.shell = optarg;
break;
case 'S':
global.silent = TRUE;
break;
case 'u':
if (optarg)
{
global.uploadkb = atoi (optarg);
}
else
{
global.uploadkb = MAX_UPLOAD_KB;
}
break;
case 'a':
global.acceptall = TRUE;
break;
case 'n':
global.acceptall = NONE;
break;
case 'U':
global.uploaddir = optarg;
break;
case 'H':
global.uploadhandler = optarg;
break;
case 'v':
case 'h':
printf ("This is " PACKAGE_NAME " version " PACKAGE_VERSION ""
" (http://haserl.sourceforge.net)\n");
exit (0);
break;
}
}
return (optind);
}
int
BecomeUser (uid_t uid, gid_t gid)
{
/* This silently fails if it doesn't work */
/* Following is from Timo Teras */
if (getuid () == 0)
setgroups (1, &gid);
setgid (gid);
setgid (getgid ());
setuid (uid);
setuid (getuid ());
return (0);
}
/*
* Assign default values to the global structure
*/
void
assignGlobalStartupValues ()
{
global.uploadkb = 0; /* how big an upload do we allow (0 for none) */
global.shell = SUBSHELL_CMD; /* The shell we use */
global.silent = FALSE; /* We do print errors if we find them */
global.uploaddir = TEMPDIR; /* where to upload to */
global.uploadhandler = NULL; /* the upload handler */
global.debug = FALSE; /* Not in debug mode. */
global.acceptall = FALSE; /* don't allow POST data for GET method */
global.uploadlist = NULL; /* we don't have any uploaded files */
global.var_prefix = "FORM_";
global.get_prefix = "GET_";
global.post_prefix = "POST_";
global.cookie_prefix = "COOKIE_";
global.haserl_prefix = "HASERL_";
global.nul_prefix = "";
}
void
unlink_uploadlist ()
{
token_t *me;
me = global.uploadlist;
while (me)
{
unlink (me->buf);
free (me->buf);
me = me->next;
}
}
void
cleanup (void)
{
if (global.uploadlist)
{
unlink_uploadlist ();
free_token_list (global.uploadlist);
global.uploadlist = NULL;
}
}
/*-------------------------------------------------------------------------
*
* Main
*
*------------------------------------------------------------------------*/
int
main (int argc, char *argv[])
{
#ifndef JUST_LUACSHELL
token_t *tokenchain = NULL;
buffer_t script_text;
#endif
script_t *scriptchain;
char *filename = NULL;
argv_t *av = NULL;
char **av2 = argv;
int av2c = argc;
int command;
int count;
list_t *env = NULL;
if (atexit (cleanup) != 0)
{
die_with_message (NULL, NULL, "atexit() failed");
}
assignGlobalStartupValues ();
#ifndef JUST_LUACSHELL
haserl_buffer_init (&script_text);
#endif
/* if more than argv[1] and argv[1] is not a file */
switch (argc)
{
case 1:
/* we were run, instead of called as a shell script */
puts ("This is " PACKAGE_NAME " version " PACKAGE_VERSION "\n"
"This program runs as a cgi interpeter, not interactively\n"
"Please see: http://haserl.sourceforge.net\n"
#ifdef USE_LUA
"This version includes Lua (precompiled"
#ifdef INCLUDE_LUASHELL
" and interpreted"
#endif
")\n"
#endif
#ifdef BASHEXTENSIONS
"Unsupported bash extensions supplied by simnux enabled\n"
#endif
);
return (0);
break;
default: /* more than one */
/* split combined #! args - linux bundles them as one */
command = argc_argv (argv[1], &av, "");
if (command > 1)
{
/* rebuild argv into new av2 */
av2c = argc - 1 + command;
av2 = xmalloc (sizeof (char *) * av2c);
av2[0] = argv[0];
for (count = 1; count <= command; count++)
{
av2[count] = av[count - 1].string;
}
for (; count < av2c; count++)
{
av2[count] = argv[count - command + 1];
}
}
parseCommandLine (av2c, av2);
free (av);
if (av2 != argv)
free (av2);
if (optind < av2c)
{
filename = av2[optind];
}
else
{
die_with_message (NULL, NULL, "No script file specified");
}
break;
}
scriptchain = load_script (filename, NULL);
/* drop permissions */
BecomeUser (scriptchain->uid, scriptchain->gid);
/* populate the function pointers based on the shell selected */
if (strcmp (global.shell, "lua") && strcmp (global.shell, "luac"))
/* default to "bash" */
{
#ifdef INCLUDE_BASHSHELL
shell_exec = &bash_exec;
shell_echo = &bash_echo;
shell_eval = &bash_eval;
shell_setup = &bash_setup;
shell_doscript = &bash_doscript;
shell_destroy = &bash_destroy;
#ifdef BASHEXTENSIONS
shell_if = &bash_if;
shell_elif = &bash_elif;
shell_else = &bash_else;
shell_endif = &bash_endif;
shell_case = &bash_case;
shell_when = &bash_when;
shell_otherwise = &bash_otherwise;
shell_endcase = &bash_endcase;
shell_while = &bash_while;
shell_endwhile = &bash_endwhile;
shell_until = &bash_until;
shell_enduntil = &bash_enduntil;
shell_for = &bash_for;
shell_endfor = &bash_endfor;
shell_unless = &bash_unless;
shell_elun = &bash_elun;
shell_unelse = &bash_unelse;
shell_endunless = &bash_endunless;
#endif
#else
die_with_message (NULL, NULL, "Bash shell is not enabled.");
#endif
}
else
{
#ifdef USE_LUA
shell_setup = &lua_common_setup;
shell_destroy = &lua_common_destroy;
global.var_prefix = "FORM.";
global.nul_prefix = "ENV.";
global.get_prefix = "GET.";
global.post_prefix = "POST.";
global.cookie_prefix = "COOKIE.";
global.haserl_prefix = "HASERL.";
if (global.shell[3] == 'c') /* luac only */
#ifdef INCLUDE_LUACSHELL
shell_doscript = &luac_doscript;
#else
die_with_message (NULL, NULL, "Compiled Lua shell is not enabled.");
#endif
else
{
#ifdef INCLUDE_LUASHELL
shell_exec = &lua_exec;
shell_echo = &lua_echo;
shell_eval = &lua_eval;
shell_doscript = &lua_doscript;
#else
die_with_message (NULL, NULL, "Standard Lua shell is not enabled.");
#endif
}
#else
die_with_message (NULL, NULL, "Lua shells are not enabled.");
#endif
}
/* Read the current environment into our chain */
env = wcversion (env);
readenv (env);
sessionid (env);
haserlflags (env);
#ifndef JUST_LUACSHELL
if (strcmp (global.shell, "luac"))
{
tokenchain = build_token_list (scriptchain, NULL);
preprocess_token_list (tokenchain);
}
#endif
/* Read the request data */
if (global.acceptall != NONE)
{
/* If we have a request method, and we were run as a #! style script */
CookieVars (env);
if (getenv ("REQUEST_METHOD"))
{
if ( (strcasecmp (getenv ("REQUEST_METHOD"), "GET") == 0) ||
(strcasecmp (getenv ("REQUEST_METHOD"), "DELETE") == 0) )
{
if (global.acceptall == TRUE)
ReadCGIPOSTValues (env);
ReadCGIQueryString (env);
}
if ( (strcasecmp (getenv ("REQUEST_METHOD"), "POST") == 0) ||
(strcasecmp (getenv ("REQUEST_METHOD"), "PUT") == 0) )
{
if (global.acceptall == TRUE)
ReadCGIQueryString (env);
ReadCGIPOSTValues (env);
}
}
}
/* build a copy of the script to send to the shell */
#ifndef JUST_LUACSHELL
if (strcmp (global.shell, "luac"))
{
process_token_list (&script_text, tokenchain);
}
#endif
/* run the script */
if (global.debug == TRUE)
{
#ifndef JUST_LUACSHELL
if (getenv ("REQUEST_METHOD"))
{
write (1, "Content-Type: text/plain\n\n", 26);
}
write (1, script_text.data, script_text.ptr - script_text.data);
#else
die_with_message (NULL, NULL,
"Debugging output doesn't work with the compiled Lua shell.");
#endif
}
else
{
shell_setup (global.shell, env);
#ifdef JUST_LUACSHELL
shell_doscript (NULL, scriptchain->name);
#else
shell_doscript (&script_text, scriptchain->name);
#endif
shell_destroy ();
}
#ifndef JUST_LUACSHELL
/* destroy the script */
buffer_destroy (&script_text);
free_token_list (tokenchain);
#endif
free_list_chain (env);
free_script_list (scriptchain);
return (0);
}