@@ -7706,6 +7706,7 @@ struct server {
77067706 visible_live_state thinking_live;
77077707 bool disable_exact_dsml_tool_replay;
77087708 bool enable_cors;
7709+ char *api_key;
77097710 pthread_mutex_t tool_mu;
77107711 pthread_mutex_t mu;
77117712 pthread_cond_t cv;
@@ -10966,10 +10967,13 @@ typedef struct {
1096610967 char path[256];
1096710968 char *body;
1096810969 size_t body_len;
10970+ char *headers;
10971+ size_t headers_len;
1096910972} http_request;
1097010973
1097110974static void http_request_free(http_request *r) {
1097210975 free(r->body);
10976+ free(r->headers);
1097310977 memset(r, 0, sizeof(*r));
1097410978}
1097510979
@@ -11000,6 +11004,32 @@ static long content_length(const char *h, size_t n) {
1100011004 return 0;
1100111005}
1100211006
11007+ static bool bearer_token_valid(const char *headers, size_t headers_len,
11008+ const char *expected_key) {
11009+ const char *p = headers, *end = headers + headers_len;
11010+ while (p < end) {
11011+ const char *line = p;
11012+ while (p < end && *p != '\n') p++;
11013+ size_t len = (size_t)(p - line);
11014+ if (len && line[len - 1] == '\r') len--;
11015+ if (len > 14 && strncasecmp(line, "Authorization:", 14) == 0) {
11016+ const char *v = line + 14;
11017+ const char *vend = line + len;
11018+ while (v < vend && isspace((unsigned char)*v)) v++;
11019+ if (vend - v < 7 || strncasecmp(v, "Bearer ", 7) != 0)
11020+ return false;
11021+ v += 7;
11022+ while (v < vend && isspace((unsigned char)*v)) v++;
11023+ while (vend > v && isspace((unsigned char)*(vend - 1))) vend--;
11024+ size_t klen = (size_t)(vend - v);
11025+ return klen == strlen(expected_key) &&
11026+ memcmp(v, expected_key, klen) == 0;
11027+ }
11028+ if (p < end) p++;
11029+ }
11030+ return false;
11031+ }
11032+
1100311033static bool read_http_request(int fd, http_request *r) {
1100411034 buf b = {0};
1100511035 ssize_t hend = -1;
@@ -11041,6 +11071,10 @@ static bool read_http_request(int fd, http_request *r) {
1104111071 r->body = xmalloc(r->body_len + 1);
1104211072 memcpy(r->body, b.ptr + hend, r->body_len);
1104311073 r->body[r->body_len] = '\0';
11074+ r->headers_len = (size_t)hend;
11075+ r->headers = xmalloc(r->headers_len + 1);
11076+ memcpy(r->headers, b.ptr, r->headers_len);
11077+ r->headers[r->headers_len] = '\0';
1104411078 buf_free(&b);
1104511079 return true;
1104611080fail:
@@ -11145,6 +11179,12 @@ static void *client_main(void *arg) {
1114511179 goto done;
1114611180 }
1114711181
11182+ if (s->api_key && !bearer_token_valid(hr.headers, hr.headers_len, s->api_key)) {
11183+ http_error(fd, s->enable_cors, 401, "unauthorized");
11184+ http_request_free(&hr);
11185+ goto done;
11186+ }
11187+
1114811188 if (!strcmp(hr.method, "GET") && !strcmp(hr.path, "/v1/models")) {
1114911189 send_models(s, fd);
1115011190 http_request_free(&hr);
@@ -11285,6 +11325,7 @@ typedef struct {
1128511325 bool disable_exact_dsml_tool_replay;
1128611326 int tool_memory_max_ids;
1128711327 bool enable_cors;
11328+ const char *api_key;
1128811329} server_config;
1128911330
1129011331static int parse_int_arg(const char *s, const char *opt) {
@@ -11338,6 +11379,7 @@ static void log_context_memory(ds4_backend backend, int ctx_size) {
1133811379}
1133911380
1134011381static void server_close_resources(server *s) {
11382+ free(s->api_key);
1134111383 if (s->trace) {
1134211384 fclose(s->trace);
1134311385 s->trace = NULL;
@@ -11400,6 +11442,8 @@ static void usage(FILE *fp) {
1140011442 " Bind port. Default: 8000\n"
1140111443 " --cors\n"
1140211444 " Add Access-Control-Allow-* headers for browser JS clients. Does not change --host.\n"
11445+ " --api-key KEY\n"
11446+ " Require Bearer token authentication for all requests except OPTIONS preflight.\n"
1140311447 " --trace FILE\n"
1140411448 " Write a human-readable session trace: prompts, cache decisions, output, tool calls.\n"
1140511449 "\n"
@@ -11514,6 +11558,8 @@ static server_config parse_options(int argc, char **argv) {
1151411558 c.port = parse_int_arg(need_arg(&i, argc, argv, arg), arg);
1151511559 } else if (!strcmp(arg, "--cors")) {
1151611560 c.enable_cors = true;
11561+ } else if (!strcmp(arg, "--api-key")) {
11562+ c.api_key = need_arg(&i, argc, argv, arg);
1151711563 } else if (!strcmp(arg, "--trace")) {
1151811564 c.trace_path = need_arg(&i, argc, argv, arg);
1151911565 } else if (!strcmp(arg, "--kv-disk-dir")) {
@@ -11619,6 +11665,7 @@ int main(int argc, char **argv) {
1161911665 s.disable_exact_dsml_tool_replay = cfg.disable_exact_dsml_tool_replay;
1162011666 s.tool_mem.max_entries = cfg.tool_memory_max_ids;
1162111667 s.enable_cors = cfg.enable_cors;
11668+ s.api_key = cfg.api_key ? xstrdup(cfg.api_key) : NULL;
1162211669 if (cfg.kv_disk_dir) {
1162311670 kv_cache_open(&s.kv, cfg.kv_disk_dir, cfg.kv_disk_space_mb,
1162411671 cfg.kv_cache_reject_different_quant, cfg.kv_cache);
@@ -11644,6 +11691,9 @@ int main(int argc, char **argv) {
1164411691 server_log(DS4_LOG_DEFAULT, "ds4-server: tracing session to %s", cfg.trace_path);
1164511692 }
1164611693
11694+ if (s.api_key)
11695+ server_log(DS4_LOG_DEFAULT, "ds4-server: API key authentication enabled");
11696+
1164711697 pthread_t worker;
1164811698 if (pthread_create(&worker, NULL, worker_main, &s) != 0) die("failed to start worker");
1164911699
@@ -14390,6 +14440,44 @@ static void test_client_socket_nonblocking_flag(void) {
1439014440 close(sv[1]);
1439114441}
1439214442
14443+ static void test_bearer_token_valid(void) {
14444+ const char *key = "secret123";
14445+
14446+ const char *req1 = "POST /v1/chat/completions HTTP/1.1\r\n"
14447+ "Authorization: Bearer secret123\r\n"
14448+ "Content-Length: 0\r\n\r\n";
14449+ TEST_ASSERT(bearer_token_valid(req1, strlen(req1), key));
14450+
14451+ const char *req2 = "POST /v1/chat/completions HTTP/1.1\r\n"
14452+ "Authorization: Bearer wrongkey\r\n"
14453+ "Content-Length: 0\r\n\r\n";
14454+ TEST_ASSERT(!bearer_token_valid(req2, strlen(req2), key));
14455+
14456+ const char *req3 = "POST /v1/chat/completions HTTP/1.1\r\n"
14457+ "Content-Length: 0\r\n\r\n";
14458+ TEST_ASSERT(!bearer_token_valid(req3, strlen(req3), key));
14459+
14460+ const char *req4 = "POST /v1/chat/completions HTTP/1.1\r\n"
14461+ "authorization: bearer secret123\r\n"
14462+ "Content-Length: 0\r\n\r\n";
14463+ TEST_ASSERT(bearer_token_valid(req4, strlen(req4), key));
14464+
14465+ const char *req5 = "POST /v1/chat/completions HTTP/1.1\r\n"
14466+ "Authorization: Bearer secret123 \r\n"
14467+ "Content-Length: 0\r\n\r\n";
14468+ TEST_ASSERT(bearer_token_valid(req5, strlen(req5), key));
14469+
14470+ const char *req6 = "POST /v1/chat/completions HTTP/1.1\r\n"
14471+ "Authorization: Basic abc123\r\n"
14472+ "Content-Length: 0\r\n\r\n";
14473+ TEST_ASSERT(!bearer_token_valid(req6, strlen(req6), key));
14474+
14475+ const char *req7 = "POST /v1/chat/completions HTTP/1.1\r\n"
14476+ "Authorization: Bearer secret123extra\r\n"
14477+ "Content-Length: 0\r\n\r\n";
14478+ TEST_ASSERT(!bearer_token_valid(req7, strlen(req7), key));
14479+ }
14480+
1439314481static void test_thinking_state_tracks_prompt_and_generated_tags(void) {
1439414482 request r;
1439514483 request_init(&r, REQ_CHAT, 128);
@@ -15584,6 +15672,7 @@ static void ds4_server_unit_tests_run(void) {
1558415672 test_json_skip_has_nesting_limit();
1558515673 test_model_metadata_clamps_completion_to_context();
1558615674 test_client_socket_nonblocking_flag();
15675+ test_bearer_token_valid();
1558715676 test_thinking_state_tracks_prompt_and_generated_tags();
1558815677 test_thinking_checkpoint_remember_gate();
1558915678 test_tool_marker_state_ignores_orphan_end();
0 commit comments