Skip to content

Commit fa609be

Browse files
hexyanhexyan
authored andcommitted
Add --api-key Bearer token authentication to ds4-server
Adds optional API key authentication via the --api-key flag. When set, all requests (except OPTIONS preflight) must include a valid Authorization: Bearer <key> header. Requests without a matching token receive 401 unauthorized. - bearer_token_valid() extracts and validates the Bearer token from raw HTTP headers (case-insensitive, whitespace-trimmed) - http_request now preserves raw headers for auth checking - 7 unit tests covering: valid key, wrong key, missing header, case-insensitive, extra whitespace, Basic auth rejection, prefix-length mismatch Closes #120
1 parent 9ca9013 commit fa609be

1 file changed

Lines changed: 89 additions & 0 deletions

File tree

ds4_server.c

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

1097110974
static 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+
1100311033
static 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;
1104611080
fail:
@@ -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

1129011331
static 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

1134011381
static 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+
1439314481
static 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

Comments
 (0)