emme coverage


Directory: src/
File: src/config.c
Date: 2026-05-14 14:35:13
Exec Total Coverage
Lines: 412 568 72.5%
Functions: 36 36 100.0%
Branches: 267 464 57.5%

Line Branch Exec Source
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <string.h>
4 #include <strings.h>
5 #include <errno.h>
6 #include <limits.h>
7 #include <yaml.h>
8 #include "log.h"
9 #include "config.h"
10
11 1992 static yaml_node_t *find_yaml_node(yaml_document_t *doc, yaml_node_t *node, const char *key)
12 {
13
2/4
✓ Branch 0 taken 1992 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1992 times.
1992 if (!node || node->type != YAML_MAPPING_NODE)
14 return NULL;
15
16 1992 for (yaml_node_pair_t *pair = node->data.mapping.pairs.start;
17
2/2
✓ Branch 0 taken 5795 times.
✓ Branch 1 taken 855 times.
6650 pair < node->data.mapping.pairs.top; pair++) {
18 5795 yaml_node_t *key_node = yaml_document_get_node(doc, pair->key);
19
2/4
✓ Branch 0 taken 5795 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5795 times.
✗ Branch 3 not taken.
5795 if (key_node && key_node->type == YAML_SCALAR_NODE &&
20
2/2
✓ Branch 0 taken 1137 times.
✓ Branch 1 taken 4658 times.
5795 strcmp((char *)key_node->data.scalar.value, key) == 0) {
21 1137 return yaml_document_get_node(doc, pair->value);
22 }
23 }
24 855 return NULL;
25 }
26
27 975 static int get_node_line(yaml_node_t *node)
28 {
29
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 975 times.
975 if (!node)
30 return 0;
31 975 return (int)node->start_mark.line + 1;
32 }
33
34 typedef struct {
35 yaml_document_t *document;
36 yaml_node_t *root;
37 ServerConfig *config;
38 const char *section_name;
39 } ConfigParser;
40
41 #define PARSE_FIELD(field_name, parser_func, min_val, max_val, dest) \
42 do { \
43 yaml_node_t *_node = find_yaml_node(ctx->document, node, field_name); \
44 if (_node) { \
45 char _full_name[128]; \
46 snprintf(_full_name, sizeof(_full_name), "%s.%s", ctx->section_name, field_name); \
47 if (parser_func(_node, _full_name, min_val, max_val, dest) != 0) { \
48 fprintf(stderr, " at line %d\n", get_node_line(_node)); \
49 return -1; \
50 } \
51 } \
52 } while(0)
53
54 #define PARSE_STRING(field_name, dest, size) \
55 do { \
56 yaml_node_t *_node = find_yaml_node(ctx->document, node, field_name); \
57 if (_node) { \
58 char _full_name[128]; \
59 snprintf(_full_name, sizeof(_full_name), "%s.%s", ctx->section_name, field_name); \
60 if (get_yaml_string(_node, _full_name, dest, size) != 0) { \
61 fprintf(stderr, " at line %d\n", get_node_line(_node)); \
62 return -1; \
63 } \
64 } \
65 } while(0)
66
67 #define PARSE_BOOL(field_name, dest) \
68 do { \
69 yaml_node_t *_node = find_yaml_node(ctx->document, node, field_name); \
70 if (_node) { \
71 char _full_name[128]; \
72 snprintf(_full_name, sizeof(_full_name), "%s.%s", ctx->section_name, field_name); \
73 if (get_yaml_bool(_node, _full_name, dest) != 0) { \
74 fprintf(stderr, " at line %d\n", get_node_line(_node)); \
75 return -1; \
76 } \
77 } \
78 } while(0)
79
80 47 static void set_config_defaults(ServerConfig *config)
81 {
82 47 memset(config, 0, sizeof(*config));
83 47 config->port = 8443;
84 47 config->max_connections = 100;
85 47 config->shutdown_timeout_seconds = 30;
86 47 snprintf(config->log_level, sizeof(config->log_level), "info");
87
88 47 config->logging.level = LOG_LEVEL_DEBUG;
89 47 config->logging.format = LOG_FORMAT_PLAIN;
90 47 config->logging.buffer_size = 16384;
91 47 config->logging.rollover_size = 10485760;
92 47 config->logging.rollover_daily = 1;
93 47 config->logging.appender_flags = APPENDER_FILE | APPENDER_CONSOLE;
94 47 snprintf(config->logging.file, sizeof(config->logging.file), "emme.log");
95
96 47 snprintf(config->ssl.certificate, sizeof(config->ssl.certificate), "certs/dev.crt");
97 47 snprintf(config->ssl.private_key, sizeof(config->ssl.private_key), "certs/dev.key");
98 47 config->ssl.session_cache_size = 100000;
99 47 config->ssl.session_timeout = 300;
100 47 config->ssl.session_ticket_key[0] = '\0';
101 47 config->ssl.read_buffer_size = 32768;
102 47 config->ssl.enable_partial_write = 1;
103 47 config->ssl.release_buffers = 1;
104
105 47 config->http2.keepalive_timeout = 60;
106 47 config->http2.max_requests_per_connection = 1000;
107 47 config->http2.max_concurrent_streams = 100;
108
109 47 config->request_timeout_ms = 30000;
110 47 config->tls_handshake_timeout_ms = 10000;
111 47 config->per_ip_connection_limit = 10;
112
113 47 config->security_headers.enabled = true;
114 47 config->security_headers.header_count = 0;
115 47 }
116
117 522 static int get_yaml_string_ext(yaml_node_t *node, const char *field, char *buffer, size_t size, int line)
118 {
119 int written;
120 const char *value;
121
122
2/4
✓ Branch 0 taken 522 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 522 times.
522 if (!node || node->type != YAML_SCALAR_NODE) {
123 fprintf(stderr, "Invalid '%s' (line %d): expected a scalar string\n", field, line);
124 return -1;
125 }
126
127 522 value = (const char *)node->data.scalar.value;
128 522 written = snprintf(buffer, size, "%s", value);
129
2/4
✓ Branch 0 taken 522 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 522 times.
522 if (written < 0 || (size_t)written >= size) {
130 fprintf(stderr, "Invalid '%s' (line %d): value too long\n", field, line);
131 return -1;
132 }
133 522 return 0;
134 }
135
136 522 static int get_yaml_string(yaml_node_t *node, const char *field, char *buffer, size_t size)
137 {
138 522 return get_yaml_string_ext(node, field, buffer, size, get_node_line(node));
139 }
140
141 200 static int parse_int_in_range(const char *text, int min, int max, int *result)
142 {
143 200 char *end = NULL;
144 long value;
145
146 200 errno = 0;
147 200 value = strtol(text, &end, 10);
148
4/6
✓ Branch 0 taken 200 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 199 times.
✓ Branch 3 taken 1 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 199 times.
200 if (errno != 0 || end == text || *end != '\0')
149 1 return -1;
150
4/4
✓ Branch 0 taken 197 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 196 times.
199 if (value < min || value > max)
151 3 return -1;
152
153 196 *result = (int)value;
154 196 return 0;
155 }
156
157 87 static int parse_size_in_range(const char *text, size_t min, size_t max, size_t *result)
158 {
159 87 char *end = NULL;
160 unsigned long long value;
161
162 87 errno = 0;
163 87 value = strtoull(text, &end, 10);
164
3/6
✓ Branch 0 taken 87 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 87 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 87 times.
87 if (errno != 0 || end == text || *end != '\0')
165 return -1;
166
2/4
✓ Branch 0 taken 87 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 87 times.
87 if (value < min || value > max)
167 return -1;
168
169 87 *result = (size_t)value;
170 87 return 0;
171 }
172
173 10 static int get_yaml_int(yaml_node_t *node, const char *field, int *result)
174 {
175
2/4
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 10 times.
10 if (!node || node->type != YAML_SCALAR_NODE) {
176 fprintf(stderr, "Invalid '%s': expected integer scalar\n", field);
177 return -1;
178 }
179 char *endptr;
180 10 long val = strtol((const char *)node->data.scalar.value, &endptr, 10);
181
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 10 times.
10 if (*endptr != '\0') {
182 fprintf(stderr, "Invalid '%s': expected integer, got '%s'\n", field, (const char *)node->data.scalar.value);
183 return -1;
184 }
185 10 *result = (int)val;
186 10 return 0;
187 }
188
189 200 static int get_yaml_int_in_range_ext(yaml_node_t *node, const char *field,
190 int min, int max, int *result, int line)
191 {
192
2/4
✓ Branch 0 taken 200 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 200 times.
200 if (!node || node->type != YAML_SCALAR_NODE) {
193 fprintf(stderr, "Invalid '%s' (line %d): expected integer scalar\n", field, line);
194 return -1;
195 }
196
2/2
✓ Branch 1 taken 4 times.
✓ Branch 2 taken 196 times.
200 if (parse_int_in_range((const char *)node->data.scalar.value, min, max, result) != 0) {
197 4 fprintf(stderr, "Invalid '%s' (line %d): expected integer in range [%d, %d]\n",
198 field, line, min, max);
199 4 return -1;
200 }
201 196 return 0;
202 }
203
204 200 static int get_yaml_int_in_range(yaml_node_t *node, const char *field,
205 int min, int max, int *result)
206 {
207 200 return get_yaml_int_in_range_ext(node, field, min, max, result, get_node_line(node));
208 }
209
210 87 static int get_yaml_size_in_range_ext(yaml_node_t *node, const char *field,
211 size_t min, size_t max, size_t *result, int line)
212 {
213
2/4
✓ Branch 0 taken 87 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 87 times.
87 if (!node || node->type != YAML_SCALAR_NODE) {
214 fprintf(stderr, "Invalid '%s' (line %d): expected integer scalar\n", field, line);
215 return -1;
216 }
217
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 87 times.
87 if (parse_size_in_range((const char *)node->data.scalar.value, min, max, result) != 0) {
218 fprintf(stderr, "Invalid '%s' (line %d): expected integer in range [%zu, %zu]\n",
219 field, line, min, max);
220 return -1;
221 }
222 87 return 0;
223 }
224
225 87 static int get_yaml_size_in_range(yaml_node_t *node, const char *field,
226 size_t min, size_t max, size_t *result)
227 {
228 87 return get_yaml_size_in_range_ext(node, field, min, max, result, get_node_line(node));
229 }
230
231 99 static int get_yaml_bool_ext(yaml_node_t *node, const char *field, int *result, int line)
232 {
233 const char *value;
234
235
2/4
✓ Branch 0 taken 99 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 99 times.
99 if (!node || node->type != YAML_SCALAR_NODE) {
236 fprintf(stderr, "Invalid '%s' (line %d): expected boolean scalar\n", field, line);
237 return -1;
238 }
239
240 99 value = (const char *)node->data.scalar.value;
241
6/6
✓ Branch 0 taken 36 times.
✓ Branch 1 taken 63 times.
✓ Branch 2 taken 14 times.
✓ Branch 3 taken 22 times.
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 13 times.
99 if (strcasecmp(value, "true") == 0 || strcmp(value, "1") == 0 || strcasecmp(value, "yes") == 0) {
242 86 *result = 1;
243 86 return 0;
244 }
245
5/6
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 11 times.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 1 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 1 times.
13 if (strcasecmp(value, "false") == 0 || strcmp(value, "0") == 0 || strcasecmp(value, "no") == 0) {
246 12 *result = 0;
247 12 return 0;
248 }
249
250 1 fprintf(stderr, "Invalid '%s' (line %d): expected true/false\n", field, line);
251 1 return -1;
252 }
253
254 99 static int get_yaml_bool(yaml_node_t *node, const char *field, int *result)
255 {
256 99 return get_yaml_bool_ext(node, field, result, get_node_line(node));
257 }
258
259 31 static int parse_log_level(const char *value, LogLevel *level)
260 {
261
2/2
✓ Branch 0 taken 21 times.
✓ Branch 1 taken 10 times.
31 if (strcasecmp(value, "debug") == 0)
262 21 *level = LOG_LEVEL_DEBUG;
263
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 8 times.
10 else if (strcasecmp(value, "info") == 0)
264 2 *level = LOG_LEVEL_INFO;
265
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 7 times.
8 else if (strcasecmp(value, "warn") == 0)
266 1 *level = LOG_LEVEL_WARN;
267
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 else if (strcasecmp(value, "error") == 0)
268 7 *level = LOG_LEVEL_ERROR;
269 else
270 return -1;
271 31 return 0;
272 }
273
274 31 static int parse_log_format(const char *value, LogFormat *format)
275 {
276
2/2
✓ Branch 0 taken 30 times.
✓ Branch 1 taken 1 times.
31 if (strcasecmp(value, "plain") == 0)
277 30 *format = LOG_FORMAT_PLAIN;
278
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 else if (strcasecmp(value, "json") == 0)
279 1 *format = LOG_FORMAT_JSON;
280 else
281 return -1;
282 31 return 0;
283 }
284
285 31 static int parse_appender_flags(yaml_document_t *doc, yaml_node_t *node, LoggingConfig *log_cfg)
286 {
287
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 31 times.
31 if (!node)
288 return 0;
289
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 31 times.
31 if (node->type != YAML_SEQUENCE_NODE) {
290 fprintf(stderr, "Invalid 'logging.appender_flags': expected sequence\n");
291 return -1;
292 }
293
294 31 log_cfg->appender_flags = 0;
295 31 for (yaml_node_item_t *item = node->data.sequence.items.start;
296
2/2
✓ Branch 0 taken 52 times.
✓ Branch 1 taken 31 times.
83 item < node->data.sequence.items.top; item++) {
297 52 yaml_node_t *flag_node = yaml_document_get_node(doc, *item);
298
2/4
✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 52 times.
52 if (!flag_node || flag_node->type != YAML_SCALAR_NODE) {
299 fprintf(stderr, "Invalid 'logging.appender_flags': entries must be scalars\n");
300 return -1;
301 }
302 52 const char *flag = (const char *)flag_node->data.scalar.value;
303
2/2
✓ Branch 0 taken 22 times.
✓ Branch 1 taken 30 times.
52 if (strcasecmp(flag, "file") == 0)
304 22 log_cfg->appender_flags |= APPENDER_FILE;
305
1/2
✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
30 else if (strcasecmp(flag, "console") == 0)
306 30 log_cfg->appender_flags |= APPENDER_CONSOLE;
307 else {
308 fprintf(stderr, "Invalid appender flag '%s': expected 'file' or 'console'\n", flag);
309 return -1;
310 }
311 }
312
313
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 31 times.
31 if (log_cfg->appender_flags == 0) {
314 fprintf(stderr, "Invalid 'logging.appender_flags': at least one appender required\n");
315 return -1;
316 }
317 31 return 0;
318 }
319
320 51 static int parse_health_check_config(yaml_document_t *doc, yaml_node_t *node, HealthCheckConfig *hc)
321 {
322
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 51 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
51 if (!node || node->type != YAML_MAPPING_NODE) {
323 51 return 0; // Optional, missing is OK
324 }
325
326 hc->enabled = false;
327 snprintf(hc->path, sizeof(hc->path), "/health");
328 hc->interval_seconds = 10;
329 hc->timeout_seconds = 5;
330 hc->unhealthy_threshold = 3;
331 hc->healthy_threshold = 2;
332
333 yaml_node_t *field;
334
335 field = find_yaml_node(doc, node, "enabled");
336 if (field) {
337 int val;
338 if (get_yaml_bool(field, "health_check.enabled", &val) == 0) {
339 hc->enabled = (bool)val;
340 }
341 }
342
343 field = find_yaml_node(doc, node, "path");
344 if (field) {
345 get_yaml_string(field, "health_check.path", hc->path, sizeof(hc->path));
346 }
347
348 field = find_yaml_node(doc, node, "interval_seconds");
349 if (field) {
350 int val;
351 if (get_yaml_int(field, "health_check.interval_seconds", &val) == 0) {
352 hc->interval_seconds = val;
353 }
354 }
355
356 field = find_yaml_node(doc, node, "timeout_seconds");
357 if (field) {
358 int val;
359 if (get_yaml_int(field, "health_check.timeout_seconds", &val) == 0) {
360 hc->timeout_seconds = val;
361 }
362 }
363
364 field = find_yaml_node(doc, node, "unhealthy_threshold");
365 if (field) {
366 int val;
367 if (get_yaml_int(field, "health_check.unhealthy_threshold", &val) == 0) {
368 hc->unhealthy_threshold = val;
369 }
370 }
371
372 field = find_yaml_node(doc, node, "healthy_threshold");
373 if (field) {
374 int val;
375 if (get_yaml_int(field, "health_check.healthy_threshold", &val) == 0) {
376 hc->healthy_threshold = val;
377 }
378 }
379
380 return 0;
381 }
382
383 51 static int parse_connection_pool_config(yaml_document_t *doc, yaml_node_t *node, ConnectionPoolConfig *cp)
384 {
385
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 51 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
51 if (!node || node->type != YAML_MAPPING_NODE) {
386 51 cp->size = BACKEND_POOL_DEFAULT_SIZE;
387 51 cp->idle_timeout_seconds = BACKEND_POOL_IDLE_TIMEOUT_SEC;
388 51 return 0;
389 }
390
391 cp->size = BACKEND_POOL_DEFAULT_SIZE;
392 cp->idle_timeout_seconds = BACKEND_POOL_IDLE_TIMEOUT_SEC;
393
394 yaml_node_t *field;
395
396 field = find_yaml_node(doc, node, "size");
397 if (field) {
398 int val;
399 if (get_yaml_int(field, "connection_pool.size", &val) == 0) {
400 cp->size = val;
401 }
402 }
403
404 field = find_yaml_node(doc, node, "idle_timeout_seconds");
405 if (field) {
406 int val;
407 if (get_yaml_int(field, "connection_pool.idle_timeout_seconds", &val) == 0) {
408 cp->idle_timeout_seconds = val;
409 }
410 }
411
412 return 0;
413 }
414
415 51 static int parse_circuit_breaker_config(yaml_document_t *doc, yaml_node_t *node, CircuitBreakerConfig *cb)
416 {
417
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 51 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
51 if (!node || node->type != YAML_MAPPING_NODE) {
418 51 cb->enabled = false;
419 51 cb->failure_threshold = 5;
420 51 cb->recovery_timeout_seconds = 30;
421 51 return 0;
422 }
423
424 cb->enabled = false;
425 cb->failure_threshold = 5;
426 cb->recovery_timeout_seconds = 30;
427
428 yaml_node_t *field;
429
430 field = find_yaml_node(doc, node, "enabled");
431 if (field) {
432 int val;
433 if (get_yaml_bool(field, "circuit_breaker.enabled", &val) == 0) {
434 cb->enabled = (bool)val;
435 }
436 }
437
438 field = find_yaml_node(doc, node, "failure_threshold");
439 if (field) {
440 int val;
441 if (get_yaml_int(field, "circuit_breaker.failure_threshold", &val) == 0) {
442 cb->failure_threshold = val;
443 }
444 }
445
446 field = find_yaml_node(doc, node, "recovery_timeout_seconds");
447 if (field) {
448 int val;
449 if (get_yaml_int(field, "circuit_breaker.recovery_timeout_seconds", &val) == 0) {
450 cb->recovery_timeout_seconds = val;
451 }
452 }
453
454 return 0;
455 }
456
457 93 static int parse_security_headers_config(yaml_document_t *doc, yaml_node_t *node, SecurityHeadersConfig *shc)
458 {
459
3/4
✓ Branch 0 taken 10 times.
✓ Branch 1 taken 83 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 10 times.
93 if (!node || node->type != YAML_MAPPING_NODE) {
460 83 shc->enabled = false;
461 83 shc->header_count = 0;
462 83 return 0;
463 }
464
465 10 shc->enabled = false;
466 10 shc->header_count = 0;
467
468 yaml_node_t *field;
469
470 10 field = find_yaml_node(doc, node, "enabled");
471
1/2
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
10 if (field) {
472 int val;
473
1/2
✓ Branch 1 taken 10 times.
✗ Branch 2 not taken.
10 if (get_yaml_bool(field, "security_headers.enabled", &val) == 0) {
474 10 shc->enabled = (bool)val;
475 }
476 }
477
478 10 yaml_node_t *headers_node = find_yaml_node(doc, node, "headers");
479
2/4
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 10 times.
✗ Branch 3 not taken.
10 if (headers_node && headers_node->type == YAML_SEQUENCE_NODE) {
480 10 for (yaml_node_item_t *item = headers_node->data.sequence.items.start;
481
3/4
✓ Branch 0 taken 60 times.
✓ Branch 1 taken 10 times.
✓ Branch 2 taken 60 times.
✗ Branch 3 not taken.
70 item < headers_node->data.sequence.items.top && shc->header_count < MAX_SECURITY_HEADERS;
482 60 item++) {
483 60 yaml_node_t *header_node = yaml_document_get_node(doc, *item);
484
2/4
✓ Branch 0 taken 60 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 60 times.
60 if (!header_node || header_node->type != YAML_MAPPING_NODE)
485 continue;
486
487 60 yaml_node_t *name_node = find_yaml_node(doc, header_node, "name");
488 60 yaml_node_t *value_node = find_yaml_node(doc, header_node, "value");
489
490
2/4
✓ Branch 0 taken 60 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 60 times.
✗ Branch 3 not taken.
60 if (name_node && value_node) {
491 60 SecurityHeader *header = &shc->headers[shc->header_count];
492
2/4
✓ Branch 1 taken 60 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 60 times.
✗ Branch 4 not taken.
120 if (get_yaml_string(name_node, "security_headers[].name", header->name, sizeof(header->name)) == 0 &&
493 60 get_yaml_string(value_node, "security_headers[].value", header->value, sizeof(header->value)) == 0) {
494 60 shc->header_count++;
495 }
496 }
497 }
498 }
499
500 10 return 0;
501 }
502
503 51 static int parse_cors_config(yaml_document_t *doc, yaml_node_t *node, CORSConfig *cors)
504 {
505
3/4
✓ Branch 0 taken 10 times.
✓ Branch 1 taken 41 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 10 times.
51 if (!node || node->type != YAML_MAPPING_NODE) {
506 41 cors->enabled = false;
507 41 cors->allow_credentials = false;
508 41 cors->max_age_seconds = 86400;
509 41 return 0;
510 }
511
512 10 cors->enabled = false;
513 10 cors->allow_credentials = false;
514 10 cors->max_age_seconds = 86400;
515
516 yaml_node_t *field;
517
518 10 field = find_yaml_node(doc, node, "enabled");
519
1/2
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
10 if (field) {
520 int val;
521
1/2
✓ Branch 1 taken 10 times.
✗ Branch 2 not taken.
10 if (get_yaml_bool(field, "cors.enabled", &val) == 0) {
522 10 cors->enabled = (bool)val;
523 }
524 }
525
526 10 field = find_yaml_node(doc, node, "allow_origin");
527
1/2
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
10 if (field) {
528 10 get_yaml_string(field, "cors.allow_origin", cors->allow_origin, sizeof(cors->allow_origin));
529 }
530
531 10 field = find_yaml_node(doc, node, "allow_methods");
532
1/2
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
10 if (field) {
533 10 get_yaml_string(field, "cors.allow_methods", cors->allow_methods, sizeof(cors->allow_methods));
534 }
535
536 10 field = find_yaml_node(doc, node, "allow_headers");
537
1/2
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
10 if (field) {
538 10 get_yaml_string(field, "cors.allow_headers", cors->allow_headers, sizeof(cors->allow_headers));
539 }
540
541 10 field = find_yaml_node(doc, node, "allow_credentials");
542
1/2
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
10 if (field) {
543 int val;
544
1/2
✓ Branch 1 taken 10 times.
✗ Branch 2 not taken.
10 if (get_yaml_bool(field, "cors.allow_credentials", &val) == 0) {
545 10 cors->allow_credentials = (bool)val;
546 }
547 }
548
549 10 field = find_yaml_node(doc, node, "max_age_seconds");
550
1/2
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
10 if (field) {
551 int val;
552
1/2
✓ Branch 1 taken 10 times.
✗ Branch 2 not taken.
10 if (get_yaml_int(field, "cors.max_age_seconds", &val) == 0) {
553 10 cors->max_age_seconds = val;
554 }
555 }
556
557 10 return 0;
558 }
559
560 27 int parse_backend_url(const char *backend, char *host, size_t host_size, int *port)
561 {
562 char temp_host[256];
563 int temp_port;
564 char trailing;
565
566
2/4
✓ Branch 0 taken 27 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 27 times.
27 if (!backend || backend[0] == '\0')
567 return -1;
568
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 26 times.
27 if (sscanf(backend, "%255[^:]:%d%c", temp_host, &temp_port, &trailing) != 2)
569 1 return -1;
570
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 26 times.
26 if (temp_host[0] == '\0')
571 return -1;
572
2/4
✓ Branch 0 taken 26 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 26 times.
26 if (temp_port < 1 || temp_port > 65535)
573 return -1;
574
575 26 snprintf(host, host_size, "%s", temp_host);
576 26 *port = temp_port;
577 26 return 0;
578 }
579
580 27 static int validate_backend(const char *backend)
581 {
582 char host[64];
583 int port;
584
585 27 return parse_backend_url(backend, host, sizeof(host), &port);
586 }
587
588 42 static int validate_routes(const ServerConfig *config)
589 {
590
2/2
✓ Branch 0 taken 51 times.
✓ Branch 1 taken 39 times.
90 for (int i = 0; i < config->route_count; i++) {
591 51 const Route *route = &config->routes[i];
592
593
2/4
✓ Branch 0 taken 51 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 51 times.
51 if (route->path[0] == '\0' || route->path[0] != '/') {
594 fprintf(stderr, "Invalid routes[%d].path: must start with '/'\n", i);
595 return -1;
596 }
597
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 51 times.
51 if (route->technology[0] == '\0') {
598 fprintf(stderr, "Invalid routes[%d].technology: required\n", i);
599 return -1;
600 }
601
602
2/2
✓ Branch 0 taken 23 times.
✓ Branch 1 taken 28 times.
51 if (strcmp(route->technology, "static") == 0) {
603
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 22 times.
23 if (route->document_root[0] == '\0') {
604 1 fprintf(stderr, "Invalid routes[%d].document_root: required for static route\n", i);
605 1 return -1;
606 }
607
2/2
✓ Branch 0 taken 27 times.
✓ Branch 1 taken 1 times.
28 } else if (strcmp(route->technology, "reverse_proxy") == 0) {
608
2/2
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 26 times.
27 if (validate_backend(route->backend) != 0) {
609 1 fprintf(stderr, "Invalid routes[%d].backend: expected host:port\n", i);
610 1 return -1;
611 }
612 } else {
613 1 fprintf(stderr, "Invalid routes[%d].technology '%s': unsupported\n", i, route->technology);
614 1 return -1;
615 }
616 }
617
618 39 return 0;
619 }
620
621 31 static int parse_server_section(ConfigParser *ctx, yaml_node_t *node)
622 {
623 31 ctx->section_name = "server";
624
625
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 31 times.
31 if (node->type != YAML_MAPPING_NODE) {
626 fprintf(stderr, "Invalid 'server' (line %d): expected mapping\n",
627 get_node_line(node));
628 return -1;
629 }
630
631
3/4
✓ Branch 1 taken 31 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 2 times.
✓ Branch 5 taken 29 times.
31 PARSE_FIELD("port", get_yaml_int_in_range, 1, 65535, &ctx->config->port);
632
2/4
✓ Branch 1 taken 29 times.
✗ Branch 2 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 29 times.
29 PARSE_FIELD("max_connections", get_yaml_int_in_range, 1, 1000000, &ctx->config->max_connections);
633
1/4
✗ Branch 1 not taken.
✓ Branch 2 taken 29 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
29 PARSE_FIELD("shutdown_timeout_seconds", get_yaml_int_in_range, 1, 300, &ctx->config->shutdown_timeout_seconds);
634
3/4
✓ Branch 1 taken 10 times.
✓ Branch 2 taken 19 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 10 times.
29 PARSE_FIELD("request_timeout_ms", get_yaml_int_in_range, 1000, 300000, &ctx->config->request_timeout_ms);
635
3/4
✓ Branch 1 taken 10 times.
✓ Branch 2 taken 19 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 10 times.
29 PARSE_FIELD("tls_handshake_timeout_ms", get_yaml_int_in_range, 1000, 60000, &ctx->config->tls_handshake_timeout_ms);
636
3/4
✓ Branch 1 taken 10 times.
✓ Branch 2 taken 19 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 10 times.
29 PARSE_FIELD("per_ip_connection_limit", get_yaml_int_in_range, 1, 10000, &ctx->config->per_ip_connection_limit);
637
2/4
✓ Branch 1 taken 29 times.
✗ Branch 2 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 29 times.
29 PARSE_STRING("log_level", ctx->config->log_level, sizeof(ctx->config->log_level));
638
639 29 return 0;
640 }
641
642 31 static int parse_logging_section(ConfigParser *ctx, yaml_node_t *node)
643 {
644 31 ctx->section_name = "logging";
645
646
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 31 times.
31 if (node->type != YAML_MAPPING_NODE) {
647 fprintf(stderr, "Invalid 'logging' (line %d): expected mapping\n",
648 get_node_line(node));
649 return -1;
650 }
651
652
3/4
✓ Branch 1 taken 30 times.
✓ Branch 2 taken 1 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 30 times.
31 PARSE_STRING("file", ctx->config->logging.file, sizeof(ctx->config->logging.file));
653
3/4
✓ Branch 1 taken 29 times.
✓ Branch 2 taken 2 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 29 times.
31 PARSE_FIELD("buffer_size", get_yaml_size_in_range, 1, 1048576, &ctx->config->logging.buffer_size);
654
655 31 yaml_node_t *level_node = find_yaml_node(ctx->document, node, "level");
656
1/2
✓ Branch 0 taken 31 times.
✗ Branch 1 not taken.
31 if (level_node) {
657 char level_str[16];
658 31 int line = get_node_line(level_node);
659
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 31 times.
31 if (get_yaml_string(level_node, "logging.level", level_str, sizeof(level_str)) != 0) {
660 fprintf(stderr, " at line %d\n", line);
661 return -1;
662 }
663
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 31 times.
31 if (parse_log_level(level_str, &ctx->config->logging.level) != 0) {
664 fprintf(stderr, "Invalid 'logging.level' (line %d): expected debug/info/warn/error\n", line);
665 return -1;
666 }
667 }
668
669 31 yaml_node_t *format_node = find_yaml_node(ctx->document, node, "format");
670
1/2
✓ Branch 0 taken 31 times.
✗ Branch 1 not taken.
31 if (format_node) {
671 char format_str[16];
672 31 int line = get_node_line(format_node);
673
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 31 times.
31 if (get_yaml_string(format_node, "logging.format", format_str, sizeof(format_str)) != 0) {
674 fprintf(stderr, " at line %d\n", line);
675 return -1;
676 }
677
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 31 times.
31 if (parse_log_format(format_str, &ctx->config->logging.format) != 0) {
678 fprintf(stderr, "Invalid 'logging.format' (line %d): expected plain/json\n", line);
679 return -1;
680 }
681 }
682
683
3/4
✓ Branch 1 taken 29 times.
✓ Branch 2 taken 2 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 29 times.
31 PARSE_FIELD("buffer_size", get_yaml_size_in_range, 1, 1048576, &ctx->config->logging.buffer_size);
684
3/4
✓ Branch 1 taken 29 times.
✓ Branch 2 taken 2 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 29 times.
31 PARSE_FIELD("rollover_size", get_yaml_size_in_range, 0, 1099511627776ULL, &ctx->config->logging.rollover_size);
685
3/4
✓ Branch 1 taken 30 times.
✓ Branch 2 taken 1 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 30 times.
31 PARSE_BOOL("rollover_daily", &ctx->config->logging.rollover_daily);
686
687 31 yaml_node_t *appender_flags_node = find_yaml_node(ctx->document, node, "appender_flags");
688
1/2
✓ Branch 0 taken 31 times.
✗ Branch 1 not taken.
31 if (appender_flags_node) {
689
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 31 times.
31 if (parse_appender_flags(ctx->document, appender_flags_node, &ctx->config->logging) != 0) {
690 fprintf(stderr, " at line %d\n", get_node_line(appender_flags_node));
691 return -1;
692 }
693 }
694
695 31 return 0;
696 }
697
698 45 static int parse_ssl_section(ConfigParser *ctx, yaml_node_t *node)
699 {
700 45 ctx->section_name = "ssl";
701
702
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 45 times.
45 if (node->type != YAML_MAPPING_NODE) {
703 fprintf(stderr, "Invalid 'ssl' (line %d): expected mapping\n",
704 get_node_line(node));
705 return -1;
706 }
707
708
3/4
✓ Branch 1 taken 44 times.
✓ Branch 2 taken 1 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 44 times.
45 PARSE_STRING("certificate", ctx->config->ssl.certificate, sizeof(ctx->config->ssl.certificate));
709
3/4
✓ Branch 1 taken 44 times.
✓ Branch 2 taken 1 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 44 times.
45 PARSE_STRING("private_key", ctx->config->ssl.private_key, sizeof(ctx->config->ssl.private_key));
710
3/4
✓ Branch 1 taken 18 times.
✓ Branch 2 taken 27 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 18 times.
45 PARSE_FIELD("session_cache_size", get_yaml_int_in_range, 1000, 1000000, &ctx->config->ssl.session_cache_size);
711
3/4
✓ Branch 1 taken 18 times.
✓ Branch 2 taken 27 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 18 times.
45 PARSE_FIELD("session_timeout", get_yaml_int_in_range, 60, 3600, &ctx->config->ssl.session_timeout);
712
3/4
✓ Branch 1 taken 11 times.
✓ Branch 2 taken 34 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 11 times.
45 PARSE_STRING("session_ticket_key", ctx->config->ssl.session_ticket_key, sizeof(ctx->config->ssl.session_ticket_key));
713
4/4
✓ Branch 1 taken 19 times.
✓ Branch 2 taken 26 times.
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 18 times.
45 PARSE_FIELD("read_buffer_size", get_yaml_int_in_range, 4096, 65536, &ctx->config->ssl.read_buffer_size);
714
4/4
✓ Branch 1 taken 20 times.
✓ Branch 2 taken 24 times.
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 19 times.
44 PARSE_BOOL("enable_partial_write", &ctx->config->ssl.enable_partial_write);
715
3/4
✓ Branch 1 taken 19 times.
✓ Branch 2 taken 24 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 19 times.
43 PARSE_BOOL("release_buffers", &ctx->config->ssl.release_buffers);
716
717 43 return 0;
718 }
719
720 19 static int parse_http2_section(ConfigParser *ctx, yaml_node_t *node)
721 {
722 19 ctx->section_name = "http2";
723
724
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 19 times.
19 if (node->type != YAML_MAPPING_NODE) {
725 fprintf(stderr, "Invalid 'http2' (line %d): expected mapping\n",
726 get_node_line(node));
727 return -1;
728 }
729
730
3/4
✓ Branch 1 taken 19 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 18 times.
19 PARSE_FIELD("keepalive_timeout", get_yaml_int_in_range, 10, 300, &ctx->config->http2.keepalive_timeout);
731
2/4
✓ Branch 1 taken 18 times.
✗ Branch 2 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 18 times.
18 PARSE_FIELD("max_requests_per_connection", get_yaml_int_in_range, 1, 100000, &ctx->config->http2.max_requests_per_connection);
732
2/4
✓ Branch 1 taken 18 times.
✗ Branch 2 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 18 times.
18 PARSE_FIELD("max_concurrent_streams", get_yaml_int_in_range, 1, 1000, &ctx->config->http2.max_concurrent_streams);
733
734 18 return 0;
735 }
736
737 51 static int parse_route_entry(ConfigParser *ctx, yaml_node_t *route_node)
738 {
739
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 51 times.
51 if (ctx->config->route_count >= MAX_ROUTES) {
740 fprintf(stderr, "Too many routes: maximum supported is %d\n", MAX_ROUTES);
741 return -1;
742 }
743
744 51 Route *route = &ctx->config->routes[ctx->config->route_count];
745 51 memset(route, 0, sizeof(*route));
746
747 51 yaml_node_t *route_field = find_yaml_node(ctx->document, route_node, "path");
748
2/4
✓ Branch 0 taken 51 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 51 times.
102 if (!route_field ||
749 51 get_yaml_string(route_field, "routes[].path",
750 51 route->path, sizeof(route->path)) != 0) {
751 fprintf(stderr, "Invalid route: missing or invalid 'path'\n");
752 return -1;
753 }
754
755 51 route_field = find_yaml_node(ctx->document, route_node, "technology");
756
2/4
✓ Branch 0 taken 51 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 51 times.
102 if (!route_field ||
757 51 get_yaml_string(route_field, "routes[].technology",
758 51 route->technology, sizeof(route->technology)) != 0) {
759 fprintf(stderr, "Invalid route: missing or invalid 'technology'\n");
760 return -1;
761 }
762
763 51 route_field = find_yaml_node(ctx->document, route_node, "document_root");
764
3/4
✓ Branch 0 taken 22 times.
✓ Branch 1 taken 29 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 22 times.
73 if (route_field &&
765 22 get_yaml_string(route_field, "routes[].document_root",
766 22 route->document_root, sizeof(route->document_root)) != 0)
767 return -1;
768
769 51 route_field = find_yaml_node(ctx->document, route_node, "backend");
770
3/4
✓ Branch 0 taken 28 times.
✓ Branch 1 taken 23 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 28 times.
79 if (route_field &&
771 28 get_yaml_string(route_field, "routes[].backend",
772 28 route->backend, sizeof(route->backend)) != 0)
773 return -1;
774
775 // Parse HTTP/2 reverse proxy options
776 51 route->http2_enabled = false;
777 51 route->tls_enabled = true;
778 51 route->tls_verify = false;
779
780 51 route_field = find_yaml_node(ctx->document, route_node, "http2_enabled");
781
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 51 times.
51 if (route_field) {
782 int val;
783 if (get_yaml_bool(route_field, "routes[].http2_enabled", &val) == 0) {
784 route->http2_enabled = (bool)val;
785 }
786 }
787
788 51 route_field = find_yaml_node(ctx->document, route_node, "tls_enabled");
789
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 51 times.
51 if (route_field) {
790 int val;
791 if (get_yaml_bool(route_field, "routes[].tls_enabled", &val) == 0) {
792 route->tls_enabled = (bool)val;
793 }
794 }
795
796 51 route_field = find_yaml_node(ctx->document, route_node, "tls_verify");
797
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 51 times.
51 if (route_field) {
798 int val;
799 if (get_yaml_bool(route_field, "routes[].tls_verify", &val) == 0) {
800 route->tls_verify = (bool)val;
801 }
802 }
803
804 // Parse nested config sections
805 51 yaml_node_t *hc_node = find_yaml_node(ctx->document, route_node, "health_check");
806 51 parse_health_check_config(ctx->document, hc_node, &route->health_check);
807
808 51 yaml_node_t *cp_node = find_yaml_node(ctx->document, route_node, "connection_pool");
809 51 parse_connection_pool_config(ctx->document, cp_node, &route->connection_pool);
810
811 51 yaml_node_t *cb_node = find_yaml_node(ctx->document, route_node, "circuit_breaker");
812 51 parse_circuit_breaker_config(ctx->document, cb_node, &route->circuit_breaker);
813
814 51 yaml_node_t *sh_node = find_yaml_node(ctx->document, route_node, "security_headers");
815 51 parse_security_headers_config(ctx->document, sh_node, &route->security_headers);
816
817 51 yaml_node_t *cors_node = find_yaml_node(ctx->document, route_node, "cors");
818 51 parse_cors_config(ctx->document, cors_node, &route->cors);
819
820 51 route->inherit_global_headers = true;
821 51 yaml_node_t *inherit_node = find_yaml_node(ctx->document, route_node, "inherit_global_headers");
822
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 51 times.
51 if (inherit_node) {
823 int val;
824 if (get_yaml_bool(inherit_node, "routes[].inherit_global_headers", &val) == 0) {
825 route->inherit_global_headers = (bool)val;
826 }
827 }
828
829
2/2
✓ Branch 0 taken 22 times.
✓ Branch 1 taken 29 times.
51 if (route->document_root[0] != '\0') {
830
1/2
✓ Branch 1 taken 22 times.
✗ Branch 2 not taken.
22 if (realpath(route->document_root, route->document_root_real)) {
831 22 route->document_root_resolved = 1;
832 } else {
833 route->document_root_resolved = 0;
834 strncpy(route->document_root_real, route->document_root,
835 sizeof(route->document_root_real) - 1);
836 route->document_root_real[sizeof(route->document_root_real) - 1] = '\0';
837 }
838 }
839
840 51 ctx->config->route_count++;
841 51 return 0;
842 }
843
844 32 static int parse_routes_section(ConfigParser *ctx, yaml_node_t *node)
845 {
846
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 32 times.
32 if (node->type != YAML_SEQUENCE_NODE) {
847 fprintf(stderr, "Invalid 'routes' (line %d): expected sequence\n",
848 get_node_line(node));
849 return -1;
850 }
851
852 32 for (yaml_node_item_t *item = node->data.sequence.items.start;
853
2/2
✓ Branch 0 taken 51 times.
✓ Branch 1 taken 32 times.
83 item < node->data.sequence.items.top; item++) {
854 51 yaml_node_t *route_node = yaml_document_get_node(ctx->document, *item);
855
2/4
✓ Branch 0 taken 51 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 51 times.
51 if (!route_node || route_node->type != YAML_MAPPING_NODE) {
856 fprintf(stderr, "Invalid route entry (line %d): expected mapping\n",
857 get_node_line(route_node));
858 return -1;
859 }
860
861
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 51 times.
51 if (parse_route_entry(ctx, route_node) != 0)
862 return -1;
863 }
864
865 32 return 0;
866 }
867
868 42 static int validate_config(const ServerConfig *config)
869 {
870
2/4
✓ Branch 0 taken 42 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 42 times.
42 if (config->ssl.certificate[0] == '\0' || config->ssl.private_key[0] == '\0') {
871 fprintf(stderr, "Invalid SSL config: certificate and private_key are required\n");
872 return -1;
873 }
874
875
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 42 times.
42 if (config->logging.appender_flags == 0) {
876 fprintf(stderr, "Invalid logging config: at least one appender must be enabled\n");
877 return -1;
878 }
879
880
3/4
✓ Branch 0 taken 33 times.
✓ Branch 1 taken 9 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 33 times.
42 if ((config->logging.appender_flags & APPENDER_FILE) && config->logging.file[0] == '\0') {
881 fprintf(stderr, "Invalid logging config: file appender requires 'logging.file'\n");
882 return -1;
883 }
884
885
2/2
✓ Branch 1 taken 3 times.
✓ Branch 2 taken 39 times.
42 if (validate_routes(config) != 0)
886 3 return -1;
887
888 39 return 0;
889 }
890
891 47 int load_config(ServerConfig *config, const char *file_path)
892 {
893 47 FILE *fh = NULL;
894 yaml_parser_t parser;
895 yaml_document_t document;
896 yaml_node_t *root;
897 ConfigParser ctx;
898 47 int parser_ready = 0;
899 47 int document_ready = 0;
900 47 int rc = -1;
901
902
2/4
✓ Branch 0 taken 47 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 47 times.
47 if (!config || !file_path) {
903 fprintf(stderr, "Invalid input: config and file_path are required\n");
904 return -1;
905 }
906
907 47 set_config_defaults(config);
908
909 47 fh = fopen(file_path, "rb");
910
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 47 times.
47 if (!fh) {
911 fprintf(stderr, "Error opening configuration file: %s\n", file_path);
912 return -1;
913 }
914
915
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 47 times.
47 if (!yaml_parser_initialize(&parser)) {
916 fprintf(stderr, "Unable to initialize YAML parser\n");
917 goto cleanup;
918 }
919 47 parser_ready = 1;
920 47 yaml_parser_set_input_file(&parser, fh);
921
922
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 47 times.
47 if (!yaml_parser_load(&parser, &document)) {
923 fprintf(stderr, "Error parsing YAML configuration file\n");
924 goto cleanup;
925 }
926 47 document_ready = 1;
927
928 47 root = yaml_document_get_root_node(&document);
929
2/4
✓ Branch 0 taken 47 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 47 times.
47 if (!root || root->type != YAML_MAPPING_NODE) {
930 fprintf(stderr, "YAML document is not a mapping\n");
931 goto cleanup;
932 }
933
934 47 ctx.document = &document;
935 47 ctx.root = root;
936 47 ctx.config = config;
937
938 47 yaml_node_t *node = find_yaml_node(&document, root, "server");
939
4/4
✓ Branch 0 taken 31 times.
✓ Branch 1 taken 16 times.
✓ Branch 3 taken 2 times.
✓ Branch 4 taken 29 times.
47 if (node && parse_server_section(&ctx, node) != 0)
940 2 goto cleanup;
941
942 45 node = find_yaml_node(&document, root, "logging");
943
3/4
✓ Branch 0 taken 31 times.
✓ Branch 1 taken 14 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 31 times.
45 if (node && parse_logging_section(&ctx, node) != 0)
944 goto cleanup;
945
946 45 node = find_yaml_node(&document, root, "ssl");
947
3/4
✓ Branch 0 taken 45 times.
✗ Branch 1 not taken.
✓ Branch 3 taken 2 times.
✓ Branch 4 taken 43 times.
45 if (node && parse_ssl_section(&ctx, node) != 0)
948 2 goto cleanup;
949
950 43 node = find_yaml_node(&document, root, "http2");
951
4/4
✓ Branch 0 taken 19 times.
✓ Branch 1 taken 24 times.
✓ Branch 3 taken 1 times.
✓ Branch 4 taken 18 times.
43 if (node && parse_http2_section(&ctx, node) != 0)
952 1 goto cleanup;
953
954 42 node = find_yaml_node(&document, root, "routes");
955
3/4
✓ Branch 0 taken 32 times.
✓ Branch 1 taken 10 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 32 times.
42 if (node && parse_routes_section(&ctx, node) != 0)
956 goto cleanup;
957
958 42 node = find_yaml_node(&document, root, "security_headers");
959 42 parse_security_headers_config(&document, node, &config->security_headers);
960
961
2/2
✓ Branch 1 taken 3 times.
✓ Branch 2 taken 39 times.
42 if (validate_config(config) != 0)
962 3 goto cleanup;
963
964 39 rc = 0;
965
966 47 cleanup:
967
1/2
✓ Branch 0 taken 47 times.
✗ Branch 1 not taken.
47 if (document_ready)
968 47 yaml_document_delete(&document);
969
1/2
✓ Branch 0 taken 47 times.
✗ Branch 1 not taken.
47 if (parser_ready)
970 47 yaml_parser_delete(&parser);
971
1/2
✓ Branch 0 taken 47 times.
✗ Branch 1 not taken.
47 if (fh)
972 47 fclose(fh);
973 47 return rc;
974 }
975
976 102 static void apply_int_env_override(const char *env_name, int *config_value, int min_val, int max_val,
977 const char *unit, int scale)
978 {
979 102 const char *env_str = getenv(env_name);
980
2/2
✓ Branch 0 taken 96 times.
✓ Branch 1 taken 6 times.
102 if (!env_str) return;
981
982 char *endptr;
983 6 long value = strtol(env_str, &endptr, 10);
984
5/6
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 4 times.
✓ Branch 5 taken 1 times.
6 if (*endptr == '\0' && value >= min_val && value <= max_val) {
985 4 *config_value = (int)(value * scale);
986 } else {
987 2 fprintf(stderr, "Warning: Invalid %s value '%s', using config value %d%s\n",
988 env_name, env_str, *config_value, unit);
989 }
990 }
991
992 51 static void apply_string_env_override(const char *env_name, char *config_value, size_t max_len)
993 {
994 51 const char *env_str = getenv(env_name);
995
2/2
✓ Branch 0 taken 44 times.
✓ Branch 1 taken 7 times.
51 if (!env_str) return;
996
997 7 strncpy(config_value, env_str, max_len - 1);
998 7 config_value[max_len - 1] = '\0';
999 }
1000
1001 17 void apply_env_overrides(ServerConfig *config)
1002 {
1003 17 apply_int_env_override("EMME_PORT", &config->port, 1, 65535, "", 1);
1004 17 apply_string_env_override("EMME_LOG_LEVEL", config->log_level, sizeof(config->log_level));
1005 17 apply_int_env_override("EMME_SHUTDOWN_TIMEOUT", &config->shutdown_timeout_seconds, 1, 300, "", 1);
1006 17 apply_int_env_override("EMME_MAX_CONNECTIONS", &config->max_connections, 1, 1000000, "", 1);
1007 17 apply_string_env_override("EMME_SSL_CERT_PATH", config->ssl.certificate, sizeof(config->ssl.certificate));
1008 17 apply_string_env_override("EMME_SSL_KEY_PATH", config->ssl.private_key, sizeof(config->ssl.private_key));
1009 17 apply_int_env_override("EMME_REQUEST_TIMEOUT", &config->request_timeout_ms, 1, 300, "ms", 1000);
1010 17 apply_int_env_override("EMME_TLS_HANDSHAKE_TIMEOUT", &config->tls_handshake_timeout_ms, 1, 60, "ms", 1000);
1011 17 apply_int_env_override("EMME_PER_IP_CONNECTION_LIMIT", &config->per_ip_connection_limit, 1, 10000, "", 1);
1012 17 }
1013