Unpack serve.c into multiple files
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
social
|
||||
social.exe
|
||||
*.db
|
||||
*.pem
|
||||
backtrace.txt
|
||||
logs/*
|
||||
3p/*
|
||||
@@ -1,2 +1,2 @@
|
||||
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
|
||||
|
||||
@@ -28,6 +28,20 @@
|
||||
<article>
|
||||
<h1>{{title}} (by {{author}})</h1>
|
||||
<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>
|
||||
</main>
|
||||
</body>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
#ifndef CFORUM_BACKTRACE_INCLUDED
|
||||
#define CFORUM_BACKTRACE_INCLUDED
|
||||
|
||||
void dump_backtrace(int signo);
|
||||
|
||||
#endif // CFORUM_BACKTRACE_INCLUDED
|
||||
+250
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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 ///
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////
|
||||
@@ -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
|
||||
+226
-2279
File diff suppressed because it is too large
Load Diff
+139
@@ -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
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user