summaryrefslogtreecommitdiff
path: root/clients/bokfctl.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 /clients/bokfctl.c
downloadbokf-0.1.0.tar.gz
bokf-0.1.0.zip
Initial commit: daemon, clients, docs, Docker deploy pipelinev0.1.0
Diffstat (limited to 'clients/bokfctl.c')
-rw-r--r--clients/bokfctl.c344
1 files changed, 344 insertions, 0 deletions
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 <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 "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"
+ " (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;
+}