Unpack serve.c into multiple files

This commit is contained in:
2024-10-06 20:14:44 +02:00
parent e8c8acf8c4
commit 0da93dd867
24 changed files with 2380 additions and 2284 deletions
+2
View File
@@ -1,5 +1,7 @@
social social
social.exe social.exe
*.db *.db
*.pem
backtrace.txt backtrace.txt
logs/* logs/*
3p/*
+1 -1
View File
@@ -1,2 +1,2 @@
all: all:
gcc serve.c tinytemplate.c sqlite3.c -o social -Wall -Wextra -ggdb -rdynamic gcc src/*.c -o social -DHTTPS=1 -Wall -Wextra -ggdb -rdynamic -l:libbearssl.a -L3p/BearSSl/build -I3p/BearSSL/inc
+14
View File
@@ -28,6 +28,20 @@
<article> <article>
<h1>{{title}} (by {{author}})</h1> <h1>{{title}} (by {{author}})</h1>
<p>{{content}}</p> <p>{{content}}</p>
<form action="/posts/{{id}}/comments" method="POST">
<textarea name="content" placeholder="comment"></textarea>
<input type="submit" value="comment" />
</form>
<table>
{% for comment in comments %}
<tr>
<td>{{comment.author}}:</td>
<td>{{comment.content}}</td>
</tr>
{% end %}
</table>
</article> </article>
</main> </main>
</body> </body>
+57
View File
@@ -0,0 +1,57 @@
#define _GNU_SOURCE
#include <stdio.h> // snprintf
#include <dlfcn.h>
#include <fcntl.h> // O_WRONLY, O_APPEND, O_CREAT
#include <signal.h>
#include <unistd.h> // write
#include <execinfo.h>
#include "basic.h"
#include "backtrace.h"
#define BACKTRACE_FILE "backtrace.txt"
#define BACKTRACE_LIMIT 30
void dump_backtrace(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);
}
+6
View File
@@ -0,0 +1,6 @@
#ifndef CFORUM_BACKTRACE_INCLUDED
#define CFORUM_BACKTRACE_INCLUDED
void dump_backtrace(int signo);
#endif // CFORUM_BACKTRACE_INCLUDED
+250
View File
@@ -0,0 +1,250 @@
#include <time.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h> // snprintf
#include <stdlib.h> // malloc, free
#include <sys/stat.h>
#include "log.h"
#include "basic.h"
void *mymalloc(size_t num)
{
return malloc(num);
}
void myfree(void *ptr, size_t num)
{
(void) num;
free(ptr);
}
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;
}
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;
}
+57
View File
@@ -0,0 +1,57 @@
#ifndef CFORUM_BASIC_INCLUDED
#define CFORUM_BASIC_INCLUDED
#include <stdint.h>
#include <stddef.h>
#include <stdarg.h>
#include <string.h>
#include <stdbool.h>
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) ((int32_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
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 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 load_file_contents(string file, string *out);
#endif // CFORUM_BASIC_INCLUDED
+275
View File
@@ -0,0 +1,275 @@
#include <assert.h>
#include <stdlib.h> // exit
#include "log.h"
#include "config.h"
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;
}
+14
View File
@@ -0,0 +1,14 @@
#ifndef CFORUM_CONFIG_INCLUDED
#define CFORUM_CONFIG_INCLUDED
#include <stdint.h>
#include "basic.h"
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);
#endif // CFORUM_CONFIG_INCLUDED
+529
View File
@@ -0,0 +1,529 @@
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h> // exit
#include <assert.h>
#include <stdbool.h>
#include "log.h"
#include "basic.h"
#include "template.h"
#include "endpoints.h"
#include "sqlite_utils.h"
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)
#define MAX_COMMENT_CONTENT (1<<12)
typedef struct {
uint32_t id;
string name;
char namebuf[MAX_USER_NAME];
} Session;
SessionID create_session(string name);
void remove_session(SessionID id);
string name_from_session(SessionID id);
SessionID session_from_request(Request request);
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"
"CREATE TABLE IF NOT EXISTS Comments(\n"
" id INTEGER PRIMARY KEY,\n"
" content TEXT NOT NULL,\n"
" author TEXT,\n"
" parent INTEGER,\n"
" FOREIGN KEY (author) REFERENCES Users(name),\n"
" FOREIGN KEY (parent) REFERENCES Posts(id)\n"
");\n"
"PRAGMA foreign_keys = ON;\n";
Session sessions[MAX_SESSIONS];
SessionID next_session_id = 1;
void init_endpoints(void)
{
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;
}
void free_endpoints(void)
{
sqlite3_close(db);
}
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;
}
sqlite3_stmt *stmt2 = sqlite3_utils_prepare(db, "SELECT id, content, author FROM Comments WHERE parent=:i", post_id);
if (stmt2 == NULL) {
status_line(b, 500);
append_content_s(b, LIT("Internal error"));
sqlite3_finalize(stmt);
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"));
sqlite3_finalize(stmt);
sqlite3_finalize(stmt2);
return;
}
if (res == 1) {
status_line(b, 404);
append_content_s(b, LIT("No such post"));
sqlite3_finalize(stmt);
sqlite3_finalize(stmt2);
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=LIT("comments"), .type=TPT_QUERY, .q=stmt2},
{.name=NULLSTR, .type=TPT_LAST }
};
append_template(b, LIT("pages/post.html"), params);
sqlite3_finalize(stmt);
sqlite3_finalize(stmt2);
return;
}
int comment_post_id;
if (!match_path_format(request.url.path, "/posts/:n/comments", &comment_post_id)) {
if (login_username.size > 0) {
status_line(b, 303);
add_header(b, LIT("Location: /"));
return;
}
char contentbuf[MAX_COMMENT_CONTENT];
string content;
if (!get_query_string_param(request.content, LIT("content"), LIT(contentbuf), &content)) {
status_line(b, 400);
append_content_s(b, LIT("Invalid content"));
return;
}
content = trim(content);
if (content.size == 0) {
status_line(b, 400);
append_content_s(b, LIT("Content field is empty"));
return;
}
if (!sqlite3_utils_exec(db, "INSERT INTO Comments(parent, content, author) VALUES (:i, :s, :s)", comment_post_id, content, login_username)) {
status_line(b, 500);
append_content_s(b, LIT("Internal error"));
return;
}
status_line(b, 303);
add_header_f(b, "Location: /posts/%d", comment_post_id);
return;
}
if (!match_path_format(request.url.path, "/users")) {
sqlite3_stmt *stmt = sqlite3_utils_prepare(db, "SELECT name FROM Users");
if (stmt == NULL) {
status_line(b, 500);
return;
}
status_line(b, 200);
add_header(b, LIT("Content-Type: text/html"));
TemplateParam params[] = {
{.name=LIT("login"), .type=TPT_INT, .i=login_username.size>0},
{.name=LIT("login_username"), .type=TPT_STRING, .s=login_username},
{.name=LIT("users"), .type=TPT_QUERY, .q=stmt},
{.name=NULLSTR, .type=TPT_LAST }
};
append_template(b, LIT("pages/users.html"), params);
sqlite3_finalize(stmt);
return;
}
string profile_username;
if (!match_path_format(request.url.path, "/users/:s", &profile_username)) {
sqlite3_stmt *stmt = sqlite3_utils_prepare(db, "SELECT bio FROM Users WHERE name=:s", profile_username);
if (stmt == NULL) {
status_line(b, 500);
append_content_s(b, LIT("Internal error"));
return;
}
string bio;
int res = sqlite3_utils_fetch(stmt, "s", &bio);
if (res == -1) {
status_line(b, 500);
append_content_s(b, LIT("Internal error"));
return;
}
if (res == 1) {
status_line(b, 404);
append_content_s(b, LIT("No such user"));
return;
}
assert(res == 0);
status_line(b, 200);
add_header(b, LIT("Content-Type: text/html"));
TemplateParam params[] = {
{.name=LIT("login"), .type=TPT_INT, .i=login_username.size>0},
{.name=LIT("login_username"), .type=TPT_STRING, .s=login_username},
{.name=LIT("name"), .type=TPT_STRING, .s=profile_username},
{.name=LIT("bio"), .type=TPT_STRING, .s=bio},
{.name=NULLSTR, .type=TPT_LAST }
};
append_template(b, LIT("pages/user.html"), params);
sqlite3_finalize(stmt);
return;
}
if (!match_path_format(request.url.path, "/login")) {
if (login_username.size > 0) {
// Already logged in
status_line(b, 303);
add_header(b, LIT("Location: /home"));
return;
}
status_line(b, 200);
append_file(b, LIT("pages/login.html"));
return;
}
if (!match_path_format(request.url.path, "/signup")) {
if (login_username.size > 0) {
// Already logged in
status_line(b, 303);
add_header(b, LIT("Location: /home"));
return;
}
status_line(b, 200);
append_file(b, LIT("pages/signup.html"));
return;
}
if (!match_path_format(request.url.path, "/home")) {
status_line(b, 200);
append_file(b, LIT("pages/home.html"));
return;
}
if (serve_file_or_dir(b, LIT("/static"), LIT("static/"), request.url.path, NULLSTR, false))
return;
status_line(b, 404);
append_content_s(b, LIT("Nothing here :|"));
}
///////////////////////////////////////////////////////////////////////////////////////////////
/// SESSIONS ///
///////////////////////////////////////////////////////////////////////////////////////////////
SessionID create_session(string name)
{
int i = 0;
while (i < MAX_SESSIONS && sessions[i].id != NO_SESSION)
i++;
if (i == MAX_SESSIONS)
return NO_SESSION;
if (next_session_id == NO_SESSION)
next_session_id++;
SessionID id = next_session_id++;
if (name.size > sizeof(sessions[i].namebuf))
log_fatal(LIT("User name buffer is too small"));
memcpy(sessions[i].namebuf, name.data, name.size);
sessions[i].id = id;
sessions[i].name = (string) { sessions[i].namebuf, name.size };
return sessions[i].id;
}
void remove_session(SessionID id)
{
assert(id != NO_SESSION);
int i = 0;
while (i < MAX_SESSIONS && sessions[i].id != id)
i++;
if (i == MAX_SESSIONS)
log_fatal(LIT("Trying to remove non existing session"));
sessions[i].id = NO_SESSION;
sessions[i].name = NULLSTR;
memset(sessions[i].namebuf, 0, sizeof(sessions[i].namebuf));
}
string name_from_session(SessionID id)
{
assert(id != NO_SESSION);
for (int i = 0; i < MAX_SESSIONS; i++)
if (sessions[i].id == id)
return sessions[i].name;
return NULLSTR;
}
SessionID session_from_request(Request request)
{
string sessid_str;
if (!get_cookie(&request, LIT("sessid"), &sessid_str))
return NO_SESSION;
SessionID id;
{
char *src = sessid_str.data;
size_t len = sessid_str.size;
size_t i = 0;
while (i < len && is_space(src[i]))
i++;
if (i == len || !is_digit(src[i]))
return NO_SESSION;
uint32_t buf = 0;
do {
int d = src[i] - '0';
if (buf > (UINT32_MAX - d) / 10)
return NO_SESSION;
buf = buf * 10 + d;
i++;
} while (i < len && is_digit(src[i]));
while (i < len && is_space(src[i]))
i++;
if (i < len)
return NO_SESSION;
assert(sizeof(buf) == sizeof(SessionID));
assert(buf != 0 && buf != NO_SESSION);
id = buf;
}
return id;
}
///////////////////////////////////////////////////////////////////////////////////////////////
/// TEMPLATE EVALUATION ///
///////////////////////////////////////////////////////////////////////////////////////////////
+10
View File
@@ -0,0 +1,10 @@
#ifndef CFORUM_ENDPOINTS_INCLUDED
#define CFORUM_ENDPOINTS_INCLUDED
#include "http.h"
void init_endpoints(void);
void free_endpoints(void);
void respond(Request request, ResponseBuilder *b);
#endif // CFORUM_ENDPOINTS_INCLUDED
+229 -2282
View File
File diff suppressed because it is too large Load Diff
+139
View File
@@ -0,0 +1,139 @@
#ifndef CFORUM_HTTP_INCLUDED
#define CFORUM_HTTP_INCLUDED
#include <stdint.h>
#include "basic.h"
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 Connection 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;
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 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);
typedef struct {
int http_port;
string http_addr;
int https_port;
string https_addr;
string cert_file;
string privkey_file;
bool access_log;
bool show_io;
bool show_requests;
int max_connections;
int keep_alive_max_requests;
int connection_timeout_sec;
int closing_timeout_sec;
int request_timeout_sec;
int log_flush_timeout_sec;
void (*respond)(Request, ResponseBuilder*);
} HTTPConfig;
void http_init(HTTPConfig config);
void http_loop(void);
void http_free(void);
void http_stop(void);
HTTPConfig http_default_config(void);
#endif // CFORUM_HTTP_INCLUDED
+324
View File
@@ -0,0 +1,324 @@
#include <errno.h>
#include <fcntl.h>
#include <stdio.h> // mkdir
#include <stdlib.h> // exit
#include <stdarg.h>
#include <string.h>
#include <dirent.h>
#include <unistd.h> // close
#include <assert.h>
#include <sys/stat.h>
#include "log.h"
static bool log_initialized = false;
static int log_last_file_index = 0;
static int log_fd = -1;
static char *log_buffer = NULL;
static size_t log_buffer_used = 0;
static size_t log_buffer_size = 0;
static bool log_failed = false;
static size_t log_total_size = 0;
static size_t log_dir_limit_mb = 0;
static size_t log_file_limit_b = 0;
static 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));
}
+16
View File
@@ -0,0 +1,16 @@
#ifndef CFORUM_LOG_INCLUDED
#define CFORUM_LOG_INCLUDED
#include <stddef.h>
#include "basic.h"
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);
#endif // CFORUM_LOG_INCLUDED
+88
View File
@@ -0,0 +1,88 @@
#include <signal.h>
#include <stdlib.h> // atexit
#include "log.h"
#include "http.h"
#include "config.h"
#include "endpoints.h"
#include "backtrace.h"
void termination_signal_handler(int signo)
{
(void) signo;
http_stop();
}
int main(int argc, char **argv)
{
atexit(log_free);
(void) argc;
(void) argv;
{
config_init();
config_load(LIT("config.txt"));
// Setup signal handlers for the crash log
struct sigaction sa_crash;
sa_crash.sa_handler = dump_backtrace;
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);
// Setup signal handlers for graceful termination
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"))
);
DEBUG("Logger configured\n");
HTTPConfig http_config = http_default_config();
http_config.http_port = config_int(LIT("http_port"));
http_config.http_addr = config_string(LIT("http_addr"));
http_config.https_port = config_int(LIT("https_port"));
http_config.https_addr = config_string(LIT("https_addr"));
http_config.cert_file = config_string(LIT("cert_file"));
http_config.privkey_file = config_string(LIT("privkey_file"));
http_config.access_log = config_bool(LIT("access_log"));
http_config.show_io = config_bool(LIT("show_io"));
http_config.show_requests = config_bool(LIT("show_requests"));
http_config.max_connections = config_int(LIT("max_connections"));
http_config.keep_alive_max_requests = config_int(LIT("keep_alive_max_requests"));
http_config.connection_timeout_sec = config_int(LIT("connection_timeout_sec"));
http_config.closing_timeout_sec = config_int(LIT("closing_timeout_sec"));
http_config.request_timeout_sec = config_int(LIT("request_timeout_sec"));
http_config.log_flush_timeout_sec = config_int(LIT("log_flush_timeout_sec"));
http_config.respond = respond;
http_init(http_config);
init_endpoints();
config_free();
}
log_data(LIT("starting\n"));
http_loop();
log_data(LIT("closing\n"));
free_endpoints();
http_free();
log_free();
return 0;
}
View File
View File
+233
View File
@@ -0,0 +1,233 @@
#include <assert.h>
#include <stdarg.h>
#include "log.h"
#include "basic.h"
#include "sqlite_utils.h"
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;
}
+11
View File
@@ -0,0 +1,11 @@
#ifndef CFORUM_SQLITE_UTILS_INCLUDED
#define CFORUM_SQLITE_UTILS_INCLUDED
#include "sqlite3.h"
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, ...);
#endif // CFORUM_SQLITE_UTILS_INCLUDED
+100
View File
@@ -0,0 +1,100 @@
#include <assert.h>
#include "log.h"
#include "template.h"
#include "tinytemplate.h"
typedef struct {
ResponseBuilder *b;
TemplateParam *params;
} TemplateContext;
static void template_output_callback(void *userp, const char *str, size_t len)
{
TemplateContext *c = userp;
append_content_s(c->b, (string) {str, len});
}
static 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;
}
static 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;
}
static 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;
}
+24
View File
@@ -0,0 +1,24 @@
#include "http.h"
#include "basic.h"
#include "sqlite_utils.h"
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;
bool append_template(ResponseBuilder *b, string file, TemplateParam *params);