Files
csocial/serve.c
T
2024-10-05 20:55:35 +02:00

5768 lines
145 KiB
C

///////////////////////////////////////////////////////////////////////////////////////////////
/// This is free and unencumbered software released into the public domain. ///
/// ///
/// Anyone is free to copy, modify, publish, use, compile, sell, or ///
/// distribute this software, either in source code form or as a compiled ///
/// binary, for any purpose, commercial or non-commercial, and by any ///
/// means. ///
/// ///
/// In jurisdictions that recognize copyright laws, the author or authors ///
/// of this software dedicate any and all copyright interest in the ///
/// software to the public domain. We make this dedication for the benefit ///
/// of the public at large and to the detriment of our heirs and ///
/// successors. We intend this dedication to be an overt act of ///
/// relinquishment in perpetuity of all present and future rights to this ///
/// software under copyright law. ///
/// ///
/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, ///
/// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF ///
/// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. ///
/// IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR ///
/// OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ///
/// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR ///
/// OTHER DEALINGS IN THE SOFTWARE. ///
/// ///
/// For more information, please refer to <https://unlicense.org> ///
/// ///
/// NOTE: Some code was adapted from BearSSL. That code uses the MIT license. ///
///////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////
/// HEADERS ///
///////////////////////////////////////////////////////////////////////////////////////////////
#define _GNU_SOURCE
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <stdint.h>
#include <stddef.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <assert.h>
#include <dirent.h>
#include <stdarg.h>
#include <limits.h>
#include <stdbool.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/resource.h>
#include <arpa/inet.h>
#include <poll.h>
#include <time.h>
#include "sqlite3.h"
#include "tinytemplate.h"
///////////////////////////////////////////////////////////////////////////////////////////////
/// CONFIGURATION ///
///////////////////////////////////////////////////////////////////////////////////////////////
#ifndef HTTPS
#define HTTPS 0
#endif
#define BACKTRACE 1
#define BACKTRACE_FILE "backtrace.txt"
#define BACKTRACE_LIMIT 30
#define EOPALLOC 0
#define PROFILE 0
#define INPUT_BUFFER_LIMIT_MB 1
///////////////////////////////////////////////////////////////////////////////////////////////
/// OPTIONAL HEADERS ///
///////////////////////////////////////////////////////////////////////////////////////////////
#if HTTPS
#include <bearssl.h>
#endif
#if PROFILE
#include <x86intrin.h>
#endif
#if EOPALLOC
#include <sys/mman.h>
#endif
#if BACKTRACE
#include <dlfcn.h>
#include <execinfo.h>
#endif
///////////////////////////////////////////////////////////////////////////////////////////////
/// TYPES & DEFINITIONS ///
///////////////////////////////////////////////////////////////////////////////////////////////
typedef struct {
char *data;
size_t size;
} string;
#define LIT(S) ((string) {.data=(S), .size=sizeof(S)-1})
#define STR(S) ((string) {.data=(S), .size=strlen(S)})
#define MIN(X, Y) ((X) < (Y) ? (X) : (Y))
#define MAX(X, Y) ((X) > (Y) ? (X) : (Y))
#define SIZEOF(X) ((ssize_t) sizeof(X))
#define COUNTOF(X) (SIZEOF(X) / SIZEOF((X)[0]))
#define NULLSTR ((string) {.data=NULL, .size=0})
#ifndef NDEBUG
#define DEBUG(fmt, ...) write_format_to_stderr(fmt, ## __VA_ARGS__)
#else
#define DEBUG(...) {}
#endif
#if PROFILE
#define TIME(label) for (uint64_t start__ = __rdtsc(), done__ = 0; !done__; timing_result(__COUNTER__, __rdtsc() - start__, LIT(label)), (done__=1))
#else
#define TIME(...)
#endif
#if HTTPS
typedef struct {
int type; // BR_KEYTYPE_RSA or BR_KEYTYPE_EC
union {
br_rsa_private_key rsa;
br_ec_private_key ec;
};
} PrivateKey;
typedef struct {
br_x509_certificate *items;
int count;
int capacity;
} CertArray;
typedef struct {
int code;
string name;
string comment;
} BearSSLErrorInfo;
#endif
typedef enum {
URL_HOSTMODE_NAME,
URL_HOSTMODE_IPV4,
URL_HOSTMODE_IPV6,
} url_hostmode;
typedef struct {
url_hostmode mode;
union {
uint32_t ipv4;
uint16_t ipv6[8];
string name;
};
bool no_port;
uint16_t port;
} url_host;
typedef struct {
string username;
string password;
} url_userinfo;
typedef struct {
url_host host;
url_userinfo userinfo;
string path;
string query;
string schema;
string fragment;
} url_t;
enum {
P_OK,
P_INCOMPLETE,
P_BADMETHOD,
P_BADVERSION,
P_BADHEADER,
P_BADURL,
};
enum {
T_CHUNKED = 1 << 0,
T_COMPRESS = 1 << 1,
T_DEFLATE = 1 << 2,
T_GZIP = 1 << 3,
};
typedef enum {
M_GET,
M_POST,
M_HEAD,
M_PUT,
M_DELETE,
M_CONNECT,
M_OPTIONS,
M_TRACE,
M_PATCH,
} Method;
#define MAX_HEADERS 32
typedef struct {
string name;
string value;
} Header;
typedef struct {
Method method;
url_t url;
int major;
int minor;
int nheaders;
Header headers[MAX_HEADERS];
string content;
} Request;
typedef struct {
char *data;
size_t head;
size_t size;
size_t capacity;
} ByteQueue;
typedef struct {
int fd;
ByteQueue input;
ByteQueue output;
uint32_t ipaddr;
int served_count;
bool https;
bool closing;
bool keep_alive;
uint64_t creation_time;
uint64_t start_time;
#if HTTPS
br_ssl_server_context https_context;
char https_buffer[BR_SSL_BUFSIZE_BIDI];
#endif
} Connection;
typedef enum {
R_STATUS,
R_HEADER,
R_CONTENT,
R_COMPLETE,
} ResponseBuilderState;
typedef struct {
ResponseBuilderState state;
Connection *conn;
bool failed;
bool keep_alive;
size_t content_length_offset;
size_t content_offset;
} ResponseBuilder;
typedef uint32_t SessionID;
#define NO_SESSION ((SessionID) -1)
#define MAX_SESSIONS 512
#define MAX_USER_NAME 32
#define MAX_USER_PASS 256
#define MAX_USER_BIO 1024
#define MAX_POST_TITLE 1024
#define MAX_POST_CONTENT (1<<14)
typedef struct {
uint32_t id;
string name;
char namebuf[MAX_USER_NAME];
} Session;
typedef enum {
TPT_INT,
TPT_FLOAT,
TPT_STRING,
TPT_QUERY,
TPT_LAST,
} TemplateParamType;
typedef struct {
TemplateParamType type;
string name;
union {
int64_t i;
double f;
string s;
sqlite3_stmt *q;
};
} TemplateParam;
///////////////////////////////////////////////////////////////////////////////////////////////
/// FORWARD DECLARATIONS ///
///////////////////////////////////////////////////////////////////////////////////////////////
bool url_parse2(string str, size_t *i, url_t *url);
bool url_parse(string str, url_t *url);
bool url_parse_ipv6(string str, uint16_t out[8]);
bool url_parse_ipv4(string str, uint32_t *out);
void config_init(void);
void config_free(void);
bool config_load(string file);
uint32_t config_int(string name);
bool config_bool(string name);
string config_string(string name);
void log_init(string dir, size_t dir_limit_mb, size_t file_limit_b, size_t buffer_size);
void log_free(void);
void log_data(string str);
void log_fatal(string str);
void log_perror(string str);
void log_format(const char *fmt, ...);
void log_flush(void);
bool log_empty(void);
void byte_queue_init(ByteQueue *q);
void byte_queue_free(ByteQueue *q);
size_t byte_queue_size(ByteQueue *q);
bool byte_queue_ensure_min_free_space(ByteQueue *q, size_t num);
string byte_queue_start_write(ByteQueue *q);
void byte_queue_end_write(ByteQueue *q, size_t num);
string byte_queue_start_read(ByteQueue *q);
void byte_queue_end_read(ByteQueue *q, size_t num);
bool byte_queue_write(ByteQueue *q, string src);
void byte_queue_patch(ByteQueue *q, size_t offset, char *src, size_t len);
sqlite3_stmt *sqlite3_utils_prepare(sqlite3 *handle, const char *fmt, ...);
int sqlite3_utils_rows_exist(sqlite3 *handle, const char *fmt, ...);
bool sqlite3_utils_exec(sqlite3 *handle, const char *fmt, ...);
int sqlite3_utils_fetch(sqlite3_stmt *stmt, char *types, ...);
#if PROFILE
void timing_init(void);
void timing_result(int scope_index, uint64_t delta_cycles, string label);
void timing_print_results(void);
#endif
#if HTTPS
bool load_private_key_from_file(string file, PrivateKey *pkey);
void free_private_key(PrivateKey *pkey);
bool load_certs_from_file(string file, CertArray *array);
void free_certs(CertArray *array);
BearSSLErrorInfo get_bearssl_error_info(int code);
#endif
char to_lower(char c);
bool is_print(char c);
bool is_pcomp(char c);
bool is_digit(char c);
bool is_alpha(char c);
bool is_space(char c);
string trim(string s);
string substr(string str, size_t start, size_t end);
bool streq(string s1, string s2);
bool string_match_case_insensitive(string x, string y);
bool endswith(string suffix, string name);
bool startswith(string prefix, string str);
void print_bytes(string prefix, string str);
void *mymalloc(size_t num);
void myfree(void *ptr, size_t num);
uint64_t get_real_time_ms(void);
uint64_t get_monotonic_time_ms(void);
uint64_t get_monotonic_time_ns(void);
bool load_file_contents(string file, string *out);
bool set_blocking(int fd, bool blocking);
bool write_string_to_stderr(string s);
bool write_format_to_stderr(const char *fmt, ...);
bool write_format_to_stderr_va(const char *fmt, va_list args);
bool read_from_socket(int fd, ByteQueue *queue);
bool write_to_socket(int fd, ByteQueue *queue);
int create_listening_socket(string addr, int port);
void status_line(ResponseBuilder *b, int status);
void add_header(ResponseBuilder *b, string header);
void add_header_f(ResponseBuilder *b, const char *fmt, ...);
void append_content_s(ResponseBuilder *b, string str);
void append_content_f(ResponseBuilder *b, const char *fmt, ...);
string append_content_start(ResponseBuilder *b, size_t cap);
void append_content_end(ResponseBuilder *b, size_t num);
bool append_file(ResponseBuilder *b, string file);
bool append_template(ResponseBuilder *b, string file, TemplateParam *params);
bool serve_file_or_dir(ResponseBuilder *b, string prefix, string docroot, string reqpath, string mime, bool enable_dir_listing);
int match_path_format(string path, char *fmt, ...);
bool get_query_string_param(string str, string key, string dst, string *out);
bool get_cookie(Request *request, string name, string *out);
SessionID create_session(string name);
void remove_session(SessionID id);
string name_from_session(SessionID id);
SessionID session_from_request(Request request);
///////////////////////////////////////////////////////////////////////////////////////////////
/// GLOBALS ///
///////////////////////////////////////////////////////////////////////////////////////////////
volatile sig_atomic_t stop = 0;
Connection *conns;
int num_conns = 0;
int max_conns = 0;
struct pollfd *pollarray;
uint64_t now;
uint64_t real_now;
int insecure_fd;
int secure_fd;
sqlite3 *db;
char schema[] =
"CREATE TABLE IF NOT EXISTS Users(\n"
" name TEXT PRIMARY KEY,\n"
" pass TEXT NOT NULL,\n"
" bio TEXT\n"
");\n"
"CREATE TABLE IF NOT EXISTS Posts(\n"
" id INTEGER PRIMARY KEY,\n"
" title TEXT NOT NULL,\n"
" content TEXT NOT NULL,\n"
" author TEXT,\n"
" FOREIGN KEY (author) REFERENCES Users(name)\n"
");\n"
"PRAGMA foreign_keys = ON;\n";
Session sessions[MAX_SESSIONS];
SessionID next_session_id = 1;
bool show_io;
bool show_requests;
bool access_log;
int keep_alive_max_requests;
int connection_timeout_sec;
int closing_timeout_sec;
int request_timeout_sec;
int log_flush_timeout_sec;
#if HTTPS
PrivateKey pkey;
CertArray certs;
#endif
///////////////////////////////////////////////////////////////////////////////////////////////
/// IMPLEMENTATION ///
///////////////////////////////////////////////////////////////////////////////////////////////
#if BACKTRACE
void crash_signal_handler(int signo)
{
string signame;
switch (signo) {
case SIGSEGV: signame = LIT("Segmentation fault"); break;
case SIGABRT: signame = LIT("Aborted"); break;
case SIGFPE: signame = LIT("Floating-point exception"); break;
case SIGILL: signame = LIT("Illegal instruction"); break;
default: signame = LIT("Unknown signal"); break;
}
void *stack_buf[BACKTRACE_LIMIT];
int num_stack = backtrace(stack_buf, COUNTOF(stack_buf));
char buffer[4096];
int used = snprintf(buffer, sizeof(buffer), "\n%.*s\nStack trace:\n", (int) signame.size, signame.data);
for (int i = 0; i < num_stack; i++) {
int n;
Dl_info info;
if (dladdr(stack_buf[i], &info) && info.dli_sname) {
n = snprintf(buffer + used, sizeof(buffer) - used, " #%d: %s (%p, %s)\n", i, info.dli_sname, info.dli_fbase, info.dli_fname);
} else {
n = snprintf(buffer + used, sizeof(buffer) - used, " #%d: ??? (%p)\n", i, stack_buf[i]);
}
used = MIN(COUNTOF(buffer)-1, used + n);
}
int fd = open(BACKTRACE_FILE, O_WRONLY | O_APPEND | O_CREAT, 0644);
if (fd < 0) return;
int cpy = 0;
while (cpy < used) {
int n = write(fd, buffer + cpy, used - cpy);
if (n < 0) return;
cpy += n;
}
close(fd);
signal(signo, SIG_DFL);
raise(signo);
}
#endif
void termination_signal_handler(int signo)
{
(void) signo;
stop = 1;
}
void init_globals(int argc, char **argv)
{
atexit(log_free);
string config_file = argc > 1 ? STR(argv[1]) : LIT("config.txt");
if (!config_load(config_file))
exit(-1);
access_log = config_bool(LIT("access_log"));
show_io = config_bool(LIT("show_io"));
show_requests = config_bool(LIT("show_requests"));
keep_alive_max_requests = config_int(LIT("keep_alive_max_requests"));
connection_timeout_sec = config_int(LIT("connection_timeout_sec"));
closing_timeout_sec = config_int(LIT("closing_timeout_sec"));
request_timeout_sec = config_int(LIT("request_timeout_sec"));
log_flush_timeout_sec = config_int(LIT("log_flush_timeout_sec"));
// Setup signal handlers
{
#if BACKTRACE
struct sigaction sa_crash;
sa_crash.sa_handler = crash_signal_handler;
sigemptyset(&sa_crash.sa_mask);
sa_crash.sa_flags = SA_RESTART | SA_NODEFER;
sigaction(SIGSEGV, &sa_crash, NULL);
sigaction(SIGABRT, &sa_crash, NULL);
sigaction(SIGFPE, &sa_crash, NULL);
sigaction(SIGILL, &sa_crash, NULL);
#endif
struct sigaction sa_term;
sa_term.sa_handler = termination_signal_handler;
sigemptyset(&sa_term.sa_mask);
sa_term.sa_flags = SA_RESTART;
sigaction(SIGTERM, &sa_term, NULL);
sigaction(SIGQUIT, &sa_term, NULL);
sigaction(SIGINT, &sa_term, NULL);
DEBUG("Signals configured\n");
}
// Setup logging
{
log_init(
config_string(LIT("log_dir_path")),
config_int(LIT("log_dir_limit_mb")),
config_int(LIT("log_file_limit_b")),
config_int(LIT("log_buff_size_b"))
);
log_data(LIT("starting\n"));
DEBUG("Logger configured\n");
}
#if PROFILE
timing_init();
#endif
// Setup connection arrays
{
struct rlimit file_desc_limit;
if (getrlimit(RLIMIT_NOFILE, &file_desc_limit))
log_fatal(LIT("Couldn't query RLIMIT_NOFILE\n"));
max_conns = config_int(LIT("max_connections"));
num_conns = 0;
if ((size_t) max_conns+2 > file_desc_limit.rlim_cur)
log_fatal(LIT("max_connections+2 is higher than the rlimit\n"));
conns = mymalloc(max_conns * sizeof(Connection));
if (conns == NULL)
log_fatal(LIT("Out of memory"));
for (int i = 0; i < max_conns; i++) {
conns[i].fd = -1;
byte_queue_init(&conns[i].input);
byte_queue_init(&conns[i].output);
}
pollarray = mymalloc((max_conns+2) * sizeof(struct pollfd));
if (pollarray == NULL)
log_fatal(LIT("Out of memory"));
DEBUG("Connection array created\n");
}
// Create plain text listener
{
string http_addr = config_string(LIT("http_addr"));
uint32_t http_port = config_int(LIT("http_port"));
insecure_fd = create_listening_socket(http_addr, http_port);
if (insecure_fd < 0)
log_fatal(LIT("Couldn't bind\n"));
log_format("Listening on %.*s:%d\n", (int) http_addr.size, http_addr.data, http_port);
DEBUG("HTTP started\n");
}
// Create secure listener
{
secure_fd = -1;
#if HTTPS
string https_addr = config_string(LIT("https_addr"));
uint32_t https_port = config_int(LIT("https_port"));
string https_cert_file = config_string(LIT("cert_file"));
string https_key_file = config_string(LIT("privkey_file"));
secure_fd = create_listening_socket(https_addr, https_port);
if (secure_fd < 0)
log_fatal(LIT("Couldn't bind\n"));
log_format("Listening on %.*s:%d\n", (int) https_addr.size, https_addr.data, https_port);
// Load certificate
if (!load_certs_from_file(https_cert_file, &certs))
log_fatal(LIT("Couldn't load certificates\n"));
DEBUG("Certificates loaded\n");
// Load private key
if (!load_private_key_from_file(https_key_file, &pkey))
log_fatal(LIT("Couldn't load private key\n"));
DEBUG("Private key loaded\n");
DEBUG("HTTPS started\n");
#endif
}
int code = sqlite3_open("file.db", &db);
if (code != SQLITE_OK) {
log_fatal(LIT("Couldn't open the database\n"));
sqlite3_close(db);
return;
}
{
char *errmsg;
int code = sqlite3_exec(db, schema, NULL, NULL, &errmsg);
if (code != SQLITE_OK) {
log_format("Couldn't apply database schema (%s)\n", errmsg);
sqlite3_free(errmsg);
sqlite3_close(db);
exit(-1);
return;
}
}
for (int i = 0; i < MAX_SESSIONS; i++)
sessions[i].id = NO_SESSION;
config_free();
}
void free_globals(void)
{
sqlite3_close(db);
#if PROFILE
timing_print_results();
#endif
#if HTTPS
free_private_key(&pkey);
free_certs(&certs);
close(secure_fd);
#endif
close(insecure_fd);
for (int i = 0; i < max_conns; i++) {
if (conns[i].fd != -1) {
close(conns[i].fd);
byte_queue_free(&conns[i].input);
byte_queue_free(&conns[i].output);
}
}
myfree(conns, max_conns * sizeof(Connection));
myfree(pollarray, (max_conns+2) * sizeof(struct pollfd));
log_data(LIT("closing\n"));
log_free();
}
///////////////////////////////////////////////////////////////////////////////////////////////
/// RESPONSE CALLBACK ///
///////////////////////////////////////////////////////////////////////////////////////////////
void respond(Request request, ResponseBuilder *b)
{
if (request.major != 1 || request.minor > 1) {
status_line(b, 505); // HTTP Version Not Supported
return;
}
SessionID sessid = session_from_request(request);
string login_username = (sessid == NO_SESSION ? NULLSTR : name_from_session(sessid));
if (streq(request.url.path, LIT("/")))
request.url.path = LIT("/posts");
if (streq(request.url.path, LIT("/action/login"))) {
if (login_username.size > 0) {
status_line(b, 303);
add_header(b, LIT("Location: /"));
return;
}
char namebuf[MAX_USER_NAME];
char passbuf[MAX_USER_PASS];
string name; string pass;
if (!get_query_string_param(request.content, LIT("name"), LIT(namebuf), &name)) {
status_line(b, 500);
append_content_s(b, LIT("Invalid name"));
return;
}
if (!get_query_string_param(request.content, LIT("pass"), LIT(passbuf), &pass)) {
status_line(b, 500);
append_content_s(b, LIT("Invalid pass"));
return;
}
int res = sqlite3_utils_rows_exist(db, "SELECT name FROM Users WHERE name=:s AND pass=:s", name, pass);
if (res == -1) {
status_line(b, 500);
append_content_s(b, LIT("Internal error"));
return;
}
if (res == 1) {
// No such user
status_line(b, 400);
append_content_s(b, LIT("Invalid credentials"));
return;
}
SessionID sessid = create_session(name);
if (sessid == NO_SESSION) {
status_line(b, 500);
append_content_s(b, LIT("Internal error"));
return;
}
// User exist
status_line(b, 303);
add_header_f(b, "Set-Cookie: sessid=%d; Path=/", sessid);
add_header(b, LIT("Location: /"));
return;
}
if (streq(request.url.path, LIT("/action/signup"))) {
if (login_username.size > 0) {
status_line(b, 303);
add_header(b, LIT("Location: /"));
return;
}
char namebuf[MAX_USER_NAME];
char passbuf[MAX_USER_PASS];
char biobuf[MAX_USER_BIO];
string name;
string pass;
string bio;
if (!get_query_string_param(request.content, LIT("name"), LIT(namebuf), &name)) {
status_line(b, 400);
append_content_s(b, LIT("Invalid name"));
return;
}
if (!get_query_string_param(request.content, LIT("pass"), LIT(passbuf), &pass)) {
status_line(b, 400);
append_content_s(b, LIT("Invalid pass"));
return;
}
if (!get_query_string_param(request.content, LIT("bio"), LIT(biobuf), &bio)) {
status_line(b, 400);
append_content_s(b, LIT("Invalid bio"));
return;
}
name = trim(name);
pass = trim(pass);
bio = trim(bio);
if (name.size == 0 || pass.size == 0 || pass.size == 0) {
status_line(b, 400);
append_content_s(b, LIT("One or more fields are empty"));
return;
}
if (!sqlite3_utils_exec(db, "INSERT INTO Users(name, pass, bio) VALUES (:s, :s, :s)", name, pass, bio)) {
status_line(b, 500);
append_content_s(b, LIT("Internal error"));
return;
}
SessionID sessid = create_session(name);
if (sessid == NO_SESSION) {
status_line(b, 500);
append_content_s(b, LIT("Internal error"));
return;
}
status_line(b, 303);
add_header_f(b, "Set-Cookie: sessid=%d; Path=/", sessid);
add_header(b, LIT("Location: /"));
return;
}
if (streq(request.url.path, LIT("/action/logout"))) {
if (login_username.size > 0)
remove_session(sessid);
status_line(b, 303);
add_header(b, LIT("Location: /login"));
return;
}
if (streq(request.url.path, LIT("/action/post"))) {
if (login_username.size == 0) {
status_line(b, 400);
append_content_s(b, LIT("Not logged in"));
return;
}
char titlebuf[MAX_POST_TITLE];
char contentbuf[MAX_POST_CONTENT];
string title;
string content;
if (!get_query_string_param(request.content, LIT("title"), LIT(titlebuf), &title)) {
status_line(b, 400);
append_content_s(b, LIT("Invalid title"));
return;
}
if (!get_query_string_param(request.content, LIT("content"), LIT(contentbuf), &content)) {
status_line(b, 400);
append_content_s(b, LIT("Invalid content"));
return;
}
title = trim(title);
content = trim(content);
if (title.size == 0 || content.size == 0) {
status_line(b, 400);
append_content_s(b, LIT("One or more fields are empty"));
return;
}
if (!sqlite3_utils_exec(db, "INSERT INTO Posts(title, content, author) VALUES (:s, :s, :s)", title, content, login_username)) {
status_line(b, 500);
append_content_s(b, LIT("Internal error"));
return;
}
int64_t post_id = sqlite3_last_insert_rowid(db);
status_line(b, 303);
add_header_f(b, "Location: /posts/%d", post_id);
return;
}
if (!match_path_format(request.url.path, "/posts")) {
sqlite3_stmt *stmt = sqlite3_utils_prepare(db, "SELECT id, title, author FROM Posts");
if (stmt == NULL) {
status_line(b, 500);
return;
}
status_line(b, 200);
add_header(b, LIT("Content-Type: text/html"));
TemplateParam params[] = {
{.name=LIT("login"), .type=TPT_INT, .i=login_username.size>0},
{.name=LIT("login_username"), .type=TPT_STRING, .s=login_username},
{.name=LIT("posts"), .type=TPT_QUERY, .q=stmt},
{.name=NULLSTR, .type=TPT_LAST }
};
append_template(b, LIT("pages/posts.html"), params);
sqlite3_finalize(stmt);
return;
}
int post_id;
if (!match_path_format(request.url.path, "/posts/:n", &post_id)) {
sqlite3_stmt *stmt = sqlite3_utils_prepare(db, "SELECT title, content, author FROM Posts WHERE id=:i", post_id);
if (stmt == NULL) {
status_line(b, 500);
append_content_s(b, LIT("Internal error"));
return;
}
string title;
string content;
string author;
int res = sqlite3_utils_fetch(stmt, "sss", &title, &content, &author);
if (res == -1) {
status_line(b, 500);
append_content_s(b, LIT("Internal error"));
return;
}
if (res == 1) {
status_line(b, 404);
append_content_s(b, LIT("No such post"));
return;
}
assert(res == 0);
status_line(b, 200);
add_header(b, LIT("Content-Type: text/html"));
TemplateParam params[] = {
{.name=LIT("login"), .type=TPT_INT, .i=login_username.size>0},
{.name=LIT("login_username"), .type=TPT_STRING, .s=login_username},
{.name=LIT("title"), .type=TPT_STRING, .s=title},
{.name=LIT("author"), .type=TPT_STRING, .s=author},
{.name=LIT("content"), .type=TPT_STRING, .s=content},
{.name=NULLSTR, .type=TPT_LAST }
};
append_template(b, LIT("pages/post.html"), params);
sqlite3_finalize(stmt);
return;
}
if (!match_path_format(request.url.path, "/users")) {
sqlite3_stmt *stmt = sqlite3_utils_prepare(db, "SELECT name FROM Users");
if (stmt == NULL) {
status_line(b, 500);
return;
}
status_line(b, 200);
add_header(b, LIT("Content-Type: text/html"));
TemplateParam params[] = {
{.name=LIT("login"), .type=TPT_INT, .i=login_username.size>0},
{.name=LIT("login_username"), .type=TPT_STRING, .s=login_username},
{.name=LIT("users"), .type=TPT_QUERY, .q=stmt},
{.name=NULLSTR, .type=TPT_LAST }
};
append_template(b, LIT("pages/users.html"), params);
sqlite3_finalize(stmt);
return;
}
string profile_username;
if (!match_path_format(request.url.path, "/users/:s", &profile_username)) {
sqlite3_stmt *stmt = sqlite3_utils_prepare(db, "SELECT bio FROM Users WHERE name=:s", profile_username);
if (stmt == NULL) {
status_line(b, 500);
append_content_s(b, LIT("Internal error"));
return;
}
string bio;
int res = sqlite3_utils_fetch(stmt, "s", &bio);
if (res == -1) {
status_line(b, 500);
append_content_s(b, LIT("Internal error"));
return;
}
if (res == 1) {
status_line(b, 404);
append_content_s(b, LIT("No such user"));
return;
}
assert(res == 0);
status_line(b, 200);
add_header(b, LIT("Content-Type: text/html"));
TemplateParam params[] = {
{.name=LIT("login"), .type=TPT_INT, .i=login_username.size>0},
{.name=LIT("login_username"), .type=TPT_STRING, .s=login_username},
{.name=LIT("name"), .type=TPT_STRING, .s=profile_username},
{.name=LIT("bio"), .type=TPT_STRING, .s=bio},
{.name=NULLSTR, .type=TPT_LAST }
};
append_template(b, LIT("pages/user.html"), params);
sqlite3_finalize(stmt);
return;
}
if (!match_path_format(request.url.path, "/login")) {
if (login_username.size > 0) {
// Already logged in
status_line(b, 303);
add_header(b, LIT("Location: /home"));
return;
}
status_line(b, 200);
append_file(b, LIT("pages/login.html"));
return;
}
if (!match_path_format(request.url.path, "/signup")) {
if (login_username.size > 0) {
// Already logged in
status_line(b, 303);
add_header(b, LIT("Location: /home"));
return;
}
status_line(b, 200);
append_file(b, LIT("pages/signup.html"));
return;
}
if (!match_path_format(request.url.path, "/home")) {
status_line(b, 200);
append_file(b, LIT("pages/home.html"));
return;
}
if (serve_file_or_dir(b, LIT("/static"), LIT("static/"), request.url.path, NULLSTR, false))
return;
status_line(b, 404);
append_content_s(b, LIT("Nothing here :|"));
}
///////////////////////////////////////////////////////////////////////////////////////////////
/// SESSIONS ///
///////////////////////////////////////////////////////////////////////////////////////////////
SessionID create_session(string name)
{
int i = 0;
while (i < MAX_SESSIONS && sessions[i].id != NO_SESSION)
i++;
if (i == MAX_SESSIONS)
return NO_SESSION;
if (next_session_id == NO_SESSION)
next_session_id++;
SessionID id = next_session_id++;
if (name.size > sizeof(sessions[i].namebuf))
log_fatal(LIT("User name buffer is too small"));
memcpy(sessions[i].namebuf, name.data, name.size);
sessions[i].id = id;
sessions[i].name = (string) { sessions[i].namebuf, name.size };
return sessions[i].id;
}
void remove_session(SessionID id)
{
assert(id != NO_SESSION);
int i = 0;
while (i < MAX_SESSIONS && sessions[i].id != id)
i++;
if (i == MAX_SESSIONS)
log_fatal(LIT("Trying to remove non existing session"));
sessions[i].id = NO_SESSION;
sessions[i].name = NULLSTR;
memset(sessions[i].namebuf, 0, sizeof(sessions[i].namebuf));
}
string name_from_session(SessionID id)
{
assert(id != NO_SESSION);
for (int i = 0; i < MAX_SESSIONS; i++)
if (sessions[i].id == id)
return sessions[i].name;
return NULLSTR;
}
SessionID session_from_request(Request request)
{
string sessid_str;
if (!get_cookie(&request, LIT("sessid"), &sessid_str))
return NO_SESSION;
SessionID id;
{
char *src = sessid_str.data;
size_t len = sessid_str.size;
size_t i = 0;
while (i < len && is_space(src[i]))
i++;
if (i == len || !is_digit(src[i]))
return NO_SESSION;
uint32_t buf = 0;
do {
int d = src[i] - '0';
if (buf > (UINT32_MAX - d) / 10)
return NO_SESSION;
buf = buf * 10 + d;
i++;
} while (i < len && is_digit(src[i]));
while (i < len && is_space(src[i]))
i++;
if (i < len)
return NO_SESSION;
assert(sizeof(buf) == sizeof(SessionID));
assert(buf != 0 && buf != NO_SESSION);
id = buf;
}
return id;
}
///////////////////////////////////////////////////////////////////////////////////////////////
/// REQUEST PARSER ///
///////////////////////////////////////////////////////////////////////////////////////////////
// TODO: Make sure every string in request is reasonaly long
int parse_request_head(string str, Request *request)
{
char *src = str.data;
size_t len = str.size;
size_t cur;
if (len > 2
&& src[0] == 'G'
&& src[1] == 'E'
&& src[2] == 'T') {
request->method = M_GET;
cur = 3;
} else if (len > 3
&& src[0] == 'H'
&& src[1] == 'E'
&& src[2] == 'A'
&& src[3] == 'D') {
request->method = M_HEAD;
cur = 4;
} else if (len > 3
&& src[0] == 'P'
&& src[1] == 'O'
&& src[2] == 'S'
&& src[3] == 'T') {
request->method = M_POST;
cur = 4;
} else if (len > 2
&& src[0] == 'P'
&& src[1] == 'U'
&& src[2] == 'T') {
request->method = M_PUT;
cur = 3;
} else if (len > 5
&& src[0] == 'D'
&& src[1] == 'E'
&& src[2] == 'L'
&& src[3] == 'E'
&& src[4] == 'T'
&& src[5] == 'E') {
request->method = M_DELETE;
cur = 6;
} else if (len > 6
&& src[0] == 'C'
&& src[1] == 'O'
&& src[2] == 'N'
&& src[3] == 'N'
&& src[4] == 'E'
&& src[5] == 'C'
&& src[6] == 'T') {
request->method = M_CONNECT;
cur = 7;
} else if (len > 6
&& src[0] == 'O'
&& src[1] == 'P'
&& src[2] == 'T'
&& src[3] == 'I'
&& src[4] == 'O'
&& src[5] == 'N'
&& src[6] == 'S') {
request->method = M_OPTIONS;
cur = 7;
} else if (len > 4
&& src[0] == 'T'
&& src[1] == 'R'
&& src[2] == 'A'
&& src[3] == 'C'
&& src[4] == 'E') {
request->method = M_TRACE;
cur = 5;
} else if (len > 4
&& src[0] == 'P'
&& src[1] == 'A'
&& src[2] == 'T'
&& src[3] == 'C'
&& src[4] == 'H') {
request->method = M_PATCH;
cur = 5;
} else {
return P_BADMETHOD;
}
if (cur == len || src[cur] != ' ')
return P_INCOMPLETE;
cur++;
if (!url_parse2(str, &cur, &request->url))
return P_BADURL;
if (cur == len || src[cur] != ' ')
return P_INCOMPLETE;
cur++;
if (cur+4 >= len
|| src[cur+0] != 'H'
|| src[cur+1] != 'T'
|| src[cur+2] != 'T'
|| src[cur+3] != 'P'
|| src[cur+4] != '/'
|| !is_digit(src[cur+5]))
return P_BADVERSION;
cur += 5;
request->major = src[cur] - '0';
cur++;
if (cur < len && src[cur] == '.') {
cur++;
if (cur == len || !is_digit(src[cur]))
return P_BADVERSION;
request->minor = src[cur] - '0';
cur++;
} else {
request->minor = 0;
}
if (cur+1 >= len
|| src[cur+0] != '\r'
|| src[cur+1] != '\n')
return P_INCOMPLETE;
cur += 2;
request->nheaders = 0;
while (cur+1 >= len
|| src[cur+0] != '\r'
|| src[cur+1] != '\n') {
string name;
string value;
size_t start = cur;
// TODO: More robust
while (cur < len && src[cur] != ':')
cur++;
name.data = src + start;
name.size = cur - start;
if (cur == len)
return P_BADHEADER;
cur++; // :
// TODO: More robust
start = cur;
while (cur < len && src[cur] != '\r')
cur++;
value.data = src + start;
value.size = cur - start;
cur++; // \r
if (cur == len || src[cur] != '\n')
return P_BADHEADER;
cur++; // \n
if (request->nheaders < MAX_HEADERS) {
request->headers[request->nheaders].name = name;
request->headers[request->nheaders].value = value;
request->nheaders++;
}
}
// cur here points to the \r in \r\n
request->content = NULLSTR;
return P_OK;
}
bool find_header(Request *request, string name, string *value)
{
for (int i = 0; i < request->nheaders; i++)
if (string_match_case_insensitive(request->headers[i].name, name)) {
*value = request->headers[i].value;
return true;
}
return false;
}
string get_status_string(int status)
{
switch(status)
{
case 100: return LIT("Continue");
case 101: return LIT("Switching Protocols");
case 102: return LIT("Processing");
case 200: return LIT("OK");
case 201: return LIT("Created");
case 202: return LIT("Accepted");
case 203: return LIT("Non-Authoritative Information");
case 204: return LIT("No Content");
case 205: return LIT("Reset Content");
case 206: return LIT("Partial Content");
case 207: return LIT("Multi-Status");
case 208: return LIT("Already Reported");
case 300: return LIT("Multiple Choices");
case 301: return LIT("Moved Permanently");
case 302: return LIT("Found");
case 303: return LIT("See Other");
case 304: return LIT("Not Modified");
case 305: return LIT("Use Proxy");
case 306: return LIT("Switch Proxy");
case 307: return LIT("Temporary Redirect");
case 308: return LIT("Permanent Redirect");
case 400: return LIT("Bad Request");
case 401: return LIT("Unauthorized");
case 402: return LIT("Payment Required");
case 403: return LIT("Forbidden");
case 404: return LIT("Not Found");
case 405: return LIT("Method Not Allowed");
case 406: return LIT("Not Acceptable");
case 407: return LIT("Proxy Authentication Required");
case 408: return LIT("Request Timeout");
case 409: return LIT("Conflict");
case 410: return LIT("Gone");
case 411: return LIT("Length Required");
case 412: return LIT("Precondition Failed");
case 413: return LIT("Request Entity Too Large");
case 414: return LIT("Request-URI Too Long");
case 415: return LIT("Unsupported Media Type");
case 416: return LIT("Requested Range Not Satisfiable");
case 417: return LIT("Expectation Failed");
case 418: return LIT("I'm a teapot");
case 420: return LIT("Enhance your calm");
case 422: return LIT("Unprocessable Entity");
case 426: return LIT("Upgrade Required");
case 429: return LIT("Too many requests");
case 431: return LIT("Request Header Fields Too Large");
case 449: return LIT("Retry With");
case 451: return LIT("Unavailable For Legal Reasons");
case 500: return LIT("Internal Server Error");
case 501: return LIT("Not Implemented");
case 502: return LIT("Bad Gateway");
case 503: return LIT("Service Unavailable");
case 504: return LIT("Gateway Timeout");
case 505: return LIT("HTTP Version Not Supported");
case 509: return LIT("Bandwidth Limit Exceeded");
}
return LIT("???");
}
size_t parse_content_length(string s)
{
char *src = s.data;
size_t len = s.size;
size_t cur = 0;
while (cur < len && is_space(src[cur]))
cur++;
if (cur == len || !is_digit(src[cur]))
return -1;
size_t x = 0;
do {
int d = src[cur] - '0';
if (x > (SIZE_MAX - d) / 10)
return -1;
x = x * 10 + d;
cur++;
} while (cur < len && is_digit(src[cur]));
while (cur < len && is_space(src[cur]))
cur++;
if (cur != len)
return -1;
return x;
}
int find_and_parse_transfer_encoding(Request *request)
{
string value;
if (!find_header(request, LIT("Transfer-Encoding"), &value))
return 0;
int res = 0;
char *src = value.data;
size_t len = value.size;
size_t cur = 0;
for (;;) {
while (cur < len && (is_space(src[cur]) || src[cur] == ','))
cur++;
if (cur+6 < len
&& src[cur+0] == 'c'
&& src[cur+1] == 'h'
&& src[cur+2] == 'u'
&& src[cur+3] == 'n'
&& src[cur+4] == 'k'
&& src[cur+5] == 'e'
&& src[cur+6] == 'd') {
cur += 7;
res |= T_CHUNKED;
} else if (cur+7 < len
&& src[cur+0] == 'c'
&& src[cur+1] == 'o'
&& src[cur+2] == 'm'
&& src[cur+3] == 'p'
&& src[cur+4] == 'r'
&& src[cur+5] == 'e'
&& src[cur+6] == 's'
&& src[cur+7] == 's') {
cur += 8;
res |= T_COMPRESS;
} else if (cur+6 < len
&& src[cur+0] == 'd'
&& src[cur+1] == 'e'
&& src[cur+2] == 'f'
&& src[cur+3] == 'l'
&& src[cur+4] == 'a'
&& src[cur+5] == 't'
&& src[cur+6] == 'e') {
cur += 7;
res |= T_DEFLATE;
} else if (cur+3 < len
&& src[cur+0] == 'g'
&& src[cur+1] == 'z'
&& src[cur+2] == 'i'
&& src[cur+3] == 'p') {
cur += 4;
res |= T_GZIP;
} else {
return -1;
}
}
return res;
}
void response_builder_init(ResponseBuilder *b, Connection *conn)
{
b->state = R_STATUS;
b->conn = conn;
b->failed = false;
b->keep_alive = true;
b->content_length_offset = -1;
b->content_offset = -1;
}
void status_line(ResponseBuilder *b, int status)
{
if (b->state != R_STATUS)
log_fatal(LIT("Appending status line twice\n"));
if (!b->failed) {
char buf[1<<10];
string status_string = get_status_string(status);
int num = snprintf(buf, sizeof(buf), "HTTP/1.1 %d %.*s\r\n", status, (int) status_string.size, status_string.data);
assert(num > 0 && num < SIZEOF(buf));
if (!byte_queue_write(&b->conn->output, (string) {buf, num}))
b->failed = true;
}
b->state = R_HEADER;
}
void add_header(ResponseBuilder *b, string header)
{
if (b->state != R_HEADER) {
if (b->state == R_STATUS)
log_fatal(LIT("Didn't write status line before headers\n"));
else
log_fatal(LIT("Can't add headers after content\n"));
}
if (b->failed)
return;
if (!byte_queue_write(&b->conn->output, header) ||
!byte_queue_write(&b->conn->output, LIT("\r\n"))) {
b->failed = true;
return;
}
}
void add_header_f(ResponseBuilder *b, const char *fmt, ...)
{
char buffer[1<<10];
va_list args;
va_start(args, fmt);
int num = vsnprintf(buffer, sizeof(buffer), fmt, args);
va_end(args);
if (num < 0 || num >= (int) sizeof(buffer)) {
b->failed = true;
return;
}
buffer[num] = '\0';
add_header(b, (string) {buffer, num});
}
bool should_keep_alive(Connection *conn);
void append_special_headers(ResponseBuilder *b)
{
if (should_keep_alive(b->conn))
add_header(b, LIT("Connection: Keep-Alive"));
else {
add_header(b, LIT("Connection: Close"));
b->conn->closing = true;
b->conn->start_time = now;
}
b->content_length_offset = byte_queue_size(&b->conn->output) + sizeof("Content-Length: ") - 1;
add_header(b, LIT("Content-Length: "));
if (!byte_queue_write(&b->conn->output, LIT("\r\n")))
b->failed = true;
b->content_offset = byte_queue_size(&b->conn->output);
}
void append_content_s(ResponseBuilder *b, string str)
{
if (b->state == R_HEADER) {
append_special_headers(b);
b->state = R_CONTENT;
}
if (b->state != R_CONTENT)
log_fatal(LIT("Invalid response builder state\n"));
if (b->failed)
return;
if (!byte_queue_write(&b->conn->output, str)) {
b->failed = true;
return;
}
}
string append_content_start(ResponseBuilder *b, size_t cap)
{
if (b->state == R_HEADER) {
append_special_headers(b);
b->state = R_CONTENT;
}
if (b->state != R_CONTENT)
log_fatal(LIT("Invalid response builder state\n"));
if (b->failed)
return NULLSTR;
if (!byte_queue_ensure_min_free_space(&b->conn->output, cap)) {
b->failed = true;
return NULLSTR;
}
return byte_queue_start_write(&b->conn->output);
}
void append_content_end(ResponseBuilder *b, size_t num)
{
byte_queue_end_write(&b->conn->output, num);
}
void append_content_f(ResponseBuilder *b, const char *fmt, ...)
{
size_t cap = 128;
for (;;) {
string dst = append_content_start(b, cap);
if (dst.size == 0)
return;
va_list args;
va_start(args, fmt);
int num = vsnprintf(dst.data, dst.size, fmt, args);
assert(num >= 0);
va_end(args);
if ((size_t) num < cap) {
append_content_end(b, num);
break;
}
cap *= 2;
}
}
bool append_file(ResponseBuilder *b, string file)
{
char copy[1<<12];
if (file.size >= sizeof(copy))
return false;
memcpy(copy, file.data, file.size);
copy[file.size] = '\0';
struct stat buf;
if (stat(copy, &buf))
return false;
if (!S_ISREG(buf.st_mode))
return false;
int fd;
do
fd = open(copy, O_RDONLY);
while (fd < 0 && errno == EINTR);
if (fd < 0)
return true;
string dst = append_content_start(b, (size_t) buf.st_size);
if (dst.size == 0) {
close(fd);
return false;
}
assert(dst.size >= (size_t) buf.st_size);
size_t copied = 0;
while (copied < (size_t) buf.st_size) {
int num = read(fd, dst.data + copied, (size_t) buf.st_size - copied);
if (num <= 0) {
if (num < 0)
return false;
break;
}
copied += num;
}
append_content_end(b, copied);
close(fd);
return true;
}
void response_builder_complete(ResponseBuilder *b)
{
if (b->state == R_COMPLETE)
return;
if (b->failed)
return;
if (b->state == R_HEADER) {
append_special_headers(b);
if (b->failed) return;
} else {
if (b->state != R_CONTENT)
log_fatal(LIT("Invalid response builder state\n"));
}
size_t current_offset = byte_queue_size(&b->conn->output);
size_t content_length = current_offset - b->content_offset;
if (content_length > 1<<30) {
// Content larger than 1GB
b->failed = true;
return;
}
int content_length_int = (int) content_length;
char content_length_string[128];
int n = snprintf(content_length_string, sizeof(content_length_string), "%d", content_length_int);
assert(n >= 1 && n <= 9);
byte_queue_patch(&b->conn->output, b->content_length_offset, content_length_string, n);
b->state = R_COMPLETE;
}
bool should_keep_alive(Connection *conn)
{
// Don't keep alive if the peer doesn't want to
if (conn->keep_alive == false)
return false;
// Don't keep alive if the request is too old
if (now - conn->creation_time > (uint64_t) connection_timeout_sec * 1000)
return false;
// Don't keep alive if we served a lot of requests to this connection
if (conn->served_count > keep_alive_max_requests)
return false;
// Don't keep alive if the server is more than 70% full
if (num_conns > 0.7 * max_conns)
return false;
return true;
}
uint64_t deadline_of(Connection *conn)
{
return conn->start_time + (conn->closing ? closing_timeout_sec : request_timeout_sec) * 1000;
}
bool respond_to_available_requests(Connection *conn)
{
bool remove = false;
int pipeline_count = 0;
while (!remove) { /* Respond loop start */
string src = byte_queue_start_read(&conn->input);
// Look for the \r\n\r\n
size_t j = 0;
while (j+3 < src.size && (src.data[j] != '\r' || src.data[j+1] != '\n' || src.data[j+2] != '\r' || src.data[j+3] != '\n'))
j++;
if (j+3 >= src.size)
break; // No \r\n\r\n
size_t head_length = j+4;
if (show_requests) {
print_bytes(LIT(""), (string) {src.data, head_length});
log_data(LIT("\n"));
}
// Found! We got the request head
Request request;
int res = parse_request_head((string) {src.data, head_length}, &request);
if (access_log) TIME("log_access") {
// Log access
time_t real_now_in_secs = real_now / 1000;
struct tm timeinfo;
localtime_r(&real_now_in_secs, &timeinfo);
char timebuf[128];
size_t timelen = strftime(timebuf, sizeof(timebuf), "%Y/%m/%d %H:%M:%S", &timeinfo);
if (timelen == 0)
log_fatal(LIT("Couldn't format time for access log"));
timebuf[timelen] = '\0';
char ipbuf[INET_ADDRSTRLEN];
const char *ipstr = inet_ntop(AF_INET, &conn->ipaddr, ipbuf, sizeof(ipbuf));
if (ipstr == NULL)
log_fatal(LIT("Couldn't format IP address for access log"));
if (res == P_OK) {
string user_agent;
if (!find_header(&request, LIT("User-Agent"), &user_agent))
user_agent = LIT("No User-Agent");
else
user_agent = trim(user_agent);
log_format("%s - %s - %.*s - %.*s\n", timebuf, ipstr,
(int) request.url.path.size, request.url.path.data,
(int) user_agent.size, user_agent.data);
} else {
log_format("%s - %s - Bad request\n", timebuf, ipstr);
}
}
if (res != P_OK) {
// Invalid HTTP request
byte_queue_write(&conn->output, LIT(
"HTTP/1.1 400 Bad Request\r\n"
"Connection: Close\r\n"
"\r\n"));
conn->closing = true;
conn->start_time = now;
break;
}
string content_length_header;
size_t content_length;
if (!find_header(&request, LIT("Content-Length"), &content_length_header)) {
if (find_and_parse_transfer_encoding(&request) & T_CHUNKED) {
// Content-Length missing
byte_queue_write(&conn->output, LIT(
"HTTP/1.1 411 Length Required\r\n"
"Connection: Close\r\n"
"\r\n"));
conn->closing = true;
conn->start_time = now;
log_data(LIT("Content-Length missing\n"));
break;
} else
content_length = 0;
} else {
content_length = parse_content_length(content_length_header);
if (content_length == (size_t) -1) {
// Invalid Content-Length
byte_queue_write(&conn->output, LIT(
"HTTP/1.1 400 Bad Request\r\n"
"Connection: Close\r\n"
"\r\n"));
conn->closing = true;
conn->start_time = now;
log_data(LIT("Invalid Content-Length\n"));
break;
}
}
if (content_length > 1<<20) {
// Request too large
byte_queue_write(&conn->output, LIT(
"HTTP/1.1 413 Content Too Large\r\n"
"Connection: Close\r\n"
"\r\n"));
conn->closing = true;
conn->start_time = now;
log_data(LIT("Request too large\n"));
break;
}
size_t request_length = head_length + content_length;
if (src.size < request_length)
break; // Request wasn't completely received yet
request.content.data = src.data + head_length;
request.content.size = content_length;
// Reset the request timer
conn->start_time = now;
conn->keep_alive = true;
string keep_alive_header;
if (find_header(&request, LIT("Connection"), &keep_alive_header)) {
if (string_match_case_insensitive(trim(keep_alive_header), LIT("Close")))
conn->keep_alive = false;
}
// Respond
ResponseBuilder builder;
response_builder_init(&builder, conn);
respond(request, &builder);
response_builder_complete(&builder);
if (builder.failed)
remove = true;
else {
conn->served_count++;
byte_queue_end_read(&conn->input, request_length);
if (!conn->keep_alive) {
conn->closing = true;
conn->start_time = now;
}
pipeline_count++;
if (pipeline_count == 10) {
// TODO: We should send a response to the client instead of dropping it
log_data(LIT("Pipeline limit reached\n"));
remove = true;
break;
}
}
}
return remove;
}
void build_poll_array(struct pollfd *pollarray, int *timeout)
{
pollarray[0].fd = insecure_fd;
pollarray[0].events = (num_conns < max_conns ? POLLIN : 0);
pollarray[0].revents = 0;
#if HTTPS
pollarray[1].fd = secure_fd;
pollarray[1].events = (num_conns < max_conns ? POLLIN : 0);
pollarray[1].revents = 0;
#else
pollarray[1].fd = -1;
pollarray[1].events = 0;
pollarray[1].revents = 0;
#endif
Connection *oldest = NULL;
for (int i = 0; i < max_conns; i++) {
Connection *conn = &conns[i];
int events = 0;
if (conn->fd == -1) {
pollarray[i+2].fd = -1;
pollarray[i+2].events = 0;
pollarray[i+2].revents = 0;
continue;
}
if (conn->https) {
#if HTTPS
int state = br_ssl_engine_current_state(&conn->https_context.eng);
if (state & BR_SSL_SENDREC) events |= POLLOUT;
if (state & BR_SSL_RECVREC) events |= POLLIN;
#endif
} else {
if (byte_queue_size(&conn->output) > 0)
events |= POLLOUT;
if (!conn->closing)
events |= POLLIN;
}
pollarray[i+2].fd = conn->fd;
pollarray[i+2].events = events;
pollarray[i+2].revents = 0;
if (oldest == NULL || deadline_of(oldest) > deadline_of(conn)) oldest = conn;
}
if (oldest == NULL)
*timeout = -1;
else {
if (deadline_of(oldest) < now)
*timeout = 0;
else
*timeout = deadline_of(oldest) - now;
}
}
void init_connection(Connection *conn, int fd, uint32_t ipaddr, bool https)
{
byte_queue_init(&conn->input);
byte_queue_init(&conn->output);
conn->fd = fd;
conn->ipaddr = ipaddr;
conn->closing = false;
conn->https = https;
conn->served_count = 0;
conn->creation_time = now;
conn->start_time = now;
#if HTTPS
if (https) {
if (pkey.type == BR_KEYTYPE_RSA)
br_ssl_server_init_full_rsa(&conn->https_context, certs.items, certs.count, &pkey.rsa);
else {
assert(pkey.type == BR_KEYTYPE_EC);
unsigned issuer_key_type = BR_KEYTYPE_RSA; // Not sure if this or BR_KEYTYPE_EC
br_ssl_server_init_full_ec(&conn->https_context, certs.items, certs.count, issuer_key_type, &pkey.ec);
}
br_ssl_engine_set_versions(&conn->https_context.eng, BR_TLS10, BR_TLS12);
br_ssl_engine_set_buffer(&conn->https_context.eng, conn->https_buffer, sizeof(conn->https_buffer), 1);
br_ssl_server_reset(&conn->https_context);
}
#endif
}
void free_connection(Connection *conn)
{
assert(conn->fd != -1);
close(conn->fd);
byte_queue_free(&conn->input);
byte_queue_free(&conn->output);
conn->fd = -1;
conn->start_time = -1;
conn->closing = false;
conn->creation_time = 0;
}
bool accept_connection(int listen_fd, bool https)
{
// Look for a connection structure
int index = 0;
while (index < max_conns && conns[index].fd != -1)
index++;
if (index == max_conns)
return false; // Stop listening for incoming connections
struct sockaddr_in accepted_addr;
socklen_t accepted_addrlen = sizeof(accepted_addr);
int accepted_fd = accept(listen_fd, (struct sockaddr*) &accepted_addr, &accepted_addrlen);
if (accepted_fd < 0) {
if (errno == EINTR)
return true;
if (errno == EAGAIN || errno == EWOULDBLOCK)
return false;
log_perror(LIT("accept"));
return false;
}
uint32_t ipaddr = (uint32_t) accepted_addr.sin_addr.s_addr;
if (!set_blocking(accepted_fd, false)) {
log_perror(LIT("fcntl"));
close(accepted_fd);
return true;
}
Connection *conn = &conns[index];
init_connection(conn, accepted_fd, ipaddr, https);
assert(num_conns < max_conns);
num_conns++;
return true;
}
// Returns true iff the connection should be dropped
bool update_connection_http(Connection *conn, struct pollfd *polldata)
{
// POLLIN
if ((!conn->closing) && (polldata->revents & (POLLIN | POLLHUP | POLLERR))) {
if (read_from_socket(conn->fd, &conn->input))
return true;
if (respond_to_available_requests(conn))
return true;
}
// POLLOUT
if (polldata->revents & POLLOUT) {
if (write_to_socket(conn->fd, &conn->output))
return true;
if (byte_queue_size(&conn->output) == 0 && conn->closing)
return true;
}
return false; // Don't close
}
#if HTTPS
// Returns true iff the connection should be dropped
bool update_connection_https(Connection *conn, struct pollfd *polldata)
{
br_ssl_engine_context *cc = &conn->https_context.eng;
bool flushed = false;
for (;;) {
int state = br_ssl_engine_current_state(cc);
if (state & BR_SSL_CLOSED) TIME("BR_SSL_CLOSED") {
// Engine is finished, no more I/O (until next reset).
int error = br_ssl_engine_last_error(cc);
if (error != BR_ERR_OK) {
BearSSLErrorInfo error_info = get_bearssl_error_info(error);
log_format("SSL failure: %.*s (%.*s)\n",
(int) error_info.name.size, error_info.name.data,
(int) error_info.comment.size, error_info.comment.data);
}
return true;
}
if ((state & BR_SSL_SENDREC) && (polldata->revents & POLLOUT)) TIME("BR_SSL_SENDREC") {
// Engine has some bytes to send to the peer
size_t len;
unsigned char *buf = br_ssl_engine_sendrec_buf(cc, &len);
size_t copied = 0;
while (copied < len) {
int num = send(conn->fd, buf + copied, len - copied, 0);
if (num < 0) {
if (errno == EINTR)
continue;
if (errno == EAGAIN || errno == EWOULDBLOCK) {
polldata->revents &= ~POLLOUT;
break;
}
log_perror(LIT("send"));
return true;
}
// TODO: Handle num=0
copied += (size_t) num;
}
br_ssl_engine_sendrec_ack(cc, copied);
flushed = false;
}
if ((state & BR_SSL_RECVAPP)) TIME("BR_SSL_RECVAPP") {
// Engine has obtained some application data from the
// peer, that should be read by the caller.
size_t len;
unsigned char *buf = br_ssl_engine_recvapp_buf(cc, &len);
if (!byte_queue_ensure_min_free_space(&conn->input, len))
return true;
string dst = byte_queue_start_write(&conn->input);
assert(dst.size >= len);
memcpy(dst.data, buf, len);
if (show_io)
print_bytes(LIT("> "), (string) {dst.data, len});
byte_queue_end_write(&conn->input, len);
br_ssl_engine_recvapp_ack(cc, len);
if (respond_to_available_requests(conn))
return true;
flushed = false;
}
if ((state & BR_SSL_RECVREC) && (polldata->revents & POLLIN)) TIME("BR_SSL_RECVREC") {
// Engine expects some bytes from the peer
size_t len;
unsigned char *buf = br_ssl_engine_recvrec_buf(cc, &len);
size_t copied = 0;
while (copied < len) {
int num = recv(conn->fd, buf + copied, len - copied, 0);
if (num < 0) {
if (errno == EINTR)
continue;
if (errno == EAGAIN || errno == EWOULDBLOCK) {
polldata->revents &= ~POLLIN;
break;
}
log_perror(LIT("recv"));
return true;
}
if (num == 0) {
return true;
}
// TODO: Handle num=0
copied += (size_t) num;
}
br_ssl_engine_recvrec_ack(cc, copied);
flushed = false;
}
if ((state & BR_SSL_SENDAPP) && byte_queue_size(&conn->output) > 0) TIME("BR_SSL_SENDAPP") {
// Engine may receive application data to send (or flush).
size_t len;
unsigned char *buf = br_ssl_engine_sendapp_buf(cc, &len);
string src = byte_queue_start_read(&conn->output);
size_t copy = MIN(len, src.size);
memcpy(buf, src.data, copy);
if (show_io)
print_bytes(LIT("< "), (string) {src.data, copy});
byte_queue_end_read(&conn->output, copy);
br_ssl_engine_sendapp_ack(cc, copy);
br_ssl_engine_flush(cc, 0); // TODO: Is this the right time to call it?
flushed = false;
}
if (flushed) break;
br_ssl_engine_flush(cc, 0);
flushed = true;
}
int state = br_ssl_engine_current_state(cc);
if ((state & BR_SSL_SENDREC) == 0 && conn->closing && byte_queue_size(&conn->output) == 0)
return true;
return false; // Don't remove
}
#endif
bool update_connection(Connection *conn, struct pollfd *polldata)
{
bool ok;
TIME("update") {
#if HTTPS
if (conn->https)
TIME("update-https") ok = update_connection_https(conn, polldata);
else
#endif
TIME("update-http") ok = update_connection_http(conn, polldata);
}
return ok;
}
#ifndef NOMAIN
int main(int argc, char **argv)
{
init_globals(argc, argv);
DEBUG("Globals initialized\n");
uint64_t last_log_time = 0;
while (!stop) {
int timeout;
build_poll_array(pollarray, &timeout);
if (!log_empty()) {
int log_timeout = (last_log_time + log_flush_timeout_sec * 1000) - now;
if (timeout < 0)
timeout = log_timeout;
else
timeout = MIN(log_timeout, timeout);
}
int ret = poll(pollarray, max_conns+2, timeout);
if (ret < 0) {
if (errno == EINTR)
break; // TODO: Should this be continue?
log_perror(LIT("poll"));
return -1;
}
now = get_monotonic_time_ms();
real_now = get_real_time_ms();
if (pollarray[0].revents & POLLIN)
while (accept_connection(insecure_fd, false));
#if HTTPS
if (pollarray[1].revents & POLLIN)
while (accept_connection(secure_fd, true));
#endif
for (int i = 0; i < max_conns; i++) {
Connection *conn = &conns[i];
if (conn->fd == -1)
continue;
struct pollfd *polldata = &pollarray[i+2];
bool remove = false;
if (now >= deadline_of(conn)) {
assert(!remove);
if (conn->closing) {
// Closing timeout
remove = true;
log_data(LIT("Closing timeout\n"));
} else {
// Request timeout
if (byte_queue_size(&conn->input) == 0) {
// Connection was idle, so just close it
remove = true;
log_data(LIT("Idle connection timeout\n"));
} else {
byte_queue_write(&conn->output, LIT(
"HTTP/1.1 408 Request Timeout\r\n"
"Connection: Close\r\n"
"\r\n"));
conn->closing = true;
conn->start_time = now;
log_data(LIT("Request timeout\n"));
}
}
}
if (!remove)
remove = update_connection(conn, polldata);
if (remove) {
free_connection(conn);
num_conns--;
}
}
if (now - last_log_time > (uint64_t) log_flush_timeout_sec * 1000) {
log_flush();
last_log_time = now;
}
} /* main loop end */
free_globals();
return 0;
}
#endif
#define PATH_SEP '/'
int split_path_components(string src, string *stack, int limit, bool allow_ddots)
{
size_t cur = 0;
// Skip the first slash
if (cur < src.size && src.data[cur] == PATH_SEP)
cur++;
int depth = 0;
while (cur < src.size) {
if (depth == limit)
return -1;
size_t start = cur;
while (cur < src.size && (is_pcomp(src.data[cur]) || (allow_ddots && src.data[cur] == ':')))
cur++;
string comp = substr(src, start, cur);
if (comp.size == 0)
return -1; // We consider paths with empty components invalid
if (streq(comp, LIT(".."))) {
if (depth == 0)
return -1;
depth--;
} else {
if (!streq(comp, LIT(".")))
stack[depth++] = comp;
}
if (cur == src.size)
break;
if (src.data[cur] != PATH_SEP)
return -1;
cur++;
}
return depth;
}
/*
* Sanitize a path string removing ./ and ../
* components. The final path has an initial
* / but not final.
*/
size_t sanitize_path(string src, char *mem, size_t max)
{
#define MAX_COMPS 64
string stack[MAX_COMPS];
int depth;
depth = split_path_components(src, stack, MAX_COMPS, false);
if (depth < 0)
return -1;
/*
* Count how many output bytes are required
*/
size_t req = depth;
for (int i = 0; i < depth; i++)
req += stack[i].size;
if (req >= max)
return -1; // Buffer too small
/*
* Copy the sanitized path into the output
* buffer.
*/
size_t n = 0;
for (int i = 0; i < depth; i++) {
mem[n++] = PATH_SEP;
memcpy(mem + n, stack[i].data, stack[i].size);
n += stack[i].size;
}
mem[n] = '\0';
return n;
}
int match_path_format(string path, char *fmt, ...)
{
#define LIMIT 32
string p_stack[LIMIT];
string f_stack[LIMIT];
int p_depth;
int f_depth;
p_depth = split_path_components(path, p_stack, LIMIT, false);
f_depth = split_path_components(STR(fmt), f_stack, LIMIT, true);
if (p_depth < 0 || f_depth < 0)
return -1; // Error
if (p_depth != f_depth)
return 1; // No match
va_list args;
va_start(args, fmt);
for (int i = 0; i < f_depth; i++) {
assert(f_stack[i].size > 0);
assert(p_stack[i].size > 0);
if (f_stack[i].data[0] == ':') {
if (f_stack[i].size != 2) {
va_end(args);
return -1; // Invalid format
}
switch (f_stack[i].data[1]) {
case 's':
{
string *sl = va_arg(args, string*);
*sl = p_stack[i];
}
break;
case 'n':
{
uint32_t n = 0;
size_t cur = 0;
while (cur < p_stack[i].size && is_digit(p_stack[i].data[cur])) {
int d = p_stack[i].data[cur] - '0';
if (n > (UINT32_MAX - d) / 10) {
va_end(args);
return -1; // Overflow
}
n = n * 10 + d;
cur++;
}
if (cur != p_stack[i].size) {
va_end(args);
return -1; // Component isn't a number
}
uint32_t *p = va_arg(args, uint32_t*);
*p = n;
}
break;
default:
va_end(args);
return -1; // Invalid formt
}
} else {
if (f_stack[i].size != p_stack[i].size) {
va_end(args);
return 1; // No match
}
if (memcmp(f_stack[i].data, p_stack[i].data, f_stack[i].size)) {
va_end(args);
return 1; // No match
}
}
}
va_end(args);
return 0; // Match
}
struct {
string mime;
string ext;
} mime_table[] = {
{LIT("text/javascript"), LIT(".js")},
{LIT("text/javascript"), LIT(".javascript")},
{LIT("text/html"), LIT(".html")},
{LIT("text/html"), LIT(".htm")},
{LIT("image/gif"), LIT(".gif")},
{LIT("image/jpeg"), LIT(".jpg")},
{LIT("image/jpeg"), LIT(".jpeg")},
{LIT("image/svg+xml"), LIT(".svg")},
{LIT("video/mp4"), LIT(".mp4")},
{LIT("video/mpeg"), LIT(".mpeg")},
{LIT("font/ttf"), LIT(".ttf")},
{LIT("font/woff"), LIT(".woff")},
{LIT("font/woff2"), LIT(".woff2")},
{LIT("text/plain"), LIT(".txt")},
{LIT("audio/wav"), LIT(".wav")},
{LIT("application/x-7z-compressed"), LIT(".7z")},
{LIT("application/zip"), LIT(".zip")},
{LIT("application/xml"), LIT(".xml")},
{LIT("application/json"), LIT(".json")},
{NULLSTR, NULLSTR},
};
string mimetype_from_filename(string name)
{
for (size_t i = 0; i < COUNTOF(mime_table); i++)
if (endswith(mime_table[i].ext, name))
return mime_table[i].mime;
return NULLSTR;
}
bool serve_file_or_dir(ResponseBuilder *b, string prefix, string docroot,
string reqpath, string mime, bool enable_dir_listing)
{
// Sanitize the request path
char pathmem[1<<10];
string path;
{
size_t len = sanitize_path(reqpath, pathmem, sizeof(pathmem));
if (len >= sizeof(pathmem)) {
status_line(b, 500);
return true;
}
path = (string) {pathmem, len};
path.data[path.size] = '\0';
}
// Only handle this request if the prefix matches
if (!startswith(prefix, path))
return false;
// Remove the matched prefix and put the docroot in its place
{
if (docroot.size + path.size - prefix.size >= sizeof(pathmem)) {
status_line(b, 500);
return true;
}
memmove(pathmem + docroot.size, pathmem + prefix.size, path.size - prefix.size);
memcpy(pathmem, docroot.data, docroot.size);
path.size -= prefix.size;
path.size += docroot.size;
path.data[path.size] = '\0';
}
struct stat buf;
if (stat(path.data, &buf)) {
if (errno == ENOENT)
return false;
status_line(b, 500);
return true;
}
if (S_ISREG(buf.st_mode)) {
int fd;
do
fd = open(path.data, O_RDONLY);
while (fd < 0 && errno == EINTR);
if (fd < 0) {
status_line(b, 500);
close(fd);
return true;
}
status_line(b, 200);
if (mime.size == 0) mime = mimetype_from_filename(path);
if (mime.size > 0) add_header_f(b, "Content-Type: %.*s", (int) mime.size, mime.data);
string dst = append_content_start(b, (size_t) buf.st_size);
if (dst.size == 0) {
status_line(b, 500);
close(fd);
return true;
}
assert(dst.size >= (size_t) buf.st_size);
size_t copied = 0;
while (copied < (size_t) buf.st_size) {
int num = read(fd, dst.data + copied, (size_t) buf.st_size - copied);
if (num <= 0) {
if (num < 0)
log_format("Failed reading from '%.*s'\n", (int) path.size, path.data);
break;
}
copied += num;
}
append_content_end(b, copied);
close(fd);
return true;
}
if (enable_dir_listing && S_ISDIR(buf.st_mode)) {
DIR *d = opendir(path.data);
if (d == NULL) {
status_line(b, 500);
return true;
}
status_line(b, 200);
append_content_s(b, LIT(
"<html>\n"
" <head>\n"
" </head>\n"
" <body>\n"
" <ul>\n"
" <li><a href=\"\">(parent)</a></li>")); // TODO: Add links
struct dirent *dir;
while ((dir = readdir(d))) {
if (!strcmp(dir->d_name, ".") || !strcmp(dir->d_name, ".."))
continue;
append_content_f(b, "<li><a href=\"\">%s</a></li>\n", dir->d_name); // TODO: Add links
}
append_content_s(b, LIT(
" </ul>\n"
" </body>\n"
"</html>\n"));
closedir(d);
return true;
}
return false;
}
///////////////////////////////////////////////////////////////////////////////////////////////
/// LOGGER ///
///////////////////////////////////////////////////////////////////////////////////////////////
bool log_initialized = false;
int log_last_file_index = 0;
int log_fd = -1;
char *log_buffer = NULL;
size_t log_buffer_used = 0;
size_t log_buffer_size = 0;
bool log_failed = false;
size_t log_total_size = 0;
size_t log_dir_limit_mb = 0;
size_t log_file_limit_b = 0;
char log_dir[1<<12];
void log_choose_file_name(char *dst, size_t max, bool startup)
{
size_t prev_size = -1;
for (;;) {
int num = snprintf(dst, max, "%s/log_%d.txt", log_dir, log_last_file_index);
if (num < 0 || (size_t) num >= max) {
write_format_to_stderr("log_failed (%s:%d)\n", __FILE__, __LINE__);
log_failed = true;
return;
}
dst[num] = '\0';
struct stat buf;
if (stat(dst, &buf)) {
if (errno == ENOENT)
break;
write_format_to_stderr("log_failed: %s (%s:%d)\n", strerror(errno), __FILE__, __LINE__);
log_failed = true;
return;
}
prev_size = (size_t) buf.st_size;
if (log_last_file_index == 100000000) {
write_format_to_stderr("log_failed (%s:%d)\n", __FILE__, __LINE__);
log_failed = true;
return;
}
log_last_file_index++;
}
// At startup don't create a new log file if the last one didn't reache its limit
if (startup && prev_size < log_file_limit_b) {
log_last_file_index--;
int num = snprintf(dst, max, "%s/log_%d.txt", log_dir, log_last_file_index);
if (num < 0 || (size_t) num >= max) {
write_format_to_stderr("log_failed (%s:%d)\n", __FILE__, __LINE__);
log_failed = true;
return;
}
dst[num] = '\0';
}
}
void log_init(string dir, size_t dir_limit_mb, size_t file_limit_b, size_t buffer_size)
{
// Copy args to "local" variables
if (dir.size >= sizeof(log_dir))
log_fatal(LIT("Log directory is too long\n"));
memcpy(log_dir, dir.data, dir.size);
log_dir[dir.size] = '\0';
log_buffer_size = buffer_size;
log_dir_limit_mb = dir_limit_mb;
log_file_limit_b = file_limit_b;
log_buffer = mymalloc(log_buffer_size);
if (log_buffer == NULL) {
write_format_to_stderr("log_failed (%s:%d)\n", __FILE__, __LINE__);
log_failed = true;
return;
}
if (mkdir(log_dir, 0755) && errno != EEXIST) {
write_format_to_stderr("log_failed (%s:%d)\n", __FILE__, __LINE__);
log_failed = true;
return;
}
char name[1<<12];
log_choose_file_name(name, sizeof(name), true);
if (log_failed) return;
log_fd = open(name, O_WRONLY | O_APPEND | O_CREAT, 0644);
if (log_fd < 0) {
write_format_to_stderr("log_failed (%s:%d)\n", __FILE__, __LINE__);
log_failed = true;
return;
}
log_total_size = 0;
DIR *d = opendir(log_dir);
if (d == NULL) {
write_format_to_stderr("log_failed (%s:%d)\n", __FILE__, __LINE__);
log_failed = true;
return;
}
struct dirent *dir_entry;
while ((dir_entry = readdir(d))) {
if (!strcmp(dir_entry->d_name, ".") || !strcmp(dir_entry->d_name, ".."))
continue;
char path[1<<12];
int k = snprintf(path, SIZEOF(path), "%s/%s", log_dir, dir_entry->d_name);
if (k < 0 || k >= SIZEOF(path)) log_fatal(LIT("Bad format"));
path[k] = '\0';
struct stat buf;
if (stat(path, &buf))
log_fatal(LIT("Couldn't stat log file"));
if ((size_t) buf.st_size > SIZE_MAX - log_total_size)
log_fatal(LIT("Log file is too big"));
log_total_size += (size_t) buf.st_size;
}
closedir(d);
static_assert(SIZEOF(size_t) > 4, "It's assumed size_t can store a number of bytes in the order of 10gb");
if (log_total_size > log_dir_limit_mb * 1024 * 1024) {
write_string_to_stderr(LIT("Log reached disk limit at startup\n"));
log_failed = true;
return;
}
log_initialized = true;
}
void log_free(void)
{
if (log_initialized) {
log_flush();
if (log_fd > -1)
close(log_fd);
myfree(log_buffer, log_buffer_size);
log_fd = -1;
log_buffer = NULL;
log_buffer_used = 0;
log_buffer_size = 0;
log_failed = false;
log_file_limit_b = 0;
log_dir_limit_mb = 0;
log_dir[0] = '\0';
log_initialized = false;
}
}
bool log_empty(void)
{
return log_failed || log_buffer_used == 0;
}
void log_flush(void)
{
if (!log_initialized || log_failed || log_buffer_used == 0)
return;
/*
* Rotate the file if the limit was reached
*/
struct stat buf;
if (fstat(log_fd, &buf)) {
write_format_to_stderr("log_failed: %s (%s:%d)\n", strerror(errno), __FILE__, __LINE__);
log_failed = true;
return;
}
if (buf.st_size + log_buffer_used >= log_file_limit_b) {
char name[1<<12];
log_choose_file_name(name, SIZEOF(name), false);
if (log_failed) return;
close(log_fd);
log_fd = open(name, O_WRONLY | O_APPEND | O_CREAT, 0644);
if (log_fd < 0) {
write_format_to_stderr("log_failed (%s:%d)\n", __FILE__, __LINE__);
log_failed = true;
return;
}
}
/*
* Buffer is full. We need to flush it to continue
*/
int zeros = 0;
size_t copied = 0;
while (copied < log_buffer_used) {
int num = write(log_fd, log_buffer + copied, log_buffer_used - copied);
if (num < 0) {
if (errno == EINTR)
continue;
write_format_to_stderr("log_failed (%s:%d)\n", __FILE__, __LINE__);
log_failed = true;
return;
}
if (num == 0) {
zeros++;
if (zeros == 1000) {
write_format_to_stderr("log_failed (%s:%d)\n", __FILE__, __LINE__);
log_failed = true;
return;
}
} else {
zeros = 0;
}
copied += num;
log_total_size += num;
if (log_total_size > log_dir_limit_mb * 1024 * 1024) {
write_string_to_stderr(LIT("Log reached disk limit\n"));
log_failed = true;
return;
}
}
assert(copied == log_buffer_used);
log_buffer_used = 0;
}
void log_fatal(string str)
{
log_data(str);
exit(-1);
}
void log_format(const char *fmt, ...)
{
if (!log_initialized) {
va_list args;
va_start(args, fmt);
write_format_to_stderr_va(fmt, args);
va_end(args);
return;
}
if (log_failed)
return;
if (log_buffer_used == log_buffer_size) {
log_flush();
if (log_failed) return;
}
int num;
{
va_list args;
va_start(args, fmt);
num = vsnprintf(log_buffer + log_buffer_used, log_buffer_size - log_buffer_used, fmt, args);
va_end(args);
}
if (num < 0 || (size_t) num > log_buffer_size) {
write_format_to_stderr("log_failed (%s:%d)\n", __FILE__, __LINE__);
log_failed = true;
return;
}
if ((size_t) num > log_buffer_size - log_buffer_used) {
log_flush();
if (log_failed) return;
va_list args;
va_start(args, fmt);
int k = vsnprintf(log_buffer + log_buffer_used, log_buffer_size - log_buffer_used, fmt, args);
va_end(args);
if (k != num) log_fatal(LIT("Bad format"));
}
log_buffer_used += num;
}
void log_data(string str)
{
if (!log_initialized) {
fwrite(str.data, 1, str.size, stdout);
return;
}
if (log_failed)
return;
if (str.size > log_buffer_size)
str = LIT("Log message was too long to log");
if (str.size > log_buffer_size - log_buffer_used) {
log_flush();
if (log_failed) return;
}
assert(str.size <= log_buffer_size - log_buffer_used);
assert(log_buffer);
memcpy(log_buffer + log_buffer_used, str.data, str.size);
log_buffer_used += str.size;
}
void log_perror(string str)
{
if (!log_initialized)
write_format_to_stderr("%.*s: %s\n", (int) str.size, str.data, strerror(errno));
else
log_format("%.*s: %s\n", (int) str.size, str.data, strerror(errno));
}
///////////////////////////////////////////////////////////////////////////////////////////////
/// BASIC UTILITIES ///
///////////////////////////////////////////////////////////////////////////////////////////////
uint64_t timespec_to_ms(struct timespec ts)
{
if ((uint64_t) ts.tv_sec > UINT64_MAX / 1000)
log_fatal(LIT("Time overflow\n"));
uint64_t ms = ts.tv_sec * 1000;
uint64_t nsec_part = ts.tv_nsec / 1000000;
if (ms > UINT64_MAX - nsec_part)
log_fatal(LIT("Time overflow\n"));
ms += nsec_part;
return ms;
}
uint64_t timespec_to_ns(struct timespec ts)
{
if ((uint64_t) ts.tv_sec > UINT64_MAX / 1000000000)
log_fatal(LIT("Time overflow\n"));
uint64_t ns = ts.tv_sec * 1000000000;
if (ns > UINT64_MAX - ts.tv_nsec)
log_fatal(LIT("Time overflow\n"));
ns += ts.tv_nsec;
return ns;
}
uint64_t get_monotonic_time_ms(void)
{
struct timespec ts;
int ret = clock_gettime(CLOCK_MONOTONIC, &ts);
if (ret) log_fatal(LIT("Couldn't read monotonic time\n"));
return timespec_to_ms(ts);
}
uint64_t get_monotonic_time_ns(void)
{
struct timespec ts;
int ret = clock_gettime(CLOCK_MONOTONIC, &ts);
if (ret) log_fatal(LIT("Couldn't read monotonic time\n"));
return timespec_to_ns(ts);
}
uint64_t get_real_time_ms(void)
{
struct timespec ts;
int ret = clock_gettime(CLOCK_REALTIME, &ts);
if (ret) log_fatal(LIT("Couldn't read real time\n"));
return timespec_to_ms(ts);
}
bool string_match_case_insensitive(string x, string y)
{
if (x.size != y.size)
return false;
for (size_t i = 0; i < x.size; i++)
if (to_lower(x.data[i]) != to_lower(y.data[i]))
return false;
return true;
}
char to_lower(char c)
{
if (c >= 'A' && c <= 'Z')
return c - 'A' + 'a';
else
return c;
}
string trim(string s)
{
size_t cur = 0;
while (cur < s.size && is_space(s.data[cur]))
cur++;
if (cur == s.size) {
s.data = "";
s.size = 0;
} else {
s.data += cur;
s.size -= cur;
while (is_space(s.data[s.size-1]))
s.size--;
}
return s;
}
string substr(string str, size_t start, size_t end)
{
return (string) {
.data = str.data + start,
.size = end - start,
};
}
bool is_digit(char c)
{
return c >= '0' && c <= '9';
}
bool is_alpha(char c)
{
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
}
bool is_space(char c)
{
return c == ' ' || c == '\t' || c == '\r' || c == '\n';
}
bool is_print(char c)
{
return c >= 32 && c < 127;
}
bool is_pcomp(char c)
{
return c != '/' && c != ':' && is_print(c);
}
bool streq(string s1, string s2)
{
// TODO: What is s1.data or s2.data is NULL?
return s1.size == s2.size && !memcmp(s1.data, s2.data, s1.size);
}
bool startswith(string prefix, string str)
{
if (prefix.size > str.size)
return false;
// TODO: What is prefix.data==NULL or str.data==NULL?
return !memcmp(prefix.data, str.data, prefix.size);
}
bool endswith(string suffix, string name)
{
char *tail = name.data + (name.size - suffix.size);
return suffix.size <= name.size && !memcmp(tail, suffix.data, suffix.size);
}
bool load_file_contents(string file, string *out)
{
char copy[1<<12];
if (file.size >= sizeof(copy)) {
log_data(LIT("File path is larger than the static buffer\n"));
return false;
}
memcpy(copy, file.data, file.size);
copy[file.size] = '\0';
int fd = open(copy, O_RDONLY);
if (fd < 0)
return false;
struct stat buf;
if (fstat(fd, &buf) || !S_ISREG(buf.st_mode)) {
log_data(LIT("Couldn't stat file or it's not a regular file\n"));
close(fd);
return false;
}
size_t size = (size_t) buf.st_size;
char *str = mymalloc(size);
if (str == NULL) {
log_data(LIT("out of memory\n"));
close(fd);
return false;
}
size_t copied = 0;
while (copied < size) {
int n = read(fd, str + copied, size - copied);
if (n < 0) {
if (errno == EINTR)
continue;
log_perror(LIT("read"));
close(fd);
myfree(str, size);
return false;
}
if (n == 0)
break; // EOF
copied += n;
}
if (copied != size) {
log_format("Read %zu bytes from file but %zu were expected\n", copied, size);
return false;
}
close(fd);
*out = (string) {str, size};
return true;
}
///////////////////////////////////////////////////////////////////////////////////////////////
/// BYTE QUEUE ///
///////////////////////////////////////////////////////////////////////////////////////////////
void byte_queue_init(ByteQueue *q)
{
q->data = NULL;
q->head = 0;
q->size = 0;
q->capacity = 0;
}
void byte_queue_free(ByteQueue *q)
{
myfree(q->data, q->capacity);
byte_queue_init(q);
}
bool byte_queue_ensure_min_free_space(ByteQueue *q, size_t num)
{
size_t total_free_space = q->capacity - q->size;
size_t free_space_after_data = q->capacity - q->size - q->head;
if (free_space_after_data < num) {
if (total_free_space < num) {
// Resize required
size_t capacity = MAX(2 * q->capacity, q->size + num);
char *data = mymalloc(capacity);
if (!data) return false;
if (q->size > 0)
memcpy(data, q->data + q->head, q->size);
myfree(q->data, q->capacity);
q->data = data;
q->head = 0;
q->capacity = capacity;
} else {
// Move required
memmove(q->data, q->data + q->head, q->size);
q->head = 0;
}
}
return true;
}
string byte_queue_start_write(ByteQueue *q)
{
if (q->data == NULL)
return NULLSTR;
return (string) {
.data = q->data + (q->head + q->size),
.size = q->capacity - (q->head + q->size),
};
}
void byte_queue_end_write(ByteQueue *q, size_t num)
{
q->size += num;
}
string byte_queue_start_read(ByteQueue *q)
{
if (q->data == NULL)
return NULLSTR;
return (string) {
.data = q->data + q->head,
.size = q->size,
};
}
size_t byte_queue_size(ByteQueue *q)
{
return q->size;
}
void byte_queue_end_read(ByteQueue *q, size_t num)
{
q->head += num;
q->size -= num;
}
bool byte_queue_write(ByteQueue *q, string src)
{
if (!byte_queue_ensure_min_free_space(q, src.size))
return false;
string dst = byte_queue_start_write(q);
assert(dst.size >= src.size);
memcpy(dst.data, src.data, src.size);
byte_queue_end_write(q, src.size);
return true;
}
void byte_queue_patch(ByteQueue *q, size_t offset, char *src, size_t len)
{
// TODO: Safety checks
memcpy(q->data + q->head + offset, src, len);
}
///////////////////////////////////////////////////////////////////////////////////////////////
/// SOCKET UTILITIES ///
///////////////////////////////////////////////////////////////////////////////////////////////
void print_bytes(string prefix, string str)
{
char *src = str.data;
size_t len = str.size;
bool line_start = true;
size_t i = 0;
while (i < len) {
size_t substr_offset = i;
while (i < len && src[i] != '\r' && src[i] != '\n')
i++;
size_t substr_length = i - substr_offset;
if (line_start) {
log_data(prefix);
line_start = false;
}
log_data((string) { src + substr_offset, substr_length });
if (i < len) {
if (src[i] == '\r')
log_data(LIT("\\r"));
else {
log_data(LIT("\\n\n"));
line_start = true;
}
i++;
}
}
if (!line_start)
log_data(LIT("\n"));
}
bool read_from_socket(int fd, ByteQueue *queue)
{
bool remove = false;
for (;;) {
if (!byte_queue_ensure_min_free_space(queue, 512)) {
remove = true;
break;
}
string dst = byte_queue_start_write(queue);
int num = recv(fd, dst.data, dst.size, 0);
if (num < 0) {
if (errno == EINTR)
continue;
if (errno == EAGAIN || errno == EWOULDBLOCK)
break;
log_perror(LIT("recv"));
remove = true;
break;
}
if (num == 0) {
remove = true;
break;
}
if (show_io)
print_bytes(LIT("> "), (string) {dst.data, num});
byte_queue_end_write(queue, (size_t) num);
// Input buffer can't go over 20Mb
if (byte_queue_size(queue) > (size_t) INPUT_BUFFER_LIMIT_MB * 1024 * 1024) {
remove = true;
break;
}
}
return remove;
}
bool write_to_socket(int fd, ByteQueue *queue)
{
bool remove = false;
for (;;) {
string src = byte_queue_start_read(queue);
if (src.size == 0) break;
int num = send(fd, src.data, src.size, 0);
if (num < 0) {
if (errno == EINTR)
continue;
if (errno == EAGAIN || errno == EWOULDBLOCK)
break;
log_perror(LIT("send"));
remove = true;
break;
}
if (show_io)
print_bytes(LIT("< "), (string) {src.data, num});
byte_queue_end_read(queue, (size_t) num);
}
return remove;
}
int create_listening_socket(string addr, int port)
{
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd < 0) {
log_perror(LIT("socket"));
return -1;
}
if (!set_blocking(listen_fd, false)) {
log_perror(LIT("fcntl"));
return -1;
}
int one = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, (char*) &one, sizeof(one));
struct in_addr addr2;
if (addr.size == 0)
addr2.s_addr = htonl(INADDR_ANY);
else {
assert(addr.data);
char addr_copy[INET_ADDRSTRLEN];
if (addr.size >= SIZEOF(addr_copy))
log_fatal(LIT("Invalid IP address (too long)\n"));
memcpy(addr_copy, addr.data, addr.size);
addr_copy[addr.size] = '\0';
int res = inet_pton(AF_INET, addr_copy, &addr2);
if (res == 0) log_fatal(LIT("Invalid IP address (too long)\n"));
if (res != 1) log_fatal(LIT("Could not parse IP address (internal error)\n"));
}
struct sockaddr_in addr3;
addr3.sin_family = AF_INET;
addr3.sin_port = htons(port);
addr3.sin_addr = addr2;
if (bind(listen_fd, (struct sockaddr*) &addr3, sizeof(addr3))) {
log_perror(LIT("bind"));
return -1;
}
if (listen(listen_fd, 32)) {
log_perror(LIT("listen"));
return -1;
}
return listen_fd;
}
bool set_blocking(int fd, bool blocking)
{
int flags = fcntl(fd, F_GETFL, 0);
if (flags == -1)
return false;
if (blocking)
flags &= ~O_NONBLOCK;
else
flags |= O_NONBLOCK;
if (fcntl(fd, F_SETFL, flags))
return false;
return true;
}
bool write_string_to_stderr(string s)
{
int fd = STDERR_FILENO;
size_t num = 0;
while (num < s.size) {
int ret = write(fd, s.data + num, s.size - num);
if (ret < 0) {
if (errno == EINTR)
continue;
return false;
}
num += ret;
};
return true;
}
bool write_format_to_stderr_va(const char *fmt, va_list args)
{
char buf[1<<10];
int num = vsnprintf(buf, sizeof(buf), fmt, args);
if (num < 0) log_fatal(LIT("Invalid format"));
string str = {buf, num};
return write_string_to_stderr(str);
}
bool write_format_to_stderr(const char *fmt, ...)
{
va_list args;
va_start(args, fmt);
bool ok = write_format_to_stderr_va(fmt, args);
va_end(args);
return ok;
}
///////////////////////////////////////////////////////////////////////////////////////////////
/// PROFILING ///
///////////////////////////////////////////////////////////////////////////////////////////////
#if PROFILE
typedef struct {
string label;
uint64_t delta_cycles;
uint64_t exec_count;
} TimedScope;
TimedScope timed_scopes[__COUNTER__+1]; // +1 is just to avoid the zero length array
uint64_t timing_init_time_ns;
uint64_t timing_init_time_cycles;
void timing_init(void)
{
timing_init_time_ns = get_monotonic_time_ns();
timing_init_time_cycles = __rdtsc();
}
void human_readable_time_interval(uint64_t ns, char *dst, size_t max)
{
if (ns > 1000000000)
snprintf(dst, max, "%.1Lf s", (long double) ns / 1000000000);
else if (ns > 1000000)
snprintf(dst, max, "%.1Lf ms", (long double) ns / 1000000);
else if (ns > 1000)
snprintf(dst, max, "%.1Lf us", (long double) ns / 1000);
else
snprintf(dst, max, "%.1Lf ns", (long double) ns);
}
void timing_print_results(void)
{
uint64_t end_cycles = __rdtsc();
uint64_t end_ns = get_monotonic_time_ns();
double cy2ns = (double) (end_ns - timing_init_time_ns) / (end_cycles - timing_init_time_cycles);
write_format_to_stderr("Printing timing results\n");
for (int i = 0; i < COUNTOF(timed_scopes); i++) {
TimedScope scope = timed_scopes[i];
if (scope.exec_count == 0)
continue;
char total_str[128];
char average_str[128];
human_readable_time_interval(cy2ns * scope.delta_cycles, total_str, sizeof(total_str));
human_readable_time_interval(cy2ns * scope.delta_cycles / scope.exec_count, average_str, sizeof(average_str));
write_format_to_stderr("%-20.*s| tot %s\t| avg %s\t| calls %lu\n",
(int) scope.label.size, scope.label.data,
total_str, average_str, scope.exec_count);
}
}
void timing_result(int scope_index, uint64_t delta_cycles, string label)
{
timed_scopes[scope_index].label = label;
timed_scopes[scope_index].delta_cycles += delta_cycles;
timed_scopes[scope_index].exec_count++;
}
#endif
///////////////////////////////////////////////////////////////////////////////////////////////
/// ALLOCATORS ///
///////////////////////////////////////////////////////////////////////////////////////////////
#if EOPALLOC
void *mymalloc(size_t num)
{
int page_size = sysconf(_SC_PAGE_SIZE);
size_t num_pages = (num + page_size - 1) / page_size;
assert(num_pages > 0);
void *addr = mmap(NULL, (num_pages + 1) * page_size, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
if (addr == MAP_FAILED)
return NULL;
void *head_page = addr;
addr = (char*) addr + page_size;
if (mprotect(head_page, page_size, PROT_NONE)) {
log_perror(LIT("mprotect"));
exit(-1);
}
return addr;
}
void myfree(void *ptr, size_t num)
{
if (ptr == NULL)
return;
int page_size = sysconf(_SC_PAGE_SIZE);
size_t num_pages = (num + page_size - 1) / page_size;
void *head_page = (char*) ptr - page_size;
munmap(head_page, (num_pages + 1) * page_size);
}
#else
void *mymalloc(size_t num)
{
return malloc(num);
}
void myfree(void *ptr, size_t num)
{
(void) num;
free(ptr);
}
#endif
///////////////////////////////////////////////////////////////////////////////////////////////
/// CONFIGURATION PARSER ///
///////////////////////////////////////////////////////////////////////////////////////////////
typedef enum {
CE_INT,
CE_STR,
CE_BOOL,
} ConfigEntryType;
typedef struct {
string name;
ConfigEntryType type;
union {
uint32_t num;
string txt;
bool yes;
};
} ConfigEntry;
string config_content;
ConfigEntry *config_entries;
int config_count;
int config_capacity;
void make_char_printable(char *buf, size_t max, char c)
{
(void) max;
if (is_print(c)) {
assert(max >= 4);
buf[0] = '\'';
buf[1] = c;
buf[2] = '\'';
buf[3] = '\0';
} else {
assert(max >= 5);
static const char hextable[] = "0123456789abcdef";
buf[0] = '0';
buf[1] = 'x';
buf[2] = hextable[(uint8_t) c >> 4];
buf[3] = hextable[c & 0xf];
buf[4] = '\0';
}
}
bool config_parse(string content)
{
char *src = content.data;
size_t len = content.size;
size_t cur = 0;
bool error = false;
for (;;) {
// Skip whitespace before the entry
while (cur < len && is_space(src[cur]))
cur++;
if (cur == len)
break;
if (src[cur] == '#') {
// Comment
while (cur < len && src[cur] != '\n')
cur++;
if (cur < len) {
assert(src[cur] == '\n');
cur++;
}
} else {
// Expecting an identifier
if (!is_alpha(src[cur]) && src[cur] != '_') {
char buf[5];
make_char_printable(buf, sizeof(buf), src[cur]);
// Configs are handled before logging, so we need to write to stderr here
log_format("Could not parse config file (invalid character %s)\n", buf);
error = true;
break;
}
ConfigEntry entry;
size_t name_start = cur;
do
cur++;
while (cur < len && (is_alpha(src[cur]) || is_digit(src[cur]) || src[cur] == '_'));
entry.name = substr(content, name_start, cur);
while (cur < len && is_space(src[cur]) && src[cur] != '\n')
cur++;
if (cur == len) {
log_format("Missing value after '%.*s' in config file\n", (int) entry.name.size, entry.name.data);
error = true;
break;
}
if (cur+2 < len
&& src[cur+0] == 'y'
&& src[cur+1] == 'e'
&& src[cur+2] == 's'
&& (cur+3 == len || is_space(src[cur+3]))) {
entry.type = CE_BOOL;
entry.yes = true;
cur += 3;
} else if (cur+1 < len
&& src[cur+0] == 'n'
&& src[cur+1] == 'o'
&& (cur+2 == len || is_space(src[cur+2]))) {
entry.type = CE_BOOL;
entry.yes = false;
cur += 2;
} else if (src[cur] == '"') {
cur++; // Skip the first double quote
size_t value_start = cur;
while (cur < len && src[cur] != '"')
cur++;
entry.type = CE_STR;
entry.txt = substr(content, value_start, cur);
if (cur < len) {
assert(src[cur] == '"');
cur++;
}
} else if (is_digit(src[cur])) {
uint32_t value = 0;
do {
int d = src[cur] - '0';
if (value > (UINT32_MAX - d) / 10) {
log_format("Invalid value after '%.*s' in config file (Integer is too big)\n", (int) entry.name.size, entry.name.data);
error = true;
break;
}
value = value * 10 + d;
cur++;
} while (cur < len && is_digit(src[cur]));
if (error) break;
entry.type = CE_INT;
entry.num = value;
} else {
size_t value_start = cur;
while (cur < len && (is_print(src[cur]) && !is_space(src[cur])))
cur++;
entry.type = CE_STR;
entry.txt = substr(content, value_start, cur);
}
if (config_count == config_capacity) {
int new_cap = MAX(2 * config_capacity, 32);
void *new_ptr = mymalloc(new_cap * sizeof(ConfigEntry));
if (new_ptr == NULL) {
log_format("Couldn't load config file (out of memory)\n");
error = true;
break;
}
if (config_count > 0)
memcpy(new_ptr, config_entries, config_count * sizeof(ConfigEntry));
myfree(config_entries, config_capacity * sizeof(ConfigEntry));
config_entries = new_ptr;
config_capacity = new_cap;
}
config_entries[config_count++] = entry;
// Skip the rest of the line
while (cur < len && is_space(src[cur]) && src[cur] != '\n')
cur++;
if (cur < len && src[cur] == '#')
while (cur < len && src[cur] != '\n')
cur++;
if (cur < len) {
if (src[cur] != '\n') {
char buf[5];
make_char_printable(buf, sizeof(buf), src[cur]);
log_format("Invalid character %s after '%.*s' entry in config file\n", buf, (int) entry.name.size, entry.name.data);
error = true;
break;
}
cur++;
}
}
}
if (error) config_free();
return !error;
}
void config_init(void)
{
config_content = NULLSTR;
config_entries = NULL;
config_count = 0;
config_capacity = 0;
}
bool config_load(string file)
{
config_init();
if (!load_file_contents(file, &config_content))
log_fatal(LIT("Could not load config file\n"));
if (!config_parse(config_content)) {
return false;
}
return true;
}
void config_free(void)
{
if (config_entries) {
myfree(config_content.data, config_content.size);
myfree(config_entries, config_capacity * sizeof(ConfigEntry));
config_content = NULLSTR;
config_entries = NULL;
config_count = 0;
config_capacity = 0;
}
}
ConfigEntry *config_any(string name)
{
for (int i = 0; i < config_count; i++)
if (streq(name, config_entries[i].name))
return &config_entries[i];
return NULL;
}
string config_string(string name)
{
ConfigEntry *entry = config_any(name);
if (entry == NULL) {
log_format("Config entry '%.*s' is not defined\n", (int) name.size, name.data);
exit(-1);
}
if (entry->type != CE_STR) {
log_format("Config entry '%.*s' is not a string\n", (int) name.size, name.data);
exit(-1);
}
return entry->txt;
}
uint32_t config_int(string name)
{
ConfigEntry *entry = config_any(name);
if (entry == NULL) {
log_format("Config entry '%.*s' is not defined\n", (int) name.size, name.data);
exit(-1);
}
if (entry->type != CE_INT) {
log_format("Config entry '%.*s' is not a string\n", (int) name.size, name.data);
exit(-1);
}
return entry->num;
}
bool config_bool(string name)
{
ConfigEntry *entry = config_any(name);
if (entry == NULL) {
log_format("Config entry '%.*s' is not defined\n", (int) name.size, name.data);
exit(-1);
}
if (entry->type != CE_BOOL) {
log_format("Config entry '%.*s' is not a boolean\n", (int) name.size, name.data);
exit(-1);
}
return entry->yes;
}
///////////////////////////////////////////////////////////////////////////////////////////////
/// URI PARSER ///
///////////////////////////////////////////////////////////////////////////////////////////////
static bool is_lower_alpha(char c)
{
return c >= 'a' && c <= 'z';
}
static bool is_upper_alpha(char c)
{
return c >= 'A' && c <= 'Z';
}
static bool is_hex_digit(char c)
{
return is_digit(c)
|| (c >= 'a' && c <= 'f')
|| (c >= 'A' && c <= 'F');
}
static bool is_unreserved(char c)
{
return is_alpha(c) || is_digit(c)
|| c == '-' || c == '.'
|| c == '_' || c == '~';
}
static bool is_subdelim(char c)
{
return c == '!' || c == '$'
|| c == '&' || c == '\''
|| c == '(' || c == ')'
|| c == '*' || c == '+'
|| c == ',' || c == ';'
|| c == '=';
}
static bool is_pchar(char c)
{
return is_unreserved(c)
|| is_subdelim(c)
|| c == ':' || c == '@';
}
static bool is_schema_first(char c)
{
return is_alpha(c);
}
static bool is_schema(char c)
{
return is_alpha(c)
|| is_digit(c)
|| c == '+'
|| c == '-'
|| c == '.';
}
// Symbol: parse_schema
// Parse the schema of an url, if there is one.
//
// Arguments:
// (in) src: The source string.
//
// len: The number of bytes pointed by [src].
//
// (in/out) i: Offset from where the parsing should
// start. When the function returns, its
// value is incremented by the number of
// parsed bytes (therefore pointing to the
// first non-parsed byte). If the url in
// the source has no schema, it's value
// is left unchanged.
//
// (out) schema: Non zero-terminated string containing
// the parsed schema. If the url didn't
// have a schema, it's set to NULL.
//
// (out) schema_len: Length of the parsed schema, or 0 if
// there wasn't one.
//
// Returns:
// - [i] is incremented by the number of parsed bytes.
//
// - [schema] points to the schema string (or NULL if
// there wasn't one).
//
// - [schema_len] is the length of the string pointed
// by [schema], or 0 if there was no schema.
//
// Notes:
// - This function can never fail.
//
static void parse_schema(string str, size_t *i, string *schema)
{
char *src = str.data;
size_t len = str.size;
size_t peek = *i; // Local cursor
bool no_schema;
size_t schema_off;
size_t schema_end;
// If there is a schema, set [no_schema] to
// [false], [schema_off] to the start of
// the schema substring (relative to [src])
// and [schema_end] to its end.
{
if (peek == len || !is_schema_first(src[peek]))
// The first character can't be the start
// of a schema.
no_schema = true;
else {
// The current character is a valid start
// for a schema. We'll assume it is until
// proven otherwise.
// Keep track of the current posizion, then
// consume all characters that compose the
// schema.
schema_off = peek;
do
peek++;
while (peek < len && is_schema(src[peek]));
schema_end = peek;
// If the character following the schema
// isn't a ':', it wasn't a schema after
// all.
if (peek == len || src[peek] != ':')
no_schema = true;
else {
no_schema = false;
peek++; // Skip the ':'
}
}
}
// Update the output parameters.
if (no_schema)
*schema = NULLSTR;
// Don't unpdate [i]
else {
*schema = substr(str, schema_off, schema_end);
*i = peek; // Commit changes.
}
}
static bool is_username(char c)
{
return is_unreserved(c) || is_subdelim(c);
}
static bool is_username_first(char c)
{
return is_username(c);
}
static bool is_password(char c)
{
return is_username(c);
}
static bool is_password_first(char c)
{
return is_password(c);
}
// Symbol: parse_userinfo
// Parse the userinfo component of an URL,
// if there is one.
//
// The userinfo component is a subcomponent
// of the authority. It contains a username
// and, optionally, a password:
//
// http://username@example.com
// http://username:password@example.com
//
// This functions parses the portion that
// goes from the byte following "//" to
// the '@'.
//
// Arguments:
// (in) src: The source string.
//
// len: The number of bytes pointed by [src]
//
// (in/out) i: Offset from where the parsing should
// start. When the function returns, its
// value is incremented by the number of
// parsed bytes (therefore pointing to the
// first non-parsed byte). If the url in
// the source has no userinfo, it's value
// is left unchanged.
//
// (out) userinfo: The result of the parsing.
//
// Returns:
// - [i] is incremented by the number of parsed bytes.
//
// - [userinfo] contains the parsed username and
// password. These strings are not zero-terminated.
// If there was no userinfo component, then
// [userinfo.username] and [userinfo.password] are
// NULL and [userinfo.username_len] and
// [userinfo.password_len] are 0. If the username
// is specified but the password isn't, then only
// [userinfo.password] is NULL and
// [userinfo.password_len] is 0.
//
// Notes:
// - This function can never fail.
//
static void
parse_userinfo(string str, size_t *i, url_userinfo *userinfo)
{
char *src = str.data;
size_t len = str.size;
size_t peek = *i; // Local cursor
bool no_username;
bool no_password;
size_t username_off;
size_t username_end;
size_t password_off;
size_t password_len;
// If the userinfo subcomponent is present,
// set [no_username] to false, [username_off]
// to the offset of the username relative to
// [src] and [username_end] to its end.
// If the password was also specified, do the
// same.
{
if (peek == len || !is_username_first(src[peek])) {
// The first character can't be the first
// of an username, therefore there's no
// userinfo subcomponent.
no_username = true;
no_password = true;
} else {
// The first character is a valid start
// for an username, therefore we'll assume
// that there's a userinfo subcomponent
// until proven otherwise.
// Scan the username while keeping track
// of it's offset and length.
username_off = peek;
do
peek++;
while (peek < len && is_username(src[peek]));
username_end = peek;
// If the username is followed by a ':' and
// a valid password character, we also expect
// a password.
if (peek+1 < len && src[peek] == ':' && is_password_first(src[peek+1])) {
peek++; // Skip the ':'
// Scan the password while keeping track
// of it's offset and length.
password_off = peek;
do
peek++;
while (peek < len && is_password(src[peek]));
password_len = peek;
if (peek == len || src[peek] != '@') {
// If the password isn't followed by a '@',
// then this wasn't a userinfo subcomponent
// after all.
no_username = true;
no_password = true;
} else {
// All done.
no_username = false;
no_password = false;
peek++; // Skip the '@'
}
} else if (peek == len || src[peek] != '@') {
// Since no password was specified, if the
// username isn't followed by a '@', then
// this wasn't a userinfo subcomponent after
// all.
no_username = true;
no_password = true;
} else {
// All done.
no_username = false;
no_password = true;
peek++; // Skip the '@'
}
}
}
// Update the output parameters.
if (no_username) {
assert(no_password);
userinfo->username = NULLSTR;
userinfo->password = NULLSTR;
// Don't update [i]
} else {
userinfo->username = substr(str, username_off, username_end);
userinfo->password = no_password ? NULLSTR : substr(str, password_off, password_len);
*i = peek; // Commit changes.
}
}
static bool
parse_ipv4_byte(string str, size_t *i, uint8_t *out)
{
char *src = str.data;
size_t len = str.size;
size_t peek = *i;
// If the cursor refers to a digit, then
// there's a byte to parse.
bool start_with_digit = (peek < len && is_digit(src[peek]));
if (start_with_digit) {
uint8_t byte = 0;
// TODO: Don't allow arbitrary sequence of
// 0s at the start.
do {
int d = src[peek] - '0';
if (byte > (UINT8_MAX - d) / 10)
break; // Overflow! This digit isn't part of the byte.
byte = byte * 10 + d;
peek++;
} while (peek < len && is_digit(src[peek]));
*i = peek;
*out = byte;
}
return start_with_digit;
}
static uint32_t pack(uint8_t *bytes)
{
return
((uint32_t) bytes[0] << 24) |
((uint32_t) bytes[1] << 16) |
((uint32_t) bytes[2] << 8) |
((uint32_t) bytes[3] << 0);
}
static bool parse_ipv4(string str, size_t *i, uint32_t *ipv4)
{
char *src = str.data;
size_t len = str.size;
size_t peek = *i;
uint8_t unpacked_ipv4[4];
for (int u = 0; u < 3; u++) {
if (!parse_ipv4_byte(str, &peek, unpacked_ipv4 + u))
return false;
if (peek == len || src[peek] != '.')
return false;
peek++; // Skip the dot
}
if (!parse_ipv4_byte(str, &peek, unpacked_ipv4 + 3))
return false;
*ipv4 = pack(unpacked_ipv4);
*i = peek;
return true;
}
static int hex_digit_to_int(char c)
{
assert(is_hex_digit(c));
if (is_lower_alpha(c))
return c - 'a' + 10;
if (is_upper_alpha(c))
return c - 'A' + 10;
assert(is_digit(c));
return c - '0';
}
static bool parse_ipv6_word(string str, size_t *i, uint16_t *out)
{
char *src = str.data;
size_t len = str.size;
size_t peek = *i;
// If there's at least one hex digit at the
// current position, then we can parse a word
// for sure.
bool start_with_hex_digit = (peek < len && is_hex_digit(src[peek]));
if (start_with_hex_digit) {
uint16_t word = 0;
// TODO: Don't allow arbitrary sequence of
// 0s at the start.
do {
int d = hex_digit_to_int(src[peek]);
if (word > (UINT16_MAX - d) / 16)
break; // Overflow! This hex digit isn't part of the word.
word = word * 16 + d;
peek++;
} while (peek < len && is_hex_digit(src[peek]));
*i = peek;
*out = word;
}
return start_with_hex_digit;
}
static bool parse_ipv6(string str, size_t *i, uint16_t ipv6[static 8])
{
char *src = str.data;
size_t len = str.size;
size_t k = *i;
uint16_t tail[8];
size_t head_count = 0;
size_t tail_count = 0;
if (k+1 < len && src[k] == ':' && src[k+1] == ':')
k += 2;
else {
while (1) {
uint16_t word;
if (!parse_ipv6_word(str, &k, &word))
return false;
ipv6[head_count++] = word;
if (head_count == 8)
break;
if (k == len || src[k] != ':')
return false;
k++; // Skip the ':'
if (k < len && src[k] == ':') {
k++;
break;
}
}
}
if (head_count + tail_count < 8) {
while (k < len && is_hex_digit(src[k])) {
// We know the current character is a
// hex digit, therefore [parse_ipv6_word]
// won't fail.
uint16_t word;
(void) parse_ipv6_word(str, &k, &word);
tail[tail_count++] = word;
if (head_count + tail_count == 7)
break;
if (k == len || src[k] != ':')
break;
k++; // Skip the ':'
}
}
for (size_t p = 0; p < 8 - head_count - tail_count; p++)
ipv6[head_count + p] = 0;
for (size_t p = 0; p < tail_count; p++)
ipv6[8 - tail_count + p] = tail[p];
*i = k;
return true;
}
static void parse_port(string str, size_t *i, bool *no_port, uint16_t *port)
{
char *src = str.data;
size_t len = str.size;
size_t k = *i;
if (k+1 < len && src[k] == ':' && is_digit(src[k+1])) {
k++; // Skip the ':'
uint16_t p = 0;
do {
int d = src[k] - '0';
if (p > (UINT16_MAX - d) / 10)
break;
p = p * 10 + d;
k++;
} while (k < len && is_digit(src[k]));
*port = p;
*no_port = false;
} else {
*port = 0;
*no_port = true;
}
*i = k;
}
static bool is_hostname(char c)
{
return is_unreserved(c) || is_subdelim(c);
}
static bool is_hostname_first(char c)
{
return is_hostname(c);
}
static bool parse_host(string str, size_t *i, url_host *host)
{
char *src = str.data;
size_t len = str.size;
size_t k = *i;
if (k == len)
return false;
if (src[k] == '[') {
k++; // Skip the '['
// IPv6
if (!parse_ipv6(str, &k, host->ipv6))
return false;
if (k == len || src[k] != ']')
return false;
k++; // Skip the ']'
host->mode = URL_HOSTMODE_IPV6;
} else {
uint32_t ipv4;
bool is_ipv4;
if (is_digit(src[k]))
is_ipv4 = parse_ipv4(str, &k, &ipv4);
else
is_ipv4 = false;
if (is_ipv4) {
host->ipv4 = ipv4;
host->mode = URL_HOSTMODE_IPV4;
} else {
if (!is_hostname_first(src[k]))
return false;
size_t name_off = k;
do
k++;
while (k < len && is_hostname(src[k]));
size_t name_end = k;
host->mode = URL_HOSTMODE_NAME;
host->name = substr(str, name_off, name_end);
}
}
uint16_t port;
bool no_port;
parse_port(str, &k, &no_port, &port);
host->port = port;
host->no_port = no_port;
*i = k;
return true;
}
static bool parse_path(string str, size_t *i, string *path)
{
char *src = str.data;
size_t len = str.size;
size_t k = *i;
size_t path_off;
path_off = k;
if (k < len && src[k] == '/')
k++;
else {
if (k == len || !is_pchar(src[k]))
return false;
}
while (k < len && is_pchar(src[k])) {
do
k++;
while (k < len && is_pchar(src[k]));
if (k == len || src[k] != '/')
break;
k++; // Skip the '/'
}
*i = k;
*path = substr(str, path_off, k);
return true;
}
static bool is_query(char c)
{
return is_pchar(c) || c == '/' || c == '?';
}
static void parse_query(string str, size_t *i, string *query)
{
char *src = str.data;
size_t len = str.size;
bool no_query;
size_t query_off;
size_t query_end;
size_t peek = *i;
if (peek < len && src[peek] == '?') {
peek++; // Skip the '?'
query_off = peek;
while (peek < len && is_query(src[peek]))
peek++;
query_end = peek;
no_query = false;
} else {
query_off = 0;
query_end = 0;
no_query = true;
}
if (no_query)
*query = NULLSTR;
else {
*i = peek;
*query = substr(str, query_off, query_end);
}
}
static bool is_fragment(char c)
{
return is_pchar(c) || c == '/';
}
static void parse_fragment(string str, size_t *i, string *fragment)
{
char *src = str.data;
size_t len = str.size;
bool no_fragment;
size_t fragment_off;
size_t fragment_end;
size_t peek = *i;
if (peek < len && src[peek] == '#') {
peek++; // Skip the '#'
fragment_off = peek;
while (peek < len && is_fragment(src[peek]))
peek++;
fragment_end = peek;
no_fragment = false;
} else {
fragment_off = 0;
fragment_end = 0;
no_fragment = true;
}
if (no_fragment)
*fragment = NULLSTR;
else {
*i = peek;
*fragment = substr(str, fragment_off, fragment_end);
}
}
static bool follows_authority(string str, size_t i)
{
return i+1 < str.size
&& str.data[i] == '/'
&& str.data[i+1] == '/';
}
bool url_parse2(string str, size_t *i, url_t *url)
{
char *src = str.data;
size_t len = str.size;
size_t maybe;
if (i == NULL) {
maybe = 0;
i = &maybe;
}
parse_schema(str, i, &url->schema);
if (follows_authority(str, *i)) {
*i += 2; // Skip the "//"
parse_userinfo(str, i, &url->userinfo);
if (!parse_host(str, i, &url->host))
return false;
if (*i < len && src[*i] == '/') {
/* absolute path */
// The parsing of the path can't fail
// because we already know there's at
// leat a '/' for it.
(void) parse_path(str, i, &url->path);
} else
url->path = NULLSTR;
} else {
url->host.mode = URL_HOSTMODE_NAME;
url->host.name = NULLSTR;
url->host.no_port = true;
url->host.port = 0;
url->userinfo.username = NULLSTR;
url->userinfo.password = NULLSTR;
// TODO: Since there was no authority,
// the path is non optional.
if (*i == len || src[*i] == '?' || src[*i] == '#')
return false; // Missing path
if (!parse_path(str, i, &url->path))
return false;
}
parse_query(str, i, &url->query);
parse_fragment(str, i, &url->fragment);
return true;
}
bool url_parse(string str, url_t *url)
{
size_t i = 0;
bool result = url_parse2(str, &i, url);
return result && i == str.size;
}
bool url_parse_ipv4(string str, uint32_t *out)
{
size_t i = 0;
return parse_ipv4(str, &i, out);
}
bool url_parse_ipv6(string str, uint16_t out[8])
{
size_t i = 0;
return parse_ipv6(str, &i, out);
}
///////////////////////////////////////////////////////////////////////////////////////////////
/// URI PARSER ///
///////////////////////////////////////////////////////////////////////////////////////////////
bool hex_to_num(char x, int *n)
{
// TODO: This is inefficient
if (x >= 'A' && x <= 'F') *n = x - 'A';
else if (x >= 'a' && x <= 'f') *n = x - 'a';
else if (x >= '0' && x <= '9') *n = x - '0';
else return false;
return true;
}
size_t skip_any_valid_chars_not_percent_encoded(string src, size_t cur)
{
while (cur < src.size
&& src.data[cur] != '=' // Start of the value
&& src.data[cur] != '&' // End of the parameter (no value)
&& src.data[cur] != '%' // The name is percent encoded
&& src.data[cur] != '+' // Also percent encoded
&& is_print(src.data[cur])) // Query stirng contains invalid chracters
cur++;
return cur;
}
// This function is like the "parse" version but doesn't
// return the decoded string.
bool skip_percent_encoded_substr(string src, size_t *cur_)
{
size_t cur = *cur_;
cur = skip_any_valid_chars_not_percent_encoded(src, cur);
while (cur < src.size && (src.data[cur] == '+' || src.data[cur] == '%')) {
if (src.data[cur] == '+')
cur++;
else {
int u, v;
if (cur+2 >= src.size
|| !hex_to_num(src.data[cur+1], &u)
|| !hex_to_num(src.data[cur+2], &v))
return false; // Invalid percent encoding
cur += 3;
}
cur = skip_any_valid_chars_not_percent_encoded(src, cur);
}
*cur_ = cur;
return true;
}
// Percent decodes the source string until a query string
// separator ('=', '&') or an invalid character. The resulting
// slice (out) may point into the source string or the
// destination buffer (dst). It only returns false if copying
// was required and the buffer was too small.
bool parse_percent_encoded_substr(string src, string dst, size_t *cur_, string *out)
{
size_t cur = *cur_;
size_t start = cur;
cur = skip_any_valid_chars_not_percent_encoded(src, cur);
// Use secondary pointer so the key to avoid pointing
// into the source buffer. If we found something to
// decode (eg a %xx token) we'll need to copy the key
// into the destination buffer and make this pointer
// refer to that.
*out = substr(src, start, cur);
// If the name is percent encoded, copying into
// a seprate buffer to decode it is necessary
if (cur < src.size && (src.data[cur] == '%' || src.data[cur] == '+')) {
// Copy what was already scanned into the
// output buffer.
if (out->size > dst.size)
return -1; // Output buffer is too small
memcpy(dst.data, out->data, out->size);
size_t copied = out->size;
do {
assert(cur < src.size);
if (src.data[cur] == '+') {
if (copied == dst.size) return false; // Output buffer is too small
dst.data[copied++] = ' ';
cur++;
} else {
// We know there is a '%' at this point
int u, v;
if (cur+2 >= src.size
|| !hex_to_num(src.data[cur+1], &u)
|| !hex_to_num(src.data[cur+2], &v))
return false; // Invalid percent encoding
cur += 3;
assert(u >= 0 && u < 16);
assert(v >= 0 && v < 16);
uint8_t byte = (u << 4) | v;
if (copied == dst.size) return false; // Output buffer is too small
dst.data[copied++] = (char) byte;
}
// Skip all plain characters and memcopy them at once
start = cur;
cur = skip_any_valid_chars_not_percent_encoded(src, cur);
if (copied + cur - start > dst.size) return false; // Buffer is too small
memcpy(dst.data + copied, src.data + start, cur - start);
copied += cur - start;
} while (cur < src.size && (src.data[cur] == '%' || src.data[cur] == '+'));
out->data = dst.data;
out->size = copied;
}
*cur_ = cur;
return true;
}
bool get_query_string_param(string str, string key, string dst, string *out)
{
assert(key.size);
size_t cur = 0;
if (cur < str.size && str.data[cur] == '?')
cur++;
while (cur < str.size) {
string key2;
if (!parse_percent_encoded_substr(str, dst, &cur, &key2))
return false; // Buffer too small
if (cur < str.size && !is_print(str.data[cur]))
return false; // Invalid query string
if (key2.size == key.size && !memcmp(key2.data, key.data, key2.size)) {
if (cur < str.size && str.data[cur] == '=') {
cur++; // =
if (!parse_percent_encoded_substr(str, dst, &cur, out))
return false;
} else {
out->data = "";
out->size = 0;
}
return true;
} else {
if (cur < str.size && str.data[cur] == '=') {
cur++; // =
if (!skip_percent_encoded_substr(str, &cur))
return false;
}
}
if (cur < str.size && str.data[cur] != '&')
return false;
cur++; // &
}
return false;
}
///////////////////////////////////////////////////////////////////////////////////////////////
/// COOKIE PARSER ///
///////////////////////////////////////////////////////////////////////////////////////////////
bool is_cookie_name(char c)
{
return (c >= 'a' && c <= 'z')
|| (c >= 'A' && c <= 'Z')
|| (c >= '0' && c <= '9')
|| c == '_' || c == '-';
}
bool is_cookie_value(char c)
{
return (c >= 'a' && c <= 'z')
|| (c >= 'A' && c <= 'Z')
|| (c >= '0' && c <= '9')
|| c == '_' || c == '-';
}
bool get_cookie(Request *request, string name, string *out)
{
string value;
if (!find_header(request, LIT("Cookie"), &value))
return false;
char *src = value.data;
size_t len = value.size;
size_t cur = 0;
while (cur < len) {
size_t start;
while (cur < len && is_space(src[cur]))
cur++;
if (cur == len)
break;
if (!is_cookie_name(src[cur]))
return false;
start = cur;
do
cur++;
while (cur < len && is_cookie_name(src[cur]));
string cookie_name = substr(value, start, cur);
while (cur < len && is_space(src[cur]))
cur++;
if (cur == len || src[cur] != '=')
return false;
cur++;
while (cur < len && is_space(src[cur]))
cur++;
if (!is_cookie_value(src[cur]))
return false;
start = cur;
do
cur++;
while (cur < len && is_cookie_value(src[cur]));
string cookie_value = substr(value, start, cur);
while (cur < len && is_space(src[cur]))
cur++;
if (cur < len) {
if (src[cur] != ';')
return false;
cur++;
}
if (streq(cookie_name, name)) {
*out = cookie_value;
return true;
}
}
return false;
}
///////////////////////////////////////////////////////////////////////////////////////////////
/// TEMPLATE EVALUATION ///
///////////////////////////////////////////////////////////////////////////////////////////////
typedef struct {
ResponseBuilder *b;
TemplateParam *params;
} TemplateContext;
void template_output_callback(void *userp, const char *str, size_t len)
{
TemplateContext *c = userp;
append_content_s(c->b, (string) {str, len});
}
bool template_sqlstmt_param_callback(void *data, const char *key_, size_t len, tinytemplate_value_t *value)
{
string key = {key_, len};
sqlite3_stmt *stmt = data;
int column_count = sqlite3_column_count(stmt);
bool found = false;
for (int i = 0; i < column_count; i++) {
const char *tmp = sqlite3_column_name(stmt, i);
string column_name = STR(tmp);
if (streq(column_name, key)) {
switch (sqlite3_column_type(stmt, i)) {
case SQLITE_INTEGER: tinytemplate_set_int(value, sqlite3_column_int(stmt, i)); break;
case SQLITE_FLOAT : tinytemplate_set_float(value, sqlite3_column_double(stmt, i)); break;
case SQLITE_TEXT : tinytemplate_set_string(value, (char*) sqlite3_column_text(stmt, i), sqlite3_column_bytes(stmt, i)); break;
case SQLITE_BLOB : log_fatal(LIT("Can't provide a BLOB column to a template")); break;
case SQLITE_NULL : log_fatal(LIT("Can't provide a NULL column to a template")); break;
}
found = true;
break;
}
}
return found;
}
bool template_next_callback(void *userp, tinytemplate_value_t *value)
{
sqlite3_stmt *stmt = userp;
int res = sqlite3_step(stmt);
if (res != SQLITE_ROW) {
sqlite3_reset(stmt);
return false;
}
tinytemplate_set_dict(value, stmt, template_sqlstmt_param_callback);
return true;
}
bool template_param_callback(void *userp, const char *key, size_t len, tinytemplate_value_t *value)
{
TemplateContext *c = userp;
string param = {.data=key, .size=len};
for (int i = 0; c->params[i].type != TPT_LAST; i++) {
if (streq(param, c->params[i].name)) {
switch (c->params[i].type) {
case TPT_INT : tinytemplate_set_int (value, c->params[i].i); break;
case TPT_FLOAT : tinytemplate_set_float (value, c->params[i].f); break;
case TPT_STRING: tinytemplate_set_string(value, c->params[i].s.data, c->params[i].s.size); break;
case TPT_QUERY : tinytemplate_set_array (value, c->params[i].q, template_next_callback); break;
case TPT_LAST : assert(0); break;
}
return true;
}
}
return false;
}
bool append_template(ResponseBuilder *b, string file, TemplateParam *params)
{
tinytemplate_status_t status;
tinytemplate_instr_t program[1<<9];
size_t num_instr;
char errmsg[1<<9];
string template_str;
if (!load_file_contents(file, &template_str))
return false;
status = tinytemplate_compile(template_str.data, template_str.size, program, COUNTOF(program), &num_instr, errmsg, sizeof(errmsg));
if (status != TINYTEMPLATE_STATUS_DONE) {
log_data(STR(errmsg));
myfree(template_str.data, template_str.size);
return false;
}
TemplateContext context;
context.b = b;
context.params = params;
status = tinytemplate_eval(template_str.data, program, &context, template_param_callback, template_output_callback, errmsg, sizeof(errmsg));
if (status != TINYTEMPLATE_STATUS_DONE)
log_data(STR(errmsg));
myfree(template_str.data, template_str.size);
return true;
}
///////////////////////////////////////////////////////////////////////////////////////////////
/// DATABASE ///
///////////////////////////////////////////////////////////////////////////////////////////////
int sqlite3_utils_fetch(sqlite3_stmt *stmt, char *types, ...)
{
va_list args;
va_start(args, types);
int step = sqlite3_step(stmt);
if (step == SQLITE_DONE)
return 1; // No more rows
if (step != SQLITE_ROW)
return -1; // Error occurred
// Have a row
for (int i = 0; types[i]; i++) {
switch (types[i]) {
case 'x':
*va_arg(args, const void**) = sqlite3_column_blob(stmt, i);
*va_arg(args, size_t*) = sqlite3_column_bytes(stmt, i);
break;
case 's':
{
string *dst = va_arg(args, string*);
dst->data = sqlite3_column_text(stmt, i);
dst->size = sqlite3_column_bytes(stmt, i);;
}
break;
case 'i': *va_arg(args, int*) = sqlite3_column_int(stmt, i); break;
default: va_end(args); return -1;
}
}
va_end(args);
return 0;
}
static sqlite3_stmt *vprep(sqlite3 *handle, const char *fmt, va_list args)
{
char buffer[1 << 10];
size_t copied = 0;
char params[8]; // The size of this buffer determines the maximum
// number of parameters in a prepared query
int num_params = 0;
const char *stmt_str;
size_t stmt_len;
size_t len = strlen(fmt);
size_t cur = 0;
while (cur < len && fmt[cur] != ':')
cur++;
if (cur == len) {
stmt_str = fmt;
stmt_len = len;
} else {
// The cursor refers to the first ':'
if (cur >= sizeof(buffer)) {
log_data(LIT("Statement text buffer is too small\n"));
return NULL;
}
memcpy(buffer, fmt, cur);
copied = cur;
do {
assert(fmt[cur] == ':');
cur++;
if (cur == len) {
log_data(LIT("Missing type specifier after ':'\n"));
return NULL;
}
char t = fmt[cur];
if (t != 'i' && t != 's' && t != 'x') {
log_format("Invalid type specifier '%c'\n", t);
return NULL;
}
cur++;
if (num_params == COUNTOF(params)) {
log_format("Parameter limit reached (%d)\n", COUNTOF(params));
return NULL;
}
params[num_params++] = t;
if (copied+1 >= sizeof(buffer)) {
log_data(LIT("Statement text buffer is too small\n"));
return NULL;
}
buffer[copied++] = '?';
size_t save = cur;
while (cur < len && fmt[cur] != ':')
cur++;
size_t copying = cur - save;
if (copied + copying >= sizeof(buffer)) {
log_data(LIT("Statement text buffer is too small\n"));
return NULL;
}
memcpy(buffer + copied, fmt + save, copying);
copied += copying;
} while (cur < len);
assert(copied < sizeof(buffer));
buffer[copied] = '\0';
stmt_str = buffer;
stmt_len = copied;
}
DEBUG("SQL: %.*s\n", (int) stmt_len, stmt_str);
sqlite3_stmt *stmt;
int code = sqlite3_prepare_v2(handle, stmt_str, stmt_len, &stmt, 0);
if (code != SQLITE_OK) {
log_format("Failed to prepare SQL statement (sqlite3: %s)\n", sqlite3_errmsg(handle));
return NULL;
}
for (int i = 0; i < num_params; i++) {
int code;
switch (params[i]) {
case 'i':
{
int v = va_arg(args, int);
DEBUG("binding param %d to int %d\n", i+1, v);
code = sqlite3_bind_int (stmt, i+1, v);
}
break;
case 's':
{
string str = va_arg(args, string);
DEBUG("binding param %d to str %.*s\n", i+1, (int) str.size, str.data);
code = sqlite3_bind_text(stmt, i+1, str.data, str.size, NULL);
}
break;
case 'x':
{
void *ptr = va_arg(args, void*);
size_t len = va_arg(args, size_t);
DEBUG("binding param %d to blob %p %d\n", i+1, ptr, (int) len);
code = sqlite3_bind_blob(stmt, i+1, ptr, len, NULL);
}
break;
}
if (code != SQLITE_OK) {
log_format("Failed to bind parameter %d to SQL statement (sqlite3: %s)\n", i+1, sqlite3_errmsg(handle));
sqlite3_finalize(stmt);
return NULL;
}
}
return stmt;
}
sqlite3_stmt *sqlite3_utils_prepare(sqlite3 *handle, const char *fmt, ...)
{
sqlite3_stmt *stmt;
va_list args;
va_start(args, fmt);
stmt = vprep(handle, fmt, args);
va_end(args);
return stmt;
}
bool sqlite3_utils_exec(sqlite3 *handle, const char *fmt, ...)
{
sqlite3_stmt *stmt;
va_list args;
va_start(args, fmt);
stmt = vprep(handle, fmt, args);
va_end(args);
if (stmt == NULL)
return false;
if (sqlite3_step(stmt) != SQLITE_DONE) {
log_format("Failed to execute SQL statement (sqlite3: %s)\n", sqlite3_errmsg(handle));
sqlite3_finalize(stmt);
return false;
}
sqlite3_finalize(stmt);
return true;
}
int sqlite3_utils_rows_exist(sqlite3 *handle, const char *fmt, ...)
{
sqlite3_stmt *stmt;
va_list args;
va_start(args, fmt);
stmt = vprep(handle, fmt, args);
va_end(args);
if (stmt == NULL)
return -1;
int step = sqlite3_step(stmt);
if (step == SQLITE_DONE) {
sqlite3_finalize(stmt);
return 1; // No rows exist
}
if (step == SQLITE_ROW) {
sqlite3_finalize(stmt);
return 0; // Rows exist
}
log_format("Failed to execute SQL statement (sqlite3: %s)\n", sqlite3_errmsg(handle));
sqlite3_finalize(stmt);
return -1;
}
///////////////////////////////////////////////////////////////////////////////////////////////
/// CERTIFICATE AND PRIVATE KEY PARSING (Adapted from BearSSL) ///
///////////////////////////////////////////////////////////////////////////////////////////////
/// Copyright (c) 2016 Thomas Pornin <pornin@bolet.org> ///
/// ///
/// Permission is hereby granted, free of charge, to any person obtaining ///
/// a copy of this software and associated documentation files (the ///
/// "Software"), to deal in the Software without restriction, including ///
/// without limitation the rights to use, copy, modify, merge, publish, ///
/// distribute, sublicense, and/or sell copies of the Software, and to ///
/// permit persons to whom the Software is furnished to do so, subject to ///
/// the following conditions: ///
/// ///
/// The above copyright notice and this permission notice shall be ///
/// included in all copies or substantial portions of the Software. ///
/// ///
/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, ///
/// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF ///
/// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND ///
/// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS ///
/// BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ///
/// ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN ///
/// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE ///
/// SOFTWARE. ///
///////////////////////////////////////////////////////////////////////////////////////////////
#if HTTPS
BearSSLErrorInfo bearssl_error_table[] = {
{ BR_ERR_BAD_PARAM, LIT("BR_ERR_BAD_PARAM"), LIT("Caller-provided parameter is incorrect.") },
{ BR_ERR_BAD_STATE, LIT("BR_ERR_BAD_STATE"), LIT("Operation requested by the caller cannot be applied with the current context state (e.g. reading data while outgoing data is waiting to be sent).") },
{ BR_ERR_UNSUPPORTED_VERSION, LIT("BR_ERR_UNSUPPORTED_VERSION"), LIT("Incoming protocol or record version is unsupported.") },
{ BR_ERR_BAD_VERSION, LIT("BR_ERR_BAD_VERSION"), LIT("Incoming record version does not match the expected version.") },
{ BR_ERR_BAD_LENGTH, LIT("BR_ERR_BAD_LENGTH"), LIT("Incoming record length is invalid.") },
{ BR_ERR_TOO_LARGE, LIT("BR_ERR_TOO_LARGE"), LIT("Incoming record is too large to be processed, or buffer is too small for the handshake message to send.") },
{ BR_ERR_BAD_MAC, LIT("BR_ERR_BAD_MAC"), LIT("Decryption found an invalid padding, or the record MAC is not correct.") },
{ BR_ERR_NO_RANDOM, LIT("BR_ERR_NO_RANDOM"), LIT("No initial entropy was provided, and none can be obtained from the OS.") },
{ BR_ERR_UNKNOWN_TYPE, LIT("BR_ERR_UNKNOWN_TYPE"), LIT("Incoming record type is unknown.") },
{ BR_ERR_UNEXPECTED, LIT("BR_ERR_UNEXPECTED"), LIT("Incoming record or message has wrong type with regards to the current engine state.") },
{ BR_ERR_BAD_CCS, LIT("BR_ERR_BAD_CCS"), LIT("ChangeCipherSpec message from the peer has invalid contents.") },
{ BR_ERR_BAD_ALERT, LIT("BR_ERR_BAD_ALERT"), LIT("Alert message from the peer has invalid contents (odd length).") },
{ BR_ERR_BAD_HANDSHAKE, LIT("BR_ERR_BAD_HANDSHAKE"), LIT("Incoming handshake message decoding failed.") },
{ BR_ERR_OVERSIZED_ID, LIT("BR_ERR_OVERSIZED_ID"), LIT("ServerHello contains a session ID which is larger than 32 bytes.") },
{ BR_ERR_BAD_CIPHER_SUITE, LIT("BR_ERR_BAD_CIPHER_SUITE"), LIT("Server wants to use a cipher suite that we did not claim to support. This is also reported if we tried to advertise a cipher suite that we do not support.") },
{ BR_ERR_BAD_COMPRESSION, LIT("BR_ERR_BAD_COMPRESSION"), LIT("Server wants to use a compression that we did not claim to support.") },
{ BR_ERR_BAD_FRAGLEN, LIT("BR_ERR_BAD_FRAGLEN"), LIT("Server's max fragment length does not match client's.") },
{ BR_ERR_BAD_SECRENEG, LIT("BR_ERR_BAD_SECRENEG"), LIT("Secure renegotiation failed.") },
{ BR_ERR_EXTRA_EXTENSION, LIT("BR_ERR_EXTRA_EXTENSION"), LIT("Server sent an extension type that we did not announce, or used the same extension type several times in a single ServerHello.") },
{ BR_ERR_BAD_SNI, LIT("BR_ERR_BAD_SNI"), LIT("Invalid Server Name Indication contents (when used by the server, this extension shall be empty).") },
{ BR_ERR_BAD_HELLO_DONE, LIT("BR_ERR_BAD_HELLO_DONE"), LIT("Invalid ServerHelloDone from the server (length is not 0).") },
{ BR_ERR_LIMIT_EXCEEDED, LIT("BR_ERR_LIMIT_EXCEEDED"), LIT("Internal limit exceeded (e.g. server's public key is too large).") },
{ BR_ERR_BAD_FINISHED, LIT("BR_ERR_BAD_FINISHED"), LIT("Finished message from peer does not match the expected value.") },
{ BR_ERR_RESUME_MISMATCH, LIT("BR_ERR_RESUME_MISMATCH"), LIT("Session resumption attempt with distinct version or cipher suite.") },
{ BR_ERR_INVALID_ALGORITHM, LIT("BR_ERR_INVALID_ALGORITHM"), LIT("Unsupported or invalid algorithm (ECDHE curve, signature algorithm, hash function).") },
{ BR_ERR_BAD_SIGNATURE, LIT("BR_ERR_BAD_SIGNATURE"), LIT("Invalid signature in ServerKeyExchange or CertificateVerify message.") },
{ BR_ERR_WRONG_KEY_USAGE, LIT("BR_ERR_WRONG_KEY_USAGE"), LIT("Peer's public key does not have the proper type or is not allowed for the requested operation.") },
{ BR_ERR_NO_CLIENT_AUTH, LIT("BR_ERR_NO_CLIENT_AUTH"), LIT("Client did not send a certificate upon request, or the client certificate could not be validated.") },
{ BR_ERR_IO, LIT("BR_ERR_IO"), LIT("I/O error or premature close on transport stream.") },
{ BR_ERR_X509_INVALID_VALUE, LIT("BR_ERR_X509_INVALID_VALUE"), LIT("Invalid value in an ASN.1 structure.") },
{ BR_ERR_X509_TRUNCATED, LIT("BR_ERR_X509_TRUNCATED"), LIT("Truncated certificate or other ASN.1 object.") },
{ BR_ERR_X509_EMPTY_CHAIN, LIT("BR_ERR_X509_EMPTY_CHAIN"), LIT("Empty certificate chain (no certificate at all).") },
{ BR_ERR_X509_INNER_TRUNC, LIT("BR_ERR_X509_INNER_TRUNC"), LIT("Decoding error: inner element extends beyond outer element size.") },
{ BR_ERR_X509_BAD_TAG_CLASS, LIT("BR_ERR_X509_BAD_TAG_CLASS"), LIT("Decoding error: unsupported tag class (application or private).") },
{ BR_ERR_X509_BAD_TAG_VALUE, LIT("BR_ERR_X509_BAD_TAG_VALUE"), LIT("Decoding error: unsupported tag value.") },
{ BR_ERR_X509_INDEFINITE_LENGTH, LIT("BR_ERR_X509_INDEFINITE_LENGTH"), LIT("Decoding error: indefinite length.") },
{ BR_ERR_X509_EXTRA_ELEMENT, LIT("BR_ERR_X509_EXTRA_ELEMENT"), LIT("Decoding error: extraneous element.") },
{ BR_ERR_X509_UNEXPECTED, LIT("BR_ERR_X509_UNEXPECTED"), LIT("Decoding error: unexpected element.") },
{ BR_ERR_X509_NOT_CONSTRUCTED, LIT("BR_ERR_X509_NOT_CONSTRUCTED"), LIT("Decoding error: expected constructed element, but is primitive.") },
{ BR_ERR_X509_NOT_PRIMITIVE, LIT("BR_ERR_X509_NOT_PRIMITIVE"), LIT("Decoding error: expected primitive element, but is constructed.") },
{ BR_ERR_X509_PARTIAL_BYTE, LIT("BR_ERR_X509_PARTIAL_BYTE"), LIT("Decoding error: BIT STRING length is not multiple of 8.") },
{ BR_ERR_X509_BAD_BOOLEAN, LIT("BR_ERR_X509_BAD_BOOLEAN"), LIT("Decoding error: BOOLEAN value has invalid length.") },
{ BR_ERR_X509_OVERFLOW, LIT("BR_ERR_X509_OVERFLOW"), LIT("Decoding error: value is off-limits.") },
{ BR_ERR_X509_BAD_DN, LIT("BR_ERR_X509_BAD_DN"), LIT("Invalid distinguished name.") },
{ BR_ERR_X509_BAD_TIME, LIT("BR_ERR_X509_BAD_TIME"), LIT("Invalid date/time representation.") },
{ BR_ERR_X509_UNSUPPORTED, LIT("BR_ERR_X509_UNSUPPORTED"), LIT("Certificate contains unsupported features that cannot be ignored.") },
{ BR_ERR_X509_LIMIT_EXCEEDED, LIT("BR_ERR_X509_LIMIT_EXCEEDED"), LIT("Key or signature size exceeds internal limits.") },
{ BR_ERR_X509_WRONG_KEY_TYPE, LIT("BR_ERR_X509_WRONG_KEY_TYPE"), LIT("Key type does not match that which was expected.") },
{ BR_ERR_X509_BAD_SIGNATURE, LIT("BR_ERR_X509_BAD_SIGNATURE"), LIT("Signature is invalid.") },
{ BR_ERR_X509_TIME_UNKNOWN, LIT("BR_ERR_X509_TIME_UNKNOWN"), LIT("Validation time is unknown.") },
{ BR_ERR_X509_EXPIRED, LIT("BR_ERR_X509_EXPIRED"), LIT("Certificate is expired or not yet valid.") },
{ BR_ERR_X509_DN_MISMATCH, LIT("BR_ERR_X509_DN_MISMATCH"), LIT("Issuer/Subject DN mismatch in the chain.") },
{ BR_ERR_X509_BAD_SERVER_NAME, LIT("BR_ERR_X509_BAD_SERVER_NAME"), LIT("Expected server name was not found in the chain.") },
{ BR_ERR_X509_CRITICAL_EXTENSION, LIT("BR_ERR_X509_CRITICAL_EXTENSION"), LIT("Unknown critical extension in certificate.") },
{ BR_ERR_X509_NOT_CA, LIT("BR_ERR_X509_NOT_CA"), LIT("Not a CA, or path length constraint violation.") },
{ BR_ERR_X509_FORBIDDEN_KEY_USAGE, LIT("BR_ERR_X509_FORBIDDEN_KEY_USAGE"), LIT("Key Usage extension prohibits intended usage.") },
{ BR_ERR_X509_WEAK_PUBLIC_KEY, LIT("BR_ERR_X509_WEAK_PUBLIC_KEY"), LIT("Public key found in certificate is too small.") },
{ BR_ERR_X509_NOT_TRUSTED, LIT("BR_ERR_X509_NOT_TRUSTED"), LIT("Chain could not be linked to a trust anchor.") },
};
BearSSLErrorInfo get_bearssl_error_info(int code)
{
for (int i = 0; i < COUNTOF(bearssl_error_table); i++)
if (bearssl_error_table[i].code == code)
return bearssl_error_table[i];
BearSSLErrorInfo fallback;
fallback.code = code;
fallback.name = LIT("Unknown");
fallback.comment = LIT(":/");
return fallback;
}
typedef struct {
string name;
string content;
} PemObject;
typedef struct {
bool failed;
char *buffer;
size_t buffer_count;
size_t buffer_capacity;
} PemDecodeContext;
int is_ign(int c)
{
if (c == 0) {
return 0;
}
if (c <= 32 || c == '-' || c == '_' || c == '.'
|| c == '/' || c == '+' || c == ':')
{
return 1;
}
return 0;
}
// Get next non-ignored character, normalised:
// ASCII letters are converted to lowercase
// control characters, space, '-', '_', '.', '/', '+' and ':' are ignored
// A terminating zero is returned as 0.
char next_char(string *s)
{
size_t i = 0;
while (i < s->size && is_ign(s->data[i]))
i++;
char c;
if (i == s->size)
c = '\0';
else {
c = s->data[i++];
assert(c != '\0');
}
s->data += i;
s->size -= i;
return to_lower(c);
}
bool eqstr__(string a, string b)
{
for (;;) {
char c1 = next_char(&a);
char c2 = next_char(&b);
if (c1 != c2) return false;
if (c1 == 0) return true;
}
}
void append_bytes(void *userptr, const void *str, size_t len)
{
PemDecodeContext *context = userptr;
if (context->failed)
return;
if (context->buffer_capacity - context->buffer_count < len) {
size_t newcap = MAX(2 * context->buffer_capacity, context->buffer_count + len);
void *newstr = mymalloc(newcap);
if (newstr == NULL) {
context->failed = true;
return;
}
if (context->buffer) {
memcpy(newstr, context->buffer, context->buffer_count);
myfree(context->buffer, context->buffer_capacity);
}
context->buffer = newstr;
context->buffer_capacity = newcap;
}
memcpy(context->buffer + context->buffer_count, str, len);
context->buffer_count += len;
}
typedef struct {
PemObject *items;
int count;
int capacity;
} PemArray;
bool append_pem(PemArray *arr, PemObject obj)
{
if (arr->count == arr->capacity) {
int newcap = MAX(2 * arr->capacity, 4);
PemObject *newitems = mymalloc(newcap * sizeof(PemObject));
if (newitems == NULL)
return false;
if (arr->count)
memcpy(arr->items, newitems, arr->count * sizeof(PemObject));
myfree(arr->items, arr->capacity * sizeof(PemObject));
arr->items = newitems;
arr->capacity = newcap;
}
arr->items[arr->count++] = obj;
return true;
}
void free_pem_array(PemArray *arr)
{
for (int i = 0; i < arr->count; i++) {
string name = arr->items[i].name;
myfree(name.data, name.size);
string content = arr->items[i].content;
if (content.data)
myfree(content.data, content.size);
}
myfree(arr->items, arr->capacity * sizeof(PemObject));
}
bool decode_pem(string src, PemArray *array)
{
br_pem_decoder_context context;
br_pem_decoder_init(&context);
PemDecodeContext context2;
context2.failed = false;
context2.buffer = NULL;
context2.buffer_count = 0;
context2.buffer_capacity = 0;
array->items = NULL;
array->count = 0;
array->capacity = 0;
PemObject po;
po.name = NULLSTR;
po.content = NULLSTR;
bool inside_object = false;
bool extra_newline = true;
while (src.size > 0) {
size_t n = br_pem_decoder_push(&context, src.data, src.size);
src.data += n;
src.size -= n;
switch (br_pem_decoder_event(&context)) {
case BR_PEM_BEGIN_OBJ:
{
const char *name = br_pem_decoder_name(&context);
size_t name_len = strlen(name);
po.name.data = mymalloc(name_len);
po.name.size = name_len;
if (po.name.data == NULL) {
myfree(context2.buffer, context2.buffer_capacity);
free_pem_array(array);
return false;
}
memcpy(po.name.data, name, name_len);
br_pem_decoder_setdest(&context, append_bytes, &context2);
inside_object = true;
}
break;
case BR_PEM_END_OBJ:
if (inside_object) {
void *copy = mymalloc(context2.buffer_count);
if (copy == NULL) {
myfree(po.name.data, po.name.size);
myfree(context2.buffer, context2.buffer_capacity);
free_pem_array(array);
return false;
}
memcpy(copy, context2.buffer, context2.buffer_count);
po.content.data = copy;
po.content.size = context2.buffer_count;
if (!append_pem(array, po)) {
myfree(po.name.data, po.name.size);
myfree(context2.buffer, context2.buffer_capacity);
free_pem_array(array);
return false;
}
po.name = NULLSTR;
po.content = NULLSTR;
context2.buffer_count = 0;
inside_object = false;
}
break;
case BR_PEM_ERROR:
myfree(po.name.data, po.name.size);
myfree(context2.buffer, context2.buffer_capacity);
free_pem_array(array);
log_data(LIT("Invalid PEM"));
return false;
}
if (src.size == 0 && extra_newline) {
src.data = "\n";
src.size = 1;
extra_newline = false;
}
}
if (context2.buffer)
myfree(context2.buffer, context2.buffer_capacity);
if (context2.failed) {
myfree(po.name.data, po.name.size);
free_pem_array(array);
return false;
}
if (inside_object) {
myfree(po.name.data, po.name.size);
free_pem_array(array);
log_data(LIT("Unfinished PEM"));
return false;
}
return true;
}
int looks_like_DER(string content)
{
int fb;
size_t dlen;
if (content.size < 2) {
return 0;
}
if (*content.data ++ != 0x30) {
return 0;
}
fb = *content.data ++;
content.size -= 2;
if (fb < 0x80) {
return (size_t)fb == content.size;
} else if (fb == 0x80) {
return 0;
} else {
fb -= 0x80;
if (content.size < (size_t)fb + 2) {
return 0;
}
content.size -= (size_t)fb;
dlen = 0;
while (fb -- > 0) {
if (dlen > (content.size >> 8)) {
return 0;
}
dlen = (dlen << 8) + (size_t)*content.data ++;
}
return dlen == content.size;
}
}
bool decode_key(string src, PrivateKey *pkey)
{
br_skey_decoder_context context;
br_skey_decoder_init(&context);
br_skey_decoder_push(&context, src.data, src.size);
int err = br_skey_decoder_last_error(&context);
if (err) {
BearSSLErrorInfo error_info = get_bearssl_error_info(err);
log_format("Error decoding key: %.*s: (code=%d, %.*s)\n",
(int) error_info.name.size, error_info.name.data, err,
(int) error_info.comment.size, error_info.comment.data);
return false;
}
switch (br_skey_decoder_key_type(&context)) {
const br_rsa_private_key *rsa_key;
const br_ec_private_key *ec_key;
case BR_KEYTYPE_RSA:
{
rsa_key = br_skey_decoder_get_rsa(&context);
unsigned char *mem = mymalloc(rsa_key->plen + rsa_key->qlen + rsa_key->dplen + rsa_key->dqlen + rsa_key->iqlen);
if (mem == NULL) return false;
pkey->type = BR_KEYTYPE_RSA;
pkey->rsa.n_bitlen = rsa_key->n_bitlen;
pkey->rsa.p = mem;
pkey->rsa.q = mem + rsa_key->plen;
pkey->rsa.dp = mem + rsa_key->plen + rsa_key->qlen;
pkey->rsa.dq = mem + rsa_key->plen + rsa_key->qlen + rsa_key->dplen;
pkey->rsa.iq = mem + rsa_key->plen + rsa_key->qlen + rsa_key->dplen + rsa_key->dqlen;
memcpy(pkey->rsa.p, rsa_key->p, rsa_key->plen);
memcpy(pkey->rsa.q, rsa_key->q, rsa_key->qlen);
memcpy(pkey->rsa.dp, rsa_key->dp, rsa_key->dplen);
memcpy(pkey->rsa.dq, rsa_key->dq, rsa_key->dqlen);
memcpy(pkey->rsa.iq, rsa_key->iq, rsa_key->iqlen);
pkey->rsa.plen = rsa_key->plen;
pkey->rsa.qlen = rsa_key->qlen;
pkey->rsa.dplen = rsa_key->dplen;
pkey->rsa.dqlen = rsa_key->dqlen;
pkey->rsa.iqlen = rsa_key->iqlen;
}
break;
case BR_KEYTYPE_EC:
{
ec_key = br_skey_decoder_get_ec(&context);
pkey->type = BR_KEYTYPE_EC;
pkey->ec.curve = ec_key->curve;
pkey->ec.x = mymalloc(ec_key->xlen);
if (pkey->ec.x == NULL)
return false;
memcpy(pkey->ec.x, ec_key->x, ec_key->xlen);
pkey->ec.xlen = ec_key->xlen;
}
break;
default:
log_format("Unknown key type: %d\n", br_skey_decoder_key_type(&context));
return false;
}
return true;
}
bool load_private_key_from_file(string file, PrivateKey *pkey)
{
string file_contents;
if (!load_file_contents(file, &file_contents))
return false;
DEBUG("loading key: file contents loaded\n");
bool ok;
if (looks_like_DER(file_contents)) {
DEBUG("loading key: detected DER file\n");
ok = decode_key(file_contents, pkey);
} else {
DEBUG("loading key: detected PEM file\n");
PemArray pem_array;
if (!decode_pem(file_contents, &pem_array)) {
myfree(file_contents.data, file_contents.size);
return false;
}
bool found = false;
bool decoded = false;
for (int i = 0; i < pem_array.count; i++)
if (eqstr__(pem_array.items[i].name, LIT("RSA PRIVATE KEY"))
|| eqstr__(pem_array.items[i].name, LIT("EC PRIVATE KEY"))
|| eqstr__(pem_array.items[i].name, LIT("PRIVATE KEY"))) {
DEBUG("loading key: found key in PEM file\n");
if (decode_key(pem_array.items[i].content, pkey))
decoded = true;
found = true;
break;
}
ok = false;
if (!found)
log_data(LIT("Missing private key in file\n"));
else {
if (!decoded)
log_data(LIT("Couldn't decode key\n"));
else
ok = true;
}
free_pem_array(&pem_array);
}
myfree(file_contents.data, file_contents.size);
return ok;
}
void free_private_key(PrivateKey *pkey)
{
switch (pkey->type) {
case BR_KEYTYPE_RSA:
myfree(pkey->rsa.p, pkey->rsa.plen + pkey->rsa.qlen + pkey->rsa.dplen + pkey->rsa.dqlen + pkey->rsa.iqlen);
break;
case BR_KEYTYPE_EC:
myfree(pkey->ec.x, pkey->ec.xlen);
break;
}
}
bool append_cert(CertArray *arr, br_x509_certificate cert)
{
if (arr->count == arr->capacity) {
int newcap = MAX(2 * arr->capacity, 4);
br_x509_certificate *newitems = mymalloc(newcap * sizeof(br_x509_certificate));
if (newitems == NULL)
return false;
if (arr->count)
memcpy(arr->items, newitems, arr->count * sizeof(br_x509_certificate));
myfree(arr->items, arr->capacity * sizeof(br_x509_certificate));
arr->items = newitems;
arr->capacity = newcap;
}
arr->items[arr->count++] = cert;
return true;
}
bool load_certs_from_file(string file, CertArray *array)
{
string file_contents;
if (!load_file_contents(file, &file_contents))
return false;
DEBUG("loading certs: file contents loaded\n");
array->items = NULL;
array->count = 0;
array->capacity = 0;
if (looks_like_DER(file_contents)) {
DEBUG("loading certs: detected DER file\n");
br_x509_certificate xc = {
(unsigned char*) file_contents.data,
file_contents.size,
};
if (!append_cert(array, xc)) {
myfree(file_contents.data, file_contents.size);
return false;
}
DEBUG("loading certs: DER file parsed\n");
} else {
DEBUG("loading certs: detected PEM file\n");
PemArray pem_array;
if (!decode_pem(file_contents, &pem_array)) {
myfree(file_contents.data, file_contents.size);
return false;
}
DEBUG("loading certs: PEM file parsed (%d entries)\n", pem_array.count);
for (int i = 0; i < pem_array.count; i++) {
PemObject po = pem_array.items[i];
if (eqstr__(po.name, LIT("CERTIFICATE")) || eqstr__(po.name, LIT("X509 CERTIFICATE"))) {
DEBUG("loading certs: found certificate in PEM file\n");
br_x509_certificate xc = { (unsigned char*) po.content.data, po.content.size };
if (!append_cert(array, xc)) {
free_pem_array(&pem_array);
free_certs(array);
myfree(file_contents.data, file_contents.size);
return false;
}
pem_array.items[i].content = NULLSTR;
} else {
DEBUG("loading certs: ignoring entry [%.*s] in PEM file\n", (int) po.name.size, po.name.data);
}
}
DEBUG("loading certs: finished loading certificates from PEM file\n");
if (array->count == 0) {
free_pem_array(&pem_array);
free_certs(array);
myfree(file_contents.data, file_contents.size);
log_data(LIT("No certificates in file\n"));
return false;
}
free_pem_array(&pem_array);
myfree(file_contents.data, file_contents.size);
}
DEBUG("loading certs: certificate loaded\n");
return true;
}
void free_certs(CertArray *array)
{
for (int i = 0; i < array->count; i++) {
br_x509_certificate item = array->items[i];
myfree(item.data, item.data_len);
}
myfree(array->items, array->capacity * sizeof(br_x509_certificate));
}
#endif /* HTTPS */