From 380195f7cd5e57acf2c1cf2bc41069e6b0b979ed Mon Sep 17 00:00:00 2001 From: Anders Betts Date: Thu, 17 Sep 2026 19:55:36 +0200 Subject: Initial commit: daemon, clients, docs, Docker deploy pipeline --- clients/bokfctl.c | 344 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 344 insertions(+) create mode 100644 clients/bokfctl.c (limited to 'clients/bokfctl.c') diff --git a/clients/bokfctl.c b/clients/bokfctl.c new file mode 100644 index 0000000..c541fec --- /dev/null +++ b/clients/bokfctl.c @@ -0,0 +1,344 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#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] [args-json]\n" + "\n" + "options:\n" + " --socket TARGET unix socket path or tcp: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" + " --org ID active org for this request\n" + " --version\n" + "\n" + "examples:\n" + " bokfctl health\n" + " bokfctl org.create '{\"name\":\"AB Ett\"}'\n" + " bokfctl describe\n"); +} + +int main(int argc, char **argv) +{ + const char *target = getenv("BOKFD_SOCKET"); + if (!target) + target = "/run/bokfd/bokfd.sock"; + const char *user = getenv("BOKFD_USER"); + const char *password = getenv("BOKFD_PASSWORD"); + int64_t org = 0; + const char *cmd = NULL; + const char *args_json = NULL; + + for (int i = 1; i < argc; i++) { + const char *a = argv[i]; + const char *val = NULL; + if (!strncmp(a, "--socket", 8) && + (a[8] == '\0' || a[8] == '=')) { + val = a[8] == '=' ? a + 9 : (i + 1 < argc ? argv[++i] : NULL); + if (!val) { + fprintf(stderr, "bokfctl: --socket requires a value\n"); + return 2; + } + target = val; + } else if (!strncmp(a, "--user", 6) && + (a[6] == '\0' || a[6] == '=')) { + val = a[6] == '=' ? a + 7 : (i + 1 < argc ? argv[++i] : NULL); + if (!val) { + fprintf(stderr, "bokfctl: --user requires a value\n"); + return 2; + } + user = val; + } else if (!strncmp(a, "--password", 10) && + (a[10] == '\0' || a[10] == '=')) { + val = a[10] == '=' ? a + 11 : (i + 1 < argc ? argv[++i] : NULL); + if (!val) { + fprintf(stderr, "bokfctl: --password requires a value\n"); + return 2; + } + password = val; + } else if (!strncmp(a, "--org", 5) && + (a[5] == '\0' || a[5] == '=')) { + val = a[5] == '=' ? a + 6 : (i + 1 < argc ? argv[++i] : NULL); + if (!val) { + fprintf(stderr, "bokfctl: --org requires a value\n"); + return 2; + } + org = strtoll(val, NULL, 10); + } else if (strcmp(a, "--version") == 0) { + printf("bokfctl %s\n", BOKF_VERSION); + return 0; + } else if (strcmp(a, "--help") == 0 || strcmp(a, "-h") == 0) { + usage(); + return 0; + } else if (a[0] == '-' && a[1] != '\0') { + fprintf(stderr, "bokfctl: unknown option %s\n", a); + usage(); + return 2; + } else if (!cmd) { + cmd = a; + } else if (!args_json) { + args_json = a; + } else { + fprintf(stderr, "bokfctl: unexpected argument %s\n", a); + return 2; + } + } + if (!cmd) { + usage(); + return 2; + } + + int fd = connect_target(target); + if (fd < 0) { + fprintf(stderr, "bokfctl: cannot connect to %s: %s\n", target, + strerror(errno)); + return 2; + } + + char session[128] = ""; + if (strcmp(cmd, "health") != 0 && strcmp(cmd, "meta") != 0 && + strcmp(cmd, "session.open") != 0) { + if (!user || !password) { + fprintf(stderr, + "bokfctl: set BOKFD_USER and BOKFD_PASSWORD (or --user/--password) to log in\n"); + close(fd); + 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; + } + 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); + return 2; + } + req = xstrdup(args_json); + } else { + req = make_request(cmd, session[0] ? session : NULL, org, args_json, + "cli"); + if (!req) { + fprintf(stderr, "bokfctl: args must be a JSON object\n"); + close(fd); + return 2; + } + } + if (write_all(fd, req, strlen(req)) < 0 || write_all(fd, "\n", 1) < 0) { + fprintf(stderr, "bokfctl: send failed\n"); + free(req); + close(fd); + return 2; + } + free(req); + + char *resp = read_line_fd(fd); + close(fd); + if (!resp) { + fprintf(stderr, "bokfctl: no response\n"); + return 2; + } + yyjson_doc *rd = yyjson_read(resp, strlen(resp), 0); + char *pretty = rd ? yyjson_write(rd, YYJSON_WRITE_PRETTY, NULL) : NULL; + printf("%s\n", pretty ? pretty : resp); + int ok = rd && yyjson_is_obj(yyjson_doc_get_root(rd)) && + yyjson_get_bool(yyjson_obj_get(yyjson_doc_get_root(rd), "ok")); + if (rd) + yyjson_doc_free(rd); + free(pretty); + free(resp); + return ok ? 0 : 1; +} -- cgit v1.3