Mercurial
comparison mrjunejune/main.c @ 200:90dfcef375fb
Added my own s3 bucket uploader url to mrjunejune.
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Sat, 14 Feb 2026 16:32:24 -0800 |
| parents | 295ac2e5ec00 |
| children | 6cdee35a7ba9 |
comparison
equal
deleted
inserted
replaced
| 199:b4a070994b54 | 200:90dfcef375fb |
|---|---|
| 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 <time.h> | 4 #include <time.h> |
| 4 | 5 |
| 5 // UUID + /tmp/ + format (max 4) | 6 // UUID + /tmp/ + format (max 4) |
| 6 #define TMP_FILE_LENGTH 47 | 7 #define TMP_FILE_LENGTH 47 |
| 7 #define UUID_LEN 37 | 8 #define UUID_LEN 37 |
| 8 | 9 |
| 9 volatile sig_atomic_t stop_server = 0; | 10 volatile sig_atomic_t stop_server = 0; |
| 10 static _Atomic uint32_t counter = 0; | 11 static _Atomic uint32_t counter = 0; |
| 12 | |
| 13 // Server configuration (loaded from .config) | |
| 14 static char g_upload_auth_token[256] = {0}; | |
| 15 static char g_s3_region[64] = "us-west-2"; | |
| 16 static char g_s3_bucket[128] = "mrjunejune"; | |
| 17 static int g_s3_url_expires = 3600; | |
| 18 static S3_Config g_s3_config = {0}; | |
| 19 | |
| 20 static void load_config(const char *config_path) | |
| 21 { | |
| 22 FILE *f = fopen(config_path, "r"); | |
| 23 if (!f) | |
| 24 { | |
| 25 printf("[CONFIG] Warning: Could not open %s, using defaults\n", config_path); | |
| 26 return; | |
| 27 } | |
| 28 | |
| 29 char line[512]; | |
| 30 while (fgets(line, sizeof(line), f)) | |
| 31 { | |
| 32 // Skip comments and empty lines | |
| 33 if (line[0] == '#' || line[0] == '\n' || line[0] == '\r') continue; | |
| 34 | |
| 35 char *eq = strchr(line, '='); | |
| 36 if (!eq) continue; | |
| 37 | |
| 38 *eq = '\0'; | |
| 39 char *key = line; | |
| 40 char *value = eq + 1; | |
| 41 | |
| 42 // Trim newline from value | |
| 43 size_t vlen = strlen(value); | |
| 44 while (vlen > 0 && (value[vlen-1] == '\n' || value[vlen-1] == '\r')) | |
| 45 value[--vlen] = '\0'; | |
| 46 | |
| 47 if (strcmp(key, "UPLOAD_AUTH_TOKEN") == 0) | |
| 48 { | |
| 49 strncpy(g_upload_auth_token, value, sizeof(g_upload_auth_token) - 1); | |
| 50 } | |
| 51 else if (strcmp(key, "S3_REGION") == 0) | |
| 52 { | |
| 53 strncpy(g_s3_region, value, sizeof(g_s3_region) - 1); | |
| 54 } | |
| 55 else if (strcmp(key, "S3_BUCKET") == 0) | |
| 56 { | |
| 57 strncpy(g_s3_bucket, value, sizeof(g_s3_bucket) - 1); | |
| 58 } | |
| 59 else if (strcmp(key, "S3_URL_EXPIRES") == 0) | |
| 60 { | |
| 61 g_s3_url_expires = atoi(value); | |
| 62 } | |
| 63 } | |
| 64 fclose(f); | |
| 65 | |
| 66 printf("[CONFIG] Loaded: token=%s..., region=%s, bucket=%s, expires=%d\n", | |
| 67 g_upload_auth_token[0] ? "***" : "(empty)", | |
| 68 g_s3_region, g_s3_bucket, g_s3_url_expires); | |
| 69 } | |
| 11 | 70 |
| 12 void handle_sigint(int sig) | 71 void handle_sigint(int sig) |
| 13 { | 72 { |
| 14 printf("Failed\n"); | 73 printf("Failed\n"); |
| 15 stop_server = 1; | 74 stop_server = 1; |
| 501 CREATE_REDIRECT_HANDLER(Tools, "/tools") | 560 CREATE_REDIRECT_HANDLER(Tools, "/tools") |
| 502 CREATE_REDIRECT_HANDLER(MarkDownToHtml, "/tools/markdown_to_html") | 561 CREATE_REDIRECT_HANDLER(MarkDownToHtml, "/tools/markdown_to_html") |
| 503 CREATE_REDIRECT_HANDLER(FileConverter, "/tools/file_converter") | 562 CREATE_REDIRECT_HANDLER(FileConverter, "/tools/file_converter") |
| 504 CREATE_REDIRECT_HANDLER(Talk, "/talk") | 563 CREATE_REDIRECT_HANDLER(Talk, "/talk") |
| 505 | 564 |
| 565 // S3 Upload URL API | |
| 566 // POST /api/s3/upload-url | |
| 567 // Headers: Authorization: Bearer <token>, Content-Type: application/json | |
| 568 // Body: {"filename": "photo.png", "content_type": "image/png"} | |
| 569 // Returns: {"upload_url": "https://...", "key": "uploads/..."} | |
| 570 Seobeo_Request_Entry *GetS3UploadUrl(Seobeo_Request_Entry *req, Dowa_Arena *arena) | |
| 571 { | |
| 572 Seobeo_Request_Entry *resp = NULL; | |
| 573 | |
| 574 // Check auth token | |
| 575 void *auth_kv = Dowa_HashMap_Get_Ptr(req, "Authorization"); | |
| 576 if (!auth_kv) | |
| 577 { | |
| 578 Dowa_HashMap_Push_Arena(resp, "status", "401", arena); | |
| 579 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); | |
| 580 Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Missing Authorization header\"}", arena); | |
| 581 return resp; | |
| 582 } | |
| 583 | |
| 584 const char *auth_header = ((Seobeo_Request_Entry*)auth_kv)->value; | |
| 585 | |
| 586 // Expect "Bearer <token>" | |
| 587 if (strncmp(auth_header, "Bearer ", 7) != 0) | |
| 588 { | |
| 589 Dowa_HashMap_Push_Arena(resp, "status", "401", arena); | |
| 590 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); | |
| 591 Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Invalid Authorization format, use Bearer token\"}", arena); | |
| 592 return resp; | |
| 593 } | |
| 594 | |
| 595 const char *token = auth_header + 7; | |
| 596 if (strlen(g_upload_auth_token) == 0 || strcmp(token, g_upload_auth_token) != 0) | |
| 597 { | |
| 598 Dowa_HashMap_Push_Arena(resp, "status", "403", arena); | |
| 599 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); | |
| 600 Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Invalid token\"}", arena); | |
| 601 return resp; | |
| 602 } | |
| 603 | |
| 604 // Parse request body for filename and content_type | |
| 605 void *body_kv = Dowa_HashMap_Get_Ptr(req, "Body"); | |
| 606 if (!body_kv) | |
| 607 { | |
| 608 Dowa_HashMap_Push_Arena(resp, "status", "400", arena); | |
| 609 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); | |
| 610 Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Missing request body\"}", arena); | |
| 611 return resp; | |
| 612 } | |
| 613 | |
| 614 const char *body = ((Seobeo_Request_Entry*)body_kv)->value; | |
| 615 | |
| 616 // Simple JSON parsing for filename and content_type | |
| 617 char filename[256] = {0}; | |
| 618 char content_type[128] = "application/octet-stream"; | |
| 619 | |
| 620 // Find "filename":"value" | |
| 621 const char *fn_key = strstr(body, "\"filename\""); | |
| 622 if (fn_key) | |
| 623 { | |
| 624 const char *fn_start = strchr(fn_key + 10, '"'); | |
| 625 if (fn_start) | |
| 626 { | |
| 627 fn_start++; | |
| 628 const char *fn_end = strchr(fn_start, '"'); | |
| 629 if (fn_end && (size_t)(fn_end - fn_start) < sizeof(filename)) | |
| 630 { | |
| 631 memcpy(filename, fn_start, fn_end - fn_start); | |
| 632 filename[fn_end - fn_start] = '\0'; | |
| 633 } | |
| 634 } | |
| 635 } | |
| 636 | |
| 637 // Find "content_type":"value" | |
| 638 const char *ct_key = strstr(body, "\"content_type\""); | |
| 639 if (ct_key) | |
| 640 { | |
| 641 const char *ct_start = strchr(ct_key + 14, '"'); | |
| 642 if (ct_start) | |
| 643 { | |
| 644 ct_start++; | |
| 645 const char *ct_end = strchr(ct_start, '"'); | |
| 646 if (ct_end && (size_t)(ct_end - ct_start) < sizeof(content_type)) | |
| 647 { | |
| 648 memcpy(content_type, ct_start, ct_end - ct_start); | |
| 649 content_type[ct_end - ct_start] = '\0'; | |
| 650 } | |
| 651 } | |
| 652 } | |
| 653 | |
| 654 if (strlen(filename) == 0) | |
| 655 { | |
| 656 Dowa_HashMap_Push_Arena(resp, "status", "400", arena); | |
| 657 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); | |
| 658 Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Missing filename in request body\"}", arena); | |
| 659 return resp; | |
| 660 } | |
| 661 | |
| 662 // Generate unique S3 key with timestamp | |
| 663 char s3_key[512]; | |
| 664 char *uuid = Dowa_Arena_Allocate(arena, UUID_LEN); | |
| 665 uint32 seed = (uint32)time(NULL) ^ (uint32)pthread_self() ^ counter++; | |
| 666 Dowa_String_UUID(seed, uuid); | |
| 667 snprintf(s3_key, sizeof(s3_key), "uploads/%s/%s", uuid, filename); | |
| 668 | |
| 669 // Generate presigned URL | |
| 670 S3_Presigned_URL presigned = S3_Presign_Put(&g_s3_config, s3_key, content_type, g_s3_url_expires); | |
| 671 | |
| 672 if (!presigned.success) | |
| 673 { | |
| 674 Dowa_HashMap_Push_Arena(resp, "status", "500", arena); | |
| 675 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); | |
| 676 char *error_body = Dowa_Arena_Allocate(arena, 256); | |
| 677 snprintf(error_body, 256, "{\"error\":\"Failed to generate upload URL: %s\"}", | |
| 678 presigned.error_message ? presigned.error_message : "unknown"); | |
| 679 Dowa_HashMap_Push_Arena(resp, "body", error_body, arena); | |
| 680 S3_Presigned_URL_Destroy(&presigned); | |
| 681 return resp; | |
| 682 } | |
| 683 | |
| 684 // Build response | |
| 685 char *response_body = Dowa_Arena_Allocate(arena, 2048 + strlen(presigned.url)); | |
| 686 snprintf(response_body, 2048 + strlen(presigned.url), | |
| 687 "{\"upload_url\":\"%s\",\"key\":\"%s\",\"expires\":%d}", | |
| 688 presigned.url, s3_key, g_s3_url_expires); | |
| 689 | |
| 690 S3_Presigned_URL_Destroy(&presigned); | |
| 691 | |
| 692 Dowa_HashMap_Push_Arena(resp, "status", "200", arena); | |
| 693 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); | |
| 694 Dowa_HashMap_Push_Arena(resp, "body", response_body, arena); | |
| 695 | |
| 696 printf("[S3] Generated upload URL for: %s\n", s3_key); | |
| 697 | |
| 698 return resp; | |
| 699 } | |
| 700 | |
| 506 int main(void) | 701 int main(void) |
| 507 { | 702 { |
| 703 // Load server config | |
| 704 load_config("mrjunejune/.config"); | |
| 705 | |
| 706 // Load S3 credentials from .env | |
| 707 FILE *env_file = fopen(".env", "r"); | |
| 708 static char s3_access_key[128] = {0}; | |
| 709 static char s3_secret_key[128] = {0}; | |
| 710 | |
| 711 if (env_file) | |
| 712 { | |
| 713 char line[512]; | |
| 714 while (fgets(line, sizeof(line), env_file)) | |
| 715 { | |
| 716 if (strncmp(line, "AWS_MRJUNEJUNE_ACCESS_KEY=", 26) == 0) | |
| 717 { | |
| 718 char *val = line + 26; | |
| 719 size_t len = strlen(val); | |
| 720 while (len > 0 && (val[len-1] == '\n' || val[len-1] == '\r')) val[--len] = '\0'; | |
| 721 strncpy(s3_access_key, val, sizeof(s3_access_key) - 1); | |
| 722 } | |
| 723 else if (strncmp(line, "AWS_MRJUNEJUNE_SECRET_ACCESS_KEY=", 33) == 0) | |
| 724 { | |
| 725 char *val = line + 33; | |
| 726 size_t len = strlen(val); | |
| 727 while (len > 0 && (val[len-1] == '\n' || val[len-1] == '\r')) val[--len] = '\0'; | |
| 728 strncpy(s3_secret_key, val, sizeof(s3_secret_key) - 1); | |
| 729 } | |
| 730 } | |
| 731 fclose(env_file); | |
| 732 } | |
| 733 | |
| 734 // Initialize S3 config | |
| 735 g_s3_config.access_key_id = s3_access_key; | |
| 736 g_s3_config.secret_access_key = s3_secret_key; | |
| 737 g_s3_config.region = g_s3_region; | |
| 738 g_s3_config.bucket = g_s3_bucket; | |
| 739 g_s3_config.endpoint = NULL; | |
| 740 g_s3_config.use_path_style = FALSE; | |
| 741 | |
| 742 printf("[S3] Configured: region=%s, bucket=%s, key=%s...\n", | |
| 743 g_s3_region, g_s3_bucket, s3_access_key[0] ? "***" : "(missing)"); | |
| 744 | |
| 508 Seobeo_Router_Init(); | 745 Seobeo_Router_Init(); |
| 509 | 746 |
| 510 Seobeo_Router_Register("GET", "/", GetHomePage); | 747 Seobeo_Router_Register("GET", "/", GetHomePage); |
| 511 Seobeo_Router_Register("GET", "/index.html", GetRedirectHomePage); | 748 Seobeo_Router_Register("GET", "/index.html", GetRedirectHomePage); |
| 512 | 749 |
| 525 // -- File converter --/ | 762 // -- File converter --/ |
| 526 Seobeo_Router_Register("POST", "/api/convert/image-to-webp", ConvertImageToWebP); | 763 Seobeo_Router_Register("POST", "/api/convert/image-to-webp", ConvertImageToWebP); |
| 527 Seobeo_Router_Register("POST", "/api/convert/video-to-mp4", ConvertVideoToMP4); | 764 Seobeo_Router_Register("POST", "/api/convert/video-to-mp4", ConvertVideoToMP4); |
| 528 Seobeo_Router_Register("GET", "/api/download/:filename", DownloadConvertedFile); | 765 Seobeo_Router_Register("GET", "/api/download/:filename", DownloadConvertedFile); |
| 529 | 766 |
| 767 // -- S3 Upload --/ | |
| 768 Seobeo_Router_Register("POST", "/api/s3/upload-url", GetS3UploadUrl); | |
| 769 | |
| 530 // -- Blog --/ | 770 // -- Blog --/ |
| 531 Seobeo_Router_Register("GET", "/blog", RenderBlogList); | 771 Seobeo_Router_Register("GET", "/blog", RenderBlogList); |
| 532 Seobeo_Router_Register("GET", "/blog/:blog_id", RenderBlog); | 772 Seobeo_Router_Register("GET", "/blog/:blog_id", RenderBlog); |
| 533 | 773 |
| 534 // -- Talk --/ | 774 // -- Talk --/ |