aboutsummaryrefslogtreecommitdiff
path: root/src/bokfd.c
diff options
context:
space:
mode:
authorAnders Betts <anders.betts@gmail.com>2026-09-17 19:55:36 +0200
committerAnders Betts <anders.betts@gmail.com>2026-09-17 19:55:36 +0200
commit380195f7cd5e57acf2c1cf2bc41069e6b0b979ed (patch)
tree32a88fb22a7fbe8f1fd5c105156d1f928c93950d /src/bokfd.c
downloadbokf-380195f7cd5e57acf2c1cf2bc41069e6b0b979ed.tar.gz
bokf-380195f7cd5e57acf2c1cf2bc41069e6b0b979ed.zip
Initial commit: daemon, clients, docs, Docker deploy pipelinev0.1.0
Diffstat (limited to 'src/bokfd.c')
-rw-r--r--src/bokfd.c494
1 files changed, 494 insertions, 0 deletions
diff --git a/src/bokfd.c b/src/bokfd.c
new file mode 100644
index 0000000..70b654a
--- /dev/null
+++ b/src/bokfd.c
@@ -0,0 +1,494 @@
+#include <errno.h>
+#include <fcntl.h>
+#include <netdb.h>
+#include <poll.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/un.h>
+#include <termios.h>
+#include <unistd.h>
+
+#include "auth.h"
+#include "config.h"
+#include "db.h"
+#include "log.h"
+#include "protocol.h"
+#include "sessions.h"
+#include "util.h"
+#include "version.h"
+
+#define MAX_CONNS 64
+#define READ_CHUNK 65536
+
+struct conn {
+ int fd;
+ struct buf in;
+ struct buf out;
+ size_t out_sent;
+ int closing;
+};
+
+static volatile sig_atomic_t g_stop = 0;
+static size_t g_line_limit = 1024 * 1024;
+
+static void on_signal(int sig)
+{
+ (void)sig;
+ g_stop = 1;
+}
+
+static int mkdir_p(const char *path, mode_t mode)
+{
+ char tmp[4096];
+ if (!path || strlen(path) >= sizeof tmp)
+ return -1;
+ strcpy(tmp, path);
+ for (char *p = tmp + 1; *p; p++) {
+ if (*p == '/') {
+ *p = '\0';
+ if (mkdir(tmp, mode) != 0 && errno != EEXIST)
+ return -1;
+ *p = '/';
+ }
+ }
+ if (mkdir(tmp, mode) != 0 && errno != EEXIST)
+ return -1;
+ return 0;
+}
+
+static void dirname_of(const char *path, char *out, size_t n)
+{
+ snprintf(out, n, "%s", path);
+ char *slash = strrchr(out, '/');
+ if (slash)
+ *slash = '\0';
+ else
+ snprintf(out, n, ".");
+}
+
+static int read_password(const char *prompt, char *buf, size_t n)
+{
+ if (!isatty(STDIN_FILENO)) {
+ const char *env = getenv("BOKFD_PASSWORD");
+ if (!env)
+ return -1;
+ snprintf(buf, n, "%s", env);
+ return 0;
+ }
+ struct termios old, noecho;
+ fprintf(stderr, "%s", prompt);
+ if (tcgetattr(STDIN_FILENO, &old) != 0)
+ return -1;
+ noecho = old;
+ noecho.c_lflag &= ~(tcflag_t)ECHO;
+ tcsetattr(STDIN_FILENO, TCSAFLUSH, &noecho);
+ char *r = fgets(buf, (int)n, stdin);
+ tcsetattr(STDIN_FILENO, TCSAFLUSH, &old);
+ fprintf(stderr, "\n");
+ if (!r)
+ return -1;
+ util_str_trim(buf);
+ return 0;
+}
+
+static int cmd_init(const char *db_path, const char *username)
+{
+ char dir[4096];
+ dirname_of(db_path, dir, sizeof dir);
+ if (mkdir_p(dir, 0700) != 0) {
+ fprintf(stderr, "bokfd: cannot create %s\n", dir);
+ return 1;
+ }
+ sqlite3 *db = NULL;
+ char *err = NULL;
+ if (db_open(db_path, &db, &err) != 0) {
+ fprintf(stderr, "bokfd: %s\n", err ? err : "cannot open database");
+ free(err);
+ return 1;
+ }
+ int64_t users = db_count(db, "SELECT count(*) FROM users");
+ if (users > 0) {
+ fprintf(stderr, "bokfd: database already initialized (%lld users)\n",
+ (long long)users);
+ sqlite3_close(db);
+ return 1;
+ }
+ char pw[256];
+ if (read_password("Password for admin: ", pw, sizeof pw) != 0 || !pw[0]) {
+ fprintf(stderr, "bokfd: no password provided\n");
+ sqlite3_close(db);
+ return 1;
+ }
+ if (isatty(STDIN_FILENO)) {
+ char pw2[256];
+ if (read_password("Repeat password: ", pw2, sizeof pw2) != 0 ||
+ strcmp(pw, pw2) != 0) {
+ fprintf(stderr, "bokfd: passwords do not match\n");
+ sqlite3_close(db);
+ return 1;
+ }
+ }
+ int64_t uid = 0;
+ if (db_create_user(db, username, username, pw, 1, &uid, &err) != 0) {
+ fprintf(stderr, "bokfd: %s\n", err ? err : "cannot create user");
+ free(err);
+ sqlite3_close(db);
+ return 1;
+ }
+ printf("initialized %s\nadmin user: %s (id %lld)\n", db_path, username,
+ (long long)uid);
+ sqlite3_close(db);
+ return 0;
+}
+
+static int unix_listen(const char *path)
+{
+ struct sockaddr_un sa;
+ memset(&sa, 0, sizeof sa);
+ sa.sun_family = AF_UNIX;
+ if (strlen(path) >= sizeof sa.sun_path)
+ return -1;
+ snprintf(sa.sun_path, sizeof sa.sun_path, "%s", path);
+ unlink(path);
+ int fd = socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
+ if (fd < 0)
+ return -1;
+ if (bind(fd, (struct sockaddr *)&sa, sizeof sa) != 0) {
+ close(fd);
+ return -1;
+ }
+ chmod(path, 0660);
+ if (listen(fd, 64) != 0) {
+ close(fd);
+ return -1;
+ }
+ return fd;
+}
+
+static int tcp_listen(const char *addrport)
+{
+ char host[256] = "127.0.0.1";
+ char port[16] = "8787";
+ const char *colon = strrchr(addrport, ':');
+ if (colon) {
+ size_t hl = (size_t)(colon - addrport);
+ if (hl < sizeof host) {
+ memcpy(host, addrport, hl);
+ host[hl] = '\0';
+ }
+ snprintf(port, sizeof port, "%s", colon + 1);
+ } else {
+ snprintf(port, sizeof port, "%s", addrport);
+ }
+ struct addrinfo hints, *res = NULL;
+ memset(&hints, 0, sizeof hints);
+ hints.ai_family = AF_UNSPEC;
+ hints.ai_socktype = SOCK_STREAM;
+ hints.ai_flags = AI_PASSIVE;
+ if (getaddrinfo(host, port, &hints, &res) != 0)
+ return -1;
+ int fd = -1;
+ for (struct addrinfo *ai = res; ai; ai = ai->ai_next) {
+ fd = socket(ai->ai_family, ai->ai_socktype | SOCK_NONBLOCK | SOCK_CLOEXEC,
+ ai->ai_protocol);
+ if (fd < 0)
+ continue;
+ int one = 1;
+ setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof one);
+ if (bind(fd, ai->ai_addr, ai->ai_addrlen) == 0 && listen(fd, 64) == 0)
+ break;
+ close(fd);
+ fd = -1;
+ }
+ freeaddrinfo(res);
+ return fd;
+}
+
+static void accept_conns(int lfd, struct conn *conns, size_t *nconns)
+{
+ for (;;) {
+ int fd = accept4(lfd, NULL, NULL, SOCK_NONBLOCK | SOCK_CLOEXEC);
+ if (fd < 0)
+ return;
+ if (*nconns >= MAX_CONNS) {
+ close(fd);
+ continue;
+ }
+ struct conn *c = &conns[(*nconns)++];
+ memset(c, 0, sizeof *c);
+ c->fd = fd;
+ buf_init(&c->in);
+ buf_init(&c->out);
+ }
+}
+
+static void conn_read(struct conn *c, sqlite3 *db)
+{
+ char tmp[READ_CHUNK];
+ ssize_t r = read(c->fd, tmp, sizeof tmp);
+ if (r == 0) {
+ c->closing = 1;
+ return;
+ }
+ if (r < 0) {
+ if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR)
+ return;
+ c->closing = 1;
+ return;
+ }
+ buf_append(&c->in, tmp, (size_t)r);
+ for (;;) {
+ unsigned char *nl = memchr(c->in.p, '\n', c->in.len);
+ if (!nl)
+ break;
+ size_t linelen = (size_t)(nl - c->in.p);
+ char *resp = protocol_handle_line(db, (const char *)c->in.p, linelen);
+ buf_append(&c->out, resp, strlen(resp));
+ buf_append(&c->out, "\n", 1);
+ free(resp);
+ size_t consumed = linelen + 1;
+ memmove(c->in.p, c->in.p + consumed, c->in.len - consumed);
+ c->in.len -= consumed;
+ }
+ if (c->in.len > g_line_limit) {
+ log_warn("dropping oversized request from fd %d", c->fd);
+ c->closing = 1;
+ }
+}
+
+static void conn_flush(struct conn *c)
+{
+ while (c->out_sent < c->out.len) {
+ ssize_t w = write(c->fd, c->out.p + c->out_sent, c->out.len - c->out_sent);
+ if (w > 0) {
+ c->out_sent += (size_t)w;
+ continue;
+ }
+ if (w < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))
+ return;
+ if (w < 0 && errno == EINTR)
+ continue;
+ c->closing = 1;
+ return;
+ }
+ c->out.len = 0;
+ c->out_sent = 0;
+}
+
+static void usage(FILE *f)
+{
+ fprintf(f,
+ "usage: bokfd [options]\n"
+ " bokfd init [--db PATH] [--user NAME]\n"
+ " bokfd --version\n"
+ "\n"
+ "options:\n"
+ " --config FILE config file (key = value)\n"
+ " --db PATH SQLite database (default /var/lib/bokfd/bokfd.db)\n"
+ " --socket PATH unix socket (default /run/bokfd/bokfd.sock)\n"
+ " --backup-dir DIR snapshot destination\n"
+ " --export-dir DIR SIE export destination\n"
+ " --log-level L error|warn|info|debug\n");
+}
+
+static const char *opt_value(const char *arg, const char *name, int *i,
+ int argc, char **argv)
+{
+ size_t n = strlen(name);
+ if (strncmp(arg, name, n) != 0)
+ return NULL;
+ if (arg[n] == '=')
+ return arg + n + 1;
+ if (arg[n] == '\0' && *i + 1 < argc)
+ return argv[++(*i)];
+ return NULL;
+}
+
+static void parse_args(int argc, char **argv, const char **config_file,
+ const char **init_user, int *init_mode)
+{
+ for (int i = 1; i < argc; i++) {
+ const char *a = argv[i];
+ const char *v;
+ if ((v = opt_value(a, "--config", &i, argc, argv)))
+ *config_file = v;
+ else if ((v = opt_value(a, "--db", &i, argc, argv)))
+ g_cfg.db_path = xstrdup(v);
+ else if ((v = opt_value(a, "--socket", &i, argc, argv)))
+ g_cfg.socket_path = xstrdup(v);
+ else if ((v = opt_value(a, "--backup-dir", &i, argc, argv)))
+ g_cfg.backup_dir = xstrdup(v);
+ else if ((v = opt_value(a, "--export-dir", &i, argc, argv)))
+ g_cfg.export_dir = xstrdup(v);
+ else if ((v = opt_value(a, "--log-level", &i, argc, argv)))
+ g_cfg.log_level = log_level_from_name(v);
+ else if ((v = opt_value(a, "--user", &i, argc, argv)))
+ *init_user = v;
+ else if (strcmp(a, "init") == 0)
+ *init_mode = 1;
+ else if (strcmp(a, "--version") == 0) {
+ printf("bokfd %s (protocol v%d)\n", BOKF_VERSION,
+ BOKF_PROTOCOL_VERSION);
+ exit(0);
+ } else {
+ fprintf(stderr, "bokfd: unknown argument %s\n", a);
+ usage(stderr);
+ exit(2);
+ }
+ }
+}
+
+int main(int argc, char **argv)
+{
+ config_defaults();
+ const char *config_file = NULL;
+ const char *init_user = "admin";
+ int init_mode = 0;
+ parse_args(argc, argv, &config_file, &init_user, &init_mode);
+
+ if (config_file) {
+ char *err = NULL;
+ if (config_load_file(config_file, &err) != 0) {
+ fprintf(stderr, "bokfd: %s\n", err ? err : "config error");
+ free(err);
+ return 1;
+ }
+ }
+ config_apply_env();
+ log_set_level(g_cfg.log_level);
+
+ if (init_mode)
+ return cmd_init(g_cfg.db_path, init_user);
+
+ sqlite3 *db = NULL;
+ char *err = NULL;
+ if (db_open(g_cfg.db_path, &db, &err) != 0) {
+ log_error("%s", err ? err : "cannot open database");
+ free(err);
+ return 1;
+ }
+ if (db_count(db, "SELECT count(*) FROM users") <= 0)
+ log_warn("no users in database; run 'bokfd init' first");
+
+ mkdir_p(g_cfg.backup_dir, 0700);
+ mkdir_p(g_cfg.export_dir, 0700);
+
+ char dir[4096];
+ dirname_of(g_cfg.socket_path, dir, sizeof dir);
+ if (mkdir_p(dir, 0755) != 0) {
+ log_error("cannot create socket directory %s", dir);
+ sqlite3_close(db);
+ return 1;
+ }
+ int lfd = unix_listen(g_cfg.socket_path);
+ if (lfd < 0) {
+ log_error("cannot listen on %s: %s", g_cfg.socket_path,
+ strerror(errno));
+ sqlite3_close(db);
+ return 1;
+ }
+ log_info("bokfd %s listening on %s", BOKF_VERSION, g_cfg.socket_path);
+
+ int tfd = -1;
+ if (g_cfg.tcp_enabled) {
+ tfd = tcp_listen(g_cfg.tcp_addr ? g_cfg.tcp_addr : "127.0.0.1:8787");
+ if (tfd < 0)
+ log_error("cannot listen on tcp %s",
+ g_cfg.tcp_addr ? g_cfg.tcp_addr : "127.0.0.1:8787");
+ else
+ log_info("bokfd listening on tcp %s",
+ g_cfg.tcp_addr ? g_cfg.tcp_addr : "127.0.0.1:8787");
+ }
+
+ signal(SIGINT, on_signal);
+ signal(SIGTERM, on_signal);
+ signal(SIGPIPE, SIG_IGN);
+ sessions_init(g_cfg.session_ttl);
+
+ /* base64 expands by 4/3; the JSON envelope needs some room too */
+ g_line_limit = (size_t)g_cfg.max_line_bytes;
+ size_t att_limit =
+ (size_t)g_cfg.max_attachment_bytes / 3 * 4 + 64 * 1024;
+ if (att_limit > g_line_limit)
+ g_line_limit = att_limit;
+
+ struct conn conns[MAX_CONNS];
+ size_t nconns = 0;
+ struct pollfd pfds[MAX_CONNS + 2];
+ struct conn *map[MAX_CONNS + 2];
+
+ while (!g_stop) {
+ int n = 0;
+ int nlisteners = 0;
+ pfds[n].fd = lfd;
+ pfds[n].events = POLLIN;
+ map[n] = NULL;
+ n++;
+ nlisteners++;
+ if (tfd >= 0) {
+ pfds[n].fd = tfd;
+ pfds[n].events = POLLIN;
+ map[n] = NULL;
+ n++;
+ nlisteners++;
+ }
+ for (size_t i = 0; i < nconns; i++) {
+ pfds[n].fd = conns[i].fd;
+ pfds[n].events = POLLIN | (conns[i].out.len > conns[i].out_sent
+ ? POLLOUT
+ : 0);
+ map[n] = &conns[i];
+ n++;
+ }
+ int pr = poll(pfds, (nfds_t)n, 1000);
+ if (pr < 0) {
+ if (errno == EINTR)
+ continue;
+ log_error("poll: %s", strerror(errno));
+ break;
+ }
+ for (int i = 0; i < nlisteners; i++)
+ if (pfds[i].revents & POLLIN)
+ accept_conns(pfds[i].fd, conns, &nconns);
+ for (int i = nlisteners; i < n; i++) {
+ struct conn *c = map[i];
+ if (c->fd < 0)
+ continue;
+ if (pfds[i].revents & (POLLIN | POLLHUP | POLLERR))
+ conn_read(c, db);
+ if (pfds[i].revents & POLLOUT)
+ conn_flush(c);
+ if (c->closing && c->out.len == c->out_sent) {
+ close(c->fd);
+ buf_free(&c->in);
+ buf_free(&c->out);
+ c->fd = -1;
+ }
+ }
+ size_t keep = 0;
+ for (size_t i = 0; i < nconns; i++) {
+ if (conns[i].fd >= 0)
+ conns[keep++] = conns[i];
+ }
+ nconns = keep;
+ }
+
+ log_info("shutting down");
+ for (size_t i = 0; i < nconns; i++) {
+ close(conns[i].fd);
+ buf_free(&conns[i].in);
+ buf_free(&conns[i].out);
+ }
+ close(lfd);
+ if (tfd >= 0)
+ close(tfd);
+ unlink(g_cfg.socket_path);
+ sessions_free_all();
+ sqlite3_close(db);
+ config_free();
+ return 0;
+}