#ifdef __linux__
#define _XOPEN_SOURCE
#define _DEFAULT_SOURCE
#endif
#include "utils.h"
#include <sys/stat.h>
#ifdef __NetBSD__
#include <sys/syslimits.h>
#endif
#if defined(__sun) && defined(__illumos__)
#include "/opt/magic/include/magic.h"
#ifndef PATH_MAX
#include <limits.h>
#endif
#endif
#ifdef __linux__
#ifndef PATH_MAX
#include <linux/limits.h> /* for PATH_MAX */
#endif
const char* process_name = NULL;
const char*
getprogname()
{
return process_name;
}
#endif
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <pwd.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include "cgi.h"
/* Attempted to write this function in somewhat accordance with
* https://datatracker.ietf.org/doc/html/rfc3986#section-5.2.4 but kind of not
* really due to it's complexity, just to have a baseline behavior and
* implementation reccomendation(s). There is a bug where if the user provides
* /~<user>/../ they will end up in the server's docroot, which while not
* necessarily intuitive, still does not allow for any breakouts to occur (the
* logic also naturally follows given how the parsing works, especially when
* looking at the spec above). It also makes sense when consider the user
* doesn't necessarily know how many terms /~<user> expands into server-side, so
* for them it's a single term.
* It's not a bug, it's a feature!
*/
void
try_remove_dots(char* path)
{
if (path == NULL || path[0] != '/')
{
fprintf(stderr, "[Debug] try_remove_dots: path is null or does not "
"begin with a /. Ignoring... \n");
return;
}
char tmp[PATH_MAX] = {0};
int index = 0;
tmp[0] = '/';
index++;
const char* p = path + 1;
/* glorified pointer for components: /a/b/c/d, 1 would be b, 2 = c, etc.*/
int entry_offset = 0;
while (*p != '\0')
{
/* find next entry */
const char* start = p;
while (*p != '\0' && *p != '/')
{
p++;
}
int len = p - start;
if (*p == '/')
{
p++;
}
/* for handling // */
if (len == 0)
{
continue;
}
if (len == 1 && start[0] == '.')
{
continue;
}
if (len == 2 && start[0] == '.' && start[1] == '.')
{
if (entry_offset > 0)
{
/* attempt to "pop" last entry */
index--; /* also to handle // */
while (index > 0 && tmp[index - 1] != '/')
{
index--;
}
entry_offset--;
}
continue;
}
if (index > 1 && tmp[index - 1] != '/')
{
tmp[index] = '/';
index++;
}
memcpy(tmp + index, start, len);
index += len;
tmp[index] = '/';
index++;
entry_offset++;
}
/* remove trailing / unless the request is literally just a / */
if (index > 1 && tmp[index - 1] == '/')
{
index--;
}
tmp[index] = '\0';
strncpy(path, tmp, PATH_MAX);
}
static void
sigpipe_ignore(int signo)
{
(void)signo;
}
static void (*default_sigpipe_handler)(int) = NULL;
void
ignore_sigpipe()
{
if ((default_sigpipe_handler = signal(SIGPIPE, sigpipe_ignore)) == SIG_ERR)
{
(void)fprintf(stderr,
"%s: Unable to ignore SIGPIPE. Cannot continue.\n",
getprogname());
exit(EXIT_FAILURE);
}
}
void
restore_default_sigpipe()
{
if (default_sigpipe_handler == NULL)
{
fprintf(stderr, "[Debug] warning - restore_default_sigpipe: default "
"handler for SIGPIPE is NULL / hasn't been set yet!\n");
}
if (signal(SIGPIPE, default_sigpipe_handler) == SIG_ERR)
{
(void)fprintf(stderr,
"%s: Unable to restore SIGPIPE's default handler. Cannot "
"continue.\n",
getprogname());
exit(EXIT_FAILURE);
}
}
/* searches directory for index.html file */
int
search_dir(char* path)
{
DIR* dir = opendir(path);
if (dir == NULL)
{
return 0;
}
struct dirent* entry;
while ((entry = readdir(dir)) != NULL)
{
if (strcmp(entry->d_name, "index.html") == 0)
{
closedir(dir);
return true;
}
}
closedir(dir);
return false;
}
/*
* Attempts to resolve the proper path, utilizing try_expand_home,
* convert_http_to_cgi_request, and also get_full_main_dir_path.
*/
void
resolve_path(REQUEST* request, CGI* cgi, struct flag_control* fc, char* dir)
{
if (strlen(request->path) && request->path[0] == '/'
&& request->path[1] == '~')
{
strncpy(request->parsed_path, request->path, strlen(request->path));
try_expand_home(request->parsed_path);
}
else if (fc->c && starts_with_cgibin(request->path))
{
convert_http_to_cgi_request(request, cgi, fc);
get_full_cgi_path(cgi, fc->cgi_dir, cgi->parsed_path,
sizeof(request->parsed_path));
/*
* headers.c uses request, so rather than rewrite tons of methods to
* also check for cgi conditionality, we let request also know the
* actual path of the cgi.
*/
strncpy(request->parsed_path, cgi->parsed_path,
strlen(cgi->parsed_path));
}
else
{
get_full_main_dir_path(request, dir, request->parsed_path,
sizeof(request->parsed_path));
}
}
/* tries expanding /~ into /home/<user>/sws/ where user is the name of the
* user running the instance of this sws server*/
void
try_expand_home(char* path)
{
if (path == NULL || strlen(path) < 3 || path[0] != '/' || path[1] != '~')
{
return;
}
char* start = path + 2;
char* end = start;
/* find out where the user ends by the presence of the next slash */
while (*end != '\0' && *end != '/')
{
end++;
}
char user[NAME_MAX] = {0};
const int user_len = end - start;
/* rejects /~ and /~/ too */
if (user_len == 0 || (user_len + 1) > (int)sizeof(user))
{
fprintf(stderr,
"%s: warning - try_expand_home(): /~ username provided is too "
"long. Ignoring...\n",
getprogname());
return;
}
memcpy(user, start, user_len);
user[user_len] = '\0';
/* user is trying to get a directory index or so */
char* target = "";
if (*end == '/')
{
target = end + 1;
}
char tmp[PATH_MAX] = {0};
int tmp_index = 0;
int n = snprintf(tmp, PATH_MAX, "/home/%s/sws/", user);
if (n < 0)
{
goto error;
}
tmp_index += n;
n = snprintf(tmp + tmp_index, PATH_MAX - tmp_index, "%s", target);
if (n < 0)
{
goto error;
}
/* tmp_index += n; */
strncpy(path, tmp, PATH_MAX);
return;
/* normally unreachable */
error:
fprintf(stderr,
"%s: warning - try_expand_home: snprintf: full path could not be "
"resolved.",
getprogname());
return;
}
ssize_t
safe_write(int fd, const void* buf, size_t nbytes)
{
const char* buffer = buf;
ssize_t total_bytes_written = 0;
while (total_bytes_written < (ssize_t)nbytes)
{
ssize_t bytesWritten = write(fd, (buffer + total_bytes_written),
(nbytes - total_bytes_written));
if (bytesWritten < 0)
{
if (errno == EINTR)
{
continue; /* try again */
}
return -1;
}
total_bytes_written += bytesWritten;
}
return total_bytes_written;
}
/*
* Resolve the full path of a normal main_directory file.
* Ignores query strings.
*/
void
get_full_main_dir_path(REQUEST* request, const char* dir_arg,
char* full_request_path, int full_path_bufsize)
{
int dir_len = strlen(dir_arg);
int req_path_len = strlen(request->path);
int bytes_written;
int req_path_len_no_qry = req_path_len;
for (int i = 0; i < req_path_len; i++)
{
if ((request->path)[i] == '?')
{
req_path_len_no_qry = i;
break;
}
}
int expected_len = dir_len + req_path_len_no_qry;
bytes_written = snprintf(full_request_path, full_path_bufsize, "%s%.*s",
dir_arg, req_path_len_no_qry, request->path);
struct stat s;
if (stat(full_request_path, &s) < 0)
{
// TODO: maybe we want to send error instead?
return;
}
if (search_dir(full_request_path))
{
snprintf(full_request_path, full_path_bufsize, "%s%s/index.html",
dir_arg, request->path);
}
if (bytes_written != expected_len)
{
(void)fprintf(
stderr,
"%s: get_full_main_dir_path: warning - full path could not be "
"written. Expected %d bytes, wrote %d "
"bytes\n",
getprogname(), expected_len, bytes_written);
}
// printf("[Debug] get_full_main_dir_path Path: %s\n",
// full_request_path);
return;
}
/*
* Resolve the full path of a CGI executable.
*/
void
get_full_cgi_path(CGI* cgi, const char* cgi_dir, char* full_request_path,
int full_path_bufsize)
{
int dir_len = strlen(cgi_dir);
int cgi_path_len = strlen(cgi->path);
int expected_len = dir_len + cgi_path_len;
int bytes_written = snprintf(full_request_path, full_path_bufsize, "%s%s",
cgi_dir, cgi->path);
if (bytes_written != expected_len)
{
(void)fprintf(stderr,
"%s: get_full_cgi_path: warning - full path could not be "
"written. Expected %d bytes, wrote %d "
"bytes\n",
getprogname(), expected_len, bytes_written);
}
/* printf("[Debug] get_full_main_dir_path Path: %s\n",
* full_request_path);
*/
return;
}
/* strip newline and carriage returns */
void
strip_nl_cr(char* string)
{
if (string == NULL)
{
fprintf(stderr, "%s: strip_nl_cr: passed string is NULL\n",
getprogname());
return;
}
char* p = string;
while (*p != '\0')
{
if (*p == '\r' || *p == '\n')
{
*p = '\0';
break;
}
p++;
}
return;
}
// NOTE: sending the status codes is implemented, need to figure out how to
// exit out if a status code is sent
void
send_status(REQUEST* request, int status)
{
request->status = status;
char response_line[BUFSIZ];
char* status_message;
switch (request->status)
{
case 200:
status_message = "OK";
break;
case 201:
status_message = "Created";
break;
case 202:
status_message = "Accepted";
break;
case 204:
status_message = "No Content";
break;
case 301:
status_message = "Moved Permanently";
break;
case 302:
status_message = "Moved Temporarily";
break;
case 304:
status_message = "Not Modified";
break;
case 400:
status_message = "Bad Request";
break;
case 401:
status_message = "Unauthorized";
break;
case 403:
status_message = "Forbidden";
break;
case 404:
status_message = "Not Found";
break;
case 500:
status_message = "Internal Server Error";
break;
case 501:
status_message = "Not Implemented";
break;
case 502:
status_message = "Bad Gateway";
break;
case 503:
status_message = "Service Unavailable";
break;
default:
status_message = "Unknown Error";
break;
}
int bytes_written = snprintf(response_line, BUFSIZ, "HTTP/1.0 %d %s\r\n",
request->status, status_message);
/* not much to be done if it fails*/
(void)safe_write(request->client_fd, response_line, bytes_written);
}
char*
format_time_GMT(void)
{
time_t t;
time(&t);
struct tm* gmt_time = gmtime(&t);
char* time_buf = (char*)malloc(BUFSIZ * sizeof(char));
/* error check by prepending a space per the manual page */
if (strftime(time_buf, BUFSIZ, " %A, %d %b %Y %T GMT", gmt_time) == 0)
{
fprintf(stderr, "%s: warning - strftime failed: %s\n", getprogname(),
strerror(errno));
}
else
{
/* remove leading space from the error check */
memmove(time_buf, time_buf + 1, strlen(time_buf));
}
return time_buf;
}
/* Populates the REQUEST* struct with arguments */
int
parse_input(REQUEST* request, char* input)
{
char* req_time = format_time_GMT();
(void)strncpy(request->request_time, req_time,
sizeof(request->request_time));
int count = 0;
/* 0 = action (GET / Head )
* 1 = file / path
* 2 = HTTP Version
*/
char* string_parse_arr[3] = {NULL, NULL,
NULL}; // 3 because we parse 3 arguments
request->status = -1;
/* strn as it is more portable than strl :c */
(void)strncpy(request->request_passedin, input,
sizeof(request->request_passedin));
char* pointer = strtok(input, " ");
while (pointer != NULL && count < 3)
{
string_parse_arr[count] = pointer;
pointer = strtok(NULL, " ");
count++;
}
if (string_parse_arr[0] == NULL || string_parse_arr[1] == NULL ||
string_parse_arr[2] == NULL)
{
return HTTP_ERROR;
}
/* if the path is too long, break out early for security reasons */
if (strlen(string_parse_arr[1]) + 1 > PATH_MAX)
{
return HTTP_ERROR;
}
(void)strncpy(request->protocol, string_parse_arr[2],
sizeof(request->protocol));
(void)strncpy(request->path, string_parse_arr[1], sizeof(request->path));
free(req_time);
if (strcmp(string_parse_arr[0], "GET") == 0)
{
request->request_type = GET;
}
else if (strcmp(string_parse_arr[0], "HEAD") == 0)
{
request->request_type = HEAD;
}
else
{
return HTTP_ERROR;
}
/* There may be trailing newline or carriage returns in the version, we
* should strip them (by going over the string and replacing the first
* instance with nul (\0)) */
strip_nl_cr(string_parse_arr[2]);
if (strcmp(string_parse_arr[2], "HTTP/1.0") != 0 &&
strcmp(string_parse_arr[2], "HTTP/1.1") != 0)
{
return HTTP_ERROR;
}
const char* const ims_header = "If-Modified-Since:";
const char* const ims_header_pos =
strstr(request->request_passedin, ims_header);
if (ims_header_pos == NULL)
{
return HTTP_OK;
}
const char* pos = ims_header_pos + strlen(ims_header);
while (*pos == ' ')
{
pos++;
}
char time_buf[BUFSIZ] = {0};
int i = 0;
while (*pos != '\0' && *pos != '\r' && *pos != '\n' &&
i < (int)sizeof(time_buf) - 1)
{
time_buf[i++] = *pos++;
}
time_buf[i] = '\0';
struct tm tm = {0};
char* time_convert = strptime(time_buf, "%A, %d %b %Y %T GMT", &tm);
if (time_convert == NULL)
{
/* Bad request, we will not accept for simplicity. */
return HTTP_ERROR;
}
request->if_modified_since_time = timegm(&tm);
time_t cur_time;
time(&cur_time);
if (request->if_modified_since_time > cur_time)
{
return HTTP_ERROR;
}
return HTTP_OK;
}
int
write_crlf_to_request(REQUEST* request)
{
size_t len = strlen("\r\n");
ssize_t bytesWritten = safe_write(request->client_fd, "\r\n", len);
if (bytesWritten == -1)
{
return HTTP_ERROR;
}
return HTTP_OK;
}