#include "cgi.h"
#include <sys/stat.h>
#include <sys/wait.h>
#ifdef __NetBSD__
#ifndef PATH_MAX
#include <sys/syslimits.h>
#endif
#endif
#ifdef __linux__
#ifndef PATH_MAX
#include <linux/limits.h>
#endif
#endif
#if defined(__sun) && defined(__illumos__)
#ifndef PATH_MAX
#include <limits.h>
#endif
#endif
#include <errno.h>
#include <signal.h>
#include <stdbool.h>
#include <string.h>
#include <sysexits.h>
#include <unistd.h>
#include "header.h"
#include "response.h"
#include "utils.h"
/*
* Upper bound of 1 MB, to be increased if we need to read more data
*/
#define MAX_BUFFER_SIZE 1 * 1024 * 1024
/*
* Returns true/false depending on whether the provided path starts with
* '/cgi-bin'. Checks against '/cgi-bin/' if the string path is larger than
* the length of `/cgi-bin'.
*
* Also stores the main path of the request up to a ? in cgi->request_path,
* and stores everything past ? in cgi->query_path;
*/
bool
starts_with_cgibin(const char* string)
{
char* cgi_prefix_short = "/cgi-bin";
size_t cgi_prefix_short_len = strlen(cgi_prefix_short);
/* In the event a query string is passed */
char* cgi_prefix_qry = "/cgi-bin?";
size_t cgi_prefix_qry_len = strlen(cgi_prefix_qry);
char* cgi_prefix_long = "/cgi-bin/";
size_t cgi_prefix_long_len = strlen(cgi_prefix_long);
size_t input_len = strlen(string);
if (input_len < cgi_prefix_short_len)
{
return false;
}
if (input_len == cgi_prefix_short_len)
{
return strncmp(string, cgi_prefix_short, cgi_prefix_short_len) == 0;
}
/*
* In the off-chance that someone passes a string that is like
* '/cgi-binaajsodijaoisjd', we verify it against the longer version,
* as the only other acceptable string besides '/cgi-bin' is '/cgi-bin/'
*/
if (string[8] == '?')
{
return strncmp(string, cgi_prefix_qry, cgi_prefix_qry_len) == 0;
}
else
{
return strncmp(string, cgi_prefix_long, cgi_prefix_long_len) == 0;
}
}
/*
* Parses a REQUEST struct into a CGI struct. Ideally only called once.
* Also separates paths from query strings. Assumes the CGI* is already
* zero-initialized (e.g. = {0})
*
* Takes a REQUEST*, a CGI*, and a flag_control*.
*/
void
convert_http_to_cgi_request(REQUEST* request, CGI* cgi, struct flag_control* fc)
{
cgi->http_request = request;
strncpy(cgi->request_method, request->request_type == GET ? "GET" : "HEAD",
strlen(request->request_type == GET ? "GET" : "HEAD"));
sprintf(cgi->server_port, "%d", fc->port);
strcpy(cgi->server_protocol, "HTTP/1.0");
sprintf(cgi->server_software, "%s/%.1f", SERVER_NAME, SERVER_VERSION);
char* cgi_prefix_1 = "/cgi-bin?";
char* cgi_prefix_2 = "/cgi-bin";
if (strncmp(request->path, cgi_prefix_1, strlen(cgi_prefix_1)) == 0)
{
sprintf(cgi->path, "/");
/* there's 9 characters in cgi_prefix_1 before the NULL byte */
sprintf(cgi->query_string, "%s", request->path + 9);
}
/* Complete equality check, in-case someone requested just "/cgi-bin" */
else if (strcmp(request->path, cgi_prefix_2) == 0)
{
sprintf(cgi->path, "/");
}
else
{
/*
* path[0] is a slash, and we want to ignore it
* we know this is still a valid path regardless, so there must be a
*/
int slash_idx = -1;
int query_idx = -1;
for (size_t idx = 1; idx < strlen(request->path); idx++)
{
if ((request->path)[idx] == '/')
{
slash_idx = idx;
break;
}
}
for (size_t idx = 1; idx < strlen(request->path); idx++)
{
if ((request->path)[idx] == '?')
{
query_idx = idx;
break;
}
}
if (query_idx < 0)
{
strncpy(cgi->path, request->path + slash_idx,
strlen(request->path + slash_idx));
}
else
{
strncpy(cgi->path, request->path + slash_idx,
query_idx - slash_idx);
strncpy(cgi->query_string,
request->path + query_idx + 1, /* ignore leading ? */
strlen(request->path + query_idx + 1));
}
}
return;
}
/*
* Attempts to execute the CGI at the given directory. Makes a faithful
* assumption that the .cgi file requested already exists.
*
* Returns the status code sent to the client.
*/
int
exec_cgi_and_respond(REQUEST* request, CGI* cgi, bool headers_only)
{
int pipe_fd[2];
char read_buffer[BUFSIZ] = {0};
pid_t fork_res;
struct stat st;
int status;
if (stat(cgi->parsed_path, &st) == -1)
{
status = 500;
send_status_and_headers(request, status);
fprintf(stderr, "[Debug] stat failed: %s\n", strerror(errno));
return status;
}
if (cgi->http_request->if_modified_since_time != 0 &&
cgi->http_request->if_modified_since_time > st.st_mtime)
{
status = 304;
send_status_and_headers(request, status);
return status;
}
if (S_ISDIR(st.st_mode))
{
return send_dir_entries(request, headers_only);
}
if (pipe(pipe_fd) == -1)
{
status = 500;
send_status_and_headers(request, status);
fprintf(stderr, "[Debug] pipe failed: %s\n", strerror(errno));
return status;
}
/* we don't want the child/cgi process to inherit our custom handler for
* SIGPIPE */
restore_default_sigpipe();
fork_res = fork();
if (fork_res == -1)
{
/* restore the signal handler & close the pipes */
ignore_sigpipe();
(void)close(pipe_fd[0]);
(void)close(pipe_fd[1]);
status = 500;
send_status_and_headers(request, status);
fprintf(stderr, "[Debug] fork failed: %s\n", strerror(errno));
return status;
}
if (fork_res == 0)
{
/* RFC3875 requires all params as environment variables on UNIX/POSIX */
setenv("QUERY_STRING", cgi->query_string, 1); /* overwrite to be safe */
setenv("REQUEST_METHOD", cgi->request_method, 1);
setenv("SERVER_PORT", cgi->server_port, 1);
setenv("SERVER_PROTOCOL", cgi->server_protocol, 1);
setenv("SERVER_SOFTWARE", cgi->server_software, 1);
(void)close(pipe_fd[0]); /* Child process, no need to read */
if (dup2(pipe_fd[1], STDOUT_FILENO) == -1)
{
(void)close(pipe_fd[1]);
_exit(EX_IOERR);
}
(void)close(pipe_fd[1]);
char* cgi_argv[] = {cgi->parsed_path, NULL};
execv(cgi->parsed_path, cgi_argv);
_exit(
EX_IOERR); /* Unreachable unless something catastrophic happened */
}
else
{
/* ignore the signal once again */
ignore_sigpipe();
(void)close(pipe_fd[1]); /* Parent process, no need to write */
int read_bytes;
char* buffer = (char*)calloc(MAX_BUFFER_SIZE, sizeof(char));
if (buffer == NULL)
{
status = 500;
send_status_and_headers(request, status);
fprintf(stderr, "[Debug] calloc failed: %s\n", strerror(errno));
return status;
}
size_t buffer_size = MAX_BUFFER_SIZE;
size_t buffer_len = 0;
/*
* Copying everything to a malloc'ed buffer for the following reasons:
* 1. .cgi's are not guaranteed to have all the headers that the sws
* manpage requires us to send. This way, we can parse them nicely
* 2. If something catastrophic happens like a 500 error, I'd rather the
* client not know what happened (security concern, if anything), so
* I'd just default to a blank 500 error to return to the user.
*/
status = 200; /* faithful assumption, not always provided with cgi's */
while (true)
{
read_bytes = read(pipe_fd[0], read_buffer, BUFSIZ);
if (read_bytes < 0)
{
status = 500;
send_status_and_headers(request, status);
fprintf(stderr, "[Debug] fork failed: %s\n", strerror(errno));
free(buffer);
(void)close(pipe_fd[0]);
/* In the event the child process is writing (or wants to) a
* lot of data to the pipe and it blocks, closing the pipe
* should send SIGPIPE to the process whose default behavior
* is to terminate it, and since we restore the default
* behavior of SIGPIPE for cgi's, it should be okay --
* unless the cgi process itself has custom logic to ignore
* SIGPIPE, in which case it could potentially block forever
* (if programmed poorly, namely). As such, we explicitly
* termiante the child process */
(void)kill(fork_res, SIGKILL);
(void)waitpid(fork_res, NULL, 0);
return status;
}
if (read_bytes == 0)
{
break;
}
/*
* RFC 1945 requires that headers be sent as CRLF. RFC 3875,
* however, doesn't mention CRLF or LF endings at all. So, because
* of that, I'd rather play it safe and just remove carriage
* returns. That's what this bit does. - AJ
*/
int idx = 0;
while (idx < read_bytes)
{
if (read_buffer[idx] == '\r' && idx + 1 < read_bytes)
{
/*
* Neat lil function I found in the SEE ALSO section when
* looking at memcpy's manpage. We just shuffle down the
* string!
*/
memmove(read_buffer + idx, read_buffer + idx + 1,
read_bytes - idx - 1);
// read_buffer[read_bytes] = '\0';
read_bytes -= 1;
fprintf(stderr, "\\r found, truncated to %d bytes\n",
read_bytes);
}
else
{
idx++;
}
}
if (buffer_len + read_bytes > buffer_size)
{
/*
* Kudos to https://stackoverflow.com/a/1986572 for illustrating
* best how to handle bad reallocs.
*/
char* new_buffer =
realloc(buffer, buffer_size + MAX_BUFFER_SIZE);
if (new_buffer == NULL)
{
status = 500;
send_status_and_headers(request, status);
fprintf(stderr, "[Debug] realloc failed: %s\n",
strerror(errno));
free(buffer);
(void)close(pipe_fd[0]);
(void)kill(fork_res, SIGKILL);
(void)waitpid(fork_res, NULL, 0);
return status;
}
buffer = new_buffer;
/*
* I'm a stickler for using calloc and zero-initializing my
* buffer, so this line is how I do that when reallocating
*/
memset(buffer + buffer_size, 0, MAX_BUFFER_SIZE);
buffer_size += MAX_BUFFER_SIZE;
}
memcpy(buffer + buffer_len, read_buffer, read_bytes);
buffer_len += read_bytes;
/* Same reason here, I just really like zero-initialized buffers */
memset(read_buffer, 0, sizeof(read_buffer));
continue;
}
size_t body_start = 0;
for (size_t idx = 0; idx < buffer_len; idx++)
{
fprintf(stderr, "%c", buffer[idx]);
if (buffer[idx] == '\n' && idx + 1 < buffer_len &&
buffer[idx + 1] == '\n')
{
body_start = idx + 2;
break;
}
if (strncmp(buffer + idx, "Status:", 7) == 0)
{
/*
* strtol ignores leading spaces, and stops if it finds a
* non-number character it shouldn't parse, so I pass in the
* point after "Status:"
*/
status = strtol(buffer + idx + 7, NULL, 10);
}
else if (strncmp(buffer + idx, "Content-Type:", 13) == 0)
{
size_t end_idx = idx + 13;
while (end_idx < buffer_len && buffer[end_idx] != '\n')
{
end_idx++;
}
strncpy(cgi->content_type, buffer + idx + 13,
end_idx - (idx + 13));
}
}
(void)waitpid(fork_res, NULL, 0);
(void)close(pipe_fd[0]);
cgi->content_length = buffer_len - body_start;
send_status_and_headers_cgi(cgi, status);
if (!headers_only)
{
int bytes_written =
safe_write(cgi->http_request->client_fd, buffer + body_start,
buffer_len - body_start);
if (bytes_written == -1)
{
fprintf(stderr, "[Debug] safe_write failed: %s\n",
strerror(errno));
}
}
free(buffer);
}
return 200;
}