blob: 0b08d3ecf9efcca65bc399f4a52614a248e5f1a9 [file] [log] [blame] [raw]
/*
copyright: Boaz segev, 2016-2017
license: MIT
Feel free to copy, use and enjoy according to the license provided.
*/
#include "spnlock.inc"
#include "fiobj.h"
#include "http.h"
#include "http1_parser.h"
#include "http1_request.h"
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
char *HTTP11_Protocol_String = "facil_http/1.1_protocol";
/* *****************************************************************************
HTTP/1.1 data structures
***************************************************************************** */
typedef struct http1_protocol_s {
protocol_s protocol;
http_settings_s *settings;
http1_parser_s parser;
void (*on_request)(http_request_s *request);
struct http1_protocol_s *next;
http1_request_s request;
size_t len; /* used as a persistent socket `read` indication. */
size_t refresh; /* a flag indicating a request callback was called. */
} http1_protocol_s;
static void http1_on_data(intptr_t uuid, http1_protocol_s *protocol);
/* *****************************************************************************
HTTP/1.1 pool
***************************************************************************** */
static struct {
spn_lock_i lock;
uint8_t init;
http1_protocol_s *next;
http1_protocol_s protocol_mem[HTTP1_POOL_SIZE];
} http1_pool = {.lock = SPN_LOCK_INIT, .init = 0};
static void http1_free(intptr_t uuid, http1_protocol_s *pr) {
if ((uintptr_t)pr < (uintptr_t)http1_pool.protocol_mem ||
(uintptr_t)pr >= (uintptr_t)(http1_pool.protocol_mem + HTTP1_POOL_SIZE))
goto use_free;
spn_lock(&http1_pool.lock);
pr->next = http1_pool.next;
http1_pool.next = pr;
spn_unlock(&http1_pool.lock);
return;
use_free:
free(pr);
return;
(void)uuid;
}
static inline void http1_set_protocol_data(http1_protocol_s *pr) {
pr->protocol = (protocol_s){
.on_data = (void (*)(intptr_t, protocol_s *))http1_on_data,
.on_close = (void (*)(intptr_t uuid, protocol_s *))http1_free};
pr->request.request = (http_request_s){.fd = 0, .http_version = HTTP_V1};
pr->refresh = pr->request.header_pos = pr->request.buffer_pos = pr->len = 0;
pr->parser = (http1_parser_s){.udata = pr};
}
static http1_protocol_s *http1_alloc(void) {
http1_protocol_s *pr;
spn_lock(&http1_pool.lock);
if (!http1_pool.next)
goto use_malloc;
pr = http1_pool.next;
http1_pool.next = pr->next;
spn_unlock(&http1_pool.lock);
http1_request_clear(&pr->request.request);
pr->request.request.settings = pr->settings;
pr->len = 0;
return pr;
use_malloc:
if (http1_pool.init == 0)
goto initialize;
spn_unlock(&http1_pool.lock);
// fprintf(stderr, "using malloc\n");
pr = malloc(sizeof(*pr));
http1_set_protocol_data(pr);
return pr;
initialize:
http1_pool.init = 1;
for (size_t i = 1; i < (HTTP1_POOL_SIZE - 1); i++) {
http1_set_protocol_data(http1_pool.protocol_mem + i);
http1_pool.protocol_mem[i].next = http1_pool.protocol_mem + (i + 1);
}
http1_pool.protocol_mem[HTTP1_POOL_SIZE - 1].next = NULL;
http1_pool.next = http1_pool.protocol_mem + 1;
spn_unlock(&http1_pool.lock);
http1_set_protocol_data(http1_pool.protocol_mem);
return http1_pool.protocol_mem;
}
/* *****************************************************************************
HTTP/1.1 error responses
***************************************************************************** */
static void err_bad_request(http1_protocol_s *pr) {
http_response_s *response = http_response_create(&pr->request.request);
if (pr->settings->log_static)
http_response_log_start(response);
response->status = 400;
if (!pr->settings->public_folder ||
http_response_sendfile2(response, &pr->request.request,
pr->settings->public_folder,
pr->settings->public_folder_length, "400.html", 8,
pr->settings->log_static)) {
response->should_close = 1;
http_response_write_body(response, "Bad Request", 11);
http_response_finish(response);
}
sock_close(pr->request.request.fd);
pr->request.buffer_pos = 0;
}
static void err_too_big(http1_protocol_s *pr) {
http_response_s *response = http_response_create(&pr->request.request);
if (pr->settings->log_static)
http_response_log_start(response);
response->status = 431;
if (!pr->settings->public_folder ||
http_response_sendfile2(response, &pr->request.request,
pr->settings->public_folder,
pr->settings->public_folder_length, "431.html", 8,
pr->settings->log_static)) {
http_response_write_body(response, "Request Header Fields Too Large", 31);
http_response_finish(response);
}
}
/* *****************************************************************************
HTTP/1.1 parsre callbacks
***************************************************************************** */
#define HTTP_BODY_CHUNK_SIZE 4096
/** called when a request was received. */
static int http1_on_request(http1_parser_s *parser) {
http1_protocol_s *pr = parser->udata;
if (!pr)
return -1;
if (pr->request.request.host == NULL) {
goto bad_request;
}
pr->request.request.content_length = parser->state.content_length;
http_settings_s *settings = pr->settings;
pr->request.request.settings = settings;
// make sure udata to NULL, making it available for the user
pr->request.request.udata = NULL;
// static file service or call request callback
if (pr->request.request.upgrade || settings->public_folder == NULL ||
http_response_sendfile2(
NULL, &pr->request.request, settings->public_folder,
settings->public_folder_length, pr->request.request.path,
pr->request.request.path_len, settings->log_static)) {
pr->on_request(&pr->request.request);
}
pr->refresh = 1;
return 0;
bad_request:
/* handle generally bad requests */
err_bad_request(pr);
return -1;
}
/** called when a response was received. */
static int http1_on_response(http1_parser_s *parser) {
return -1;
(void)parser;
}
/** called when a request method is parsed. */
static int http1_on_method(http1_parser_s *parser, char *method,
size_t method_len) {
http1_protocol_s *pr = parser->udata;
if (!pr)
return -1;
pr->request.request.method = method;
pr->request.request.method_len = method_len;
return 0;
}
/** called when a response status is parsed. the status_str is the string
* without the prefixed numerical status indicator.*/
static int http1_on_status(http1_parser_s *parser, size_t status,
char *status_str, size_t len) {
return -1;
(void)parser;
(void)status;
(void)status_str;
(void)len;
}
/** called when a request path (excluding query) is parsed. */
static int http1_on_path(http1_parser_s *parser, char *path, size_t path_len) {
http1_protocol_s *pr = parser->udata;
if (!pr)
return -1;
pr->request.request.path = path;
pr->request.request.path_len = path_len;
return 0;
}
/** called when a request path (excluding query) is parsed. */
static int http1_on_query(http1_parser_s *parser, char *query,
size_t query_len) {
http1_protocol_s *pr = parser->udata;
if (!pr)
return -1;
pr->request.request.query = query;
pr->request.request.query_len = query_len;
return 0;
}
/** called when a the HTTP/1.x version is parsed. */
static int http1_on_http_version(http1_parser_s *parser, char *version,
size_t len) {
http1_protocol_s *pr = parser->udata;
if (!pr)
return -1;
pr->request.request.version = version;
pr->request.request.version_len = len;
return 0;
}
/** called when a header is parsed. */
static int http1_on_header(http1_parser_s *parser, char *name, size_t name_len,
char *data, size_t data_len) {
http1_protocol_s *pr = parser->udata;
if (!pr || pr->request.header_pos >= HTTP1_MAX_HEADER_COUNT - 1)
goto too_big;
if (parser->state.read)
goto too_big; /* refuse trailer header data, it isn't in buffer */
/** test for special headers that should be easily accessible **/
#if HTTP_HEADERS_LOWERCASE
if (name_len == 4 && *((uint32_t *)name) == *((uint32_t *)"host")) {
pr->request.request.host = data;
pr->request.request.host_len = data_len;
} else if (name_len == 12 && *((uint32_t *)name) == *((uint32_t *)"cont") &&
*((uint64_t *)(name + 4)) == *((uint64_t *)"ent-type")) {
pr->request.request.content_type = data;
pr->request.request.content_type_len = data_len;
} else if (name_len == 7 && *((uint64_t *)name) == *((uint64_t *)"upgrade")) {
pr->request.request.upgrade = data;
pr->request.request.upgrade_len = data_len;
} else if (name_len == 10 && *((uint32_t *)name) == *((uint32_t *)"conn") &&
*((uint64_t *)(name + 2)) == *((uint64_t *)"nnection")) {
pr->request.request.connection = data;
pr->request.request.connection_len = data_len;
}
#else
if (name_len == 4 && HEADER_NAME_IS_EQ(name, "host", name_len)) {
pr->request.request.host = data;
pr->request.request.host_len = data_len;
} else if (name_len == 12 &&
HEADER_NAME_IS_EQ(name, "content-type", name_len)) {
pr->request.request.content_type = data;
pr->request.request.content_type_len = data_len;
} else if (name_len == 7 && HEADER_NAME_IS_EQ(name, "upgrade", name_len)) {
pr->request.request.upgrade = data;
pr->request.request.upgrade_len = data_len;
} else if (name_len == 10 &&
HEADER_NAME_IS_EQ(name, "connection", name_len)) {
pr->request.request.connection = data;
pr->request.request.connection_len = data_len;
}
#endif
pr->request.headers[pr->request.header_pos].name = name;
pr->request.headers[pr->request.header_pos].name_len = name_len;
pr->request.headers[pr->request.header_pos].data = data;
pr->request.headers[pr->request.header_pos].data_len = data_len;
pr->request.header_pos++;
pr->request.request.headers_count++;
return 0;
too_big:
/* handle oversized headers */
err_too_big(pr);
return -1;
}
/** called when a body chunk is parsed. */
static int http1_on_body_chunk(http1_parser_s *parser, char *data,
size_t data_len) {
http1_protocol_s *pr = parser->udata;
if (!pr)
return -1;
if (parser->state.content_length > (ssize_t)pr->settings->max_body_size ||
parser->state.read > (ssize_t)pr->settings->max_body_size)
return -1; /* test every time, in case of chunked data */
if (!parser->state.read) {
if (parser->state.content_length > 0 &&
(pr->request.buffer_pos + parser->state.content_length <
HTTP1_MAX_HEADER_SIZE)) {
pr->request.request.body_str = data;
} else {
// create a temporary file to contain the data.
#ifdef P_tmpdir
#if defined(__linux__) /* linux doesn't end with a divider */
char template[] = P_tmpdir "/http_request_body_XXXXXXXX";
#else
char template[] = P_tmpdir "http_request_body_XXXXXXXX";
#endif
#else
char template[] = "/tmp/http_request_body_XXXXXXXX";
#endif
pr->request.request.body_file = mkstemp(template);
if (pr->request.request.body_file == -1)
return -1;
}
}
if (pr->request.request.body_file) {
if (write(pr->request.request.body_file, data, data_len) !=
(ssize_t)data_len)
return -1;
} else {
/* nothing to do... the parser and `on_data` are doing all the work */
}
return 0;
}
/** called when a protocol error occured. */
static int http1_on_error(http1_parser_s *parser) {
http1_protocol_s *pr = parser->udata;
if (!pr)
return -1;
sock_close(pr->request.request.fd);
http1_request_clear(&pr->request.request);
return 0;
}
/* *****************************************************************************
HTTP/1.1 protocol callbacks
***************************************************************************** */
/* parse and call callback */
static void http1_on_data(intptr_t uuid, http1_protocol_s *pr) {
size_t consumed;
char buff[HTTP_BODY_CHUNK_SIZE];
http1_request_s *request = &pr->request;
char *buffer = request->buffer;
ssize_t tmp = 0;
// handle requests with no file data
if (request->request.body_file <= 0) {
// read into the request buffer.
tmp = sock_read(uuid, request->buffer + request->buffer_pos,
HTTP1_MAX_HEADER_SIZE - request->buffer_pos);
if (tmp > 0) {
request->buffer_pos += tmp;
pr->len += tmp;
} else
tmp = 0;
buffer = request->buffer + request->buffer_pos - pr->len;
} else {
if (pr->len) {
/* protocol error, we shouldn't have letfovers during file processing */
err_bad_request(pr);
http1_on_error(&pr->parser);
return;
}
buffer = buff;
tmp = sock_read(uuid, buffer, HTTP_BODY_CHUNK_SIZE);
if (tmp > 0) {
request->buffer_pos += tmp;
pr->len += tmp;
} else
tmp = 0;
}
if (pr->len == 0)
return;
// parse HTTP data
consumed =
http1_fio_parser(.parser = &pr->parser, .buffer = buffer,
.length = pr->len, .on_request = http1_on_request,
.on_response = http1_on_response,
.on_method = http1_on_method,
.on_status = http1_on_status, .on_path = http1_on_path,
.on_query = http1_on_query,
.on_http_version = http1_on_http_version,
.on_header = http1_on_header,
.on_body_chunk = http1_on_body_chunk,
.on_error = http1_on_error);
// handle leftovers, if any
if (pr->refresh) {
pr->refresh = 0;
if (pr->len > consumed) {
memmove(request->buffer, buffer + consumed, pr->len - consumed);
pr->len = pr->len - consumed;
http1_request_clear(&request->request);
request->buffer_pos = pr->len;
facil_force_event(uuid, FIO_EVENT_ON_DATA);
return;
}
http1_request_clear(&request->request);
pr->len = 0;
} else {
pr->len = pr->len - consumed;
}
if (tmp)
facil_force_event(uuid, FIO_EVENT_ON_DATA);
}
/* *****************************************************************************
HTTP listening helpers
*/
/**
Allocates memory for an upgradable HTTP/1.1 protocol.
The protocol self destructs when the `on_close` callback is called.
*/
protocol_s *http1_on_open(intptr_t fd, http_settings_s *settings) {
if (sock_uuid2fd(fd) >= (sock_max_capacity() - HTTP_BUSY_UNLESS_HAS_FDS))
goto is_busy;
http1_protocol_s *pr = http1_alloc();
pr->request.request.fd = fd;
pr->settings = settings;
pr->on_request = settings->on_request;
facil_set_timeout(fd, pr->settings->timeout);
return (protocol_s *)pr;
is_busy:
if (settings->public_folder && settings->public_folder_length) {
size_t p_len = settings->public_folder_length;
struct stat file_data = {.st_mode = 0};
char fname[p_len + 8 + 1];
memcpy(fname, settings->public_folder, p_len);
if (settings->public_folder[p_len - 1] == '/' ||
settings->public_folder[p_len - 1] == '\\')
p_len--;
memcpy(fname + p_len, "/503.html", 9);
p_len += 9;
if (stat(fname, &file_data))
goto busy_no_file;
// check that we have a file and not something else
if (!S_ISREG(file_data.st_mode) && !S_ISLNK(file_data.st_mode))
goto busy_no_file;
int file = open(fname, O_RDONLY);
if (file == -1)
goto busy_no_file;
fiobj_s *str = fiobj_str_buf(4096);
fiobj_str_write(str,
"HTTP/1.1 503 Service Unavailable\r\n"
"Content-Type: text/html\r\n"
"Connection: close\r\n"
"Content-Length: ",
94);
fiobj_str_resize(str, 94 + fio_ltoa(fiobj_obj2cstr(str).data + 94,
file_data.st_size, 10));
fiobj_str_write(str, "\r\n\r\n", 4);
fio_cstr_s s = fiobj_obj2cstr(str);
if (file_data.st_size < 3992) {
if (read(file, s.data + s.length, file_data.st_size) < 0) {
close(file);
fiobj_free(str);
goto busy_no_file;
}
close(file);
sock_write2(.uuid = fd, .buffer = str,
.offset = ((intptr_t)str - (intptr_t)s.data),
.length = (file_data.st_size + s.length),
.dealloc = (void (*)(void *))fiobj_free);
} else {
sock_write2(.uuid = fd, .buffer = str,
.offset = ((intptr_t)str - (intptr_t)s.data),
.length = s.length, .dealloc = (void (*)(void *))fiobj_free);
sock_sendfile(fd, file, 0, file_data.st_size);
sock_close(fd);
}
return NULL;
}
busy_no_file:
sock_write(fd,
"HTTP/1.1 503 Service Unavailable\r\n"
"Content-Length: 13\r\n\r\nServer Busy.",
68);
sock_close(fd);
return NULL;
}