summaryrefslogtreecommitdiff
path: root/clients/client.c
diff options
context:
space:
mode:
authorAnders Betts <anders.betts@gmail.com>2026-09-17 21:26:20 +0200
committerAnders Betts <anders.betts@gmail.com>2026-09-17 21:26:20 +0200
commited1c15929d2eb2dbc6432986c26661bf1549964a (patch)
tree4f04934edef686b19d16e1bb2f79bb5c15e142de /clients/client.c
parent380195f7cd5e57acf2c1cf2bc41069e6b0b979ed (diff)
downloadbokf-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/client.c')
-rw-r--r--clients/client.c296
1 files changed, 240 insertions, 56 deletions
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)) {