comparison mrjunejune/main.c @ 264:04fee26ecce0

add authenticated JRPG conversation platform Add reusable auth/session storage, owned conversation recovery, guest quotas, admin workflows, URL-routed conversation UI, mobile frame support, and parallel browser acceptance. Co-authored-by: Copilot <[email protected]>
author MrJuneJune <me@mrjunejune.com>
date Fri, 07 Aug 2026 07:34:12 -0700
parents ee04e4e69fed
children 056790c4fb0d
comparison
equal deleted inserted replaced
263:ee04e4e69fed 264:04fee26ecce0
2 #include "markdown_converter/markdown_to_html.h" 2 #include "markdown_converter/markdown_to_html.h"
3 #include "s3/s3_uploader.h" 3 #include "s3/s3_uploader.h"
4 #include "deita/deita.h" 4 #include "deita/deita.h"
5 #include "mrjunejune/latex_renderer.h" 5 #include "mrjunejune/latex_renderer.h"
6 #include "mrjunejune/conversation_api.h" 6 #include "mrjunejune/conversation_api.h"
7 #include "mrjunejune/auth_api.h"
8 #include "mrjunejune/admin_api.h"
9 #include "mrjunejune/template_renderer.h"
10 #include "auth/auth_crypto.h"
11 #include <stdio.h>
12 #include <stdlib.h>
13 #include <string.h>
14 #include <strings.h>
7 #include <time.h> 15 #include <time.h>
8 #include <sys/stat.h> 16 #include <sys/stat.h>
9 #include <stdarg.h> 17 #include <stdarg.h>
10 #include <stdatomic.h> 18 #include <stdatomic.h>
11 #include <pthread.h> 19 #include <pthread.h>
20 #include <arpa/inet.h>
21 #include <openssl/crypto.h>
12 22
13 // UUID + /tmp/ + format (max 4) 23 // UUID + /tmp/ + format (max 4)
14 #define TMP_FILE_LENGTH 47 24 #define TMP_FILE_LENGTH 47
15 #define UUID_LEN 37 25 #define UUID_LEN 37
16 26
43 static char g_s3_cloudfront_url[256] = {0}; 53 static char g_s3_cloudfront_url[256] = {0};
44 static char g_db_path[256] = "mrjunejune/data/mrjunejune.db"; 54 static char g_db_path[256] = "mrjunejune/data/mrjunejune.db";
45 static int g_s3_url_expires = 3600; 55 static int g_s3_url_expires = 3600;
46 static S3_Config g_s3_config = {0}; 56 static S3_Config g_s3_config = {0};
47 static Deita_Connection *g_db_connection = NULL; 57 static Deita_Connection *g_db_connection = NULL;
58 /* S3 credentials — never logged; zero after use in init. */
59 static char g_s3_access_key[128] = {0};
60 static char g_s3_secret_key[128] = {0};
61
62 /* Auth configuration */
63 static char g_auth_cookie_secret[AUTH_CRYPTO_COOKIE_SECRET_MAX_BYTES * 2 + 1] = {0};
64 static char g_auth_bootstrap_username[64] = {0};
65 static char g_auth_bootstrap_password_hash[AUTH_CRYPTO_PASSWORD_HASH_ENCODED_SIZE] = {0};
66 static char g_auth_trusted_proxy[AUTH_CRYPTO_IP_MAX_BYTES] = {0};
67 static int64 g_auth_session_idle_ttl = AUTH_API_SESSION_IDLE_TTL_DEFAULT;
68 static int64 g_auth_session_abs_ttl = AUTH_API_SESSION_ABS_TTL_DEFAULT;
69 static int64 g_auth_guest_ttl = AUTH_API_GUEST_TTL_DEFAULT;
70 static boolean g_auth_dev_insecure = FALSE;
71 static char g_server_host[128] = {0}; /* SERVER_HOST; empty → 0.0.0.0 */
72
73 /* Guest inference / quota configuration */
74 #define G_GUEST_DAILY_TURNS_DEFAULT 10
75 #define G_GUEST_DAILY_OUTPUT_TOKENS_DEFAULT 20000
76 #define G_GUEST_REQUEST_OUTPUT_TOKENS_MIN 1
77 #define G_GUEST_DAILY_TURNS_MIN 1
78 #define G_GUEST_DAILY_TURNS_MAX 10000
79 #define G_GUEST_DAILY_OUTPUT_TOKENS_MIN 1
80 #define G_GUEST_DAILY_OUTPUT_TOKENS_MAX 1000000
81 #define G_GUEST_REQUEST_OUTPUT_TOKENS_DEFAULT 2048
82 static boolean g_guest_inference_enabled = FALSE;
83 static int64 g_guest_daily_turns = G_GUEST_DAILY_TURNS_DEFAULT;
84 static int64 g_guest_daily_output_tokens = G_GUEST_DAILY_OUTPUT_TOKENS_DEFAULT;
85 static int64 g_guest_request_output_tokens = G_GUEST_REQUEST_OUTPUT_TOKENS_DEFAULT;
86
87 /*
88 * Strict full-string integer parse: returns FALSE if value is empty,
89 * has trailing non-digit characters, or overflows a long.
90 */
91 static boolean config__parse_int64_strict(const char *value, int64 *out)
92 {
93 if (!value || value[0] == '\0')
94 return FALSE;
95 char *end = NULL;
96 long v = strtol(value, &end, 10);
97 if (end == value || *end != '\0')
98 return FALSE;
99 *out = (int64)v;
100 return TRUE;
101 }
102
103 /*
104 * Decode a lowercase or uppercase hex string into raw bytes.
105 * Returns the number of decoded bytes, or 0 on any error.
106 * hex_len must be even; each pair of hex chars produces one byte.
107 */
108 static size_t config__hex_decode(const char *hex, uint8 *out, size_t out_capacity)
109 {
110 if (!hex || !out) return 0;
111 size_t hex_len = strlen(hex);
112 if (hex_len == 0 || hex_len % 2 != 0) return 0;
113 size_t byte_count = hex_len / 2;
114 if (byte_count > out_capacity) return 0;
115 for (size_t i = 0; i < byte_count; i++)
116 {
117 int h, l;
118 char hi = hex[i * 2];
119 char lo = hex[i * 2 + 1];
120 if (hi >= '0' && hi <= '9') h = hi - '0';
121 else if (hi >= 'a' && hi <= 'f') h = hi - 'a' + 10;
122 else if (hi >= 'A' && hi <= 'F') h = hi - 'A' + 10;
123 else return 0;
124 if (lo >= '0' && lo <= '9') l = lo - '0';
125 else if (lo >= 'a' && lo <= 'f') l = lo - 'a' + 10;
126 else if (lo >= 'A' && lo <= 'F') l = lo - 'A' + 10;
127 else return 0;
128 out[i] = (uint8)((h << 4) | l);
129 }
130 return byte_count;
131 }
48 132
49 static void load_config(const char *config_path) 133 static void load_config(const char *config_path)
50 { 134 {
51 FILE *f = fopen(config_path, "r"); 135 FILE *f = fopen(config_path, "r");
136 char workspace_config_path[1024] = {0};
52 if (!f) 137 if (!f)
53 { 138 {
139 const char *workspace = getenv("BUILD_WORKSPACE_DIRECTORY");
140 if (workspace && workspace[0] != '\0')
141 {
142 int written = snprintf(
143 workspace_config_path,
144 sizeof(workspace_config_path),
145 "%s/%s",
146 workspace,
147 config_path);
148 if (written > 0 && (size_t)written < sizeof(workspace_config_path))
149 f = fopen(workspace_config_path, "r");
150 }
151 }
152 if (!f)
153 {
54 printf("[CONFIG] Warning: Could not open %s, using defaults\n", config_path); 154 printf("[CONFIG] Warning: Could not open %s, using defaults\n", config_path);
55 return; 155 }
56 } 156 else
57 157 {
58 char line[512]; 158 char line[512];
59 while (fgets(line, sizeof(line), f)) 159 while (fgets(line, sizeof(line), f))
60 { 160 {
61 // Skip comments and empty lines 161 // Skip comments and empty lines
62 if (line[0] == '#' || line[0] == '\n' || line[0] == '\r') continue; 162 if (line[0] == '#' || line[0] == '\n' || line[0] == '\r') continue;
63 163
64 char *eq = strchr(line, '='); 164 char *eq = strchr(line, '=');
65 if (!eq) continue; 165 if (!eq) continue;
66 166
67 *eq = '\0'; 167 *eq = '\0';
68 char *key = line; 168 char *key = line;
69 char *value = eq + 1; 169 char *value = eq + 1;
70 170
71 // Trim newline from value 171 // Trim newline from value
72 size_t vlen = strlen(value); 172 size_t vlen = strlen(value);
73 while (vlen > 0 && (value[vlen-1] == '\n' || value[vlen-1] == '\r')) 173 while (vlen > 0 && (value[vlen-1] == '\n' || value[vlen-1] == '\r'))
74 value[--vlen] = '\0'; 174 value[--vlen] = '\0';
75 175
76 if (strcmp(key, "UPLOAD_AUTH_TOKEN") == 0) 176 if (strcmp(key, "UPLOAD_AUTH_TOKEN") == 0)
77 { 177 {
78 strncpy(g_upload_auth_token, value, sizeof(g_upload_auth_token) - 1); 178 strncpy(g_upload_auth_token, value, sizeof(g_upload_auth_token) - 1);
79 } 179 }
80 else if (strcmp(key, "S3_REGION") == 0) 180 else if (strcmp(key, "S3_REGION") == 0)
81 { 181 {
82 strncpy(g_s3_region, value, sizeof(g_s3_region) - 1); 182 strncpy(g_s3_region, value, sizeof(g_s3_region) - 1);
83 } 183 }
84 else if (strcmp(key, "S3_BUCKET") == 0) 184 else if (strcmp(key, "S3_BUCKET") == 0)
85 { 185 {
86 strncpy(g_s3_bucket, value, sizeof(g_s3_bucket) - 1); 186 strncpy(g_s3_bucket, value, sizeof(g_s3_bucket) - 1);
87 } 187 }
88 else if (strcmp(key, "S3_URL_EXPIRES") == 0) 188 else if (strcmp(key, "S3_URL_EXPIRES") == 0)
89 { 189 {
90 g_s3_url_expires = atoi(value); 190 int64 v;
191 if (!config__parse_int64_strict(value, &v) || v <= 0)
192 {
193 fprintf(stderr, "[CONFIG] ERROR: S3_URL_EXPIRES must be a positive integer\n");
194 exit(1);
195 }
196 g_s3_url_expires = (int)v;
91 } 197 }
92 else if (strcmp(key, "S3_CLOUDFRONT_URL") == 0) 198 else if (strcmp(key, "S3_CLOUDFRONT_URL") == 0)
93 { 199 {
94 strncpy(g_s3_cloudfront_url, value, sizeof(g_s3_cloudfront_url) - 1); 200 strncpy(g_s3_cloudfront_url, value, sizeof(g_s3_cloudfront_url) - 1);
95 } 201 }
96 else if (strcmp(key, "DB_PATH") == 0) 202 else if (strcmp(key, "DB_PATH") == 0)
97 { 203 {
98 strncpy(g_db_path, value, sizeof(g_db_path) - 1); 204 strncpy(g_db_path, value, sizeof(g_db_path) - 1);
99 } 205 }
100 } 206 else if (strcmp(key, "AWS_MRJUNEJUNE_ACCESS_KEY") == 0)
101 fclose(f); 207 {
208 strncpy(g_s3_access_key, value, sizeof(g_s3_access_key) - 1);
209 }
210 else if (strcmp(key, "AWS_MRJUNEJUNE_SECRET_ACCESS_KEY") == 0)
211 {
212 strncpy(g_s3_secret_key, value, sizeof(g_s3_secret_key) - 1);
213 }
214 else if (strcmp(key, "AUTH_COOKIE_SECRET") == 0)
215 {
216 /* Never log this value */
217 strncpy(g_auth_cookie_secret, value, sizeof(g_auth_cookie_secret) - 1);
218 }
219 else if (strcmp(key, "AUTH_BOOTSTRAP_USERNAME") == 0)
220 {
221 strncpy(g_auth_bootstrap_username, value,
222 sizeof(g_auth_bootstrap_username) - 1);
223 }
224 else if (strcmp(key, "AUTH_BOOTSTRAP_PASSWORD_HASH") == 0)
225 {
226 strncpy(g_auth_bootstrap_password_hash, value,
227 sizeof(g_auth_bootstrap_password_hash) - 1);
228 }
229 else if (strcmp(key, "AUTH_TRUSTED_PROXY") == 0)
230 {
231 strncpy(g_auth_trusted_proxy, value, sizeof(g_auth_trusted_proxy) - 1);
232 }
233 else if (strcmp(key, "AUTH_SESSION_IDLE_TTL") == 0)
234 {
235 int64 v;
236 if (!config__parse_int64_strict(value, &v) || v <= 0)
237 {
238 fprintf(stderr, "[CONFIG] ERROR: AUTH_SESSION_IDLE_TTL must be a positive integer\n");
239 exit(1);
240 }
241 g_auth_session_idle_ttl = v;
242 }
243 else if (strcmp(key, "AUTH_SESSION_ABS_TTL") == 0)
244 {
245 int64 v;
246 if (!config__parse_int64_strict(value, &v) || v <= 0)
247 {
248 fprintf(stderr, "[CONFIG] ERROR: AUTH_SESSION_ABS_TTL must be a positive integer\n");
249 exit(1);
250 }
251 g_auth_session_abs_ttl = v;
252 }
253 else if (strcmp(key, "AUTH_GUEST_TTL") == 0)
254 {
255 int64 v;
256 if (!config__parse_int64_strict(value, &v) || v <= 0)
257 {
258 fprintf(stderr, "[CONFIG] ERROR: AUTH_GUEST_TTL must be a positive integer\n");
259 exit(1);
260 }
261 g_auth_guest_ttl = v;
262 }
263 else if (strcmp(key, "AUTH_DEV_INSECURE_COOKIE") == 0)
264 {
265 g_auth_dev_insecure =
266 (strcmp(value, "1") == 0 || strcmp(value, "true") == 0);
267 }
268 else if (strcmp(key, "SERVER_HOST") == 0)
269 {
270 strncpy(g_server_host, value, sizeof(g_server_host) - 1);
271 }
272 else if (strcmp(key, "AUTH_GUEST_DAILY_TURNS") == 0)
273 {
274 int64 v;
275 if (!config__parse_int64_strict(value, &v) ||
276 v < G_GUEST_DAILY_TURNS_MIN || v > G_GUEST_DAILY_TURNS_MAX)
277 {
278 printf("[CONFIG] ERROR: AUTH_GUEST_DAILY_TURNS must be %d..%d\n",
279 G_GUEST_DAILY_TURNS_MIN, G_GUEST_DAILY_TURNS_MAX);
280 exit(1);
281 }
282 g_guest_daily_turns = v;
283 }
284 else if (strcmp(key, "AUTH_GUEST_DAILY_OUTPUT_TOKENS") == 0)
285 {
286 int64 v;
287 if (!config__parse_int64_strict(value, &v) ||
288 v < G_GUEST_DAILY_OUTPUT_TOKENS_MIN ||
289 v > G_GUEST_DAILY_OUTPUT_TOKENS_MAX)
290 {
291 printf("[CONFIG] ERROR: AUTH_GUEST_DAILY_OUTPUT_TOKENS must be %d..%d\n",
292 G_GUEST_DAILY_OUTPUT_TOKENS_MIN, G_GUEST_DAILY_OUTPUT_TOKENS_MAX);
293 exit(1);
294 }
295 g_guest_daily_output_tokens = v;
296 }
297 else if (strcmp(key, "AUTH_GUEST_REQUEST_OUTPUT_TOKENS") == 0)
298 {
299 int64 v;
300 if (!config__parse_int64_strict(value, &v) ||
301 v < G_GUEST_REQUEST_OUTPUT_TOKENS_MIN)
302 {
303 printf("[CONFIG] ERROR: AUTH_GUEST_REQUEST_OUTPUT_TOKENS must be >= %d\n",
304 G_GUEST_REQUEST_OUTPUT_TOKENS_MIN);
305 exit(1);
306 }
307 g_guest_request_output_tokens = v;
308 }
309 }
310 fclose(f);
311 }
102 312
103 printf("[CONFIG] Loaded: token=%s..., region=%s, bucket=%s, expires=%d, cloudfront=%s, db=%s\n", 313 printf("[CONFIG] Loaded: token=%s..., region=%s, bucket=%s, expires=%d, cloudfront=%s, db=%s\n",
104 g_upload_auth_token[0] ? "***" : "(empty)", 314 g_upload_auth_token[0] ? "***" : "(empty)",
105 g_s3_region, g_s3_bucket, g_s3_url_expires, 315 g_s3_region, g_s3_bucket, g_s3_url_expires,
106 g_s3_cloudfront_url[0] ? g_s3_cloudfront_url : "(none)", 316 g_s3_cloudfront_url[0] ? g_s3_cloudfront_url : "(none)",
107 g_db_path); 317 g_db_path);
318 printf("[CONFIG] Auth: secret=%s, bootstrap_user=%s, trusted_proxy=%s\n",
319 g_auth_cookie_secret[0] ? "(set)" : "(not set)",
320 g_auth_bootstrap_username[0] ? g_auth_bootstrap_username : "(none)",
321 g_auth_trusted_proxy[0] ? "(set)" : "(not set)");
322
323 const char *database_path_override = getenv("DB_PATH");
324 const char *test_tmpdir = getenv("TEST_TMPDIR");
325 if (database_path_override && database_path_override[0] != '\0')
326 {
327 strncpy(g_db_path, database_path_override, sizeof(g_db_path) - 1);
328 }
329 else if (test_tmpdir && test_tmpdir[0] != '\0')
330 {
331 snprintf(g_db_path, sizeof(g_db_path), "%s/mrjunejune.db", test_tmpdir);
332 }
333
334 /* Environment overrides: env vars always take precedence over file.
335 * Values are never logged. */
336 {
337 const char *env;
338
339 /* S3 / server config env overrides */
340 if ((env = getenv("UPLOAD_AUTH_TOKEN")) && env[0] != '\0')
341 strncpy(g_upload_auth_token, env, sizeof(g_upload_auth_token) - 1);
342 if ((env = getenv("S3_REGION")) && env[0] != '\0')
343 strncpy(g_s3_region, env, sizeof(g_s3_region) - 1);
344 if ((env = getenv("S3_BUCKET")) && env[0] != '\0')
345 strncpy(g_s3_bucket, env, sizeof(g_s3_bucket) - 1);
346 if ((env = getenv("S3_CLOUDFRONT_URL")) && env[0] != '\0')
347 strncpy(g_s3_cloudfront_url, env, sizeof(g_s3_cloudfront_url) - 1);
348 if ((env = getenv("S3_URL_EXPIRES")) && env[0] != '\0')
349 {
350 int64 v;
351 if (!config__parse_int64_strict(env, &v) || v <= 0)
352 {
353 fprintf(stderr, "[CONFIG] ERROR: S3_URL_EXPIRES must be a positive integer\n");
354 exit(1);
355 }
356 g_s3_url_expires = (int)v;
357 }
358 if ((env = getenv("MRJUNEJUNE_DB_PATH")) && env[0] != '\0')
359 strncpy(g_db_path, env, sizeof(g_db_path) - 1);
360 if ((env = getenv("AWS_MRJUNEJUNE_ACCESS_KEY")) && env[0] != '\0')
361 strncpy(g_s3_access_key, env, sizeof(g_s3_access_key) - 1);
362 if ((env = getenv("AWS_MRJUNEJUNE_SECRET_ACCESS_KEY")) && env[0] != '\0')
363 strncpy(g_s3_secret_key, env, sizeof(g_s3_secret_key) - 1);
364
365 /* Auth env overrides */
366 if ((env = getenv("AUTH_COOKIE_SECRET")) && env[0] != '\0')
367 strncpy(g_auth_cookie_secret, env, sizeof(g_auth_cookie_secret) - 1);
368 if ((env = getenv("AUTH_BOOTSTRAP_USERNAME")) && env[0] != '\0')
369 strncpy(g_auth_bootstrap_username, env,
370 sizeof(g_auth_bootstrap_username) - 1);
371 if ((env = getenv("AUTH_BOOTSTRAP_PASSWORD_HASH")) && env[0] != '\0')
372 strncpy(g_auth_bootstrap_password_hash, env,
373 sizeof(g_auth_bootstrap_password_hash) - 1);
374 if ((env = getenv("AUTH_TRUSTED_PROXY")) && env[0] != '\0')
375 strncpy(g_auth_trusted_proxy, env, sizeof(g_auth_trusted_proxy) - 1);
376 if ((env = getenv("AUTH_SESSION_IDLE_TTL")) && env[0] != '\0')
377 {
378 int64 v;
379 if (!config__parse_int64_strict(env, &v) || v <= 0)
380 {
381 fprintf(stderr, "[CONFIG] ERROR: AUTH_SESSION_IDLE_TTL must be a positive integer\n");
382 exit(1);
383 }
384 g_auth_session_idle_ttl = v;
385 }
386 if ((env = getenv("AUTH_SESSION_ABS_TTL")) && env[0] != '\0')
387 {
388 int64 v;
389 if (!config__parse_int64_strict(env, &v) || v <= 0)
390 {
391 fprintf(stderr, "[CONFIG] ERROR: AUTH_SESSION_ABS_TTL must be a positive integer\n");
392 exit(1);
393 }
394 g_auth_session_abs_ttl = v;
395 }
396 if ((env = getenv("AUTH_GUEST_TTL")) && env[0] != '\0')
397 {
398 int64 v;
399 if (!config__parse_int64_strict(env, &v) || v <= 0)
400 {
401 fprintf(stderr, "[CONFIG] ERROR: AUTH_GUEST_TTL must be a positive integer\n");
402 exit(1);
403 }
404 g_auth_guest_ttl = v;
405 }
406 if ((env = getenv("AUTH_DEV_INSECURE_COOKIE")) && env[0] != '\0')
407 g_auth_dev_insecure =
408 (strcmp(env, "1") == 0 || strcmp(env, "true") == 0);
409 if ((env = getenv("SERVER_HOST")) && env[0] != '\0')
410 strncpy(g_server_host, env, sizeof(g_server_host) - 1);
411
412 /* Guest inference enable: runtime-only, boolean parsed strictly. */
413 if ((env = getenv("MRJUNEJUNE_ALLOW_GUEST_INFERENCE")) && env[0] != '\0')
414 g_guest_inference_enabled =
415 (strcmp(env, "1") == 0 || strcasecmp(env, "true") == 0);
416 else if ((env = getenv("MRJUNEJUNE_ALLOW_ANONYMOUS_INFERENCE")) && env[0] != '\0')
417 g_guest_inference_enabled =
418 (strcmp(env, "1") == 0 || strcasecmp(env, "true") == 0);
419
420 /* Guest quota env overrides: strict full-string integer, fail on malformed. */
421 if ((env = getenv("AUTH_GUEST_DAILY_TURNS")) && env[0] != '\0')
422 {
423 int64 v;
424 if (!config__parse_int64_strict(env, &v) ||
425 v < G_GUEST_DAILY_TURNS_MIN || v > G_GUEST_DAILY_TURNS_MAX)
426 {
427 fprintf(stderr,
428 "[CONFIG] ERROR: AUTH_GUEST_DAILY_TURNS must be %d..%d\n",
429 G_GUEST_DAILY_TURNS_MIN, G_GUEST_DAILY_TURNS_MAX);
430 exit(1);
431 }
432 g_guest_daily_turns = v;
433 }
434 if ((env = getenv("AUTH_GUEST_DAILY_OUTPUT_TOKENS")) && env[0] != '\0')
435 {
436 int64 v;
437 if (!config__parse_int64_strict(env, &v) ||
438 v < G_GUEST_DAILY_OUTPUT_TOKENS_MIN ||
439 v > G_GUEST_DAILY_OUTPUT_TOKENS_MAX)
440 {
441 fprintf(stderr,
442 "[CONFIG] ERROR: AUTH_GUEST_DAILY_OUTPUT_TOKENS must be %d..%d\n",
443 G_GUEST_DAILY_OUTPUT_TOKENS_MIN, G_GUEST_DAILY_OUTPUT_TOKENS_MAX);
444 exit(1);
445 }
446 g_guest_daily_output_tokens = v;
447 }
448 if ((env = getenv("AUTH_GUEST_REQUEST_OUTPUT_TOKENS")) && env[0] != '\0')
449 {
450 int64 v;
451 if (!config__parse_int64_strict(env, &v) ||
452 v < G_GUEST_REQUEST_OUTPUT_TOKENS_MIN)
453 {
454 fprintf(stderr,
455 "[CONFIG] ERROR: AUTH_GUEST_REQUEST_OUTPUT_TOKENS must be >= %d\n",
456 G_GUEST_REQUEST_OUTPUT_TOKENS_MIN);
457 exit(1);
458 }
459 g_guest_request_output_tokens = v;
460 }
461 }
108 } 462 }
109 463
110 static void init_database(void) 464 static void init_database(void)
111 { 465 {
112 // Create data directory if needed 466 // Create data directory if needed
195 (void)sig; 549 (void)sig;
196 stop_server = 1; 550 stop_server = 1;
197 Seobeo_Web_Server_Stop(); 551 Seobeo_Web_Server_Stop();
198 } 552 }
199 553
200 void Seobeo_Render_Html( 554 static Seobeo_Request_Entry *html_render_error(Dowa_Arena *arena, const char *msg)
201 char *final_body, 555 {
202 char *template, 556 Seobeo_Request_Entry *resp = NULL;
203 Dowa_Arena *arena 557 Dowa_HashMap_Push_Arena(resp, "status", "500", arena);
204 ) 558 Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain; charset=utf-8", arena);
205 { 559 Dowa_HashMap_Push_Arena(resp, "body", (char *)msg, arena);
206 size_t current_offset = 0; 560 return resp;
207 char *cursor = template; 561 }
208 562
209 int32 token_len = 2; 563 #define HTML_PAGE_CAP (128 * 1024)
210
211 while (1)
212 {
213 char *start_tag = strstr(cursor, "{{");
214 if (!start_tag) break;
215
216 char *end_tag = strstr(start_tag, "}}");
217 if (!end_tag) break;
218
219 Seobeo_Log(SEOBEO_INFO, "[Curr] Life\n");
220
221 size_t leading_len = start_tag - cursor;
222 memcpy(final_body + current_offset, cursor, leading_len);
223 current_offset += leading_len;
224
225 size_t name_len = end_tag - (start_tag + token_len);
226 char *include_name = Dowa_Arena_Allocate(arena, name_len + 1);
227 memcpy(include_name, start_tag + token_len, name_len);
228 include_name[name_len] = '\0';
229
230 size_t sub_file_size = 0;
231 char *sub_content = Seobeo_Web_LoadFile(include_name, &sub_file_size);
232 Seobeo_Log(SEOBEO_DEBUG, "[TEMPLATE] Loading include: '%s' -> %s (size=%zu)\n",
233 include_name, sub_content ? "OK" : "FAILED", sub_file_size);
234 if (sub_content)
235 {
236 memcpy(final_body + current_offset, sub_content, sub_file_size);
237 current_offset += sub_file_size;
238 free(sub_content);
239 }
240
241 cursor = end_tag + 2;
242 }
243 strcpy(final_body + current_offset, cursor);
244 }
245
246 void Seobeo_Render_Html_FilePath(
247 char *final_body,
248 char *path,
249 Dowa_Arena *arena
250 ) {
251 Seobeo_Log(SEOBEO_DEBUG, "[TEMPLATE] Loading main template: '%s'\n", path);
252 size_t html_size = 0;
253 char *template = Seobeo_Web_LoadFile(path, &html_size);
254 Seobeo_Log(SEOBEO_DEBUG, "[TEMPLATE] Main template loaded: %s (size=%zu)\n", template ? "OK" : "FAILED", html_size);
255 if (!template) return;
256 Seobeo_Render_Html(final_body, template, arena);
257 }
258 564
259 Seobeo_Request_Entry* GetHomePage(Seobeo_Request_Entry *req, Dowa_Arena *arena) 565 Seobeo_Request_Entry* GetHomePage(Seobeo_Request_Entry *req, Dowa_Arena *arena)
260 { 566 {
261 Seobeo_Request_Entry *resp = NULL; 567 Seobeo_Request_Entry *resp = NULL;
262 char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024); 568 char *final_body = Dowa_Arena_Allocate(arena, HTML_PAGE_CAP);
263 Seobeo_Render_Html_FilePath(final_body, "/index.html", arena); 569 if (!final_body || !Mjj_Template_Render_File(final_body, HTML_PAGE_CAP, "/index.html", arena))
570 return html_render_error(arena, "Internal Server Error");
264 Dowa_HashMap_Push_Arena(resp, "body", final_body, arena); 571 Dowa_HashMap_Push_Arena(resp, "body", final_body, arena);
265 return resp; 572 return resp;
266 } 573 }
267 574
268 Seobeo_Request_Entry* GetResume(Seobeo_Request_Entry *req, Dowa_Arena *arena) 575 Seobeo_Request_Entry* GetResume(Seobeo_Request_Entry *req, Dowa_Arena *arena)
269 { 576 {
270 Seobeo_Request_Entry *resp = NULL; 577 Seobeo_Request_Entry *resp = NULL;
271 char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024); 578 char *final_body = Dowa_Arena_Allocate(arena, HTML_PAGE_CAP);
272 Seobeo_Render_Html_FilePath(final_body, "/resume/index.html", arena); 579 if (!final_body || !Mjj_Template_Render_File(final_body, HTML_PAGE_CAP, "/resume/index.html", arena))
580 return html_render_error(arena, "Internal Server Error");
273 Dowa_HashMap_Push_Arena(resp, "body", final_body, arena); 581 Dowa_HashMap_Push_Arena(resp, "body", final_body, arena);
274 return resp; 582 return resp;
275 } 583 }
276 584
277 Seobeo_Request_Entry* GetTools(Seobeo_Request_Entry *req, Dowa_Arena *arena) 585 Seobeo_Request_Entry* GetTools(Seobeo_Request_Entry *req, Dowa_Arena *arena)
278 { 586 {
279 Seobeo_Request_Entry *resp = NULL; 587 Seobeo_Request_Entry *resp = NULL;
280 char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024); 588 char *final_body = Dowa_Arena_Allocate(arena, HTML_PAGE_CAP);
281 Seobeo_Render_Html_FilePath(final_body, "/tools/index.html", arena); 589 if (!final_body || !Mjj_Template_Render_File(final_body, HTML_PAGE_CAP, "/tools/index.html", arena))
590 return html_render_error(arena, "Internal Server Error");
282 Dowa_HashMap_Push_Arena(resp, "body", final_body, arena); 591 Dowa_HashMap_Push_Arena(resp, "body", final_body, arena);
283 return resp; 592 return resp;
284 } 593 }
285 594
286 595
287 Seobeo_Request_Entry* GetMDToHTML(Seobeo_Request_Entry *req, Dowa_Arena *arena) 596 Seobeo_Request_Entry* GetMDToHTML(Seobeo_Request_Entry *req, Dowa_Arena *arena)
288 { 597 {
289 Seobeo_Request_Entry *resp = NULL; 598 Seobeo_Request_Entry *resp = NULL;
290 char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024); 599 char *final_body = Dowa_Arena_Allocate(arena, HTML_PAGE_CAP);
291 Seobeo_Render_Html_FilePath(final_body, "/tools/markdown_to_html/index.html", arena); 600 if (!final_body || !Mjj_Template_Render_File(final_body, HTML_PAGE_CAP, "/tools/markdown_to_html/index.html", arena))
601 return html_render_error(arena, "Internal Server Error");
292 Dowa_HashMap_Push_Arena(resp, "body", final_body, arena); 602 Dowa_HashMap_Push_Arena(resp, "body", final_body, arena);
293 return resp; 603 return resp;
294 } 604 }
295 605
296 Seobeo_Request_Entry* GetFileConverter(Seobeo_Request_Entry *req, Dowa_Arena *arena) 606 Seobeo_Request_Entry* GetFileConverter(Seobeo_Request_Entry *req, Dowa_Arena *arena)
297 { 607 {
298 Seobeo_Request_Entry *resp = NULL; 608 Seobeo_Request_Entry *resp = NULL;
299 char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024); 609 char *final_body = Dowa_Arena_Allocate(arena, HTML_PAGE_CAP);
300 Seobeo_Render_Html_FilePath(final_body, "/tools/file_converter/index.html", arena); 610 if (!final_body || !Mjj_Template_Render_File(final_body, HTML_PAGE_CAP, "/tools/file_converter/index.html", arena))
611 return html_render_error(arena, "Internal Server Error");
301 Dowa_HashMap_Push_Arena(resp, "body", final_body, arena); 612 Dowa_HashMap_Push_Arena(resp, "body", final_body, arena);
302 return resp; 613 return resp;
303 } 614 }
304 615
305 Seobeo_Request_Entry* GetHlsPlayer(Seobeo_Request_Entry *req, Dowa_Arena *arena) 616 Seobeo_Request_Entry* GetHlsPlayer(Seobeo_Request_Entry *req, Dowa_Arena *arena)
306 { 617 {
307 Seobeo_Request_Entry *resp = NULL; 618 Seobeo_Request_Entry *resp = NULL;
308 char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024); 619 char *final_body = Dowa_Arena_Allocate(arena, HTML_PAGE_CAP);
309 Seobeo_Render_Html_FilePath(final_body, "/tools/hls_player/index.html", arena); 620 if (!final_body || !Mjj_Template_Render_File(final_body, HTML_PAGE_CAP, "/tools/hls_player/index.html", arena))
621 return html_render_error(arena, "Internal Server Error");
310 Dowa_HashMap_Push_Arena(resp, "body", final_body, arena); 622 Dowa_HashMap_Push_Arena(resp, "body", final_body, arena);
311 return resp; 623 return resp;
312 } 624 }
313 625
314 Seobeo_Request_Entry* GetLatexEditor(Seobeo_Request_Entry *req, Dowa_Arena *arena) 626 Seobeo_Request_Entry* GetLatexEditor(Seobeo_Request_Entry *req, Dowa_Arena *arena)
315 { 627 {
316 Seobeo_Request_Entry *resp = NULL; 628 Seobeo_Request_Entry *resp = NULL;
317 char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024); 629 char *final_body = Dowa_Arena_Allocate(arena, HTML_PAGE_CAP);
318 Seobeo_Render_Html_FilePath(final_body, "/tools/latex_editor/index.html", arena); 630 if (!final_body || !Mjj_Template_Render_File(final_body, HTML_PAGE_CAP, "/tools/latex_editor/index.html", arena))
631 return html_render_error(arena, "Internal Server Error");
319 Dowa_HashMap_Push_Arena(resp, "body", final_body, arena); 632 Dowa_HashMap_Push_Arena(resp, "body", final_body, arena);
320 return resp; 633 return resp;
321 } 634 }
635
322 636
323 static Seobeo_Request_Entry *LatexErrorResponse( 637 static Seobeo_Request_Entry *LatexErrorResponse(
324 Dowa_Arena *arena, 638 Dowa_Arena *arena,
325 int status, 639 int status,
326 const char *message) 640 const char *message)
829 } 1143 }
830 1144
831 Seobeo_Request_Entry *RenderBlogList(Seobeo_Request_Entry *req, Dowa_Arena *arena) 1145 Seobeo_Request_Entry *RenderBlogList(Seobeo_Request_Entry *req, Dowa_Arena *arena)
832 { 1146 {
833 Seobeo_Request_Entry *resp = NULL; 1147 Seobeo_Request_Entry *resp = NULL;
834 char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024); 1148 char *final_body = Dowa_Arena_Allocate(arena, HTML_PAGE_CAP);
835 Seobeo_Render_Html_FilePath(final_body, "/blog/index.html", arena); 1149 if (!final_body || !Mjj_Template_Render_File(final_body, HTML_PAGE_CAP, "/blog/index.html", arena))
1150 return html_render_error(arena, "Internal Server Error");
836 Dowa_HashMap_Push_Arena(resp, "body", final_body, arena); 1151 Dowa_HashMap_Push_Arena(resp, "body", final_body, arena);
837 return resp; 1152 return resp;
838 } 1153 }
839 1154
840 1155
845 char *file_path = Dowa_Arena_Allocate(arena, 1024); 1160 char *file_path = Dowa_Arena_Allocate(arena, 1024);
846 void *blog_id_kv = Dowa_HashMap_Get_Ptr(req, ":blog_id"); 1161 void *blog_id_kv = Dowa_HashMap_Get_Ptr(req, ":blog_id");
847 char *blog_id = ((Seobeo_Request_Entry*)blog_id_kv)->value; 1162 char *blog_id = ((Seobeo_Request_Entry*)blog_id_kv)->value;
848 snprintf(file_path, 1024, "/blog/%s/index.html", blog_id); 1163 snprintf(file_path, 1024, "/blog/%s/index.html", blog_id);
849 1164
850 char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024); 1165 char *final_body = Dowa_Arena_Allocate(arena, HTML_PAGE_CAP);
851 Seobeo_Render_Html_FilePath(final_body, file_path, arena); 1166 if (!final_body || !Mjj_Template_Render_File(final_body, HTML_PAGE_CAP, file_path, arena))
1167 {
1168 Seobeo_Request_Entry *err = NULL;
1169 Dowa_HashMap_Push_Arena(err, "status", "404", arena);
1170 Dowa_HashMap_Push_Arena(err, "content-type", "text/plain; charset=utf-8", arena);
1171 Dowa_HashMap_Push_Arena(err, "body", "Not found", arena);
1172 return err;
1173 }
852 Dowa_HashMap_Push_Arena(resp, "body", final_body, arena); 1174 Dowa_HashMap_Push_Arena(resp, "body", final_body, arena);
853 return resp; 1175 return resp;
854 } 1176 }
855 1177
856 void Chat_Handler(Seobeo_WebSocket_Server_Connection *p_conn, Seobeo_WebSocket_Message *p_msg, void *p_user_data) 1178 void Chat_Handler(Seobeo_WebSocket_Server_Connection *p_conn, Seobeo_WebSocket_Message *p_msg, void *p_user_data)
868 } 1190 }
869 1191
870 Seobeo_Request_Entry *GetTalk(Seobeo_Request_Entry *req, Dowa_Arena *arena) 1192 Seobeo_Request_Entry *GetTalk(Seobeo_Request_Entry *req, Dowa_Arena *arena)
871 { 1193 {
872 Seobeo_Request_Entry *resp = NULL; 1194 Seobeo_Request_Entry *resp = NULL;
873 char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024); 1195 char *final_body = Dowa_Arena_Allocate(arena, HTML_PAGE_CAP);
874 Seobeo_Render_Html_FilePath(final_body, "/talk/index.html", arena); 1196 if (!final_body || !Mjj_Template_Render_File(final_body, HTML_PAGE_CAP, "/talk/index.html", arena))
1197 return html_render_error(arena, "Internal Server Error");
875 Dowa_HashMap_Push_Arena(resp, "body", final_body, arena); 1198 Dowa_HashMap_Push_Arena(resp, "body", final_body, arena);
876 return resp; 1199 return resp;
877 } 1200 }
878 1201
879 Seobeo_Request_Entry *GetJrpg(Seobeo_Request_Entry *req, Dowa_Arena *arena) 1202 Seobeo_Request_Entry *GetJrpg(Seobeo_Request_Entry *req, Dowa_Arena *arena)
880 { 1203 {
881 Seobeo_Request_Entry *resp = NULL; 1204 Seobeo_Request_Entry *resp = NULL;
882 char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024); 1205 char *final_body = Dowa_Arena_Allocate(arena, HTML_PAGE_CAP);
883 Seobeo_Render_Html_FilePath(final_body, "/jrpg/index.html", arena); 1206 if (!final_body || !Mjj_Template_Render_File(final_body, HTML_PAGE_CAP, "/jrpg/index.html", arena))
1207 return html_render_error(arena, "Internal Server Error");
884 Dowa_HashMap_Push_Arena(resp, "body", final_body, arena); 1208 Dowa_HashMap_Push_Arena(resp, "body", final_body, arena);
1209 Dowa_HashMap_Push_Arena(resp, "referrer-policy", "no-referrer", arena);
1210 return resp;
1211 }
1212
1213 Seobeo_Request_Entry *GetLogin(Seobeo_Request_Entry *req, Dowa_Arena *arena)
1214 {
1215 (void)req;
1216 Seobeo_Request_Entry *resp = NULL;
1217 char *final_body = Dowa_Arena_Allocate(arena, HTML_PAGE_CAP);
1218 if (!final_body || !Mjj_Template_Render_File(final_body, HTML_PAGE_CAP, "/login/index.html", arena))
1219 {
1220 Seobeo_Request_Entry *err = NULL;
1221 Dowa_HashMap_Push_Arena(err, "status", "500", arena);
1222 Dowa_HashMap_Push_Arena(err, "content-type", "text/plain; charset=utf-8", arena);
1223 Dowa_HashMap_Push_Arena(err, "cache-control", "no-store", arena);
1224 Dowa_HashMap_Push_Arena(err, "body", "Internal Server Error", arena);
1225 return err;
1226 }
1227 Dowa_HashMap_Push_Arena(resp, "body", final_body, arena);
1228 Dowa_HashMap_Push_Arena(
1229 resp, "content-type", "text/html; charset=utf-8", arena);
1230 Dowa_HashMap_Push_Arena(resp, "cache-control", "no-store", arena);
1231 Dowa_HashMap_Push_Arena(resp, "pragma", "no-cache", arena);
1232 Dowa_HashMap_Push_Arena(resp, "x-content-type-options", "nosniff", arena);
1233 Dowa_HashMap_Push_Arena(resp, "referrer-policy", "no-referrer", arena);
1234 Dowa_HashMap_Push_Arena(
1235 resp, "content-security-policy", "frame-ancestors 'none'", arena);
885 return resp; 1236 return resp;
886 } 1237 }
887 1238
888 Seobeo_Request_Entry *GetNotesLogin(Seobeo_Request_Entry *req, Dowa_Arena *arena) 1239 Seobeo_Request_Entry *GetNotesLogin(Seobeo_Request_Entry *req, Dowa_Arena *arena)
889 { 1240 {
890 Seobeo_Request_Entry *resp = NULL; 1241 Seobeo_Request_Entry *resp = NULL;
891 char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024); 1242 char *final_body = Dowa_Arena_Allocate(arena, HTML_PAGE_CAP);
892 Seobeo_Render_Html_FilePath(final_body, "/notes/login.html", arena); 1243 if (!final_body || !Mjj_Template_Render_File(final_body, HTML_PAGE_CAP, "/notes/login.html", arena))
1244 return html_render_error(arena, "Internal Server Error");
893 Dowa_HashMap_Push_Arena(resp, "body", final_body, arena); 1245 Dowa_HashMap_Push_Arena(resp, "body", final_body, arena);
894 return resp; 1246 return resp;
895 } 1247 }
896 1248
897 Seobeo_Request_Entry *GetNotes(Seobeo_Request_Entry *req, Dowa_Arena *arena) 1249 Seobeo_Request_Entry *GetNotes(Seobeo_Request_Entry *req, Dowa_Arena *arena)
898 { 1250 {
899 Seobeo_Request_Entry *resp = NULL; 1251 Seobeo_Request_Entry *resp = NULL;
900 char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024); 1252 char *final_body = Dowa_Arena_Allocate(arena, HTML_PAGE_CAP);
901 Seobeo_Render_Html_FilePath(final_body, "/notes/index.html", arena); 1253 if (!final_body || !Mjj_Template_Render_File(final_body, HTML_PAGE_CAP, "/notes/index.html", arena))
1254 return html_render_error(arena, "Internal Server Error");
902 Dowa_HashMap_Push_Arena(resp, "body", final_body, arena); 1255 Dowa_HashMap_Push_Arena(resp, "body", final_body, arena);
903 return resp; 1256 return resp;
904 } 1257 }
905 1258
906 Seobeo_Request_Entry *GetNoteById(Seobeo_Request_Entry *req, Dowa_Arena *arena) 1259 Seobeo_Request_Entry *GetNoteById(Seobeo_Request_Entry *req, Dowa_Arena *arena)
907 { 1260 {
908 Seobeo_Request_Entry *resp = NULL; 1261 Seobeo_Request_Entry *resp = NULL;
909 char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024); 1262 char *final_body = Dowa_Arena_Allocate(arena, HTML_PAGE_CAP);
910 // Same template - JavaScript handles the note_id from URL 1263 if (!final_body || !Mjj_Template_Render_File(final_body, HTML_PAGE_CAP, "/notes/index.html", arena))
911 Seobeo_Render_Html_FilePath(final_body, "/notes/index.html", arena); 1264 return html_render_error(arena, "Internal Server Error");
912 Dowa_HashMap_Push_Arena(resp, "body", final_body, arena); 1265 Dowa_HashMap_Push_Arena(resp, "body", final_body, arena);
913 return resp; 1266 return resp;
914 } 1267 }
915 1268
916 CREATE_REDIRECT_HANDLER(HomePage, "/") 1269 CREATE_REDIRECT_HANDLER(HomePage, "/")
2001 int main(void) 2354 int main(void)
2002 { 2355 {
2003 signal(SIGINT, handle_sigint); 2356 signal(SIGINT, handle_sigint);
2004 signal(SIGTERM, handle_sigint); 2357 signal(SIGTERM, handle_sigint);
2005 2358
2006 // Load server config 2359 // Load the ignored runtime config when present; environment overrides follow.
2007 load_config("mrjunejune/.config"); 2360 load_config("mrjunejune/.config");
2008 const char *database_override = getenv("MRJUNEJUNE_DB_PATH"); 2361
2009 if (database_override && database_override[0] != '\0') 2362 // Initialize S3 config using global credentials populated by load_config
2010 { 2363 g_s3_config.access_key_id = g_s3_access_key;
2011 snprintf(g_db_path, sizeof(g_db_path), "%s", database_override); 2364 g_s3_config.secret_access_key = g_s3_secret_key;
2012 }
2013
2014 // Load S3 credentials from .env
2015 FILE *env_file = fopen(".env", "r");
2016 static char s3_access_key[128] = {0};
2017 static char s3_secret_key[128] = {0};
2018
2019 if (env_file)
2020 {
2021 char line[512];
2022 while (fgets(line, sizeof(line), env_file))
2023 {
2024 if (strncmp(line, "AWS_MRJUNEJUNE_ACCESS_KEY=", 26) == 0)
2025 {
2026 char *val = line + 26;
2027 size_t len = strlen(val);
2028 while (len > 0 && (val[len-1] == '\n' || val[len-1] == '\r')) val[--len] = '\0';
2029 strncpy(s3_access_key, val, sizeof(s3_access_key) - 1);
2030 }
2031 else if (strncmp(line, "AWS_MRJUNEJUNE_SECRET_ACCESS_KEY=", 33) == 0)
2032 {
2033 char *val = line + 33;
2034 size_t len = strlen(val);
2035 while (len > 0 && (val[len-1] == '\n' || val[len-1] == '\r')) val[--len] = '\0';
2036 strncpy(s3_secret_key, val, sizeof(s3_secret_key) - 1);
2037 }
2038 }
2039 fclose(env_file);
2040 }
2041
2042 // Initialize S3 config
2043 g_s3_config.access_key_id = s3_access_key;
2044 g_s3_config.secret_access_key = s3_secret_key;
2045 g_s3_config.region = g_s3_region; 2365 g_s3_config.region = g_s3_region;
2046 g_s3_config.bucket = g_s3_bucket; 2366 g_s3_config.bucket = g_s3_bucket;
2047 g_s3_config.endpoint = NULL; 2367 g_s3_config.endpoint = NULL;
2048 g_s3_config.use_path_style = FALSE; 2368 g_s3_config.use_path_style = FALSE;
2049 2369
2050 printf("[S3] Configured: region=%s, bucket=%s, key=%s...\n", 2370 printf("[S3] Configured: region=%s, bucket=%s, key=%s...\n",
2051 g_s3_region, g_s3_bucket, s3_access_key[0] ? "***" : "(missing)"); 2371 g_s3_region, g_s3_bucket, g_s3_access_key[0] ? "***" : "(missing)");
2052 2372
2053 // Show current working directory 2373 // Show current working directory
2054 char cwd[1024]; 2374 char cwd[1024];
2055 if (getcwd(cwd, sizeof(cwd)) != NULL) 2375 if (getcwd(cwd, sizeof(cwd)) != NULL)
2056 { 2376 {
2058 printf("[STARTUP] Database path (relative): %s\n", g_db_path); 2378 printf("[STARTUP] Database path (relative): %s\n", g_db_path);
2059 } 2379 }
2060 2380
2061 // Initialize database 2381 // Initialize database
2062 init_database(); 2382 init_database();
2063 if (!Conversation_API_Init(g_db_path)) 2383 {
2064 Seobeo_Log(SEOBEO_ERROR, "[CONVERSATION] Store unavailable\n"); 2384 /* Validate per-request token cap against daily limit now that both
2385 * are finalised (env overrides run inside load_config). */
2386 if (g_guest_request_output_tokens > g_guest_daily_output_tokens)
2387 g_guest_request_output_tokens = g_guest_daily_output_tokens;
2388
2389 Conversation_API_Guest_Policy policy = {
2390 .guest_inference_enabled = g_guest_inference_enabled,
2391 .daily_turns = g_guest_daily_turns,
2392 .daily_output_tokens = g_guest_daily_output_tokens,
2393 .request_output_tokens = g_guest_request_output_tokens,
2394 };
2395 if (!Conversation_API_Init(g_db_path, &policy))
2396 Seobeo_Log(SEOBEO_ERROR, "[CONVERSATION] Store unavailable\n");
2397 }
2398
2399 /* Validate and decode AUTH_COOKIE_SECRET: hex string → raw bytes. */
2400 uint8 cookie_secret_bytes[AUTH_CRYPTO_COOKIE_SECRET_MAX_BYTES];
2401 size_t cookie_secret_byte_len = 0;
2402 {
2403 size_t hex_len = strlen(g_auth_cookie_secret);
2404 boolean bad_len = (
2405 hex_len < (size_t)(AUTH_CRYPTO_COOKIE_SECRET_MIN_BYTES * 2) ||
2406 hex_len > (size_t)(AUTH_CRYPTO_COOKIE_SECRET_MAX_BYTES * 2) ||
2407 hex_len % 2 != 0);
2408 if (bad_len)
2409 {
2410 OPENSSL_cleanse(g_auth_cookie_secret, sizeof(g_auth_cookie_secret));
2411 fprintf(stderr,
2412 "[AUTH] AUTH_COOKIE_SECRET must be %d–%d hex chars "
2413 "(%d–%d random bytes). "
2414 "Generate with: openssl rand -hex 32\n",
2415 AUTH_CRYPTO_COOKIE_SECRET_MIN_BYTES * 2,
2416 AUTH_CRYPTO_COOKIE_SECRET_MAX_BYTES * 2,
2417 AUTH_CRYPTO_COOKIE_SECRET_MIN_BYTES,
2418 AUTH_CRYPTO_COOKIE_SECRET_MAX_BYTES);
2419 exit(1);
2420 }
2421 cookie_secret_byte_len = config__hex_decode(
2422 g_auth_cookie_secret, cookie_secret_bytes, sizeof(cookie_secret_bytes));
2423 OPENSSL_cleanse(g_auth_cookie_secret, sizeof(g_auth_cookie_secret));
2424 if (cookie_secret_byte_len < (size_t)AUTH_CRYPTO_COOKIE_SECRET_MIN_BYTES)
2425 {
2426 OPENSSL_cleanse(cookie_secret_bytes, sizeof(cookie_secret_bytes));
2427 fprintf(stderr,
2428 "[AUTH] AUTH_COOKIE_SECRET contains non-hex characters or "
2429 "is too short.\n");
2430 exit(1);
2431 }
2432 }
2433
2434 /* Bootstrap pairing: both username+hash must be set, or both absent. */
2435 {
2436 boolean has_user = g_auth_bootstrap_username[0] != '\0';
2437 boolean has_hash = g_auth_bootstrap_password_hash[0] != '\0';
2438 if (has_user != has_hash)
2439 {
2440 OPENSSL_cleanse(cookie_secret_bytes, sizeof(cookie_secret_bytes));
2441 fprintf(stderr,
2442 "[AUTH] AUTH_BOOTSTRAP_USERNAME and AUTH_BOOTSTRAP_PASSWORD_HASH "
2443 "must both be set or both absent.\n");
2444 exit(1);
2445 }
2446 if (has_hash &&
2447 Auth_Crypto_Password_Hash_Validate(g_auth_bootstrap_password_hash) != AUTH_CRYPTO_OK)
2448 {
2449 OPENSSL_cleanse(cookie_secret_bytes, sizeof(cookie_secret_bytes));
2450 fprintf(stderr,
2451 "[AUTH] AUTH_BOOTSTRAP_PASSWORD_HASH has unrecognized format "
2452 "(expected zenbu-scrypt$v=1$...).\n");
2453 exit(1);
2454 }
2455 }
2456
2457 /* TTL validation: all positive, idle <= abs, all within 1 min – 1 year. */
2458 #define CONFIG_TTL_MIN_SECS 60
2459 #define CONFIG_TTL_MAX_SECS 31536000
2460 {
2461 int64 idle = g_auth_session_idle_ttl;
2462 int64 abst = g_auth_session_abs_ttl;
2463 int64 guest = g_auth_guest_ttl;
2464 if (idle <= 0 || abst <= 0 || guest <= 0 ||
2465 idle < CONFIG_TTL_MIN_SECS || idle > CONFIG_TTL_MAX_SECS ||
2466 abst < CONFIG_TTL_MIN_SECS || abst > CONFIG_TTL_MAX_SECS ||
2467 guest < CONFIG_TTL_MIN_SECS || guest > CONFIG_TTL_MAX_SECS)
2468 {
2469 OPENSSL_cleanse(cookie_secret_bytes, sizeof(cookie_secret_bytes));
2470 fprintf(stderr,
2471 "[AUTH] TTL values must be %d–%d seconds.\n",
2472 CONFIG_TTL_MIN_SECS, CONFIG_TTL_MAX_SECS);
2473 exit(1);
2474 }
2475 if (idle > abst)
2476 {
2477 OPENSSL_cleanse(cookie_secret_bytes, sizeof(cookie_secret_bytes));
2478 fprintf(stderr,
2479 "[AUTH] AUTH_SESSION_IDLE_TTL must not exceed "
2480 "AUTH_SESSION_ABS_TTL.\n");
2481 exit(1);
2482 }
2483 }
2484
2485 /* Trusted proxy: validate and canonicalize via inet_pton/inet_ntop. */
2486 if (g_auth_trusted_proxy[0] != '\0')
2487 {
2488 struct in_addr addr4;
2489 struct in6_addr addr6;
2490 char canonical[AUTH_CRYPTO_IP_MAX_BYTES];
2491 canonical[0] = '\0';
2492 if (inet_pton(AF_INET, g_auth_trusted_proxy, &addr4) == 1)
2493 {
2494 if (!inet_ntop(AF_INET, &addr4, canonical, sizeof(canonical)))
2495 {
2496 OPENSSL_cleanse(cookie_secret_bytes, sizeof(cookie_secret_bytes));
2497 fprintf(stderr,
2498 "[AUTH] AUTH_TRUSTED_PROXY: IPv4 canonicalization failed.\n");
2499 exit(1);
2500 }
2501 }
2502 else if (inet_pton(AF_INET6, g_auth_trusted_proxy, &addr6) == 1)
2503 {
2504 if (!inet_ntop(AF_INET6, &addr6, canonical, sizeof(canonical)))
2505 {
2506 OPENSSL_cleanse(cookie_secret_bytes, sizeof(cookie_secret_bytes));
2507 fprintf(stderr,
2508 "[AUTH] AUTH_TRUSTED_PROXY: IPv6 canonicalization failed.\n");
2509 exit(1);
2510 }
2511 }
2512 else
2513 {
2514 OPENSSL_cleanse(cookie_secret_bytes, sizeof(cookie_secret_bytes));
2515 fprintf(stderr,
2516 "[AUTH] AUTH_TRUSTED_PROXY must be a valid IPv4 or IPv6 "
2517 "address (e.g. 192.168.1.1 or ::1).\n");
2518 exit(1);
2519 }
2520 strncpy(g_auth_trusted_proxy, canonical, sizeof(g_auth_trusted_proxy) - 1);
2521 g_auth_trusted_proxy[sizeof(g_auth_trusted_proxy) - 1] = '\0';
2522 }
2523
2524 /*
2525 * Loopback enforcement: AUTH_DEV_INSECURE_COOKIE=true is only permitted
2526 * when SERVER_HOST is explicitly a loopback address. Production/edge
2527 * deployments must use Secure cookies.
2528 */
2529 {
2530 boolean is_loopback = (
2531 strcmp(g_server_host, "127.0.0.1") == 0 ||
2532 strcmp(g_server_host, "::1") == 0 ||
2533 strcmp(g_server_host, "localhost") == 0);
2534 if (g_auth_dev_insecure && !is_loopback)
2535 {
2536 OPENSSL_cleanse(cookie_secret_bytes, sizeof(cookie_secret_bytes));
2537 fprintf(stderr,
2538 "[AUTH] AUTH_DEV_INSECURE_COOKIE=true requires SERVER_HOST "
2539 "to be a loopback address (127.0.0.1, ::1, or localhost). "
2540 "Set SERVER_HOST=127.0.0.1 for local development.\n");
2541 exit(1);
2542 }
2543 }
2544
2545 /* Initialize auth module — fail closed: no auth means no server. */
2546 {
2547 if (!Auth_API_Init(
2548 g_db_path,
2549 cookie_secret_bytes, cookie_secret_byte_len,
2550 g_auth_bootstrap_username[0] ? g_auth_bootstrap_username : NULL,
2551 g_auth_bootstrap_password_hash[0] ? g_auth_bootstrap_password_hash : NULL,
2552 g_auth_trusted_proxy[0] ? g_auth_trusted_proxy : NULL,
2553 g_auth_session_idle_ttl,
2554 g_auth_session_abs_ttl,
2555 g_auth_guest_ttl,
2556 g_auth_dev_insecure))
2557 {
2558 OPENSSL_cleanse(cookie_secret_bytes, sizeof(cookie_secret_bytes));
2559 fprintf(stderr,
2560 "[AUTH] Auth init failed — refusing to start server.\n");
2561 exit(1);
2562 }
2563 OPENSSL_cleanse(cookie_secret_bytes, sizeof(cookie_secret_bytes));
2564 }
2065 const char *sidecar_path = getenv("MRJUNEJUNE_INFERENCE_SIDECAR_PATH"); 2565 const char *sidecar_path = getenv("MRJUNEJUNE_INFERENCE_SIDECAR_PATH");
2066 const char *copilot_cli_path = getenv("MRJUNEJUNE_COPILOT_CLI_PATH"); 2566 const char *copilot_cli_path = getenv("MRJUNEJUNE_COPILOT_CLI_PATH");
2067 if (sidecar_path && copilot_cli_path) 2567 if (sidecar_path && copilot_cli_path)
2068 { 2568 {
2069 if (!Conversation_API_Enable_Inference(sidecar_path, copilot_cli_path)) 2569 if (!Conversation_API_Enable_Inference(sidecar_path, copilot_cli_path))
2083 SEOBEO_ERROR, 2583 SEOBEO_ERROR,
2084 "[MEDIA] Unable to initialize the media worker pool\n"); 2584 "[MEDIA] Unable to initialize the media worker pool\n");
2085 } 2585 }
2086 2586
2087 Seobeo_Router_Init(); 2587 Seobeo_Router_Init();
2588 Auth_API_Register_Routes();
2589 Admin_API_Register_Routes();
2088 Conversation_API_Register_Routes(); 2590 Conversation_API_Register_Routes();
2089 2591
2090 Seobeo_Router_Register("GET", "/", GetHomePage); 2592 Seobeo_Router_Register("GET", "/", GetHomePage);
2091 Seobeo_Router_Register("GET", "/index.html", GetRedirectHomePage); 2593 Seobeo_Router_Register("GET", "/index.html", GetRedirectHomePage);
2092 2594
2134 Seobeo_Router_Register("GET", "/talk/index.html", GetRedirectTalk); 2636 Seobeo_Router_Register("GET", "/talk/index.html", GetRedirectTalk);
2135 2637
2136 // -- JRPG agent chat --/ 2638 // -- JRPG agent chat --/
2137 Seobeo_Router_Register("GET", "/jrpg", GetJrpg); 2639 Seobeo_Router_Register("GET", "/jrpg", GetJrpg);
2138 Seobeo_Router_Register("GET", "/jrpg/index.html", GetRedirectJrpg); 2640 Seobeo_Router_Register("GET", "/jrpg/index.html", GetRedirectJrpg);
2641
2642 // -- Login --/
2643 Seobeo_Router_Register("GET", "/login", GetLogin);
2139 2644
2140 // -- Notes --/ 2645 // -- Notes --/
2141 Seobeo_Router_Register("GET", "/notes", GetNotes); 2646 Seobeo_Router_Register("GET", "/notes", GetNotes);
2142 Seobeo_Router_Register("GET", "/notes/", GetNotes); 2647 Seobeo_Router_Register("GET", "/notes/", GetNotes);
2143 Seobeo_Router_Register("GET", "/notes/index.html", GetNotes); 2648 Seobeo_Router_Register("GET", "/notes/index.html", GetNotes);
2150 2655
2151 Seobeo_Log(SEOBEO_INFO, "WTF is going on\n"); 2656 Seobeo_Log(SEOBEO_INFO, "WTF is going on\n");
2152 const char *server_port = getenv("MRJUNEJUNE_PORT"); 2657 const char *server_port = getenv("MRJUNEJUNE_PORT");
2153 if (!server_port || server_port[0] == '\0') 2658 if (!server_port || server_port[0] == '\0')
2154 server_port = "6969"; 2659 server_port = "6969";
2155 Seobeo_Web_Server_Start("mrjunejune/src", server_port, SEOBEO_MODE_EDGE, 4); 2660 const char *server_bind = g_server_host[0] ? g_server_host : "0.0.0.0";
2661 Mjj_Template_Renderer_Init("mrjunejune/src");
2662 int server_result = Seobeo_Web_Server_Start_On(
2663 server_bind, "mrjunejune/src", server_port, SEOBEO_MODE_EDGE, 4);
2156 Seobeo_Worker_Pool_Destroy(g_media_worker_pool); 2664 Seobeo_Worker_Pool_Destroy(g_media_worker_pool);
2157 g_media_worker_pool = NULL; 2665 g_media_worker_pool = NULL;
2158 Conversation_API_Destroy(); 2666 Conversation_API_Destroy();
2159 } 2667 Auth_API_Destroy();
2668 if (server_result != 0)
2669 {
2670 fprintf(stderr, "[STARTUP] Server bind/listen failed (host=%s port=%s)\n",
2671 server_bind, server_port);
2672 return 1;
2673 }
2674 return 0;
2675 }