summaryrefslogtreecommitdiff
path: root/clients
diff options
context:
space:
mode:
Diffstat (limited to 'clients')
-rw-r--r--clients/bokfctl.c236
-rw-r--r--clients/bokftui.c112
-rw-r--r--clients/client.c296
-rw-r--r--clients/client.h26
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. */