/* http_cgi.c -- Routines for handling Common Gateway Interface scripts in httpd
*
* Copyright (c) 1994-2019 Carnegie Mellon University. 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.
*
* 3. The name "Carnegie Mellon University" must not be used to
* endorse or promote products derived from this software without
* prior written permission. For permission or any legal
* details, please contact
* Carnegie Mellon University
* Center for Technology Transfer and Enterprise Creation
* 4615 Forbes Avenue
* Suite 302
* Pittsburgh, PA 15213
* (412) 268-7393, fax: (412) 268-7395
* innovation@andrew.cmu.edu
*
* 4. Redistributions of any form whatsoever must retain the following
* acknowledgment:
* "This product includes software developed by Computing Services
* at Carnegie Mellon University (http://www.cmu.edu/computing/)."
*
* CARNEGIE MELLON UNIVERSITY DISCLAIMS ALL WARRANTIES WITH REGARD TO
* THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS, IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE
* FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
* AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
* OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*
*/
#include <config.h>
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#include <ctype.h>
#include <string.h>
#include <syslog.h>
#include <assert.h>
#include "command.h"
#include "global.h"
#include "httpd.h"
#include "http_proxy.h"
#include "proxy.h"
#include "tok.h"
#include "util.h"
#include "version.h"
/* generated headers are not necessarily in current directory */
#include "imap/http_err.h"
#include "imap/imap_err.h"
static void cgi_init(struct buf *serverinfo);
static int meth_get(struct transaction_t *txn, void *params);
static int meth_post(struct transaction_t *txn, void *params);
/* Namespace for CGI */
struct namespace_t namespace_cgi = {
URL_NS_CGI, 0, "cgi", "/cgi-bin", NULL,
http_allow_noauth, /*authschemes*/0,
/*mbtype*/0,
ALLOW_READ | ALLOW_POST,
cgi_init, NULL, NULL, NULL, NULL, NULL,
{
{ NULL, NULL }, /* ACL */
{ NULL, NULL }, /* BIND */
{ NULL, NULL }, /* CONNECT */
{ NULL, NULL }, /* COPY */
{ NULL, NULL }, /* DELETE */
{ &meth_get, NULL }, /* GET */
{ &meth_get, NULL }, /* HEAD */
{ NULL, NULL }, /* LOCK */
{ NULL, NULL }, /* MKCALENDAR */
{ NULL, NULL }, /* MKCOL */
{ NULL, NULL }, /* MOVE */
{ &meth_options, NULL }, /* OPTIONS */
{ NULL, NULL }, /* PATCH */
{ &meth_post, NULL }, /* POST */
{ NULL, NULL }, /* PROPFIND */
{ NULL, NULL }, /* PROPPATCH */
{ NULL, NULL }, /* PUT */
{ NULL, NULL }, /* REPORT */
{ &meth_trace, NULL }, /* TRACE */
{ NULL, NULL }, /* UNBIND */
{ NULL, NULL } /* UNLOCK */
}
};
static void cgi_init(struct buf *serverinfo __attribute__((unused)))
{
namespace_cgi.enabled = config_httpmodules & IMAP_ENUM_HTTPMODULES_CGI;
}
/* Add request headers to environment for script */
static void req_hdr_to_env(const char *name, const char *contents,
const char *raw __attribute__((unused)), void *rock)
{
struct buf *environ = (struct buf *) rock;
const char **hdr, *skip[] =
{ "authorization", "cookie", "expect", "http2-settings",
"proxy-authorization", "transfer-encoding", "upgrade", NULL };
/* Ignore private headers in our cache */
if (name[0] == ':') return;
for (hdr = skip; *hdr && strcasecmp(name, *hdr); hdr++);
if (!*hdr) {
static struct buf env_name = BUF_INITIALIZER;
const char *env_str = buf_cstring(environ);
const char *exists;
buf_reset(&env_name);
buf_printf(&env_name, "\tHTTP_%s=", name);
buf_ucase(&env_name);
buf_replace_all(&env_name, "-", "_");
/* See if this name already exists */
exists = strstr(env_str, buf_cstring(&env_name));
if (exists) {
/* Append value to existing value(s) */
const char *next = strchr(exists + 1, '\t');
unsigned offset = next ? next - env_str : (unsigned) strlen(env_str);
buf_insertcstr(environ, offset, ", ");
buf_insertcstr(environ, offset + 2, contents);
}
else {
/* Add name and value */
buf_appendcstr(environ, buf_cstring(&env_name));
buf_appendcstr(environ, contents);
}
}
}
/* Perform a GET/HEAD request */
static int meth_get(struct transaction_t *txn,
void *params __attribute__((unused)))
{
int ret = 0;
const char *prefix, *urls, *errstr = NULL;
const char **hdr, *extra, *port, *query;
char *script, *cwd;
struct stat sbuf;
strarray_t *env = NULL;
struct command *cmd = NULL;
hdrcache_t resp_hdrs = NULL;
struct body_t resp_body;
long code = 0;
extern char **environ;
memset(&resp_body, 0, sizeof(struct body_t));
prefix = config_getstring(IMAPOPT_HTTPDOCROOT);
if (!prefix) return HTTP_NOT_FOUND;
if ((urls = config_getstring(IMAPOPT_HTTPALLOWEDURLS))) {
tok_t tok = TOK_INITIALIZER(urls, " \t", TOK_TRIMLEFT|TOK_TRIMRIGHT);
char *token;
while ((token = tok_next(&tok)) && strcmp(token, txn->req_uri->path));
tok_fini(&tok);
if (!token) return HTTP_NOT_FOUND;
}
/* Construct path to script */
extra = strchr(txn->req_uri->path + strlen(namespace_cgi.prefix) + 1, '/');
buf_setcstr(&txn->buf, prefix);
if (extra)
buf_appendmap(&txn->buf, txn->req_uri->path, extra - txn->req_uri->path);
else buf_appendcstr(&txn->buf, txn->req_uri->path);
script = buf_release(&txn->buf);
cwd = strconcat(prefix, namespace_cgi.prefix, NULL);
/* See if script exists */
if (stat(script, &sbuf) || !S_ISREG(sbuf.st_mode)) {
ret = HTTP_NOT_FOUND;
goto done;
}
/* See if script is executable */
if (access(script, X_OK)) {
syslog(LOG_ERR, "CGI script %s is not executable",
txn->req_uri->path);
txn->error.desc = "CGI script is not executable";
ret = HTTP_SERVER_ERROR;
goto done;
}
/* Construct environment for script */
buf_setcstr(&txn->buf, "GATEWAY_INTERFACE=CGI/1.1");
if ((hdr = spool_getheader(txn->req_hdrs, "Content-Length"))) {
buf_printf(&txn->buf, "\tCONTENT_LENGTH=%s", hdr[0]);
}
if ((hdr = spool_getheader(txn->req_hdrs, "Content-Type"))) {
buf_printf(&txn->buf, "\tCONTENT_TYPE=%s", hdr[0]);
}
buf_printf(&txn->buf, "\tPATH_INFO=%s", extra ? extra : "");
if (extra) {
buf_printf(&txn->buf, "\tPATH_TRANSLATED=%s%s", prefix, extra);
}
buf_printf(&txn->buf, "\tQUERY_STRING=%s",
(query = URI_QUERY(txn->req_uri)) ? query : "");
buf_printf(&txn->buf, "\tREMOTE_ADDR=%.*s",
(int) (httpd_remoteip ? strcspn(httpd_remoteip, ";") : 0),
httpd_remoteip);
buf_printf(&txn->buf, "\tREMOTE_HOST=%.*s",
(int) (txn->conn->clienthost ?
strcspn(txn->conn->clienthost, " ") : 0),
txn->conn->clienthost);
if (httpd_userid) {
buf_printf(&txn->buf, "\tREMOTE_USER=%s", httpd_userid);
if (txn->auth_chal.scheme) {
buf_printf(&txn->buf, "\tAUTH_TYPE=%s", txn->auth_chal.scheme->name);
}
}
buf_printf(&txn->buf, "\tREQUEST_METHOD=%s", http_methods[txn->meth].name);
buf_printf(&txn->buf, "\tSCRIPT_NAME=%s", script + strlen(prefix));
buf_printf(&txn->buf, "\tSERVER_NAME=%s", config_servername);
buf_printf(&txn->buf, "\tSERVER_PORT=%s",
(port = strchr(httpd_localip ? httpd_localip : "", ';')) ?
port + 1 : (https ? "443" : "80"));
buf_printf(&txn->buf, "\tSERVER_PROTOCOL=%s", HTTP_VERSION);
buf_printf(&txn->buf, "\tSERVER_SOFTWARE=%s", buf_cstring(&serverinfo));
/* Add some HTTP headers from request */
spool_enum_hdrcache(txn->req_hdrs, &req_hdr_to_env, &txn->buf);
env = strarray_splitm(buf_release(&txn->buf), "\t", 0);
strarray_append(env, NULL);
environ = env->data;
/* Run script */
if (command_popen(&cmd, "rw", cwd, script, NULL)) {
ret = HTTP_SERVER_ERROR;
goto done;
}
/* Send request body */
prot_putbuf(cmd->stdin_prot, &txn->req_body.payload);
prot_flush(cmd->stdin_prot);
/* Read response headers */
ret = http_read_headers(cmd->stdout_prot,
0 /* read_sep */, &resp_hdrs, &errstr);
if (ret) {
syslog(LOG_ERR, "Failed to read headers from CGI script %s",
txn->req_uri->path);
txn->error.desc = "Failed to read headers from CGI script";
ret = HTTP_SERVER_ERROR;
}
else {
/* Read [CR]LF separating headers and body */
int c = prot_getc(cmd->stdout_prot);
if (c == '\r') c = prot_getc(cmd->stdout_prot);
if (c != '\n') {
syslog(LOG_ERR, "Failed to read newline from CGI script %s",
txn->req_uri->path);
txn->error.desc = "Failed to read newline from CGI script";
ret = HTTP_SERVER_ERROR;
}
if (!ret) {
/* Check for and read response body */
hdr = spool_getheader(resp_hdrs, "Content-Type");
if (hdr) {
txn->resp_body.type = hdr[0];
resp_body.flags = BODY_RESPONSE | BODY_CLOSE;
ret = http_read_body(cmd->stdout_prot,
resp_hdrs, &resp_body, &errstr);
if (ret) {
syslog(LOG_ERR, "Failed to body from CGI script %s",
txn->req_uri->path);
txn->error.desc = "Failed to read body from CGI script";
ret = HTTP_SERVER_ERROR;
}
}
}
}
if (command_pclose(&cmd)) ret = HTTP_SERVER_ERROR;
if (ret) goto done;
/* Check for a status code */
hdr = spool_getheader(resp_hdrs, "Status");
if (hdr) code = http_status_to_code(atoi(hdr[0]));
/* Check the type of the CGI response */
hdr = spool_getheader(resp_hdrs, "Location");
if (hdr) {
/* Redirect */
if (hdr[0][0] == '/') {
/* Local Redirect */
txn->flags.redirect = 1;
/* Reset the URI part of our current transaction */
xmlFreeURI(txn->req_uri);
free_hash_table(&txn->req_qparams, (void (*)(void *)) &freestrlist);
/* Examine new request */
ret = examine_request(txn, hdr[0]);
/* Reprocess the requested method on new URI */
if (!ret) ret = process_request(txn);
goto done;
}
else {
/* Client Redirect */
if (!code) code = HTTP_FOUND;
txn->location = hdr[0];
}
}
else {
/* Document */
if (!code) code = HTTP_OK;
}
/* Output response */
txn->resp_body.extra_hdrs = resp_hdrs;
write_body(code, txn,
buf_base(&resp_body.payload), buf_len(&resp_body.payload));
done:
spool_free_hdrcache(resp_hdrs);
buf_free(&resp_body.payload);
strarray_free(env);
free(script);
free(cwd);
return ret;
}
/* Perform a POST request */
static int meth_post(struct transaction_t *txn, void *params)
{
int ret;
/* Read request body */
txn->req_body.flags |= BODY_DECODE;
ret = http_read_req_body(txn);
if (ret) {
txn->flags.conn = CONN_CLOSE;
return ret;
}
return meth_get(txn, params);
}