You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

976 lines
24 KiB
C

#define _GNU_SOURCE
#include "request.h"
#include "collections/list.h"
#include "worker_pool.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <unistd.h>
#include <errno.h>
#include <stdbool.h>
#include <stddef.h>
#include <ctype.h>
#include <fcntl.h>
#include <stdint.h>
#include <time.h>
#include <sys/stat.h>
enum {
recv_buffer_size = 512,
};
struct buffered_write
{
struct buffered_write* next;
char* buffer;
int size;
};
struct http_request
{
bool debug;
// Socket request was received on
int sock;
bool has_nonblocking_read;
// Status
bool complete;
struct {
bool (*callback)( void* data );
void* data;
} nonblock;
// HTTP Protocol request data
char* remote_addr;
char* method;
char* full_path;
int levels_to_root;
char* path;
struct header* request_headers;
// body
FILE* response_body;
FILE* request;
bool chunked_body;
long response_size;
struct buffered_write* response_body_buffer;
struct header* response_headers;
bool sent_response_headers;
size_t unread_post_content;
// Memory holds
char* first_line;
// Fiber
ucontext_t context;
// Request handler callback
void (*handler)( struct http_request* req, void* handler_data );
void* handler_data;
// Linked list data
struct http_request* next;
};
struct header
{
struct header* next;
struct http_header data;
};
ssize_t getline_stripped( char** restrict lineptr, size_t* restrict n, FILE* restrict stream)
{
ssize_t res = getline( lineptr, n, stream );
if( res == -1 ) {
return -1;
}
if( res == 0 ) {
return 0;
}
while( res > 0 && isspace((*lineptr)[res-1]) ) {
(*lineptr)[res-1] = '\0';
res -= 1;
}
return res;
}
static __thread struct http_request* current_request = NULL;
static void request_fiber_entry()
{
// Pull off current_request so that other fibers can be created and we won't lose our data
struct http_request* req = current_request;
FILE* f = http_request_get_request_data( req );
// Read first line, should be something like: <METHOD> <PATH> HTTP/1.1\n
size_t n = 0;
if( getline_stripped( &req->first_line, &n, f ) <= 0 ) {
goto failed;
}
char* ptr = NULL;
req->method = strtok_r( req->first_line, " ", &ptr );
req->path = req->full_path = strtok_r( NULL, " ", &ptr );
if( !req->method ) { goto failed; }
if( !req->path ) { goto failed; }
printf( "> %s %s\n", req->method, req->path );
int depth = -1;
int len = strlen(req->full_path);
for( int i = 0; i < len; ++i ) {
if( req->full_path[i] == '/' ) {
depth += 1;
}
}
req->levels_to_root = depth;
int length;
int content_length = 0;
do {
n = 0;
getline_stripped( &ptr, &n, f );
length = strlen(ptr);
if( length > 0 ) {
char* value;
char* key = strtok_r( ptr, ":", &value );
while( *value == ' ' ) { ++value; }
struct header* h = (struct header*)malloc(sizeof(struct header));
h->data.key = key;
h->data.value = strdup(value);
if( req->debug ) {
printf( "Header: %s: %s\n", h->data.key, h->data.value );
}
list_add_front( &req->request_headers, h );
if( 0 == strcmp("Content-Length", key ) ) {
sscanf( value, "%u", &content_length );
}
} else {
free(ptr);
}
} while( length > 0 );
req->unread_post_content = content_length;
req->handler( req, req->handler_data );
fflush( req->response_body );
if( req->chunked_body ) {
req->chunked_body = false;
fprintf( req->response_body, "0\n\n" );
}
fflush( req->response_body );
//printf( "Request completed.\n" );
failed:
req->complete = true;
}
static bool ready( void* data )
{
return true;
}
struct io_bytes_args
{
int fd;
int size;
char* buffer;
int size_read;
bool failed;
};
static void read_bytes_async( void* data )
{
//struct read_bytes_args* args = (struct read_bytes_args*)data;
}
static bool read_bytes_nonblock( void* data )
{
struct io_bytes_args* args = (struct io_bytes_args*)data;
ssize_t res = read( args->fd, args->buffer, args->size );
if( res == -1 ) {
if( errno == EAGAIN || errno == EWOULDBLOCK ) {
return false;
} else {
printf( "Error reading from %d: %s\n", args->fd, strerror(errno) );
args->failed = true;
return true;
}
} else {
args->size_read = res;
return true;
}
}
static bool write_bytes_nonblock( void* data )
{
struct io_bytes_args* args = (struct io_bytes_args*)data;
if( args->failed ) {
return true;
}
ssize_t res = write( args->fd, args->buffer, args->size );
if( res == -1 ) {
if( errno == EAGAIN || errno == EWOULDBLOCK ) {
return false;
} else {
printf( "Error writing to %d: %s\n", args->fd, strerror(errno) );
args->failed = true;
return true;
}
} else {
args->size_read += res;
if( res < args->size ) {
args->buffer = &args->buffer[res];
args->size -= res;
return false;
} else {
return true;
}
}
}
static ssize_t http_request_read( void* cookie, char* buf, size_t size )
{
struct http_request* req = (struct http_request*)cookie;
if( req->unread_post_content == -1 ) {
size = 1;
} else if( req->unread_post_content == 0 ) {
return 0;
} else {
if( size > req->unread_post_content ) {
size = req->unread_post_content;
}
}
struct io_bytes_args args;
args.fd = req->sock;
args.failed = false;
args.buffer = buf;
args.size = size;
if( req->has_nonblocking_read ) {
http_request_call_nonblock( read_bytes_nonblock, &args );
} else {
http_request_call_async( read_bytes_async, &args );
}
// Force the request to terminate
if( args.failed ) {
printf( "Read request failed\n" );
req->complete = true;
http_request_call_nonblock( ready, NULL );
}
if( req->unread_post_content != -1 ) {
req->unread_post_content -= args.size_read;
}
return args.size_read;
}
static ssize_t http_request_write_body( struct http_request* req, const char* buf, size_t size )
{
if( !req ) {
return 0;
}
/*
printf( "> " );
for( int i = 0; i < size; ++i ) {
if( isprint(buf[i]) ) {
printf( "%.1s", &buf[i] );
} else if( buf[i] == '\n' ) {
printf( "\\n" );
} else {
printf( "\\x%02x", (int)buf[i] );
}
}
printf( " (%d)\n", size );
//*/
struct io_bytes_args args;
args.fd = req->sock;
args.failed = false;
args.buffer = (char*)buf;
args.size_read = 0;
args.size = size;
//if( req->has_nonblocking_read ) {
http_request_call_nonblock( write_bytes_nonblock, &args );
//} else {
// http_request_call_async( read_bytes_async, &args );
//}
// Force the request to terminate
if( args.failed ) {
printf( "Write request failed\n" );
req->complete = true;
http_request_call_nonblock( ready, NULL );
}
return args.size_read;
}
static ssize_t http_request_body_write( void* cookie, const char* buf, size_t size )
{
// TODO: rework cookie to allow request to be deleted and have this just dump the write data
struct http_request* req = (struct http_request*)cookie;
if( req->sent_response_headers ) {
if( req->chunked_body ) {
char buffer[32];
int res = snprintf( buffer, 32, "%lX\n", size );
http_request_write_body( req, buffer, res );
ssize_t written = http_request_write_body( req, buf, size );
http_request_write_body( req, "\n", 1 );
return written;
} else {
ssize_t written = http_request_write_body( req, buf, size );
if( written != size ) {
printf( "tried to write %lu, but only wrote %lu\n", size, written );
}
return written;
}
} else {
printf( "Response headers haven't been sent yet.\n" );
printf( "TODO: buffer response\n" );
}
return 0;
}
static int http_request_body_close( void* cookie )
{
return 0;
}
static int http_request_seek( void* cookie, off64_t*, int )
{
return 0;
}
static cookie_io_functions_t http_response_functions = {
.read = http_request_read,
.write = http_request_body_write,
.seek = http_request_seek,
.close = http_request_body_close,
};
struct http_request* http_request_new( ucontext_t* main, int sock, const char* remote_addr, http_server_handler handler, void* handler_data )
{
struct http_request* req = (struct http_request*)malloc(sizeof(struct http_request));
if( !req ) {
printf( "Failed to allocate memory for request\n" );
return NULL;
}
req->debug = false;
req->complete = false;
req->nonblock.callback = ready;
req->nonblock.data = NULL;
req->sock = sock;
req->remote_addr = strdup(remote_addr);
req->handler = handler;
req->handler_data = handler_data;
req->first_line = NULL;
req->request_headers = NULL;
req->response_headers = NULL;
req->sent_response_headers = false;
req->chunked_body = false;
req->response_size = -1;
req->unread_post_content = -1;
req->response_body = fopencookie( (void*)req, "w", http_response_functions );
req->request = fopencookie( (void*)req, "r", http_response_functions );
if( -1 == fcntl( sock, F_SETFL, O_NONBLOCK ) ) {
printf( "Unable to set O_NONBLOCK on request socket, continuing with reduced capacity: %s\n", strerror(errno) );
req->has_nonblocking_read = false;
} else {
req->has_nonblocking_read = true;
}
if( -1 == getcontext( &req->context ) ) {
printf( "Failed to create context for request: %s\n", strerror(errno) );
req->complete = true;
return req;
}
req->context.uc_stack.ss_size = 16384 + 15000;
req->context.uc_stack.ss_sp = (char*)malloc( req->context.uc_stack.ss_size );
if( !req->context.uc_stack.ss_sp ) {
printf( "Failed to allocate stack for request.\n");
free(req);
close(sock);
return NULL;
}
req->context.uc_link = main;
makecontext( &req->context, request_fiber_entry, 0 );
return req;
}
static uint64_t curr_time()
{
//uint64_t time;
struct timespec ts;
clock_gettime( CLOCK_REALTIME, &ts );
return (uint64_t)ts.tv_sec * 1000000000 + (uint64_t)ts.tv_nsec;
}
void http_request_release( struct http_request* req )
{
close( req->sock );
//printf( "socket %d closed at %lu\n", req->sock, curr_time() );
for( struct header* iter = req->request_headers; iter; ) {
struct header* next = iter->next;
free(iter->data.key);
free(iter->data.value);
free(iter);
iter = next;
}
for( struct header* iter = req->response_headers; iter; ) {
struct header* next = iter->next;
free(iter->data.key);
free(iter->data.value);
free(iter);
iter = next;
}
free( req->first_line );
free( req->context.uc_stack.ss_sp );
free( req->remote_addr );
free( req );
}
void http_request_set_debug( struct http_request* req, bool debug )
{
req->debug = debug;
}
void http_request_append_list_front( struct http_request* req, struct http_request** list_head )
{
req->next = (*list_head);
*list_head = req;
}
int http_request_clear_completed_at( struct http_request** list_head )
{
int count = 0;
struct http_request* req;
while( (req = *list_head) && req->complete ) {
*list_head = req->next;
http_request_release( req );
count += 1;
}
return count;
}
int http_request_clear_completed( struct http_request* req )
{
return http_request_clear_completed_at( &req->next );
}
struct http_request* http_request_get_next( struct http_request* req )
{
return req->next;
}
const char* http_request_get_remote_host_address( struct http_request* req )
{
return req->remote_addr;
}
const char* http_request_get_header( struct http_request* req, const char* header )
{
for( struct header* iter = req->request_headers; iter; iter = iter->next ) {
if( 0 == strcasecmp( header, iter->data.key ) ) {
return iter->data.value;
}
}
return NULL;
}
void http_request_copy_headers( struct http_request* req, struct collection dst )
{
struct collection src = {
.ptr = &req->request_headers,
.vtable = &list_vtable,
.itable = &http_header_itable,
};
list_copy_cc( src, dst );
}
const char* string_for_status( int status_code )
{
switch( status_code ) {
case 200:
return "Ok";
case 202:
return "Accepted";
case 302:
return "Found";
case 304:
return "Not Modified";
case 401:
return "Unauthorized";
case 402:
return "Payment Required";
case 404:
return "Not Found";
default:
return "Unknown";
}
}
void http_request_begin_send_headers( struct http_request* req, int status_code, bool chunked )
{
req->chunked_body = false;
FILE* f = http_request_get_response_body(req);
req->sent_response_headers = true;
fprintf( f, "HTTP/1.1 %d %s\n", status_code, string_for_status( status_code ) );
if( req->debug || ( status_code >= 400 ) ) {
printf( "> %s %s -> %d %s\n", req->method, req->full_path, status_code, string_for_status( status_code ) );
}
//http_request_send_header( req, "Connection", "close" );
if( chunked ) {
http_request_send_header( req, "Transfer-Encoding", "chunked" );
} else {
char buffer[512];
snprintf( buffer, 512, "%ld", req->response_size );
http_request_send_header( req, "Content-Size", buffer );
}
}
void http_request_send_header( struct http_request* req, const char* header, const char* value )
{
FILE* f = http_request_get_response_body(req);
fprintf( f, "%s: %s\n", header, value );
}
static const char* day_of_week_str[] = {
"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
};
static const char* month_of_year_str[] = {
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
};
void http_request_send_header_last_modified( struct http_request* req, time_t timestamp )
{
struct tm t;
gmtime_r( &timestamp, &t );
char buffer[100];
snprintf( buffer, sizeof(buffer), "%s, %02d %s %04d %02d:%02d:%02d GMT",
day_of_week_str[t.tm_wday],
t.tm_mday,
month_of_year_str[t.tm_mon],
t.tm_year + 1900,
t.tm_hour, t.tm_min, t.tm_sec
);
http_request_send_header( req, "Last-Modified", buffer );
}
void http_request_end_send_headers( struct http_request* req, bool chunked )
{
FILE* f = http_request_get_response_body(req);
for( struct header* h = req->response_headers; h; h = h->next ) {
fprintf( f, "%s: %s\n", h->data.key, h->data.value );
}
fprintf( f, "\n" );
fflush(f);
req->chunked_body = chunked;
}
void http_request_add_header( struct http_request* req, const char* header, const char* value )
{
struct header* h;
h = malloc(sizeof(*h));
memset(h,0,sizeof(*h));
h->data.key = strdup(header);
h->data.value = strdup(value);
h->next = req->response_headers;
req->response_headers = h;
}
void http_request_send_headers( struct http_request* req, int status_code, const char* content_type, bool chunked )
{
http_request_begin_send_headers( req, status_code, chunked );
http_request_send_header( req, "Content-Type", content_type );
http_request_end_send_headers( req, chunked );
}
FILE* http_request_get_response_body( struct http_request* req )
{
return req->response_body;
}
FILE* http_request_get_request_data( struct http_request* req )
{
return req->request;
}
static void find_precompressed_data( struct http_request* req, const char* fs_path, char* buffer, int size, char* encoding, int encoding_size )
{
const char* accept_encoding = http_request_get_header(req, "Accept-Encoding" );
strncpy( buffer, fs_path, size );
struct stat file_data;
if( -1 == stat(fs_path,&file_data) ) {
return;
}
int min_size = file_data.st_size;
char candidate[512];
if( accept_encoding ) {
//printf( "Acceptable encodings: %s\n", accept_encoding );
char encodings[512];
strncpy(encodings,accept_encoding,sizeof(encodings));
char* remain = NULL;
for( char* enc = strtok_r(encodings,",",&remain); enc; enc = strtok_r(NULL,",",&remain) ) {
// Strip whitespace
while( *enc == ' ') { ++enc; }
char* end = &enc[strlen(enc)-1];
while( *end == ' ') { *enc = '\0'; --enc; }
//printf( "Encoding: %s\n", enc );
for( char* i = enc; *i; ++i ) {
if( !isalpha(*i) ) { goto skip_encoding; }
}
snprintf( candidate, sizeof(candidate), "%s.%s", fs_path, enc );
//printf( "Candidate filename: %s\n", candidate );
if( 0 == stat(candidate,&file_data) ) {
if( file_data.st_size < min_size ) {
//printf( "Current minimum is %d bytes for %s\n", size, candidate );
strncpy( buffer, candidate, size );
min_size = file_data.st_size;
strncpy( encoding, enc, encoding_size );
}
}
skip_encoding:
}
}
if( *encoding ) {
printf( "Using file: %s (encoded with %s)\n", buffer, encoding );
} else {
printf( "Using file: %s\n", buffer );
}
}
static bool try_if_modified_since( struct http_request* req, time_t last_modified )
{
const char* if_modified_since_header = http_request_get_header( req, "If-Modified-Since" );
if( !if_modified_since_header ) {
//printf( "IMS -> No header\n" );
return false;
}
// Expected format Sat, 31 Dec 2022 19:59:14 GMT
struct tm t = { 0 };
char day_of_week[4] = { 0 };
char month[4] = { 0 };
char timezone[4] = { 0 };
int count = 0;
if( 8 != (count=sscanf( if_modified_since_header, "%3s, %d %3s %d %d:%d:%d %3s",
day_of_week,
&t.tm_mday,
month,
&t.tm_year,
&t.tm_hour, &t.tm_min, &t.tm_sec,
timezone
)) ) {
printf( "IMS -> Date format error. Got only %d items\n", count );
return false;
}
if( 0 != strcmp(timezone,"GMT") ) {
printf( "IMS -> Unsupported timezone\n" );
return false;
}
t.tm_year -= 1900;
time_t test_time = timegm(&t);
if( last_modified < test_time ) {
printf( "IMS -> %ld < %ld = Has been modified, resend required\n", last_modified, test_time );
return false;
}
printf( "If-Modified-Since = %s -> hasn't been modified\n", if_modified_since_header );
http_request_begin_send_headers( req, 304, false );
http_request_send_header_last_modified( req, last_modified );
http_request_end_send_headers( req, false );
return true;
}
bool http_request_send_file( struct http_request* req, const char* fs_path, const char* content_type )
{
char filename[512];
char encoding[30] = "";
find_precompressed_data( req, fs_path, filename, sizeof(filename), encoding, sizeof(encoding) );
FILE* f = fopen( filename, "r" );
if( !f ) {
printf( "Unable to open file %s: %s\n", fs_path, strerror(errno) );
return false;
}
// Get file information
struct stat s;
stat( fs_path, &s );
time_t last_modified = s.st_mtim.tv_sec;
if( try_if_modified_since( req, last_modified ) ) {
return true;
}
// Get filesize
stat( filename, &s );
req->response_size = s.st_size;
http_request_begin_send_headers( req, 200, false );
if( *encoding ) {
http_request_send_header( req, "Content-Encoding", encoding );
}
http_request_send_header( req, "Content-Type", content_type );
http_request_send_header( req, "Cache-Control", "public,max-age=3600" );
http_request_send_header_last_modified( req, last_modified );
http_request_end_send_headers( req, false );
FILE* body = http_request_get_response_body( req );
size_t res;
char buffer[4096];
uint64_t start_time = curr_time();
/*
printf( "Starting file transfer %s at %lu.%lu\n",fs_path,
start_time / 1000000000,
start_time % 1000000000
);
//*/
while(( res = fread( buffer, 1, sizeof(buffer), f ) )) {
//printf( "Writing %d bytes at %llu\n", res, curr_time() );
fwrite( buffer, 1, res, body );
}
uint64_t finish_time = curr_time();
printf( "File transfer %s completed at %lu.%lu (took %lu.%09lus)\n",
fs_path,
finish_time / 1000000000,
finish_time % 1000000000,
(finish_time - start_time) / 1000000000,
(finish_time - start_time) % 1000000000
);
fflush(body);
fclose(f);
return true;
}
const char* http_request_get_relative_path( struct http_request* req, const char* path )
{
if( path[0] != '/' ) {
return path;
}
int size = strlen(path) + 3 * req->levels_to_root;
char* res = malloc( size );
res[0] = '\0';
for( int i = 0; i < req->levels_to_root; ++i ) {
strcat( res, "../" );
}
return strcat( res, &path[1] );
}
bool http_request_route( struct http_request* req, const char* str )
{
int len = strlen(str);
if( 0 == strncmp( req->path, str, len ) ) {
req->path += len;
return true;
}
return false;
}
bool http_request_route_term( struct http_request* req, const char* str )
{
int len = strlen(str);
if( 0 == strncmp( req->path, str, len ) ) {
if( req->path[len] == '\0' ) {
req->path += len;
return true;
}
}
return false;
}
bool http_request_route_method( struct http_request* req, const char* method )
{
return ( 0 == strcmp( method, req->method ) );
}
char* http_request_route_get_dir( struct http_request* req )
{
int len = strlen( req->path );
for( int i = 0; i < len; ++i ) {
char ch = req->path[i];
if( ch == '/' ) {
char* res = strndup( req->path, i );
req->path += ( i + 1 );
return res;
}
}
return NULL;
}
char* http_request_route_get_dir_or_file( struct http_request* req )
{
int len = strlen( req->path );
for( int i = 0; i < len; ++i ) {
char ch = req->path[i];
if( ch == '?' || ch == '/' ) {
char* res = strndup( req->path, i );
req->path += ( i + 1 );
return res;
}
}
char* res = strdup(req->path);
req->path += len;
return res;
}
const char* http_request_route_query_key( struct http_request* req )
{
return strtok_r( req->path, "=", &req->path );
}
const char* http_request_route_query_value( struct http_request* req )
{
if( req->path[0] == '&' ) {
req->path += 1;
return "";
}
char* result = strtok_r( req->path, "&", &req->path );
if( !result ) { return ""; }
// In-place unescape of %XX codes
int i;
char* out = result;
for( char* iter = result; *iter; ++iter,++out ) {
if( *iter == '%' ) {
sscanf(&iter[1],"%02X",&i);
*out = i;
iter += 2;
} else {
*out = *iter;
}
}
*out = '\0';
return result;
}
const char* http_request_get_full_path( struct http_request* req )
{
return req->full_path;
}
const char* http_request_get_remaining_path( struct http_request* req )
{
return req->path;
}
void http_request_route_eat_path( struct http_request* req, int count )
{
int remaining = strlen(req->path);
if( count > remaining ) {
count = remaining;
}
req->path = &req->path[count];
}
void http_request_process( struct http_request* req, ucontext_t* fallback )
{
if( !req->nonblock.callback( req->nonblock.data ) ) {
//printf( "Non-block isn't finished for %p -> %s\n", req, req->full_path );
return;
}
// Pass the current request into the entry point thru thread-local storage (safe-ish)
current_request = req;
// Run the fiber
if( swapcontext( fallback, &req->context ) == -1 ) {
printf( "Error entering fiber context: %s\n", strerror(errno) );
req->complete = true;
}
// Clear entry point data
current_request = NULL;
}
struct async_work_data
{
void (*func)( void* data );
void* func_data;
bool complete;
};
static void async_work( void* data_void )
{
struct async_work_data* data = (struct async_work_data*)( data_void );
data->func( data->func_data );
data->complete = true;
}
static bool check_async_work_complete( void* data_void )
{
struct async_work_data* data = (struct async_work_data*)( data_void );
return data->complete;
}
void http_request_call_async( void(*func)( void* ), void* data )
{
// Convert async call to future + nonblocking check for completion
struct async_work_data aw_data;
aw_data.func = func;
aw_data.func_data = data;
aw_data.complete = false;
// Submit the task to the worker pool
worker_pool_add_task( async_work, &aw_data );
// Handle nonblocking callback (will return when async work is finished)
http_request_call_nonblock( check_async_work_complete, &aw_data );
}
void http_request_call_nonblock( bool(*func)( void* ), void* data )
{
if( func != ready && func(data) ) {
return;
}
// Hold on to the current request
struct http_request* req = current_request;
if( !req ) {
func(data);
return;
}
// Preserve the old callback data
bool (*old_nonblock_callback)(void*) = req->nonblock.callback;
void* old_nonblock_data = req->nonblock.data;
// Update to point to new callback
req->nonblock.callback = func;
req->nonblock.data = data;
// Yield this fiber. We will not return here until the callback returns
// true.
//printf( "Swapping out %p -> %s\n", req, req->full_path );
if( -1 == swapcontext( &req->context, req->context.uc_link ) ) {
printf( "Failure during context swap: %s\n", strerror(errno) );
exit(1);
}
//printf( "Swapping in %p -> %s\n", req, req->full_path );
// restor the old callback data
req->nonblock.callback = old_nonblock_callback;
req->nonblock.data = old_nonblock_data;
}