diff --git a/mongoose.c b/mongoose.c index 42135a5c..915608b8 100644 --- a/mongoose.c +++ b/mongoose.c @@ -47,7 +47,6 @@ #define NO_LIBC #define MG_DISABLE_FILESYSTEM #define MG_DISABLE_POPEN -#define MG_DISABLE_CGI #define MG_DISABLE_DIRECTORY_LISTING #define MG_DISABLE_SOCKETPAIR #define MG_DISABLE_PFS @@ -123,6 +122,15 @@ extern void *(*test_calloc)(size_t count, size_t size); #define MIN(a, b) ((a) < (b) ? (a) : (b)) #endif +#if !MG_DISABLE_HTTP && MG_ENABLE_CGI +MG_INTERNAL void mg_handle_cgi(struct mg_connection *nc, const char *prog, + const struct mg_str *path_info, + const struct http_message *hm, + const struct mg_serve_http_opts *opts); +struct mg_http_proto_data_cgi; +MG_INTERNAL void mg_http_free_proto_data_cgi(struct mg_http_proto_data_cgi *d); +#endif + #endif /* CS_MONGOOSE_SRC_INTERNAL_H_ */ #ifdef MG_MODULE_LINES #line 1 "common/cs_dbg.h" @@ -3806,11 +3814,6 @@ int mg_normalize_uri_path(const struct mg_str *in, struct mg_str *out) { #define MG_WS_NO_HOST_HEADER_MAGIC ((char *) 0x1) #endif -/* CGI requires socketpair. */ -#if MG_DISABLE_SOCKETPAIR && !MG_DISABLE_CGI -#define MG_DISABLE_CGI 1 -#endif - static const char *mg_version_header = "Mongoose/" MG_VERSION; enum mg_http_proto_data_type { DATA_NONE, DATA_FILE, DATA_PUT }; @@ -3823,9 +3826,11 @@ struct mg_http_proto_data_file { enum mg_http_proto_data_type type; }; +#if MG_ENABLE_CGI struct mg_http_proto_data_cgi { struct mg_connection *cgi_nc; }; +#endif struct mg_http_proto_data_chuncked { int64_t body_len; /* How many bytes of chunked body was reassembled. */ @@ -3863,7 +3868,7 @@ struct mg_http_proto_data { #if !MG_DISABLE_FILESYSTEM struct mg_http_proto_data_file file; #endif -#if !MG_DISABLE_CGI +#if MG_ENABLE_CGI struct mg_http_proto_data_cgi cgi; #endif #if MG_ENABLE_HTTP_STREAMING_MULTIPART @@ -3907,15 +3912,6 @@ static void mg_http_free_proto_data_file(struct mg_http_proto_data_file *d) { } #endif -#if !MG_DISABLE_CGI -static void mg_http_free_proto_data_cgi(struct mg_http_proto_data_cgi *d) { - if (d != NULL) { - if (d->cgi_nc != NULL) d->cgi_nc->flags |= MG_F_CLOSE_IMMEDIATELY; - memset(d, 0, sizeof(struct mg_http_proto_data_cgi)); - } -} -#endif - static void mg_http_free_proto_data_endpoints(struct mg_http_endpoint **ep) { struct mg_http_endpoint *current = *ep; @@ -3934,7 +3930,7 @@ static void mg_http_conn_destructor(void *proto_data) { #if !MG_DISABLE_FILESYSTEM mg_http_free_proto_data_file(&pd->file); #endif -#if !MG_DISABLE_CGI +#if MG_ENABLE_CGI mg_http_free_proto_data_cgi(&pd->cgi); #endif #if MG_ENABLE_HTTP_STREAMING_MULTIPART @@ -3944,24 +3940,6 @@ static void mg_http_conn_destructor(void *proto_data) { free(proto_data); } -/* - * This structure helps to create an environment for the spawned CGI program. - * Environment is an array of "VARIABLE=VALUE\0" ASCIIZ strings, - * last element must be NULL. - * However, on Windows there is a requirement that all these VARIABLE=VALUE\0 - * strings must reside in a contiguous buffer. The end of the buffer is - * marked by two '\0' characters. - * We satisfy both worlds: we create an envp array (which is vars), all - * entries are actually pointers inside buf. - */ -struct mg_cgi_env_block { - struct mg_connection *nc; - char buf[MG_CGI_ENVIRONMENT_SIZE]; /* Environment buffer */ - const char *vars[MG_MAX_CGI_ENVIR_VARS]; /* char *envp[] */ - int len; /* Space taken */ - int nvars; /* Number of variables in envp[] */ -}; - #if !MG_DISABLE_FILESYSTEM #define MIME_ENTRY(_ext, _type) \ @@ -4539,7 +4517,7 @@ static void mg_http_transfer_file_data(struct mg_connection *nc) { mg_http_free_proto_data_file(&pd->file); } } -#if !MG_DISABLE_CGI +#if MG_ENABLE_CGI else if (pd->cgi.cgi_nc != NULL) { /* This is POST data that needs to be forwarded to the CGI process */ if (pd->cgi.cgi_nc != NULL) { @@ -6684,459 +6662,6 @@ out: return ok; } -#if !MG_DISABLE_CGI -#ifdef _WIN32 -struct mg_threadparam { - sock_t s; - HANDLE hPipe; -}; - -static int mg_wait_until_ready(sock_t sock, int for_read) { - fd_set set; - FD_ZERO(&set); - FD_SET(sock, &set); - return select(sock + 1, for_read ? &set : 0, for_read ? 0 : &set, 0, 0) == 1; -} - -static void *mg_push_to_stdin(void *arg) { - struct mg_threadparam *tp = (struct mg_threadparam *) arg; - int n, sent, stop = 0; - DWORD k; - char buf[BUFSIZ]; - - while (!stop && mg_wait_until_ready(tp->s, 1) && - (n = recv(tp->s, buf, sizeof(buf), 0)) > 0) { - if (n == -1 && GetLastError() == WSAEWOULDBLOCK) continue; - for (sent = 0; !stop && sent < n; sent += k) { - if (!WriteFile(tp->hPipe, buf + sent, n - sent, &k, 0)) stop = 1; - } - } - DBG(("%s", "FORWARED EVERYTHING TO CGI")); - CloseHandle(tp->hPipe); - MG_FREE(tp); - _endthread(); - return NULL; -} - -static void *mg_pull_from_stdout(void *arg) { - struct mg_threadparam *tp = (struct mg_threadparam *) arg; - int k = 0, stop = 0; - DWORD n, sent; - char buf[BUFSIZ]; - - while (!stop && ReadFile(tp->hPipe, buf, sizeof(buf), &n, NULL)) { - for (sent = 0; !stop && sent < n; sent += k) { - if (mg_wait_until_ready(tp->s, 0) && - (k = send(tp->s, buf + sent, n - sent, 0)) <= 0) - stop = 1; - } - } - DBG(("%s", "EOF FROM CGI")); - CloseHandle(tp->hPipe); - shutdown(tp->s, 2); // Without this, IO thread may get truncated data - closesocket(tp->s); - MG_FREE(tp); - _endthread(); - return NULL; -} - -static void mg_spawn_stdio_thread(sock_t sock, HANDLE hPipe, - void *(*func)(void *)) { - struct mg_threadparam *tp = (struct mg_threadparam *) MG_MALLOC(sizeof(*tp)); - if (tp != NULL) { - tp->s = sock; - tp->hPipe = hPipe; - mg_start_thread(func, tp); - } -} - -static void mg_abs_path(const char *utf8_path, char *abs_path, size_t len) { - wchar_t buf[MAX_PATH_SIZE], buf2[MAX_PATH_SIZE]; - to_wchar(utf8_path, buf, ARRAY_SIZE(buf)); - GetFullPathNameW(buf, ARRAY_SIZE(buf2), buf2, NULL); - WideCharToMultiByte(CP_UTF8, 0, buf2, wcslen(buf2) + 1, abs_path, len, 0, 0); -} - -static int mg_start_process(const char *interp, const char *cmd, - const char *env, const char *envp[], - const char *dir, sock_t sock) { - STARTUPINFOW si; - PROCESS_INFORMATION pi; - HANDLE a[2], b[2], me = GetCurrentProcess(); - wchar_t wcmd[MAX_PATH_SIZE], full_dir[MAX_PATH_SIZE]; - char buf[MAX_PATH_SIZE], buf2[MAX_PATH_SIZE], buf5[MAX_PATH_SIZE], - buf4[MAX_PATH_SIZE], cmdline[MAX_PATH_SIZE]; - DWORD flags = DUPLICATE_CLOSE_SOURCE | DUPLICATE_SAME_ACCESS; - FILE *fp; - - memset(&si, 0, sizeof(si)); - memset(&pi, 0, sizeof(pi)); - - si.cb = sizeof(si); - si.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW; - si.wShowWindow = SW_HIDE; - si.hStdError = GetStdHandle(STD_ERROR_HANDLE); - - CreatePipe(&a[0], &a[1], NULL, 0); - CreatePipe(&b[0], &b[1], NULL, 0); - DuplicateHandle(me, a[0], me, &si.hStdInput, 0, TRUE, flags); - DuplicateHandle(me, b[1], me, &si.hStdOutput, 0, TRUE, flags); - - if (interp == NULL && (fp = fopen(cmd, "r")) != NULL) { - buf[0] = buf[1] = '\0'; - fgets(buf, sizeof(buf), fp); - buf[sizeof(buf) - 1] = '\0'; - if (buf[0] == '#' && buf[1] == '!') { - interp = buf + 2; - /* Trim leading spaces: https://github.com/cesanta/mongoose/issues/489 */ - while (*interp != '\0' && isspace(*(unsigned char *) interp)) { - interp++; - } - } - fclose(fp); - } - - snprintf(buf, sizeof(buf), "%s/%s", dir, cmd); - mg_abs_path(buf, buf2, ARRAY_SIZE(buf2)); - - mg_abs_path(dir, buf5, ARRAY_SIZE(buf5)); - to_wchar(dir, full_dir, ARRAY_SIZE(full_dir)); - - if (interp != NULL) { - mg_abs_path(interp, buf4, ARRAY_SIZE(buf4)); - snprintf(cmdline, sizeof(cmdline), "%s \"%s\"", buf4, buf2); - } else { - snprintf(cmdline, sizeof(cmdline), "\"%s\"", buf2); - } - to_wchar(cmdline, wcmd, ARRAY_SIZE(wcmd)); - - if (CreateProcessW(NULL, wcmd, NULL, NULL, TRUE, CREATE_NEW_PROCESS_GROUP, - (void *) env, full_dir, &si, &pi) != 0) { - mg_spawn_stdio_thread(sock, a[1], mg_push_to_stdin); - mg_spawn_stdio_thread(sock, b[0], mg_pull_from_stdout); - - CloseHandle(si.hStdOutput); - CloseHandle(si.hStdInput); - - CloseHandle(pi.hThread); - CloseHandle(pi.hProcess); - } else { - CloseHandle(a[1]); - CloseHandle(b[0]); - closesocket(sock); - } - DBG(("CGI command: [%ls] -> %p", wcmd, pi.hProcess)); - - /* Not closing a[0] and b[1] because we've used DUPLICATE_CLOSE_SOURCE */ - (void) envp; - return (pi.hProcess != NULL); -} -#else -static int mg_start_process(const char *interp, const char *cmd, - const char *env, const char *envp[], - const char *dir, sock_t sock) { - char buf[500]; - pid_t pid = fork(); - (void) env; - - if (pid == 0) { - /* - * In Linux `chdir` declared with `warn_unused_result` attribute - * To shutup compiler we have yo use result in some way - */ - int tmp = chdir(dir); - (void) tmp; - (void) dup2(sock, 0); - (void) dup2(sock, 1); - closesocket(sock); - - /* - * After exec, all signal handlers are restored to their default values, - * with one exception of SIGCHLD. According to POSIX.1-2001 and Linux's - * implementation, SIGCHLD's handler will leave unchanged after exec - * if it was set to be ignored. Restore it to default action. - */ - signal(SIGCHLD, SIG_DFL); - - if (interp == NULL) { - execle(cmd, cmd, (char *) 0, envp); /* (char *) 0 to squash warning */ - } else { - execle(interp, interp, cmd, (char *) 0, envp); - } - snprintf(buf, sizeof(buf), - "Status: 500\r\n\r\n" - "500 Server Error: %s%s%s: %s", - interp == NULL ? "" : interp, interp == NULL ? "" : " ", cmd, - strerror(errno)); - send(1, buf, strlen(buf), 0); - exit(EXIT_FAILURE); /* exec call failed */ - } - - return (pid != 0); -} -#endif /* _WIN32 */ - -/* - * Append VARIABLE=VALUE\0 string to the buffer, and add a respective - * pointer into the vars array. - */ -static char *mg_addenv(struct mg_cgi_env_block *block, const char *fmt, ...) { - int n, space; - char *added = block->buf + block->len; - va_list ap; - - /* Calculate how much space is left in the buffer */ - space = sizeof(block->buf) - (block->len + 2); - if (space > 0) { - /* Copy VARIABLE=VALUE\0 string into the free space */ - va_start(ap, fmt); - n = vsnprintf(added, (size_t) space, fmt, ap); - va_end(ap); - - /* Make sure we do not overflow buffer and the envp array */ - if (n > 0 && n + 1 < space && - block->nvars < (int) ARRAY_SIZE(block->vars) - 2) { - /* Append a pointer to the added string into the envp array */ - block->vars[block->nvars++] = added; - /* Bump up used length counter. Include \0 terminator */ - block->len += n + 1; - } - } - - return added; -} - -static void mg_addenv2(struct mg_cgi_env_block *blk, const char *name) { - const char *s; - if ((s = getenv(name)) != NULL) mg_addenv(blk, "%s=%s", name, s); -} - -static void mg_prepare_cgi_environment(struct mg_connection *nc, - const char *prog, - const struct mg_str *path_info, - const struct http_message *hm, - const struct mg_serve_http_opts *opts, - struct mg_cgi_env_block *blk) { - const char *s; - struct mg_str *h; - char *p; - size_t i; - char buf[100]; - - blk->len = blk->nvars = 0; - blk->nc = nc; - - if ((s = getenv("SERVER_NAME")) != NULL) { - mg_addenv(blk, "SERVER_NAME=%s", s); - } else { - mg_sock_to_str(nc->sock, buf, sizeof(buf), 3); - mg_addenv(blk, "SERVER_NAME=%s", buf); - } - mg_addenv(blk, "SERVER_ROOT=%s", opts->document_root); - mg_addenv(blk, "DOCUMENT_ROOT=%s", opts->document_root); - mg_addenv(blk, "SERVER_SOFTWARE=%s/%s", "Mongoose", MG_VERSION); - - /* Prepare the environment block */ - mg_addenv(blk, "%s", "GATEWAY_INTERFACE=CGI/1.1"); - mg_addenv(blk, "%s", "SERVER_PROTOCOL=HTTP/1.1"); - mg_addenv(blk, "%s", "REDIRECT_STATUS=200"); /* For PHP */ - - mg_addenv(blk, "REQUEST_METHOD=%.*s", (int) hm->method.len, hm->method.p); - - mg_addenv(blk, "REQUEST_URI=%.*s%s%.*s", (int) hm->uri.len, hm->uri.p, - hm->query_string.len == 0 ? "" : "?", (int) hm->query_string.len, - hm->query_string.p); - - mg_conn_addr_to_str(nc, buf, sizeof(buf), - MG_SOCK_STRINGIFY_REMOTE | MG_SOCK_STRINGIFY_IP); - mg_addenv(blk, "REMOTE_ADDR=%s", buf); - mg_conn_addr_to_str(nc, buf, sizeof(buf), MG_SOCK_STRINGIFY_PORT); - mg_addenv(blk, "SERVER_PORT=%s", buf); - - s = hm->uri.p + hm->uri.len - path_info->len - 1; - if (*s == '/') { - const char *base_name = strrchr(prog, DIRSEP); - mg_addenv(blk, "SCRIPT_NAME=%.*s/%s", (int) (s - hm->uri.p), hm->uri.p, - (base_name != NULL ? base_name + 1 : prog)); - } else { - mg_addenv(blk, "SCRIPT_NAME=%.*s", (int) (s - hm->uri.p + 1), hm->uri.p); - } - mg_addenv(blk, "SCRIPT_FILENAME=%s", prog); - - if (path_info != NULL && path_info->len > 0) { - mg_addenv(blk, "PATH_INFO=%.*s", (int) path_info->len, path_info->p); - /* Not really translated... */ - mg_addenv(blk, "PATH_TRANSLATED=%.*s", (int) path_info->len, path_info->p); - } - -#if MG_ENABLE_SSL - mg_addenv(blk, "HTTPS=%s", nc->ssl != NULL ? "on" : "off"); -#else - mg_addenv(blk, "HTTPS=off"); -#endif - - if ((h = mg_get_http_header((struct http_message *) hm, "Content-Type")) != - NULL) { - mg_addenv(blk, "CONTENT_TYPE=%.*s", (int) h->len, h->p); - } - - if (hm->query_string.len > 0) { - mg_addenv(blk, "QUERY_STRING=%.*s", (int) hm->query_string.len, - hm->query_string.p); - } - - if ((h = mg_get_http_header((struct http_message *) hm, "Content-Length")) != - NULL) { - mg_addenv(blk, "CONTENT_LENGTH=%.*s", (int) h->len, h->p); - } - - mg_addenv2(blk, "PATH"); - mg_addenv2(blk, "TMP"); - mg_addenv2(blk, "TEMP"); - mg_addenv2(blk, "TMPDIR"); - mg_addenv2(blk, "PERLLIB"); - mg_addenv2(blk, MG_ENV_EXPORT_TO_CGI); - -#if defined(_WIN32) - mg_addenv2(blk, "COMSPEC"); - mg_addenv2(blk, "SYSTEMROOT"); - mg_addenv2(blk, "SystemDrive"); - mg_addenv2(blk, "ProgramFiles"); - mg_addenv2(blk, "ProgramFiles(x86)"); - mg_addenv2(blk, "CommonProgramFiles(x86)"); -#else - mg_addenv2(blk, "LD_LIBRARY_PATH"); -#endif /* _WIN32 */ - - /* Add all headers as HTTP_* variables */ - for (i = 0; hm->header_names[i].len > 0; i++) { - p = mg_addenv(blk, "HTTP_%.*s=%.*s", (int) hm->header_names[i].len, - hm->header_names[i].p, (int) hm->header_values[i].len, - hm->header_values[i].p); - - /* Convert variable name into uppercase, and change - to _ */ - for (; *p != '=' && *p != '\0'; p++) { - if (*p == '-') *p = '_'; - *p = (char) toupper(*(unsigned char *) p); - } - } - - blk->vars[blk->nvars++] = NULL; - blk->buf[blk->len++] = '\0'; -} - -static void mg_cgi_ev_handler(struct mg_connection *cgi_nc, int ev, - void *ev_data) { - struct mg_connection *nc = (struct mg_connection *) cgi_nc->user_data; - (void) ev_data; - - if (nc == NULL) return; - - switch (ev) { - case MG_EV_RECV: - /* - * CGI script does not output reply line, like "HTTP/1.1 CODE XXXXX\n" - * It outputs headers, then body. Headers might include "Status" - * header, which changes CODE, and it might include "Location" header - * which changes CODE to 302. - * - * Therefore we do not send the output from the CGI script to the user - * until all CGI headers are received. - * - * Here we parse the output from the CGI script, and if all headers has - * been received, send appropriate reply line, and forward all - * received headers to the client. - */ - if (nc->flags & MG_F_USER_1) { - struct mbuf *io = &cgi_nc->recv_mbuf; - int len = mg_http_get_request_len(io->buf, io->len); - - if (len == 0) break; - if (len < 0 || io->len > MG_MAX_HTTP_REQUEST_SIZE) { - cgi_nc->flags |= MG_F_CLOSE_IMMEDIATELY; - mg_http_send_error(nc, 500, "Bad headers"); - } else { - struct http_message hm; - struct mg_str *h; - mg_http_parse_headers(io->buf, io->buf + io->len, io->len, &hm); - if (mg_get_http_header(&hm, "Location") != NULL) { - mg_printf(nc, "%s", "HTTP/1.1 302 Moved\r\n"); - } else if ((h = mg_get_http_header(&hm, "Status")) != NULL) { - mg_printf(nc, "HTTP/1.1 %.*s\r\n", (int) h->len, h->p); - } else { - mg_printf(nc, "%s", "HTTP/1.1 200 OK\r\n"); - } - } - nc->flags &= ~MG_F_USER_1; - } - if (!(nc->flags & MG_F_USER_1)) { - mg_forward(cgi_nc, nc); - } - break; - case MG_EV_CLOSE: - mg_http_free_proto_data_cgi(&mg_http_get_proto_data(cgi_nc)->cgi); - nc->flags |= MG_F_SEND_AND_CLOSE; - break; - } -} - -static void mg_handle_cgi(struct mg_connection *nc, const char *prog, - const struct mg_str *path_info, - const struct http_message *hm, - const struct mg_serve_http_opts *opts) { - struct mg_cgi_env_block blk; - char dir[MAX_PATH_SIZE]; - const char *p; - sock_t fds[2]; - - DBG(("%p [%s]", nc, prog)); - mg_prepare_cgi_environment(nc, prog, path_info, hm, opts, &blk); - /* - * CGI must be executed in its own directory. 'dir' must point to the - * directory containing executable program, 'p' must point to the - * executable program name relative to 'dir'. - */ - if ((p = strrchr(prog, DIRSEP)) == NULL) { - snprintf(dir, sizeof(dir), "%s", "."); - } else { - snprintf(dir, sizeof(dir), "%.*s", (int) (p - prog), prog); - prog = p + 1; - } - - /* - * Try to create socketpair in a loop until success. mg_socketpair() - * can be interrupted by a signal and fail. - * TODO(lsm): use sigaction to restart interrupted syscall - */ - do { - mg_socketpair(fds, SOCK_STREAM); - } while (fds[0] == INVALID_SOCKET); - - if (mg_start_process(opts->cgi_interpreter, prog, blk.buf, blk.vars, dir, - fds[1]) != 0) { - size_t n = nc->recv_mbuf.len - (hm->message.len - hm->body.len); - struct mg_connection *cgi_nc = - mg_add_sock(nc->mgr, fds[0], mg_cgi_ev_handler); - struct mg_http_proto_data *cgi_pd = mg_http_get_proto_data(cgi_nc); - cgi_pd->cgi.cgi_nc = cgi_nc; - cgi_pd->cgi.cgi_nc->user_data = nc; - nc->flags |= MG_F_USER_1; - /* Push POST data to the CGI */ - if (n > 0 && n < nc->recv_mbuf.len) { - mg_send(cgi_pd->cgi.cgi_nc, hm->body.p, n); - } - mbuf_remove(&nc->recv_mbuf, nc->recv_mbuf.len); - } else { - closesocket(fds[0]); - mg_http_send_error(nc, 500, "CGI failure"); - } - -#ifndef _WIN32 - closesocket(fds[1]); /* On Windows, CGI stdio thread closes that socket */ -#endif -} -#endif - static int mg_get_month_index(const char *s) { static const char *month_names[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; @@ -7262,7 +6787,7 @@ MG_INTERNAL void mg_send_http_file(struct mg_connection *nc, char *path, opts->per_directory_auth_file, 0)) { mg_http_send_digest_auth_request(nc, opts->auth_domain); } else if (is_cgi) { -#if !MG_DISABLE_CGI +#if MG_ENABLE_CGI mg_handle_cgi(nc, index_file ? index_file : path, path_info, hm, opts); #else mg_http_send_error(nc, 501, NULL); @@ -7591,6 +7116,493 @@ void mg_register_http_endpoint(struct mg_connection *nc, const char *uri_path, #endif /* MG_DISABLE_HTTP */ #ifdef MG_MODULE_LINES +#line 1 "mongoose/src/http_cgi.c" +#endif +/* + * Copyright (c) 2014-2016 Cesanta Software Limited + * All rights reserved + */ + +#if !MG_DISABLE_HTTP && MG_ENABLE_CGI + +/* + * This structure helps to create an environment for the spawned CGI program. + * Environment is an array of "VARIABLE=VALUE\0" ASCIIZ strings, + * last element must be NULL. + * However, on Windows there is a requirement that all these VARIABLE=VALUE\0 + * strings must reside in a contiguous buffer. The end of the buffer is + * marked by two '\0' characters. + * We satisfy both worlds: we create an envp array (which is vars), all + * entries are actually pointers inside buf. + */ +struct mg_cgi_env_block { + struct mg_connection *nc; + char buf[MG_CGI_ENVIRONMENT_SIZE]; /* Environment buffer */ + const char *vars[MG_MAX_CGI_ENVIR_VARS]; /* char *envp[] */ + int len; /* Space taken */ + int nvars; /* Number of variables in envp[] */ +}; + +#ifdef _WIN32 +struct mg_threadparam { + sock_t s; + HANDLE hPipe; +}; + +static int mg_wait_until_ready(sock_t sock, int for_read) { + fd_set set; + FD_ZERO(&set); + FD_SET(sock, &set); + return select(sock + 1, for_read ? &set : 0, for_read ? 0 : &set, 0, 0) == 1; +} + +static void *mg_push_to_stdin(void *arg) { + struct mg_threadparam *tp = (struct mg_threadparam *) arg; + int n, sent, stop = 0; + DWORD k; + char buf[BUFSIZ]; + + while (!stop && mg_wait_until_ready(tp->s, 1) && + (n = recv(tp->s, buf, sizeof(buf), 0)) > 0) { + if (n == -1 && GetLastError() == WSAEWOULDBLOCK) continue; + for (sent = 0; !stop && sent < n; sent += k) { + if (!WriteFile(tp->hPipe, buf + sent, n - sent, &k, 0)) stop = 1; + } + } + DBG(("%s", "FORWARED EVERYTHING TO CGI")); + CloseHandle(tp->hPipe); + MG_FREE(tp); + _endthread(); + return NULL; +} + +static void *mg_pull_from_stdout(void *arg) { + struct mg_threadparam *tp = (struct mg_threadparam *) arg; + int k = 0, stop = 0; + DWORD n, sent; + char buf[BUFSIZ]; + + while (!stop && ReadFile(tp->hPipe, buf, sizeof(buf), &n, NULL)) { + for (sent = 0; !stop && sent < n; sent += k) { + if (mg_wait_until_ready(tp->s, 0) && + (k = send(tp->s, buf + sent, n - sent, 0)) <= 0) + stop = 1; + } + } + DBG(("%s", "EOF FROM CGI")); + CloseHandle(tp->hPipe); + shutdown(tp->s, 2); // Without this, IO thread may get truncated data + closesocket(tp->s); + MG_FREE(tp); + _endthread(); + return NULL; +} + +static void mg_spawn_stdio_thread(sock_t sock, HANDLE hPipe, + void *(*func)(void *)) { + struct mg_threadparam *tp = (struct mg_threadparam *) MG_MALLOC(sizeof(*tp)); + if (tp != NULL) { + tp->s = sock; + tp->hPipe = hPipe; + mg_start_thread(func, tp); + } +} + +static void mg_abs_path(const char *utf8_path, char *abs_path, size_t len) { + wchar_t buf[MAX_PATH_SIZE], buf2[MAX_PATH_SIZE]; + to_wchar(utf8_path, buf, ARRAY_SIZE(buf)); + GetFullPathNameW(buf, ARRAY_SIZE(buf2), buf2, NULL); + WideCharToMultiByte(CP_UTF8, 0, buf2, wcslen(buf2) + 1, abs_path, len, 0, 0); +} + +static int mg_start_process(const char *interp, const char *cmd, + const char *env, const char *envp[], + const char *dir, sock_t sock) { + STARTUPINFOW si; + PROCESS_INFORMATION pi; + HANDLE a[2], b[2], me = GetCurrentProcess(); + wchar_t wcmd[MAX_PATH_SIZE], full_dir[MAX_PATH_SIZE]; + char buf[MAX_PATH_SIZE], buf2[MAX_PATH_SIZE], buf5[MAX_PATH_SIZE], + buf4[MAX_PATH_SIZE], cmdline[MAX_PATH_SIZE]; + DWORD flags = DUPLICATE_CLOSE_SOURCE | DUPLICATE_SAME_ACCESS; + FILE *fp; + + memset(&si, 0, sizeof(si)); + memset(&pi, 0, sizeof(pi)); + + si.cb = sizeof(si); + si.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW; + si.wShowWindow = SW_HIDE; + si.hStdError = GetStdHandle(STD_ERROR_HANDLE); + + CreatePipe(&a[0], &a[1], NULL, 0); + CreatePipe(&b[0], &b[1], NULL, 0); + DuplicateHandle(me, a[0], me, &si.hStdInput, 0, TRUE, flags); + DuplicateHandle(me, b[1], me, &si.hStdOutput, 0, TRUE, flags); + + if (interp == NULL && (fp = fopen(cmd, "r")) != NULL) { + buf[0] = buf[1] = '\0'; + fgets(buf, sizeof(buf), fp); + buf[sizeof(buf) - 1] = '\0'; + if (buf[0] == '#' && buf[1] == '!') { + interp = buf + 2; + /* Trim leading spaces: https://github.com/cesanta/mongoose/issues/489 */ + while (*interp != '\0' && isspace(*(unsigned char *) interp)) { + interp++; + } + } + fclose(fp); + } + + snprintf(buf, sizeof(buf), "%s/%s", dir, cmd); + mg_abs_path(buf, buf2, ARRAY_SIZE(buf2)); + + mg_abs_path(dir, buf5, ARRAY_SIZE(buf5)); + to_wchar(dir, full_dir, ARRAY_SIZE(full_dir)); + + if (interp != NULL) { + mg_abs_path(interp, buf4, ARRAY_SIZE(buf4)); + snprintf(cmdline, sizeof(cmdline), "%s \"%s\"", buf4, buf2); + } else { + snprintf(cmdline, sizeof(cmdline), "\"%s\"", buf2); + } + to_wchar(cmdline, wcmd, ARRAY_SIZE(wcmd)); + + if (CreateProcessW(NULL, wcmd, NULL, NULL, TRUE, CREATE_NEW_PROCESS_GROUP, + (void *) env, full_dir, &si, &pi) != 0) { + mg_spawn_stdio_thread(sock, a[1], mg_push_to_stdin); + mg_spawn_stdio_thread(sock, b[0], mg_pull_from_stdout); + + CloseHandle(si.hStdOutput); + CloseHandle(si.hStdInput); + + CloseHandle(pi.hThread); + CloseHandle(pi.hProcess); + } else { + CloseHandle(a[1]); + CloseHandle(b[0]); + closesocket(sock); + } + DBG(("CGI command: [%ls] -> %p", wcmd, pi.hProcess)); + + /* Not closing a[0] and b[1] because we've used DUPLICATE_CLOSE_SOURCE */ + (void) envp; + return (pi.hProcess != NULL); +} +#else +static int mg_start_process(const char *interp, const char *cmd, + const char *env, const char *envp[], + const char *dir, sock_t sock) { + char buf[500]; + pid_t pid = fork(); + (void) env; + + if (pid == 0) { + /* + * In Linux `chdir` declared with `warn_unused_result` attribute + * To shutup compiler we have yo use result in some way + */ + int tmp = chdir(dir); + (void) tmp; + (void) dup2(sock, 0); + (void) dup2(sock, 1); + closesocket(sock); + + /* + * After exec, all signal handlers are restored to their default values, + * with one exception of SIGCHLD. According to POSIX.1-2001 and Linux's + * implementation, SIGCHLD's handler will leave unchanged after exec + * if it was set to be ignored. Restore it to default action. + */ + signal(SIGCHLD, SIG_DFL); + + if (interp == NULL) { + execle(cmd, cmd, (char *) 0, envp); /* (char *) 0 to squash warning */ + } else { + execle(interp, interp, cmd, (char *) 0, envp); + } + snprintf(buf, sizeof(buf), + "Status: 500\r\n\r\n" + "500 Server Error: %s%s%s: %s", + interp == NULL ? "" : interp, interp == NULL ? "" : " ", cmd, + strerror(errno)); + send(1, buf, strlen(buf), 0); + exit(EXIT_FAILURE); /* exec call failed */ + } + + return (pid != 0); +} +#endif /* _WIN32 */ + +/* + * Append VARIABLE=VALUE\0 string to the buffer, and add a respective + * pointer into the vars array. + */ +static char *mg_addenv(struct mg_cgi_env_block *block, const char *fmt, ...) { + int n, space; + char *added = block->buf + block->len; + va_list ap; + + /* Calculate how much space is left in the buffer */ + space = sizeof(block->buf) - (block->len + 2); + if (space > 0) { + /* Copy VARIABLE=VALUE\0 string into the free space */ + va_start(ap, fmt); + n = vsnprintf(added, (size_t) space, fmt, ap); + va_end(ap); + + /* Make sure we do not overflow buffer and the envp array */ + if (n > 0 && n + 1 < space && + block->nvars < (int) ARRAY_SIZE(block->vars) - 2) { + /* Append a pointer to the added string into the envp array */ + block->vars[block->nvars++] = added; + /* Bump up used length counter. Include \0 terminator */ + block->len += n + 1; + } + } + + return added; +} + +static void mg_addenv2(struct mg_cgi_env_block *blk, const char *name) { + const char *s; + if ((s = getenv(name)) != NULL) mg_addenv(blk, "%s=%s", name, s); +} + +static void mg_prepare_cgi_environment(struct mg_connection *nc, + const char *prog, + const struct mg_str *path_info, + const struct http_message *hm, + const struct mg_serve_http_opts *opts, + struct mg_cgi_env_block *blk) { + const char *s; + struct mg_str *h; + char *p; + size_t i; + char buf[100]; + + blk->len = blk->nvars = 0; + blk->nc = nc; + + if ((s = getenv("SERVER_NAME")) != NULL) { + mg_addenv(blk, "SERVER_NAME=%s", s); + } else { + mg_sock_to_str(nc->sock, buf, sizeof(buf), 3); + mg_addenv(blk, "SERVER_NAME=%s", buf); + } + mg_addenv(blk, "SERVER_ROOT=%s", opts->document_root); + mg_addenv(blk, "DOCUMENT_ROOT=%s", opts->document_root); + mg_addenv(blk, "SERVER_SOFTWARE=%s/%s", "Mongoose", MG_VERSION); + + /* Prepare the environment block */ + mg_addenv(blk, "%s", "GATEWAY_INTERFACE=CGI/1.1"); + mg_addenv(blk, "%s", "SERVER_PROTOCOL=HTTP/1.1"); + mg_addenv(blk, "%s", "REDIRECT_STATUS=200"); /* For PHP */ + + mg_addenv(blk, "REQUEST_METHOD=%.*s", (int) hm->method.len, hm->method.p); + + mg_addenv(blk, "REQUEST_URI=%.*s%s%.*s", (int) hm->uri.len, hm->uri.p, + hm->query_string.len == 0 ? "" : "?", (int) hm->query_string.len, + hm->query_string.p); + + mg_conn_addr_to_str(nc, buf, sizeof(buf), + MG_SOCK_STRINGIFY_REMOTE | MG_SOCK_STRINGIFY_IP); + mg_addenv(blk, "REMOTE_ADDR=%s", buf); + mg_conn_addr_to_str(nc, buf, sizeof(buf), MG_SOCK_STRINGIFY_PORT); + mg_addenv(blk, "SERVER_PORT=%s", buf); + + s = hm->uri.p + hm->uri.len - path_info->len - 1; + if (*s == '/') { + const char *base_name = strrchr(prog, DIRSEP); + mg_addenv(blk, "SCRIPT_NAME=%.*s/%s", (int) (s - hm->uri.p), hm->uri.p, + (base_name != NULL ? base_name + 1 : prog)); + } else { + mg_addenv(blk, "SCRIPT_NAME=%.*s", (int) (s - hm->uri.p + 1), hm->uri.p); + } + mg_addenv(blk, "SCRIPT_FILENAME=%s", prog); + + if (path_info != NULL && path_info->len > 0) { + mg_addenv(blk, "PATH_INFO=%.*s", (int) path_info->len, path_info->p); + /* Not really translated... */ + mg_addenv(blk, "PATH_TRANSLATED=%.*s", (int) path_info->len, path_info->p); + } + +#if MG_ENABLE_SSL + mg_addenv(blk, "HTTPS=%s", nc->ssl != NULL ? "on" : "off"); +#else + mg_addenv(blk, "HTTPS=off"); +#endif + + if ((h = mg_get_http_header((struct http_message *) hm, "Content-Type")) != + NULL) { + mg_addenv(blk, "CONTENT_TYPE=%.*s", (int) h->len, h->p); + } + + if (hm->query_string.len > 0) { + mg_addenv(blk, "QUERY_STRING=%.*s", (int) hm->query_string.len, + hm->query_string.p); + } + + if ((h = mg_get_http_header((struct http_message *) hm, "Content-Length")) != + NULL) { + mg_addenv(blk, "CONTENT_LENGTH=%.*s", (int) h->len, h->p); + } + + mg_addenv2(blk, "PATH"); + mg_addenv2(blk, "TMP"); + mg_addenv2(blk, "TEMP"); + mg_addenv2(blk, "TMPDIR"); + mg_addenv2(blk, "PERLLIB"); + mg_addenv2(blk, MG_ENV_EXPORT_TO_CGI); + +#ifdef _WIN32 + mg_addenv2(blk, "COMSPEC"); + mg_addenv2(blk, "SYSTEMROOT"); + mg_addenv2(blk, "SystemDrive"); + mg_addenv2(blk, "ProgramFiles"); + mg_addenv2(blk, "ProgramFiles(x86)"); + mg_addenv2(blk, "CommonProgramFiles(x86)"); +#else + mg_addenv2(blk, "LD_LIBRARY_PATH"); +#endif /* _WIN32 */ + + /* Add all headers as HTTP_* variables */ + for (i = 0; hm->header_names[i].len > 0; i++) { + p = mg_addenv(blk, "HTTP_%.*s=%.*s", (int) hm->header_names[i].len, + hm->header_names[i].p, (int) hm->header_values[i].len, + hm->header_values[i].p); + + /* Convert variable name into uppercase, and change - to _ */ + for (; *p != '=' && *p != '\0'; p++) { + if (*p == '-') *p = '_'; + *p = (char) toupper(*(unsigned char *) p); + } + } + + blk->vars[blk->nvars++] = NULL; + blk->buf[blk->len++] = '\0'; +} + +static void mg_cgi_ev_handler(struct mg_connection *cgi_nc, int ev, + void *ev_data) { + struct mg_connection *nc = (struct mg_connection *) cgi_nc->user_data; + (void) ev_data; + + if (nc == NULL) return; + + switch (ev) { + case MG_EV_RECV: + /* + * CGI script does not output reply line, like "HTTP/1.1 CODE XXXXX\n" + * It outputs headers, then body. Headers might include "Status" + * header, which changes CODE, and it might include "Location" header + * which changes CODE to 302. + * + * Therefore we do not send the output from the CGI script to the user + * until all CGI headers are received. + * + * Here we parse the output from the CGI script, and if all headers has + * been received, send appropriate reply line, and forward all + * received headers to the client. + */ + if (nc->flags & MG_F_USER_1) { + struct mbuf *io = &cgi_nc->recv_mbuf; + int len = mg_http_get_request_len(io->buf, io->len); + + if (len == 0) break; + if (len < 0 || io->len > MG_MAX_HTTP_REQUEST_SIZE) { + cgi_nc->flags |= MG_F_CLOSE_IMMEDIATELY; + mg_http_send_error(nc, 500, "Bad headers"); + } else { + struct http_message hm; + struct mg_str *h; + mg_http_parse_headers(io->buf, io->buf + io->len, io->len, &hm); + if (mg_get_http_header(&hm, "Location") != NULL) { + mg_printf(nc, "%s", "HTTP/1.1 302 Moved\r\n"); + } else if ((h = mg_get_http_header(&hm, "Status")) != NULL) { + mg_printf(nc, "HTTP/1.1 %.*s\r\n", (int) h->len, h->p); + } else { + mg_printf(nc, "%s", "HTTP/1.1 200 OK\r\n"); + } + } + nc->flags &= ~MG_F_USER_1; + } + if (!(nc->flags & MG_F_USER_1)) { + mg_forward(cgi_nc, nc); + } + break; + case MG_EV_CLOSE: + mg_http_free_proto_data_cgi(&mg_http_get_proto_data(cgi_nc)->cgi); + nc->flags |= MG_F_SEND_AND_CLOSE; + break; + } +} + +MG_INTERNAL void mg_handle_cgi(struct mg_connection *nc, const char *prog, + const struct mg_str *path_info, + const struct http_message *hm, + const struct mg_serve_http_opts *opts) { + struct mg_cgi_env_block blk; + char dir[MAX_PATH_SIZE]; + const char *p; + sock_t fds[2]; + + DBG(("%p [%s]", nc, prog)); + mg_prepare_cgi_environment(nc, prog, path_info, hm, opts, &blk); + /* + * CGI must be executed in its own directory. 'dir' must point to the + * directory containing executable program, 'p' must point to the + * executable program name relative to 'dir'. + */ + if ((p = strrchr(prog, DIRSEP)) == NULL) { + snprintf(dir, sizeof(dir), "%s", "."); + } else { + snprintf(dir, sizeof(dir), "%.*s", (int) (p - prog), prog); + prog = p + 1; + } + + /* + * Try to create socketpair in a loop until success. mg_socketpair() + * can be interrupted by a signal and fail. + * TODO(lsm): use sigaction to restart interrupted syscall + */ + do { + mg_socketpair(fds, SOCK_STREAM); + } while (fds[0] == INVALID_SOCKET); + + if (mg_start_process(opts->cgi_interpreter, prog, blk.buf, blk.vars, dir, + fds[1]) != 0) { + size_t n = nc->recv_mbuf.len - (hm->message.len - hm->body.len); + struct mg_connection *cgi_nc = + mg_add_sock(nc->mgr, fds[0], mg_cgi_ev_handler); + struct mg_http_proto_data *cgi_pd = mg_http_get_proto_data(cgi_nc); + cgi_pd->cgi.cgi_nc = cgi_nc; + cgi_pd->cgi.cgi_nc->user_data = nc; + nc->flags |= MG_F_USER_1; + /* Push POST data to the CGI */ + if (n > 0 && n < nc->recv_mbuf.len) { + mg_send(cgi_pd->cgi.cgi_nc, hm->body.p, n); + } + mbuf_remove(&nc->recv_mbuf, nc->recv_mbuf.len); + } else { + closesocket(fds[0]); + mg_http_send_error(nc, 500, "CGI failure"); + } + +#ifndef _WIN32 + closesocket(fds[1]); /* On Windows, CGI stdio thread closes that socket */ +#endif +} + +MG_INTERNAL void mg_http_free_proto_data_cgi(struct mg_http_proto_data_cgi *d) { + if (d != NULL) { + if (d->cgi_nc != NULL) d->cgi_nc->flags |= MG_F_CLOSE_IMMEDIATELY; + memset(d, 0, sizeof(struct mg_http_proto_data_cgi)); + } +} + +#endif /* MG_ENABLE_HTTP && MG_ENABLE_CGI */ +#ifdef MG_MODULE_LINES #line 1 "mongoose/src/util.c" #endif /* diff --git a/mongoose.h b/mongoose.h index 4a1300f1..e929f0eb 100644 --- a/mongoose.h +++ b/mongoose.h @@ -454,7 +454,6 @@ void mg_lwip_set_keepalive_params(struct mg_connection *nc, int idle, #define MG_DISABLE_SOCKETPAIR 1 #define MG_DISABLE_SYNC_RESOLVER 1 #define MG_DISABLE_POPEN 1 -#define MG_DISABLE_CGI 1 #define MG_DISABLE_DAV 1 #define MG_DISABLE_DIRECTORY_LISTING 1 #define MG_DISABLE_FILESYSTEM 1 @@ -514,7 +513,6 @@ int inet_pton(int af, const char *src, void *dst); #define MG_DISABLE_SOCKETPAIR 1 #define MG_DISABLE_SYNC_RESOLVER 1 #define MG_DISABLE_POPEN 1 -#define MG_DISABLE_CGI 1 /* Only SPIFFS supports directories, SLFS does not. */ #ifndef CC3200_FS_SPIFFS #define MG_DISABLE_DAV 1 @@ -644,7 +642,6 @@ struct dirent *readdir(DIR *dir); #define MG_DISABLE_SOCKETPAIR 1 #define MG_DISABLE_SYNC_RESOLVER 1 #define MG_DISABLE_POPEN 1 -#define MG_DISABLE_CGI 1 #define MG_DISABLE_DAV 1 #define MG_DISABLE_DIRECTORY_LISTING 1 @@ -1186,10 +1183,6 @@ const char *c_strnstr(const char *s, const char *find, size_t slen); #ifndef CS_MONGOOSE_SRC_FEATURES_H_ #define CS_MONGOOSE_SRC_FEATURES_H_ -#ifndef MG_DISABLE_CGI -#define MG_DISABLE_CGI 0 -#endif - #ifndef MG_DISABLE_DIRECTORY_LISTING #define MG_DISABLE_DIRECTORY_LISTING 0 #endif @@ -1262,6 +1255,10 @@ const char *c_strnstr(const char *s, const char *find, size_t slen); #define MG_DISABLE_WS_RANDOM_MASK 0 #endif +#ifndef MG_ENABLE_CGI +#define MG_ENABLE_CGI (CS_PLATFORM == CS_P_UNIX || CS_PLATFORM == CS_P_WINDOWS) +#endif + #ifndef MG_ENABLE_COAP #define MG_ENABLE_COAP 0 #endif