#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; }