pserv/src/http.c
2022-12-11 19:10:09 -05:00

761 lines
21 KiB
C

/*
* A partial implementation of HTTP/1.0
*
* This code is mainly intended as a replacement for the book's 'tiny.c' server
* It provides a *partial* implementation of HTTP/1.0 which can form a basis for
* the assignment.
*
* @author G. Back for CS 3214 Spring 2018
*/
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <string.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdbool.h>
#include <errno.h>
#include <unistd.h>
#include <time.h>
#include <fcntl.h>
#include <linux/limits.h>
#include <dirent.h>
#include <jansson.h>
#include "http.h"
#include "hexdump.h"
#include "socket.h"
#include "bufio.h"
#include "main.h"
static const char * NEVER_EMBED_A_SECRET_IN_CODE = "supa secret";
// Need macros here because of the sizeof
#define CRLF "\r\n"
#define CR "\r"
#define STARTS_WITH(field_name, header) \
(!strncasecmp(field_name, header, sizeof(header) - 1))
/* Parse HTTP request line, setting req_method, req_path, and req_version. */
static bool
http_parse_request(struct http_transaction *ta)
{
size_t req_offset;
ssize_t len = bufio_readline(ta->client->bufio, &req_offset);
if (len < 2) // error, EOF, or less than 2 characters
return false;
char *request = bufio_offset2ptr(ta->client->bufio, req_offset);
request[len-2] = '\0'; // replace LF with 0 to ensure zero-termination
char *endptr;
char *method = strtok_r(request, " ", &endptr);
if (method == NULL)
return false;
if (!strcmp(method, "GET"))
ta->req_method = HTTP_GET;
else if (!strcmp(method, "POST"))
ta->req_method = HTTP_POST;
else
ta->req_method = HTTP_UNKNOWN;
char *req_path = strtok_r(NULL, " ", &endptr);
if (req_path == NULL)
return false;
ta->req_path = bufio_ptr2offset(ta->client->bufio, req_path);
char *http_version = strtok_r(NULL, CR, &endptr);
if (http_version == NULL) // would be HTTP 0.9
return false;
// record client's HTTP version in request
if (!strcmp(http_version, "HTTP/1.1"))
ta->req_version = HTTP_1_1;
else if (!strcmp(http_version, "HTTP/1.0"))
ta->req_version = HTTP_1_0;
else
return false;
return true;
}
/* Process HTTP headers. */
static bool
http_process_headers(struct http_transaction *ta)
{
ta->from = -1;
ta->to = -1;
for (;;) {
size_t header_offset;
ssize_t len = bufio_readline(ta->client->bufio, &header_offset);
if (len <= 0)
return false;
char *header = bufio_offset2ptr(ta->client->bufio, header_offset);
if (len == 2 && STARTS_WITH(header, CRLF)) // empty CRLF
return true;
header[len-2] = '\0';
/* Each header field consists of a name followed by a
* colon (":") and the field value. Field names are
* case-insensitive. The field value MAY be preceded by
* any amount of LWS, though a single SP is preferred.
*/
char *endptr;
char *field_name = strtok_r(header, ":", &endptr);
if (field_name == NULL)
return false;
// skip white space
char *field_value = endptr;
while (*field_value == ' ' || *field_value == '\t')
field_value++;
// you may print the header like so
// printf("Header: %s: %s\n", field_name, field_value);
if (!strcasecmp(field_name, "Content-Length")) {
ta->req_content_len = atoi(field_value);
}
ta->valid_token = false;
/* Handle other headers here. Both field_value and field_name
* are zero-terminated strings.
*/
// Cookie header
if (!strcasecmp(field_name, "Cookie")) {
// Gets the cookies.
char* cookie2;
char* cookie1 = strtok_r(field_value, ";", &cookie2);
cookie2++;
// Stores the 2nd cookie.
ta->extra_cookie = cookie2;
// Determines if the 1st cookie is invalid.
if ((cookie1 == NULL) || (strlen(cookie1) <= 11))
{
ta->resp_status = HTTP_PERMISSION_DENIED;
return false;
}
// If not, find token.
ta->token = cookie1 + 11; // + 11 gets rid of "auth_token=" heading.
ta->valid_token = validate_token_exp(ta, ta->token);
}
// Range header
if (!strcasecmp(field_name, "Range")){
// Gets token.
char *endp;
char *token = strtok_r(field_value, "= ", &endp);
while (token != NULL && strcasecmp(token, "bytes")) {
token = strtok_r(NULL, "= ", &endp);
}
token = strtok_r(NULL, "= ", &endp);
// Determines if the token exist.
if (token != NULL) {
if (token[0] == '-') {
ta->to = atol(token + 1);
}
else if (token[strlen(token) - 1] == '-') {
token[strlen(token) - 1] = '\0';
ta->from = atol(token);
}
else {
char *to;
char *from = strtok_r(token, "- ", &to);
ta->from = atol(from);
ta->to = atol(to);
}
}
ta->range_request = true;
}
}
}
const int MAX_HEADER_LEN = 2048;
/* add a formatted header to the response buffer. */
void
http_add_header(buffer_t * resp, char* key, char* fmt, ...)
{
va_list ap;
buffer_appends(resp, key);
buffer_appends(resp, ": ");
va_start(ap, fmt);
char *error = buffer_ensure_capacity(resp, MAX_HEADER_LEN);
int len = vsnprintf(error, MAX_HEADER_LEN, fmt, ap);
resp->len += len > MAX_HEADER_LEN ? MAX_HEADER_LEN - 1 : len;
va_end(ap);
buffer_appends(resp, "\r\n");
}
/* add a content-length header. */
static void
add_content_length(buffer_t *res, size_t len)
{
http_add_header(res, "Content-Length", "%ld", len);
}
/* start the response by writing the first line of the response
* to the response buffer. Used in send_response_header */
static void
start_response(struct http_transaction * ta, buffer_t *res)
{
switch (ta->req_version) {
case HTTP_1_0:
buffer_appends(res, "HTTP/1.0 ");
break;
case HTTP_1_1:
default:
buffer_appends(res, "HTTP/1.1 ");
break;
}
switch (ta->resp_status) {
case HTTP_OK:
buffer_appends(res, "200 OK");
break;
case HTTP_PARTIAL_CONTENT:
buffer_appends(res, "206 Partial Content");
break;
case HTTP_BAD_REQUEST:
buffer_appends(res, "400 Bad Request");
break;
case HTTP_PERMISSION_DENIED:
buffer_appends(res, "403 Permission Denied");
break;
case HTTP_NOT_FOUND:
buffer_appends(res, "404 Not Found");
break;
case HTTP_METHOD_NOT_ALLOWED:
buffer_appends(res, "405 Method Not Allowed");
break;
case HTTP_REQUEST_TIMEOUT:
buffer_appends(res, "408 Request Timeout");
break;
case HTTP_REQUEST_TOO_LONG:
buffer_appends(res, "414 Request Too Long");
break;
case HTTP_NOT_IMPLEMENTED:
buffer_appends(res, "501 Not Implemented");
break;
case HTTP_SERVICE_UNAVAILABLE:
buffer_appends(res, "503 Service Unavailable");
break;
case HTTP_INTERNAL_ERROR:
default:
buffer_appends(res, "500 Internal Server Error");
break;
}
buffer_appends(res, CRLF);
}
/* Send response headers to client */
static bool
send_response_header(struct http_transaction *ta)
{
buffer_t response;
buffer_init(&response, 80);
start_response(ta, &response);
if (bufio_sendbuffer(ta->client->bufio, &response) == -1)
return false;
buffer_appends(&ta->resp_headers, CRLF);
if (bufio_sendbuffer(ta->client->bufio, &ta->resp_headers) == -1)
return false;
buffer_delete(&response);
return true;
}
/* Send a full response to client with the content in resp_body. */
static bool
send_response(struct http_transaction *ta)
{
// add content-length. All other headers must have already been set.
add_content_length(&ta->resp_headers, ta->resp_body.len);
if (!send_response_header(ta))
return false;
return bufio_sendbuffer(ta->client->bufio, &ta->resp_body) != -1;
}
const int MAX_ERROR_LEN = 2048;
/* Send an error response. */
static bool
send_error(struct http_transaction * ta, enum http_response_status status, const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
char *error = buffer_ensure_capacity(&ta->resp_body, MAX_ERROR_LEN);
int len = vsnprintf(error, MAX_ERROR_LEN, fmt, ap);
ta->resp_body.len += len > MAX_ERROR_LEN ? MAX_ERROR_LEN - 1 : len;
va_end(ap);
ta->resp_status = status;
http_add_header(&ta->resp_headers, "Content-Type", "text/plain");
return send_response(ta);
}
/* Send Not Found response. */
static bool
send_not_found(struct http_transaction *ta)
{
return send_error(ta, HTTP_NOT_FOUND, "File %s not found",
bufio_offset2ptr(ta->client->bufio, ta->req_path));
}
/* A start at assigning an appropriate mime type. Real-world
* servers use more extensive lists such as /etc/mime.types
*/
static const char *
guess_mime_type(char *filename)
{
char *suffix = strrchr(filename, '.');
if (suffix == NULL)
return "text/plain";
if (!strcasecmp(suffix, ".html"))
return "text/html";
if (!strcasecmp(suffix, ".gif"))
return "image/gif";
if (!strcasecmp(suffix, ".png"))
return "image/png";
if (!strcasecmp(suffix, ".jpg"))
return "image/jpeg";
if (!strcasecmp(suffix, ".js"))
return "text/javascript";
if (!strcasecmp(suffix, ".mp4"))
return "video/mp4";
return "text/plain";
}
/* Check a token is not expired. */
bool validate_token_exp(struct http_transaction *ta, char* token) {
jwt_t* cookie;
// Decode token.
int rc = jwt_decode(&cookie, ta->token, (unsigned char *)NEVER_EMBED_A_SECRET_IN_CODE, strlen(NEVER_EMBED_A_SECRET_IN_CODE));
if (rc)
return false;
// Get claim (formatted grants).
char* grants = jwt_get_grants_json(cookie, NULL);
if (grants == NULL)
return false;
ta->grants = grants;
// Get expiration time.
json_error_t error;
json_t *jgrants = json_loadb(grants, strlen(grants), 0, &error);
json_int_t exp, iat;
const char *sub;
json_unpack(jgrants, "{s:I, s:I, s:s}", "exp", &exp, "iat", &iat, "sub", &sub);
// Check expiration time.
if (time(NULL) >= exp)
return false;
return true;
}
/* Handle HTTP transaction for static files. */
static bool
handle_static_asset(struct http_transaction *ta, char *basedir)
{
char fname[PATH_MAX];
char *req_path = bufio_offset2ptr(ta->client->bufio, ta->req_path);
// The code below is vulnerable to an attack. Can you see
// which? Fix it to avoid indirect object reference (IDOR) attacks.
snprintf(fname, sizeof fname, "%s%s", basedir, req_path);
char *endptr;
char *p = calloc(strlen(fname) + 1, sizeof(char));
memcpy(p, fname, strlen(fname) + 1);
char *dir = strtok_r(p, "/", &endptr);
dir = strtok_r(NULL, "/", &endptr); // initial ".." is okay
while (dir != NULL) {
if (!strcmp(dir, "..")) {
return send_not_found(ta);
}
dir = strtok_r(NULL, "/", &endptr);
}
if (!strcmp(req_path, "/")) {
memset(fname, 0, PATH_MAX);
snprintf(fname, sizeof fname, "%s%s", server_root, "/index.html");
}
if (access(fname, R_OK)) {
if (errno == EACCES)
return send_error(ta, HTTP_PERMISSION_DENIED, "Permission denied.");
else {
if (html5_fallback) {
memset(fname, 0, PATH_MAX);
snprintf(fname, sizeof fname, "%s%s", server_root, "/index.html");
} else {
return send_not_found(ta);
}
}
}
// Determine file size
struct stat st;
int rc = stat(fname, &st);
if (rc == -1)
return send_error(ta, HTTP_INTERNAL_ERROR, "Could not stat file.");
int filefd = open(fname, O_RDONLY);
if (filefd == -1) {
return send_not_found(ta);
}
ta->resp_status = HTTP_OK;
http_add_header(&ta->resp_headers, "Content-Type", "%s", guess_mime_type(fname));
off_t from = 0, to = st.st_size - 1;
if (ta->range_request) {
if (ta->from >= 0) {
from = ta->from;
}
if (ta->to >= 0) {
to = ta->to;
}
}
off_t content_length = to + 1 - from;
add_content_length(&ta->resp_headers, content_length);
http_add_header(&ta->resp_headers, "Accept-Ranges", "bytes");
if (ta->range_request) {
ta->resp_status = HTTP_PARTIAL_CONTENT;
http_add_header(&ta->resp_headers, "Content-Range",
"bytes %ld-%ld/%ld", from, to, st.st_size);
}
bool success = send_response_header(ta);
if (!success)
goto out;
// sendfile may send fewer bytes than requested, hence the loop
while (success && from <= to)
success = bufio_sendfile(ta->client->bufio, filefd, &from, to + 1 - from) > 0;
out:
close(filefd);
return success;
}
/* Handles api/login POST */
static bool
post_handle_login(struct http_transaction *ta)
{
http_add_header(&ta->resp_headers, "Content-Type", "application/json");
char* req_body = bufio_offset2ptr(ta->client->bufio, ta->req_body);
// Gets the username and password.
req_body[ta->req_content_len] = '\0';
json_error_t error;
json_t *jgrants = json_loadb(req_body, strlen(req_body), 0, &error);
const char *username;
const char *password;
int rc = json_unpack(jgrants, "{s:s, s:s}", "username", &username, "password", &password);
if (rc == -1)
{
return send_error(ta, HTTP_PERMISSION_DENIED, "???");
}
// Authenticates the username and password.
if (!strcmp(username, "user0") && !strcmp(password, "thepassword"))
{
int iat = time(NULL);
int exp = iat + token_expiration_time;
// Create cookie.
jwt_t* cookie;
int rc = jwt_new(&cookie);
if (rc)
return send_error(ta, HTTP_INTERNAL_ERROR, "jwt_new");
// Add grants.
rc = jwt_add_grant(cookie, "sub", username);
if (rc)
return send_error(ta, HTTP_INTERNAL_ERROR, "jwt_add_grant");
//time_t age = time(NULL);
rc = jwt_add_grant_int(cookie, "iat", iat);
if (rc)
return send_error(ta, HTTP_INTERNAL_ERROR, "jwt_add_grant");
//time_t max_age = age + 3600 * 24;
rc = jwt_add_grant_int(cookie, "exp", exp);
if (rc)
return send_error(ta, HTTP_INTERNAL_ERROR, "jwt_add_grant");
// Set algorithm.
rc = jwt_set_alg(cookie, JWT_ALG_HS256, (unsigned char *)NEVER_EMBED_A_SECRET_IN_CODE, strlen(NEVER_EMBED_A_SECRET_IN_CODE));
if (rc)
return send_error(ta, HTTP_INTERNAL_ERROR, "jwt_set_alg");
// Create token.
char* token = jwt_encode_str(cookie);
if (token == NULL)
return send_error(ta, HTTP_INTERNAL_ERROR, "jwt_encode_str");
// Add Set-Cookie header.
http_add_header(&ta->resp_headers, "Set-Cookie", "auth_token=%s; Path=%s; Max-Age=%d; HttpOnly", token, "/", token_expiration_time);
// Get claim (formatted grants).
char *grants = jwt_get_grants_json(cookie, NULL);
buffer_appends(&ta->resp_body, grants);
// Send claim.
ta->resp_status = HTTP_OK;
return send_response(ta);
}
return send_error(ta, HTTP_PERMISSION_DENIED, "Permission denied");
}
/* Handles api/login GET */
static bool get_handle_login(struct http_transaction *ta) {
http_add_header(&ta->resp_headers, "Content-Type", "application/json");
// Determines if the token is invalid.
if (!ta->valid_token) {
buffer_appends(&ta->resp_body, "{}");
ta->resp_status = HTTP_OK;
return send_response(ta);
}
// If not, send response.
buffer_appends(&ta->resp_body, ta->grants);
ta->resp_status = HTTP_OK;
return send_response(ta);
}
/* Lists the .mp4 files in the given directory in the proper format. */
static char *list_videos(DIR* dir) {
json_t *list = json_array();
struct dirent *file;
while ((file = readdir(dir)) != NULL) {
char *suffix = strrchr(file->d_name, '.');
if (suffix != NULL && !strcmp(suffix, ".mp4")) {
// stat the file
char fname[PATH_MAX];
struct stat st;
snprintf(fname, sizeof(fname), "%s/%s", server_root, file->d_name);
int rc = stat(fname, &st);
if (rc == -1)
perror("Could not list videos.");
// json components
json_t *entry = json_object();
json_t *size = json_integer(st.st_size);
json_t *name = json_string(file->d_name);
rc = json_object_set(entry, "size", size);
if (rc)
perror("JSON append failed.");
rc = json_object_set(entry, "name", name);
if (rc)
perror("JSON append failed.");
// add entry
rc = json_array_append_new(list, entry);
if (rc)
perror("JSON append failed.");
}
}
return json_dumps(list, 0);
}
/* Determines what the API request is. */
static int val_api_url(struct http_transaction *ta) {
char *req_path = bufio_offset2ptr(ta->client->bufio, ta->req_path);
if (!strcmp(req_path, "/api/login")) {
return 0;
}
if (!strcmp(req_path, "/api/video")) {
return 1;
}
if (!strcmp(req_path, "/api/logout")) {
return 2;
}
return -1;
}
/* Handles API requests like login/logout/video. */
static bool
handle_api(struct http_transaction *ta)
{
int val = val_api_url(ta);
if (val == -1){
return send_not_found(ta);
}
// API login
if (val == 0) {
if (ta->req_method == HTTP_POST) {
// Handle login post
return post_handle_login(ta);
}
else if (ta->req_method == HTTP_GET){
// Handle login get
return get_handle_login(ta);
}
else{
return send_error(ta, HTTP_METHOD_NOT_ALLOWED, "Unknown method.\n");
}
}
// API video
if (val == 1) {
http_add_header(&ta->resp_headers, "Accept-Ranges", "bytes");
DIR* dir = opendir(server_root);
char *json = list_videos(dir); // handling
fprintf(stderr, "json: %s\n", json);
http_add_header(&ta->resp_headers, "Content-Type", "application/json");
// Ensures response can be sent and sends it.
char *body = buffer_ensure_capacity(&ta->resp_body, MAX_HEADER_LEN);
int len = snprintf(body, strlen(json) + 2, "%s\n", json);
int length = len > MAX_HEADER_LEN ? MAX_HEADER_LEN - 1 : len;
ta->resp_body.len += length;
ta->resp_status = HTTP_OK;
return send_response(ta);
}
// API logout
if (val == 2)
{
// Add Set-Cookie header.
http_add_header(&ta->resp_headers, "Set-Cookie", "auth_token=; Path=/; Max-Age=0; HttpOnly");
// Send message.
buffer_appends(&ta->resp_body, "{\"message\":\"logging out\"}");
ta->resp_status = HTTP_OK;
return send_response(ta);
}
return false;
}
/* Set up an http client, associating it with a bufio buffer. */
void
http_setup_client(struct http_client *self, struct bufio *bufio)
{
self->bufio = bufio;
}
/* Handles attempts to access private files. */
static bool
handle_private(struct http_transaction *ta)
{
// Determines if the token from the 1st cookie is valid.
if (ta->valid_token) {
ta->resp_status = HTTP_OK;
return handle_static_asset(ta, server_root);
}
// Determines if there's a 2nd cookie.
if ((ta->extra_cookie == NULL) || (strlen(ta->extra_cookie) <= 11)) {
return send_error(ta, HTTP_PERMISSION_DENIED, "Permission denied.");
}
// If so, find its token.
ta->token = ta->extra_cookie + 11; // + 11 gets rid of "auth_token=" heading.
ta->valid_token = validate_token_exp(ta, ta->token);
// valid
if (ta->valid_token) {
ta->resp_status = HTTP_OK;
return handle_static_asset(ta, server_root);
}
// invalid
return send_error(ta, HTTP_PERMISSION_DENIED, "Permission denied.");
}
/* Handle a single HTTP transaction. Returns true on success. */
bool
http_handle_transaction(struct http_client *self)
{
struct http_transaction ta;
memset(&ta, 0, sizeof ta);
ta.client = self;
if (!http_parse_request(&ta))
return false;
bool is_http_1_1 = false; // false
if (ta.req_version == HTTP_1_1)
is_http_1_1 = true; // true
if (!http_process_headers(&ta))
return false;
if (ta.req_content_len > 0) {
int rc = bufio_read(self->bufio, ta.req_content_len, &ta.req_body);
if (rc != ta.req_content_len)
return false;
// To see the body, use this:
// char *body = bufio_offset2ptr(ta.client->bufio, ta.req_body);
// hexdump(body, ta.req_content_len);
}
buffer_init(&ta.resp_headers, 1024);
http_add_header(&ta.resp_headers, "Server", "CS3214-Personal-Server");
buffer_init(&ta.resp_body, 0);
bool rc = false;
char *req_path = bufio_offset2ptr(ta.client->bufio, ta.req_path);
if (STARTS_WITH(req_path, "/api")) {
rc = handle_api(&ta);
} else
if (STARTS_WITH(req_path, "/private")) {
rc = handle_private(&ta);
} else {
rc = handle_static_asset(&ta, server_root);
}
buffer_delete(&ta.resp_headers);
buffer_delete(&ta.resp_body);
return is_http_1_1 && rc;
}