Mercurial
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 } |