/////////////////////////////////////////////////////////////////////////////////////////////// /// 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 /// /// /// /// NOTE: Some code was adapted from BearSSL. That code uses the MIT license. /// /////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////// /// HEADERS /// /////////////////////////////////////////////////////////////////////////////////////////////// #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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 #endif #if PROFILE #include #endif #if EOPALLOC #include #endif #if BACKTRACE #include #include #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); /////////////////////////////////////////////////////////////////////////////////////////////// /// 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 /// /////////////////////////////////////////////////////////////////////////////////////////////// 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; } 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 :|")); } /////////////////////////////////////////////////////////////////////////////////////////////// /// 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( "\n" " \n" " \n" " \n" "
    \n" "
  • (parent)
  • ")); // TODO: Add links struct dirent *dir; while ((dir = readdir(d))) { if (!strcmp(dir->d_name, ".") || !strcmp(dir->d_name, "..")) continue; append_content_f(b, "
  • %s
  • \n", dir->d_name); // TODO: Add links } append_content_s(b, LIT( "
\n" " \n" "\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 /// /// /// /// 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 */