Mercurial
changeset 67:6626ec933933
[Seobeo] Separated out Client Server logic. Created test tools.
| author | June Park <parkjune1995@gmail.com> |
|---|---|
| date | Wed, 24 Dec 2025 09:15:55 -0800 |
| parents | a0f0ad5e42eb |
| children | 70ca1d99f3fd |
| files | mrjunejune/BUILD mrjunejune/test/create_snapshots.c mrjunejune/test/integration_test.c mrjunejune/test/snapshots/README.md mrjunejune/test/snapshots/does_not_exist.snapshot mrjunejune/test/snapshots/index.html.snapshot mrjunejune/test/snapshots/missing.html.snapshot mrjunejune/test/snapshots/nonexistent.snapshot mrjunejune/test/snapshots/root.snapshot seobeo/BUILD seobeo/s_linux_network.c seobeo/s_ssl.c seobeo/s_web.c seobeo/seobeo_http_client_test.c seobeo/seobeo_internal.h seobeo/snapshot_creator.c seobeo/snapshot_creator.h |
| diffstat | 17 files changed, 2127 insertions(+), 84 deletions(-) [+] |
line wrap: on
line diff
--- a/mrjunejune/BUILD Wed Dec 24 06:34:19 2025 -0800 +++ b/mrjunejune/BUILD Wed Dec 24 09:15:55 2025 -0800 @@ -20,7 +20,7 @@ cc_binary( name = "mrjunejune_server", srcs = ["main.c"], - deps = ["//seobeo:seobeo"], + deps = ["//seobeo:seobeo_server"], # Use server-only target (no OpenSSL) data = [":pages_files"], ) @@ -32,7 +32,7 @@ cc_library( name = "mrjunejune_server_lib", srcs = ["server_entry.c"], - deps = ["//seobeo:seobeo"], + deps = ["//seobeo:seobeo_server"], # Use server-only target (no OpenSSL) linkstatic = False, # ensures dynamic linking visibility = ["//visibility:public"], ) @@ -47,3 +47,32 @@ data = [":mrjunejune_server_lib"], ) +cc_test( + name = "integration_test", + srcs = ["test/integration_test.c"], + deps = ["//seobeo:seobeo_client"], + data = [ + "//mrjunejune:mrjunejune_server", + "//mrjunejune:pages_files", + "//mrjunejune:test_snapshots", + ], + size = "medium", + timeout = "moderate", + args = ["$(location //mrjunejune:mrjunejune_server)"], +) + +cc_binary( + name = "create_snapshots", + srcs = ["test/create_snapshots.c"], + deps = ["//seobeo:seobeo_client"], + data = [ + "//mrjunejune:mrjunejune_server", + "//mrjunejune:pages_files", + ], + args = ["$(location //mrjunejune:mrjunejune_server)"], +) + +filegroup( + name = "test_snapshots", + srcs = glob(["test/snapshots/**"]), +)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mrjunejune/test/create_snapshots.c Wed Dec 24 09:15:55 2025 -0800 @@ -0,0 +1,173 @@ +#include "seobeo/seobeo.h" +#include "seobeo/snapshot_creator.h" +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> +#include <sys/wait.h> +#include <signal.h> + +#define TEST_PORT "6969" +#define TEST_HOST "127.0.0.1" +#define SNAPSHOT_DIR "mrjunejune/test/snapshots" + +// Start the server process +static pid_t start_server(const char *server_binary) +{ + pid_t server_pid = fork(); + + if (server_pid < 0) + { + perror("fork"); + return -1; + } + + if (server_pid == 0) + { + // Child process - run server + printf("Starting server on port %s...\n", TEST_PORT); + execl(server_binary, server_binary, NULL); + perror("execl failed"); + exit(1); + } + + // Parent - verify server started + printf("Server started (PID: %d)\n", server_pid); + + usleep(100000); + int status; + pid_t result = waitpid(server_pid, &status, WNOHANG); + if (result != 0) + { + if (WIFEXITED(status)) + { + fprintf(stderr, "Server exited with code: %d\n", WEXITSTATUS(status)); + } + else if (WIFSIGNALED(status)) + { + fprintf(stderr, "Server killed by signal: %d\n", WTERMSIG(status)); + } + return -1; + } + + sleep(2); + printf("Server ready\n\n"); + + return server_pid; +} + +// Stop the server process +static void stop_server_test(pid_t server_pid) +{ + if (server_pid > 0) + { + printf("\nStopping server (PID: %d)...\n", server_pid); + kill(server_pid, SIGTERM); + waitpid(server_pid, NULL, 0); + printf("Server stopped\n"); + } +} + +int main(int argc, char *argv[]) +{ + printf("=== Seobeo Snapshot Creator ===\n\n"); + + // Get workspace directory (where the source files are) + const char *workspace_dir = getenv("BUILD_WORKSPACE_DIRECTORY"); + if (!workspace_dir) + { + fprintf(stderr, "Error: BUILD_WORKSPACE_DIRECTORY not set\n"); + fprintf(stderr, "This binary must be run with 'bazel run'\n"); + return 1; + } + + // Construct full path to snapshot directory + char snapshot_path[1024]; + snprintf(snapshot_path, sizeof(snapshot_path), "%s/%s", workspace_dir, SNAPSHOT_DIR); + + // Get server binary path + const char *server_binary = "./mrjunejune_server"; + if (argc > 1) + { + server_binary = argv[1]; + } + + printf("Workspace: %s\n", workspace_dir); + printf("Server binary: %s\n", server_binary); + printf("Snapshot directory: %s\n\n", snapshot_path); + + // Start server + pid_t server_pid = start_server(server_binary); + if (server_pid < 0) + { + fprintf(stderr, "Failed to start server\n"); + return 1; + } + + // Define snapshots to create - paths that should succeed (200 OK) + SnapshotConfig success_snapshots[] = { + {"/", 200, snapshot_path, TEST_HOST, TEST_PORT}, + {"/index.html", 200, snapshot_path, TEST_HOST, TEST_PORT}, + }; + int num_success = sizeof(success_snapshots) / sizeof(success_snapshots[0]); + + // Define snapshots for error paths (404) + SnapshotConfig error_snapshots[] = { + {"/nonexistent", 404, snapshot_path, TEST_HOST, TEST_PORT}, + {"/does/not/exist", 404, snapshot_path, TEST_HOST, TEST_PORT}, + {"/missing.html", 404, snapshot_path, TEST_HOST, TEST_PORT}, + }; + int num_errors = sizeof(error_snapshots) / sizeof(error_snapshots[0]); + + int total_failed = 0; + int total_passed = 0; + + // Create success snapshots + printf("Creating snapshots for successful paths:\n\n"); + for (int i = 0; i < num_success; i++) + { + if (Seobeo_Snapshot_Create(&success_snapshots[i]) == 0) + { + total_passed++; + } + else + { + total_failed++; + } + } + + // Create error snapshots + printf("Creating snapshots for error paths:\n\n"); + for (int i = 0; i < num_errors; i++) + { + if (Seobeo_Snapshot_Create(&error_snapshots[i]) == 0) + { + total_passed++; + } + else + { + total_failed++; + } + } + + // Stop server + stop_server_test(server_pid); + + // Print summary + printf("\n=== Summary ===\n"); + printf("Snapshots created: %d\n", total_passed); + printf("Failed: %d\n", total_failed); + + if (total_failed == 0) + { + printf("\n✓ All snapshots created successfully!\n"); + printf("\nSnapshots saved to: %s/\n", snapshot_path); + printf("\nRun tests to verify:\n"); + printf(" bazel test //mrjunejune:integration_test\n"); + return 0; + } + else + { + printf("\n✗ Some snapshots failed\n"); + return 1; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mrjunejune/test/integration_test.c Wed Dec 24 09:15:55 2025 -0800 @@ -0,0 +1,616 @@ +#include "seobeo/seobeo.h" +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <sys/wait.h> +#include <signal.h> +#include <assert.h> + +#define TEST_PORT "6969" +#define TEST_HOST "127.0.0.1" +#define MAX_RESPONSE_SIZE (1024 * 1024) +#define SNAPSHOT_DIR "mrjunejune/test/snapshots" + +// Test case structure +typedef struct { + const char *path; + int expected_status; + const char *expected_content; // Loaded from file + char *expected_file_path; // Path to expected snapshot file + char *actual_response; + size_t response_len; +} TestCase; + +// Helper: Convert URL path to filename +// "/" -> "root.snapshot" +// "/index.html" -> "index.html.snapshot" +// "/api/users" -> "api_users.snapshot" +void path_to_filename(const char *path, char *filename, size_t max_len) +{ + if (strcmp(path, "/") == 0) + { + snprintf(filename, max_len, "root.snapshot"); + return; + } + + // Remove leading slash and convert remaining slashes to underscores + const char *p = path; + if (*p == '/') + { + p++; + } + + char *out = filename; + size_t remaining = max_len - 1; + + while (*p && remaining > 0) + { + if (*p == '/') + { + *out++ = '_'; + remaining--; + } + else + { + *out++ = *p; + remaining--; + } + p++; + } + + // Add .snapshot extension + snprintf(out, remaining, ".snapshot"); +} + +// Helper: Read file contents into buffer +char* read_file(const char *filepath, size_t *size_out) +{ + FILE *f = fopen(filepath, "rb"); + if (!f) + { + return NULL; + } + + fseek(f, 0, SEEK_END); + long fsize = ftell(f); + fseek(f, 0, SEEK_SET); + + char *buffer = malloc(fsize + 1); + if (!buffer) + { + fclose(f); + return NULL; + } + + size_t read_size = fread(buffer, 1, fsize, f); + buffer[read_size] = '\0'; + fclose(f); + + if (size_out) + { + *size_out = read_size; + } + + return buffer; +} + +// Helper: Load expected content for a test case +int load_expected_content(TestCase *test) +{ + if (!test->expected_file_path) + { + return -1; + } + + size_t size; + test->expected_content = read_file(test->expected_file_path, &size); + + if (!test->expected_content) + { + return -1; + } + + return 0; +} + +// Helper: Create test client +Seobeo_Handle* create_test_client() +{ + Seobeo_Handle *client = Seobeo_Stream_Handle_Client_Create(TEST_HOST, TEST_PORT, FALSE); + if (!client || client->socket < 0) + { + if (client) + { + Seobeo_Handle_Destroy(client); + } + return NULL; + } + return client; +} + +// Helper: Generate default HTTP GET request +int generate_http_get_request(char *buffer, size_t buffer_size, const char *path) +{ + return snprintf( + buffer, buffer_size, + "GET %s HTTP/1.1\r\n" + "Host: %s\r\n" + "Connection: close\r\n" + "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" + "User-Agent: SeobeoTestClient/1.0\r\n" + "\r\n", + path, TEST_HOST + ); +} + +// Helper: Send HTTP request +int send_http_request(Seobeo_Handle *client, const char *path, const char *custom_request) +{ + char request_buffer[4096]; + int request_len; + + if (custom_request) + { + request_len = snprintf(request_buffer, sizeof(request_buffer), "%s", custom_request); + } + else + { + request_len = generate_http_get_request(request_buffer, sizeof(request_buffer), path); + } + + if (request_len < 0 || request_len >= sizeof(request_buffer)) + { + fprintf(stderr, "Request buffer too small\n"); + return -1; + } + + Seobeo_Handle_Queue(client, (uint8*)request_buffer, (uint32)request_len); + return Seobeo_Handle_Flush(client); +} + +// Helper: Read HTTP response +int read_http_response(Seobeo_Handle *client, char **response_out, size_t *response_len_out) +{ + char *response = malloc(MAX_RESPONSE_SIZE); + if (!response) + { + return -1; + } + + size_t total_bytes = 0; + int attempts = 0; + const int max_attempts = 100; + + while (attempts++ < max_attempts && total_bytes < MAX_RESPONSE_SIZE - 1) + { + int bytes_read = Seobeo_Handle_Read(client); + + if (bytes_read > 0) + { + size_t to_copy = client->read_buffer_len; + if (total_bytes + to_copy > MAX_RESPONSE_SIZE - 1) + { + to_copy = MAX_RESPONSE_SIZE - 1 - total_bytes; + } + + memcpy(response + total_bytes, client->read_buffer, to_copy); + total_bytes += to_copy; + Seobeo_Handle_Consume(client, (uint32)to_copy); + } + else if (bytes_read == -2) + { + // Connection closed + break; + } + else if (bytes_read == 0) + { + // Would block + usleep(10000); + continue; + } + else + { + free(response); + return -1; + } + } + + response[total_bytes] = '\0'; + *response_out = response; + *response_len_out = total_bytes; + + return (total_bytes > 0) ? 0 : -1; +} + +// Helper: Parse HTTP status code +int parse_http_status(const char *response) +{ + if (!response || strlen(response) < 12) + { + return -1; + } + + const char *status_start = strstr(response, "HTTP/1.1 "); + if (!status_start) + { + status_start = strstr(response, "HTTP/1.0 "); + } + + if (!status_start) + { + return -1; + } + + int status_code; + if (sscanf(status_start + 9, "%d", &status_code) == 1) + { + return status_code; + } + + return -1; +} + +// Helper: Check if status is a redirect +int is_redirect_status(int status) +{ + return (status >= 300 && status < 400); +} + +// Helper: Execute a test case +int execute_test_case(TestCase *test, pid_t server_pid) +{ + printf(" Testing: GET %s (expecting %d)\n", test->path, test->expected_status); + + Seobeo_Handle *client = create_test_client(); + if (!client) + { + printf(" ✗ Failed to create client connection\n"); + return -1; + } + + if (send_http_request(client, test->path, NULL) < 0) + { + printf(" ✗ Failed to send request\n"); + Seobeo_Handle_Destroy(client); + return -1; + } + + char *response = NULL; + size_t response_len = 0; + if (read_http_response(client, &response, &response_len) < 0) + { + printf(" ✗ Failed to read response\n"); + Seobeo_Handle_Destroy(client); + return -1; + } + + test->actual_response = response; + test->response_len = response_len; + + int actual_status = parse_http_status(response); + if (actual_status != test->expected_status) + { + printf(" ✗ Status mismatch: expected %d, got %d\n", + test->expected_status, actual_status); + Seobeo_Handle_Destroy(client); + return -1; + } + + printf(" ✓ Status code: %d\n", actual_status); + + // For redirects, skip content comparison + if (is_redirect_status(actual_status)) + { + printf(" ⚠ Redirect status - skipping content comparison\n"); + Seobeo_Handle_Destroy(client); + return 0; + } + + // Only verify 200 OK responses against snapshots + if (actual_status == 200) + { + if (!test->expected_content) + { + printf(" ✗ No expected snapshot found: %s\n", test->expected_file_path); + printf(" → Run: bazel run //mrjunejune:create_snapshots\n"); + Seobeo_Handle_Destroy(client); + return -1; + } + + if (strcmp(response, test->expected_content) != 0) + { + printf(" ✗ Response does not match expected snapshot\n"); + printf(" Expected file: %s\n", test->expected_file_path); + Seobeo_Handle_Destroy(client); + return -1; + } + + printf(" ✓ Response matches snapshot (%zu bytes)\n", response_len); + } + + Seobeo_Handle_Destroy(client); + return 0; +} + +// Helper: Execute custom request test +int execute_custom_request_test(const char *name, const char *custom_request, + int expected_status, pid_t server_pid) +{ + printf(" Testing: %s (expecting %d)\n", name, expected_status); + + Seobeo_Handle *client = create_test_client(); + if (!client) + { + printf(" ✗ Failed to create client connection\n"); + return -1; + } + + if (send_http_request(client, NULL, custom_request) < 0) + { + printf(" ✗ Failed to send request\n"); + Seobeo_Handle_Destroy(client); + return -1; + } + + char *response = NULL; + size_t response_len = 0; + if (read_http_response(client, &response, &response_len) < 0) + { + printf(" ✗ Failed to read response\n"); + Seobeo_Handle_Destroy(client); + return -1; + } + + int actual_status = parse_http_status(response); + if (actual_status != expected_status) + { + printf(" ✗ Status mismatch: expected %d, got %d\n", + expected_status, actual_status); + free(response); + Seobeo_Handle_Destroy(client); + return -1; + } + + printf(" ✓ Status code: %d\n", actual_status); + printf(" ✓ Response received (%zu bytes)\n", response_len); + + free(response); + Seobeo_Handle_Destroy(client); + return 0; +} + +// Helper: Start test server +pid_t start_test_server(const char *server_binary) +{ + pid_t server_pid = fork(); + + if (server_pid < 0) + { + perror("fork"); + return -1; + } + + if (server_pid == 0) + { + printf("Starting server on port %s...\n", TEST_PORT); + execl(server_binary, server_binary, NULL); + perror("execl failed"); + exit(1); + } + + printf("Server started (PID: %d)\n", server_pid); + + usleep(100000); + int status; + pid_t result = waitpid(server_pid, &status, WNOHANG); + if (result != 0) + { + if (WIFEXITED(status)) + { + fprintf(stderr, "Server exited immediately with code: %d\n", WEXITSTATUS(status)); + } + else if (WIFSIGNALED(status)) + { + fprintf(stderr, "Server was killed by signal: %d\n", WTERMSIG(status)); + } + return -1; + } + + sleep(2); + printf("Server ready\n\n"); + + return server_pid; +} + +// Helper: Stop test server +void stop_test_server(pid_t server_pid) +{ + if (server_pid > 0) + { + printf("\nStopping server (PID: %d)...\n", server_pid); + kill(server_pid, SIGTERM); + waitpid(server_pid, NULL, 0); + printf("Server stopped\n"); + } +} + +// Helper: Initialize test case with snapshot file +void init_test_case(TestCase *test) +{ + char filename[256]; + path_to_filename(test->path, filename, sizeof(filename)); + + test->expected_file_path = malloc(512); + snprintf(test->expected_file_path, 512, "%s/%s", SNAPSHOT_DIR, filename); + + // Load expected content from snapshot file + load_expected_content(test); +} + +// Helper: Cleanup test case +void cleanup_test_case(TestCase *test) +{ + if (test->actual_response) + { + free(test->actual_response); + } + if (test->expected_file_path) + { + free(test->expected_file_path); + } + if (test->expected_content) + { + free((void*)test->expected_content); + } +} + +// Main integration test +int test_server_client_integration(const char *server_binary) +{ + printf("=== Server-Client Integration Test ===\n"); + printf("MODE: Verifying Against Snapshots\n\n"); + + char cwd[1024]; + if (getcwd(cwd, sizeof(cwd)) != NULL) + { + printf("Working directory: %s\n", cwd); + } + + if (access(server_binary, X_OK) != 0) + { + printf("Server binary not found: %s\n", server_binary); + perror("access"); + return -1; + } + printf("Server binary: %s\n", server_binary); + printf("Snapshot directory: %s\n\n", SNAPSHOT_DIR); + + pid_t server_pid = start_test_server(server_binary); + if (server_pid < 0) + { + return -1; + } + + int failed_tests = 0; + int passed_tests = 0; + + // Define test cases - paths that should succeed (200 OK) + TestCase success_tests[] = { + {"/", 200, NULL, NULL, NULL, 0}, + {"/index.html", 200, NULL, NULL, NULL, 0}, + }; + int num_success_tests = sizeof(success_tests) / sizeof(success_tests[0]); + + // Define test cases - paths that should fail (404) + TestCase failure_tests[] = { + {"/nonexistent", 404, NULL, NULL, NULL, 0}, + {"/does/not/exist", 404, NULL, NULL, NULL, 0}, + {"/missing.html", 404, NULL, NULL, NULL, 0}, + }; + int num_failure_tests = sizeof(failure_tests) / sizeof(failure_tests[0]); + + // Initialize all test cases + for (int i = 0; i < num_success_tests; i++) + { + init_test_case(&success_tests[i]); + } + for (int i = 0; i < num_failure_tests; i++) + { + init_test_case(&failure_tests[i]); + } + + // Run success tests + printf("Running tests for paths that should succeed:\n"); + for (int i = 0; i < num_success_tests; i++) + { + if (execute_test_case(&success_tests[i], server_pid) == 0) + { + passed_tests++; + } + else + { + failed_tests++; + } + } + + printf("\n"); + + // Run failure tests + printf("Running tests for paths that should fail:\n"); + for (int i = 0; i < num_failure_tests; i++) + { + if (execute_test_case(&failure_tests[i], server_pid) == 0) + { + passed_tests++; + } + else + { + failed_tests++; + } + } + + printf("\n"); + + // Test with custom request + printf("Running tests with custom requests:\n"); + char custom_request[4096]; + snprintf(custom_request, sizeof(custom_request), + "GET / HTTP/1.1\r\n" + "Host: %s\r\n" + "Connection: close\r\n" + "X-Custom-Header: TestValue\r\n" + "\r\n", + TEST_HOST); + + if (execute_custom_request_test("Custom headers GET /", custom_request, 200, server_pid) == 0) + { + passed_tests++; + } + else + { + failed_tests++; + } + + // Cleanup test cases + for (int i = 0; i < num_success_tests; i++) + { + cleanup_test_case(&success_tests[i]); + } + for (int i = 0; i < num_failure_tests; i++) + { + cleanup_test_case(&failure_tests[i]); + } + + stop_test_server(server_pid); + + printf("\n=== Test Summary ===\n"); + printf("Passed: %d\n", passed_tests); + printf("Failed: %d\n", failed_tests); + + return (failed_tests == 0) ? 0 : -1; +} + +int main(int argc, char *argv[]) +{ + printf("=== Seobeo Integration Tests ===\n\n"); + + const char *server_binary = "./mrjunejune_server"; + if (argc > 1) + { + server_binary = argv[1]; + } + + int result = test_server_client_integration(server_binary); + + if (result == 0) + { + printf("\n✓ All tests passed!\n"); + } + else + { + printf("\n✗ Some tests failed\n"); + } + + return result; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mrjunejune/test/snapshots/README.md Wed Dec 24 09:15:55 2025 -0800 @@ -0,0 +1,76 @@ +# Test Snapshots + +This directory contains expected HTTP response snapshots for integration tests. + +## How It Works + +Each test case generates a snapshot file based on its URL path: + +- `/` → `root.snapshot` +- `/index.html` → `index.html.snapshot` +- `/api/users` → `api_users.snapshot` +- `/path/to/resource` → `path_to_resource.snapshot` + +## Workflow + +### First time (no snapshots exist): + +1. Create snapshots: +```bash +bazel run //mrjunejune:create_snapshots +``` + +This will: +- Start the test server +- Make HTTP requests to all test paths +- Save responses to `mrjunejune/test/snapshots/` + +2. Verify tests pass: +```bash +bazel test //mrjunejune:integration_test +``` + +All tests should now pass since snapshots exist. + +### Updating Snapshots + +When you change the server response format: + +1. Regenerate snapshots: +```bash +bazel run //mrjunejune:create_snapshots +``` + +2. Verify tests still pass: +```bash +bazel test //mrjunejune:integration_test +``` + +3. Commit the updated snapshot files + +## Two Separate Commands + +- **`bazel run //mrjunejune:create_snapshots`** - Creates/updates snapshot files (uses seobeo snapshot creator library) +- **`bazel test //mrjunejune:integration_test`** - Verifies responses match snapshots + +This separation makes it clear when you're creating new snapshots vs. verifying against existing ones. + +## Implementation Details + +The snapshot creation is implemented in the seobeo library: +- `seobeo/snapshot_creator.h` - Snapshot creation API +- `seobeo/snapshot_creator.c` - Implementation +- `mrjunejune/test/create_snapshots.c` - Binary that uses the library + +This makes snapshot creation reusable across different projects. + +## Special Cases + +### 200 OK Responses +Only 200 OK responses are verified against snapshots. This ensures successful responses remain consistent. + +### Redirects (3xx status codes) +Redirect responses skip content comparison - only the status code is verified. + +### Error Responses (4xx, 5xx) +Error status codes are verified but content comparison is skipped to allow for response format flexibility.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mrjunejune/test/snapshots/does_not_exist.snapshot Wed Dec 24 09:15:55 2025 -0800 @@ -0,0 +1,5 @@ +HTTP/1.1 404 Not Found +Content-Type: text/html +Content-Length: 0 +Connection: close +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mrjunejune/test/snapshots/index.html.snapshot Wed Dec 24 09:15:55 2025 -0800 @@ -0,0 +1,245 @@ +HTTP/1.1 200 OK +Content-Type: text/html; charset=utf-8 +Content-Length: 12120 +Connection: close + +<!doctype html> +<html lang="en"> + <head> + <BaseHead title="Resume" description="June's resume" /> + <link rel="stylesheet" href="base.css" /> + <link rel="stylesheet" href="resume.css" /> + </head> + <body> + <main> + <div> + <p>Hi, my name is Juntae, but most people call me <b>June</b>.</p> + <p>I am a software engineer with experience spanning a wide range of companies, from small startups to FAANG. </p> + <p>Feel free to check out my <a href="/resume.pdf"> resume </a> below, and if you're interested, don’t hesitate to contact me for contract work ranging from web/app development to embedded programming.</p> + </div> + <div class="info"> + <p><span class="header-firstname-style"> JUNTAE </span><span class="header-lastname-style">PARK</span><p> + <p class="header-position-style"> FULL STACK DEVELOPER · SOFTWARE ENGINEER </p> + <div class="header-address-style"> + Bay Area, CA, USA + </div> + <div class="header-social-style"> + <p>📱(US) 650-531-1728 |📱(CA) 437-580-8026 | <a href="mailto:[email protected]"> ✉️ [email protected] </a>| <a href="https://github.com/mrjunejune"> + <svg viewBox="0 0 16 16" aria-hidden="true" width="12" height="12"> + <path + fill="currentColor" + d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z" + ></path> + </svg> mrjunejune + </a>| <a href="https://www.linkedin.com/in/junepark"> + <svg width="12" height="12" fill="currentColor" class="bi bi-linkedin" viewBox="0 0 16 16"> + <path d="M0 1.146C0 .513.526 0 1.175 0h13.65C15.474 0 16 .513 16 1.146v13.708c0 .633-.526 1.146-1.175 1.146H1.175C.526 16 0 15.487 0 14.854zm4.943 12.248V6.169H2.542v7.225zm-1.2-8.212c.837 0 1.358-.554 1.358-1.248-.015-.709-.52-1.248-1.342-1.248S2.4 3.226 2.4 3.934c0 .694.521 1.248 1.327 1.248zm4.908 8.212V9.359c0-.216.016-.432.08-.586.173-.431.568-.878 1.232-.878.869 0 1.216.662 1.216 1.634v3.865h2.401V9.25c0-2.22-1.184-3.252-2.764-3.252-1.274 0-1.845.7-2.165 1.193v.025h-.016l.016-.025V6.169h-2.4c.03.678 0 7.225 0 7.225z"/> + </svg> + junepark + </a> + </p> + </div> + </div> + + <div class="sub-header"> + <h2 class="section-style"> Summary </h2> + <div class="line"></div> + </div> + <p class="paragraph-style">Software Engineer with 8 years of hands-on experience across diverse tech stacks, from early-stage startups to FANG-scale systems. Adept in designing and delivering robust software solutions using modern languages, frameworks, and cloud platforms. Open to impactful work.</p> + <div class="sub-header"> + + <h2 class="section-style"> Skills </h2> + <div class="line"></div> + </div> + <div class="paragraph-style"> + <p> + <span class="entry-title-style"> Programming Languages:</span> + <span class="skill-type-style"> TypeScript, Python, C++/C, Ruby, Java, MATLAB </span> + </p> + <p> + <span class="entry-title-style">Tools & Platforms:</span> + <span class="skill-type-style">Bazel, PostgresSQL, Mercurial, Git, Pands, Raylib, XCode</span> + </p> + <p> + <span class="entry-title-style"> Web Frameworks: </span> + <span class="skill-type-style"> Django, Rails, React, Flask</span> + </p> + <p> + <span class="entry-title-style"> DevOp:</span> + <span class="skill-type-style"> Plummi, Heroku, DigitalOcean, AWS, Google Cloud </span> + </p> + <p> + <span class="entry-title-style"> Language:</span> + <span class="skill-type-style"> English, Korean, Japanese </span> + </p> + </div> + + <!-- Experiences --> + <div class="sub-header"> + <h2 class="section-style"> Experience </h2> + <div class="line"></div> + </div> + <div class="flex-box"> + <p class="entry-title-style"> + <a href="https://www.meta.com/">Meta</a> + </p> + <p class="entry-location-style">San Francisco, CA, USA</p> + </div> + <div class="flex-box"> + <p class="entry-position-style">SOFTWARE ENGINEER</p> + <p class="entry-date-style">Oct, 2024 - Present</p> + </div> + <ul class="description-style"> + <li> + Took initiative on Channel Value Rule, targeting the 16% of ad traffic with both app and web destinations to improve value attribution and ROI. + </li> + <li> + Built full-stack features using React and Hack/GraphQL, contributing to scalable, production-ready systems. + </li> + <li> + Partnered with data science to design A/B tests and analyze revenue impact of ads destination. + </li> + <li> + Proposed and implemented alpha improvements to internal testing infrastructure, reducing test time by 50% and enhancing developer velocity. + </li> + </ul> + <div class="flex-box"> + <p class="entry-title-style"> + <a href="https://www.wmg.com/">Warner Music Group</a> + </p> + <p class="entry-location-style">Toronto, ON, Canada</p> + </div> + <div class="flex-box"> + <p class="entry-position-style">TECHNICAL LEAD ENGINEER</p> + <p class="entry-date-style">July, 2023 - Sept, 2024</p> + </div> + <ul class="description-style"> + <li> + Implements <a href="https://bazel.build/">bazel </a>structure for the company for TypeScript and JavaScript code base for hermiticity and stablishing standards for JavaScript and + </li> + <li> + TypeScript testing and code structures. + </li> + <li> + Led a team of five engineers in building GraphQL endpoints for client-facing applications using Apollo and AppSync, supporting over 2000 RPS and auto scaling depending on request values. + </li> + <li> + Improved application response times by up to 85% for graphQL response by updating database schema and SQL queries, eliminating N+1 queries and lack of indexes. + </li> + <li> + Developed CI/CD pipelines for backend structures. + </li> + <li> + Designed infrastructure for pub/sub, caching, and media processing logic. + </li> + </ul> + + <div class="flex-box"> + <p class="entry-title-style"> + <a href="https://www.google.com/">Google</a> + </p> + <p class="entry-location-style">Toronto, ON, Canada</p> + </div> + <div class="flex-box"> + <p class="entry-position-style">SOFTWARE ENGINEER</p> + <p class="entry-date-style">Feb, 2022 - July 2023</p> + </div> + <ul class="description-style"> + <li> + Implements and maintained new features relating to App Script across google workspace platform including Gmail, sheets, and Docs.</li> + <li> + Improved a response time and render time of App Script hover card components.</li> + <li> + Collaborated with a team of developers to ensure timely and accurate delivery of features.</li> + <li> + Conducted user testing and gathered feedback to iterate on features for optimal user experience.</li> + </ul> + + <div class="flex-box"> + <p class="entry-title-style"> + <a href="https://www.everlywell.com/">Everlywell</a> + </p> + <p class="entry-location-style">Toronto, ON, Canada</p> + </div> + <div class="flex-box"> + <p class="entry-position-style">SOFTWARE ENGINEER</p> + <p class="entry-date-style">December, 2020 - Jan, 2022</p> + </div> + <ul class="description-style"> + <li> + Maintained Amazon amplify apps to create and deploy React web applications for companies such as <a href="https://brooklynnets.everlywell.com/">NBA</a>, <a href="https://tinder.everlywell.com/">Tinder</a>, and other companies for COVID-19 at-home test kits.</li> + <li> + Implemented a script that helps accurately access and refund unused covid test kits; helping company save up to 200,000 USD.</li> + <li> + Created several Rails controllers for internal purposes; mocking end to end user experience for QA, mass refund features for CX department, and more, ultimately reducing support tickets amount by 50 percent.</li> + <li> + Implemented an audit table to help debug problems and logged which process was responsible for the change of the record using PaperTrail gems</li> + </ul> + + <div class="flex-box"> + <p class="entry-title-style"> + <a href="https://www.spiria.com/">Spiria</a> + </p> + <p class="entry-location-style">Oakville, ON, Canada</p> + </div> + <div class="flex-box"> + <p class="entry-position-style">SOFTWARE ENGINEER</p> + <p class="entry-date-style">October, 2018 - October, 2020</p> + </div> + <ul class="description-style"> + <li> + Constructed RESTful API endpoints in multiple different frameworks such as Django, Ruby on Rails, and Flask and automated API documentation process using swagger. + </li> + <li> + Designed custom rake tasks for importing production data into newly updated data structure to meet client's needs. + </li> + <li> + Maintained or updated staging/productions servers. Debugged problems in production postgres database using ssh and postgres console on Heroku or AWS servers + </li> + <li> + Collaborated in creating automation python scripts for websites and application using selenium covering for QA eliminating 80% of QA's manual work + </li> + </ul> + + <div class="flex-box"> + <p class="entry-title-style"> + <a href="https://www.apexscore.ai/">Apex Score</a> + </p> + <p class="entry-location-style">Oakville, ON, Canada</p> + </div> + <div class="flex-box"> + <p class="entry-position-style">SOFTWARE ENGINEER</p> + <p class="entry-date-style">September, 2019 - October, 2020</p> + </div> + <ul class="description-style"> + <li> + Developed custom Shapley value regression model to calculate importance of independent variables of data sets using sklearn, pandas, and numpy. + </li> + <li> + Created custom image uploader to Amazon s3 bucket using boto3 library. + </li> + <li> + Built RESTful API application using Flask framework and automated extensive API documentation pages using flask-restplus, pytest, and swagger, covering 95% of the code base. + </li> + <li> + Created an interactive graph using D3.js in Vue.js with data from Flask backend API. + </li> + </ul> + + <div class="sub-header"> + <h2 class="section-style"> Education </h2> + <div class="line"></div> + </div> + <div class="flex-box"> + <p class="entry-title-style"> + University of British Columbia + </p> + <p class="entry-location-style">Kelowna, British Columbia</p> + </div> + <div class="flex-box"> + <p class="entry-position-style">BACHELOR OF SCIENCE IN PHYSICS</p> + <p class="entry-date-style">2014 - 2018</p> + </div> + </main> + </body> +</html>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mrjunejune/test/snapshots/missing.html.snapshot Wed Dec 24 09:15:55 2025 -0800 @@ -0,0 +1,5 @@ +HTTP/1.1 404 Not Found +Content-Type: text/html +Content-Length: 0 +Connection: close +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mrjunejune/test/snapshots/nonexistent.snapshot Wed Dec 24 09:15:55 2025 -0800 @@ -0,0 +1,5 @@ +HTTP/1.1 404 Not Found +Content-Type: text/html +Content-Length: 0 +Connection: close +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mrjunejune/test/snapshots/root.snapshot Wed Dec 24 09:15:55 2025 -0800 @@ -0,0 +1,245 @@ +HTTP/1.1 200 OK +Content-Type: text/html; charset=utf-8 +Content-Length: 12120 +Connection: close + +<!doctype html> +<html lang="en"> + <head> + <BaseHead title="Resume" description="June's resume" /> + <link rel="stylesheet" href="base.css" /> + <link rel="stylesheet" href="resume.css" /> + </head> + <body> + <main> + <div> + <p>Hi, my name is Juntae, but most people call me <b>June</b>.</p> + <p>I am a software engineer with experience spanning a wide range of companies, from small startups to FAANG. </p> + <p>Feel free to check out my <a href="/resume.pdf"> resume </a> below, and if you're interested, don’t hesitate to contact me for contract work ranging from web/app development to embedded programming.</p> + </div> + <div class="info"> + <p><span class="header-firstname-style"> JUNTAE </span><span class="header-lastname-style">PARK</span><p> + <p class="header-position-style"> FULL STACK DEVELOPER · SOFTWARE ENGINEER </p> + <div class="header-address-style"> + Bay Area, CA, USA + </div> + <div class="header-social-style"> + <p>📱(US) 650-531-1728 |📱(CA) 437-580-8026 | <a href="mailto:[email protected]"> ✉️ [email protected] </a>| <a href="https://github.com/mrjunejune"> + <svg viewBox="0 0 16 16" aria-hidden="true" width="12" height="12"> + <path + fill="currentColor" + d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z" + ></path> + </svg> mrjunejune + </a>| <a href="https://www.linkedin.com/in/junepark"> + <svg width="12" height="12" fill="currentColor" class="bi bi-linkedin" viewBox="0 0 16 16"> + <path d="M0 1.146C0 .513.526 0 1.175 0h13.65C15.474 0 16 .513 16 1.146v13.708c0 .633-.526 1.146-1.175 1.146H1.175C.526 16 0 15.487 0 14.854zm4.943 12.248V6.169H2.542v7.225zm-1.2-8.212c.837 0 1.358-.554 1.358-1.248-.015-.709-.52-1.248-1.342-1.248S2.4 3.226 2.4 3.934c0 .694.521 1.248 1.327 1.248zm4.908 8.212V9.359c0-.216.016-.432.08-.586.173-.431.568-.878 1.232-.878.869 0 1.216.662 1.216 1.634v3.865h2.401V9.25c0-2.22-1.184-3.252-2.764-3.252-1.274 0-1.845.7-2.165 1.193v.025h-.016l.016-.025V6.169h-2.4c.03.678 0 7.225 0 7.225z"/> + </svg> + junepark + </a> + </p> + </div> + </div> + + <div class="sub-header"> + <h2 class="section-style"> Summary </h2> + <div class="line"></div> + </div> + <p class="paragraph-style">Software Engineer with 8 years of hands-on experience across diverse tech stacks, from early-stage startups to FANG-scale systems. Adept in designing and delivering robust software solutions using modern languages, frameworks, and cloud platforms. Open to impactful work.</p> + <div class="sub-header"> + + <h2 class="section-style"> Skills </h2> + <div class="line"></div> + </div> + <div class="paragraph-style"> + <p> + <span class="entry-title-style"> Programming Languages:</span> + <span class="skill-type-style"> TypeScript, Python, C++/C, Ruby, Java, MATLAB </span> + </p> + <p> + <span class="entry-title-style">Tools & Platforms:</span> + <span class="skill-type-style">Bazel, PostgresSQL, Mercurial, Git, Pands, Raylib, XCode</span> + </p> + <p> + <span class="entry-title-style"> Web Frameworks: </span> + <span class="skill-type-style"> Django, Rails, React, Flask</span> + </p> + <p> + <span class="entry-title-style"> DevOp:</span> + <span class="skill-type-style"> Plummi, Heroku, DigitalOcean, AWS, Google Cloud </span> + </p> + <p> + <span class="entry-title-style"> Language:</span> + <span class="skill-type-style"> English, Korean, Japanese </span> + </p> + </div> + + <!-- Experiences --> + <div class="sub-header"> + <h2 class="section-style"> Experience </h2> + <div class="line"></div> + </div> + <div class="flex-box"> + <p class="entry-title-style"> + <a href="https://www.meta.com/">Meta</a> + </p> + <p class="entry-location-style">San Francisco, CA, USA</p> + </div> + <div class="flex-box"> + <p class="entry-position-style">SOFTWARE ENGINEER</p> + <p class="entry-date-style">Oct, 2024 - Present</p> + </div> + <ul class="description-style"> + <li> + Took initiative on Channel Value Rule, targeting the 16% of ad traffic with both app and web destinations to improve value attribution and ROI. + </li> + <li> + Built full-stack features using React and Hack/GraphQL, contributing to scalable, production-ready systems. + </li> + <li> + Partnered with data science to design A/B tests and analyze revenue impact of ads destination. + </li> + <li> + Proposed and implemented alpha improvements to internal testing infrastructure, reducing test time by 50% and enhancing developer velocity. + </li> + </ul> + <div class="flex-box"> + <p class="entry-title-style"> + <a href="https://www.wmg.com/">Warner Music Group</a> + </p> + <p class="entry-location-style">Toronto, ON, Canada</p> + </div> + <div class="flex-box"> + <p class="entry-position-style">TECHNICAL LEAD ENGINEER</p> + <p class="entry-date-style">July, 2023 - Sept, 2024</p> + </div> + <ul class="description-style"> + <li> + Implements <a href="https://bazel.build/">bazel </a>structure for the company for TypeScript and JavaScript code base for hermiticity and stablishing standards for JavaScript and + </li> + <li> + TypeScript testing and code structures. + </li> + <li> + Led a team of five engineers in building GraphQL endpoints for client-facing applications using Apollo and AppSync, supporting over 2000 RPS and auto scaling depending on request values. + </li> + <li> + Improved application response times by up to 85% for graphQL response by updating database schema and SQL queries, eliminating N+1 queries and lack of indexes. + </li> + <li> + Developed CI/CD pipelines for backend structures. + </li> + <li> + Designed infrastructure for pub/sub, caching, and media processing logic. + </li> + </ul> + + <div class="flex-box"> + <p class="entry-title-style"> + <a href="https://www.google.com/">Google</a> + </p> + <p class="entry-location-style">Toronto, ON, Canada</p> + </div> + <div class="flex-box"> + <p class="entry-position-style">SOFTWARE ENGINEER</p> + <p class="entry-date-style">Feb, 2022 - July 2023</p> + </div> + <ul class="description-style"> + <li> + Implements and maintained new features relating to App Script across google workspace platform including Gmail, sheets, and Docs.</li> + <li> + Improved a response time and render time of App Script hover card components.</li> + <li> + Collaborated with a team of developers to ensure timely and accurate delivery of features.</li> + <li> + Conducted user testing and gathered feedback to iterate on features for optimal user experience.</li> + </ul> + + <div class="flex-box"> + <p class="entry-title-style"> + <a href="https://www.everlywell.com/">Everlywell</a> + </p> + <p class="entry-location-style">Toronto, ON, Canada</p> + </div> + <div class="flex-box"> + <p class="entry-position-style">SOFTWARE ENGINEER</p> + <p class="entry-date-style">December, 2020 - Jan, 2022</p> + </div> + <ul class="description-style"> + <li> + Maintained Amazon amplify apps to create and deploy React web applications for companies such as <a href="https://brooklynnets.everlywell.com/">NBA</a>, <a href="https://tinder.everlywell.com/">Tinder</a>, and other companies for COVID-19 at-home test kits.</li> + <li> + Implemented a script that helps accurately access and refund unused covid test kits; helping company save up to 200,000 USD.</li> + <li> + Created several Rails controllers for internal purposes; mocking end to end user experience for QA, mass refund features for CX department, and more, ultimately reducing support tickets amount by 50 percent.</li> + <li> + Implemented an audit table to help debug problems and logged which process was responsible for the change of the record using PaperTrail gems</li> + </ul> + + <div class="flex-box"> + <p class="entry-title-style"> + <a href="https://www.spiria.com/">Spiria</a> + </p> + <p class="entry-location-style">Oakville, ON, Canada</p> + </div> + <div class="flex-box"> + <p class="entry-position-style">SOFTWARE ENGINEER</p> + <p class="entry-date-style">October, 2018 - October, 2020</p> + </div> + <ul class="description-style"> + <li> + Constructed RESTful API endpoints in multiple different frameworks such as Django, Ruby on Rails, and Flask and automated API documentation process using swagger. + </li> + <li> + Designed custom rake tasks for importing production data into newly updated data structure to meet client's needs. + </li> + <li> + Maintained or updated staging/productions servers. Debugged problems in production postgres database using ssh and postgres console on Heroku or AWS servers + </li> + <li> + Collaborated in creating automation python scripts for websites and application using selenium covering for QA eliminating 80% of QA's manual work + </li> + </ul> + + <div class="flex-box"> + <p class="entry-title-style"> + <a href="https://www.apexscore.ai/">Apex Score</a> + </p> + <p class="entry-location-style">Oakville, ON, Canada</p> + </div> + <div class="flex-box"> + <p class="entry-position-style">SOFTWARE ENGINEER</p> + <p class="entry-date-style">September, 2019 - October, 2020</p> + </div> + <ul class="description-style"> + <li> + Developed custom Shapley value regression model to calculate importance of independent variables of data sets using sklearn, pandas, and numpy. + </li> + <li> + Created custom image uploader to Amazon s3 bucket using boto3 library. + </li> + <li> + Built RESTful API application using Flask framework and automated extensive API documentation pages using flask-restplus, pytest, and swagger, covering 95% of the code base. + </li> + <li> + Created an interactive graph using D3.js in Vue.js with data from Flask backend API. + </li> + </ul> + + <div class="sub-header"> + <h2 class="section-style"> Education </h2> + <div class="line"></div> + </div> + <div class="flex-box"> + <p class="entry-title-style"> + University of British Columbia + </p> + <p class="entry-location-style">Kelowna, British Columbia</p> + </div> + <div class="flex-box"> + <p class="entry-position-style">BACHELOR OF SCIENCE IN PHYSICS</p> + <p class="entry-date-style">2014 - 2018</p> + </div> + </main> + </body> +</html>
--- a/seobeo/BUILD Wed Dec 24 06:34:19 2025 -0800 +++ b/seobeo/BUILD Wed Dec 24 09:15:55 2025 -0800 @@ -1,5 +1,6 @@ load("@rules_cc//cc:cc_binary.bzl", "cc_binary") load("@rules_cc//cc:cc_library.bzl", "cc_library") +load("@rules_cc//cc:cc_test.bzl", "cc_test") filegroup( @@ -7,25 +8,78 @@ srcs = [ "seobeo.h", "seobeo_internal.h", + "snapshot_creator.h", ], visibility = ["//visibility:public"], ) +# Server-only target (no SSL, no OpenSSL dependency) alias( - name = "seobeo", + name = "seobeo_server", actual = select({ - "//config:macos": ":seobeo_macos", - "//config:linux": ":seobeo_linux", - "//conditions:default": ":seobeo_linux", + "//config:macos": ":seobeo_server_macos", + "//config:linux": ":seobeo_server_linux", + "//conditions:default": ":seobeo_server_linux", }), visibility = ["//visibility:public"], ) cc_library( - name = "seobeo_macos", + name = "seobeo_server_macos", + srcs = [ + "s_linux_network.c", + "s_web.c", + "s_ssl.c", + "os/s_macos_edge.c", + ], + hdrs = [":seobeo_hdrs"], + deps = [ + "//dowa:dowa", + ], + defines = ["SEOBEO_NO_SSL"], + target_compatible_with = [ + "@platforms//os:osx", + ], + visibility = ["//visibility:public"], +) + +cc_library( + name = "seobeo_server_linux", srcs = [ "s_linux_network.c", "s_web.c", + "s_ssl.c", + "os/s_linux_edge.c", + ], + hdrs = [":seobeo_hdrs"], + deps = [ + "//dowa:dowa", + ], + defines = ["SEOBEO_NO_SSL"], + target_compatible_with = [ + "@platforms//os:linux", + ], + visibility = ["//visibility:public"], +) + +# Client target with SSL support (full functionality) +alias( + name = "seobeo_client", + actual = select({ + "//config:macos": ":seobeo_client_macos", + "//config:linux": ":seobeo_client_linux", + "//conditions:default": ":seobeo_client_linux", + }), + visibility = ["//visibility:public"], +) + +cc_library( + name = "seobeo_client_macos", + srcs = [ + "s_linux_network.c", + "s_web.c", + "s_ssl.c", + "snapshot_creator.c", "os/s_macos_edge.c", ], hdrs = [":seobeo_hdrs"], @@ -40,10 +94,12 @@ ) cc_library( - name = "seobeo_linux", + name = "seobeo_client_linux", srcs = [ "s_linux_network.c", "s_web.c", + "s_ssl.c", + "snapshot_creator.c", "os/s_linux_edge.c", ], hdrs = [":seobeo_hdrs"], @@ -57,3 +113,19 @@ visibility = ["//visibility:public"], ) +# Default target for backward compatibility (points to client with SSL) +alias( + name = "seobeo", + actual = ":seobeo_client", + visibility = ["//visibility:public"], +) + +# Tests +cc_test( + name = "seobeo_http_client_test", + srcs = ["seobeo_http_client_test.c"], + deps = [":seobeo_client"], + size = "small", + timeout = "short", +) +
--- a/seobeo/s_linux_network.c Wed Dec 24 06:34:19 2025 -0800 +++ b/seobeo/s_linux_network.c Wed Dec 24 09:15:55 2025 -0800 @@ -105,30 +105,17 @@ p_handle->socket = socket_fd; p_handle->type = SEOBEO_STREAM_TYPE_CLIENT; + + p_handle->ssl_ctx = NULL; + p_handle->ssl = NULL; + if (use_tls) { - printf("USE SSL\n\n"); - Seobeo_Web_SSL_Init(); - p_handle->ssl_ctx = SSL_CTX_new(TLS_client_method()); - SSL_CTX_set_default_verify_paths(p_handle->ssl_ctx); - - p_handle->ssl = SSL_new(p_handle->ssl_ctx); - SSL_set_fd(p_handle->ssl, p_handle->socket); - - SSL_set_tlsext_host_name(p_handle->ssl, host); - // Blocking for TSL handshake - fcntl(socket_fd, F_SETFL, 0); - - if (SSL_connect(p_handle->ssl) != 1) + if (Seobeo_SSL_Setup_Client(p_handle, host, socket_fd) != 0) { - fprintf(stderr, "SSL_connect failed\n"); - ERR_print_errors_fp(stderr); + free(p_handle); return NULL; } - }else - { - p_handle->ssl_ctx = NULL; - p_handle->ssl = NULL; } p_handle->connected = true; @@ -212,18 +199,7 @@ if (p_handle->host) Dowa_Free(p_handle->host); if (p_handle->port) Dowa_Free(p_handle->port); - if (p_handle->ssl) - { - SSL_shutdown(p_handle->ssl); - SSL_free(p_handle->ssl); - p_handle->ssl = NULL; - } - - if (p_handle->ssl_ctx) - { - SSL_CTX_free(p_handle->ssl_ctx); - p_handle->ssl_ctx = NULL; - } + Seobeo_SSL_Cleanup(p_handle); if (p_handle->socket) { printf("Closing: %d", p_handle->socket); @@ -248,18 +224,9 @@ { if (p_handle->ssl) { - int n = SSL_write(p_handle->ssl, p_handle->write_buffer + sent, total - sent); - if (n < 0) - { - int err = SSL_get_error(p_handle->ssl, n); - if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) - { - // caller must wait for socket readiness and retry - return 0; - } - ERR_print_errors_fp(stderr); - return -1; - } + int n = Seobeo_SSL_Write(p_handle, p_handle->write_buffer + sent, total - sent); + if (n < 0) return -1; + if (n == 0) return 0; // would block sent += (uint32)n; }else { @@ -364,22 +331,9 @@ if (p_handle->ssl) { - read_size = (int32)SSL_read(p_handle->ssl, p_handle->read_buffer, - free_space); - if (read_size <= 0) - { - int err = SSL_get_error(p_handle->ssl, read_size); - switch (err) - { - case SSL_ERROR_WANT_READ: - case SSL_ERROR_WANT_WRITE: - return 0; - case SSL_ERROR_ZERO_RETURN: - default: - // TODO: Handle these errors - return -2; - } - } + read_size = Seobeo_SSL_Read(p_handle, p_handle->read_buffer + p_handle->read_buffer_len, free_space); + if (read_size < 0) return read_size; // -1 for error, -2 for closed + if (read_size == 0) return 0; // would block }else { read_size = (int32)read(p_handle->socket,
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/seobeo/s_ssl.c Wed Dec 24 09:15:55 2025 -0800 @@ -0,0 +1,144 @@ +#include "seobeo/seobeo.h" + +#ifndef SEOBEO_NO_SSL + +void Seobeo_Web_SSL_Init() +{ + SSL_load_error_strings(); + OpenSSL_add_ssl_algorithms(); +} + +void Seobeo_Web_SSL_Cleanup(void) +{ + EVP_cleanup(); // I don't think these are needed... +} + +int Seobeo_SSL_Setup_Client(Seobeo_Handle *p_handle, const char *host, int socket_fd) +{ + if (!p_handle) return -1; + + printf("USE SSL\n\n"); + Seobeo_Web_SSL_Init(); + p_handle->ssl_ctx = SSL_CTX_new(TLS_client_method()); + if (!p_handle->ssl_ctx) + { + fprintf(stderr, "SSL_CTX_new failed\n"); + ERR_print_errors_fp(stderr); + return -1; + } + + SSL_CTX_set_default_verify_paths(p_handle->ssl_ctx); + + p_handle->ssl = SSL_new(p_handle->ssl_ctx); + if (!p_handle->ssl) + { + fprintf(stderr, "SSL_new failed\n"); + ERR_print_errors_fp(stderr); + SSL_CTX_free(p_handle->ssl_ctx); + p_handle->ssl_ctx = NULL; + return -1; + } + + SSL_set_fd(p_handle->ssl, socket_fd); + SSL_set_tlsext_host_name(p_handle->ssl, host); + + // Blocking for TLS handshake + fcntl(socket_fd, F_SETFL, 0); + + if (SSL_connect(p_handle->ssl) != 1) + { + fprintf(stderr, "SSL_connect failed\n"); + ERR_print_errors_fp(stderr); + SSL_free(p_handle->ssl); + SSL_CTX_free(p_handle->ssl_ctx); + p_handle->ssl = NULL; + p_handle->ssl_ctx = NULL; + return -1; + } + + return 0; +} + +void Seobeo_SSL_Cleanup(Seobeo_Handle *p_handle) +{ + if (!p_handle) return; + + if (p_handle->ssl) + { + SSL_shutdown(p_handle->ssl); + SSL_free(p_handle->ssl); + p_handle->ssl = NULL; + } + + if (p_handle->ssl_ctx) + { + SSL_CTX_free(p_handle->ssl_ctx); + p_handle->ssl_ctx = NULL; + } +} + +int32 Seobeo_SSL_Write(Seobeo_Handle *p_handle, const uint8 *data, uint32 length) +{ + if (!p_handle || !p_handle->ssl) return -1; + + int n = SSL_write(p_handle->ssl, data, length); + if (n < 0) + { + int err = SSL_get_error(p_handle->ssl, n); + if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) + { + // caller must wait for socket readiness and retry + return 0; + } + ERR_print_errors_fp(stderr); + return -1; + } + return n; +} + +int32 Seobeo_SSL_Read(Seobeo_Handle *p_handle, uint8 *buffer, uint32 length) +{ + if (!p_handle || !p_handle->ssl) return -1; + + int32 read_size = (int32)SSL_read(p_handle->ssl, buffer, length); + if (read_size <= 0) + { + int err = SSL_get_error(p_handle->ssl, read_size); + switch (err) + { + case SSL_ERROR_WANT_READ: + case SSL_ERROR_WANT_WRITE: + return 0; + case SSL_ERROR_ZERO_RETURN: + default: + // TODO: Handle these errors + return -2; + } + } + return read_size; +} + +#else + +// Stub implementations when SSL is disabled +void Seobeo_Web_SSL_Init() {} +void Seobeo_Web_SSL_Cleanup(void) {} +int Seobeo_SSL_Setup_Client(Seobeo_Handle *p_handle, const char *host, int socket_fd) +{ + (void)p_handle; (void)host; (void)socket_fd; + fprintf(stderr, "SSL support not compiled in\n"); + return -1; +} +void Seobeo_SSL_Cleanup(Seobeo_Handle *p_handle) { (void)p_handle; } +int32 Seobeo_SSL_Write(Seobeo_Handle *p_handle, const uint8 *data, uint32 length) +{ + (void)p_handle; (void)data; (void)length; + return -1; +} +int32 Seobeo_SSL_Read(Seobeo_Handle *p_handle, uint8 *buffer, uint32 length) +{ + (void)p_handle; (void)buffer; (void)length; + return -1; +} + +#endif
--- a/seobeo/s_web.c Wed Dec 24 06:34:19 2025 -0800 +++ b/seobeo/s_web.c Wed Dec 24 09:15:55 2025 -0800 @@ -578,14 +578,3 @@ Seobeo_Handle_Destroy(h); return 0; } - -void Seobeo_Web_SSL_Init() -{ - SSL_load_error_strings(); - OpenSSL_add_ssl_algorithms(); -} - -void Seobeo_Web_SSL_Cleanup(void) -{ - EVP_cleanup(); // I don't think these are needed... -}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/seobeo/seobeo_http_client_test.c Wed Dec 24 09:15:55 2025 -0800 @@ -0,0 +1,174 @@ +#include "seobeo/seobeo.h" +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <assert.h> + +// Test basic HTTP client functionality +void test_http_client_without_ssl() { + printf("Testing HTTP client (non-SSL)...\n"); + + // Create a client handle without TLS + Seobeo_Handle *h = Seobeo_Stream_Handle_Client_Create("example.com", "80", FALSE); + + if (!h || h->socket < 0) { + printf(" ✗ Failed to create client handle\n"); + if (h) Seobeo_Handle_Destroy(h); + return; + } + + printf(" ✓ Client handle created successfully\n"); + + // Generate and send HTTP request + char request[4096]; + int request_len = sprintf( + request, + "GET / HTTP/1.1\r\n" + "Host: example.com\r\n" + "Connection: close\r\n" + "\r\n" + ); + + Seobeo_Handle_Queue(h, (uint8*)request, (uint32)request_len); + int flush_result = Seobeo_Handle_Flush(h); + + if (flush_result < 0) { + printf(" ✗ Failed to send request\n"); + Seobeo_Handle_Destroy(h); + return; + } + + printf(" ✓ Request sent successfully\n"); + + // Read response + int bytes_read = 0; + int total_bytes = 0; + int attempts = 0; + const int max_attempts = 100; + + while (attempts++ < max_attempts) { + bytes_read = Seobeo_Handle_Read(h); + if (bytes_read > 0) { + total_bytes += bytes_read; + } else if (bytes_read == -2) { + // Connection closed + break; + } else if (bytes_read == 0) { + // Would block, wait a bit + usleep(10000); // 10ms + continue; + } else { + printf(" ✗ Error reading response\n"); + Seobeo_Handle_Destroy(h); + return; + } + } + + if (total_bytes > 0) { + printf(" ✓ Received response (%d bytes)\n", total_bytes); + + // Check if we got HTTP response + if (h->read_buffer_len > 0 && strstr((char*)h->read_buffer, "HTTP/1.1") != NULL) { + printf(" ✓ Valid HTTP response received\n"); + } else { + printf(" ✗ Invalid HTTP response\n"); + } + } else { + printf(" ⚠ No response received (this may be a network issue)\n"); + } + + Seobeo_Handle_Destroy(h); + printf(" ✓ Client handle destroyed\n"); +} + +void test_http_client_with_ssl() { + printf("\nTesting HTTP client (with SSL)...\n"); + +#ifdef SEOBEO_NO_SSL + printf(" ⚠ SSL not compiled in, skipping SSL test\n"); + return; +#else + // Create a client handle with TLS + Seobeo_Handle *h = Seobeo_Stream_Handle_Client_Create("example.com", "443", TRUE); + + if (!h || h->socket < 0) { + printf(" ✗ Failed to create SSL client handle\n"); + if (h) Seobeo_Handle_Destroy(h); + return; + } + + printf(" ✓ SSL client handle created successfully\n"); + printf(" ✓ SSL handshake completed\n"); + + // Generate and send HTTPS request + char request[4096]; + int request_len = sprintf( + request, + "GET / HTTP/1.1\r\n" + "Host: example.com\r\n" + "Connection: close\r\n" + "\r\n" + ); + + Seobeo_Handle_Queue(h, (uint8*)request, (uint32)request_len); + int flush_result = Seobeo_Handle_Flush(h); + + if (flush_result < 0) { + printf(" ✗ Failed to send SSL request\n"); + Seobeo_Handle_Destroy(h); + return; + } + + printf(" ✓ SSL request sent successfully\n"); + + // Read response + int bytes_read = 0; + int total_bytes = 0; + int attempts = 0; + const int max_attempts = 100; + + while (attempts++ < max_attempts) { + bytes_read = Seobeo_Handle_Read(h); + if (bytes_read > 0) { + total_bytes += bytes_read; + } else if (bytes_read == -2) { + // Connection closed + break; + } else if (bytes_read == 0) { + // Would block, wait a bit + usleep(10000); // 10ms + continue; + } else { + printf(" ✗ Error reading SSL response\n"); + Seobeo_Handle_Destroy(h); + return; + } + } + + if (total_bytes > 0) { + printf(" ✓ Received SSL response (%d bytes)\n", total_bytes); + + // Check if we got HTTP response + if (h->read_buffer_len > 0 && strstr((char*)h->read_buffer, "HTTP/1.1") != NULL) { + printf(" ✓ Valid HTTPS response received\n"); + } else { + printf(" ✗ Invalid HTTPS response\n"); + } + } else { + printf(" ⚠ No SSL response received (this may be a network issue)\n"); + } + + Seobeo_Handle_Destroy(h); + printf(" ✓ SSL client handle destroyed\n"); +#endif +} + +int main() { + printf("=== Seobeo HTTP Client Tests ===\n\n"); + + test_http_client_without_ssl(); + test_http_client_with_ssl(); + + printf("\n=== Tests Complete ===\n"); + return 0; +}
--- a/seobeo/seobeo_internal.h Wed Dec 24 06:34:19 2025 -0800 +++ b/seobeo/seobeo_internal.h Wed Dec 24 09:15:55 2025 -0800 @@ -1,9 +1,16 @@ #ifndef SEOBEO_SERVER_INTERNAL_H #define SEOBEO_SERVER_INTERNAL_H -// SSL -#include <openssl/ssl.h> +// SSL - conditionally included +#ifndef SEOBEO_NO_SSL +#include <openssl/ssl.h> #include <openssl/err.h> +#define SSL_CTX_TYPE SSL_CTX +#define SSL_TYPE SSL +#else +#define SSL_CTX_TYPE void +#define SSL_TYPE void +#endif #ifndef DIRECTORY #define DIRECTORY @@ -22,9 +29,9 @@ char *host; char *port; - - SSL_CTX *ssl_ctx; - SSL *ssl; + + SSL_CTX_TYPE *ssl_ctx; + SSL_TYPE *ssl; uint8 *read_buffer; uint32 read_buffer_capacity; @@ -64,6 +71,10 @@ // --- SSL --- // extern void Seobeo_Web_SSL_Init(); extern void Seobeo_Web_SSL_Cleanup(); // Not used +extern int Seobeo_SSL_Setup_Client(Seobeo_Handle *p_handle, const char *host, int socket_fd); +extern void Seobeo_SSL_Cleanup(Seobeo_Handle *p_handle); +extern int32 Seobeo_SSL_Write(Seobeo_Handle *p_handle, const uint8 *data, uint32 length); +extern int32 Seobeo_SSL_Read(Seobeo_Handle *p_handle, uint8 *buffer, uint32 length); // --- Serving using Edge --- // extern void *Seobeo_Web_Edge_Worker(void *vargs); @@ -71,4 +82,3 @@ extern void Seobeo_Web_Edge_2(Seobeo_Handle *p_server_handle, Dowa_HashMap *p_html_cache); #endif -
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/seobeo/snapshot_creator.c Wed Dec 24 09:15:55 2025 -0800 @@ -0,0 +1,280 @@ +#include "seobeo/snapshot_creator.h" +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <unistd.h> + +#define MAX_RESPONSE_SIZE (1024 * 1024) + +// Helper: Convert URL path to filename +static void path_to_filename(const char *path, char *filename, size_t max_len) +{ + if (strcmp(path, "/") == 0) + { + snprintf(filename, max_len, "root.snapshot"); + return; + } + + const char *p = path; + if (*p == '/') + { + p++; + } + + char *out = filename; + size_t remaining = max_len - 1; + + while (*p && remaining > 0) + { + if (*p == '/') + { + *out++ = '_'; + remaining--; + } + else + { + *out++ = *p; + remaining--; + } + p++; + } + + snprintf(out, remaining, ".snapshot"); +} + +// Helper: Create directory recursively +static int create_directory(const char *path) +{ + char tmp[1024]; + char *p = NULL; + size_t len; + + snprintf(tmp, sizeof(tmp), "%s", path); + len = strlen(tmp); + if (tmp[len - 1] == '/') + { + tmp[len - 1] = 0; + } + + for (p = tmp + 1; *p; p++) + { + if (*p == '/') + { + *p = 0; + mkdir(tmp, 0755); + *p = '/'; + } + } + return mkdir(tmp, 0755); +} + +// Helper: Generate HTTP GET request +static int generate_http_get(char *buffer, size_t size, const char *path, const char *host) +{ + return snprintf( + buffer, size, + "GET %s HTTP/1.1\r\n" + "Host: %s\r\n" + "Connection: close\r\n" + "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" + "User-Agent: SeobeoSnapshotCreator/1.0\r\n" + "\r\n", + path, host + ); +} + +// Helper: Read full HTTP response +static char* read_full_response(Seobeo_Handle *client, size_t *len_out) +{ + char *response = malloc(MAX_RESPONSE_SIZE); + if (!response) + { + return NULL; + } + + size_t total = 0; + int attempts = 0; + const int max_attempts = 100; + + while (attempts++ < max_attempts && total < MAX_RESPONSE_SIZE - 1) + { + int n = Seobeo_Handle_Read(client); + + if (n > 0) + { + size_t to_copy = client->read_buffer_len; + if (total + to_copy > MAX_RESPONSE_SIZE - 1) + { + to_copy = MAX_RESPONSE_SIZE - 1 - total; + } + + memcpy(response + total, client->read_buffer, to_copy); + total += to_copy; + Seobeo_Handle_Consume(client, (uint32)to_copy); + } + else if (n == -2) + { + break; + } + else if (n == 0) + { + usleep(10000); + continue; + } + else + { + free(response); + return NULL; + } + } + + response[total] = '\0'; + *len_out = total; + return response; +} + +// Helper: Write snapshot to file +static int write_snapshot(const char *filepath, const char *data, size_t size) +{ + // Create directory if needed + char dir_copy[1024]; + snprintf(dir_copy, sizeof(dir_copy), "%s", filepath); + + char *last_slash = strrchr(dir_copy, '/'); + if (last_slash) + { + *last_slash = '\0'; + create_directory(dir_copy); + } + + FILE *f = fopen(filepath, "wb"); + if (!f) + { + perror("fopen"); + return -1; + } + + size_t written = fwrite(data, 1, size, f); + fclose(f); + + return (written == size) ? 0 : -1; +} + +int Seobeo_Snapshot_Create(const SnapshotConfig *config) +{ + if (!config || !config->path || !config->snapshot_dir || !config->host || !config->port) + { + fprintf(stderr, "Invalid snapshot config\n"); + return -1; + } + + printf("Creating snapshot: %s (expecting %d)\n", config->path, config->expected_status); + + // Connect to server + Seobeo_Handle *client = Seobeo_Stream_Handle_Client_Create(config->host, config->port, FALSE); + if (!client || client->socket < 0) + { + fprintf(stderr, " ✗ Failed to connect to %s:%s\n", config->host, config->port); + if (client) + { + Seobeo_Handle_Destroy(client); + } + return -1; + } + + // Send GET request + char request[4096]; + int req_len = generate_http_get(request, sizeof(request), config->path, config->host); + Seobeo_Handle_Queue(client, (uint8*)request, (uint32)req_len); + + if (Seobeo_Handle_Flush(client) < 0) + { + fprintf(stderr, " ✗ Failed to send request\n"); + Seobeo_Handle_Destroy(client); + return -1; + } + + // Read response + size_t response_len = 0; + char *response = read_full_response(client, &response_len); + Seobeo_Handle_Destroy(client); + + if (!response || response_len == 0) + { + fprintf(stderr, " ✗ Failed to read response\n"); + if (response) + { + free(response); + } + return -1; + } + + // Parse status code + int status = -1; + const char *status_line = strstr(response, "HTTP/1.1 "); + if (!status_line) + { + status_line = strstr(response, "HTTP/1.0 "); + } + if (status_line) + { + sscanf(status_line + 9, "%d", &status); + } + + if (status != config->expected_status) + { + fprintf(stderr, " ✗ Status mismatch: expected %d, got %d\n", + config->expected_status, status); + free(response); + return -1; + } + + printf(" ✓ Status: %d\n", status); + + // Generate snapshot filename + char filename[256]; + path_to_filename(config->path, filename, sizeof(filename)); + + char filepath[1024]; + snprintf(filepath, sizeof(filepath), "%s/%s", config->snapshot_dir, filename); + + // Write snapshot + if (write_snapshot(filepath, response, response_len) == 0) + { + printf(" ✓ Snapshot saved: %s (%zu bytes)\n", filepath, response_len); + free(response); + return 0; + } + else + { + fprintf(stderr, " ✗ Failed to write snapshot: %s\n", filepath); + free(response); + return -1; + } +} + +int Seobeo_Snapshots_Create_Batch(const SnapshotConfig configs[], int count) +{ + int failed = 0; + int passed = 0; + + for (int i = 0; i < count; i++) + { + if (Seobeo_Snapshot_Create(&configs[i]) == 0) + { + passed++; + } + else + { + failed++; + } + printf("\n"); + } + + printf("=== Summary ===\n"); + printf("Created: %d\n", passed); + printf("Failed: %d\n", failed); + + return (failed == 0) ? 0 : -1; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/seobeo/snapshot_creator.h Wed Dec 24 09:15:55 2025 -0800 @@ -0,0 +1,21 @@ +#ifndef SEOBEO_SNAPSHOT_CREATOR_H +#define SEOBEO_SNAPSHOT_CREATOR_H + +#include "seobeo/seobeo.h" + +typedef struct +{ + const char *path; + int expected_status; + const char *snapshot_dir; + const char *host; + const char *port; +} SnapshotConfig; + +// Create a snapshot for a single path +int Seobeo_Snapshot_Create(const SnapshotConfig *config); + +// Create snapshots for multiple paths +int Seobeo_Snapshots_Create_Batch(const SnapshotConfig configs[], int count); + +#endif