/* * 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, "", mod->name, mod->icon, gettext(mod->lname)); } else { Cat(v, "", q->url, mod->name, mod->icon, gettext(mod->lname)); } } else { if (mod->sections[0].name != NULL) { CatS(v, ""); } } } if (cgi->flags & CGI_PRETTY_URL) { Cat(v, "", CGI_GLYPHICON(log-out), _("Logout")); } else { Cat(v, "", 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()); }