258 lines
9.6 KiB
C
258 lines
9.6 KiB
C
/* fips_control.c — synchronous FIPS JSON-line control client */
|
|
|
|
#define _POSIX_C_SOURCE 200809L
|
|
|
|
#include "fips_control.h"
|
|
#include "cjson/cJSON.h"
|
|
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <poll.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/un.h>
|
|
#include <unistd.h>
|
|
|
|
#define FIPS_RESPONSE_MAX (1024 * 1024)
|
|
|
|
static void set_error(char *out, size_t size, const char *format, const char *detail) {
|
|
if (out && size) snprintf(out, size, format, detail ? detail : "unknown error");
|
|
}
|
|
|
|
static int wait_fd(int fd, short events, int timeout_ms) {
|
|
struct pollfd pfd = { fd, events, 0 };
|
|
int rc;
|
|
do { rc = poll(&pfd, 1, timeout_ms); } while (rc < 0 && errno == EINTR);
|
|
return rc > 0 && (pfd.revents & events) ? 0 : -1;
|
|
}
|
|
|
|
int fips_control_connect(const char *socket_path, int timeout_ms,
|
|
char *error, size_t error_size) {
|
|
if (!socket_path || !socket_path[0]) {
|
|
set_error(error, error_size, "%s", "empty FIPS control socket path");
|
|
return -1;
|
|
}
|
|
int fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
|
|
if (fd < 0) {
|
|
set_error(error, error_size, "FIPS socket failed: %s", strerror(errno));
|
|
return -1;
|
|
}
|
|
struct sockaddr_un addr;
|
|
memset(&addr, 0, sizeof(addr));
|
|
addr.sun_family = AF_UNIX;
|
|
if (strlen(socket_path) >= sizeof(addr.sun_path)) {
|
|
close(fd);
|
|
set_error(error, error_size, "%s", "FIPS control socket path too long");
|
|
return -1;
|
|
}
|
|
snprintf(addr.sun_path, sizeof(addr.sun_path), "%s", socket_path);
|
|
int flags = fcntl(fd, F_GETFL, 0);
|
|
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
|
|
int rc = connect(fd, (struct sockaddr *)&addr, sizeof(addr));
|
|
if (rc != 0 && errno == EINPROGRESS) {
|
|
if (wait_fd(fd, POLLOUT, timeout_ms) == 0) {
|
|
int socket_error = 0;
|
|
socklen_t len = sizeof(socket_error);
|
|
if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &socket_error, &len) == 0 &&
|
|
socket_error == 0) rc = 0;
|
|
else { errno = socket_error; rc = -1; }
|
|
} else rc = -1;
|
|
}
|
|
if (rc != 0) {
|
|
set_error(error, error_size, "FIPS connect failed: %s", strerror(errno));
|
|
close(fd);
|
|
return -1;
|
|
}
|
|
fcntl(fd, F_SETFL, flags);
|
|
return fd;
|
|
}
|
|
|
|
static int write_all(int fd, const char *text) {
|
|
size_t length = strlen(text), sent = 0;
|
|
while (sent < length) {
|
|
if (wait_fd(fd, POLLOUT, 1500) != 0) return -1;
|
|
ssize_t n = send(fd, text + sent, length - sent, MSG_NOSIGNAL);
|
|
if (n < 0 && errno == EINTR) continue;
|
|
if (n <= 0) return -1;
|
|
sent += (size_t)n;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static cJSON *request(int fd, cJSON *request_obj, char *error, size_t error_size) {
|
|
char *json = cJSON_PrintUnformatted(request_obj);
|
|
if (!json) {
|
|
set_error(error, error_size, "%s", "cannot serialize FIPS request");
|
|
return NULL;
|
|
}
|
|
char *line = g_strdup_printf("%s\n", json);
|
|
cJSON_free(json);
|
|
if (write_all(fd, line) != 0) {
|
|
g_free(line);
|
|
set_error(error, error_size, "FIPS write failed: %s", strerror(errno));
|
|
return NULL;
|
|
}
|
|
g_free(line);
|
|
|
|
GString *response = g_string_sized_new(4096);
|
|
while (response->len < FIPS_RESPONSE_MAX) {
|
|
if (wait_fd(fd, POLLIN, 1500) != 0) {
|
|
g_string_free(response, TRUE);
|
|
set_error(error, error_size, "%s", "FIPS response timed out");
|
|
return NULL;
|
|
}
|
|
char buffer[4096];
|
|
ssize_t n = recv(fd, buffer, sizeof(buffer), 0);
|
|
if (n < 0 && errno == EINTR) continue;
|
|
if (n <= 0) {
|
|
g_string_free(response, TRUE);
|
|
set_error(error, error_size, "%s", "FIPS closed control connection");
|
|
return NULL;
|
|
}
|
|
char *newline = memchr(buffer, '\n', (size_t)n);
|
|
if (newline) {
|
|
g_string_append_len(response, buffer, newline - buffer);
|
|
break;
|
|
}
|
|
g_string_append_len(response, buffer, n);
|
|
}
|
|
cJSON *root = cJSON_Parse(response->str);
|
|
g_string_free(response, TRUE);
|
|
if (!root) {
|
|
set_error(error, error_size, "%s", "invalid FIPS JSON response");
|
|
return NULL;
|
|
}
|
|
cJSON *status = cJSON_GetObjectItemCaseSensitive(root, "status");
|
|
if (!cJSON_IsString(status) || strcmp(status->valuestring, "ok") != 0) {
|
|
cJSON *message = cJSON_GetObjectItemCaseSensitive(root, "message");
|
|
set_error(error, error_size, "FIPS control error: %s",
|
|
cJSON_IsString(message) ? message->valuestring : "request failed");
|
|
cJSON_Delete(root);
|
|
return NULL;
|
|
}
|
|
return root;
|
|
}
|
|
|
|
static void copy_json_string(cJSON *object, const char *name, char *out, size_t size) {
|
|
cJSON *item = cJSON_GetObjectItemCaseSensitive(object, name);
|
|
if (cJSON_IsString(item) && size) snprintf(out, size, "%s", item->valuestring);
|
|
}
|
|
|
|
int fips_control_show_status(int fd, fips_status_t *status,
|
|
char *error, size_t error_size) {
|
|
if (!status) return -1;
|
|
cJSON *cmd = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(cmd, "command", "show_status");
|
|
cJSON *root = request(fd, cmd, error, error_size);
|
|
cJSON_Delete(cmd);
|
|
if (!root) return -1;
|
|
cJSON *data = cJSON_GetObjectItemCaseSensitive(root, "data");
|
|
if (!cJSON_IsObject(data)) {
|
|
cJSON_Delete(root);
|
|
set_error(error, error_size, "%s", "FIPS status response lacks data");
|
|
return -1;
|
|
}
|
|
memset(status, 0, sizeof(*status));
|
|
copy_json_string(data, "npub", status->npub, sizeof(status->npub));
|
|
copy_json_string(data, "tun_state", status->tun_state, sizeof(status->tun_state));
|
|
copy_json_string(data, "tun_name", status->tun_name, sizeof(status->tun_name));
|
|
copy_json_string(data, "state", status->state, sizeof(status->state));
|
|
copy_json_string(data, "tree_state", status->tree_state, sizeof(status->tree_state));
|
|
if (!status->tree_state[0]) copy_json_string(data, "tree", status->tree_state, sizeof(status->tree_state));
|
|
cJSON *peers = cJSON_GetObjectItemCaseSensitive(data, "peer_count");
|
|
if (cJSON_IsNumber(peers)) status->peer_count = peers->valueint;
|
|
status->tun_active = strcmp(status->tun_state, "active") == 0 ||
|
|
strcmp(status->tun_state, "up") == 0;
|
|
cJSON_Delete(root);
|
|
return 0;
|
|
}
|
|
|
|
char *fips_control_show_peers(int fd, char *error, size_t error_size) {
|
|
cJSON *cmd = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(cmd, "command", "show_peers");
|
|
cJSON *root = request(fd, cmd, error, error_size);
|
|
cJSON_Delete(cmd);
|
|
if (!root) return NULL;
|
|
cJSON *data = cJSON_GetObjectItemCaseSensitive(root, "data");
|
|
char *printed = data ? cJSON_PrintUnformatted(data) : NULL;
|
|
char *result = printed ? g_strdup(printed) : NULL;
|
|
cJSON_free(printed);
|
|
cJSON_Delete(root);
|
|
return result;
|
|
}
|
|
|
|
/* Generic single-command "show_*" that returns the data field as
|
|
* compact JSON. Used by show_tree and show_identity_cache, which have
|
|
* the same response shape as show_peers. */
|
|
static char *fips_control_show_generic(int fd, const char *command,
|
|
char *error, size_t error_size) {
|
|
cJSON *cmd = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(cmd, "command", command);
|
|
cJSON *root = request(fd, cmd, error, error_size);
|
|
cJSON_Delete(cmd);
|
|
if (!root) return NULL;
|
|
cJSON *data = cJSON_GetObjectItemCaseSensitive(root, "data");
|
|
char *printed = data ? cJSON_PrintUnformatted(data) : NULL;
|
|
char *result = printed ? g_strdup(printed) : NULL;
|
|
cJSON_free(printed);
|
|
cJSON_Delete(root);
|
|
return result;
|
|
}
|
|
|
|
char *fips_control_show_tree(int fd, char *error, size_t error_size) {
|
|
return fips_control_show_generic(fd, "show_tree", error, error_size);
|
|
}
|
|
|
|
char *fips_control_show_identity_cache(int fd, char *error, size_t error_size) {
|
|
return fips_control_show_generic(fd, "show_identity_cache", error, error_size);
|
|
}
|
|
|
|
int fips_control_connect_peer(int fd, const char *npub, const char *address,
|
|
const char *transport,
|
|
char *error, size_t error_size) {
|
|
if (!npub || !npub[0] || !address || !address[0]) {
|
|
set_error(error, error_size, "%s", "FIPS peer npub and address are required");
|
|
return -1;
|
|
}
|
|
cJSON *cmd = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(cmd, "command", "connect");
|
|
cJSON *params = cJSON_AddObjectToObject(cmd, "params");
|
|
cJSON_AddStringToObject(params, "npub", npub);
|
|
cJSON_AddStringToObject(params, "address", address);
|
|
cJSON_AddStringToObject(params, "transport", transport && transport[0] ? transport : "udp");
|
|
cJSON *root = request(fd, cmd, error, error_size);
|
|
cJSON_Delete(cmd);
|
|
if (!root) return -1;
|
|
cJSON_Delete(root);
|
|
return 0;
|
|
}
|
|
|
|
int fips_control_disconnect_peer(int fd, const char *npub,
|
|
char *error, size_t error_size) {
|
|
if (!npub || !npub[0]) {
|
|
set_error(error, error_size, "%s", "FIPS peer npub is required");
|
|
return -1;
|
|
}
|
|
cJSON *cmd = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(cmd, "command", "disconnect");
|
|
cJSON *params = cJSON_AddObjectToObject(cmd, "params");
|
|
cJSON_AddStringToObject(params, "npub", npub);
|
|
cJSON *root = request(fd, cmd, error, error_size);
|
|
cJSON_Delete(cmd);
|
|
if (!root) return -1;
|
|
cJSON_Delete(root);
|
|
return 0;
|
|
}
|
|
|
|
void fips_control_close(int *fd) {
|
|
if (fd && *fd >= 0) { close(*fd); *fd = -1; }
|
|
}
|
|
|
|
gboolean fips_control_socket_available(const char *socket_path, int timeout_ms) {
|
|
int fd = fips_control_connect(socket_path, timeout_ms, NULL, 0);
|
|
if (fd < 0) return FALSE;
|
|
close(fd);
|
|
return TRUE;
|
|
}
|