diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..99142bc --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/build/ +*.sock diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..d640e9e --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,19 @@ +cmake_minimum_required(VERSION 3.20) +project(amduat_api LANGUAGES C) + +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_C_EXTENSIONS OFF) + +add_subdirectory(vendor/amduat) + +add_executable(amduatd src/amduatd.c) + +target_include_directories(amduatd + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/vendor/amduat/src/internal + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/vendor/amduat/include +) + +target_link_libraries(amduatd + PRIVATE amduat_asl_store_fs amduat_asl amduat_enc amduat_hash_asl1 amduat_util +) diff --git a/src/amduatd.c b/src/amduatd.c new file mode 100644 index 0000000..f501300 --- /dev/null +++ b/src/amduatd.c @@ -0,0 +1,690 @@ +#define _POSIX_C_SOURCE 200809L + +#include "amduat/asl/artifact_io.h" +#include "amduat/asl/asl_store_fs.h" +#include "amduat/asl/asl_store_fs_meta.h" +#include "amduat/asl/ref_text.h" +#include "amduat/asl/store.h" +#include "amduat/enc/asl1_core_codec.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +static const char *const AMDUATD_DEFAULT_ROOT = ".amduat-asl"; +static const char *const AMDUATD_DEFAULT_SOCK = "amduatd.sock"; + +static volatile sig_atomic_t amduatd_should_exit = 0; + +static void amduatd_on_signal(int signo) { + (void)signo; + amduatd_should_exit = 1; +} + +static bool amduatd_write_all(int fd, const uint8_t *buf, size_t len) { + size_t off = 0; + while (off < len) { + ssize_t n = write(fd, buf + off, len - off); + if (n < 0) { + if (errno == EINTR) { + continue; + } + return false; + } + if (n == 0) { + return false; + } + off += (size_t)n; + } + return true; +} + +static bool amduatd_read_exact(int fd, uint8_t *buf, size_t len) { + size_t off = 0; + while (off < len) { + ssize_t n = read(fd, buf + off, len - off); + if (n < 0) { + if (errno == EINTR) { + continue; + } + return false; + } + if (n == 0) { + return false; + } + off += (size_t)n; + } + return true; +} + +static bool amduatd_read_line(int fd, char *buf, size_t cap, size_t *out_len) { + size_t len = 0; + while (len + 1 < cap) { + char c = 0; + ssize_t n = read(fd, &c, 1); + if (n < 0) { + if (errno == EINTR) { + continue; + } + return false; + } + if (n == 0) { + return false; + } + if (c == '\n') { + break; + } + buf[len++] = c; + } + if (len == 0) { + return false; + } + if (len > 0 && buf[len - 1] == '\r') { + len--; + } + buf[len] = '\0'; + if (out_len != NULL) { + *out_len = len; + } + return true; +} + +typedef struct { + char method[8]; + char path[1024]; + char content_type[128]; + char accept[128]; + char x_type_tag[64]; + size_t content_length; +} amduatd_http_req_t; + +static void amduatd_http_req_init(amduatd_http_req_t *req) { + memset(req, 0, sizeof(*req)); +} + +static bool amduatd_http_parse_request(int fd, amduatd_http_req_t *out_req) { + char line[2048]; + size_t line_len = 0; + char *sp1 = NULL; + char *sp2 = NULL; + + if (out_req == NULL) { + return false; + } + amduatd_http_req_init(out_req); + + if (!amduatd_read_line(fd, line, sizeof(line), &line_len)) { + return false; + } + + sp1 = strchr(line, ' '); + if (sp1 == NULL) { + return false; + } + *sp1++ = '\0'; + sp2 = strchr(sp1, ' '); + if (sp2 == NULL) { + return false; + } + *sp2++ = '\0'; + + if (strlen(line) >= sizeof(out_req->method)) { + return false; + } + strncpy(out_req->method, line, sizeof(out_req->method) - 1); + + if (strlen(sp1) >= sizeof(out_req->path)) { + return false; + } + strncpy(out_req->path, sp1, sizeof(out_req->path) - 1); + + for (;;) { + if (!amduatd_read_line(fd, line, sizeof(line), &line_len)) { + return false; + } + if (line_len == 0) { + break; + } + + if (strncasecmp(line, "Content-Length:", 15) == 0) { + const char *v = line + 15; + while (*v == ' ' || *v == '\t') { + v++; + } + out_req->content_length = (size_t)strtoull(v, NULL, 10); + } else if (strncasecmp(line, "Content-Type:", 13) == 0) { + const char *v = line + 13; + while (*v == ' ' || *v == '\t') { + v++; + } + strncpy(out_req->content_type, v, sizeof(out_req->content_type) - 1); + } else if (strncasecmp(line, "Accept:", 7) == 0) { + const char *v = line + 7; + while (*v == ' ' || *v == '\t') { + v++; + } + strncpy(out_req->accept, v, sizeof(out_req->accept) - 1); + } else if (strncasecmp(line, "X-Amduat-Type-Tag:", 18) == 0) { + const char *v = line + 18; + while (*v == ' ' || *v == '\t') { + v++; + } + strncpy(out_req->x_type_tag, v, sizeof(out_req->x_type_tag) - 1); + } + } + + return true; +} + +static bool amduatd_http_send_status(int fd, + int code, + const char *reason, + const char *content_type, + const uint8_t *body, + size_t body_len, + bool head_only) { + char hdr[512]; + int n = snprintf(hdr, + sizeof(hdr), + "HTTP/1.1 %d %s\r\n" + "Content-Length: %zu\r\n" + "Content-Type: %s\r\n" + "Connection: close\r\n" + "\r\n", + code, + reason != NULL ? reason : "", + body_len, + content_type != NULL ? content_type : "text/plain"); + if (n <= 0 || (size_t)n >= sizeof(hdr)) { + return false; + } + if (!amduatd_write_all(fd, (const uint8_t *)hdr, (size_t)n)) { + return false; + } + if (!head_only && body != NULL && body_len != 0) { + return amduatd_write_all(fd, body, body_len); + } + return true; +} + +static bool amduatd_http_send_text(int fd, + int code, + const char *reason, + const char *text, + bool head_only) { + const uint8_t *body = (const uint8_t *)(text != NULL ? text : ""); + size_t len = text != NULL ? strlen(text) : 0; + return amduatd_http_send_status( + fd, code, reason, "text/plain; charset=utf-8", body, len, head_only); +} + +static bool amduatd_http_send_json(int fd, + int code, + const char *reason, + const char *json, + bool head_only) { + const uint8_t *body = (const uint8_t *)(json != NULL ? json : "{}"); + size_t len = json != NULL ? strlen(json) : 2; + return amduatd_http_send_status( + fd, code, reason, "application/json", body, len, head_only); +} + +static const char *amduatd_query_param(const char *path, + const char *key, + char *out_value, + size_t out_cap) { + const char *q = strchr(path, '?'); + size_t key_len = 0; + if (out_value != NULL && out_cap != 0) { + out_value[0] = '\0'; + } + if (q == NULL || key == NULL || out_value == NULL || out_cap == 0) { + return NULL; + } + q++; + key_len = strlen(key); + + while (*q != '\0') { + const char *k = q; + const char *eq = strchr(k, '='); + const char *amp = strchr(k, '&'); + size_t klen = 0; + const char *v = NULL; + size_t vlen = 0; + + if (amp == NULL) { + amp = q + strlen(q); + } + if (eq == NULL || eq > amp) { + q = (*amp == '&') ? (amp + 1) : amp; + continue; + } + klen = (size_t)(eq - k); + if (klen == key_len && strncmp(k, key, key_len) == 0) { + v = eq + 1; + vlen = (size_t)(amp - v); + if (vlen >= out_cap) { + vlen = out_cap - 1; + } + memcpy(out_value, v, vlen); + out_value[vlen] = '\0'; + return out_value; + } + q = (*amp == '&') ? (amp + 1) : amp; + } + + return NULL; +} + +static void amduatd_path_without_query(const char *path, + char *out, + size_t cap) { + const char *q = NULL; + size_t n = 0; + if (out == NULL || cap == 0) { + return; + } + out[0] = '\0'; + if (path == NULL) { + return; + } + q = strchr(path, '?'); + n = q != NULL ? (size_t)(q - path) : strlen(path); + if (n >= cap) { + n = cap - 1; + } + memcpy(out, path, n); + out[n] = '\0'; +} + +static bool amduatd_parse_type_tag_hex(const char *text, + bool *out_has_type_tag, + amduat_type_tag_t *out_type_tag) { + char *end = NULL; + unsigned long v = 0; + if (out_has_type_tag != NULL) { + *out_has_type_tag = false; + } + if (text == NULL || text[0] == '\0' || out_type_tag == NULL || + out_has_type_tag == NULL) { + return true; + } + errno = 0; + v = strtoul(text, &end, 0); + if (errno != 0 || end == text || *end != '\0' || v > 0xffffffffUL) { + return false; + } + *out_has_type_tag = true; + out_type_tag->tag_id = (uint32_t)v; + return true; +} + +static bool amduatd_handle_meta(int fd, + const amduat_asl_store_fs_config_t *cfg, + bool head_only) { + char json[512]; + int n = snprintf(json, + sizeof(json), + "{" + "\"store_id\":\"%s\"," + "\"encoding_profile_id\":\"0x%04x\"," + "\"hash_id\":\"0x%04x\"" + "}\n", + cfg != NULL ? cfg->store_id : "", + cfg != NULL ? (unsigned int)cfg->config.encoding_profile_id + : 0u, + cfg != NULL ? (unsigned int)cfg->config.hash_id : 0u); + if (n <= 0 || (size_t)n >= sizeof(json)) { + return amduatd_http_send_text(fd, 500, "Internal Server Error", "error\n", + head_only); + } + return amduatd_http_send_json(fd, 200, "OK", json, head_only); +} + +static bool amduatd_handle_get_artifact(int fd, + amduat_asl_store_t *store, + const char *path, + bool head_only) { + char no_query[1024]; + char format[32]; + const char *ref_text = NULL; + amduat_reference_t ref; + amduat_artifact_t artifact; + amduat_asl_store_error_t err; + bool want_artifact = false; + + memset(&ref, 0, sizeof(ref)); + memset(&artifact, 0, sizeof(artifact)); + amduatd_path_without_query(path, no_query, sizeof(no_query)); + + if (strncmp(no_query, "/v1/artifacts/", 14) != 0) { + return amduatd_http_send_text(fd, 404, "Not Found", "not found\n", + head_only); + } + ref_text = no_query + 14; + if (ref_text[0] == '\0') { + return amduatd_http_send_text(fd, 400, "Bad Request", "missing ref\n", + head_only); + } + + if (!amduat_asl_ref_decode_hex(ref_text, &ref)) { + return amduatd_http_send_text(fd, 400, "Bad Request", "invalid ref\n", + head_only); + } + + memset(format, 0, sizeof(format)); + if (amduatd_query_param(path, "format", format, sizeof(format)) != NULL) { + if (strcmp(format, "artifact") == 0) { + want_artifact = true; + } else if (strcmp(format, "raw") == 0 || format[0] == '\0') { + want_artifact = false; + } else { + free((void *)ref.digest.data); + return amduatd_http_send_text(fd, 400, "Bad Request", + "invalid format\n", head_only); + } + } + + err = amduat_asl_store_get(store, ref, &artifact); + free((void *)ref.digest.data); + if (err == AMDUAT_ASL_STORE_ERR_NOT_FOUND) { + return amduatd_http_send_text(fd, 404, "Not Found", "not found\n", + head_only); + } + if (err != AMDUAT_ASL_STORE_OK) { + amduat_asl_artifact_free(&artifact); + return amduatd_http_send_text(fd, 500, "Internal Server Error", + "store error\n", head_only); + } + + if (!want_artifact) { + bool ok = amduatd_http_send_status( + fd, + 200, + "OK", + "application/octet-stream", + artifact.bytes.data, + artifact.bytes.len, + head_only); + amduat_asl_artifact_free(&artifact); + return ok; + } + + { + amduat_octets_t out = amduat_octets(NULL, 0); + bool ok = amduat_enc_asl1_core_encode_artifact_v1(artifact, &out); + amduat_asl_artifact_free(&artifact); + if (!ok) { + return amduatd_http_send_text(fd, 500, "Internal Server Error", + "encode error\n", head_only); + } + ok = amduatd_http_send_status(fd, + 200, + "OK", + "application/vnd.amduat.asl.artifact+v1", + out.data, + out.len, + head_only); + free((void *)out.data); + return ok; + } +} + +static bool amduatd_handle_head_artifact(int fd, + amduat_asl_store_t *store, + const char *path) { + return amduatd_handle_get_artifact(fd, store, path, true); +} + +static bool amduatd_handle_post_artifacts(int fd, + amduat_asl_store_t *store, + const amduatd_http_req_t *req) { + uint8_t *body = NULL; + bool has_type_tag = false; + amduat_type_tag_t type_tag; + amduat_asl_io_format_t input_format = AMDUAT_ASL_IO_RAW; + amduat_artifact_t artifact; + amduat_reference_t ref; + amduat_asl_store_error_t err; + char *ref_hex = NULL; + char json[2048]; + + memset(&artifact, 0, sizeof(artifact)); + memset(&ref, 0, sizeof(ref)); + memset(&type_tag, 0, sizeof(type_tag)); + + if (req == NULL) { + return amduatd_http_send_text(fd, 400, "Bad Request", "bad request\n", + false); + } + + if (req->content_length > (64u * 1024u * 1024u)) { + return amduatd_http_send_text(fd, 413, "Payload Too Large", + "payload too large\n", false); + } + + if (req->content_length != 0) { + body = (uint8_t *)malloc(req->content_length); + if (body == NULL) { + return amduatd_http_send_text(fd, 500, "Internal Server Error", + "oom\n", false); + } + if (!amduatd_read_exact(fd, body, req->content_length)) { + free(body); + return false; + } + } else { + body = NULL; + } + + if (req->content_type[0] != '\0' && + strstr(req->content_type, "application/vnd.amduat.asl.artifact+v1") != + NULL) { + input_format = AMDUAT_ASL_IO_ARTIFACT; + } + + if (input_format == AMDUAT_ASL_IO_RAW) { + if (!amduatd_parse_type_tag_hex(req->x_type_tag, + &has_type_tag, + &type_tag)) { + free(body); + return amduatd_http_send_text(fd, 400, "Bad Request", + "invalid X-Amduat-Type-Tag\n", false); + } + } + + if (!amduat_asl_artifact_from_bytes(amduat_octets(body, req->content_length), + input_format, + has_type_tag, + type_tag, + &artifact)) { + free(body); + return amduatd_http_send_text(fd, 400, "Bad Request", + "invalid artifact\n", false); + } + + err = amduat_asl_store_put(store, artifact, &ref); + amduat_asl_artifact_free(&artifact); + if (err != AMDUAT_ASL_STORE_OK) { + free((void *)ref.digest.data); + return amduatd_http_send_text(fd, 500, "Internal Server Error", + "store error\n", false); + } + + if (!amduat_asl_ref_encode_hex(ref, &ref_hex)) { + free((void *)ref.digest.data); + return amduatd_http_send_text(fd, 500, "Internal Server Error", + "encode ref error\n", false); + } + free((void *)ref.digest.data); + + { + int n = snprintf(json, sizeof(json), "{\"ref\":\"%s\"}\n", ref_hex); + free(ref_hex); + if (n <= 0 || (size_t)n >= sizeof(json)) { + return amduatd_http_send_text(fd, 500, "Internal Server Error", + "error\n", false); + } + return amduatd_http_send_json(fd, 200, "OK", json, false); + } +} + +static bool amduatd_handle_conn(int fd, + amduat_asl_store_t *store, + const amduat_asl_store_fs_config_t *cfg) { + amduatd_http_req_t req; + char no_query[1024]; + + if (!amduatd_http_parse_request(fd, &req)) { + return false; + } + + amduatd_path_without_query(req.path, no_query, sizeof(no_query)); + + if (strcmp(req.method, "GET") == 0 && strcmp(no_query, "/v1/meta") == 0) { + return amduatd_handle_meta(fd, cfg, false); + } + if (strcmp(req.method, "HEAD") == 0 && strcmp(no_query, "/v1/meta") == 0) { + return amduatd_handle_meta(fd, cfg, true); + } + + if (strcmp(req.method, "POST") == 0 && strcmp(no_query, "/v1/artifacts") == 0) { + return amduatd_handle_post_artifacts(fd, store, &req); + } + if (strcmp(req.method, "GET") == 0 && + strncmp(no_query, "/v1/artifacts/", 14) == 0) { + return amduatd_handle_get_artifact(fd, store, req.path, false); + } + if (strcmp(req.method, "HEAD") == 0 && + strncmp(no_query, "/v1/artifacts/", 14) == 0) { + return amduatd_handle_head_artifact(fd, store, req.path); + } + + return amduatd_http_send_text(fd, 404, "Not Found", "not found\n", false); +} + +static void amduatd_print_usage(FILE *stream) { + fprintf(stream, + "usage:\n" + " amduatd [--root PATH] [--sock PATH]\n" + "\n" + "defaults:\n" + " --root %s\n" + " --sock %s\n", + AMDUATD_DEFAULT_ROOT, + AMDUATD_DEFAULT_SOCK); +} + +int main(int argc, char **argv) { + const char *root = AMDUATD_DEFAULT_ROOT; + const char *sock_path = AMDUATD_DEFAULT_SOCK; + amduat_asl_store_fs_config_t cfg; + amduat_asl_store_fs_t fs; + amduat_asl_store_t store; + int i; + int sfd = -1; + + for (i = 1; i < argc; ++i) { + if (strcmp(argv[i], "--root") == 0) { + if (i + 1 >= argc) { + fprintf(stderr, "error: --root requires a path\n"); + return 2; + } + root = argv[++i]; + } else if (strcmp(argv[i], "--sock") == 0) { + if (i + 1 >= argc) { + fprintf(stderr, "error: --sock requires a path\n"); + return 2; + } + sock_path = argv[++i]; + } else if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) { + amduatd_print_usage(stdout); + return 0; + } else { + fprintf(stderr, "error: unknown option: %s\n", argv[i]); + return 2; + } + } + + memset(&cfg, 0, sizeof(cfg)); + if (!amduat_asl_store_fs_load_config(root, &cfg)) { + fprintf(stderr, "error: failed to load store config: %s\n", root); + return 8; + } + + memset(&fs, 0, sizeof(fs)); + if (!amduat_asl_store_fs_init(&fs, cfg.config, root)) { + fprintf(stderr, "error: failed to initialize store: %s\n", root); + return 8; + } + amduat_asl_store_init(&store, cfg.config, amduat_asl_store_fs_ops(), &fs); + + signal(SIGINT, amduatd_on_signal); + signal(SIGTERM, amduatd_on_signal); + + sfd = socket(AF_UNIX, SOCK_STREAM, 0); + if (sfd < 0) { + perror("socket"); + return 8; + } + + { + struct sockaddr_un addr; + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + if (strlen(sock_path) >= sizeof(addr.sun_path)) { + fprintf(stderr, "error: socket path too long\n"); + close(sfd); + return 2; + } + strncpy(addr.sun_path, sock_path, sizeof(addr.sun_path) - 1); + + (void)unlink(sock_path); + + if (bind(sfd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { + perror("bind"); + close(sfd); + return 8; + } + (void)chmod(sock_path, 0660); + + if (listen(sfd, 16) < 0) { + perror("listen"); + (void)unlink(sock_path); + close(sfd); + return 8; + } + } + + fprintf(stderr, "amduatd: root=%s sock=%s\n", root, sock_path); + + while (!amduatd_should_exit) { + int cfd = accept(sfd, NULL, NULL); + if (cfd < 0) { + if (errno == EINTR) { + continue; + } + perror("accept"); + break; + } + + (void)amduatd_handle_conn(cfd, &store, &cfg); + (void)close(cfd); + } + + (void)unlink(sock_path); + (void)close(sfd); + return 0; +}