pserv/src/http.c

369 lines
10 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 "http.h"
#include "hexdump.h"
#include "socket.h"
#include "bufio.h"
// Need macros here because of the sizeof
#define CRLF "\r\n"
#define STARTS_WITH(field_name, header) \
(!strncasecmp(field_name, header, sizeof(header) - 1))
char * server_root; // root from which static files are served
/* 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);
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, CRLF, &endptr);
if (http_version == NULL) // would be HTTP 0.9
return false;
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)
{
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);
char *field_value = strtok_r(NULL, " \t", &endptr); // skip leading & trailing OWS
if (field_name == NULL)
return false;
// printf("Header: %s: %s\n", field_name, field_value);
if (!strcasecmp(field_name, "Content-Length")) {
ta->req_content_len = atoi(field_value);
}
/* Handle other headers here. */
}
}
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)
{
buffer_appends(res, "HTTP/1.0 ");
switch (ta->resp_status) {
case HTTP_OK:
buffer_appends(res, "200 OK");
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;
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";
return "text/plain";
}
/* 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);
if (access(fname, R_OK)) {
if (errno == EACCES)
return send_error(ta, HTTP_PERMISSION_DENIED, "Permission denied.");
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;
add_content_length(&ta->resp_headers, st.st_size);
http_add_header(&ta->resp_headers, "Content-Type", "%s", guess_mime_type(fname));
bool success = send_response_header(ta);
if (!success)
goto out;
success = bufio_sendfile(ta->client->bufio, filefd, NULL, st.st_size) == st.st_size;
out:
close(filefd);
return success;
}
static int
handle_api(struct http_transaction *ta)
{
return send_error(ta, HTTP_NOT_FOUND, "API not implemented");
}
/* 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;
}
/* 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;
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")) {
/* not implemented */
} else {
rc = handle_static_asset(&ta, server_root);
}
buffer_delete(&ta.resp_headers);
buffer_delete(&ta.resp_body);
return rc;
}