/*
* Copyright (c) 2003-2016 Hypertriton, Inc.
* 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 AUTHOR 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.
*/
/*
* Main interface to CGI/FastCGI. This includes language/charset negotiation,
* parameter variables, cookies and URL processing.
*/
#include "cgi.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "arc4random.h"
#include "pathnames.h"
CGI_Application *cgi; /* Application info */
Uint cgiQueryCount = 0; /* Total queries served */
char cgiLogFile[MAXPATHLEN]; /* Logfile path */
CGI_Module **cgiModules = NULL; /* Modules */
int nCgiModules = 0;
CGI_LanguageFn cgiLanguageFn = NULL; /* Language switch callback */
void *cgiLanguageFnArg = NULL; /* cgiLanguageFn() user argument */
CGI_MenuFn cgiMenuFn = NULL; /* Menu constructor routine */
void *cgiMenuFnArg = NULL; /* cgiMenuFn() user argument */
static char cgiErrorMsg[CGI_ERROR_MAX];
static volatile sig_atomic_t termFlag = 0;
static volatile sig_atomic_t chldFlag = 0;
static const int cgiLogLvlNameLength = 6;
static const char *cgiLogLvlNames[] = {
" emerg",
" alert",
" crit",
" err",
" warn",
"notice",
" info",
" debug",
" query"
};
#define CGI_DEBUG_ARGS
#define CGI_DEBUG_QUERIES
/* #define CGI_DEBUG_HEADERS */
/* #define CGI_DEBUG_COOKIES */
/* #define CGI_DEBUG_IO */
void
CGI_RegisterModule(CGI_Module *mod)
{
cgiModules = Realloc(cgiModules, (nCgiModules+1)*sizeof(CGI_Module *));
cgiModules[nCgiModules++] = mod;
}
/* Escape characters as described in RFC1738. */
char *
CGI_EscapeURL(CGI_Query *q, const char *url)
{
char hex[3];
size_t len;
const u_char *sp;
char *dst, *dp;
len = strlen(url)+1;
dp = dst = Malloc(len);
hex[2] = '\0';
for (sp = (const u_char *)url; *sp != '\0'; dp++, sp++) {
const char *url_unsafe = "<>\"#%{}|\\^~[]`", *p;
int unsafe = 0;
if (!isgraph(*sp)) {
unsafe++;
} else {
for (p = &url_unsafe[0]; *p != '\0'; p++) {
if (*p == *sp) {
unsafe++;
break;
}
}
}
if (unsafe) {
snprintf(hex, sizeof(hex), "%02x", *sp);
len += 3;
dst = Realloc(dst, len);
dp[0] = '%';
dp[1] = hex[0];
dp[2] = hex[1];
dp += 2;
} else {
*dp = *sp;
}
}
*dp = '\0';
return (dst);
}
/*
* Unescape characters as described in RFC1738, except for NUL sequences
* which are replaced by underscores.
*/
char *
CGI_UnescapeURL(CGI_Query *q, const char *url)
{
char hex[3];
const char *sp;
char *dst, *dp;
Uint n;
if ((dp = dst = TryMalloc(strlen(url)+2)) == NULL) {
return (NULL);
}
hex[2] = '\0';
for (sp = url; *sp != '\0'; dp++, sp++) {
if (sp[0] == '%' && isxdigit(sp[1]) && isxdigit(sp[2])) {
hex[0] = sp[1];
hex[1] = sp[2];
n = (Uint)strtoul(hex, NULL, 16);
if (n == '\0') {
*dp = '_';
} else {
*dp = n;
}
sp += 2;
} else if (sp[0] == '+') {
*dp = ' ';
} else {
*dp = sp[0];
}
}
*dp = '\0';
return (dst);
}
/* Parse application/x-www-form-urlencoded arguments. */
int
CGI_ParseForm_URLENC(CGI_Query *q, const char *qsinput, enum cgi_argument_type t)
{
CGI_Argument *arg = NULL;
char *qstring, *qs, *s;
char *name, *value;
int nargs;
if ((qs = qstring = TryStrdup(qsinput)) == NULL)
return (-1);
for (nargs = 0;
(s = Strsep(&qs, "&")) != NULL && nargs < CGI_MAX_ARGS;
nargs++) {
if ((arg = TryMalloc(sizeof(CGI_Argument))) == NULL) {
free(qstring);
return (-1);
}
if ((name = Strsep(&s, "=")) != NULL && *name != '\0') {
if (Strlcpy(arg->key, name, sizeof(arg->key)) >=
sizeof(arg->key)) {
CGI_SetError("Key is too long");
goto fail;
}
if ((value = Strsep(&s, "=")) != NULL) {
if ((arg->value = CGI_UnescapeURL(q, value)) == NULL) {
goto fail;
}
arg->len = strlen(arg->value)+1;
} else {
if ((arg->value = TryStrdup("")) == NULL) {
goto fail;
}
arg->len = 0;
}
arg->type = t;
arg->contentType[0] = '\0';
TAILQ_INSERT_TAIL(&q->args, arg, args);
q->nArgs++;
}
}
free(qstring);
return (0);
fail:
Free(arg);
free(qstring);
return (-1);
}
/*
* Parse multipart/form-data content from stdin.
* Parts may include binary content.
*/
int
CGI_ReadForm_FORMDATA(CGI_Query *q, const char *contentType)
{
char boundary[73];
char *s, *t, *tEnd, *buf, *c, *cStart, *cEnd, *cNext;
size_t lenContent, lenBoundary, lenHeader, lenPart;
CGI_Argument *arg = NULL;
int partIndex = 0;
if ((s = getenv("CONTENT_LENGTH")) == NULL ||
(lenContent = (size_t)strtol(s, NULL, 10)) == 0) {
CGI_SetError("No CONTENT_LENGTH");
return (-1);
}
if ((s = strstr(contentType, "boundary=")) == NULL ||
s[9] == '\0' ||
Strlcpy(boundary, "--", sizeof(boundary)) >= sizeof(boundary) ||
Strlcat(boundary, &s[9], sizeof(boundary)) >= sizeof(boundary)) {
CGI_SetError("Invalid boundary");
return (-1);
}
lenBoundary = strlen(boundary);
#ifdef CGI_DEBUG_ARGS
CGI_LogDebug("FormData: Content-Length: %lu", (Ulong)lenContent);
CGI_LogDebug("FormData: Boundary: \"%s\"", boundary);
#endif
if ((buf = TryMalloc(lenContent+1)) == NULL) {
return (-1);
}
if (CGI_Read(q, buf, lenContent) != 0) {
CGI_SetError("stdin: %s", strerror(errno));
goto fail;
}
buf[lenContent] = '\0';
c = &buf[0];
#ifdef CGI_DEBUG_HEADERS
{
FILE *dbgOut = fopen("debug-form.txt", "w");
if (dbgOut != NULL) {
fwrite(buf, 1, lenContent, dbgOut);
fclose(dbgOut);
}
}
#endif
while (*c != '\0') {
char contentType[64];
if (*c != '-' ||
(partIndex==0 && strncmp(c, boundary, lenBoundary) != 0)) {
c++;
continue;
}
c += lenBoundary + 2; /* + \r\n */
if ((cEnd = strchr(c,'\n')) == NULL) {
CGI_SetError("Incomplete MIME header");
goto fail;
}
cStart = &cEnd[1];
while (isspace(*cStart)) { cStart++; }
cNext = (*cEnd != '\0' && cEnd[1] == 'C') ? &cEnd[1] : NULL;
*cEnd = '\0';
/* CGI_LogDebug("at c: `%s'", c); */
/* Extract name from Content-Disposition header. */
if (strncasecmp(c, "Content-Disposition:", 20) == 0) {
s = c;
while ((t = strsep(&s, ";")) != NULL) {
while (isspace(*t)) { t++; }
if (strncmp(t, "name=\"", 6) == 0 &&
(tEnd = strchr(&t[6], '"')) != NULL) {
*tEnd = '\0';
t += 6;
break;
}
}
if (t == NULL) /* End or header without name */
break;
} else {
break;
}
if ((arg = TryMalloc(sizeof(CGI_Argument))) == NULL) {
goto fail;
}
arg->type = CGI_POST_ARGUMENT;
Strlcpy(arg->key, t, sizeof(arg->key));
/* Extract type from optional Content-Type header. */
if (cNext && strncasecmp(cNext, "Content-Type:", 13) == 0 &&
(tEnd = strchr(&cNext[13], '\r')) != NULL) {
if ((cEnd = strchr(cNext,'\n')) == NULL) {
CGI_SetError("Incomplete Content-Type header");
free(arg);
goto fail;
}
*tEnd = '\0';
Strlcpy(arg->contentType,
isspace(cNext[13]) ? &cNext[14] : &cNext[13],
sizeof(arg->contentType));
cStart = &cEnd[3]; /* 1+\r\n */
} else {
arg->contentType[0] = '\0';
}
if ((cEnd = (char *)memmem(cStart, (&buf[lenContent] - cStart),
boundary, lenBoundary)) == NULL) {
CGI_SetError("Incomplete FORM data (part #%u)", partIndex);
free(arg);
goto fail;
}
lenPart = (size_t)(cEnd - cStart);
partIndex++;
if ((arg->value = TryMalloc(lenPart+1)) == NULL) {
free(arg);
goto fail;
}
memcpy(arg->value, cStart, lenPart);
if (arg->value[lenPart-1] == '\n' && /* Strip \r\n */
arg->value[lenPart-2] == '\r') {
arg->value[lenPart-2] = '\0';
arg->len = lenPart-2+1;
} else {
arg->value[lenPart] = '\0';
arg->len = lenPart+1;
}
#ifdef CGI_DEBUG_ARGS
CGI_LogDebug("FormData: Part %d: \"%s\"=[%s]", partIndex,
arg->key, arg->value);
#endif
TAILQ_INSERT_TAIL(&q->args, arg, args);
q->nArgs++;
c = cEnd;
}
free(buf);
return (0);
fail:
free(buf);
return (-1);
}
/* Parse URL-encoded form data from stdin. */
int
CGI_ReadForm_URLENC(CGI_Query *q)
{
char *s, *buf;
size_t lenContent;
int rv;
if ((s = getenv("CONTENT_LENGTH")) == NULL ||
(lenContent = (size_t)strtol(s, NULL, 10)) == 0) {
CGI_SetError("No CONTENT_LENGTH");
return (-1);
}
#ifdef CGI_DEBUG_ARGS
CGI_LogDebug("URLEncoded: Content-Length: %lu", (Ulong)lenContent);
#endif
if ((buf = TryMalloc(lenContent+1)) == NULL) {
return (-1);
}
if (CGI_Read(q, buf, lenContent) != 0) {
CGI_SetError("stdin: %s", strerror(errno));
goto fail;
}
buf[lenContent] = '\0';
rv = CGI_ParseForm_URLENC(q, buf, CGI_POST_ARGUMENT);
free(buf);
return (rv);
fail:
free(buf);
return (-1);
}
/* Decode HTTP_COOKIES. */
static int
DecodeCookies(CGI_Query *q, char *s)
{
CGI_Cookie *ck, *ckIn = NULL;
char *sp = s, *t, *end;
#ifdef CGI_DEBUG_COOKIES
CGI_LogDebug("HTTP_COOKIE: \"%s\"", s);
#endif
while ((t = Strsep(&sp, ";")) != NULL) {
char *sKey, *sVal;
if (strchr(t, '=') != NULL) {
if ((sKey = Strsep(&t, "=")) == NULL) { continue; }
CGI_TRIM_WHITESPACE(sKey, end);
if ((sVal = Strsep(&t, "=")) != NULL) {
CGI_TRIM_WHITESPACE(sVal, end);
} else {
sVal = "";
}
if ((ck = CGI_LookupCookie(q, sKey)) == NULL) {
if (!(ck = TryMalloc(sizeof(CGI_Cookie)))) {
return (-1);
}
Strlcpy(ck->name, sKey, sizeof(ck->name));
TAILQ_INSERT_HEAD(&q->cookies, ck, cookies);
q->nCookies++;
#ifdef CGI_DEBUG_COOKIES
CGI_LogDebug("DecodeCookies: [%s]=[%s]", sKey, sVal);
#endif
}
ck->expires[0] = '\0';
ck->domain[0] = '\0';
ck->path[0] = '\0';
ck->flags = 0;
Strlcpy(ck->value, sVal, sizeof(ck->value));
ckIn = ck;
#if 1
} else if (ckIn) {
CGI_TRIM_WHITESPACE(t, end);
if (!strncasecmp(t, "Expires=", 8)) {
Strlcpy(ckIn->expires, &t[8], sizeof(ckIn->expires));
} else if (strncasecmp(t, "Domain=", 7)) {
Strlcpy(ckIn->domain, &t[7], sizeof(ckIn->domain));
} else if (strncasecmp(t, "Path=", 5)) {
Strlcpy(ckIn->path, &t[5], sizeof(ckIn->path));
} else if (strcasecmp(t, "Secure")) {
ckIn->flags |= CGI_COOKIE_SECURE;
} else if (strcasecmp(t, "HttpOnly")) {
ckIn->flags |= CGI_COOKIE_HTTPONLY;
}
}
#endif
}
return (0);
}
/* Set a cookie value. */
CGI_Cookie *
CGI_SetCookieS(CGI_Query *q, const char *name, const char *value)
{
CGI_Cookie *ck;
va_list ap;
TAILQ_FOREACH(ck, &q->cookies, cookies) {
if (strcmp(ck->name, name) == 0)
break;
}
if (ck == NULL) {
ck = Malloc(sizeof(CGI_Cookie));
Strlcpy(ck->name, name, sizeof(ck->name));
TAILQ_INSERT_HEAD(&q->cookies, ck, cookies);
q->nCookies++;
}
ck->expires[0] = '\0';
ck->domain[0] = '\0';
ck->path[0] = '\0';
ck->flags = 0;
Strlcpy(ck->value, value, sizeof(ck->value));
return (ck);
}
/* Set a cookie value (format string variant). */
CGI_Cookie *
CGI_SetCookie(CGI_Query *q, const char *name, const char *fmt, ...)
{
CGI_Cookie *ck;
va_list ap;
ck = CGI_SetCookieS(q, name, "");
va_start(ap, fmt);
vsnprintf(ck->value, sizeof(ck->value), fmt, ap);
va_end(ap);
return (ck);
}
void
CGI_DelCookie(CGI_Query *q, const char *name)
{
CGI_Cookie *ck;
ck = CGI_SetCookieS(q, name, "");
Strlcpy(ck->expires, "Thu, 01 Jan 1970 00:00:01 GMT", sizeof(ck->expires));
}
/* Register a language-switch callback function. */
void
CGI_SetLanguageFn(CGI_LanguageFn fn, void *p)
{
cgiLanguageFn = fn;
cgiLanguageFnArg = p;
}
/* Register an alternate function to construct the menu. */
void
CGI_SetMenuFn(CGI_MenuFn fn, void *p)
{
cgiMenuFn = fn;
cgiMenuFnArg = p;
}
/* Initialize a CGI_Query structure. */
void
CGI_QueryInit(CGI_Query *q)
{
q->url = NULL;
q->nAcceptLangs = 0;
q->nAcceptCharsets = 0;
TAILQ_INIT(&q->args);
q->nArgs = 0;
TAILQ_INIT(&q->cookies);
q->nCookies = 0;
q->contentType[0] = '\0';
q->contentRead = 0;
q->wrType = NULL;
Strlcpy(q->lang, cgi->availLangs[0], sizeof(q->lang));
Strlcpy(q->cset, cgi->availCharsets[0], sizeof(q->cset));
Strlcpy(q->html_dir, _PATH_HTML, sizeof(q->html_dir));
q->sess = NULL;
q->sock = -1;
}
/* Check the validity of an op string */
int
CGI_ValidOp(const char *op, const CGI_SessionOps *sessOps)
{
CGI_Module *mod;
CGI_Command *cmd;
int i;
if (strcmp(op, "logout") == 0 ||
strcmp(op, "help") == 0 ||
strcmp(op, "main_index") == 0) {
return (1);
}
if (sessOps) {
if (strcmp(op, sessOps->authChallengeName) == 0 ||
strcmp(op, sessOps->authRegisterName) == 0 ||
strcmp(op, sessOps->authRegConfirmName) == 0 ||
strcmp(op, sessOps->authAssistName) == 0 ||
strcmp(op, sessOps->authRequestName) == 0 ||
strcmp(op, sessOps->authConfirmName) == 0 ||
strcmp(op, sessOps->authCommitName) == 0 ||
strcmp(op, sessOps->authCmdName) == 0)
return (1);
}
for (i = 0; i < nCgiModules; i++) {
mod = cgiModules[i];
if (strcmp(mod->name, op) == 0) {
return (1);
}
if (strncmp(mod->name, op, strlen(mod->name)) == 0)
break;
}
if (i == nCgiModules) {
return (0);
}
for (cmd = mod->commands; cmd->name != NULL; cmd++) {
if (strcmp(cmd->name, op) == 0)
return (1);
}
return (0);
}
/*
* Parse a request from the web server and initialize the CGI_Query
* (including the "op" argument).
* Decode HTTP_ACCEPT_*, QUERY_STRING, SCRIPT_NAME and HTTP_COOKIE.
* Read form data from stdin if CONTENT_TYPE dictates it.
*/
int
CGI_QueryReadHTTP(CGI_Query *q, const CGI_SessionOps *sessOps)
{
char *oklang, *okcset, *cookies, *pOklang, *pOkcset, **lang, **cset;
const char *rlang, *op;
char *s, *sep, **ap;
CGI_Argument *arg;
VAR *v;
/* Parse HTTP request language and character set. */
if ((s = getenv("HTTP_ACCEPT_LANGUAGE")) != NULL) {
if ((pOklang = oklang = TryStrdup(s)) == NULL) {
return (-1);
}
for (q->nAcceptLangs = 0, ap = &q->acceptLangs[0];
q->nAcceptLangs < CGI_LANGS_MAX && (s = Strsep(&oklang, ","));
q->nAcceptLangs++, ap++) {
if ((*ap = TryStrdup(s)) == NULL) {
free(pOklang);
return (-1);
}
if ((sep = strchr(*ap, ';')) != NULL)
*sep = '\0';
}
*ap = NULL;
free(pOklang);
}
if ((s = getenv("HTTP_ACCEPT_CHARSET")) != NULL) {
if ((pOkcset = okcset = TryStrdup(s)) == NULL) {
return (-1);
}
for (q->nAcceptCharsets = 0, ap = &q->acceptCharsets[0];
q->nAcceptCharsets < CGI_CHARSETS_MAX && (s = Strsep(&okcset, ","));
q->nAcceptCharsets++, ap++) {
if ((*ap = TryStrdup(s)) == NULL) {
free(pOkcset);
return (-1);
}
if ((sep = strchr(*ap, ';')) != NULL)
*sep = '\0';
}
*ap = NULL;
free(pOkcset);
}
/* Set script URL. */
if ((s = getenv("SCRIPT_NAME")) != NULL) {
q->url = TryStrdup(s);
} else {
q->url = TryStrdup("");
}
if (q->url == NULL)
return (-1);
/* Decode the HTTP cookies. */
if ((s = getenv("HTTP_COOKIE")) != NULL && *s != '\0') {
if ((cookies = TryStrdup(s)) == NULL) {
return (-1);
}
if ((DecodeCookies(q, cookies)) == -1) {
CGI_SetError("HTTP_COOKIE: %s", CGI_GetError());
free(cookies);
return (-1);
}
free(cookies);
}
/* Decode input data based on Content-Type. */
if ((s = getenv("CONTENT_TYPE")) != NULL) {
#ifdef CGI_DEBUG_ARGS
CGI_LogDebug("Client Content-Type: \"%s\"", s);
#endif
if (strncmp(s, "application/x-www-form-urlencoded", strlen("application/x-www-form-urlencoded")) == 0 ||
strncmp(s, "application/json", strlen("application/json")) == 0) {
if (CGI_ReadForm_URLENC(q) == -1) {
CGI_LogErr("[urlenc]: %s", CGI_GetError());
return (-1);
}
q->contentRead = 1;
} else if (strncmp(s, "multipart/form-data", strlen("multipart/form-data")) == 0) {
if (CGI_ReadForm_FORMDATA(q, s) == -1) {
CGI_LogErr("[multipart]: %s", CGI_GetError());
return (-1);
}
q->contentRead = 1;
}
Strlcpy(q->contentType, s, sizeof(q->contentType));
} else {
q->contentRead = 1;
}
/* Decode URL */
if (cgi->flags & CGI_PRETTY_URL) {
if (((s = getenv("SCRIPT_FILENAME")) != NULL && *s == '/') ||
((s = getenv("SCRIPT_NAME")) != NULL && *s == '/')) {
char *c, cOrig, *path, *pathArgs, *pc;
int nonSp = 0;
if ((path = TryStrdup(&s[1])) == NULL)
return (-1);
if ((c = strpbrk(path, "?&/")) != NULL) {
pathArgs = &c[1];
*c = '\0';
} else {
pathArgs = "";
}
if (strlen(path) >= CGI_OPNAME_MAX) {
CGI_SetError("Too long op");
return (-1);
}
for (pc = path; *pc != '\0'; pc++) {
if (!isalnum(*pc) && !strchr("_-.", *pc)) {
CGI_SetError("Malformed op: \"%s\"", path);
return (-1);
}
if (!isspace(*pc)) { nonSp = 1; }
}
/* XXX double set? */
if (!nonSp) {
CGI_SetS(q, "op", cgi->homeOp);
} else {
if (CGI_ValidOp(path, sessOps) && nonSp) {
CGI_SetS(q, "op", path);
} else {
CGI_LogErr("Bad op \"%s\"; ignored", path);
CGI_SetS(q, "op", cgi->homeOp);
}
}
if (pathArgs[0] != '\0') {
if (strchr(pathArgs,'=')) {
if (CGI_ParseForm_URLENC(q, pathArgs,
CGI_GET_ARGUMENT) == -1) {
CGI_SetError("QUERY_STRING: %s", CGI_GetError());
return (-1);
}
} else {
CGI_SetS(q, "_argument", pathArgs);
}
}
}
} else {
/*
* Use standard URLs (like /prog.fcgi?op=op&a1=val2&a2=val2).
*/
if ((s = getenv("QUERY_STRING")) != NULL && *s != '\0') {
if (CGI_ParseForm_URLENC(q, s, CGI_GET_ARGUMENT) == -1) {
CGI_SetError("QUERY_STRING: %s", CGI_GetError());
return (-1);
}
}
}
if (!(op = CGI_Get(q, "op", CGI_OPNAME_MAX))) {
CGI_SetS(q, "op", cgi->homeOp);
}
return (0);
}
/* Set q->lang and q->cset per HTTP negotiation. */
static void
NegotiateLanguageHTTP(CGI_Query *q)
{
char **t;
const char **s;
const char *lang;
Uint i;
for (i = 0; i < q->nAcceptCharsets; i++) {
for (t = &cgi->availCharsets[0]; *t != NULL; t++) {
if (strcasecmp(q->acceptCharsets[i], *t) == 0)
break;
}
if (*t != NULL) {
Strlcpy(q->cset, *t, sizeof(q->cset));
break;
}
}
for (i = 0; i < q->nAcceptLangs; i++) {
for (s = &cgi->availLangs[0]; *s != NULL; s++) {
if (strcasecmp(q->acceptLangs[i], *s) == 0)
break;
}
if (*s != NULL) {
Strlcpy(q->lang, *s, sizeof(q->lang));
break;
}
}
}
/*
* Initialize a basic environment for processing unauthenticated queries.
*/
void
CGI_BeginQueryUnauth(CGI_Query *q, const char *op, CGI_SessionOps *sessOps)
{
HTML_SetDirectory(q, _PATH_HTML);
CGI_ClearCookies(q);
/* Set the language per HTTP negotiation. */
NegotiateLanguageHTTP(q);
if (cgiLanguageFn != NULL)
cgiLanguageFn(q->lang, cgiLanguageFnArg);
if (sessOps->beginQueryUnauth != NULL) {
sessOps->beginQueryUnauth(q, op);
} else {
SetS("_error", "");
SetS("_user", "(nobody)");
SetS("_theme", "default");
SetS("_lang", q->lang);
if (cgi->urlBase == NULL) { /* Assume .fcgi */
char *urlbase, *c;
urlbase = Strdup(q->url);
if ((c = strstr(urlbase, ".fcgi")) != NULL)
c[5] = '\0';
SetS("_prog", urlbase);
Set("_modules",
""
""
"%s %s"
"",
urlbase, CGI_GLYPHICON(log-in), _("Log in"));
free(urlbase);
} else {
SetS("_prog", cgi->urlBase);
Set("_modules",
""
""
"%s %s"
"",
cgi->urlBase, CGI_GLYPHICON(log-in), _("Log in"));
}
}
}
static int
ProcessHelpQuery(CGI_Query *q)
{
char topicLower[CGI_HELP_TOPIC_MAX], *d;
const char *topic, *c;
CGI_WriteHeaderHTML(q);
if ((topic = CGI_Get(q, "topic", CGI_HELP_TOPIC_MAX)) == NULL) {
return (-1);
}
for (c = topic, d = topicLower;
*c != '\0';
c++, d++) {
if (!isalnum(*c) && *c != '_' && *c != '-') {
return (-1);
}
*d = tolower(*c);
}
*d = '\0';
HTML_SetDirectory(q, _PATH_HELP_HTML);
return HTML_Output(q, topicLower);
}
/* Initialize the environment for processing a query. */
void
CGI_BeginQuery(CGI_Query *q, const char *user)
{
char key[CGI_SESSION_VAR_KEY_MAX+1];
CGI_Session *sess = q->sess;
CGI_SessionVar *sv;
const char *s, *op, *c;
char **lang;
VAR *v;
int i;
if ((op = CGI_Get(q, "op", CGI_OPNAME_MAX)) != NULL) {
if (!CGI_ValidOp(op, sess->ops)) {
CGI_LogErr("Bad op \"%s\"; ignored", op);
op = cgi->homeOp;
}
}
SetS("op", op);
HTML_SetDirectory(q, _PATH_HTML);
/* Set the language per current session setting. */
Strlcpy(q->lang, GetSV(sess,"language"), sizeof(q->lang));
Strlcpy(q->cset, GetSV(sess,"character-set"), sizeof(q->cset));
if (cgiLanguageFn != NULL)
cgiLanguageFn(q->lang, cgiLanguageFnArg);
SetS("_error", "");
SetS("_user", user);
SetS("_lang", q->lang);
SetS("_prog", q->url);
if ((s = GetSV(sess,"theme")) != NULL) {
SetS("_theme", s);
} else {
SetS("_theme", "default");
}
/* Set useful substitution variables. */
SetS("_sess", sess->id);
key[0] = '_';
TAILQ_FOREACH(sv, &sess->vars, vars) {
if (strcmp(sv->key, "pass") == 0) {
continue;
}
Strlcpy(&key[1], sv->key, sizeof(key)-1);
SetS(key, sv->value);
}
/* Generate the navigation menu. */
v = SetS("_modules", NULL);
if (cgiMenuFn != NULL) { /* Custom */
cgiMenuFn(q, v, cgiMenuFnArg);
return;
}
for (i = 0; i < nCgiModules; i++) {
CGI_Module *mod = cgiModules[i];
CGI_Section *sec;
if (mod->menu != NULL) {
mod->menu(q, v);
} else if (mod->sections == NULL) {
if (cgi->flags & CGI_PRETTY_URL) {
Cat(v, "%s%s",
mod->name, mod->icon, gettext(mod->lname));
} else {
Cat(v, "%s%s",
q->url, mod->name, mod->icon,
gettext(mod->lname));
}
} else {
if (mod->sections[0].name != NULL) {
CatS(v,
""
"");
CatS(v, mod->icon);
CatS(v, gettext(mod->lname));
CatS(v, ""
""
"");
}
}
}
if (cgi->flags & CGI_PRETTY_URL) {
Cat(v,
"%s %s",
CGI_GLYPHICON(log-out),
_("Logout"));
} else {
Cat(v,
"%s %s",
q->url,
CGI_GLYPHICON(log-out),
_("Logout"));
}
}
/* Execute a module command (in worker process). */
int
CGI_ExecQuery(CGI_Query *q, CGI_SessionOps *sessOps)
{
const char *op, *c;
CGI_Module *mod = NULL; /* compiler happy */
CGI_Command *cmd;
VAR *v;
int i;
if ((op = CGI_Get(q, "op", CGI_OPNAME_MAX)) != NULL) {
for (c = op; *c != '\0'; c++) {
if (!isalnum(*c) && *c != '_' && *c != '-' && *c != '.') {
CGI_SetError("Malformed op");
return (-1);
}
}
} else {
op = cgi->homeOp;
}
/* Process "logout" and "help" queries specially. */
if (strcmp(op, "logout") == 0) {
CGI_Cookie *ck;
ck = CGI_SetCookieS(q, "sess", "");
ck->path[0] = '/';
ck->path[1] = '\0';
sessOps->logout(q);
CGI_CloseSession(q->sess);
termFlag = 1;
return (0);
} else if (strcmp(op, "help") == 0) {
if (ProcessHelpQuery(q) == -1) {
goto fail;
}
return (0);
}
/* Search for a module based on the operation prefix. */
for (i = 0; i < nCgiModules; i++) {
mod = cgiModules[i];
if (strncmp(mod->name, op, strlen(mod->name)) == 0)
break;
}
if (i == nCgiModules) {
CGI_SetError("No such operation");
goto fail;
}
CGI_SetError("Undefined parameter");
/* Alias the module name to the index operation. */
if (strcmp(mod->name, op) == 0 && mod->indexFn != NULL) {
for (cmd = mod->commands; cmd->name != NULL; cmd++) {
if (cmd->fn == mod->indexFn) {
op = cmd->name;
break;
}
}
}
/* General command execution. */
for (cmd = mod->commands; cmd->name != NULL; cmd++) {
if (strcmp(cmd->name, op) != 0) {
continue;
}
if (cmd->type != NULL && strcmp(cmd->type, "[json-status]")==0) {
CGI_WriteHeader(q, "application/json", "utf8", 0);
if (cmd->fn(q) == 0) {
CGI_PutS(q, "{\"code\": 0}\r\n");
return (0);
} else {
CGI_PutS(q, "{\"code\": -1,");
CGI_PutJSON_NoHTML_S(q, "error", CGI_GetError());
CGI_PutS(q, "\"backend_version\": \"" VERSION "\"}\r\n");
CGI_LogErr("%s (JSON)", CGI_GetError());
return (-1);
}
} else if (cmd->type != NULL && strcmp(cmd->type, "[json]")==0) {
CGI_WriteHeader(q, "application/json", "utf8", 0);
CGI_PutS(q, "{\"lang\": \"");
CGI_PutS(q, q->lang);
CGI_PutS(q, "\",");
if (cmd->fn(q) == 0) {
CGI_PutS(q, "\"code\": 0}\r\n");
return (0);
} else {
CGI_PutS(q, "\"code\": -1,");
CGI_PutJSON_NoHTML_S(q, "error", CGI_GetError());
CGI_PutS(q, "\"backend_version\": \"" VERSION "\"}\r\n");
CGI_LogErr("%s (JSON)", CGI_GetError());
return (-1);
}
} else {
if (cmd->type != NULL) {
CGI_WriteHeader(q, cmd->type, NULL, 1);
}
if (cmd->fn(q) == 0) {
return (0);
} else {
goto fail;
}
}
}
if (cmd->name == NULL) {
CGI_SetError("No such operation");
goto fail;
}
return (0);
fail:
if (q->wrType == NULL) {
CGI_WriteHeaderHTML(q);
HTML_OutputError(q, "%s", CGI_GetError());
} else if (strcmp(q->wrType, "text/html") == 0) {
HTML_OutputError(q, "%s", CGI_GetError());
}
CGI_LogS(CGI_LOG_ERR, CGI_GetError());
return (-1);
}
/*
* Write a standard HTTP header for the output.
* One may also write different headers using MIME_Entity(3) and MIME_Write(3).
*/
void
CGI_WriteHeader(CGI_Query *q, const char *contentType, const char *charset,
int setCookies)
{
CGI_Cookie *ck;
CGI_PutS(q, "Content-type: ");
CGI_PutS(q, contentType);
if (charset != NULL) {
CGI_PutS(q, "; charset=");
CGI_PutS(q, charset);
}
CGI_PutS(q, "\r\n"
"Cache-Control: no-cache, no-store, must-revalidate\r\n"
"Expires: 0\r\n");
if (setCookies) {
TAILQ_FOREACH(ck, &q->cookies, cookies) {
CGI_Printf(q, "Set-Cookie: %s=%s", ck->name, ck->value);
if (ck->expires[0] != '\0') {
CGI_Printf(q, "; Expires=%s", ck->expires);
}
if (ck->domain[0] != '\0') {
CGI_Printf(q, "; Domain=%s", ck->domain);
}
if (ck->path[0] != '\0') {
CGI_Printf(q, "; Path=%s", ck->path);
}
if (ck->flags & CGI_COOKIE_SECURE) {
CGI_PutS(q, "; Secure");
}
if (ck->flags & CGI_COOKIE_HTTPONLY) {
CGI_PutS(q, "; HttpOnly");
}
CGI_PutS(q, "; SameSite=Strict\r\n");
}
}
CGI_PutS(q, "\r\n");
q->wrType = contentType;
}
/* Return 1 if an argument exists and is nonempty. */
int
CGI_GetBool(CGI_Query *q, const char *key)
{
CGI_Argument *arg;
TAILQ_FOREACH(arg, &q->args, args) {
if (strcmp(arg->key, key) == 0)
break;
}
if (arg == NULL) {
return (0);
}
return (*arg->value != '\0');
}
/* Validate an integer argument and return its value into pRet. */
int
CGI_GetInt(CGI_Query *q, const char *key, int *pRet)
{
CGI_Argument *arg;
char *ep;
long rv;
TAILQ_FOREACH(arg, &q->args, args) {
if (strcmp(arg->key, key) == 0)
break;
}
if (arg == NULL) {
CGI_SetError("Argument \"%s\" is missing", key);
return (-1);
}
errno = 0;
rv = strtol(arg->value, &ep, 10);
if (arg->value[0] == '\0' || *ep != '\0') {
CGI_SetError("%s: Not a number", key);
return (-1);
}
if ((errno == ERANGE) &&
((rv == LONG_MAX || rv == LONG_MIN) ||
(rv > INT_MAX || rv < INT_MIN))) {
CGI_SetError("%s: Out of range", key);
return (-1);
}
*pRet = (int)rv;
return (0);
}
int
CGI_GetUint(CGI_Query *q, const char *key, Uint *pRet)
{
CGI_Argument *arg;
char *ep;
unsigned long rv;
TAILQ_FOREACH(arg, &q->args, args) {
if (strcmp(arg->key, key) == 0)
break;
}
if (arg == NULL) {
CGI_SetError("Argument \"%s\" is missing", key);
return (-1);
}
errno = 0;
rv = strtoul(arg->value, &ep, 10);
if (arg->value[0] == '\0' || *ep != '\0') {
CGI_SetError("%s: Not a number", key);
return (-1);
}
if (errno == ERANGE && rv == ULONG_MAX) {
CGI_SetError("%s: Out of range", key);
return (-1);
}
*pRet = (Uint)rv;
return (0);
}
int
CGI_GetUint64(CGI_Query *q, const char *key, Uint64 *pRet)
{
CGI_Argument *arg;
char *ep;
unsigned long long rv;
TAILQ_FOREACH(arg, &q->args, args) {
if (strcmp(arg->key, key) == 0)
break;
}
if (arg == NULL) {
CGI_SetError("Argument \"%s\" is missing", key);
return (-1);
}
errno = 0;
rv = strtoull(arg->value, &ep, 10);
if (arg->value[0] == '\0' || *ep != '\0') {
CGI_SetError("%s: Not a number", key);
return (-1);
}
if (errno == ERANGE && rv == ULLONG_MAX) {
CGI_SetError("%s: Out of range", key);
return (-1);
}
*pRet = (Uint64)rv;
return (0);
}
int
CGI_GetSint64(CGI_Query *q, const char *key, Sint64 *pRet)
{
CGI_Argument *arg;
char *ep;
long long rv;
TAILQ_FOREACH(arg, &q->args, args) {
if (strcmp(arg->key, key) == 0)
break;
}
if (arg == NULL) {
CGI_SetError("Argument \"%s\" is missing", key);
return (-1);
}
errno = 0;
rv = strtoll(arg->value, &ep, 10);
if (arg->value[0] == '\0' || *ep != '\0') {
CGI_SetError("%s: Not a number", key);
return (-1);
}
if (errno == ERANGE && rv == LLONG_MAX) {
CGI_SetError("%s: Out of range", key);
return (-1);
}
*pRet = (Sint64)rv;
return (0);
}
/* Version of CGI_GetInt() with range-checking. */
int
CGI_GetIntR(CGI_Query *q, const char *key, int *pRet, int min, int max)
{
CGI_Argument *arg;
char *ep;
long rv;
TAILQ_FOREACH(arg, &q->args, args) {
if (strcmp(arg->key, key) == 0)
break;
}
if (arg == NULL) {
CGI_SetError("Argument \"%s\" is missing", key);
return (-1);
}
errno = 0;
rv = strtol(arg->value, &ep, 10);
if (arg->value[0] == '\0' || *ep != '\0') {
CGI_SetError("%s: Not a number", key);
return (-1);
}
if ((errno == ERANGE) && ((rv == LONG_MAX || rv == LONG_MIN) ||
(rv > INT_MAX || rv < INT_MIN))) {
CGI_SetError("%s: Out of range", key);
return (-1);
}
if (rv < min || rv > max) {
CGI_SetError("%s: Out of range (%d-%d)", key, min, max);
return (-1);
}
*pRet = (int)rv;
return (0);
}
int
CGI_GetUintR(CGI_Query *q, const char *key, Uint *pRet, Uint max)
{
CGI_Argument *arg;
char *ep;
unsigned long rv;
TAILQ_FOREACH(arg, &q->args, args) {
if (strcmp(arg->key, key) == 0)
break;
}
if (arg == NULL) {
CGI_SetError("Argument \"%s\" is missing", key);
return (-1);
}
errno = 0;
rv = strtoul(arg->value, &ep, 10);
if (arg->value[0] == '\0' || *ep != '\0') {
CGI_SetError("%s: Not a number", key);
return (-1);
}
if ((errno == ERANGE) && ((rv == UINT_MAX) || (rv > UINT_MAX))) {
CGI_SetError("%s: Out of range", key);
return (-1);
}
if (rv > max) {
CGI_SetError("%s: Out of range (0-%d)", key, max);
return (-1);
}
*pRet = (Uint)rv;
return (0);
}
/*
* Obtain a range bounded by two integers (or a single number, in which case
* set pMin=pMax).
*/
int
CGI_GetIntRange(CGI_Query *q, const char *key, int *pMin, const char *sep,
int *pMax)
{
char buf[CGI_INT_RANGE_MAX];
char *val, *c, *ep, *rangeFrom, *rangeTo, *t;
CGI_Argument *arg;
int nSeps;
TAILQ_FOREACH(arg, &q->args, args) {
if (strcmp(arg->key, key) == 0)
break;
}
if (arg == NULL) {
CGI_SetError("Argument \"%s\" is missing", key);
return (-1);
}
val = arg->value;
if (strpbrk(val, sep)) { /* Is a range */
for (c=val, nSeps=0; *c != '\0'; c++) {
if (!isdigit(*c) && !isspace(*c) && !strchr(sep,*c)) {
break;
}
if (strchr(sep,*c) && ++nSeps > 1) { break; }
}
if (*c != '\0') {
CGI_SetError("%s: Invalid range (near \"%s\")", key, c);
return (-1);
}
Strlcpy(buf, val, sizeof(buf));
t = buf;
if ((rangeFrom = strsep(&t,sep)) && *rangeFrom != '\0' &&
(rangeTo = strsep(&t,sep)) && *rangeTo != '\0') {
*pMin = (int)strtol(rangeFrom, NULL, 10);
*pMax = (int)strtol(rangeTo, NULL, 10);
}
} else {
*pMin = *pMax = (int)strtol(val, NULL, 10);
}
if (*pMin > *pMax) {
CGI_SetError("%s: Invalid range (%d > %d)", key, *pMin, *pMax);
return (-1);
}
return (0);
}
/* Version of CGI_GetIntR() with range-checking and min=0. */
int
CGI_GetEnum(CGI_Query *q, const char *key, Uint *pRet, Uint last)
{
CGI_Argument *arg;
char *ep;
long rv;
TAILQ_FOREACH(arg, &q->args, args) {
if (strcmp(arg->key, key) == 0)
break;
}
if (arg == NULL) {
CGI_SetError("Argument \"%s\" is missing", key);
return (-1);
}
errno = 0;
rv = strtol(arg->value, &ep, 10);
if (arg->value[0] == '\0' || *ep != '\0') {
CGI_SetError("%s: Not a number", key);
return (-1);
}
if ((errno == ERANGE) && ((rv == LONG_MAX || rv == LONG_MIN) ||
(rv > INT_MAX || rv < INT_MIN))) {
CGI_SetError("%s: Out of range", key);
return (-1);
}
if (rv < 0 || rv > last) {
CGI_SetError("%s: Out of range (last=%u)", key, last-1);
return (-1);
}
*pRet = (Uint)rv;
return (0);
}
/* Validate a floating-point argument and return its value into pRet. */
int
CGI_GetFloat(CGI_Query *q, const char *key, float *pRet)
{
CGI_Argument *arg;
char *ep;
float rv;
TAILQ_FOREACH(arg, &q->args, args) {
if (strcmp(arg->key, key) == 0)
break;
}
if (arg == NULL) {
CGI_SetError("Argument \"%s\" is missing", key);
return (-1);
}
errno = 0;
rv = strtof(arg->value, &ep);
if (arg->value[0] == '\0' || *ep != '\0') {
CGI_SetError("%s: Not a number", key);
return (-1);
}
if (errno == ERANGE) {
CGI_SetError("%s: Out of range", key);
return (-1);
}
*pRet = rv;
return (0);
}
/* Validate a double-precision argument and return its value into pRet. */
int
CGI_GetDouble(CGI_Query *q, const char *key, double *pRet)
{
CGI_Argument *arg;
char *ep;
double rv;
TAILQ_FOREACH(arg, &q->args, args) {
if (strcmp(arg->key, key) == 0)
break;
}
if (arg == NULL) {
CGI_SetError("Argument \"%s\" is missing", key);
return (-1);
}
errno = 0;
rv = strtod(arg->value, &ep);
if (arg->value[0] == '\0' || *ep != '\0') {
CGI_SetError("%s: Not a number", key);
return (-1);
}
if (errno == ERANGE) {
CGI_SetError("%s: Out of range", key);
return (-1);
}
*pRet = rv;
return (0);
}
/*
* Modify the value associated with a cgi argument. If the argument does
* not exist, create it.
*/
void
CGI_SetS(CGI_Query *q, const char *key, const char *val)
{
CGI_Argument *arg;
TAILQ_FOREACH(arg, &q->args, args) {
if (strcmp(arg->key, key) == 0)
break;
}
if (arg == NULL) {
arg = Malloc(sizeof(CGI_Argument));
arg->type = CGI_GET_ARGUMENT;
arg->contentType[0] = '\0';
Strlcpy(arg->key, key, sizeof(arg->key));
TAILQ_INSERT_TAIL(&q->args, arg, args);
q->nArgs++;
} else {
if (arg->value != NULL)
free(arg->value);
}
arg->value = (val != NULL) ? Strdup(val) : Strdup("");
arg->len = strlen(val)+1;
}
/*
* Modify the value associated with a cgi argument. If the argument does
* not exist, create it.
*/
void
CGI_Set(CGI_Query *q, const char *key, const char *fmt, ...)
{
CGI_Argument *arg;
va_list ap;
TAILQ_FOREACH(arg, &q->args, args) {
if (strcmp(arg->key, key) == 0)
break;
}
if (arg == NULL) {
arg = Malloc(sizeof(CGI_Argument));
arg->type = CGI_GET_ARGUMENT;
arg->contentType[0] = '\0';
Strlcpy(arg->key, key, sizeof(arg->key));
TAILQ_INSERT_TAIL(&q->args, arg, args);
q->nArgs++;
} else {
if (arg->value != NULL)
free(arg->value);
}
if (fmt != NULL) {
va_start(ap, fmt);
if (vasprintf(&arg->value, fmt, ap) == -1) {
CGI_OutOfMem();
}
va_end(ap);
arg->len = strlen(arg->value)+1;
} else {
arg->value = NULL;
arg->len = 0;
}
}
/* Remove the given argument. */
int
CGI_Unset(CGI_Query *q, const char *key)
{
CGI_Argument *arg;
TAILQ_FOREACH(arg, &q->args, args) {
if (strcmp(arg->key, key) == 0)
break;
}
if (arg == NULL) {
CGI_SetError("%s: No such argument", key);
return (-1);
}
TAILQ_REMOVE(&q->args, arg, args);
q->nArgs--;
free(arg->value);
free(arg);
return (0);
}
/*
* Get a pointer to a C string argument. Return NULL if the named
* argument is undefined or exceeds len-1 bytes.
*/
const char *
CGI_Get(CGI_Query *q, const char *key, size_t len)
{
CGI_Argument *arg;
TAILQ_FOREACH(arg, &q->args, args) {
if (strcmp(arg->key, key) == 0)
break;
}
if (arg == NULL) {
CGI_SetError("Argument \"%s\" is missing", key);
return (NULL);
}
if (arg->len > len) {
CGI_SetError("Argument \"%s\": Exceeds %lu characters", key,
(unsigned long)len);
return (NULL);
}
return (arg->value);
}
/*
* Variant of CGI_Get() which trims leading and ending whitespaces off
* the value of the argument. Size checking is performed on the trimmed string.
*/
const char *
CGI_GetTrim(CGI_Query *q, const char *key, size_t len)
{
CGI_Argument *arg;
char *s, *end;
TAILQ_FOREACH(arg, &q->args, args) {
if (strcmp(arg->key, key) == 0)
break;
}
if (arg == NULL || (s = arg->value) == NULL) {
CGI_SetError("Argument \"%s\" is missing", key);
return (NULL);
}
CGI_TRIM_WHITESPACE(s, end);
if (strlen(s) >= len) {
CGI_SetError("Argument \"%s\": Exceeds %lu characters", key,
(unsigned long)len);
return (NULL);
}
return (s);
}
void
CGI_ClearCookies(CGI_Query *q)
{
CGI_Cookie *c, *nc;
for (c = TAILQ_FIRST(&q->cookies);
c != TAILQ_END(&q->cookies);
c = nc) {
nc = TAILQ_NEXT(c, cookies);
free(c);
}
TAILQ_INIT(&q->cookies);
}
void
CGI_PutJSON(CGI_Query *q, const char *key, const char *fmt, ...)
{
char *val;
va_list ap;
va_start(ap, fmt);
if (vasprintf(&val, fmt, ap) == -1) {
CGI_OutOfMem();
}
va_end(ap);
CGI_PutJSON_S(q, key, val);
free(val);
}
/* Release the resources allocated by a CGI query and check for signals. */
void
CGI_QueryDestroy(CGI_Query *q)
{
CGI_Argument *arg, *narg;
VAR *var, *nvar;
int i;
CGI_ClearCookies(q);
Free(q->url);
q->url = NULL;
for (i = 0; i < q->nAcceptLangs; i++) {
Free(q->acceptLangs[i]);
q->acceptLangs[i] = NULL;
}
q->nAcceptLangs = 0;
for (i = 0; i < q->nAcceptCharsets; i++) {
Free(q->acceptCharsets[i]);
q->acceptCharsets[i] = NULL;
}
q->nAcceptCharsets = 0;
for (arg = TAILQ_FIRST(&q->args);
arg != TAILQ_END(&q->args);
arg = narg) {
narg = TAILQ_NEXT(arg, args);
free(arg->value);
free(arg);
}
TAILQ_INIT(&q->args);
q->nArgs = 0;
}
/*
* Serialize a PerCGI Query (i.e., the fields of CGI_Query pertaining to
* the client request).
*/
int
CGI_QuerySave(int fd, const CGI_Query *q)
{
char *s;
Uint i;
CGI_Argument *arg;
CGI_Cookie *ck;
/* Write script URL */
SYS_WriteString(fd, q->url);
/* Write accepted languages and charsets */
SYS_WriteUint32(fd, (Uint32)q->nAcceptLangs);
for (i = 0; i < q->nAcceptLangs; i++) {
SYS_WriteString(fd, q->acceptLangs[i]);
}
SYS_WriteUint32(fd, (Uint32)q->nAcceptCharsets);
for (i = 0; i < q->nAcceptCharsets; i++)
SYS_WriteString(fd, q->acceptCharsets[i]);
/* Write query arguments */
SYS_WriteUint32(fd, (Uint32)q->nArgs);
TAILQ_FOREACH(arg, &q->args, args) {
SYS_WriteUint8(fd, (Uint8)arg->type);
SYS_WriteString(fd, arg->contentType);
SYS_WriteString(fd, arg->key);
SYS_WriteUint32(fd, (Uint32)arg->len);
SYS_Write(fd, arg->value, arg->len);
}
/* Write cookie information */
SYS_WriteUint32(fd, (Uint32)q->nCookies);
TAILQ_FOREACH(ck, &q->cookies, cookies) {
SYS_WriteString(fd, ck->name);
SYS_WriteString(fd, ck->value);
SYS_WriteString(fd, ck->expires);
SYS_WriteString(fd, ck->domain);
SYS_WriteString(fd, ck->path);
SYS_WriteUint32(fd, ck->flags);
}
/* Write client content information */
SYS_WriteString(fd, q->contentType);
SYS_WriteUint8(fd, (Uint8)q->contentRead);
return (0);
}
/* Unserialize a PerCGI Query. */
int
CGI_QueryLoad(int fd, CGI_Query *q)
{
Uint count;
int i;
/* Read script URL */
if ((q->url = SYS_ReadStringLen(fd, CGI_URL_MAX)) == NULL)
goto fail;
/* Read accepted languages and charsets. */
if ((count = SYS_ReadUint32(fd)) > CGI_LANGS_MAX) {
CGI_SetError("Too many languages");
goto fail;
}
q->nAcceptLangs = 0;
for (i = 0; i < count; i++) {
if ((q->acceptLangs[i] =
SYS_ReadStringLen(fd, CGI_LANG_CODE_MAX)) == NULL) {
goto fail;
}
q->nAcceptLangs++;
}
if ((count = SYS_ReadUint32(fd)) > CGI_CHARSETS_MAX) {
CGI_SetError("Too many charsets");
goto fail;
}
for (i = 0; i < count; i++) {
if ((q->acceptCharsets[i] =
SYS_ReadStringLen(fd, CGI_CHARSET_NAME_MAX)) == NULL) {
goto fail;
}
q->nAcceptCharsets++;
}
q->acceptCharsets[q->nAcceptCharsets] = NULL;
/* Read query arguments */
if ((count = SYS_ReadUint32(fd)) > CGI_MAX_ARGS) {
CGI_SetError("Too many args");
goto fail;
}
for (i = 0; i < count; i++) {
CGI_Argument *arg;
if ((arg = TryMalloc(sizeof(CGI_Argument))) == NULL) {
goto fail;
}
arg->type = (enum cgi_argument_type)SYS_ReadUint8(fd);
if (SYS_CopyString(arg->contentType, fd, sizeof(arg->contentType)) == -1 ||
SYS_CopyString(arg->key, fd, sizeof(arg->key)) == -1) {
free(arg);
goto fail;
}
if ((arg->len = SYS_ReadUint32(fd)) > CGI_ARG_LENGTH_MAX ||
(arg->value = TryMalloc(arg->len)) == NULL) {
CGI_SetError("Argument is too long");
free(arg);
goto fail;
}
if (SYS_Read(fd, arg->value, arg->len) == -1) {
free(arg->value);
free(arg);
goto fail;
}
TAILQ_INSERT_TAIL(&q->args, arg, args);
q->nArgs++;
}
/* Read cookie information */
if ((count = SYS_ReadUint32(fd)) > CGI_MAX_COOKIES) {
CGI_SetError("Too many cookies");
goto fail;
}
for (i = 0; i < count; i++) {
CGI_Cookie *ck;
Uint32 flags;
if ((ck = TryMalloc(sizeof(CGI_Cookie))) == NULL) {
goto fail;
}
if (SYS_CopyString(ck->name, fd, sizeof(ck->name)) == -1 ||
SYS_CopyString(ck->value, fd, sizeof(ck->value)) == -1 ||
SYS_CopyString(ck->expires, fd, sizeof(ck->expires)) == -1 ||
SYS_CopyString(ck->domain, fd, sizeof(ck->domain)) == -1 ||
SYS_CopyString(ck->path, fd, sizeof(ck->path)) == -1 ||
SYS_ReadUint32v(fd, &ck->flags) == -1) {
free(ck);
goto fail;
}
TAILQ_INSERT_TAIL(&q->cookies, ck, cookies);
#ifdef CGI_DEBUG_COOKIES
CGI_LogDebug("QueryLoad: Read cookie: [%s]=[%s], 0x%x",
ck->name, ck->value, ck->flags);
#endif
q->nCookies++;
}
/* Read client content information */
SYS_CopyString(q->contentType, fd, sizeof(q->contentType));
q->contentRead = (int)SYS_ReadUint8(fd);
return (0);
fail:
return (-1);
}
static void
SigTERM(int sigraised)
{
termFlag++;
}
static void
SigCHLD(int sigraised)
{
chldFlag++;
}
void
CGI_CheckSignals(void)
{
if (termFlag) {
CGI_Exit(0, "SIGTERM");
}
if (chldFlag) {
pid_t pid;
int status;
chldFlag = 0;
do {
if ((pid = waitpid(WAIT_ANY, &status, WNOHANG)) <= 0) {
continue;
}
if (WIFSIGNALED(status)) {
CGI_LogNotice("Subprocess %d terminated "
"(signal %d)", pid,
WTERMSIG(status));
} else if (WIFEXITED(status)) {
if (WEXITSTATUS(status) != 0) {
CGI_LogNotice("Subprocess %d terminated "
"(exit %d)", pid,
WEXITSTATUS(status));
}
}
} while (pid > 0 || (pid == -1 && errno == EINTR));
}
}
/* Initialize the CGI library. */
void
CGI_Init(CGI_Application *pcgi)
{
extern CGI_Filter cgiVarSubstFilter;
extern char *__progname;
char *sCharsets, *s, *pCharset;
struct sigaction sa;
int i;
cgi = pcgi;
Strlcpy(cgiLogFile, __progname, sizeof(cgiLogFile));
Strlcat(cgiLogFile, ".log", sizeof(cgiLogFile));
#if 0
bindtextdomain("percgi", LOCALEDIR);
bind_textdomain_codeset("percgi", "UTF-8");
#endif
#ifndef HAVE_FASTCGI
if (cgi->flags & CGI_PERSISTENT) {
CGI_Log(CGI_LOG_EMERG,
"FastCGI is required. You must recompile PerCGI with "
"the --with-fastcgi option to use %s.\n", __progname);
exit(1);
}
#else
CGI_LogNotice("%s is now accepting requests", __progname);
#endif
TAILQ_INIT(&cgi->filters);
TAILQ_INIT(&cgi->vars);
SetGlobalS("_progname", __progname);
/* Register standard content filters. */
CGI_AddFilter(&cgiVarSubstFilter);
/* Set the list of acceptable charsets (--with-charsets) */
pCharset = sCharsets = Strdup(WITH_CHARSETS);
i = 0;
while ((s = Strsep(&pCharset, ",")) != NULL &&
i+1 < CGI_CHARSETS_MAX-1) {
cgi->availCharsets[i++] = Strdup(s);
}
cgi->availCharsets[i] = NULL;
Free(sCharsets);
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
sa.sa_handler = SIG_IGN;
sigaction(SIGURG, &sa, NULL);
sigaction(SIGHUP, &sa, NULL);
sigaction(SIGPIPE, &sa, NULL);
sigaction(SIGUSR1, &sa, NULL);
sigfillset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
sa.sa_handler = SigTERM;
sigaction(SIGINT, &sa, NULL);
sigaction(SIGQUIT, &sa, NULL);
sigaction(SIGTERM, &sa, NULL);
sigfillset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
sa.sa_handler = SigCHLD;
sigaction(SIGCHLD, &sa, NULL);
CGI_SessionMgrInit();
}
void
CGI_Destroy(void)
{
VAR *v, *vNext;
char **s;
if (cgi->destroyFn != NULL)
cgi->destroyFn();
for (s = &cgi->availCharsets[0]; *s != NULL; s++) {
free(*s);
}
for (v = TAILQ_FIRST(&cgi->vars);
v != TAILQ_END(&cgi->vars);
v = vNext) {
vNext = TAILQ_NEXT(v, vars);
Free(v->value);
free(v);
}
CGI_SessionMgrDestroy();
}
void
CGI_SetLogFile(const char *path)
{
Strlcpy(cgiLogFile, path, sizeof(cgiLogFile));
}
/* Register a content filter. */
void
CGI_AddFilter(CGI_Filter *fil)
{
TAILQ_INSERT_TAIL(&cgi->filters, fil, filters);
}
/* Unregister a content filter. */
void
CGI_DelFilter(CGI_Filter *fil)
{
TAILQ_REMOVE(&cgi->filters, fil, filters);
}
void
CGI_OutOfMem(void)
{
CGI_Exit(CGI_EX_TEMPFAIL, "Out of memory");
}
/*
* Clean up and exit. Use of "CGI_" prefixed sysexits(3) codes is preferred.
* The optional format string is logged before exiting.
*/
void
CGI_Exit(int excode, const char *fmt, ...)
{
CGI_Destroy();
if (fmt != NULL) {
char msg[BUFSIZ];
va_list ap;
va_start(ap, fmt);
vsnprintf(msg, sizeof(msg), fmt, ap);
va_end(ap);
CGI_LogNotice("%s; exiting", msg);
} else if (excode != 0) {
CGI_LogNotice("exit code %d", excode);
}
exit(excode);
}
/*
* Write data in response to query, applying any output filters registered
* for the current output Content-Type.
*/
ssize_t
CGI_WriteFiltered(CGI_Query *q, void *buf, size_t size)
{
if (q->wrType != NULL) {
CGI_Filter *filt;
TAILQ_FOREACH(filt, &cgi->filters, filters) {
if (filt->type == CGI_OUTPUT_FILTER &&
strcmp(filt->content_type, q->wrType) == 0) {
size = filt->func(q, &buf, size);
}
}
}
if (CGI_Write(q, buf, size) == -1) {
CGI_LogErr("CGI_WriteFiltered");
}
return (ssize_t)size;
}
static void
CGI_LogToFile(enum cgi_loglvl level, const char *s)
{
char buf[CGI_ERROR_MAX], *p = buf;
size_t hlen, alen, slen;
char *remoteAddr;
const char *c;
size_t rem;
int fd;
if (!(remoteAddr = getenv("REMOTE_ADDR"))) { remoteAddr = ""; }
alen = strlen(remoteAddr);
slen = strlen(s);
hlen = 1+cgiLogLvlNameLength+1+alen+2+1;
if (hlen+slen >= sizeof(buf)) {
slen = sizeof(buf) - hlen; /* Truncate */
}
*p='['; p++;
memcpy(p, remoteAddr, alen); p += alen;
*p = ' '; p++;
memcpy(p, cgiLogLvlNames[level], cgiLogLvlNameLength); p += cgiLogLvlNameLength;
*p = ']'; p++;
*p = ' '; p++;
rem = slen;
for (c = s; *c != '\0'; c++) {
*p = isprint(*c) ? *c : '*';
p++;
if (--rem == 0)
break;
}
*p = '\n'; p++;
fd = open(cgiLogFile, O_WRONLY|O_APPEND|O_CREAT|O_EXLOCK, 0600);
if (fd != -1) {
SYS_Write(fd, buf, hlen+slen);
close(fd);
}
}
/*
* Emit an error message to the error output. Format the message into a
* fixed-size buffer in order to limit the size of log entries.
*/
void
CGI_Log(enum cgi_loglvl level, const char *fmt, ...)
{
char msg[CGI_ERROR_MAX];
va_list ap;
va_start(ap, fmt);
vsnprintf(msg, sizeof(msg), fmt, ap);
va_end(ap);
if (cgi->logFn != NULL) {
cgi->logFn(level, msg);
return;
}
#ifdef HAVE_FASTCGI
CGI_LogToFile(level, msg);
#else
fprintf(stderr, "[%s] %s\n", cgiLogLvlNames[level], msg);
#endif
}
void
CGI_LogS(enum cgi_loglvl level, const char *s)
{
if (cgi->logFn != NULL) {
cgi->logFn(level, s);
return;
}
#ifdef HAVE_FASTCGI
CGI_LogToFile(level, s);
#else
fprintf(stderr, "[%s] %s\n", cgiLogLvlNames[level], s);
#endif
}
void
CGI_LogErr(const char *fmt, ...)
{
char buf[CGI_ERROR_MAX];
va_list ap;
va_start(ap, fmt);
vsnprintf(buf, sizeof(buf), fmt, ap);
va_end(ap);
CGI_LogS(CGI_LOG_ERR, buf);
}
void
CGI_LogWarn(const char *fmt, ...)
{
char buf[CGI_ERROR_MAX];
va_list ap;
va_start(ap, fmt);
vsnprintf(buf, sizeof(buf), fmt, ap);
va_end(ap);
CGI_LogS(CGI_LOG_WARNING, buf);
}
void
CGI_LogInfo(const char *fmt, ...)
{
char buf[CGI_ERROR_MAX];
va_list ap;
va_start(ap, fmt);
vsnprintf(buf, sizeof(buf), fmt, ap);
va_end(ap);
CGI_LogS(CGI_LOG_INFO, buf);
}
void
CGI_LogNotice(const char *fmt, ...)
{
char buf[CGI_ERROR_MAX];
va_list ap;
va_start(ap, fmt);
vsnprintf(buf, sizeof(buf), fmt, ap);
va_end(ap);
CGI_LogS(CGI_LOG_NOTICE, buf);
}
void
CGI_LogDebug(const char *fmt, ...)
{
char buf[CGI_ERROR_MAX];
va_list ap;
va_start(ap, fmt);
vsnprintf(buf, sizeof(buf), fmt, ap);
va_end(ap);
CGI_LogS(CGI_LOG_DEBUG, buf);
}
/*
* Write a formatted string to the standard output, assuming the formatted
* text is consistent with the current output Content-Type.
*/
void
CGI_Printf(CGI_Query *q, const char *fmt, ...)
{
char *buf;
va_list ap;
va_start(ap, fmt);
if (vasprintf(&buf, fmt, ap) == -1) {
CGI_OutOfMem();
}
va_end(ap);
CGI_Write(q, buf, strlen(buf));
free(buf);
}
void
CGI_SetError(const char *fmt, ...)
{
char s[CGI_ERROR_MAX];
va_list args;
va_start(args, fmt);
vsnprintf(s, sizeof(s), fmt, args);
va_end(args);
Strlcpy(cgiErrorMsg, s, sizeof(cgiErrorMsg));
}
char *
CGI_GetError(void)
{
return (cgiErrorMsg);
}
/* Return the list of variables with a key matching the given pattern. */
CGI_KeySet *
CGI_GetKeySet(CGI_Query *q, const char *keypat)
{
size_t patlen = strlen(keypat);
CGI_Argument *arg;
CGI_KeySet *set;
char *s;
set = Malloc(sizeof(CGI_KeySet));
set->nents = 0;
set->ents = Malloc(sizeof(char *));
TAILQ_FOREACH(arg, &q->args, args) {
if (strncmp(arg->key, keypat, patlen) != 0)
continue;
set->ents = Realloc(set->ents, (set->nents+1) * sizeof(char *));
if ((s = CGI_UnescapeURL(q, arg->key+patlen)) == NULL) {
continue;
}
set->ents[set->nents++] = s;
}
return (set);
}
void
CGI_FreeKeySet(CGI_KeySet *set)
{
int i;
for (i = 0; i < set->nents; i++) {
free(set->ents[i]);
}
free(set->ents);
free(set);
}
static __inline__ int
ValidSessionID(const char *sessID)
{
const char *c;
int n;
if (sessID[0] == '\0') {
goto fail;
}
for (c = &sessID[0], n = 0;
*c != '\0';
c++, n++) {
if (n+1 >= CGI_SESSID_MAX || !isdigit(*c))
goto fail;
}
return (1);
fail:
CGI_SetError("Malformed session ID.");
return (0);
}
static __inline__ int
ExistingSession(const char *sessID)
{
char sessPath[MAXPATHLEN];
struct stat sb;
Strlcpy(sessPath, _PATH_SESSIONS, sizeof(sessPath));
Strlcat(sessPath, sessID, sizeof(sessPath));
return (stat(sessPath, &sb) == -1) ? 0 : 1;
}
/* Authenticate and process a query in the standard way. */
void
CGI_ProcessQuery(CGI_Query *q, CGI_SessionOps *sessOps)
{
char sockPath[MAXPATHLEN];
char sessID[CGI_SESSID_MAX];
char user[CGI_USERNAME_MAX];
char pass[CGI_PASSWORD_MAX];
struct sockaddr_un sun;
CGI_SockLen_t sockLen;
const char *op, *sessArg;
int sock = -1, status;
int nRestoreAttempts = 0;
size_t readTotal;
if (!(op = CGI_Get(q, "op", CGI_OPNAME_MAX))) { op = ""; }
sessID[0] = '\0';
user[0] = '\0';
pass[0] = '\0';
#ifdef CGI_DEBUG_QUERIES
CGI_LogS(CGI_LOG_QUERY, op);
#endif
/* Process unauthenticated built-in functions. */
if (strcmp(op, sessOps->authChallengeName) == 0) {
CGI_BeginQueryUnauth(q, op, sessOps);
sessOps->authChallenge(q); fflush(stdout);
return;
} else if (strcmp(op, sessOps->authRegisterName) == 0) {
CGI_BeginQueryUnauth(q, op, sessOps);
sessOps->authRegister(q); fflush(stdout);
return;
} else if (strcmp(op, sessOps->authRegConfirmName) == 0) {
CGI_BeginQueryUnauth(q, op, sessOps);
sessOps->authRegConfirm(q); fflush(stdout);
return;
} else if (strcmp(op, sessOps->authAssistName) == 0) {
CGI_BeginQueryUnauth(q, op, sessOps);
sessOps->authAssist(q); fflush(stdout);
return;
} else if (strcmp(op, sessOps->authRequestName) == 0) {
CGI_BeginQueryUnauth(q, op, sessOps);
sessOps->authRequest(q); fflush(stdout);
return;
} else if (strcmp(op, sessOps->authConfirmName) == 0) {
CGI_BeginQueryUnauth(q, op, sessOps);
sessOps->authConfirm(q); fflush(stdout);
return;
} else if (strcmp(op, sessOps->authCommitName) == 0) {
CGI_BeginQueryUnauth(q, op, sessOps);
sessOps->authCommit(q); fflush(stdout);
return;
} else if (strcmp(op, sessOps->authCmdName) == 0) {
CGI_BeginQueryUnauth(q, op, sessOps);
sessOps->authCmd(q); fflush(stdout);
return;
}
/* Authenticate and get session handle. */
if ((sessArg = CGI_GetCookie(q, "sess")) == NULL ||
!ValidSessionID(sessArg) ||
!ExistingSession(sessArg)) {
const char *userArg, *passArg;
int pp[2];
pid_t pid;
ssize_t rv;
/*
* Fork a new worker process, authenticate and
* create a new session if successful.
*/
if ((userArg = CGI_Get(q, "username", CGI_USERNAME_MAX)) == NULL ||
(passArg = CGI_Get(q, "password", CGI_PASSWORD_MAX)) == NULL) {
CGI_BeginQueryUnauth(q, op, sessOps);
sessOps->loginPage(q);
fflush(stdout);
return;
}
/*
* If PREFORK_AUTH is set, perform authentication prior to
* forking (not recommended with network-based auth methods,
* but recommended for local methods).
*/
if ((sessOps->flags & CGI_SESSION_PREFORK_AUTH) &&
sessOps->auth != NULL &&
sessOps->auth(NULL, userArg, passArg) != 0) {
goto fail;
}
#ifdef CGI_DEBUG_QUERIES
CGI_LogDebug("AUTH as %s (\"%s\")", userArg, passArg);
#endif
Strlcpy(user, userArg, sizeof(user));
Strlcpy(pass, passArg, sizeof(pass));
open_session:
if (pipe(pp) == -1) {
CGI_SetError("pipe: %s", strerror(errno));
goto fail;
}
if ((pid = fork()) == -1) {
CGI_SetError("fork: %s", strerror(errno));
close(pp[0]);
close(pp[1]);
goto fail;
}
if (pid == 0) { /* In worker */
int rv;
rv = CGI_WorkerMain(sessOps, q, user, pass, sessID, pp,
nRestoreAttempts);
if (rv != 0) {
CGI_LogErr("Worker(%d) Failed: %s", getpid(),
CGI_GetError());
}
exit(0);
}
close(pp[1]);
/*
* Read status from worker process. Expect a session number
* on success, otherwise "AUTH" or "FAIL".
*/
read_worker_st:
rv = read(pp[0], sessID, sizeof(sessID));
if (rv == -1) {
if (errno == EINTR || errno == EAGAIN) {
goto read_worker_st;
}
CGI_SetError("Reading worker status: %s", strerror(errno));
close(pp[0]);
goto fail;
}
if (sessID[0] == 'A') {
CGI_SetError(_("Username or password is invalid"));
(void)write(pp[0], "0", 1);
close(pp[0]);
goto fail;
} else if (sessID[0] == 'R') {
CGI_SetError(_("You are trying to log in too quickly. "
"Please wait 5 seconds and try again."));
(void)write(pp[0], "0", 1);
close(pp[0]);
goto fail;
} else if (!ValidSessionID(sessID)) {
CGI_SetError("Worker process failed (%s)", sessID);
close(pp[0]);
goto fail;
}
close(pp[0]);
{
char *userAgent = getenv("HTTP_USER_AGENT");
CGI_LogInfo(
"%s: New session (%s, %s, pid %d, agent \"%s\")",
user, sessID, q->lang, (int)pid,
(userAgent && strlen(userAgent) <= CGI_USERAGENT_MAX) ? userAgent : "");
}
} else {
Strlcpy(sessID, sessArg, sizeof(sessID));
}
/*
* Now that we have a valid session handle, look for a running
* worker process pipe.
*/
Strlcpy(sockPath, _PATH_SOCKETS, sizeof(sockPath));
Strlcat(sockPath, sessID, sizeof(sockPath));
Strlcat(sockPath, ".sock", sizeof(sockPath));
if ((sock = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) {
CGI_SetError("socket: %s", strerror(errno));
goto fail;
}
sun.sun_family = AF_UNIX;
Strlcpy(sun.sun_path, sockPath, sizeof(sun.sun_path));
sockLen = SUN_LEN(&sun);
try_connect:
/* CGI_LogDebug("Connecting to session: %s", sessID); */
if (connect(sock, (struct sockaddr *)&sun, sockLen) == -1) {
if (errno == EINTR || errno == EAGAIN) {
goto try_connect;
} else if (errno != ECONNREFUSED && errno != ENOENT) {
CGI_SetError("connect: %s", strerror(errno));
CGI_LogErr("%s", CGI_GetError());
goto fail;
} else { /* Restart worker */
char sessPath[MAXPATHLEN];
CGI_Session sessOrig;
const char *userArg, *passArg;
int fd;
CGI_LogNotice("Restarting worker for %s", sessID);
if (nRestoreAttempts > 0) {
CGI_SetError("connect: %s (restore failed)",
strerror(errno));
goto fail;
}
Strlcpy(sessPath, _PATH_SESSIONS, sizeof(sessPath));
Strlcat(sessPath, sessID, sizeof(sessPath));
if ((fd = open(sessPath, O_RDONLY)) == -1) {
CGI_SetError("%s: %s", sessID, strerror(errno));
goto fail;
}
CGI_SessionInit(&sessOrig, sessOps);
if (CGI_SessionLoad(&sessOrig, fd) == -1) {
close(fd);
goto fail;
}
close(fd);
if ((userArg = GetSV(&sessOrig,"user")) == NULL ||
(passArg = GetSV(&sessOrig,"pass")) == NULL) {
CGI_SetError("Bad session data");
CGI_SessionDestroy(&sessOrig);
goto fail;
}
Strlcpy(user, userArg, sizeof(user));
Strlcpy(pass, passArg, sizeof(pass));
CGI_SessionDestroy(&sessOrig);
CGI_LogNotice("AUTH: Restored session %s (as %s)",
sessID, user);
nRestoreAttempts++;
unlink(sockPath);
goto open_session;
}
}
/* Write the processed query data */
if (CGI_QuerySave(sock, q) == -1)
goto fail;
/* Transfer any unprocessed data as-is. */
if (!q->contentRead) {
char buf[CGI_WORKER_WRBUFSIZE];
size_t nRead, contentLen;
ssize_t rv, rvWrite;
char *sl;
if ((sl = getenv("CONTENT_LENGTH")) == NULL ||
(contentLen = (size_t)strtoul(sl, NULL, 10)) == 0) {
CGI_SetError("No CONTENT_LENGTH");
goto fail_json;
}
for (nRead = 0; nRead < contentLen; ) {
rv = (ssize_t)fread(buf, 1, sizeof(buf), stdin);
if (rv < 1) {
break;
}
nRead += rv;
write_again:
if ((rvWrite = write(sock, buf, rv)) == -1) {
if (errno == EINTR || errno == EAGAIN) {
CGI_CheckSignals();
goto write_again;
} else {
CGI_SetError("Content rejected (%s)",
strerror(errno));
goto fail_json;
}
} else if (rvWrite == 0) {
CGI_SetError("EOF writing to worker");
goto fail_json;
}
}
}
/* Read the response back from the worker process, forward to client */
#ifdef CGI_DEBUG_HEADERS
FILE *dbgOut = fopen("debug-out.txt", "w");
#endif
for (readTotal = 0; ; ) {
char rbuf[CGI_WORKER_RDBUFSIZE], *pBuf = rbuf;
ssize_t rv;
read_worker:
if ((rv = read(sock, rbuf, sizeof(rbuf))) == -1) {
if (errno == EINTR || errno == EAGAIN) {
CGI_CheckSignals();
goto read_worker;
} else {
CGI_SetError("Reading worker: %s", strerror(errno));
goto fail;
}
} else if (rv == 0) {
break;
}
readTotal += rv;
#ifdef CGI_DEBUG_IO
CGI_LogDebug("MASTER: Read %lu bytes chunk from worker",
(Ulong)rv);
#endif
#ifdef CGI_DEBUG_HEADERS
if (dbgOut) {
fwrite(rbuf, 1, rv, dbgOut);
fflush(dbgOut);
}
#endif
clearerr(stdout);
if (fwrite(rbuf, 1, rv, stdout) < rv) {
CGI_SetError("Writing client: %s", feof(stdout) ? "EOF" : "Error");
goto fail;
}
fflush(stdout);
#ifdef CGI_DEBUG_IO
CGI_LogDebug("MASTER: Wrote %lu bytes back to client",
(Ulong)rv);
#endif
}
#ifdef CGI_DEBUG_HEADERS
if (dbgOut) { fclose(dbgOut); }
#endif
#ifdef CGI_DEBUG_IO
CGI_LogDebug("MASTER: Transferred %lu bytes total", (Ulong)readTotal);
#endif
close(sock);
return;
fail:
q->sock = -1;
CGI_BeginQueryUnauth(q, "login", sessOps);
CGI_LogS(CGI_LOG_ERR, CGI_GetError());
HTML_SetError("%s", CGI_GetError());
sessOps->loginPage(q);
fflush(stdout);
if (sock != -1) { close(sock); }
return;
fail_json:
CGI_LogS(CGI_LOG_ERR, CGI_GetError());
CGI_WriteHeader(q, "application/json", "utf8", 0);
CGI_PutS(q, "{\"code\": -1,");
CGI_PutJSON_NoHTML_S(q, "error", CGI_GetError());
CGI_PutS(q, "\"backend_version\": \"" VERSION "\"}\r\n");
q->sock = -1;
fflush(stdout);
if (sock != -1) { close(sock); }
}
/*
* Main routine for session worker process. Authenticate, write return
* code to the parent pipe and loop reading queries. If sessID is non-empty,
* load an existing session file from disk, otherwise create a new session.
*/
int
CGI_WorkerMain(CGI_SessionOps *sessOps, CGI_Query *qServer,
const char *user, const char *pass, const char *sessID,
int pp[2], int nRestoreAttempts)
{
char sessPath[MAXPATHLEN], sockPath[MAXPATHLEN];
CGI_Session *sess;
struct sigaction sa;
struct sockaddr_un sun;
CGI_SockLen_t socklen;
int sock = -1, fd;
int rv = 0, i;
const char *s, *agent;
time_t tLastQuery = time(NULL);
Uint nQueries = 0;
termFlag = 0;
chldFlag = 0;
close(pp[0]);
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
setproctitle("percgi auth");
if ((sess = TryMalloc(sessOps->size)) == NULL) {
rv = CGI_EX_OSERR;
goto fail;
}
CGI_SessionInit(sess, sessOps);
/* Authenticate */
if (!(sessOps->flags & CGI_SESSION_PREFORK_AUTH) &&
sessOps->auth != NULL) {
int srv;
srv = sessOps->auth(sess, user, pass);
if (srv == -2) {
rv = CGI_EX_IOERR;
goto fail;
} else if (srv == -1) {
rv = CGI_EX_NOPERM;
goto fail;
}
}
CGI_SetError("No error");
if (*sessID != '\0') { /* Reuse ID */
Strlcpy(sess->id, sessID, sizeof(sess->id));
Strlcpy(sessPath, _PATH_SESSIONS, sizeof(sessPath));
Strlcat(sessPath, sess->id, sizeof(sessPath));
if ((fd = open(sessPath, O_RDONLY, 0600)) == -1) {
CGI_SetError("%s: %s", sessPath, strerror(errno));
rv = CGI_EX_OSFILE;
goto fail;
}
if (CGI_SessionLoad(sess, fd) == -1) {
close(fd);
rv = CGI_EX_OSFILE;
goto fail;
}
if ((s = GetSV(sess, "agent")) &&
(agent = getenv("HTTP_USER_AGENT")) &&
strlen(agent) <= CGI_USERAGENT_MAX &&
strcmp(agent, s) != 0) {
CGI_LogWarn("%s: Agent changed from \"%s\" to \"%s\"",
sess->id, s, agent);
}
close(fd);
if (cgiLanguageFn != NULL &&
(s = GetSV(sess, "language")) && *s != '\0')
cgiLanguageFn(s, cgiLanguageFnArg);
for (i = 0; i < nCgiModules; i++) {
if (cgiModules[i]->init != NULL &&
cgiModules[i]->init(sess) != 0) {
for (i--; i >= 0; i--) {
if (cgiModules[i]->destroy)
cgiModules[i]->destroy();
}
rv = CGI_EX_SOFTWARE;
goto fail;
}
}
if (nRestoreAttempts > 0 && sessOps->sessRestored) {
sessOps->sessRestored(sess, user);
}
} else {
regen_id:
/* Generate a new unique session ID. */
snprintf(sess->id, sizeof(sess->id), "%lu", (u_long)CGI_Arc4random());
Strlcpy(sessPath, _PATH_SESSIONS, sizeof(sessPath));
Strlcat(sessPath, sess->id, sizeof(sessPath));
if ((fd = open(sessPath, O_WRONLY|O_CREAT|O_EXCL, 0600)) == -1) {
if (errno == EEXIST) {
goto regen_id;
} else {
CGI_SetError("%s: %s", sessPath, strerror(errno));
rv = CGI_EX_OSFILE;
goto fail;
}
}
/* Initialize a default session environment. */
SetSV_S(sess, "user", user);
SetSV_S(sess, "pass", pass);
/* Select a default language and character-set. */
NegotiateLanguageHTTP(qServer);
SetSV_S(sess, "character-set", qServer->cset);
SetSV_S(sess, "language", qServer->lang);
if ((s = getenv("HTTP_USER_AGENT")) &&
strlen(s) <= CGI_USERAGENT_MAX) {
SetSV_S(sess, "agent", s);
} else {
SetSV_S(sess, "agent", "");
}
if (sessOps->sessOpen &&
sessOps->sessOpen(sess, user) == -1) {
CGI_LogErr("%s: %s; aborting session", sess->id,
CGI_GetError());
for (i = 0; i < nCgiModules; i++) {
if (cgiModules[i]->destroy)
cgiModules[i]->destroy();
}
close(fd);
unlink(sessPath);
rv = CGI_EX_TEMPFAIL;
goto fail;
}
for (i = 0; i < nCgiModules; i++) {
if (cgiModules[i]->init &&
cgiModules[i]->init(sess) != 0) {
for (i--; i >= 0; i--) {
if (cgiModules[i]->destroy)
cgiModules[i]->destroy();
}
close(fd);
unlink(sessPath);
rv = CGI_EX_SOFTWARE;
goto fail;
}
}
for (i = 0; i < nCgiModules; i++) {
if (cgiModules[i]->sessOpen &&
cgiModules[i]->sessOpen(sess) != 0) {
CGI_LogErr("%s: %s; aborting session", sess->id,
CGI_GetError());
for (; i >= 0; i--) {
if (cgiModules[i]->destroy)
cgiModules[i]->destroy();
}
close(fd);
unlink(sessPath);
rv = CGI_EX_TEMPFAIL;
goto fail;
}
}
if (CGI_SessionSaveToFD(sess, fd) == -1) {
close(fd);
goto fail_close;
}
close(fd);
}
setproctitle("worker %s", user);
sigfillset(&sa.sa_mask);
sa.sa_flags = 0;
sa.sa_handler = SigTERM;
sigaction(SIGINT, &sa, NULL);
sigaction(SIGQUIT, &sa, NULL);
sigaction(SIGTERM, &sa, NULL);
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sa.sa_handler = SigCHLD;
sigaction(SIGCHLD, &sa, NULL);
if ((sock = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) {
CGI_SetError("socket(AF_UNIX): %s", strerror(errno));
goto fail_close;
}
sun.sun_family = AF_UNIX;
Strlcpy(sockPath, _PATH_SOCKETS, sizeof(sockPath));
Strlcat(sockPath, sess->id, sizeof(sockPath));
Strlcat(sockPath, ".sock", sizeof(sockPath));
Strlcpy(sun.sun_path, sockPath, sizeof(sun.sun_path));
socklen = SUN_LEN(&sun);
if (bind(sock, (struct sockaddr *)&sun, socklen) == -1) {
CGI_SetError("bind(%s): %s", sockPath, strerror(errno));
goto fail_close;
}
if (listen(sock, 20) == -1) {
CGI_SetError("listen: %s", strerror(errno));
goto fail_close;
}
chmod(sockPath, 0700);
/*
* Ready to accept queries. Communicate the new session ID
* to the parent process.
*/
if (SYS_Write(pp[1], sess->id, strlen(sess->id)+1) == -1) {
close(pp[1]); pp[1] = -1;
goto fail_close;
}
close(pp[1]); pp[1] = -1;
for (;;) {
struct sockaddr_un paddr;
CGI_Query q;
fd_set rdFds, wrFds;
int maxFd = 0;
struct timeval tv;
tv.tv_usec = 300000;
tv.tv_sec = 0;
FD_ZERO(&rdFds);
FD_ZERO(&wrFds);
FD_SET(sock, &rdFds);
if (sock > maxFd) { maxFd = sock; }
if (sessOps->addSelectFDs != NULL)
sessOps->addSelectFDs(sess, &rdFds, &wrFds, &maxFd);
if (select(maxFd+1, &rdFds, &wrFds, 0, &tv) < 0) {
if (errno == EINTR || errno == EAGAIN) {
CGI_CheckSignals();
continue;
} else {
CGI_SetError("select: %s", strerror(errno));
goto fail_close;
}
}
if ((time(NULL) - tLastQuery) > sessOps->workerTimeout) {
CGI_LogInfo("Inactivity timeout (%u queries)",
nQueries);
break;
}
if (sessOps->procSelectFDs != NULL) {
sessOps->procSelectFDs(sess, &rdFds, &wrFds);
}
if (FD_ISSET(sock, &rdFds)) {
CGI_Cookie *ck;
time_t tExpire;
struct tm *tmExpire;
int s;
s = accept(sock, (struct sockaddr *)&paddr, &socklen);
if (s == -1) {
if (errno == EINTR || errno == EAGAIN) {
CGI_CheckSignals();
continue;
} else {
CGI_SetError("accept: %s", strerror(errno));
goto fail_close;
}
}
CGI_QueryInit(&q);
if (CGI_QueryLoad(s, &q) == -1)
goto fail_close;
#ifdef CGI_DEBUG_ARGS
{
CGI_Argument *arg;
TAILQ_FOREACH(arg, &q.args, args) {
if (strcmp(arg->key, "password")==0 ||
strcmp(arg->key, "pass")==0) {
continue;
}
if (arg->contentType[0] != '\0') {
CGI_LogDebug("ARG: %s=[%s %lu]",
arg->key, arg->contentType,
arg->len);
} else {
CGI_LogDebug("ARG: %s=[%s]",
arg->key, arg->value);
}
}
}
#endif /* CGI_DEBUG_ARGS */
q.sess = sess;
q.sock = s;
tLastQuery = time(NULL);
tExpire = tLastQuery + sessOps->sessTimeout;
tmExpire = localtime(&tExpire);
ck = CGI_SetCookieS(&q, "sess", sess->id);
ck->path[0] = '/';
ck->path[1] = '\0';
strftime(ck->expires, sizeof(ck->expires),
"%a, %d %b %Y %H:%M:%S GMT", tmExpire);
/* ck->flags |= CGI_COOKIE_SECURE; */
CGI_BeginQuery(&q, user);
CGI_ExecQuery(&q, sessOps);
CGI_QueryDestroy(&q);
close(s);
nQueries++;
}
CGI_CheckSignals();
}
for (i = 0; i < nCgiModules; i++) {
if (cgiModules[i]->destroy) { cgiModules[i]->destroy(); }
}
close(sock);
unlink(sockPath);
CGI_SessionDestroy(sess);
free(sess);
return (0);
fail_close:
for (i = 0; i < nCgiModules; i++) {
if (cgiModules[i]->destroy) { cgiModules[i]->destroy(); }
}
rv = CGI_EX_OSERR;
if (sock != -1) { close(sock); }
unlink(sockPath);
unlink(sessPath);
fail:
CGI_SessionDestroy(sess);
free(sess);
if (pp[1] != -1) { /* Communicate error with parent */
if (rv == CGI_EX_NOPERM) {
SYS_Write(pp[1], "AUTH", 5);
} else if (rv == CGI_EX_IOERR) {
SYS_Write(pp[1], "RATE", 5);
} else {
SYS_Write(pp[1], "FAIL", 5);
}
}
if (rv == CGI_EX_NOPERM) {
char rbuf;
SYS_Read(pp[1], &rbuf, 1);
CGI_LogErr("Worker: response %c from parent", rbuf);
}
close(pp[1]);
sleep(1);
return (1);
}
/* Standard loop for processing queries */
void
CGI_QueryLoop(CGI_SessionOps *sessOps)
{
CGI_Query q;
int rv;
cgi->queryCount = 0;
#ifdef HAVE_FASTCGI
while (FCGI_Accept() >= 0) {
#endif
if (getenv("SCRIPT_NAME") == NULL) {
#ifdef HAVE_FASTCGI
char *err = "This program must be executed via FastCGI\n";
#else
char *err = "This program must be executed as CGI\n";
#endif
fputs(err, stdout);
CGI_SetError("SCRIPT_NAME is not set (run from command-line?)");
goto fail;
}
CGI_QueryInit(&q);
if (CGI_QueryReadHTTP(&q, sessOps) == 0) {
CGI_ProcessQuery(&q, sessOps);
} else {
CGI_LogErr("QueryReadHTTP: %s", CGI_GetError());
CGI_WriteHeader(&q, "application/json", "utf8", 0);
CGI_PutS(&q, "{\"code\": -1,");
CGI_PutJSON_NoHTML_S(&q, "error", CGI_GetError());
CGI_PutS(&q, "\"backend_version\": \"" VERSION "\"}\r\n");
}
#ifdef HAVE_FASTCGI
FCGI_Finish();
#endif
CGI_QueryDestroy(&q);
CGI_CheckSignals();
cgi->queryCount++;
#ifdef HAVE_FASTCGI
}
CGI_LogDebug("FCGI_Accept: %s; terminating", strerror(errno));
#endif
return;
fail:
CGI_LogErr("QueryLoop: %s", CGI_GetError());
}