Files
n_signer/src/server.c
T
2026-05-02 12:31:26 -04:00

367 lines
9.3 KiB
C

#define _GNU_SOURCE
#include "server.h"
#include "enforcement.h"
#include "selector.h"
#include "cjson/cJSON.h"
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
static int read_full(int fd, void *buf, size_t len) {
unsigned char *p = (unsigned char *)buf;
size_t off = 0;
while (off < len) {
ssize_t n = read(fd, p + off, len - off);
if (n == 0) {
return -1;
}
if (n < 0) {
if (errno == EINTR) {
continue;
}
return -1;
}
off += (size_t)n;
}
return 0;
}
static int write_full(int fd, const void *buf, size_t len) {
const unsigned char *p = (const unsigned char *)buf;
size_t off = 0;
while (off < len) {
ssize_t n = write(fd, p + off, len - off);
if (n < 0) {
if (errno == EINTR) {
continue;
}
return -1;
}
off += (size_t)n;
}
return 0;
}
static int recv_framed(int fd, char **out_payload) {
uint32_t be_len;
uint32_t len;
char *payload;
if (out_payload == NULL) {
return -1;
}
*out_payload = NULL;
if (read_full(fd, &be_len, sizeof(be_len)) != 0) {
return -1;
}
len = ntohl(be_len);
if (len == 0 || len > SERVER_MAX_MSG_SIZE) {
return -1;
}
payload = (char *)malloc((size_t)len + 1U);
if (payload == NULL) {
return -1;
}
if (read_full(fd, payload, len) != 0) {
free(payload);
return -1;
}
payload[len] = '\0';
*out_payload = payload;
return 0;
}
static int send_framed(int fd, const char *payload) {
uint32_t len;
uint32_t be_len;
if (payload == NULL) {
return -1;
}
len = (uint32_t)strlen(payload);
be_len = htonl(len);
if (write_full(fd, &be_len, sizeof(be_len)) != 0) {
return -1;
}
if (write_full(fd, payload, len) != 0) {
return -1;
}
return 0;
}
static void json_copy_string(char *dst, size_t dst_sz, const char *src, const char *fallback) {
const char *s = (src != NULL) ? src : fallback;
if (dst == NULL || dst_sz == 0) {
return;
}
if (s == NULL) {
dst[0] = '\0';
return;
}
strncpy(dst, s, dst_sz - 1);
dst[dst_sz - 1] = '\0';
}
static int extract_method_and_selector(const char *json,
char *method,
size_t method_sz,
selector_request_t *selector_req) {
cJSON *root;
cJSON *method_item;
cJSON *params_item;
cJSON *options_item;
cJSON *tmp;
if (json == NULL || method == NULL || selector_req == NULL) {
return -1;
}
method[0] = '\0';
selector_request_init(selector_req);
root = cJSON_Parse(json);
if (root == NULL) {
return -1;
}
method_item = cJSON_GetObjectItemCaseSensitive(root, "method");
if (!cJSON_IsString(method_item) || method_item->valuestring == NULL) {
cJSON_Delete(root);
return -1;
}
json_copy_string(method, method_sz, method_item->valuestring, "unknown");
params_item = cJSON_GetObjectItemCaseSensitive(root, "params");
if (cJSON_IsArray(params_item)) {
options_item = cJSON_GetArrayItem(params_item, 1);
if (cJSON_IsObject(options_item)) {
tmp = cJSON_GetObjectItemCaseSensitive(options_item, "role");
if (cJSON_IsString(tmp) && tmp->valuestring != NULL) {
selector_req->has_role = 1;
json_copy_string(selector_req->role_name, sizeof(selector_req->role_name), tmp->valuestring, "");
}
tmp = cJSON_GetObjectItemCaseSensitive(options_item, "nostr_index");
if (cJSON_IsNumber(tmp)) {
selector_req->has_nostr_index = 1;
selector_req->nostr_index = tmp->valueint;
}
tmp = cJSON_GetObjectItemCaseSensitive(options_item, "role_path");
if (cJSON_IsString(tmp) && tmp->valuestring != NULL) {
selector_req->has_role_path = 1;
json_copy_string(selector_req->role_path, sizeof(selector_req->role_path), tmp->valuestring, "");
}
}
}
cJSON_Delete(root);
return 0;
}
void server_init(server_ctx_t *ctx, const char *socket_name,
dispatcher_ctx_t *dispatcher, policy_table_t *policy) {
if (ctx == NULL) {
return;
}
memset(ctx, 0, sizeof(*ctx));
if (socket_name != NULL) {
strncpy(ctx->socket_name, socket_name, sizeof(ctx->socket_name) - 1);
ctx->socket_name[sizeof(ctx->socket_name) - 1] = '\0';
}
ctx->listen_fd = -1;
ctx->dispatcher = dispatcher;
ctx->policy = policy;
}
int server_start(server_ctx_t *ctx) {
int fd;
struct sockaddr_un addr;
socklen_t addr_len;
int flags;
if (ctx == NULL || ctx->dispatcher == NULL || ctx->policy == NULL || ctx->socket_name[0] == '\0') {
return -1;
}
fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd < 0) {
return -1;
}
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
addr.sun_path[0] = '\0';
strncpy(&addr.sun_path[1], ctx->socket_name, sizeof(addr.sun_path) - 2);
addr.sun_path[sizeof(addr.sun_path) - 1] = '\0';
addr_len = (socklen_t)(sizeof(sa_family_t) + 1 + strlen(ctx->socket_name));
if (bind(fd, (struct sockaddr *)&addr, addr_len) != 0) {
close(fd);
return -1;
}
if (listen(fd, 5) != 0) {
close(fd);
return -1;
}
flags = fcntl(fd, F_GETFL, 0);
if (flags < 0 || fcntl(fd, F_SETFL, flags | O_NONBLOCK) != 0) {
close(fd);
return -1;
}
ctx->listen_fd = fd;
ctx->running = 1;
return 0;
}
int server_get_caller(int fd, caller_identity_t *out) {
struct ucred cred;
socklen_t len = sizeof(cred);
if (out == NULL) {
return -1;
}
memset(out, 0, sizeof(*out));
if (getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &cred, &len) != 0) {
return -1;
}
out->uid = cred.uid;
out->gid = cred.gid;
out->pid = cred.pid;
(void)snprintf(out->caller_id, sizeof(out->caller_id), "uid:%u", (unsigned int)out->uid);
return 0;
}
int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data) {
int client_fd;
caller_identity_t caller;
char *request = NULL;
char *response = NULL;
char method[64];
char role_name[ROLE_NAME_MAX];
char purpose[ROLE_PURPOSE_MAX];
selector_request_t selector_req;
role_entry_t *role = NULL;
int pchk;
char activity[256];
const char *verdict = "DENIED";
if (ctx == NULL || ctx->listen_fd < 0 || ctx->dispatcher == NULL || ctx->policy == NULL) {
return -1;
}
client_fd = accept(ctx->listen_fd, NULL, NULL);
if (client_fd < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
return 0;
}
return -1;
}
if (server_get_caller(client_fd, &caller) != 0) {
close(client_fd);
return -1;
}
if (recv_framed(client_fd, &request) != 0) {
response = strdup("{\"id\":\"null\",\"error\":{\"code\":-32700,\"message\":\"parse_error\"}}");
if (response != NULL) {
(void)send_framed(client_fd, response);
free(response);
}
close(client_fd);
return 1;
}
json_copy_string(method, sizeof(method), "unknown", "unknown");
json_copy_string(role_name, sizeof(role_name), "unknown", "unknown");
json_copy_string(purpose, sizeof(purpose), "unknown", "unknown");
if (extract_method_and_selector(request, method, sizeof(method), &selector_req) == 0) {
if (ctx->dispatcher->role_table != NULL &&
selector_resolve(&selector_req, ctx->dispatcher->role_table, &role) == SELECTOR_OK &&
role != NULL) {
json_copy_string(role_name, sizeof(role_name), role->name, "main");
json_copy_string(purpose, sizeof(purpose), role_purpose_to_str(role->purpose), "nostr");
}
}
pchk = policy_check(ctx->policy, caller.caller_id, method, role_name, purpose);
if (pchk == POLICY_ALLOW) {
verdict = "ALLOWED";
response = dispatcher_handle_request(ctx->dispatcher, request);
if (response == NULL) {
response = strdup("{\"id\":\"null\",\"error\":{\"code\":-32603,\"message\":\"internal_error\"}}");
}
} else {
response = strdup("{\"id\":\"null\",\"error\":{\"code\":2001,\"message\":\"policy_denied\"}}");
}
if (response != NULL) {
(void)send_framed(client_fd, response);
}
(void)snprintf(activity,
sizeof(activity),
"uid=%u pid=%d %s(%s) %s",
(unsigned int)caller.uid,
(int)caller.pid,
method,
role_name,
verdict);
if (cb != NULL) {
cb(activity, cb_data);
}
free(request);
free(response);
close(client_fd);
return 1;
}
void server_stop(server_ctx_t *ctx) {
if (ctx == NULL) {
return;
}
if (ctx->listen_fd >= 0) {
close(ctx->listen_fd);
ctx->listen_fd = -1;
}
ctx->running = 0;
}