blob: 5b5a0628104fe89e3351613d34821ee3b7bde0a5 [file] [log] [blame] [raw]
/*
copyright: Boaz segev, 2016
license: MIT
Feel free to copy, use and enjoy according to the license provided.
*/
#define _GNU_SOURCE
#include "http-protocol.h"
#include "http-mime-types.h"
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <time.h>
#include <signal.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <netdb.h>
#include <arpa/inet.h>
/////////////////
// functions used by the Http protocol, internally
#define _http_(protocol) ((struct HttpProtocol*)(protocol))
#define is_hex(c) \
(((c) >= '0' && (c) <= '9') || ((c) >= 'a' && (c) <= 'f') || \
((c) >= 'A' && c <= 'F'))
#define hex_val(c) (((c) >= '0' && (c) <= '9') ? ((c)-48) : (((c) | 32) - 87))
#define is_num(c) ((c) >= '0' && (c) <= '9')
#define num_val(c) ((c)-48)
static char* Day2Str[] = {"Sun", // the week starts on Sunday.
"Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
static char* Mon2Str[] = {"Jan", // the year starts on January.
"Feb", "Mar", "Apr", "May", "Jun", "Jul",
"Aug", "Sep", "Oct", "Nov", "Dec"};
// reviewes the request and attempts to answer with a static file.
// returns 0 if no file was found, otherwise returns 1.
static int http_sendfile(struct HttpRequest* req) {
static char* http_file_response_no_mime =
"HTTP/1.1 200 OK\r\n"
"Connection: keep-alive\r\n"
"Keep-Alive: timeout=1\r\n"
"Accept-Ranges: none\r\n"
"Content-Length: %lu\r\n"
"Date: %s, %d %s %04d %02d:%02d:%02d GMT\r\n"
"Last-Modified: %s, %d %s %04d %02d:%02d:%02d GMT\r\n"
"Cache-Control: public, max-age=3600\r\n"
"\r\n";
static char* http_file_response =
"HTTP/1.1 200 OK\r\n"
"Connection: keep-alive\r\n"
"Keep-Alive: timeout=1\r\n"
"Accept-Ranges: none\r\n"
"Content-Type: %s\r\n"
"Content-Length: %lu\r\n"
"Date: %s, %d %s %04d %02d:%02d:%02d GMT\r\n"
"Last-Modified: %s, %d %s %04d %02d:%02d:%02d GMT\r\n"
"Cache-Control: public, max-age=3600\r\n"
"\r\n";
FILE* file;
char* mime = NULL;
char* ext = NULL;
struct stat file_data = {};
// collect the protocol path and the request's path length and data
struct HttpProtocol* protocol =
(struct HttpProtocol*)Server.get_protocol(req->server, req->sockfd);
if (!protocol)
return 1;
size_t len = strlen(protocol->public_folder);
// create and initialize the filename, including decoding the path
char fname[strlen(req->path) + len + 1];
memcpy(fname, protocol->public_folder, len);
// if the ast character is a '/', step back.
if (fname[len - 1] == '/' || fname[len - 1] == '\\')
len--;
// decode and review the request->path data
int i = 0;
while (req->path[i]) {
if (req->path[i] == '+') // decode space
fname[len] = ' ';
else if (req->path[i] == '%') {
// decode hex value
if (is_hex(req->path[i + 1]) && is_hex(req->path[i + 2])) {
// this is a percent encoded value.
fname[len] =
(hex_val(req->path[i + 1]) * 16) + hex_val(req->path[i + 2]);
i += 2;
} else {
// there was an error in the URL encoding... what to do? ignore?
return 0;
}
} else
fname[len] = req->path[i];
len++;
i++;
}
fname[len] = 0;
i = 0;
// scan path string for double dots (security - prevent path manipulation)
// set the extention point value, while were doing so.
while (fname[i]) {
if (fname[i] == '.')
ext = fname + i;
// return false if we found a "/.." in our string.
if (fname[i++] == '/' && fname[i++] == '.' && fname[i++] == '.')
return 0;
}
// get file data (prevent folder access and get modification date)
if (stat(fname, &file_data))
return 0;
// check that we have a file and not something else
if (!S_ISREG(file_data.st_mode) && !S_ISLNK(file_data.st_mode))
return 0;
// fprintf(stderr, "looking for file: %s\n", fname);
if ((file = fopen(fname, "rb"))) {
// we will recycle len for the headers size and other things we want
len = 0;
// get the mime type (we have an ext pointer and the string isn't empty)
if (ext && ext[1]) {
mime = MimeType.find(ext + 1);
}
// Get a date data
struct tm t_file;
gmtime_r(&file_data.st_mtime, &t_file);
struct tm t_now;
gmtime_r(&Server.reactor(req->server)->last_tick, &t_now);
// we now need to write some headers... we can recycle the ext pointer
// for
// the data
if (HttpRequest.find(req, "range") && ((ext = HttpRequest.value(req))) &&
(ext[0] | 32) == 'b' && (ext[1] | 32) == 'y' && (ext[2] | 32) == 't' &&
(ext[3] | 32) == 'e' && (ext[4] | 32) == 's' && (ext[5] | 32) == '=') {
// ext holds the first range, starting on index 6 i.e. RANGE: bytes=0-1
static char* http_range_response =
"HTTP/1.1 206 Partial content\r\n"
"Connection: keep-alive\r\n"
"Keep-Alive: timeout=1\r\n"
"%s%s%s"
"Content-Length: %lu\r\n"
"Date: %s, %d %s %04d %02d:%02d:%02d GMT\r\n"
"Last-Modified: %s, %d %s %04d %02d:%02d:%02d GMT\r\n"
"Accept-Ranges: bytes\r\n"
"Content-Range: bytes %lu-%lu/%lu\r\n"
"Cache-Control: public, max-age=3600\r\n"
"\r\n";
size_t start = 0, finish = 0;
ext = ext + 6;
while (is_num(*ext)) {
start = start * 10;
start += num_val(*ext);
ext++;
}
if (start >= file_data.st_size - 1)
goto invalid_range;
ext++;
while (is_num(*ext)) {
finish = finish * 10;
finish += num_val(*ext);
ext++;
}
if (finish)
finish++;
if (finish && finish >= start && (finish - start) < 65536 &&
finish < file_data.st_size - 1) {
// it's a small chunk, put it in the buffer and send it as data
char* data = malloc(finish - start);
if (!data) {
fclose(file);
return 0;
}
len = fread(data, 1, finish - start, file);
if (len <= 0) {
free(data);
fclose(file);
return 0;
}
len = snprintf(
req->buffer, HTTP_HEAD_MAX_SIZE, http_range_response,
(mime ? "Content-Type: " : ""), (mime ? mime : ""),
(mime ? "\r\n" : ""), len, Day2Str[t_now.tm_wday], t_now.tm_mday,
Mon2Str[t_now.tm_mon], t_now.tm_year + 1900, t_now.tm_hour,
t_now.tm_min, t_now.tm_sec, Day2Str[t_file.tm_wday], t_file.tm_mday,
Mon2Str[t_file.tm_mon], t_file.tm_year + 1900, t_file.tm_hour,
t_file.tm_min, t_file.tm_sec, start, finish - 1, file_data.st_size);
// review the string
if (!len) {
fclose(file);
return 0;
}
// send the headers and the data (moving the pointers to the buffer)
Server.write(req->server, req->sockfd, req->buffer, len);
Server.write_move(req->server, req->sockfd, data, finish - start);
return 1;
} else {
// going to the EOF (big chunk or EOL requested) - send as file
finish = file_data.st_size - 1;
fseek(file, start, SEEK_SET);
// len = asprintf(
// &ext, http_range_response, (mime ? "Content-Type: " : ""),
// (mime ? mime : ""), (mime ? "\r\n" : ""), file_data.st_size -
// start,
// Day2Str[t_now.tm_wday], t_now.tm_mday, Mon2Str[t_now.tm_mon],
// t_now.tm_year + 1900, t_now.tm_hour, t_now.tm_min, t_now.tm_sec,
// Day2Str[t_file.tm_wday], t_file.tm_mday, Mon2Str[t_file.tm_mon],
// t_file.tm_year + 1900, t_file.tm_hour, t_file.tm_min,
// t_file.tm_sec,
// start, finish, file_data.st_size);
// // send the headers and the file
// Server.write_move(req->server, req->sockfd, ext, len);
len = snprintf(
req->buffer, HTTP_HEAD_MAX_SIZE, http_range_response,
(mime ? "Content-Type: " : ""), (mime ? mime : ""),
(mime ? "\r\n" : ""), file_data.st_size - start,
Day2Str[t_now.tm_wday], t_now.tm_mday, Mon2Str[t_now.tm_mon],
t_now.tm_year + 1900, t_now.tm_hour, t_now.tm_min, t_now.tm_sec,
Day2Str[t_file.tm_wday], t_file.tm_mday, Mon2Str[t_file.tm_mon],
t_file.tm_year + 1900, t_file.tm_hour, t_file.tm_min, t_file.tm_sec,
start, finish, file_data.st_size);
// send the headers and the file
Server.write(req->server, req->sockfd, req->buffer, len);
Server.sendfile(req->server, req->sockfd, file);
return 1;
}
}
invalid_range:
if (mime)
len = snprintf(req->buffer, HTTP_HEAD_MAX_SIZE, http_file_response, mime,
file_data.st_size, Day2Str[t_now.tm_wday], t_now.tm_mday,
Mon2Str[t_now.tm_mon], t_now.tm_year + 1900, t_now.tm_hour,
t_now.tm_min, t_now.tm_sec, Day2Str[t_file.tm_wday],
t_file.tm_mday, Mon2Str[t_file.tm_mon],
t_file.tm_year + 1900, t_file.tm_hour, t_file.tm_min,
t_file.tm_sec);
else
len = snprintf(
req->buffer, HTTP_HEAD_MAX_SIZE, http_file_response_no_mime,
file_data.st_size, Day2Str[t_now.tm_wday], t_now.tm_mday,
Mon2Str[t_now.tm_mon], t_now.tm_year + 1900, t_now.tm_hour,
t_now.tm_min, t_now.tm_sec, Day2Str[t_file.tm_wday], t_file.tm_mday,
Mon2Str[t_file.tm_mon], t_file.tm_year + 1900, t_file.tm_hour,
t_file.tm_min, t_file.tm_sec);
// review the string
if (!len) {
fclose(file);
return 0;
}
// send headers
Server.write(req->server, req->sockfd, req->buffer, len);
// send file, unless the request method is "HEAD"
if (strcasecmp("HEAD", req->method))
Server.sendfile(req->server, req->sockfd, file);
else // The file will be closed by the buffer if it's sent, otherwise...
fclose(file);
// DEBUG - print headers
// HttpRequest.first(req);
// do {
// fprintf(stderr, "%s: %s\n", HttpRequest.name(req),
// HttpRequest.value(req));
// } while (HttpRequest.next(req));
return 1;
}
return 0;
}
// implement on_close to close the FILE * for the body (if exists).
static void http_on_close(struct Server* server, int sockfd) {
struct HttpRequest* request = Server.get_udata(server, sockfd);
if (HttpRequest.is_request(request)) {
// clear the request data.
HttpRequest.clear(request);
// store the request in the object pool.
ObjectPool.push(
((struct HttpProtocol*)(Server.get_protocol(server, sockfd)))
->request_pool,
request);
}
}
// implement on_data to parse incoming requests.
static void http_on_data(struct Server* server, int sockfd) {
// setup static error codes
static char* options_req =
"HTTP/1.1 200 OK\r\n"
"Allow: GET,HEAD,POST,PUT,DELETE,OPTIONS\r\n"
"Access-Control-Allow-Origin: *\r\n"
"Accept-Ranges: none\r\n"
"Connection: closed\r\n"
"Content-Length: 0\r\n\r\n";
static char* bad_req =
"HTTP/1.1 400 Bad HttpRequest\r\n"
"Connection: closed\r\n"
"Content-Length: 16\r\n\r\n"
"Bad HTTP Request\r\n";
static char* too_big_err =
"HTTP/1.1 413 Entity Too Large\r\n"
"Connection: closed\r\n"
"Content-Length: 18\r\n\r\n"
"Entity Too Large\r\n";
static char* intr_err =
"HTTP/1.1 502 Internal Error\r\n"
"Connection: closed\r\n"
"Content-Length: 16\r\n\r\n"
"Internal Error\r\n";
// top : { ; }
int len = 0;
char* tmp1 = NULL;
char* tmp2 = NULL;
struct HttpProtocol* protocol =
(struct HttpProtocol*)Server.get_protocol(server, sockfd);
if (!protocol) {
return;
}
struct HttpRequest* request = Server.get_udata(server, sockfd);
if (request && !HttpRequest.is_request(request)) {
// someone else is using the connection and/or storage, we can't continue.
return;
}
if (!request) {
Server.set_udata(server, sockfd,
(request = ObjectPool.pop(protocol->request_pool)));
request->server = server;
request->sockfd = sockfd;
}
char* buff = request->buffer;
int pos = request->private.pos;
restart:
// is this an ongoing request?
if (request->body_file) {
char buff[HTTP_HEAD_MAX_SIZE];
int t = 0;
while ((len = Server.read(server, sockfd, buff, HTTP_HEAD_MAX_SIZE)) > 0) {
pos = len;
if (request->content_length - request->private.bd_rcved < pos) {
pos = request->content_length - request->private.bd_rcved;
}
if ((t = fwrite(buff, 1, pos, request->body_file)) < pos) {
perror("Tmpfile Err");
goto internal_error;
}
request->private.bd_rcved += pos;
}
if (request->private.bd_rcved >= request->content_length) {
rewind(request->body_file);
goto finish;
}
return;
}
// review used size
if (HTTP_HEAD_MAX_SIZE <= pos - 1)
goto too_big;
// read from the buffer
len = Server.read(server, sockfd, buff + pos, HTTP_HEAD_MAX_SIZE - pos);
if (len <= 0) {
// buffer is empty, but more data is underway or error
// anyway, don't cleanup - let `on_close` do it's job
request->private.pos = pos;
return;
}
// adjust length for buffer size positioing (so that len == max pos - 1).
len += pos;
// review the data
parse:
// check if the request is new
if (!pos) {
// start parsing the request
request->method = request->buffer;
// get query
while (pos < (len - 1) && buff[pos] != ' ')
pos++;
buff[pos++] = 0;
if (pos > len - 3) {
if (len >= HTTP_HEAD_MAX_SIZE - 32)
goto too_big;
else
goto bad_request;
}
request->path = &buff[pos];
// get query and version
while (pos < (len - 1) && buff[pos] != ' ' && buff[pos] != '?')
pos++;
if (buff[pos] == '?') {
buff[pos++] = 0;
request->query = buff + pos;
while (pos < (len - 1) && buff[pos] != ' ')
pos++;
}
buff[pos++] = 0;
if (pos + 5 > len) {
if (len >= HTTP_HEAD_MAX_SIZE - 32)
goto too_big;
else
goto bad_request;
}
request->version = buff + pos;
if (buff[pos] != 'H' || buff[pos + 1] != 'T' || buff[pos + 2] != 'T' ||
buff[pos + 3] != 'P')
goto bad_request;
// find first header name
while (pos < len - 2 && buff[pos] != '\r')
pos++;
if (pos > len - 2) // must have 2 EOL markers before a header
{
if (len >= HTTP_HEAD_MAX_SIZE - 32)
goto too_big;
else
goto bad_request;
}
buff[pos++] = 0;
buff[pos++] = 0;
request->private.header_hash = buff + pos;
request->private.max = pos;
}
if (len == 2 && buff[pos] == '\r' && buff[pos + 1] == '\n')
goto finish_headers;
// get headers
while (pos < len && buff[pos] != '\r') {
tmp1 = buff + pos;
while (pos < len && buff[pos] != ':') {
// uppercase is Ruby style.
if (buff[pos] >= 'a' && buff[pos] <= 'z')
buff[pos] = buff[pos] & 223;
// // lowercase is Node.js style.
// if (buff[pos] >= 'A' && buff[pos] <= 'Z')
// buff[pos] = buff[pos] | 32;
pos++;
}
if (pos >= len - 1) // must have at least 2 eol markers + data
{
if (len >= HTTP_HEAD_MAX_SIZE - 32)
goto too_big;
else
goto bad_request;
}
buff[pos++] = 0;
if (buff[pos] == ' ') // space after colon?
buff[pos++] = 0;
tmp2 = buff + pos;
// skip value
while (pos + 1 < len && buff[pos] != '\r')
pos++;
if (pos >= len - 1) // must have at least 2 eol markers...
{
if (len >= HTTP_HEAD_MAX_SIZE - 32)
goto too_big;
else
goto bad_request;
}
buff[pos++] = 0;
buff[pos++] = 0;
if (!strcasecmp(tmp1, "host")) {
request->host = tmp2;
// lowercase of hosts, to support case agnostic dns resolution
while (*tmp2 && (*tmp2) != ':') {
if (*tmp2 >= 'A' && *tmp2 <= 'Z')
*tmp2 = *tmp2 | 32;
tmp2++;
}
} else if (!strcasecmp(tmp1, "content-type")) {
request->content_type = tmp2;
} else if (!strcasecmp(tmp1, "content-length")) {
request->content_length = atoi(tmp2);
} else if (!strcasecmp(tmp1, "upgrade")) {
request->upgrade = tmp2;
}
}
// check if the the request was fully sent (the trailing \r\n is available)
if (pos >= len - 1) {
// break it up...
goto restart;
}
finish_headers:
// set the safety endpoint
request->private.max = pos - request->private.max;
// check for required `host` header and body content length (not chuncked)
if (!request->host || (request->content_type && !request->content_length))
goto bad_request;
// zero out the last two "\r\n" before any message body
buff[pos++] = 0;
buff[pos++] = 0;
// no body, finish up
if (!request->content_length)
goto finish;
// manage body
if (request->content_length > protocol->maximum_body_size * 1024 * 1024)
goto too_big;
// did the body fit inside the received buffer?
if (request->content_length + pos <= len) {
// point the budy to the data
request->body_str = buff + pos;
// setup a NULL terminator?
request->body_str[request->content_length] = 0;
// advance the buffer pos
pos += request->content_length;
// finish up
goto finish;
} else {
// we need a temporary file for the data.
request->body_file = tmpfile();
if (!request->body_file)
goto internal_error;
// write any trailing data to the tmpfile
if (len - pos > 0) {
if (fwrite(buff + pos, 1, len - pos, request->body_file) < len - pos)
goto internal_error;
}
// add data count to marker
request->private.bd_rcved = len - pos;
// notifications are edge based. If there's still data in the stream, we
// need to read it.
goto restart;
}
finish:
// answer the OPTIONS method, if exists
if (!strcasecmp(request->method, "OPTIONS"))
goto options;
// reset inner "pos"
request->private.pos = 0;
// disconnect the request object from the server storage
// this prevents on_close from clearing the memory while on_request is still
// accessing the request.
// It also allows upgrade protocol objects to use the storage for their
// data.
Server.set_udata(server, sockfd, NULL);
// perform callback if a file wasn't sent.
if ((!protocol->public_folder || !http_sendfile(request)) &&
protocol->on_request) {
protocol->on_request(request);
}
if (Server.get_udata(server, sockfd)) {
// someone else already started using this connection...
goto cleanup_after_finish;
}
if (pos < len) {
// if we have more data in the pipe, clear the request, move the buffer data
// and return to the beginning of the parsing.
HttpRequest.clear(request);
// move the data left in the buffer to the beginning of the buffer.
for (size_t i = 0; i < len - pos; i++) {
request->buffer[i] = request->buffer[pos + i];
}
len = len - pos;
pos = 0;
Server.set_udata(server, sockfd, request);
goto parse;
}
if (len == HTTP_HEAD_MAX_SIZE) {
// we might not have read all the data in the network socket.
// since we're edge triggered, we should continue reading.
len = Server.read(server, sockfd, buff, HTTP_HEAD_MAX_SIZE);
if (len > 0) {
HttpRequest.clear(request);
Server.set_udata(server, sockfd, request);
goto parse;
}
}
cleanup_after_finish:
// we need to destroy the request ourselves, because we disconnected the
// request from the server's udata.
HttpRequest.clear(request);
ObjectPool.push(protocol->request_pool, request);
return;
options:
// send a bed request response. hang up.
send(sockfd, options_req, strlen(options_req), 0);
Server.close(request->server, sockfd);
return;
bad_request:
// send a bed request response. hang up.
send(sockfd, bad_req, strlen(bad_req), 0);
Server.close(request->server, sockfd);
return;
too_big:
// send a bed request response. hang up.
send(sockfd, too_big_err, strlen(too_big_err), 0);
Server.close(request->server, sockfd);
return;
internal_error:
// send an internal error response. hang up.
send(sockfd, intr_err, strlen(intr_err), 0);
Server.close(request->server, sockfd);
return;
}
// implement on_data to parse incoming requests.
void http_default_on_request(struct HttpRequest* req) {
// the response format
static char* http_format =
"HTTP/1.1 200 OK\r\n"
"Connection: keep-alive\r\n"
"Keep-Alive: 1\r\n"
"Content-Length: %d\r\n\r\n"
"%s";
static char* http_file_echo =
"HTTP/1.1 200 OK\r\n"
"Connection: keep-alive\r\n"
"Keep-Alive: 1\r\n"
"Content-Type: %s\r\n"
"Content-Length: %d\r\n\r\n";
if (req->body_file) {
char* head = NULL;
if (asprintf(&head, http_file_echo, req->content_type,
req->content_length) <= 0 ||
!head) {
perror("WTF?! head");
return;
}
if (Server.write_move(req->server, req->sockfd, head, strlen(head)) < 0)
return;
head = malloc(req->content_length + 1); // the +1 is redundent.
if (!head) {
perror("WTF?! body");
return;
}
if (!fread(head, 1, req->content_length, req->body_file)) {
perror("WTF?! file reading");
free(head);
return;
}
Server.write_move(req->server, req->sockfd, head, req->content_length);
return;
}
// write reques's head onto the buffer
char buff[HTTP_HEAD_MAX_SIZE] = {0};
int pos = 0;
strcpy(buff, req->method);
pos += strlen(req->method);
buff[pos++] = ' ';
strcpy(buff + pos, req->path);
pos += strlen(req->path);
if (req->query) {
buff[pos++] = '?';
strcpy(buff + pos, req->query);
pos += strlen(req->query);
}
buff[pos++] = ' ';
strcpy(buff + pos, req->version);
pos += strlen(req->version);
buff[pos++] = '\r';
buff[pos++] = '\n';
HttpRequest.first(req);
do {
strcpy(buff + pos, HttpRequest.name(req));
pos += strlen(HttpRequest.name(req));
buff[pos++] = ':';
strcpy(buff + pos, HttpRequest.value(req));
pos += strlen(HttpRequest.value(req));
buff[pos++] = '\r';
buff[pos++] = '\n';
} while (HttpRequest.next(req));
if (req->body_str) {
buff[pos++] = '\r';
buff[pos++] = '\n';
memcpy(buff + pos, req->body_str, req->content_length);
pos += req->content_length;
}
buff[pos++] = 0;
// Prep reply
char* reply;
int buff_len = strlen(buff);
buff_len = asprintf(&reply, http_format, buff_len, buff);
// check
if (!reply) {
perror("WTF?!");
Server.close(req->server, req->sockfd);
return;
}
// send(req->sockfd, reply, strlen(reply), 0);
Server.write_move(req->server, req->sockfd, reply, buff_len);
}
////////////////
// public API
static char http_service_name[] = "http";
/** returns a new, initialized, Http Protocol object. */
struct HttpProtocol* HttpProtocol_new(void) {
struct HttpProtocol* http = malloc(sizeof(struct HttpProtocol));
memset(http, 0, sizeof(struct HttpProtocol));
http->parent.service = http_service_name;
http->parent.on_data = http_on_data;
http->parent.on_close = http_on_close;
http->maximum_body_size = 32;
http->on_request = http_default_on_request;
http->public_folder = NULL;
// void* (*create)(void), void (*destroy)(void* object), int size
http->request_pool =
ObjectPool.new_dynamic((void* (*)(void))HttpRequest.new,
(void (*)(void*))HttpRequest.destroy, 32);
return http;
}
void HttpProtocol_destroy(struct HttpProtocol* http) {
ObjectPool.destroy(http->request_pool);
free(http);
}
struct HttpProtocolClass HttpProtocol = {
.new = HttpProtocol_new,
.destroy = HttpProtocol_destroy,
};