Mercurial
view hg-web/main.c @ 279:b3b547563ec7
Add Google connector service and agent wiki
Implement the C/Seobeo Google Drive and Gmail connector with encrypted OAuth storage, Zenbu authentication, browser testing, AI tool discovery, chunked HTTP decoding, and Bazel coverage. Consolidate repository guidance into progressive wiki documentation and enforce arena-first allocation for new first-party C code.
Co-authored-by: Copilot <[email protected]>
Copilot-Session: 84c338fd-0939-4bb3-b7f3-1062eb213e5d
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Mon, 17 Aug 2026 22:22:36 -0700 |
| parents | c5129452493e |
| children |
line wrap: on
line source
#include "seobeo/seobeo.h" #include "dowa/dowa.h" #include <ctype.h> #include <errno.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <strings.h> #include <time.h> #include <unistd.h> #define HG_SERVE_HOST "127.0.0.1" #define HG_SERVE_PORT "4444" #define HG_API_TIMEOUT_MS 15000 #define HG_STREAM_IDLE_TIMEOUT_MS 3600000 #define MAX_PATH_LENGTH 4096 #define MAX_WIRE_QUERY_LENGTH 8192 #define MAX_WIRE_HEADER_LENGTH 8192 static const char *map_value_case_insensitive(Seobeo_Request_Entry *map, const char *key) { if (!map || !key) return NULL; for (size_t i = 0; i < Dowa_Array_Length(map); i++) { if (map[i].key && strcasecmp(map[i].key, key) == 0) return map[i].value; } return NULL; } static char *arena_string(Dowa_Arena *arena, const char *value) { size_t length = strlen(value); char *copy = Dowa_Arena_Allocate(arena, length + 1); memcpy(copy, value, length + 1); return copy; } static Seobeo_Request_Entry *text_response( Dowa_Arena *arena, const char *status, const char *content_type, const char *body) { Seobeo_Request_Entry *response = NULL; Dowa_HashMap_Push_Arena(response, "status", arena_string(arena, status), arena); Dowa_HashMap_Push_Arena( response, "content-type", arena_string(arena, content_type), arena); Dowa_HashMap_Push_Arena(response, "body", arena_string(arena, body), arena); return response; } static boolean decode_url_component( const char *encoded, Dowa_Arena *arena, char **decoded_out, size_t *decoded_length_out) { if (!encoded || !decoded_out) return FALSE; size_t encoded_length = strlen(encoded); if (encoded_length >= MAX_PATH_LENGTH) return FALSE; char *decoded = Dowa_Arena_Allocate(arena, encoded_length + 1); size_t output_length = 0; for (size_t i = 0; i < encoded_length; i++) { unsigned char value = (unsigned char)encoded[i]; if (encoded[i] == '%') { if (i + 2 >= encoded_length || !isxdigit((unsigned char)encoded[i + 1]) || !isxdigit((unsigned char)encoded[i + 2])) return FALSE; char hex[3] = {encoded[i + 1], encoded[i + 2], '\0'}; value = (unsigned char)strtoul(hex, NULL, 16); i += 2; if (value == '\0') return FALSE; } decoded[output_length++] = (char)value; } decoded[output_length] = '\0'; *decoded_out = decoded; if (decoded_length_out) *decoded_length_out = output_length; return TRUE; } static boolean normalize_repository_path( const char *encoded_path, Dowa_Arena *arena, char **normalized_out) { char *decoded = NULL; size_t decoded_length = 0; if (!decode_url_component(encoded_path ? encoded_path : "", arena, &decoded, &decoded_length)) return FALSE; size_t start = 0; size_t end = decoded_length; if (start < end && decoded[start] == '/') { start++; if (start < end && decoded[start] == '/') return FALSE; } if (end > start && decoded[end - 1] == '/') { if (end - 1 > start && decoded[end - 2] == '/') return FALSE; end--; } size_t segment_start = start; for (size_t i = start; i <= end; i++) { boolean at_end = i == end; unsigned char c = at_end ? '/' : (unsigned char)decoded[i]; if (!at_end && (iscntrl(c) || c == '\\' || c == '?' || c == '#')) return FALSE; if (c == '/') { size_t segment_length = i - segment_start; if (segment_length == 0 && !at_end) return FALSE; if ((segment_length == 1 && decoded[segment_start] == '.') || (segment_length == 2 && decoded[segment_start] == '.' && decoded[segment_start + 1] == '.')) return FALSE; segment_start = i + 1; } } size_t normalized_length = end - start; char *normalized = Dowa_Arena_Allocate(arena, normalized_length + 1); memcpy(normalized, decoded + start, normalized_length); normalized[normalized_length] = '\0'; *normalized_out = normalized; return TRUE; } static boolean validate_revision(const char *revision) { if (!revision || revision[0] == '\0') return FALSE; if (strcmp(revision, "tip") == 0) return TRUE; size_t length = strlen(revision); if (length > 40) return FALSE; for (size_t i = 0; i < length; i++) { if (!isxdigit((unsigned char)revision[i])) return FALSE; } return TRUE; } static char *encode_repository_path(const char *path, Dowa_Arena *arena) { static const char hex[] = "0123456789ABCDEF"; size_t length = strlen(path); char *encoded = Dowa_Arena_Allocate(arena, length * 3 + 1); size_t output = 0; for (size_t i = 0; i < length; i++) { unsigned char c = (unsigned char)path[i]; if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~' || c == '/') encoded[output++] = (char)c; else { encoded[output++] = '%'; encoded[output++] = hex[c >> 4]; encoded[output++] = hex[c & 0x0F]; } } encoded[output] = '\0'; return encoded; } static boolean safe_header_value(const char *value, size_t maximum_length) { if (!value) return TRUE; size_t length = strlen(value); return length <= maximum_length && strchr(value, '\r') == NULL && strchr(value, '\n') == NULL; } static boolean extension_is(const char *extension, const char *expected) { return extension && strcasecmp(extension, expected) == 0; } static const char *repository_file_content_type( const char *path, boolean *inline_preview, boolean *sandbox_content) { const char *extension = strrchr(path, '.'); *inline_preview = TRUE; *sandbox_content = FALSE; if (extension_is(extension, ".png")) return "image/png"; if (extension_is(extension, ".jpg") || extension_is(extension, ".jpeg")) return "image/jpeg"; if (extension_is(extension, ".gif")) return "image/gif"; if (extension_is(extension, ".webp")) return "image/webp"; if (extension_is(extension, ".avif")) return "image/avif"; if (extension_is(extension, ".bmp")) return "image/bmp"; if (extension_is(extension, ".ico")) return "image/x-icon"; if (extension_is(extension, ".svg")) { *sandbox_content = TRUE; return "image/svg+xml"; } if (extension_is(extension, ".mp4") || extension_is(extension, ".m4v")) return "video/mp4"; if (extension_is(extension, ".webm")) return "video/webm"; if (extension_is(extension, ".mov")) return "video/quicktime"; if (extension_is(extension, ".ogv")) return "video/ogg"; if (extension_is(extension, ".mp3")) return "audio/mpeg"; if (extension_is(extension, ".wav")) return "audio/wav"; if (extension_is(extension, ".ogg") || extension_is(extension, ".oga")) return "audio/ogg"; if (extension_is(extension, ".flac")) return "audio/flac"; if (extension_is(extension, ".m4a")) return "audio/mp4"; if (extension_is(extension, ".aac")) return "audio/aac"; if (extension_is(extension, ".pdf")) return "application/pdf"; if (extension_is(extension, ".wasm")) return "application/wasm"; *inline_preview = FALSE; if (extension_is(extension, ".md") || extension_is(extension, ".markdown")) return "text/markdown; charset=utf-8"; if (extension_is(extension, ".txt") || extension_is(extension, ".log") || extension_is(extension, ".c") || extension_is(extension, ".h") || extension_is(extension, ".cc") || extension_is(extension, ".cpp") || extension_is(extension, ".js") || extension_is(extension, ".jsx") || extension_is(extension, ".ts") || extension_is(extension, ".tsx") || extension_is(extension, ".css") || extension_is(extension, ".html") || extension_is(extension, ".htm") || extension_is(extension, ".xml") || extension_is(extension, ".json") || extension_is(extension, ".yaml") || extension_is(extension, ".yml") || extension_is(extension, ".toml") || extension_is(extension, ".sh") || extension_is(extension, ".py") || extension_is(extension, ".rs") || extension_is(extension, ".go")) return "text/plain; charset=utf-8"; return "application/octet-stream"; } static Seobeo_Client_Response *hg_proxy_request( const char *method, const char *path, const char *request_body, size_t request_body_length, const char *hg_argument, const char *accept) { char url[MAX_PATH_LENGTH]; int url_length = snprintf( url, sizeof(url), "http://%s:%s%s", HG_SERVE_HOST, HG_SERVE_PORT, path); if (url_length < 0 || (size_t)url_length >= sizeof(url)) return NULL; Seobeo_Client_Request *request = Seobeo_Client_Request_Create(url); if (!request) return NULL; Seobeo_Client_Request_Set_Method(request, method); Seobeo_Client_Request_Add_Header_Map(request, "User-Agent", "Seobeo/1.0"); Seobeo_Client_Request_Add_Header_Map( request, "Accept", accept ? accept : "application/json"); Seobeo_Client_Request_Set_Timeout_Milliseconds(request, HG_API_TIMEOUT_MS); if (hg_argument && hg_argument[0] != '\0') { if (!safe_header_value(hg_argument, MAX_WIRE_HEADER_LENGTH)) { Seobeo_Client_Request_Destroy(request); return NULL; } Seobeo_Client_Request_Add_Header_Map(request, "x-hgarg-1", hg_argument); } if (request_body && request_body_length > 0) Seobeo_Client_Request_Set_Body(request, request_body, request_body_length); Seobeo_Client_Response *response = Seobeo_Client_Request_Execute(request); Seobeo_Client_Request_Destroy(request); return response; } static Seobeo_Request_Entry *forward_hg_response( Seobeo_Client_Response *hg_response, const char *default_content_type, const char *override_content_type, Dowa_Arena *arena) { if (!hg_response) return text_response( arena, "502", "application/json", "{\"error\":\"Mercurial backend unavailable\"}"); const char *upstream_content_type = map_value_case_insensitive(hg_response->headers, "Content-Type"); const char *upstream_or_default_content_type = override_content_type ? override_content_type : upstream_content_type ? upstream_content_type : default_content_type; if (!upstream_or_default_content_type) upstream_or_default_content_type = "application/octet-stream"; char *content_type = arena_string(arena, upstream_or_default_content_type); char *status = Dowa_Arena_Allocate(arena, 8); snprintf(status, 8, "%d", hg_response->status_code); size_t body_length = hg_response->body ? hg_response->body_length : 0; char *body = Dowa_Arena_Allocate(arena, body_length + 1); if (body_length > 0) memcpy(body, hg_response->body, body_length); body[body_length] = '\0'; char *content_length = Dowa_Arena_Allocate(arena, 32); snprintf(content_length, 32, "%zu", body_length); Seobeo_Request_Entry *response = NULL; Dowa_HashMap_Push_Arena(response, "status", status, arena); Dowa_HashMap_Push_Arena(response, "content-type", content_type, arena); Dowa_HashMap_Push_Arena(response, "body", body, arena); Dowa_HashMap_Push_Arena(response, "content-length", content_length, arena); Seobeo_Client_Response_Destroy(hg_response); return response; } Seobeo_Request_Entry *ApiListDirectory(Seobeo_Request_Entry *request, Dowa_Arena *arena) { const char *encoded_path = map_value_case_insensitive(request, "query_path"); char *path = NULL; if (!normalize_repository_path(encoded_path ? encoded_path : "", arena, &path)) return text_response(arena, "400", "application/json", "{\"error\":\"Invalid repository path\"}"); char *encoded = encode_repository_path(path, arena); char hg_path[MAX_PATH_LENGTH]; int length = snprintf( hg_path, sizeof(hg_path), encoded[0] ? "/file/tip/%s?style=json" : "/file/tip/?style=json", encoded); if (length < 0 || (size_t)length >= sizeof(hg_path)) return text_response(arena, "400", "application/json", "{\"error\":\"Repository path is too long\"}"); return forward_hg_response( hg_proxy_request("GET", hg_path, NULL, 0, NULL, "application/json"), "application/json", NULL, arena); } Seobeo_Request_Entry *ApiGetFile(Seobeo_Request_Entry *request, Dowa_Arena *arena) { const char *encoded_path = map_value_case_insensitive(request, "query_path"); char *path = NULL; if (!normalize_repository_path(encoded_path ? encoded_path : "", arena, &path) || path[0] == '\0') return text_response(arena, "400", "text/plain", "A valid file path is required"); char *encoded = encode_repository_path(path, arena); char hg_path[MAX_PATH_LENGTH]; int length = snprintf(hg_path, sizeof(hg_path), "/raw-file/tip/%s", encoded); if (length < 0 || (size_t)length >= sizeof(hg_path)) return text_response(arena, "400", "text/plain", "File path is too long"); Seobeo_Client_Response *hg_response = hg_proxy_request("GET", hg_path, NULL, 0, NULL, "application/octet-stream"); if (!hg_response) return forward_hg_response(NULL, "application/json", NULL, arena); boolean inline_preview = FALSE; boolean sandbox_content = FALSE; const char *content_type = repository_file_content_type(path, &inline_preview, &sandbox_content); Seobeo_Request_Entry *response = forward_hg_response( hg_response, "application/octet-stream", content_type, arena); Dowa_HashMap_Push_Arena( response, "Content-Disposition", inline_preview ? "inline" : "attachment", arena); Dowa_HashMap_Push_Arena( response, "X-Content-Type-Options", "nosniff", arena); if (sandbox_content) Dowa_HashMap_Push_Arena( response, "Content-Security-Policy", "sandbox", arena); return response; } Seobeo_Request_Entry *ApiGetReadme(Seobeo_Request_Entry *request, Dowa_Arena *arena) { const char *encoded_path = map_value_case_insensitive(request, "query_path"); char *directory = NULL; if (!normalize_repository_path(encoded_path ? encoded_path : "", arena, &directory)) return text_response(arena, "400", "text/plain", "Invalid repository path"); size_t readme_length = strlen(directory) + strlen("/README.md") + 1; if (readme_length >= MAX_PATH_LENGTH) return text_response(arena, "400", "text/plain", "README path is too long"); char *readme_path = Dowa_Arena_Allocate(arena, readme_length); snprintf( readme_path, readme_length, directory[0] ? "%s/README.md" : "README.md", directory); char *encoded = encode_repository_path(readme_path, arena); char hg_path[MAX_PATH_LENGTH]; int length = snprintf(hg_path, sizeof(hg_path), "/raw-file/tip/%s", encoded); if (length < 0 || (size_t)length >= sizeof(hg_path)) return text_response(arena, "400", "text/plain", "README path is too long"); Seobeo_Client_Response *hg_response = hg_proxy_request("GET", hg_path, NULL, 0, NULL, "text/markdown"); if (hg_response && hg_response->status_code == HTTP_NOT_FOUND) { Seobeo_Client_Response_Destroy(hg_response); return text_response(arena, "204", "text/markdown", ""); } return forward_hg_response( hg_response, "text/markdown", "text/markdown; charset=utf-8", arena); } Seobeo_Request_Entry *ApiGetGraph(Seobeo_Request_Entry *request, Dowa_Arena *arena) { const char *graph_id = map_value_case_insensitive(request, ":graph_id"); if (!validate_revision(graph_id)) return text_response(arena, "400", "application/json", "{\"error\":\"Invalid graph revision\"}"); const char *encoded_graph_top = map_value_case_insensitive(request, "query_graphtop"); char *graph_top = NULL; if (encoded_graph_top) { if (!decode_url_component(encoded_graph_top, arena, &graph_top, NULL) || !validate_revision(graph_top)) return text_response(arena, "400", "application/json", "{\"error\":\"Invalid graph top revision\"}"); } char hg_path[MAX_PATH_LENGTH]; int length = graph_top ? snprintf( hg_path, sizeof(hg_path), "/graph/%s?graphtop=%s&style=json", graph_id, graph_top) : snprintf(hg_path, sizeof(hg_path), "/graph/%s?style=json", graph_id); if (length < 0 || (size_t)length >= sizeof(hg_path)) return text_response(arena, "400", "application/json", "{\"error\":\"Graph request is too long\"}"); return forward_hg_response( hg_proxy_request("GET", hg_path, NULL, 0, NULL, "application/json"), "application/json", NULL, arena); } Seobeo_Request_Entry *ApiGetChangeset(Seobeo_Request_Entry *request, Dowa_Arena *arena) { const char *changeset_id = map_value_case_insensitive(request, ":changeset_id"); if (!validate_revision(changeset_id)) return text_response(arena, "400", "application/json", "{\"error\":\"Invalid changeset revision\"}"); char hg_path[128]; int length = snprintf(hg_path, sizeof(hg_path), "/json-rev/%s", changeset_id); if (length < 0 || (size_t)length >= sizeof(hg_path)) return text_response(arena, "400", "application/json", "{\"error\":\"Changeset request is too long\"}"); return forward_hg_response( hg_proxy_request("GET", hg_path, NULL, 0, NULL, "application/json"), "application/json", NULL, arena); } static int64 monotonic_milliseconds(void) { struct timespec now; clock_gettime(CLOCK_MONOTONIC, &now); return (int64)now.tv_sec * 1000 + now.tv_nsec / 1000000; } static size_t find_http_header_length(const uint8 *buffer, size_t length) { if (!buffer || length < 4) return 0; for (size_t i = 0; i + 3 < length; i++) { if (buffer[i] == '\r' && buffer[i + 1] == '\n' && buffer[i + 2] == '\r' && buffer[i + 3] == '\n') return i + 4; } return 0; } static void send_proxy_error(Seobeo_Handle *client, int status, const char *message) { const char *reason = status == 504 ? "Gateway Timeout" : "Bad Gateway"; char response[512]; int length = snprintf( response, sizeof(response), "HTTP/1.1 %d %s\r\n" "Content-Type: text/plain\r\n" "Content-Length: %zu\r\n" "Connection: close\r\n" "\r\n" "%s", status, reason, strlen(message), message); if (length > 0 && (size_t)length < sizeof(response)) { Seobeo_Handle_Queue(client, (const uint8 *)response, (uint32)length); Seobeo_Handle_Flush(client); } } static boolean parse_content_length(const char *value, size_t *length_out) { if (!value || !length_out || value[0] == '\0') return FALSE; errno = 0; char *end = NULL; unsigned long long parsed = strtoull(value, &end, 10); if (errno != 0 || !end || *end != '\0' || parsed > SIZE_MAX) return FALSE; *length_out = (size_t)parsed; return TRUE; } void StreamHgWireProtocol( Seobeo_Handle *client, Seobeo_Request_Entry *request, Dowa_Arena *arena) { (void)arena; const char *method = map_value_case_insensitive(request, "HTTP_Method"); const char *query = map_value_case_insensitive(request, "QueryString"); const char *body = map_value_case_insensitive(request, "Body"); const char *content_length_value = map_value_case_insensitive(request, "Content-Length"); const char *content_type = map_value_case_insensitive(request, "Content-Type"); const char *hg_argument = map_value_case_insensitive(request, "x-hgarg-1"); if (!method || !query || (strcmp(method, "GET") != 0 && strcmp(method, "POST") != 0) || !safe_header_value(query, MAX_WIRE_QUERY_LENGTH) || !safe_header_value(hg_argument, MAX_WIRE_HEADER_LENGTH) || !safe_header_value(content_type, 256)) { send_proxy_error(client, 502, "Invalid Mercurial proxy request"); return; } size_t body_length = 0; if (content_length_value && !parse_content_length(content_length_value, &body_length)) { send_proxy_error(client, 502, "Invalid Mercurial request length"); return; } if (body_length > 0 && !body) { send_proxy_error(client, 502, "Missing Mercurial request body"); return; } if (body_length > UINT32_MAX) { send_proxy_error(client, 502, "Mercurial request body is too large"); return; } Seobeo_Handle *upstream = Seobeo_Stream_Handle_Client_Create(HG_SERVE_HOST, HG_SERVE_PORT, FALSE); if (!upstream || upstream->socket < 0) { if (upstream) Seobeo_Handle_Destroy(upstream); send_proxy_error(client, 502, "Mercurial backend unavailable"); return; } char request_header[16384]; int header_length = snprintf( request_header, sizeof(request_header), "%s /?%s HTTP/1.1\r\n" "Host: %s:%s\r\n" "User-Agent: Seobeo/1.0\r\n" "Connection: close\r\n", method, query, HG_SERVE_HOST, HG_SERVE_PORT); if (header_length < 0 || (size_t)header_length >= sizeof(request_header)) { Seobeo_Handle_Destroy(upstream); send_proxy_error(client, 502, "Mercurial request headers are too large"); return; } #define APPEND_WIRE_HEADER(...) \ do { \ int appended = snprintf( \ request_header + header_length, \ sizeof(request_header) - (size_t)header_length, \ __VA_ARGS__); \ if (appended < 0 || (size_t)appended >= sizeof(request_header) - (size_t)header_length) { \ Seobeo_Handle_Destroy(upstream); \ send_proxy_error(client, 502, "Mercurial request headers are too large"); \ return; \ } \ header_length += appended; \ } while (0) if (hg_argument && hg_argument[0] != '\0') APPEND_WIRE_HEADER("x-hgarg-1: %s\r\n", hg_argument); if (content_type && content_type[0] != '\0') APPEND_WIRE_HEADER("Content-Type: %s\r\n", content_type); if (body_length > 0) APPEND_WIRE_HEADER("Content-Length: %zu\r\n", body_length); APPEND_WIRE_HEADER("\r\n"); #undef APPEND_WIRE_HEADER if (Seobeo_Handle_Queue( upstream, (const uint8 *)request_header, (uint32)header_length) != 0 || (body_length > 0 && Seobeo_Handle_Queue(upstream, (const uint8 *)body, (uint32)body_length) != 0) || Seobeo_Handle_Flush(upstream) != 0) { Seobeo_Handle_Destroy(upstream); send_proxy_error(client, 502, "Mercurial backend write failed"); return; } boolean response_started = FALSE; int64 last_progress = monotonic_milliseconds(); while (!response_started) { int read_result = Seobeo_Handle_Read(upstream); if (read_result == -2 || read_result < 0) { Seobeo_Handle_Destroy(upstream); send_proxy_error(client, 502, "Mercurial backend closed before responding"); return; } if (read_result > 0) last_progress = monotonic_milliseconds(); size_t header_size = find_http_header_length(upstream->read_buffer, upstream->read_buffer_len); if (header_size > 0) { (void)header_size; if (Seobeo_Handle_Queue( client, upstream->read_buffer, upstream->read_buffer_len) != 0 || Seobeo_Handle_Flush(client) != 0) { Seobeo_Handle_Destroy(upstream); return; } Seobeo_Handle_Consume(upstream, upstream->read_buffer_len); response_started = TRUE; break; } if (read_result == 0) { if (monotonic_milliseconds() - last_progress >= HG_STREAM_IDLE_TIMEOUT_MS) { Seobeo_Handle_Destroy(upstream); send_proxy_error(client, 504, "Mercurial backend response timed out"); return; } usleep(1000); } } while (TRUE) { int read_result = Seobeo_Handle_Read(upstream); if (read_result == -2) break; if (read_result < 0) break; if (read_result == 0) { if (monotonic_milliseconds() - last_progress >= HG_STREAM_IDLE_TIMEOUT_MS) { Seobeo_Log(SEOBEO_ERROR, "Mercurial response stream timed out\n"); break; } usleep(1000); continue; } last_progress = monotonic_milliseconds(); if (Seobeo_Handle_Queue( client, upstream->read_buffer, upstream->read_buffer_len) != 0 || Seobeo_Handle_Flush(client) != 0) break; Seobeo_Handle_Consume(upstream, upstream->read_buffer_len); } Seobeo_Handle_Destroy(upstream); } Seobeo_Request_Entry *GetReactHome(Seobeo_Request_Entry *request, Dowa_Arena *arena) { (void)request; size_t file_size = 0; char *html = Seobeo_Web_LoadFile("/index.html", &file_size); if (!html) return text_response(arena, "500", "text/plain", "Application shell unavailable"); Seobeo_Request_Entry *response = NULL; char *content_length = Dowa_Arena_Allocate(arena, 32); snprintf(content_length, 32, "%zu", file_size); Dowa_HashMap_Push_Arena(response, "status", "200", arena); Dowa_HashMap_Push_Arena(response, "content-type", "text/html", arena); Dowa_HashMap_Push_Arena(response, "body", html, arena); Dowa_HashMap_Push_Arena(response, "content-length", content_length, arena); return response; } int main(void) { Seobeo_Router_Init(); Seobeo_Router_Register("GET", "/", GetReactHome); Seobeo_Router_Register("GET", "/directories", GetReactHome); Seobeo_Router_Register("GET", "/directory", GetReactHome); Seobeo_Router_Register("GET", "/graph", GetReactHome); Seobeo_Router_Register("GET", "/changeset/:changeset_id", GetReactHome); Seobeo_Router_Register("GET", "/api/repo/list", ApiListDirectory); Seobeo_Router_Register("GET", "/api/repo/file", ApiGetFile); Seobeo_Router_Register("GET", "/api/repo/readme", ApiGetReadme); Seobeo_Router_Register("GET", "/api/graph/:graph_id", ApiGetGraph); Seobeo_Router_Register("GET", "/api/changeset/:changeset_id", ApiGetChangeset); Seobeo_Router_Register_Stream("GET", "/repo", StreamHgWireProtocol); Seobeo_Router_Register_Stream("POST", "/repo", StreamHgWireProtocol); printf("Starting on Port 6970...\n"); int result = Seobeo_Web_Server_Start("hg-web/src", "6970", SEOBEO_MODE_EDGE, 1); Seobeo_Router_Destroy(); return result; }