diff options
| author | Anders Betts <anders.betts@gmail.com> | 2026-09-17 21:26:20 +0200 |
|---|---|---|
| committer | Anders Betts <anders.betts@gmail.com> | 2026-09-17 21:26:20 +0200 |
| commit | ed1c15929d2eb2dbc6432986c26661bf1549964a (patch) | |
| tree | 4f04934edef686b19d16e1bb2f79bb5c15e142de /clients | |
| parent | 380195f7cd5e57acf2c1cf2bc41069e6b0b979ed (diff) | |
| download | bokf-0.1.1.tar.gz bokf-0.1.1.zip | |
Add native TLS transport, TLS clients and lego cert sidecarv0.1.1
- bokfd: optional TLS listener (OpenSSL), certificate reload on change
- clients: tls:host:port targets with chain and host verification
- compose: port 8788 and an INWX/lego renewal sidecar
- Makefile: header dependency tracking (-MMD -MP)
Diffstat (limited to 'clients')
| -rw-r--r-- | clients/bokfctl.c | 236 | ||||
| -rw-r--r-- | clients/bokftui.c | 112 | ||||
| -rw-r--r-- | clients/client.c | 296 | ||||
| -rw-r--r-- | clients/client.h | 26 |
4 files changed, 349 insertions, 321 deletions
diff --git a/clients/bokfctl.c b/clients/bokfctl.c index c541fec..1f1372c 100644 --- a/clients/bokfctl.c +++ b/clients/bokfctl.c @@ -1,168 +1,19 @@ -#include <errno.h> -#include <netdb.h> #include <stdio.h> #include <stdlib.h> #include <string.h> -#include <sys/socket.h> -#include <sys/un.h> -#include <unistd.h> +#include "client.h" #include "util.h" #include "version.h" #include "yyjson.h" -static ssize_t write_all(int fd, const char *buf, size_t len) -{ - size_t off = 0; - while (off < len) { - ssize_t w = write(fd, buf + off, len - off); - if (w < 0) { - if (errno == EINTR) - continue; - return -1; - } - off += (size_t)w; - } - return (ssize_t)off; -} - -static char *read_line_fd(int fd) -{ - struct buf b; - buf_init(&b); - char chunk[4096]; - for (;;) { - ssize_t r = read(fd, chunk, sizeof chunk); - if (r < 0) { - if (errno == EINTR) - continue; - buf_free(&b); - return NULL; - } - if (r == 0) - break; - unsigned char *nl = memchr(chunk, '\n', (size_t)r); - if (nl) { - buf_append(&b, chunk, (size_t)(nl - (unsigned char *)chunk)); - break; - } - buf_append(&b, chunk, (size_t)r); - } - char *out = xmalloc(b.len + 1); - memcpy(out, b.p ? (char *)b.p : "", b.len); - out[b.len] = '\0'; - buf_free(&b); - return out; -} - -static int tcp_connect_addr(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; - 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, ai->ai_protocol); - if (fd < 0) - continue; - if (connect(fd, ai->ai_addr, ai->ai_addrlen) == 0) - break; - close(fd); - fd = -1; - } - freeaddrinfo(res); - return fd; -} - -static int connect_target(const char *target) -{ - if (strncmp(target, "tcp:", 4) == 0) - return tcp_connect_addr(target + 4); - struct sockaddr_un sa; - memset(&sa, 0, sizeof sa); - sa.sun_family = AF_UNIX; - if (strlen(target) >= sizeof sa.sun_path) { - errno = ENAMETOOLONG; - return -1; - } - snprintf(sa.sun_path, sizeof sa.sun_path, "%s", target); - int fd = socket(AF_UNIX, SOCK_STREAM, 0); - if (fd < 0) - return -1; - if (connect(fd, (struct sockaddr *)&sa, sizeof sa) != 0) { - close(fd); - return -1; - } - return fd; -} - -static char *make_request(const char *cmd, const char *session, int64_t org, - const char *args_json, const char *id) -{ - yyjson_doc *adoc = NULL; - if (args_json) { - adoc = yyjson_read(args_json, strlen(args_json), 0); - if (!adoc || !yyjson_is_obj(yyjson_doc_get_root(adoc))) { - yyjson_doc_free(adoc); - return NULL; - } - } - yyjson_mut_doc *d = yyjson_mut_doc_new(NULL); - yyjson_mut_val *o = yyjson_mut_obj(d); - yyjson_mut_doc_set_root(d, o); - yyjson_mut_obj_add_int(d, o, "v", 1); - yyjson_mut_obj_add_str(d, o, "id", id ? id : "cli"); - yyjson_mut_obj_add_str(d, o, "cmd", cmd); - if (session) - yyjson_mut_obj_add_str(d, o, "session", session); - if (org > 0) - yyjson_mut_obj_add_int(d, o, "org", org); - if (adoc) { - yyjson_mut_val *args = yyjson_val_mut_copy(d, yyjson_doc_get_root(adoc)); - yyjson_mut_obj_add_val(d, o, "args", args); - yyjson_doc_free(adoc); - } - char *s = yyjson_mut_write(d, 0, NULL); - yyjson_mut_doc_free(d); - return s; -} - -static char *make_login_args(const char *user, const char *password) -{ - yyjson_mut_doc *d = yyjson_mut_doc_new(NULL); - yyjson_mut_val *o = yyjson_mut_obj(d); - yyjson_mut_doc_set_root(d, o); - yyjson_mut_obj_add_str(d, o, "method", "password"); - yyjson_mut_obj_add_str(d, o, "username", user); - yyjson_mut_obj_add_str(d, o, "password", password); - char *s = yyjson_mut_write(d, 0, NULL); - yyjson_mut_doc_free(d); - return s; -} - static void usage(void) { fprintf(stderr, "usage: bokfctl [options] <cmd> [args-json]\n" "\n" "options:\n" - " --socket TARGET unix socket path or tcp:host:port\n" + " --socket TARGET unix path, tcp:host:port or tls:host:port\n" " (env BOKFD_SOCKET, default /run/bokfd/bokfd.sock)\n" " --user NAME login user (env BOKFD_USER)\n" " --password PW login password (env BOKFD_PASSWORD)\n" @@ -175,6 +26,12 @@ static void usage(void) " bokfctl describe\n"); } +static int is_local_cmd(const char *cmd) +{ + return strcmp(cmd, "health") == 0 || strcmp(cmd, "meta") == 0 || + strcmp(cmd, "session.open") == 0; +} + int main(int argc, char **argv) { const char *target = getenv("BOKFD_SOCKET"); @@ -245,90 +102,61 @@ int main(int argc, char **argv) return 2; } - int fd = connect_target(target); - if (fd < 0) { + struct client_conn conn; + if (client_connect(target, &conn) != 0) { fprintf(stderr, "bokfctl: cannot connect to %s: %s\n", target, - strerror(errno)); + client_last_error()); return 2; } - char session[128] = ""; - if (strcmp(cmd, "health") != 0 && strcmp(cmd, "meta") != 0 && - strcmp(cmd, "session.open") != 0) { + char *session = NULL; + if (!is_local_cmd(cmd)) { if (!user || !password) { fprintf(stderr, "bokfctl: set BOKFD_USER and BOKFD_PASSWORD (or --user/--password) to log in\n"); - close(fd); + client_close(&conn); return 2; } - char *largs = make_login_args(user, password); - char *lreq = make_request("session.open", NULL, 0, largs, "login"); - free(largs); - if (!lreq || write_all(fd, lreq, strlen(lreq)) < 0 || - write_all(fd, "\n", 1) < 0) { - fprintf(stderr, "bokfctl: send failed\n"); - free(lreq); - close(fd); - return 2; - } - free(lreq); - char *lresp = read_line_fd(fd); - if (!lresp) { - fprintf(stderr, "bokfctl: no response\n"); - close(fd); - return 2; - } - yyjson_doc *ld = yyjson_read(lresp, strlen(lresp), 0); - int ok = ld && yyjson_is_obj(yyjson_doc_get_root(ld)) && - yyjson_get_bool(yyjson_obj_get(yyjson_doc_get_root(ld), "ok")); - const char *sid = NULL; - if (ok) { - yyjson_val *r = yyjson_obj_get(yyjson_doc_get_root(ld), "result"); - yyjson_val *s = r ? yyjson_obj_get(r, "session") : NULL; - if (s && yyjson_is_str(s)) - sid = yyjson_get_str(s); - } - if (!ok || !sid) { - fprintf(stderr, "%s\n", lresp); - yyjson_doc_free(ld); - free(lresp); - close(fd); - return 1; + char *lerr = NULL; + if (client_login(&conn, user, password, &session, &lerr) != 0) { + fprintf(stderr, "%s\n", lerr ? lerr : "login failed"); + int rc = lerr && lerr[0] == '{' ? 1 : 2; + free(lerr); + client_close(&conn); + return rc; } - snprintf(session, sizeof session, "%s", sid); - yyjson_doc_free(ld); - free(lresp); } char *req = NULL; if (strcmp(cmd, "raw") == 0) { if (!args_json) { fprintf(stderr, "bokfctl: raw requires a full request JSON\n"); - close(fd); + client_close(&conn); return 2; } req = xstrdup(args_json); } else { - req = make_request(cmd, session[0] ? session : NULL, org, args_json, - "cli"); + req = client_make_request(cmd, session, org, args_json, "cli"); if (!req) { fprintf(stderr, "bokfctl: args must be a JSON object\n"); - close(fd); + free(session); + client_close(&conn); return 2; } } - if (write_all(fd, req, strlen(req)) < 0 || write_all(fd, "\n", 1) < 0) { - fprintf(stderr, "bokfctl: send failed\n"); + free(session); + if (client_send_line(&conn, req) != 0) { + fprintf(stderr, "bokfctl: send failed: %s\n", client_last_error()); free(req); - close(fd); + client_close(&conn); return 2; } free(req); - char *resp = read_line_fd(fd); - close(fd); + char *resp = client_read_line(&conn); + client_close(&conn); if (!resp) { - fprintf(stderr, "bokfctl: no response\n"); + fprintf(stderr, "bokfctl: no response: %s\n", client_last_error()); return 2; } yyjson_doc *rd = yyjson_read(resp, strlen(resp), 0); diff --git a/clients/bokftui.c b/clients/bokftui.c index 1939eeb..c26d188 100644 --- a/clients/bokftui.c +++ b/clients/bokftui.c @@ -32,7 +32,7 @@ static int ui_getch(void) } struct app { - int fd; + struct client_conn conn; char socket[256]; char session[128]; char username[64]; @@ -895,10 +895,10 @@ static void app_refresh_context(struct app *a) { char args[64]; snprintf(args, sizeof args, "{\"org\":%lld}", (long long)a->org); - char *resp = client_rpc(a->fd, "session.use_org", a->session, 0, args); + char *resp = client_rpc(&a->conn, "session.use_org", a->session, 0, args); free(resp); - resp = client_rpc(a->fd, "org.get", a->session, a->org, "{}"); + resp = client_rpc(&a->conn, "org.get", a->session, a->org, "{}"); if (resp && client_ok(resp)) { char *name = jstr_dup(resp, "result.name"); if (name) { @@ -908,7 +908,7 @@ static void app_refresh_context(struct app *a) } free(resp); - resp = client_rpc(a->fd, "org.list", a->session, 0, "{}"); + resp = client_rpc(&a->conn, "org.list", a->session, 0, "{}"); if (resp) { size_t n = jarr_size(resp, "result.items"); for (size_t i = 0; i < n; i++) { @@ -928,7 +928,7 @@ static void app_refresh_context(struct app *a) snprintf(a->default_series, sizeof a->default_series, "%s", "A"); a->attachment_dir[0] = '\0'; - resp = client_rpc(a->fd, "settings.get", a->session, a->org, "{}"); + resp = client_rpc(&a->conn, "settings.get", a->session, a->org, "{}"); if (resp && client_ok(resp)) { char *ser = jstr_dup(resp, "result.default_series"); if (ser && *ser) @@ -942,7 +942,7 @@ static void app_refresh_context(struct app *a) free(resp); a->max_attachment_bytes = 10 * 1024 * 1024; - resp = client_rpc(a->fd, "meta", NULL, 0, NULL); + resp = client_rpc(&a->conn, "meta", NULL, 0, NULL); if (resp && client_ok(resp)) { int64_t v = jint_val(resp, "result.limits.max_attachment_bytes", 0); @@ -951,7 +951,7 @@ static void app_refresh_context(struct app *a) } free(resp); - resp = client_rpc(a->fd, "fiscal_year.get", a->session, a->org, "{}"); + resp = client_rpc(&a->conn, "fiscal_year.get", a->session, a->org, "{}"); if (resp && client_ok(resp)) { a->fy = jint_val(resp, "result.id", 0); char *label = jstr_dup(resp, "result.label"); @@ -983,7 +983,7 @@ static int64_t fy_new_form(struct app *a) { char label[32] = "", start[16] = "", end[16] = ""; char *resp = - client_rpc(a->fd, "fiscal_year.list", a->session, a->org, "{}"); + client_rpc(&a->conn, "fiscal_year.list", a->session, a->org, "{}"); if (resp && client_ok(resp)) { size_t n = jarr_size(resp, "result.items"); char max_end[16] = ""; @@ -1098,7 +1098,7 @@ static int64_t fy_new_form(struct app *a) yyjson_mut_obj_add_strcpy(d, o, "end_date", end); char *args = yyjson_mut_write(d, 0, NULL); yyjson_mut_doc_free(d); - resp = client_rpc(a->fd, "fiscal_year.open", a->session, a->org, + resp = client_rpc(&a->conn, "fiscal_year.open", a->session, a->org, args); free(args); if (resp && client_ok(resp)) { @@ -1131,7 +1131,7 @@ static void select_fiscal_year(struct app *a) if (g_quit) return; char *resp = - client_rpc(a->fd, "fiscal_year.list", a->session, a->org, "{}"); + client_rpc(&a->conn, "fiscal_year.list", a->session, a->org, "{}"); if (!resp || !client_ok(resp)) { show_error("Räkenskapsår", resp); free(resp); @@ -1375,7 +1375,7 @@ static int voucher_detail(struct app *a, int64_t id, int64_t *out_new) snprintf(args, sizeof args, "{\"id\":%lld}", (long long)id); for (;;) { char *resp = - client_rpc(a->fd, "voucher.get", a->session, a->org, args); + client_rpc(&a->conn, "voucher.get", a->session, a->org, args); if (!resp || !client_ok(resp)) { show_error("Verifikat", resp); free(resp); @@ -1494,7 +1494,7 @@ static int voucher_detail(struct app *a, int64_t id, int64_t *out_new) yyjson_mut_obj_add_strcpy(d, o, "client_ref", ref); char *cargs = yyjson_mut_write(d, 0, NULL); yyjson_mut_doc_free(d); - char *r = client_rpc(a->fd, "voucher.correct", a->session, + char *r = client_rpc(&a->conn, "voucher.correct", a->session, a->org, cargs); if (r && client_ok(r)) { int64_t new_id = jint_val(r, "result.id", 0); @@ -1612,7 +1612,7 @@ static void acct_cache_load(struct app *a) if (g_accts.loaded && g_accts.org == a->org) return; acct_cache_free(); - char *resp = client_rpc(a->fd, "account.list", a->session, a->org, + char *resp = client_rpc(&a->conn, "account.list", a->session, a->org, "{\"active_only\":true}"); if (!resp || !client_ok(resp)) { free(resp); @@ -1844,7 +1844,7 @@ static int64_t vouchers_new(struct app *a) } if (ch == KEY_F(4)) { char tname[128] = ""; - char *lresp = client_rpc(a->fd, "template.list", a->session, + char *lresp = client_rpc(&a->conn, "template.list", a->session, a->org, "{\"active_only\":true}"); if (!lresp || !client_ok(lresp)) { show_error("Mallar", lresp); @@ -1901,7 +1901,7 @@ static int64_t vouchers_new(struct app *a) yyjson_mut_obj_add_strcpy(d, to, "name", tbuf); char *targs = yyjson_mut_write(d, 0, NULL); yyjson_mut_doc_free(d); - char *resp = client_rpc(a->fd, "template.get", a->session, + char *resp = client_rpc(&a->conn, "template.get", a->session, a->org, targs); free(targs); if (!resp || !client_ok(resp)) { @@ -2030,8 +2030,8 @@ static int64_t vouchers_new(struct app *a) a->session, (long long)a->org, args); free(args); char *r = NULL; - if (client_send_line(a->fd, raw) == 0) - r = client_read_line(a->fd); + if (client_send_line(&a->conn, raw) == 0) + r = client_read_line(&a->conn); free(raw); if (r && client_ok(r)) { att_ids[natt] = jint_val(r, "result.id", 0); @@ -2107,8 +2107,8 @@ static int64_t vouchers_new(struct app *a) "\"session\":\"%s\",\"org\":%lld,\"dry_run\":true," "\"args\":%s}", a->session, (long long)a->org, args); - if (client_send_line(a->fd, raw) == 0) { - char *r = client_read_line(a->fd); + if (client_send_line(&a->conn, raw) == 0) { + char *r = client_read_line(&a->conn); if (r && client_ok(r)) { int64_t num = jint_val(r, "result.number", 0); message("Validering OK", @@ -2122,7 +2122,7 @@ static int64_t vouchers_new(struct app *a) } free(raw); } else { - char *r = client_rpc(a->fd, "voucher.post", a->session, a->org, + char *r = client_rpc(&a->conn, "voucher.post", a->session, a->org, args); if (r && client_ok(r)) { int64_t id = jint_val(r, "result.id", 0); @@ -2221,7 +2221,7 @@ static void vouchers_screen(struct app *a) snprintf(largs, sizeof largs, "{\"limit\":200,\"fiscal_year\":%lld}", (long long)a->fy); - char *resp = client_rpc(a->fd, "voucher.list", a->session, a->org, + char *resp = client_rpc(&a->conn, "voucher.list", a->session, a->org, largs); if (!resp || !client_ok(resp)) { show_error("Verifikat", resp); @@ -2401,7 +2401,7 @@ static const char *vat_display(const char *name, const char *vat_code, static void accounts_report(struct app *a) { for (;;) { - char *resp = client_rpc(a->fd, "account.list", a->session, a->org, + char *resp = client_rpc(&a->conn, "account.list", a->session, a->org, "{\"active_only\":false}"); if (!resp || !client_ok(resp)) { show_error("Kontolista", resp); @@ -2523,7 +2523,7 @@ static void reports_screen(struct app *a) } } for (;;) { - char *resp = client_rpc(a->fd, cmd, a->session, a->org, args); + char *resp = client_rpc(&a->conn, cmd, a->session, a->org, args); if (!resp || !client_ok(resp)) { show_error("Rapport", resp); free(resp); @@ -2550,7 +2550,7 @@ static void inbox_screen(struct app *a) for (;;) { if (g_quit) return; - char *resp = client_rpc(a->fd, "attachment.list", a->session, a->org, + char *resp = client_rpc(&a->conn, "attachment.list", a->session, a->org, "{\"unlinked\":true,\"limit\":200}"); if (!resp || !client_ok(resp)) { show_error("Underlag", resp); @@ -2639,8 +2639,8 @@ static void inbox_screen(struct app *a) a->session, (long long)a->org, args); free(args); char *r = NULL; - if (client_send_line(a->fd, raw) == 0) - r = client_read_line(a->fd); + if (client_send_line(&a->conn, raw) == 0) + r = client_read_line(&a->conn); free(raw); if (r && client_ok(r)) message("Underlag", "Sparat."); @@ -2663,7 +2663,7 @@ static void audit_screen(struct app *a) return; if (sel == 0) { char *resp = - client_rpc(a->fd, "audit.verify", a->session, a->org, "{}"); + client_rpc(&a->conn, "audit.verify", a->session, a->org, "{}"); if (!resp || !client_ok(resp)) { show_error("Revision", resp); free(resp); @@ -2677,7 +2677,7 @@ static void audit_screen(struct app *a) free(resp); } else { for (;;) { - char *resp = client_rpc(a->fd, "audit.list", a->session, + char *resp = client_rpc(&a->conn, "audit.list", a->session, a->org, "{\"limit\":200}"); if (!resp || !client_ok(resp)) { show_error("Revision", resp); @@ -2779,7 +2779,7 @@ static int template_form(struct app *a, const char *load_name) char *args = yyjson_mut_write(d, 0, NULL); yyjson_mut_doc_free(d); char *resp = - client_rpc(a->fd, "template.get", a->session, a->org, args); + client_rpc(&a->conn, "template.get", a->session, a->org, args); free(args); if (!resp || !client_ok(resp)) { show_error("Mall", resp); @@ -3037,7 +3037,7 @@ static int template_form(struct app *a, const char *load_name) char *args = yyjson_mut_write(d, 0, NULL); yyjson_mut_doc_free(d); const char *cmd = tpl_id ? "template.update" : "template.create"; - char *resp = client_rpc(a->fd, cmd, a->session, a->org, args); + char *resp = client_rpc(&a->conn, cmd, a->session, a->org, args); free(args); if (resp && client_ok(resp)) { if (ch == KEY_F(5)) { @@ -3106,7 +3106,7 @@ static void template_archive_ui(struct app *a) char *args = yyjson_mut_write(d, 0, NULL); yyjson_mut_doc_free(d); char *resp = - client_rpc(a->fd, "template.archive", a->session, a->org, args); + client_rpc(&a->conn, "template.archive", a->session, a->org, args); free(args); if (resp && client_ok(resp)) message("Mall", "Mallen '%s' arkiverad.", name); @@ -3170,7 +3170,7 @@ static int ib_load(struct app *a, char ***out_acc, int64_t **out_amt, "\"limit\":200}", (long long)a->fy); char *resp = - client_rpc(a->fd, "voucher.list", a->session, a->org, args); + client_rpc(&a->conn, "voucher.list", a->session, a->org, args); if (!resp || !client_ok(resp)) { show_error("Ingående balans", resp); free(resp); @@ -3188,7 +3188,7 @@ static int ib_load(struct app *a, char ***out_acc, int64_t **out_amt, continue; char vargs[64]; snprintf(vargs, sizeof vargs, "{\"id\":%lld}", (long long)id); - char *v = client_rpc(a->fd, "voucher.get", a->session, a->org, vargs); + char *v = client_rpc(&a->conn, "voucher.get", a->session, a->org, vargs); if (!v || !client_ok(v)) { free(v); continue; @@ -3474,7 +3474,7 @@ static int ib_form(struct app *a, char **old_acc, int64_t *old_amt, int nold) char *args = yyjson_mut_write(d, 0, NULL); yyjson_mut_doc_free(d); char *resp = - client_rpc(a->fd, "voucher.post", a->session, a->org, args); + client_rpc(&a->conn, "voucher.post", a->session, a->org, args); free(args); if (resp && client_ok(resp)) { int64_t num = jint_val(resp, "result.number", 0); @@ -3618,7 +3618,7 @@ static void settings_screen(struct app *a) if (g_quit) return; char *resp = - client_rpc(a->fd, "settings.get", a->session, a->org, "{}"); + client_rpc(&a->conn, "settings.get", a->session, a->org, "{}"); if (!resp || !client_ok(resp)) { show_error("Inställningar", resp); free(resp); @@ -3673,7 +3673,7 @@ static void settings_screen(struct app *a) yyjson_mut_obj_add_strcpy(d, o, "value", val); char *args = yyjson_mut_write(d, 0, NULL); yyjson_mut_doc_free(d); - char *r = client_rpc(a->fd, "settings.set", a->session, + char *r = client_rpc(&a->conn, "settings.set", a->session, a->org, args); free(args); if (r && client_ok(r)) { @@ -3731,7 +3731,7 @@ static void templates_screen(struct app *a) continue; } for (;;) { - char *resp = client_rpc(a->fd, "template.list", a->session, + char *resp = client_rpc(&a->conn, "template.list", a->session, a->org, "{\"active_only\":true}"); if (!resp || !client_ok(resp)) { show_error("Mallar", resp); @@ -3936,22 +3936,26 @@ static int login_screen(struct app *a) /* attempt login */ snprintf(a->socket, sizeof a->socket, "%s", socket_path); - a->fd = client_connect(a->socket); - if (a->fd < 0) { - message("Fel", "Kunde inte ansluta till %s", a->socket); + if (client_connect(a->socket, &a->conn) != 0) { + message("Fel", "Kunde inte ansluta till %s: %s", a->socket, + client_last_error()); continue; } char *err = NULL, *session = NULL; - if (client_login(a->fd, user, pass, &session, &err) != 0) { - char *code = jstr_dup(err, "error.code"); - char *msg = jstr_dup(err, "error.message"); - message("Inloggning misslyckades", "%s: %s", - code ? code : "fel", msg ? msg : "okänt fel"); - free(code); - free(msg); + if (client_login(&a->conn, user, pass, &session, &err) != 0) { + if (err && err[0] == '{') { + char *code = jstr_dup(err, "error.code"); + char *msg = jstr_dup(err, "error.message"); + message("Inloggning misslyckades", "%s: %s", + code ? code : "fel", msg ? msg : "okänt fel"); + free(code); + free(msg); + } else { + message("Inloggning misslyckades", "%s", + err ? err : "transportfel"); + } free(err); - close(a->fd); - a->fd = -1; + client_close(&a->conn); continue; } snprintf(a->session, sizeof a->session, "%s", session); @@ -3963,7 +3967,7 @@ static int login_screen(struct app *a) static int select_org(struct app *a) { - char *resp = client_rpc(a->fd, "session.list_orgs", a->session, 0, "{}"); + char *resp = client_rpc(&a->conn, "session.list_orgs", a->session, 0, "{}"); if (!resp || !client_ok(resp)) { show_error("Organisationer", resp); free(resp); @@ -4010,7 +4014,7 @@ static void usage(FILE *f) { fprintf(f, "usage: bokftui [options]\n" - " --socket TARGET unix socket or tcp:host:port\n" + " --socket TARGET unix path, tcp:host:port or tls:host:port\n" " --user NAME prefill username\n" " --org ID select org directly\n" " --version\n"); @@ -4020,7 +4024,7 @@ int main(int argc, char **argv) { struct app app; memset(&app, 0, sizeof app); - app.fd = -1; + app.conn.fd = -1; const char *socket = getenv("BOKFD_SOCKET"); if (!socket) socket = "/run/bokfd/bokfd.sock"; @@ -4068,9 +4072,9 @@ int main(int argc, char **argv) update_status(&app); dashboard(&app); - client_rpc(app.fd, "session.close", app.session, 0, "{}"); + client_rpc(&app.conn, "session.close", app.session, 0, "{}"); acct_cache_free(); - close(app.fd); + client_close(&app.conn); endwin(); return 0; } diff --git a/clients/client.c b/clients/client.c index ea5b906..8fb604b 100644 --- a/clients/client.c +++ b/clients/client.c @@ -1,7 +1,11 @@ #include "client.h" +#include <arpa/inet.h> #include <errno.h> #include <netdb.h> +#include <openssl/err.h> +#include <openssl/ssl.h> +#include <openssl/x509v3.h> #include <stdio.h> #include <stdlib.h> #include <string.h> @@ -12,6 +16,28 @@ #include "util.h" #include "yyjson.h" +static char g_last_error[256]; + +const char *client_last_error(void) +{ + return g_last_error[0] ? g_last_error : "connection error"; +} + +static void set_error(const char *msg) +{ + snprintf(g_last_error, sizeof g_last_error, "%s", msg); +} + +static void set_tls_error(const char *what) +{ + unsigned long e = ERR_get_error(); + char buf[200] = "unknown TLS error"; + if (e) + ERR_error_string_n(e, buf, sizeof buf); + snprintf(g_last_error, sizeof g_last_error, "%s: %s", what, buf); + ERR_clear_error(); +} + static ssize_t write_all(int fd, const char *buf, size_t len) { size_t off = 0; @@ -27,63 +53,34 @@ static ssize_t write_all(int fd, const char *buf, size_t len) return (ssize_t)off; } -int client_send_line(int fd, const char *line) +static void split_addrport(const char *addrport, char *host, size_t host_sz, + char *port, size_t port_sz, const char *def_port) { - if (write_all(fd, line, strlen(line)) < 0) - return -1; - return write_all(fd, "\n", 1) < 0 ? -1 : 0; -} - -char *client_read_line(int fd) -{ - struct buf b; - buf_init(&b); - char chunk[4096]; - for (;;) { - ssize_t r = read(fd, chunk, sizeof chunk); - if (r < 0) { - if (errno == EINTR) - continue; - buf_free(&b); - return NULL; - } - if (r == 0) - break; - unsigned char *nl = memchr(chunk, '\n', (size_t)r); - if (nl) { - buf_append(&b, chunk, (size_t)(nl - (unsigned char *)chunk)); - break; - } - buf_append(&b, chunk, (size_t)r); - } - char *out = xmalloc(b.len + 1); - memcpy(out, b.p ? (char *)b.p : "", b.len); - out[b.len] = '\0'; - buf_free(&b); - return out; -} - -static int tcp_connect_addr(const char *addrport) -{ - char host[256] = "127.0.0.1"; - char port[16] = "8787"; + snprintf(host, host_sz, "127.0.0.1"); + snprintf(port, port_sz, "%s", def_port); const char *colon = strrchr(addrport, ':'); if (colon) { size_t hl = (size_t)(colon - addrport); - if (hl < sizeof host) { + if (hl < host_sz) { memcpy(host, addrport, hl); host[hl] = '\0'; } - snprintf(port, sizeof port, "%s", colon + 1); - } else { - snprintf(port, sizeof port, "%s", addrport); + snprintf(port, port_sz, "%s", colon + 1); + } else if (*addrport) { + snprintf(port, port_sz, "%s", addrport); } +} + +static int connect_tcp(const char *host, const char *port) +{ struct addrinfo hints, *res = NULL; memset(&hints, 0, sizeof hints); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; - if (getaddrinfo(host, port, &hints, &res) != 0) + if (getaddrinfo(host, port, &hints, &res) != 0) { + set_error("cannot resolve host"); return -1; + } int fd = -1; for (struct addrinfo *ai = res; ai; ai = ai->ai_next) { fd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol); @@ -94,30 +91,217 @@ static int tcp_connect_addr(const char *addrport) close(fd); fd = -1; } + if (fd < 0) + set_error(strerror(errno)); freeaddrinfo(res); return fd; } -int client_connect(const char *target) +static int set_verify_host(SSL *ssl, const char *host) +{ + X509_VERIFY_PARAM *param = SSL_get0_param(ssl); + struct in_addr in4; + struct in6_addr in6; + X509_VERIFY_PARAM_set_hostflags(param, + X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS); + if (inet_pton(AF_INET, host, &in4) == 1 || + inet_pton(AF_INET6, host, &in6) == 1) + return X509_VERIFY_PARAM_set1_ip_asc(param, host); + return X509_VERIFY_PARAM_set1_host(param, host, 0); +} + +static int tls_connect_addr(const char *addrport, struct client_conn *out) { - if (strncmp(target, "tcp:", 4) == 0) - return tcp_connect_addr(target + 4); + char host[256], port[16]; + split_addrport(addrport, host, sizeof host, port, sizeof port, "8788"); + int fd = connect_tcp(host, port); + if (fd < 0) + return -1; + + SSL_CTX *ctx = SSL_CTX_new(TLS_client_method()); + if (!ctx) { + set_tls_error("TLS context"); + close(fd); + return -1; + } + SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION); + SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL); + SSL_CTX_set_default_verify_paths(ctx); + const char *ca = getenv("BOKFD_TLS_CA"); + if (ca && *ca && SSL_CTX_load_verify_locations(ctx, ca, NULL) != 1) { + set_tls_error("TLS CA file"); + SSL_CTX_free(ctx); + close(fd); + return -1; + } + SSL *ssl = SSL_new(ctx); + if (!ssl) { + set_tls_error("TLS connection"); + SSL_CTX_free(ctx); + close(fd); + return -1; + } + SSL_set_fd(ssl, fd); + SSL_set_tlsext_host_name(ssl, host); + if (set_verify_host(ssl, host) != 1) { + set_error("invalid TLS host name"); + SSL_free(ssl); + SSL_CTX_free(ctx); + close(fd); + return -1; + } + if (SSL_connect(ssl) != 1) { + long vr = SSL_get_verify_result(ssl); + if (vr != X509_V_OK) + snprintf(g_last_error, sizeof g_last_error, + "certificate verification failed: %s", + X509_verify_cert_error_string(vr)); + else + set_tls_error("TLS handshake"); + SSL_free(ssl); + SSL_CTX_free(ctx); + close(fd); + return -1; + } + out->fd = fd; + out->ssl = ssl; + out->ctx = ctx; + return 0; +} + +int client_connect(const char *target, struct client_conn *out) +{ + memset(out, 0, sizeof *out); + out->fd = -1; + g_last_error[0] = '\0'; + + if (strncmp(target, "tls:", 4) == 0) + return tls_connect_addr(target + 4, out); + if (strncmp(target, "tcp:", 4) == 0) { + char host[256], port[16]; + split_addrport(target + 4, host, sizeof host, port, sizeof port, + "8787"); + out->fd = connect_tcp(host, port); + return out->fd < 0 ? -1 : 0; + } + struct sockaddr_un sa; memset(&sa, 0, sizeof sa); sa.sun_family = AF_UNIX; if (strlen(target) >= sizeof sa.sun_path) { + set_error("socket path too long"); errno = ENAMETOOLONG; return -1; } snprintf(sa.sun_path, sizeof sa.sun_path, "%s", target); int fd = socket(AF_UNIX, SOCK_STREAM, 0); - if (fd < 0) + if (fd < 0) { + set_error(strerror(errno)); return -1; + } if (connect(fd, (struct sockaddr *)&sa, sizeof sa) != 0) { + set_error(strerror(errno)); close(fd); return -1; } - return fd; + out->fd = fd; + return 0; +} + +void client_close(struct client_conn *c) +{ + if (c->ssl) { + SSL_shutdown(c->ssl); + SSL_free(c->ssl); + c->ssl = NULL; + } + if (c->ctx) { + SSL_CTX_free(c->ctx); + c->ctx = NULL; + } + if (c->fd >= 0) { + close(c->fd); + c->fd = -1; + } +} + +int client_send_line(struct client_conn *c, const char *line) +{ + size_t len = strlen(line); + if (c->ssl) { + size_t off = 0; + while (off < len) { + int n = SSL_write(c->ssl, line + off, (int)(len - off)); + if (n <= 0) { + int e = SSL_get_error(c->ssl, n); + if (e == SSL_ERROR_WANT_READ || e == SSL_ERROR_WANT_WRITE) + set_error("TLS write would block"); + else + set_tls_error("TLS write"); + return -1; + } + off += (size_t)n; + } + int n = SSL_write(c->ssl, "\n", 1); + if (n <= 0) { + set_tls_error("TLS write"); + return -1; + } + return 0; + } + if (write_all(c->fd, line, len) < 0) + return -1; + return write_all(c->fd, "\n", 1) < 0 ? -1 : 0; +} + +char *client_read_line(struct client_conn *c) +{ + struct buf b; + buf_init(&b); + char chunk[4096]; + for (;;) { + ssize_t r; + if (c->ssl) { + int n = SSL_read(c->ssl, chunk, sizeof chunk); + if (n > 0) { + r = n; + } else { + int e = SSL_get_error(c->ssl, n); + if (e == SSL_ERROR_ZERO_RETURN) { + r = 0; + } else { + if (e == SSL_ERROR_WANT_READ || e == SSL_ERROR_WANT_WRITE) + set_error("TLS read would block"); + else + set_tls_error("TLS read"); + buf_free(&b); + return NULL; + } + } + } else { + r = read(c->fd, chunk, sizeof chunk); + if (r < 0) { + if (errno == EINTR) + continue; + set_error(strerror(errno)); + buf_free(&b); + return NULL; + } + } + if (r == 0) + break; + unsigned char *nl = memchr(chunk, '\n', (size_t)r); + if (nl) { + buf_append(&b, chunk, (size_t)(nl - (unsigned char *)chunk)); + break; + } + buf_append(&b, chunk, (size_t)r); + } + char *out = xmalloc(b.len + 1); + memcpy(out, b.p ? (char *)b.p : "", b.len); + out[b.len] = '\0'; + buf_free(&b); + return out; } char *client_make_request(const char *cmd, const char *session, int64_t org, @@ -164,20 +348,20 @@ char *client_make_login_args(const char *user, const char *password) return s; } -char *client_rpc(int fd, const char *cmd, const char *session, int64_t org, - const char *args_json) +char *client_rpc(struct client_conn *c, const char *cmd, const char *session, + int64_t org, const char *args_json) { char *req = client_make_request(cmd, session, org, args_json, "rpc"); if (!req) return NULL; - int rc = client_send_line(fd, req); + int rc = client_send_line(c, req); free(req); if (rc != 0) return NULL; - return client_read_line(fd); + return client_read_line(c); } -int client_login(int fd, const char *user, const char *password, +int client_login(struct client_conn *c, const char *user, const char *password, char **session_out, char **err_out) { *session_out = NULL; @@ -187,10 +371,10 @@ int client_login(int fd, const char *user, const char *password, *err_out = xstrdup("could not build login request"); return -1; } - char *resp = client_rpc(fd, "session.open", NULL, 0, args); + char *resp = client_rpc(c, "session.open", NULL, 0, args); free(args); if (!resp) { - *err_out = xstrdup(strerror(errno)); + *err_out = xstrdup(client_last_error()); return -1; } if (!client_ok(resp)) { diff --git a/clients/client.h b/clients/client.h index 7e18a23..9372e72 100644 --- a/clients/client.h +++ b/clients/client.h @@ -4,11 +4,23 @@ #include <stdint.h> /* Thin protocol client shared by bokfctl and bokftui. Connects to a unix - socket path or "tcp:host:port". */ + socket path, "tcp:host:port" (plaintext) or "tls:host:port". The TLS + client verifies the certificate chain and host name; BOKFD_TLS_CA adds a + PEM file to the trust store (for private CAs and tests). */ -int client_connect(const char *target); -int client_send_line(int fd, const char *line); -char *client_read_line(int fd); +struct client_conn { + int fd; + void *ssl; + void *ctx; +}; + +int client_connect(const char *target, struct client_conn *out); +void client_close(struct client_conn *c); +int client_send_line(struct client_conn *c, const char *line); +char *client_read_line(struct client_conn *c); + +/* Human-readable reason for the last failed call. */ +const char *client_last_error(void); char *client_make_request(const char *cmd, const char *session, int64_t org, const char *args_json, const char *id); @@ -16,12 +28,12 @@ char *client_make_login_args(const char *user, const char *password); /* Sends one command and returns the raw response line (malloc'd), or NULL on a transport error. */ -char *client_rpc(int fd, const char *cmd, const char *session, int64_t org, - const char *args_json); +char *client_rpc(struct client_conn *c, const char *cmd, const char *session, + int64_t org, const char *args_json); /* Password login. Returns 0 and sets *session_out on success; on failure returns -1 and sets *err_out to the response line or an error message. */ -int client_login(int fd, const char *user, const char *password, +int client_login(struct client_conn *c, const char *user, const char *password, char **session_out, char **err_out); /* Convenience: true when the response line has "ok":true. */ |
