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 }