Mercurial
comparison mrjunejune/main.c @ 244:b8aa08503378
[tools] Add sandboxed online LaTeX editor
Co-authored-by: Copilot <[email protected]>
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Mon, 03 Aug 2026 16:56:25 -0700 |
| parents | 543df0fe7168 |
| children | b8b6e726964a |
comparison
equal
deleted
inserted
replaced
| 243:823f2a8b16c8 | 244:b8aa08503378 |
|---|---|
| 1 #include "seobeo/seobeo.h" | 1 #include "seobeo/seobeo.h" |
| 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 <time.h> | 6 #include <time.h> |
| 6 #include <sys/stat.h> | 7 #include <sys/stat.h> |
| 7 #include <stdarg.h> | 8 #include <stdarg.h> |
| 9 #include <stdatomic.h> | |
| 8 #include <pthread.h> | 10 #include <pthread.h> |
| 9 | 11 |
| 10 // UUID + /tmp/ + format (max 4) | 12 // UUID + /tmp/ + format (max 4) |
| 11 #define TMP_FILE_LENGTH 47 | 13 #define TMP_FILE_LENGTH 47 |
| 12 #define UUID_LEN 37 | 14 #define UUID_LEN 37 |
| 13 | 15 |
| 14 volatile sig_atomic_t stop_server = 0; | 16 volatile sig_atomic_t stop_server = 0; |
| 15 static _Atomic uint32_t counter = 0; | 17 static _Atomic uint32_t counter = 0; |
| 18 static _Atomic boolean g_latex_rendering = FALSE; | |
| 16 | 19 |
| 17 // Media Processing Context for background threads | 20 // Media Processing Context for background threads |
| 18 typedef struct { | 21 typedef struct { |
| 19 int64 media_id; | 22 int64 media_id; |
| 20 char s3_key_original[512]; | 23 char s3_key_original[512]; |
| 302 Seobeo_Render_Html_FilePath(final_body, "/tools/hls_player/index.html", arena); | 305 Seobeo_Render_Html_FilePath(final_body, "/tools/hls_player/index.html", arena); |
| 303 Dowa_HashMap_Push_Arena(resp, "body", final_body, arena); | 306 Dowa_HashMap_Push_Arena(resp, "body", final_body, arena); |
| 304 return resp; | 307 return resp; |
| 305 } | 308 } |
| 306 | 309 |
| 310 Seobeo_Request_Entry* GetLatexEditor(Seobeo_Request_Entry *req, Dowa_Arena *arena) | |
| 311 { | |
| 312 Seobeo_Request_Entry *resp = NULL; | |
| 313 char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024); | |
| 314 Seobeo_Render_Html_FilePath(final_body, "/tools/latex_editor/index.html", arena); | |
| 315 Dowa_HashMap_Push_Arena(resp, "body", final_body, arena); | |
| 316 return resp; | |
| 317 } | |
| 318 | |
| 319 static Seobeo_Request_Entry *LatexErrorResponse( | |
| 320 Dowa_Arena *arena, | |
| 321 int status, | |
| 322 const char *message) | |
| 323 { | |
| 324 Seobeo_Request_Entry *resp = NULL; | |
| 325 char *status_value = Dowa_Arena_Allocate(arena, 16); | |
| 326 snprintf(status_value, 16, "%d", status); | |
| 327 char *body = Dowa_Arena_Allocate(arena, strlen(message) + 1); | |
| 328 strcpy(body, message); | |
| 329 Dowa_HashMap_Push_Arena(resp, "status", status_value, arena); | |
| 330 Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain; charset=utf-8", arena); | |
| 331 Dowa_HashMap_Push_Arena(resp, "cache-control", "no-store", arena); | |
| 332 Dowa_HashMap_Push_Arena(resp, "body", body, arena); | |
| 333 return resp; | |
| 334 } | |
| 335 | |
| 336 Seobeo_Request_Entry *RenderLatexPdf(Seobeo_Request_Entry *req, Dowa_Arena *arena) | |
| 337 { | |
| 338 void *body_kv = Dowa_HashMap_Get_Ptr(req, "Body"); | |
| 339 void *length_kv = Dowa_HashMap_Get_Ptr(req, "Content-Length"); | |
| 340 if (!body_kv || !length_kv) | |
| 341 return LatexErrorResponse(arena, 400, "LaTeX source and Content-Length are required."); | |
| 342 | |
| 343 const char *length_value = ((Seobeo_Request_Entry *)length_kv)->value; | |
| 344 char *length_end = NULL; | |
| 345 errno = 0; | |
| 346 unsigned long long parsed_length = strtoull(length_value, &length_end, 10); | |
| 347 if (errno != 0 || | |
| 348 length_end == length_value || | |
| 349 *length_end != '\0' || | |
| 350 parsed_length == 0) | |
| 351 return LatexErrorResponse(arena, 400, "Content-Length is invalid."); | |
| 352 if (parsed_length > LATEX_SOURCE_MAX_BYTES) | |
| 353 return LatexErrorResponse(arena, 413, "LaTeX source exceeds the 64 KiB limit."); | |
| 354 | |
| 355 boolean expected = FALSE; | |
| 356 if (!atomic_compare_exchange_strong( | |
| 357 &g_latex_rendering, | |
| 358 &expected, | |
| 359 TRUE)) | |
| 360 return LatexErrorResponse(arena, 429, "The LaTeX compiler is busy. Try again shortly."); | |
| 361 | |
| 362 Latex_Render_Result render = Latex_Render( | |
| 363 (const uint8_t *)((Seobeo_Request_Entry *)body_kv)->value, | |
| 364 (size_t)parsed_length); | |
| 365 atomic_store(&g_latex_rendering, FALSE); | |
| 366 if (render.status != LATEX_RENDER_OK) | |
| 367 { | |
| 368 int status = 500; | |
| 369 if (render.status == LATEX_RENDER_INVALID_INPUT) | |
| 370 status = 400; | |
| 371 else if (render.status == LATEX_RENDER_COMPILE_ERROR) | |
| 372 status = 422; | |
| 373 else if (render.status == LATEX_RENDER_TIMEOUT) | |
| 374 status = 504; | |
| 375 else if (render.status == LATEX_RENDER_COMPILER_UNAVAILABLE) | |
| 376 status = 503; | |
| 377 | |
| 378 const char *diagnostics = render.diagnostics | |
| 379 ? render.diagnostics | |
| 380 : "LaTeX rendering failed."; | |
| 381 char *body = Dowa_Arena_Allocate(arena, strlen(diagnostics) + 1); | |
| 382 strcpy(body, diagnostics); | |
| 383 Latex_Render_Result_Destroy(&render); | |
| 384 return LatexErrorResponse(arena, status, body); | |
| 385 } | |
| 386 | |
| 387 char *pdf = Dowa_Arena_Allocate(arena, render.pdf_size + 1); | |
| 388 if (!pdf) | |
| 389 { | |
| 390 Latex_Render_Result_Destroy(&render); | |
| 391 return LatexErrorResponse(arena, 500, "The generated PDF is too large to return."); | |
| 392 } | |
| 393 memcpy(pdf, render.pdf_data, render.pdf_size); | |
| 394 pdf[render.pdf_size] = '\0'; | |
| 395 char *content_length = Dowa_Arena_Allocate(arena, 32); | |
| 396 snprintf(content_length, 32, "%zu", render.pdf_size); | |
| 397 Latex_Render_Result_Destroy(&render); | |
| 398 | |
| 399 Seobeo_Request_Entry *resp = NULL; | |
| 400 Dowa_HashMap_Push_Arena(resp, "status", "200", arena); | |
| 401 Dowa_HashMap_Push_Arena(resp, "content-type", "application/pdf", arena); | |
| 402 Dowa_HashMap_Push_Arena(resp, "content-length", content_length, arena); | |
| 403 Dowa_HashMap_Push_Arena(resp, "content-disposition", "inline; filename=\"document.pdf\"", arena); | |
| 404 Dowa_HashMap_Push_Arena(resp, "cache-control", "no-store", arena); | |
| 405 Dowa_HashMap_Push_Arena(resp, "x-content-type-options", "nosniff", arena); | |
| 406 Dowa_HashMap_Push_Arena(resp, "body", pdf, arena); | |
| 407 return resp; | |
| 408 } | |
| 409 | |
| 307 // Background thread function for media processing | 410 // Background thread function for media processing |
| 308 void *Simple_WebpConverter_Background(void *arg) | 411 void *Simple_WebpConverter_Background(void *arg) |
| 309 { | 412 { |
| 310 File_Converter_Config *configuration = (File_Converter_Config *)arg; | 413 File_Converter_Config *configuration = (File_Converter_Config *)arg; |
| 311 | 414 |
| 744 CREATE_REDIRECT_HANDLER(Resume, "/resume") | 847 CREATE_REDIRECT_HANDLER(Resume, "/resume") |
| 745 CREATE_REDIRECT_HANDLER(Tools, "/tools") | 848 CREATE_REDIRECT_HANDLER(Tools, "/tools") |
| 746 CREATE_REDIRECT_HANDLER(MarkDownToHtml, "/tools/markdown_to_html") | 849 CREATE_REDIRECT_HANDLER(MarkDownToHtml, "/tools/markdown_to_html") |
| 747 CREATE_REDIRECT_HANDLER(FileConverter, "/tools/file_converter") | 850 CREATE_REDIRECT_HANDLER(FileConverter, "/tools/file_converter") |
| 748 CREATE_REDIRECT_HANDLER(HlsPlayer, "/tools/hls_player") | 851 CREATE_REDIRECT_HANDLER(HlsPlayer, "/tools/hls_player") |
| 852 CREATE_REDIRECT_HANDLER(LatexEditor, "/tools/latex_editor") | |
| 749 CREATE_REDIRECT_HANDLER(Talk, "/talk") | 853 CREATE_REDIRECT_HANDLER(Talk, "/talk") |
| 750 CREATE_REDIRECT_HANDLER(Editor, "/editor") | 854 CREATE_REDIRECT_HANDLER(Editor, "/editor") |
| 751 | 855 |
| 752 // S3 Upload URL API | 856 // S3 Upload URL API |
| 753 // POST /api/s3/upload-url | 857 // POST /api/s3/upload-url |
| 1252 } | 1356 } |
| 1253 | 1357 |
| 1254 // Insert into database | 1358 // Insert into database |
| 1255 const char *insert_query = | 1359 const char *insert_query = |
| 1256 "INSERT INTO media_uploads (access_token, original_filename, content_type, s3_key_original, s3_key_processed, status) " | 1360 "INSERT INTO media_uploads (access_token, original_filename, content_type, s3_key_original, s3_key_processed, status) " |
| 1257 "VALUES (?, ?, ?, ?, ?, 'pending')"; | 1361 "VALUES (?, ?, ?, ?, ?, 'pending') RETURNING id"; |
| 1258 | 1362 |
| 1259 const char *params[] = { token, filename, content_type, s3_key_original, s3_key_processed }; | 1363 const char *params[] = { token, filename, content_type, s3_key_original, s3_key_processed }; |
| 1260 int32 result = Deita_Query_Execute_Update_Prepared(g_db_connection, insert_query, 5, params); | 1364 Deita_Result_Set *id_result = Deita_Query_Execute_Prepared( |
| 1261 | 1365 g_db_connection, |
| 1262 if (result < 0) | 1366 insert_query, |
| 1263 { | 1367 5, |
| 1368 params, | |
| 1369 arena); | |
| 1370 if (!id_result || !Deita_Result_Set_Next(id_result)) | |
| 1371 { | |
| 1372 if (id_result) Deita_Result_Set_Free(id_result); | |
| 1264 Dowa_HashMap_Push_Arena(resp, "status", "500", arena); | 1373 Dowa_HashMap_Push_Arena(resp, "status", "500", arena); |
| 1265 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); | 1374 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); |
| 1266 Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Failed to create media record\"}", arena); | 1375 Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Failed to create media record\"}", arena); |
| 1267 return resp; | 1376 return resp; |
| 1268 } | 1377 } |
| 1269 | 1378 |
| 1270 // Get the inserted media_id using last_insert_rowid() | 1379 int64 media_id = Deita_Result_Set_Get_Integer(id_result, 0); |
| 1271 const char *last_id_query = "SELECT last_insert_rowid()"; | 1380 Deita_Result_Set_Free(id_result); |
| 1272 Deita_Result_Set *id_result = Deita_Query_Execute(g_db_connection, last_id_query, arena); | |
| 1273 int64 media_id = 0; | |
| 1274 if (id_result && Deita_Result_Set_Next(id_result)) | |
| 1275 { | |
| 1276 media_id = Deita_Result_Set_Get_Integer(id_result, 0); | |
| 1277 } | |
| 1278 if (id_result) Deita_Result_Set_Free(id_result); | |
| 1279 | 1381 |
| 1280 // Generate presigned PUT URL | 1382 // Generate presigned PUT URL |
| 1281 S3_Presigned_URL presigned = S3_Presign_Put(&g_s3_config, s3_key_original, content_type, g_s3_url_expires); | 1383 S3_Presigned_URL presigned = S3_Presign_Put(&g_s3_config, s3_key_original, content_type, g_s3_url_expires); |
| 1282 | 1384 |
| 1283 if (!presigned.success) | 1385 if (!presigned.success) |
| 1866 | 1968 |
| 1867 Seobeo_Router_Register("GET", "/tools/file_converter", GetFileConverter); | 1969 Seobeo_Router_Register("GET", "/tools/file_converter", GetFileConverter); |
| 1868 Seobeo_Router_Register("GET", "/tools/file_converter/index.html", GetRedirectFileConverter); | 1970 Seobeo_Router_Register("GET", "/tools/file_converter/index.html", GetRedirectFileConverter); |
| 1869 Seobeo_Router_Register("GET", "/tools/hls_player", GetHlsPlayer); | 1971 Seobeo_Router_Register("GET", "/tools/hls_player", GetHlsPlayer); |
| 1870 Seobeo_Router_Register("GET", "/tools/hls_player/index.html", GetRedirectHlsPlayer); | 1972 Seobeo_Router_Register("GET", "/tools/hls_player/index.html", GetRedirectHlsPlayer); |
| 1973 Seobeo_Router_Register("GET", "/tools/latex_editor", GetLatexEditor); | |
| 1974 Seobeo_Router_Register("GET", "/tools/latex_editor/index.html", GetRedirectLatexEditor); | |
| 1871 | 1975 |
| 1872 // -- File converter --/ | 1976 // -- File converter --/ |
| 1873 Seobeo_Router_Register("POST", "/api/convert/image-to-webp", ConvertImageToWebP); | 1977 Seobeo_Router_Register("POST", "/api/convert/image-to-webp", ConvertImageToWebP); |
| 1874 Seobeo_Router_Register("POST", "/api/convert/video-to-mp4", ConvertVideoToMP4); | 1978 Seobeo_Router_Register("POST", "/api/convert/video-to-mp4", ConvertVideoToMP4); |
| 1875 Seobeo_Router_Register("GET", "/api/download/:filename", DownloadConvertedFile); | 1979 Seobeo_Router_Register("GET", "/api/download/:filename", DownloadConvertedFile); |
| 1980 Seobeo_Router_Register("POST", "/api/latex/render", RenderLatexPdf); | |
| 1876 | 1981 |
| 1877 // -- S3 Upload --/ | 1982 // -- S3 Upload --/ |
| 1878 Seobeo_Router_Register("POST", "/api/s3/upload-url", GetS3UploadUrl); | 1983 Seobeo_Router_Register("POST", "/api/s3/upload-url", GetS3UploadUrl); |
| 1879 | 1984 |
| 1880 // -- Media Upload --/ | 1985 // -- Media Upload --/ |
| 1907 | 2012 |
| 1908 Seobeo_Log(SEOBEO_INFO, "WTF is going on\n"); | 2013 Seobeo_Log(SEOBEO_INFO, "WTF is going on\n"); |
| 1909 const char *server_port = getenv("MRJUNEJUNE_PORT"); | 2014 const char *server_port = getenv("MRJUNEJUNE_PORT"); |
| 1910 if (!server_port || server_port[0] == '\0') | 2015 if (!server_port || server_port[0] == '\0') |
| 1911 server_port = "6969"; | 2016 server_port = "6969"; |
| 1912 Seobeo_Web_Server_Start("mrjunejune/src", server_port, SEOBEO_MODE_EDGE, 1); | 2017 Seobeo_Web_Server_Start("mrjunejune/src", server_port, SEOBEO_MODE_EDGE, 4); |
| 1913 } | 2018 } |