367 lines
9.3 KiB
C
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;
|
|
}
|