Mercurial
diff 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 |
line wrap: on
line diff
--- a/mrjunejune/main.c Thu Aug 06 11:31:30 2026 -0700 +++ b/mrjunejune/main.c Fri Aug 07 07:34:12 2026 -0700 @@ -4,11 +4,21 @@ #include "deita/deita.h" #include "mrjunejune/latex_renderer.h" #include "mrjunejune/conversation_api.h" +#include "mrjunejune/auth_api.h" +#include "mrjunejune/admin_api.h" +#include "mrjunejune/template_renderer.h" +#include "auth/auth_crypto.h" +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <strings.h> #include <time.h> #include <sys/stat.h> #include <stdarg.h> #include <stdatomic.h> #include <pthread.h> +#include <arpa/inet.h> +#include <openssl/crypto.h> // UUID + /tmp/ + format (max 4) #define TMP_FILE_LENGTH 47 @@ -45,38 +55,128 @@ static int g_s3_url_expires = 3600; static S3_Config g_s3_config = {0}; static Deita_Connection *g_db_connection = NULL; +/* S3 credentials — never logged; zero after use in init. */ +static char g_s3_access_key[128] = {0}; +static char g_s3_secret_key[128] = {0}; + +/* Auth configuration */ +static char g_auth_cookie_secret[AUTH_CRYPTO_COOKIE_SECRET_MAX_BYTES * 2 + 1] = {0}; +static char g_auth_bootstrap_username[64] = {0}; +static char g_auth_bootstrap_password_hash[AUTH_CRYPTO_PASSWORD_HASH_ENCODED_SIZE] = {0}; +static char g_auth_trusted_proxy[AUTH_CRYPTO_IP_MAX_BYTES] = {0}; +static int64 g_auth_session_idle_ttl = AUTH_API_SESSION_IDLE_TTL_DEFAULT; +static int64 g_auth_session_abs_ttl = AUTH_API_SESSION_ABS_TTL_DEFAULT; +static int64 g_auth_guest_ttl = AUTH_API_GUEST_TTL_DEFAULT; +static boolean g_auth_dev_insecure = FALSE; +static char g_server_host[128] = {0}; /* SERVER_HOST; empty → 0.0.0.0 */ + +/* Guest inference / quota configuration */ +#define G_GUEST_DAILY_TURNS_DEFAULT 10 +#define G_GUEST_DAILY_OUTPUT_TOKENS_DEFAULT 20000 +#define G_GUEST_REQUEST_OUTPUT_TOKENS_MIN 1 +#define G_GUEST_DAILY_TURNS_MIN 1 +#define G_GUEST_DAILY_TURNS_MAX 10000 +#define G_GUEST_DAILY_OUTPUT_TOKENS_MIN 1 +#define G_GUEST_DAILY_OUTPUT_TOKENS_MAX 1000000 +#define G_GUEST_REQUEST_OUTPUT_TOKENS_DEFAULT 2048 +static boolean g_guest_inference_enabled = FALSE; +static int64 g_guest_daily_turns = G_GUEST_DAILY_TURNS_DEFAULT; +static int64 g_guest_daily_output_tokens = G_GUEST_DAILY_OUTPUT_TOKENS_DEFAULT; +static int64 g_guest_request_output_tokens = G_GUEST_REQUEST_OUTPUT_TOKENS_DEFAULT; + +/* + * Strict full-string integer parse: returns FALSE if value is empty, + * has trailing non-digit characters, or overflows a long. + */ +static boolean config__parse_int64_strict(const char *value, int64 *out) +{ + if (!value || value[0] == '\0') + return FALSE; + char *end = NULL; + long v = strtol(value, &end, 10); + if (end == value || *end != '\0') + return FALSE; + *out = (int64)v; + return TRUE; +} + +/* + * Decode a lowercase or uppercase hex string into raw bytes. + * Returns the number of decoded bytes, or 0 on any error. + * hex_len must be even; each pair of hex chars produces one byte. + */ +static size_t config__hex_decode(const char *hex, uint8 *out, size_t out_capacity) +{ + if (!hex || !out) return 0; + size_t hex_len = strlen(hex); + if (hex_len == 0 || hex_len % 2 != 0) return 0; + size_t byte_count = hex_len / 2; + if (byte_count > out_capacity) return 0; + for (size_t i = 0; i < byte_count; i++) + { + int h, l; + char hi = hex[i * 2]; + char lo = hex[i * 2 + 1]; + if (hi >= '0' && hi <= '9') h = hi - '0'; + else if (hi >= 'a' && hi <= 'f') h = hi - 'a' + 10; + else if (hi >= 'A' && hi <= 'F') h = hi - 'A' + 10; + else return 0; + if (lo >= '0' && lo <= '9') l = lo - '0'; + else if (lo >= 'a' && lo <= 'f') l = lo - 'a' + 10; + else if (lo >= 'A' && lo <= 'F') l = lo - 'A' + 10; + else return 0; + out[i] = (uint8)((h << 4) | l); + } + return byte_count; +} static void load_config(const char *config_path) { FILE *f = fopen(config_path, "r"); + char workspace_config_path[1024] = {0}; + if (!f) + { + const char *workspace = getenv("BUILD_WORKSPACE_DIRECTORY"); + if (workspace && workspace[0] != '\0') + { + int written = snprintf( + workspace_config_path, + sizeof(workspace_config_path), + "%s/%s", + workspace, + config_path); + if (written > 0 && (size_t)written < sizeof(workspace_config_path)) + f = fopen(workspace_config_path, "r"); + } + } if (!f) { printf("[CONFIG] Warning: Could not open %s, using defaults\n", config_path); - return; } - - char line[512]; - while (fgets(line, sizeof(line), f)) + else { - // Skip comments and empty lines - if (line[0] == '#' || line[0] == '\n' || line[0] == '\r') continue; + char line[512]; + while (fgets(line, sizeof(line), f)) + { + // Skip comments and empty lines + if (line[0] == '#' || line[0] == '\n' || line[0] == '\r') continue; - char *eq = strchr(line, '='); - if (!eq) continue; + char *eq = strchr(line, '='); + if (!eq) continue; - *eq = '\0'; - char *key = line; - char *value = eq + 1; + *eq = '\0'; + char *key = line; + char *value = eq + 1; - // Trim newline from value - size_t vlen = strlen(value); - while (vlen > 0 && (value[vlen-1] == '\n' || value[vlen-1] == '\r')) - value[--vlen] = '\0'; + // Trim newline from value + size_t vlen = strlen(value); + while (vlen > 0 && (value[vlen-1] == '\n' || value[vlen-1] == '\r')) + value[--vlen] = '\0'; - if (strcmp(key, "UPLOAD_AUTH_TOKEN") == 0) - { - strncpy(g_upload_auth_token, value, sizeof(g_upload_auth_token) - 1); - } + if (strcmp(key, "UPLOAD_AUTH_TOKEN") == 0) + { + strncpy(g_upload_auth_token, value, sizeof(g_upload_auth_token) - 1); + } else if (strcmp(key, "S3_REGION") == 0) { strncpy(g_s3_region, value, sizeof(g_s3_region) - 1); @@ -87,7 +187,13 @@ } else if (strcmp(key, "S3_URL_EXPIRES") == 0) { - g_s3_url_expires = atoi(value); + int64 v; + if (!config__parse_int64_strict(value, &v) || v <= 0) + { + fprintf(stderr, "[CONFIG] ERROR: S3_URL_EXPIRES must be a positive integer\n"); + exit(1); + } + g_s3_url_expires = (int)v; } else if (strcmp(key, "S3_CLOUDFRONT_URL") == 0) { @@ -97,14 +203,262 @@ { strncpy(g_db_path, value, sizeof(g_db_path) - 1); } + else if (strcmp(key, "AWS_MRJUNEJUNE_ACCESS_KEY") == 0) + { + strncpy(g_s3_access_key, value, sizeof(g_s3_access_key) - 1); + } + else if (strcmp(key, "AWS_MRJUNEJUNE_SECRET_ACCESS_KEY") == 0) + { + strncpy(g_s3_secret_key, value, sizeof(g_s3_secret_key) - 1); + } + else if (strcmp(key, "AUTH_COOKIE_SECRET") == 0) + { + /* Never log this value */ + strncpy(g_auth_cookie_secret, value, sizeof(g_auth_cookie_secret) - 1); + } + else if (strcmp(key, "AUTH_BOOTSTRAP_USERNAME") == 0) + { + strncpy(g_auth_bootstrap_username, value, + sizeof(g_auth_bootstrap_username) - 1); + } + else if (strcmp(key, "AUTH_BOOTSTRAP_PASSWORD_HASH") == 0) + { + strncpy(g_auth_bootstrap_password_hash, value, + sizeof(g_auth_bootstrap_password_hash) - 1); + } + else if (strcmp(key, "AUTH_TRUSTED_PROXY") == 0) + { + strncpy(g_auth_trusted_proxy, value, sizeof(g_auth_trusted_proxy) - 1); + } + else if (strcmp(key, "AUTH_SESSION_IDLE_TTL") == 0) + { + int64 v; + if (!config__parse_int64_strict(value, &v) || v <= 0) + { + fprintf(stderr, "[CONFIG] ERROR: AUTH_SESSION_IDLE_TTL must be a positive integer\n"); + exit(1); + } + g_auth_session_idle_ttl = v; + } + else if (strcmp(key, "AUTH_SESSION_ABS_TTL") == 0) + { + int64 v; + if (!config__parse_int64_strict(value, &v) || v <= 0) + { + fprintf(stderr, "[CONFIG] ERROR: AUTH_SESSION_ABS_TTL must be a positive integer\n"); + exit(1); + } + g_auth_session_abs_ttl = v; + } + else if (strcmp(key, "AUTH_GUEST_TTL") == 0) + { + int64 v; + if (!config__parse_int64_strict(value, &v) || v <= 0) + { + fprintf(stderr, "[CONFIG] ERROR: AUTH_GUEST_TTL must be a positive integer\n"); + exit(1); + } + g_auth_guest_ttl = v; + } + else if (strcmp(key, "AUTH_DEV_INSECURE_COOKIE") == 0) + { + g_auth_dev_insecure = + (strcmp(value, "1") == 0 || strcmp(value, "true") == 0); + } + else if (strcmp(key, "SERVER_HOST") == 0) + { + strncpy(g_server_host, value, sizeof(g_server_host) - 1); + } + else if (strcmp(key, "AUTH_GUEST_DAILY_TURNS") == 0) + { + int64 v; + if (!config__parse_int64_strict(value, &v) || + v < G_GUEST_DAILY_TURNS_MIN || v > G_GUEST_DAILY_TURNS_MAX) + { + printf("[CONFIG] ERROR: AUTH_GUEST_DAILY_TURNS must be %d..%d\n", + G_GUEST_DAILY_TURNS_MIN, G_GUEST_DAILY_TURNS_MAX); + exit(1); + } + g_guest_daily_turns = v; + } + else if (strcmp(key, "AUTH_GUEST_DAILY_OUTPUT_TOKENS") == 0) + { + int64 v; + if (!config__parse_int64_strict(value, &v) || + v < G_GUEST_DAILY_OUTPUT_TOKENS_MIN || + v > G_GUEST_DAILY_OUTPUT_TOKENS_MAX) + { + printf("[CONFIG] ERROR: AUTH_GUEST_DAILY_OUTPUT_TOKENS must be %d..%d\n", + G_GUEST_DAILY_OUTPUT_TOKENS_MIN, G_GUEST_DAILY_OUTPUT_TOKENS_MAX); + exit(1); + } + g_guest_daily_output_tokens = v; + } + else if (strcmp(key, "AUTH_GUEST_REQUEST_OUTPUT_TOKENS") == 0) + { + int64 v; + if (!config__parse_int64_strict(value, &v) || + v < G_GUEST_REQUEST_OUTPUT_TOKENS_MIN) + { + printf("[CONFIG] ERROR: AUTH_GUEST_REQUEST_OUTPUT_TOKENS must be >= %d\n", + G_GUEST_REQUEST_OUTPUT_TOKENS_MIN); + exit(1); + } + g_guest_request_output_tokens = v; + } + } + fclose(f); } - fclose(f); printf("[CONFIG] Loaded: token=%s..., region=%s, bucket=%s, expires=%d, cloudfront=%s, db=%s\n", g_upload_auth_token[0] ? "***" : "(empty)", g_s3_region, g_s3_bucket, g_s3_url_expires, g_s3_cloudfront_url[0] ? g_s3_cloudfront_url : "(none)", g_db_path); + printf("[CONFIG] Auth: secret=%s, bootstrap_user=%s, trusted_proxy=%s\n", + g_auth_cookie_secret[0] ? "(set)" : "(not set)", + g_auth_bootstrap_username[0] ? g_auth_bootstrap_username : "(none)", + g_auth_trusted_proxy[0] ? "(set)" : "(not set)"); + + const char *database_path_override = getenv("DB_PATH"); + const char *test_tmpdir = getenv("TEST_TMPDIR"); + if (database_path_override && database_path_override[0] != '\0') + { + strncpy(g_db_path, database_path_override, sizeof(g_db_path) - 1); + } + else if (test_tmpdir && test_tmpdir[0] != '\0') + { + snprintf(g_db_path, sizeof(g_db_path), "%s/mrjunejune.db", test_tmpdir); + } + + /* Environment overrides: env vars always take precedence over file. + * Values are never logged. */ + { + const char *env; + + /* S3 / server config env overrides */ + if ((env = getenv("UPLOAD_AUTH_TOKEN")) && env[0] != '\0') + strncpy(g_upload_auth_token, env, sizeof(g_upload_auth_token) - 1); + if ((env = getenv("S3_REGION")) && env[0] != '\0') + strncpy(g_s3_region, env, sizeof(g_s3_region) - 1); + if ((env = getenv("S3_BUCKET")) && env[0] != '\0') + strncpy(g_s3_bucket, env, sizeof(g_s3_bucket) - 1); + if ((env = getenv("S3_CLOUDFRONT_URL")) && env[0] != '\0') + strncpy(g_s3_cloudfront_url, env, sizeof(g_s3_cloudfront_url) - 1); + if ((env = getenv("S3_URL_EXPIRES")) && env[0] != '\0') + { + int64 v; + if (!config__parse_int64_strict(env, &v) || v <= 0) + { + fprintf(stderr, "[CONFIG] ERROR: S3_URL_EXPIRES must be a positive integer\n"); + exit(1); + } + g_s3_url_expires = (int)v; + } + if ((env = getenv("MRJUNEJUNE_DB_PATH")) && env[0] != '\0') + strncpy(g_db_path, env, sizeof(g_db_path) - 1); + if ((env = getenv("AWS_MRJUNEJUNE_ACCESS_KEY")) && env[0] != '\0') + strncpy(g_s3_access_key, env, sizeof(g_s3_access_key) - 1); + if ((env = getenv("AWS_MRJUNEJUNE_SECRET_ACCESS_KEY")) && env[0] != '\0') + strncpy(g_s3_secret_key, env, sizeof(g_s3_secret_key) - 1); + + /* Auth env overrides */ + if ((env = getenv("AUTH_COOKIE_SECRET")) && env[0] != '\0') + strncpy(g_auth_cookie_secret, env, sizeof(g_auth_cookie_secret) - 1); + if ((env = getenv("AUTH_BOOTSTRAP_USERNAME")) && env[0] != '\0') + strncpy(g_auth_bootstrap_username, env, + sizeof(g_auth_bootstrap_username) - 1); + if ((env = getenv("AUTH_BOOTSTRAP_PASSWORD_HASH")) && env[0] != '\0') + strncpy(g_auth_bootstrap_password_hash, env, + sizeof(g_auth_bootstrap_password_hash) - 1); + if ((env = getenv("AUTH_TRUSTED_PROXY")) && env[0] != '\0') + strncpy(g_auth_trusted_proxy, env, sizeof(g_auth_trusted_proxy) - 1); + if ((env = getenv("AUTH_SESSION_IDLE_TTL")) && env[0] != '\0') + { + int64 v; + if (!config__parse_int64_strict(env, &v) || v <= 0) + { + fprintf(stderr, "[CONFIG] ERROR: AUTH_SESSION_IDLE_TTL must be a positive integer\n"); + exit(1); + } + g_auth_session_idle_ttl = v; + } + if ((env = getenv("AUTH_SESSION_ABS_TTL")) && env[0] != '\0') + { + int64 v; + if (!config__parse_int64_strict(env, &v) || v <= 0) + { + fprintf(stderr, "[CONFIG] ERROR: AUTH_SESSION_ABS_TTL must be a positive integer\n"); + exit(1); + } + g_auth_session_abs_ttl = v; + } + if ((env = getenv("AUTH_GUEST_TTL")) && env[0] != '\0') + { + int64 v; + if (!config__parse_int64_strict(env, &v) || v <= 0) + { + fprintf(stderr, "[CONFIG] ERROR: AUTH_GUEST_TTL must be a positive integer\n"); + exit(1); + } + g_auth_guest_ttl = v; + } + if ((env = getenv("AUTH_DEV_INSECURE_COOKIE")) && env[0] != '\0') + g_auth_dev_insecure = + (strcmp(env, "1") == 0 || strcmp(env, "true") == 0); + if ((env = getenv("SERVER_HOST")) && env[0] != '\0') + strncpy(g_server_host, env, sizeof(g_server_host) - 1); + + /* Guest inference enable: runtime-only, boolean parsed strictly. */ + if ((env = getenv("MRJUNEJUNE_ALLOW_GUEST_INFERENCE")) && env[0] != '\0') + g_guest_inference_enabled = + (strcmp(env, "1") == 0 || strcasecmp(env, "true") == 0); + else if ((env = getenv("MRJUNEJUNE_ALLOW_ANONYMOUS_INFERENCE")) && env[0] != '\0') + g_guest_inference_enabled = + (strcmp(env, "1") == 0 || strcasecmp(env, "true") == 0); + + /* Guest quota env overrides: strict full-string integer, fail on malformed. */ + if ((env = getenv("AUTH_GUEST_DAILY_TURNS")) && env[0] != '\0') + { + int64 v; + if (!config__parse_int64_strict(env, &v) || + v < G_GUEST_DAILY_TURNS_MIN || v > G_GUEST_DAILY_TURNS_MAX) + { + fprintf(stderr, + "[CONFIG] ERROR: AUTH_GUEST_DAILY_TURNS must be %d..%d\n", + G_GUEST_DAILY_TURNS_MIN, G_GUEST_DAILY_TURNS_MAX); + exit(1); + } + g_guest_daily_turns = v; + } + if ((env = getenv("AUTH_GUEST_DAILY_OUTPUT_TOKENS")) && env[0] != '\0') + { + int64 v; + if (!config__parse_int64_strict(env, &v) || + v < G_GUEST_DAILY_OUTPUT_TOKENS_MIN || + v > G_GUEST_DAILY_OUTPUT_TOKENS_MAX) + { + fprintf(stderr, + "[CONFIG] ERROR: AUTH_GUEST_DAILY_OUTPUT_TOKENS must be %d..%d\n", + G_GUEST_DAILY_OUTPUT_TOKENS_MIN, G_GUEST_DAILY_OUTPUT_TOKENS_MAX); + exit(1); + } + g_guest_daily_output_tokens = v; + } + if ((env = getenv("AUTH_GUEST_REQUEST_OUTPUT_TOKENS")) && env[0] != '\0') + { + int64 v; + if (!config__parse_int64_strict(env, &v) || + v < G_GUEST_REQUEST_OUTPUT_TOKENS_MIN) + { + fprintf(stderr, + "[CONFIG] ERROR: AUTH_GUEST_REQUEST_OUTPUT_TOKENS must be >= %d\n", + G_GUEST_REQUEST_OUTPUT_TOKENS_MIN); + exit(1); + } + g_guest_request_output_tokens = v; + } + } } static void init_database(void) @@ -197,88 +551,43 @@ Seobeo_Web_Server_Stop(); } -void Seobeo_Render_Html( - char *final_body, - char *template, - Dowa_Arena *arena -) +static Seobeo_Request_Entry *html_render_error(Dowa_Arena *arena, const char *msg) { - size_t current_offset = 0; - char *cursor = template; - - int32 token_len = 2; - - while (1) - { - char *start_tag = strstr(cursor, "{{"); - if (!start_tag) break; - - char *end_tag = strstr(start_tag, "}}"); - if (!end_tag) break; - - Seobeo_Log(SEOBEO_INFO, "[Curr] Life\n"); - - size_t leading_len = start_tag - cursor; - memcpy(final_body + current_offset, cursor, leading_len); - current_offset += leading_len; - - size_t name_len = end_tag - (start_tag + token_len); - char *include_name = Dowa_Arena_Allocate(arena, name_len + 1); - memcpy(include_name, start_tag + token_len, name_len); - include_name[name_len] = '\0'; - - size_t sub_file_size = 0; - char *sub_content = Seobeo_Web_LoadFile(include_name, &sub_file_size); - Seobeo_Log(SEOBEO_DEBUG, "[TEMPLATE] Loading include: '%s' -> %s (size=%zu)\n", - include_name, sub_content ? "OK" : "FAILED", sub_file_size); - if (sub_content) - { - memcpy(final_body + current_offset, sub_content, sub_file_size); - current_offset += sub_file_size; - free(sub_content); - } - - cursor = end_tag + 2; - } - strcpy(final_body + current_offset, cursor); + Seobeo_Request_Entry *resp = NULL; + Dowa_HashMap_Push_Arena(resp, "status", "500", arena); + Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain; charset=utf-8", arena); + Dowa_HashMap_Push_Arena(resp, "body", (char *)msg, arena); + return resp; } -void Seobeo_Render_Html_FilePath( - char *final_body, - char *path, - Dowa_Arena *arena -) { - Seobeo_Log(SEOBEO_DEBUG, "[TEMPLATE] Loading main template: '%s'\n", path); - size_t html_size = 0; - char *template = Seobeo_Web_LoadFile(path, &html_size); - Seobeo_Log(SEOBEO_DEBUG, "[TEMPLATE] Main template loaded: %s (size=%zu)\n", template ? "OK" : "FAILED", html_size); - if (!template) return; - Seobeo_Render_Html(final_body, template, arena); -} +#define HTML_PAGE_CAP (128 * 1024) Seobeo_Request_Entry* GetHomePage(Seobeo_Request_Entry *req, Dowa_Arena *arena) { - Seobeo_Request_Entry *resp = NULL; - char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024); - Seobeo_Render_Html_FilePath(final_body, "/index.html", arena); + Seobeo_Request_Entry *resp = NULL; + char *final_body = Dowa_Arena_Allocate(arena, HTML_PAGE_CAP); + if (!final_body || !Mjj_Template_Render_File(final_body, HTML_PAGE_CAP, "/index.html", arena)) + return html_render_error(arena, "Internal Server Error"); Dowa_HashMap_Push_Arena(resp, "body", final_body, arena); return resp; } Seobeo_Request_Entry* GetResume(Seobeo_Request_Entry *req, Dowa_Arena *arena) { - Seobeo_Request_Entry *resp = NULL; - char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024); - Seobeo_Render_Html_FilePath(final_body, "/resume/index.html", arena); + Seobeo_Request_Entry *resp = NULL; + char *final_body = Dowa_Arena_Allocate(arena, HTML_PAGE_CAP); + if (!final_body || !Mjj_Template_Render_File(final_body, HTML_PAGE_CAP, "/resume/index.html", arena)) + return html_render_error(arena, "Internal Server Error"); Dowa_HashMap_Push_Arena(resp, "body", final_body, arena); return resp; } Seobeo_Request_Entry* GetTools(Seobeo_Request_Entry *req, Dowa_Arena *arena) { - Seobeo_Request_Entry *resp = NULL; - char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024); - Seobeo_Render_Html_FilePath(final_body, "/tools/index.html", arena); + Seobeo_Request_Entry *resp = NULL; + char *final_body = Dowa_Arena_Allocate(arena, HTML_PAGE_CAP); + if (!final_body || !Mjj_Template_Render_File(final_body, HTML_PAGE_CAP, "/tools/index.html", arena)) + return html_render_error(arena, "Internal Server Error"); Dowa_HashMap_Push_Arena(resp, "body", final_body, arena); return resp; } @@ -287,8 +596,9 @@ Seobeo_Request_Entry* GetMDToHTML(Seobeo_Request_Entry *req, Dowa_Arena *arena) { Seobeo_Request_Entry *resp = NULL; - char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024); - Seobeo_Render_Html_FilePath(final_body, "/tools/markdown_to_html/index.html", arena); + char *final_body = Dowa_Arena_Allocate(arena, HTML_PAGE_CAP); + if (!final_body || !Mjj_Template_Render_File(final_body, HTML_PAGE_CAP, "/tools/markdown_to_html/index.html", arena)) + return html_render_error(arena, "Internal Server Error"); Dowa_HashMap_Push_Arena(resp, "body", final_body, arena); return resp; } @@ -296,8 +606,9 @@ Seobeo_Request_Entry* GetFileConverter(Seobeo_Request_Entry *req, Dowa_Arena *arena) { Seobeo_Request_Entry *resp = NULL; - char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024); - Seobeo_Render_Html_FilePath(final_body, "/tools/file_converter/index.html", arena); + char *final_body = Dowa_Arena_Allocate(arena, HTML_PAGE_CAP); + if (!final_body || !Mjj_Template_Render_File(final_body, HTML_PAGE_CAP, "/tools/file_converter/index.html", arena)) + return html_render_error(arena, "Internal Server Error"); Dowa_HashMap_Push_Arena(resp, "body", final_body, arena); return resp; } @@ -305,8 +616,9 @@ Seobeo_Request_Entry* GetHlsPlayer(Seobeo_Request_Entry *req, Dowa_Arena *arena) { Seobeo_Request_Entry *resp = NULL; - char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024); - Seobeo_Render_Html_FilePath(final_body, "/tools/hls_player/index.html", arena); + char *final_body = Dowa_Arena_Allocate(arena, HTML_PAGE_CAP); + if (!final_body || !Mjj_Template_Render_File(final_body, HTML_PAGE_CAP, "/tools/hls_player/index.html", arena)) + return html_render_error(arena, "Internal Server Error"); Dowa_HashMap_Push_Arena(resp, "body", final_body, arena); return resp; } @@ -314,12 +626,14 @@ Seobeo_Request_Entry* GetLatexEditor(Seobeo_Request_Entry *req, Dowa_Arena *arena) { Seobeo_Request_Entry *resp = NULL; - char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024); - Seobeo_Render_Html_FilePath(final_body, "/tools/latex_editor/index.html", arena); + char *final_body = Dowa_Arena_Allocate(arena, HTML_PAGE_CAP); + if (!final_body || !Mjj_Template_Render_File(final_body, HTML_PAGE_CAP, "/tools/latex_editor/index.html", arena)) + return html_render_error(arena, "Internal Server Error"); Dowa_HashMap_Push_Arena(resp, "body", final_body, arena); return resp; } + static Seobeo_Request_Entry *LatexErrorResponse( Dowa_Arena *arena, int status, @@ -831,8 +1145,9 @@ Seobeo_Request_Entry *RenderBlogList(Seobeo_Request_Entry *req, Dowa_Arena *arena) { Seobeo_Request_Entry *resp = NULL; - char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024); - Seobeo_Render_Html_FilePath(final_body, "/blog/index.html", arena); + char *final_body = Dowa_Arena_Allocate(arena, HTML_PAGE_CAP); + if (!final_body || !Mjj_Template_Render_File(final_body, HTML_PAGE_CAP, "/blog/index.html", arena)) + return html_render_error(arena, "Internal Server Error"); Dowa_HashMap_Push_Arena(resp, "body", final_body, arena); return resp; } @@ -847,8 +1162,15 @@ char *blog_id = ((Seobeo_Request_Entry*)blog_id_kv)->value; snprintf(file_path, 1024, "/blog/%s/index.html", blog_id); - char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024); - Seobeo_Render_Html_FilePath(final_body, file_path, arena); + char *final_body = Dowa_Arena_Allocate(arena, HTML_PAGE_CAP); + if (!final_body || !Mjj_Template_Render_File(final_body, HTML_PAGE_CAP, file_path, arena)) + { + Seobeo_Request_Entry *err = NULL; + Dowa_HashMap_Push_Arena(err, "status", "404", arena); + Dowa_HashMap_Push_Arena(err, "content-type", "text/plain; charset=utf-8", arena); + Dowa_HashMap_Push_Arena(err, "body", "Not found", arena); + return err; + } Dowa_HashMap_Push_Arena(resp, "body", final_body, arena); return resp; } @@ -870,8 +1192,9 @@ Seobeo_Request_Entry *GetTalk(Seobeo_Request_Entry *req, Dowa_Arena *arena) { Seobeo_Request_Entry *resp = NULL; - char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024); - Seobeo_Render_Html_FilePath(final_body, "/talk/index.html", arena); + char *final_body = Dowa_Arena_Allocate(arena, HTML_PAGE_CAP); + if (!final_body || !Mjj_Template_Render_File(final_body, HTML_PAGE_CAP, "/talk/index.html", arena)) + return html_render_error(arena, "Internal Server Error"); Dowa_HashMap_Push_Arena(resp, "body", final_body, arena); return resp; } @@ -879,17 +1202,46 @@ Seobeo_Request_Entry *GetJrpg(Seobeo_Request_Entry *req, Dowa_Arena *arena) { Seobeo_Request_Entry *resp = NULL; - char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024); - Seobeo_Render_Html_FilePath(final_body, "/jrpg/index.html", arena); + char *final_body = Dowa_Arena_Allocate(arena, HTML_PAGE_CAP); + if (!final_body || !Mjj_Template_Render_File(final_body, HTML_PAGE_CAP, "/jrpg/index.html", arena)) + return html_render_error(arena, "Internal Server Error"); Dowa_HashMap_Push_Arena(resp, "body", final_body, arena); + Dowa_HashMap_Push_Arena(resp, "referrer-policy", "no-referrer", arena); + return resp; +} + +Seobeo_Request_Entry *GetLogin(Seobeo_Request_Entry *req, Dowa_Arena *arena) +{ + (void)req; + Seobeo_Request_Entry *resp = NULL; + char *final_body = Dowa_Arena_Allocate(arena, HTML_PAGE_CAP); + if (!final_body || !Mjj_Template_Render_File(final_body, HTML_PAGE_CAP, "/login/index.html", arena)) + { + Seobeo_Request_Entry *err = NULL; + Dowa_HashMap_Push_Arena(err, "status", "500", arena); + Dowa_HashMap_Push_Arena(err, "content-type", "text/plain; charset=utf-8", arena); + Dowa_HashMap_Push_Arena(err, "cache-control", "no-store", arena); + Dowa_HashMap_Push_Arena(err, "body", "Internal Server Error", arena); + return err; + } + Dowa_HashMap_Push_Arena(resp, "body", final_body, arena); + Dowa_HashMap_Push_Arena( + resp, "content-type", "text/html; charset=utf-8", arena); + Dowa_HashMap_Push_Arena(resp, "cache-control", "no-store", arena); + Dowa_HashMap_Push_Arena(resp, "pragma", "no-cache", arena); + Dowa_HashMap_Push_Arena(resp, "x-content-type-options", "nosniff", arena); + Dowa_HashMap_Push_Arena(resp, "referrer-policy", "no-referrer", arena); + Dowa_HashMap_Push_Arena( + resp, "content-security-policy", "frame-ancestors 'none'", arena); return resp; } Seobeo_Request_Entry *GetNotesLogin(Seobeo_Request_Entry *req, Dowa_Arena *arena) { Seobeo_Request_Entry *resp = NULL; - char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024); - Seobeo_Render_Html_FilePath(final_body, "/notes/login.html", arena); + char *final_body = Dowa_Arena_Allocate(arena, HTML_PAGE_CAP); + if (!final_body || !Mjj_Template_Render_File(final_body, HTML_PAGE_CAP, "/notes/login.html", arena)) + return html_render_error(arena, "Internal Server Error"); Dowa_HashMap_Push_Arena(resp, "body", final_body, arena); return resp; } @@ -897,8 +1249,9 @@ Seobeo_Request_Entry *GetNotes(Seobeo_Request_Entry *req, Dowa_Arena *arena) { Seobeo_Request_Entry *resp = NULL; - char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024); - Seobeo_Render_Html_FilePath(final_body, "/notes/index.html", arena); + char *final_body = Dowa_Arena_Allocate(arena, HTML_PAGE_CAP); + if (!final_body || !Mjj_Template_Render_File(final_body, HTML_PAGE_CAP, "/notes/index.html", arena)) + return html_render_error(arena, "Internal Server Error"); Dowa_HashMap_Push_Arena(resp, "body", final_body, arena); return resp; } @@ -906,9 +1259,9 @@ Seobeo_Request_Entry *GetNoteById(Seobeo_Request_Entry *req, Dowa_Arena *arena) { Seobeo_Request_Entry *resp = NULL; - char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024); - // Same template - JavaScript handles the note_id from URL - Seobeo_Render_Html_FilePath(final_body, "/notes/index.html", arena); + char *final_body = Dowa_Arena_Allocate(arena, HTML_PAGE_CAP); + if (!final_body || !Mjj_Template_Render_File(final_body, HTML_PAGE_CAP, "/notes/index.html", arena)) + return html_render_error(arena, "Internal Server Error"); Dowa_HashMap_Push_Arena(resp, "body", final_body, arena); return resp; } @@ -2003,52 +2356,19 @@ signal(SIGINT, handle_sigint); signal(SIGTERM, handle_sigint); - // Load server config + // Load the ignored runtime config when present; environment overrides follow. load_config("mrjunejune/.config"); - const char *database_override = getenv("MRJUNEJUNE_DB_PATH"); - if (database_override && database_override[0] != '\0') - { - snprintf(g_db_path, sizeof(g_db_path), "%s", database_override); - } - - // Load S3 credentials from .env - FILE *env_file = fopen(".env", "r"); - static char s3_access_key[128] = {0}; - static char s3_secret_key[128] = {0}; - if (env_file) - { - char line[512]; - while (fgets(line, sizeof(line), env_file)) - { - if (strncmp(line, "AWS_MRJUNEJUNE_ACCESS_KEY=", 26) == 0) - { - char *val = line + 26; - size_t len = strlen(val); - while (len > 0 && (val[len-1] == '\n' || val[len-1] == '\r')) val[--len] = '\0'; - strncpy(s3_access_key, val, sizeof(s3_access_key) - 1); - } - else if (strncmp(line, "AWS_MRJUNEJUNE_SECRET_ACCESS_KEY=", 33) == 0) - { - char *val = line + 33; - size_t len = strlen(val); - while (len > 0 && (val[len-1] == '\n' || val[len-1] == '\r')) val[--len] = '\0'; - strncpy(s3_secret_key, val, sizeof(s3_secret_key) - 1); - } - } - fclose(env_file); - } - - // Initialize S3 config - g_s3_config.access_key_id = s3_access_key; - g_s3_config.secret_access_key = s3_secret_key; + // Initialize S3 config using global credentials populated by load_config + g_s3_config.access_key_id = g_s3_access_key; + g_s3_config.secret_access_key = g_s3_secret_key; g_s3_config.region = g_s3_region; g_s3_config.bucket = g_s3_bucket; g_s3_config.endpoint = NULL; g_s3_config.use_path_style = FALSE; printf("[S3] Configured: region=%s, bucket=%s, key=%s...\n", - g_s3_region, g_s3_bucket, s3_access_key[0] ? "***" : "(missing)"); + g_s3_region, g_s3_bucket, g_s3_access_key[0] ? "***" : "(missing)"); // Show current working directory char cwd[1024]; @@ -2060,8 +2380,188 @@ // Initialize database init_database(); - if (!Conversation_API_Init(g_db_path)) - Seobeo_Log(SEOBEO_ERROR, "[CONVERSATION] Store unavailable\n"); + { + /* Validate per-request token cap against daily limit now that both + * are finalised (env overrides run inside load_config). */ + if (g_guest_request_output_tokens > g_guest_daily_output_tokens) + g_guest_request_output_tokens = g_guest_daily_output_tokens; + + Conversation_API_Guest_Policy policy = { + .guest_inference_enabled = g_guest_inference_enabled, + .daily_turns = g_guest_daily_turns, + .daily_output_tokens = g_guest_daily_output_tokens, + .request_output_tokens = g_guest_request_output_tokens, + }; + if (!Conversation_API_Init(g_db_path, &policy)) + Seobeo_Log(SEOBEO_ERROR, "[CONVERSATION] Store unavailable\n"); + } + + /* Validate and decode AUTH_COOKIE_SECRET: hex string → raw bytes. */ + uint8 cookie_secret_bytes[AUTH_CRYPTO_COOKIE_SECRET_MAX_BYTES]; + size_t cookie_secret_byte_len = 0; + { + size_t hex_len = strlen(g_auth_cookie_secret); + boolean bad_len = ( + hex_len < (size_t)(AUTH_CRYPTO_COOKIE_SECRET_MIN_BYTES * 2) || + hex_len > (size_t)(AUTH_CRYPTO_COOKIE_SECRET_MAX_BYTES * 2) || + hex_len % 2 != 0); + if (bad_len) + { + OPENSSL_cleanse(g_auth_cookie_secret, sizeof(g_auth_cookie_secret)); + fprintf(stderr, + "[AUTH] AUTH_COOKIE_SECRET must be %d–%d hex chars " + "(%d–%d random bytes). " + "Generate with: openssl rand -hex 32\n", + AUTH_CRYPTO_COOKIE_SECRET_MIN_BYTES * 2, + AUTH_CRYPTO_COOKIE_SECRET_MAX_BYTES * 2, + AUTH_CRYPTO_COOKIE_SECRET_MIN_BYTES, + AUTH_CRYPTO_COOKIE_SECRET_MAX_BYTES); + exit(1); + } + cookie_secret_byte_len = config__hex_decode( + g_auth_cookie_secret, cookie_secret_bytes, sizeof(cookie_secret_bytes)); + OPENSSL_cleanse(g_auth_cookie_secret, sizeof(g_auth_cookie_secret)); + if (cookie_secret_byte_len < (size_t)AUTH_CRYPTO_COOKIE_SECRET_MIN_BYTES) + { + OPENSSL_cleanse(cookie_secret_bytes, sizeof(cookie_secret_bytes)); + fprintf(stderr, + "[AUTH] AUTH_COOKIE_SECRET contains non-hex characters or " + "is too short.\n"); + exit(1); + } + } + + /* Bootstrap pairing: both username+hash must be set, or both absent. */ + { + boolean has_user = g_auth_bootstrap_username[0] != '\0'; + boolean has_hash = g_auth_bootstrap_password_hash[0] != '\0'; + if (has_user != has_hash) + { + OPENSSL_cleanse(cookie_secret_bytes, sizeof(cookie_secret_bytes)); + fprintf(stderr, + "[AUTH] AUTH_BOOTSTRAP_USERNAME and AUTH_BOOTSTRAP_PASSWORD_HASH " + "must both be set or both absent.\n"); + exit(1); + } + if (has_hash && + Auth_Crypto_Password_Hash_Validate(g_auth_bootstrap_password_hash) != AUTH_CRYPTO_OK) + { + OPENSSL_cleanse(cookie_secret_bytes, sizeof(cookie_secret_bytes)); + fprintf(stderr, + "[AUTH] AUTH_BOOTSTRAP_PASSWORD_HASH has unrecognized format " + "(expected zenbu-scrypt$v=1$...).\n"); + exit(1); + } + } + + /* TTL validation: all positive, idle <= abs, all within 1 min – 1 year. */ +#define CONFIG_TTL_MIN_SECS 60 +#define CONFIG_TTL_MAX_SECS 31536000 + { + int64 idle = g_auth_session_idle_ttl; + int64 abst = g_auth_session_abs_ttl; + int64 guest = g_auth_guest_ttl; + if (idle <= 0 || abst <= 0 || guest <= 0 || + idle < CONFIG_TTL_MIN_SECS || idle > CONFIG_TTL_MAX_SECS || + abst < CONFIG_TTL_MIN_SECS || abst > CONFIG_TTL_MAX_SECS || + guest < CONFIG_TTL_MIN_SECS || guest > CONFIG_TTL_MAX_SECS) + { + OPENSSL_cleanse(cookie_secret_bytes, sizeof(cookie_secret_bytes)); + fprintf(stderr, + "[AUTH] TTL values must be %d–%d seconds.\n", + CONFIG_TTL_MIN_SECS, CONFIG_TTL_MAX_SECS); + exit(1); + } + if (idle > abst) + { + OPENSSL_cleanse(cookie_secret_bytes, sizeof(cookie_secret_bytes)); + fprintf(stderr, + "[AUTH] AUTH_SESSION_IDLE_TTL must not exceed " + "AUTH_SESSION_ABS_TTL.\n"); + exit(1); + } + } + + /* Trusted proxy: validate and canonicalize via inet_pton/inet_ntop. */ + if (g_auth_trusted_proxy[0] != '\0') + { + struct in_addr addr4; + struct in6_addr addr6; + char canonical[AUTH_CRYPTO_IP_MAX_BYTES]; + canonical[0] = '\0'; + if (inet_pton(AF_INET, g_auth_trusted_proxy, &addr4) == 1) + { + if (!inet_ntop(AF_INET, &addr4, canonical, sizeof(canonical))) + { + OPENSSL_cleanse(cookie_secret_bytes, sizeof(cookie_secret_bytes)); + fprintf(stderr, + "[AUTH] AUTH_TRUSTED_PROXY: IPv4 canonicalization failed.\n"); + exit(1); + } + } + else if (inet_pton(AF_INET6, g_auth_trusted_proxy, &addr6) == 1) + { + if (!inet_ntop(AF_INET6, &addr6, canonical, sizeof(canonical))) + { + OPENSSL_cleanse(cookie_secret_bytes, sizeof(cookie_secret_bytes)); + fprintf(stderr, + "[AUTH] AUTH_TRUSTED_PROXY: IPv6 canonicalization failed.\n"); + exit(1); + } + } + else + { + OPENSSL_cleanse(cookie_secret_bytes, sizeof(cookie_secret_bytes)); + fprintf(stderr, + "[AUTH] AUTH_TRUSTED_PROXY must be a valid IPv4 or IPv6 " + "address (e.g. 192.168.1.1 or ::1).\n"); + exit(1); + } + strncpy(g_auth_trusted_proxy, canonical, sizeof(g_auth_trusted_proxy) - 1); + g_auth_trusted_proxy[sizeof(g_auth_trusted_proxy) - 1] = '\0'; + } + + /* + * Loopback enforcement: AUTH_DEV_INSECURE_COOKIE=true is only permitted + * when SERVER_HOST is explicitly a loopback address. Production/edge + * deployments must use Secure cookies. + */ + { + boolean is_loopback = ( + strcmp(g_server_host, "127.0.0.1") == 0 || + strcmp(g_server_host, "::1") == 0 || + strcmp(g_server_host, "localhost") == 0); + if (g_auth_dev_insecure && !is_loopback) + { + OPENSSL_cleanse(cookie_secret_bytes, sizeof(cookie_secret_bytes)); + fprintf(stderr, + "[AUTH] AUTH_DEV_INSECURE_COOKIE=true requires SERVER_HOST " + "to be a loopback address (127.0.0.1, ::1, or localhost). " + "Set SERVER_HOST=127.0.0.1 for local development.\n"); + exit(1); + } + } + + /* Initialize auth module — fail closed: no auth means no server. */ + { + if (!Auth_API_Init( + g_db_path, + cookie_secret_bytes, cookie_secret_byte_len, + g_auth_bootstrap_username[0] ? g_auth_bootstrap_username : NULL, + g_auth_bootstrap_password_hash[0] ? g_auth_bootstrap_password_hash : NULL, + g_auth_trusted_proxy[0] ? g_auth_trusted_proxy : NULL, + g_auth_session_idle_ttl, + g_auth_session_abs_ttl, + g_auth_guest_ttl, + g_auth_dev_insecure)) + { + OPENSSL_cleanse(cookie_secret_bytes, sizeof(cookie_secret_bytes)); + fprintf(stderr, + "[AUTH] Auth init failed — refusing to start server.\n"); + exit(1); + } + OPENSSL_cleanse(cookie_secret_bytes, sizeof(cookie_secret_bytes)); + } const char *sidecar_path = getenv("MRJUNEJUNE_INFERENCE_SIDECAR_PATH"); const char *copilot_cli_path = getenv("MRJUNEJUNE_COPILOT_CLI_PATH"); if (sidecar_path && copilot_cli_path) @@ -2085,6 +2585,8 @@ } Seobeo_Router_Init(); + Auth_API_Register_Routes(); + Admin_API_Register_Routes(); Conversation_API_Register_Routes(); Seobeo_Router_Register("GET", "/", GetHomePage); @@ -2137,6 +2639,9 @@ Seobeo_Router_Register("GET", "/jrpg", GetJrpg); Seobeo_Router_Register("GET", "/jrpg/index.html", GetRedirectJrpg); + // -- Login --/ + Seobeo_Router_Register("GET", "/login", GetLogin); + // -- Notes --/ Seobeo_Router_Register("GET", "/notes", GetNotes); Seobeo_Router_Register("GET", "/notes/", GetNotes); @@ -2152,8 +2657,19 @@ const char *server_port = getenv("MRJUNEJUNE_PORT"); if (!server_port || server_port[0] == '\0') server_port = "6969"; - Seobeo_Web_Server_Start("mrjunejune/src", server_port, SEOBEO_MODE_EDGE, 4); + const char *server_bind = g_server_host[0] ? g_server_host : "0.0.0.0"; + Mjj_Template_Renderer_Init("mrjunejune/src"); + int server_result = Seobeo_Web_Server_Start_On( + server_bind, "mrjunejune/src", server_port, SEOBEO_MODE_EDGE, 4); Seobeo_Worker_Pool_Destroy(g_media_worker_pool); g_media_worker_pool = NULL; Conversation_API_Destroy(); + Auth_API_Destroy(); + if (server_result != 0) + { + fprintf(stderr, "[STARTUP] Server bind/listen failed (host=%s port=%s)\n", + server_bind, server_port); + return 1; + } + return 0; }