Mercurial
comparison hg-web/main.c @ 231:09a96dcb2b4c hg-web
[merge] Join existing hg-web branch head
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Sun, 02 Aug 2026 16:50:48 -0700 |
| parents | 3007ef5fc0ed |
| children | c5129452493e |
comparison
equal
deleted
inserted
replaced
| 217:7ef4c9d2a72d | 231:09a96dcb2b4c |
|---|---|
| 1 #include "seobeo/seobeo.h" | 1 #include "seobeo/seobeo.h" |
| 2 #include "dowa/dowa.h" | 2 #include "dowa/dowa.h" |
| 3 | |
| 4 #include <ctype.h> | |
| 5 #include <errno.h> | |
| 3 #include <stdio.h> | 6 #include <stdio.h> |
| 4 #include <stdlib.h> | 7 #include <stdlib.h> |
| 5 #include <string.h> | 8 #include <string.h> |
| 6 #include <ctype.h> | 9 #include <strings.h> |
| 10 #include <time.h> | |
| 7 #include <unistd.h> | 11 #include <unistd.h> |
| 8 #include <sys/socket.h> | |
| 9 #include <netinet/in.h> | |
| 10 #include <arpa/inet.h> | |
| 11 #include <netdb.h> | |
| 12 | 12 |
| 13 #define HG_SERVE_HOST "127.0.0.1" | 13 #define HG_SERVE_HOST "127.0.0.1" |
| 14 #define HG_SERVE_PORT "4444" | 14 #define HG_SERVE_PORT "4444" |
| 15 | 15 #define HG_API_TIMEOUT_MS 15000 |
| 16 #define MAX_PATH 4096 | 16 #define HG_STREAM_IDLE_TIMEOUT_MS 60000 |
| 17 | 17 #define MAX_PATH_LENGTH 4096 |
| 18 static char* sanitize_path(const char *input_path, Dowa_Arena *arena) | 18 #define MAX_WIRE_QUERY_LENGTH 8192 |
| 19 { | 19 #define MAX_WIRE_HEADER_LENGTH 8192 |
| 20 if (!input_path || strlen(input_path) == 0) | 20 |
| 21 { | 21 static const char *map_value_case_insensitive(Seobeo_Request_Entry *map, const char *key) |
| 22 char *empty = Dowa_Arena_Allocate(arena, 1); | 22 { |
| 23 empty[0] = '\0'; | 23 if (!map || !key) |
| 24 return empty; | 24 return NULL; |
| 25 } | 25 |
| 26 | 26 for (size_t i = 0; i < Dowa_Array_Length(map); i++) |
| 27 size_t len = strlen(input_path); | 27 { |
| 28 char *result = Dowa_Arena_Allocate(arena, len + 1); | 28 if (map[i].key && strcasecmp(map[i].key, key) == 0) |
| 29 size_t j = 0; | 29 return map[i].value; |
| 30 | 30 } |
| 31 for (size_t i = 0; i < len; i++) | 31 return NULL; |
| 32 { | 32 } |
| 33 if (input_path[i] == '.' && (i == 0 || input_path[i-1] == '/')) | 33 |
| 34 static char *arena_string(Dowa_Arena *arena, const char *value) | |
| 35 { | |
| 36 size_t length = strlen(value); | |
| 37 char *copy = Dowa_Arena_Allocate(arena, length + 1); | |
| 38 memcpy(copy, value, length + 1); | |
| 39 return copy; | |
| 40 } | |
| 41 | |
| 42 static Seobeo_Request_Entry *text_response( | |
| 43 Dowa_Arena *arena, | |
| 44 const char *status, | |
| 45 const char *content_type, | |
| 46 const char *body) | |
| 47 { | |
| 48 Seobeo_Request_Entry *response = NULL; | |
| 49 Dowa_HashMap_Push_Arena(response, "status", arena_string(arena, status), arena); | |
| 50 Dowa_HashMap_Push_Arena( | |
| 51 response, "content-type", arena_string(arena, content_type), arena); | |
| 52 Dowa_HashMap_Push_Arena(response, "body", arena_string(arena, body), arena); | |
| 53 return response; | |
| 54 } | |
| 55 | |
| 56 static boolean decode_url_component( | |
| 57 const char *encoded, | |
| 58 Dowa_Arena *arena, | |
| 59 char **decoded_out, | |
| 60 size_t *decoded_length_out) | |
| 61 { | |
| 62 if (!encoded || !decoded_out) | |
| 63 return FALSE; | |
| 64 | |
| 65 size_t encoded_length = strlen(encoded); | |
| 66 if (encoded_length >= MAX_PATH_LENGTH) | |
| 67 return FALSE; | |
| 68 | |
| 69 char *decoded = Dowa_Arena_Allocate(arena, encoded_length + 1); | |
| 70 size_t output_length = 0; | |
| 71 for (size_t i = 0; i < encoded_length; i++) | |
| 72 { | |
| 73 unsigned char value = (unsigned char)encoded[i]; | |
| 74 if (encoded[i] == '%') | |
| 34 { | 75 { |
| 35 if (i + 1 < len && input_path[i+1] == '.') | 76 if (i + 2 >= encoded_length || |
| 77 !isxdigit((unsigned char)encoded[i + 1]) || | |
| 78 !isxdigit((unsigned char)encoded[i + 2])) | |
| 79 return FALSE; | |
| 80 | |
| 81 char hex[3] = {encoded[i + 1], encoded[i + 2], '\0'}; | |
| 82 value = (unsigned char)strtoul(hex, NULL, 16); | |
| 83 i += 2; | |
| 84 if (value == '\0') | |
| 85 return FALSE; | |
| 86 } | |
| 87 decoded[output_length++] = (char)value; | |
| 88 } | |
| 89 decoded[output_length] = '\0'; | |
| 90 | |
| 91 *decoded_out = decoded; | |
| 92 if (decoded_length_out) | |
| 93 *decoded_length_out = output_length; | |
| 94 return TRUE; | |
| 95 } | |
| 96 | |
| 97 static boolean normalize_repository_path( | |
| 98 const char *encoded_path, | |
| 99 Dowa_Arena *arena, | |
| 100 char **normalized_out) | |
| 101 { | |
| 102 char *decoded = NULL; | |
| 103 size_t decoded_length = 0; | |
| 104 if (!decode_url_component(encoded_path ? encoded_path : "", arena, &decoded, &decoded_length)) | |
| 105 return FALSE; | |
| 106 | |
| 107 size_t start = 0; | |
| 108 size_t end = decoded_length; | |
| 109 if (start < end && decoded[start] == '/') | |
| 110 { | |
| 111 start++; | |
| 112 if (start < end && decoded[start] == '/') | |
| 113 return FALSE; | |
| 114 } | |
| 115 if (end > start && decoded[end - 1] == '/') | |
| 116 { | |
| 117 if (end - 1 > start && decoded[end - 2] == '/') | |
| 118 return FALSE; | |
| 119 end--; | |
| 120 } | |
| 121 | |
| 122 size_t segment_start = start; | |
| 123 for (size_t i = start; i <= end; i++) | |
| 124 { | |
| 125 boolean at_end = i == end; | |
| 126 unsigned char c = at_end ? '/' : (unsigned char)decoded[i]; | |
| 127 if (!at_end && (iscntrl(c) || c == '\\' || c == '?' || c == '#')) | |
| 128 return FALSE; | |
| 129 | |
| 130 if (c == '/') | |
| 131 { | |
| 132 size_t segment_length = i - segment_start; | |
| 133 if (segment_length == 0 && !at_end) | |
| 134 return FALSE; | |
| 135 if ((segment_length == 1 && decoded[segment_start] == '.') || | |
| 136 (segment_length == 2 && decoded[segment_start] == '.' && | |
| 137 decoded[segment_start + 1] == '.')) | |
| 138 return FALSE; | |
| 139 segment_start = i + 1; | |
| 140 } | |
| 141 } | |
| 142 | |
| 143 size_t normalized_length = end - start; | |
| 144 char *normalized = Dowa_Arena_Allocate(arena, normalized_length + 1); | |
| 145 memcpy(normalized, decoded + start, normalized_length); | |
| 146 normalized[normalized_length] = '\0'; | |
| 147 *normalized_out = normalized; | |
| 148 return TRUE; | |
| 149 } | |
| 150 | |
| 151 static boolean validate_revision(const char *revision) | |
| 152 { | |
| 153 if (!revision || revision[0] == '\0') | |
| 154 return FALSE; | |
| 155 if (strcmp(revision, "tip") == 0) | |
| 156 return TRUE; | |
| 157 | |
| 158 size_t length = strlen(revision); | |
| 159 if (length > 40) | |
| 160 return FALSE; | |
| 161 for (size_t i = 0; i < length; i++) | |
| 162 { | |
| 163 if (!isxdigit((unsigned char)revision[i])) | |
| 164 return FALSE; | |
| 165 } | |
| 166 return TRUE; | |
| 167 } | |
| 168 | |
| 169 static char *encode_repository_path(const char *path, Dowa_Arena *arena) | |
| 170 { | |
| 171 static const char hex[] = "0123456789ABCDEF"; | |
| 172 size_t length = strlen(path); | |
| 173 char *encoded = Dowa_Arena_Allocate(arena, length * 3 + 1); | |
| 174 size_t output = 0; | |
| 175 for (size_t i = 0; i < length; i++) | |
| 176 { | |
| 177 unsigned char c = (unsigned char)path[i]; | |
| 178 if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~' || c == '/') | |
| 179 encoded[output++] = (char)c; | |
| 180 else | |
| 181 { | |
| 182 encoded[output++] = '%'; | |
| 183 encoded[output++] = hex[c >> 4]; | |
| 184 encoded[output++] = hex[c & 0x0F]; | |
| 185 } | |
| 186 } | |
| 187 encoded[output] = '\0'; | |
| 188 return encoded; | |
| 189 } | |
| 190 | |
| 191 static boolean safe_header_value(const char *value, size_t maximum_length) | |
| 192 { | |
| 193 if (!value) | |
| 194 return TRUE; | |
| 195 size_t length = strlen(value); | |
| 196 return length <= maximum_length && | |
| 197 strchr(value, '\r') == NULL && | |
| 198 strchr(value, '\n') == NULL; | |
| 199 } | |
| 200 | |
| 201 static boolean extension_is(const char *extension, const char *expected) | |
| 202 { | |
| 203 return extension && strcasecmp(extension, expected) == 0; | |
| 204 } | |
| 205 | |
| 206 static const char *repository_file_content_type( | |
| 207 const char *path, | |
| 208 boolean *inline_preview, | |
| 209 boolean *sandbox_content) | |
| 210 { | |
| 211 const char *extension = strrchr(path, '.'); | |
| 212 *inline_preview = TRUE; | |
| 213 *sandbox_content = FALSE; | |
| 214 | |
| 215 if (extension_is(extension, ".png")) return "image/png"; | |
| 216 if (extension_is(extension, ".jpg") || | |
| 217 extension_is(extension, ".jpeg")) return "image/jpeg"; | |
| 218 if (extension_is(extension, ".gif")) return "image/gif"; | |
| 219 if (extension_is(extension, ".webp")) return "image/webp"; | |
| 220 if (extension_is(extension, ".avif")) return "image/avif"; | |
| 221 if (extension_is(extension, ".bmp")) return "image/bmp"; | |
| 222 if (extension_is(extension, ".ico")) return "image/x-icon"; | |
| 223 if (extension_is(extension, ".svg")) | |
| 224 { | |
| 225 *sandbox_content = TRUE; | |
| 226 return "image/svg+xml"; | |
| 227 } | |
| 228 if (extension_is(extension, ".mp4") || | |
| 229 extension_is(extension, ".m4v")) return "video/mp4"; | |
| 230 if (extension_is(extension, ".webm")) return "video/webm"; | |
| 231 if (extension_is(extension, ".mov")) return "video/quicktime"; | |
| 232 if (extension_is(extension, ".ogv")) return "video/ogg"; | |
| 233 if (extension_is(extension, ".mp3")) return "audio/mpeg"; | |
| 234 if (extension_is(extension, ".wav")) return "audio/wav"; | |
| 235 if (extension_is(extension, ".ogg") || | |
| 236 extension_is(extension, ".oga")) return "audio/ogg"; | |
| 237 if (extension_is(extension, ".flac")) return "audio/flac"; | |
| 238 if (extension_is(extension, ".m4a")) return "audio/mp4"; | |
| 239 if (extension_is(extension, ".aac")) return "audio/aac"; | |
| 240 if (extension_is(extension, ".pdf")) return "application/pdf"; | |
| 241 if (extension_is(extension, ".wasm")) return "application/wasm"; | |
| 242 | |
| 243 *inline_preview = FALSE; | |
| 244 if (extension_is(extension, ".md") || | |
| 245 extension_is(extension, ".markdown")) return "text/markdown; charset=utf-8"; | |
| 246 if (extension_is(extension, ".txt") || | |
| 247 extension_is(extension, ".log") || | |
| 248 extension_is(extension, ".c") || | |
| 249 extension_is(extension, ".h") || | |
| 250 extension_is(extension, ".cc") || | |
| 251 extension_is(extension, ".cpp") || | |
| 252 extension_is(extension, ".js") || | |
| 253 extension_is(extension, ".jsx") || | |
| 254 extension_is(extension, ".ts") || | |
| 255 extension_is(extension, ".tsx") || | |
| 256 extension_is(extension, ".css") || | |
| 257 extension_is(extension, ".html") || | |
| 258 extension_is(extension, ".htm") || | |
| 259 extension_is(extension, ".xml") || | |
| 260 extension_is(extension, ".json") || | |
| 261 extension_is(extension, ".yaml") || | |
| 262 extension_is(extension, ".yml") || | |
| 263 extension_is(extension, ".toml") || | |
| 264 extension_is(extension, ".sh") || | |
| 265 extension_is(extension, ".py") || | |
| 266 extension_is(extension, ".rs") || | |
| 267 extension_is(extension, ".go")) | |
| 268 return "text/plain; charset=utf-8"; | |
| 269 return "application/octet-stream"; | |
| 270 } | |
| 271 | |
| 272 static Seobeo_Client_Response *hg_proxy_request( | |
| 273 const char *method, | |
| 274 const char *path, | |
| 275 const char *request_body, | |
| 276 size_t request_body_length, | |
| 277 const char *hg_argument, | |
| 278 const char *accept) | |
| 279 { | |
| 280 char url[MAX_PATH_LENGTH]; | |
| 281 int url_length = snprintf( | |
| 282 url, sizeof(url), "http://%s:%s%s", HG_SERVE_HOST, HG_SERVE_PORT, path); | |
| 283 if (url_length < 0 || (size_t)url_length >= sizeof(url)) | |
| 284 return NULL; | |
| 285 | |
| 286 Seobeo_Client_Request *request = Seobeo_Client_Request_Create(url); | |
| 287 if (!request) | |
| 288 return NULL; | |
| 289 | |
| 290 Seobeo_Client_Request_Set_Method(request, method); | |
| 291 Seobeo_Client_Request_Add_Header_Map(request, "User-Agent", "Seobeo/1.0"); | |
| 292 Seobeo_Client_Request_Add_Header_Map( | |
| 293 request, "Accept", accept ? accept : "application/json"); | |
| 294 Seobeo_Client_Request_Set_Timeout_Milliseconds(request, HG_API_TIMEOUT_MS); | |
| 295 | |
| 296 if (hg_argument && hg_argument[0] != '\0') | |
| 297 { | |
| 298 if (!safe_header_value(hg_argument, MAX_WIRE_HEADER_LENGTH)) | |
| 299 { | |
| 300 Seobeo_Client_Request_Destroy(request); | |
| 301 return NULL; | |
| 302 } | |
| 303 Seobeo_Client_Request_Add_Header_Map(request, "x-hgarg-1", hg_argument); | |
| 304 } | |
| 305 | |
| 306 if (request_body && request_body_length > 0) | |
| 307 Seobeo_Client_Request_Set_Body(request, request_body, request_body_length); | |
| 308 | |
| 309 Seobeo_Client_Response *response = Seobeo_Client_Request_Execute(request); | |
| 310 Seobeo_Client_Request_Destroy(request); | |
| 311 return response; | |
| 312 } | |
| 313 | |
| 314 static Seobeo_Request_Entry *forward_hg_response( | |
| 315 Seobeo_Client_Response *hg_response, | |
| 316 const char *default_content_type, | |
| 317 const char *override_content_type, | |
| 318 Dowa_Arena *arena) | |
| 319 { | |
| 320 if (!hg_response) | |
| 321 return text_response( | |
| 322 arena, "502", "application/json", "{\"error\":\"Mercurial backend unavailable\"}"); | |
| 323 | |
| 324 const char *upstream_content_type = | |
| 325 map_value_case_insensitive(hg_response->headers, "Content-Type"); | |
| 326 const char *upstream_or_default_content_type = | |
| 327 override_content_type | |
| 328 ? override_content_type | |
| 329 : upstream_content_type ? upstream_content_type : default_content_type; | |
| 330 if (!upstream_or_default_content_type) | |
| 331 upstream_or_default_content_type = "application/octet-stream"; | |
| 332 char *content_type = arena_string(arena, upstream_or_default_content_type); | |
| 333 | |
| 334 char *status = Dowa_Arena_Allocate(arena, 8); | |
| 335 snprintf(status, 8, "%d", hg_response->status_code); | |
| 336 | |
| 337 size_t body_length = hg_response->body ? hg_response->body_length : 0; | |
| 338 char *body = Dowa_Arena_Allocate(arena, body_length + 1); | |
| 339 if (body_length > 0) | |
| 340 memcpy(body, hg_response->body, body_length); | |
| 341 body[body_length] = '\0'; | |
| 342 char *content_length = Dowa_Arena_Allocate(arena, 32); | |
| 343 snprintf(content_length, 32, "%zu", body_length); | |
| 344 | |
| 345 Seobeo_Request_Entry *response = NULL; | |
| 346 Dowa_HashMap_Push_Arena(response, "status", status, arena); | |
| 347 Dowa_HashMap_Push_Arena(response, "content-type", content_type, arena); | |
| 348 Dowa_HashMap_Push_Arena(response, "body", body, arena); | |
| 349 Dowa_HashMap_Push_Arena(response, "content-length", content_length, arena); | |
| 350 Seobeo_Client_Response_Destroy(hg_response); | |
| 351 return response; | |
| 352 } | |
| 353 | |
| 354 Seobeo_Request_Entry *ApiListDirectory(Seobeo_Request_Entry *request, Dowa_Arena *arena) | |
| 355 { | |
| 356 const char *encoded_path = map_value_case_insensitive(request, "query_path"); | |
| 357 char *path = NULL; | |
| 358 if (!normalize_repository_path(encoded_path ? encoded_path : "", arena, &path)) | |
| 359 return text_response(arena, "400", "application/json", "{\"error\":\"Invalid repository path\"}"); | |
| 360 | |
| 361 char *encoded = encode_repository_path(path, arena); | |
| 362 char hg_path[MAX_PATH_LENGTH]; | |
| 363 int length = snprintf( | |
| 364 hg_path, | |
| 365 sizeof(hg_path), | |
| 366 encoded[0] ? "/file/tip/%s?style=json" : "/file/tip/?style=json", | |
| 367 encoded); | |
| 368 if (length < 0 || (size_t)length >= sizeof(hg_path)) | |
| 369 return text_response(arena, "400", "application/json", "{\"error\":\"Repository path is too long\"}"); | |
| 370 | |
| 371 return forward_hg_response( | |
| 372 hg_proxy_request("GET", hg_path, NULL, 0, NULL, "application/json"), | |
| 373 "application/json", | |
| 374 NULL, | |
| 375 arena); | |
| 376 } | |
| 377 | |
| 378 Seobeo_Request_Entry *ApiGetFile(Seobeo_Request_Entry *request, Dowa_Arena *arena) | |
| 379 { | |
| 380 const char *encoded_path = map_value_case_insensitive(request, "query_path"); | |
| 381 char *path = NULL; | |
| 382 if (!normalize_repository_path(encoded_path ? encoded_path : "", arena, &path) || | |
| 383 path[0] == '\0') | |
| 384 return text_response(arena, "400", "text/plain", "A valid file path is required"); | |
| 385 | |
| 386 char *encoded = encode_repository_path(path, arena); | |
| 387 char hg_path[MAX_PATH_LENGTH]; | |
| 388 int length = snprintf(hg_path, sizeof(hg_path), "/raw-file/tip/%s", encoded); | |
| 389 if (length < 0 || (size_t)length >= sizeof(hg_path)) | |
| 390 return text_response(arena, "400", "text/plain", "File path is too long"); | |
| 391 | |
| 392 Seobeo_Client_Response *hg_response = | |
| 393 hg_proxy_request("GET", hg_path, NULL, 0, NULL, "application/octet-stream"); | |
| 394 if (!hg_response) | |
| 395 return forward_hg_response(NULL, "application/json", NULL, arena); | |
| 396 | |
| 397 boolean inline_preview = FALSE; | |
| 398 boolean sandbox_content = FALSE; | |
| 399 const char *content_type = | |
| 400 repository_file_content_type(path, &inline_preview, &sandbox_content); | |
| 401 Seobeo_Request_Entry *response = forward_hg_response( | |
| 402 hg_response, "application/octet-stream", content_type, arena); | |
| 403 Dowa_HashMap_Push_Arena( | |
| 404 response, | |
| 405 "Content-Disposition", | |
| 406 inline_preview ? "inline" : "attachment", | |
| 407 arena); | |
| 408 Dowa_HashMap_Push_Arena( | |
| 409 response, "X-Content-Type-Options", "nosniff", arena); | |
| 410 if (sandbox_content) | |
| 411 Dowa_HashMap_Push_Arena( | |
| 412 response, "Content-Security-Policy", "sandbox", arena); | |
| 413 return response; | |
| 414 } | |
| 415 | |
| 416 Seobeo_Request_Entry *ApiGetReadme(Seobeo_Request_Entry *request, Dowa_Arena *arena) | |
| 417 { | |
| 418 const char *encoded_path = map_value_case_insensitive(request, "query_path"); | |
| 419 char *directory = NULL; | |
| 420 if (!normalize_repository_path(encoded_path ? encoded_path : "", arena, &directory)) | |
| 421 return text_response(arena, "400", "text/plain", "Invalid repository path"); | |
| 422 | |
| 423 size_t readme_length = strlen(directory) + strlen("/README.md") + 1; | |
| 424 if (readme_length >= MAX_PATH_LENGTH) | |
| 425 return text_response(arena, "400", "text/plain", "README path is too long"); | |
| 426 | |
| 427 char *readme_path = Dowa_Arena_Allocate(arena, readme_length); | |
| 428 snprintf( | |
| 429 readme_path, | |
| 430 readme_length, | |
| 431 directory[0] ? "%s/README.md" : "README.md", | |
| 432 directory); | |
| 433 char *encoded = encode_repository_path(readme_path, arena); | |
| 434 | |
| 435 char hg_path[MAX_PATH_LENGTH]; | |
| 436 int length = snprintf(hg_path, sizeof(hg_path), "/raw-file/tip/%s", encoded); | |
| 437 if (length < 0 || (size_t)length >= sizeof(hg_path)) | |
| 438 return text_response(arena, "400", "text/plain", "README path is too long"); | |
| 439 | |
| 440 Seobeo_Client_Response *hg_response = | |
| 441 hg_proxy_request("GET", hg_path, NULL, 0, NULL, "text/markdown"); | |
| 442 if (hg_response && hg_response->status_code == HTTP_NOT_FOUND) | |
| 443 { | |
| 444 Seobeo_Client_Response_Destroy(hg_response); | |
| 445 return text_response(arena, "204", "text/markdown", ""); | |
| 446 } | |
| 447 return forward_hg_response( | |
| 448 hg_response, "text/markdown", "text/markdown; charset=utf-8", arena); | |
| 449 } | |
| 450 | |
| 451 Seobeo_Request_Entry *ApiGetGraph(Seobeo_Request_Entry *request, Dowa_Arena *arena) | |
| 452 { | |
| 453 const char *graph_id = map_value_case_insensitive(request, ":graph_id"); | |
| 454 if (!validate_revision(graph_id)) | |
| 455 return text_response(arena, "400", "application/json", "{\"error\":\"Invalid graph revision\"}"); | |
| 456 | |
| 457 const char *encoded_graph_top = | |
| 458 map_value_case_insensitive(request, "query_graphtop"); | |
| 459 char *graph_top = NULL; | |
| 460 if (encoded_graph_top) | |
| 461 { | |
| 462 if (!decode_url_component(encoded_graph_top, arena, &graph_top, NULL) || | |
| 463 !validate_revision(graph_top)) | |
| 464 return text_response(arena, "400", "application/json", "{\"error\":\"Invalid graph top revision\"}"); | |
| 465 } | |
| 466 | |
| 467 char hg_path[MAX_PATH_LENGTH]; | |
| 468 int length = graph_top | |
| 469 ? snprintf( | |
| 470 hg_path, | |
| 471 sizeof(hg_path), | |
| 472 "/graph/%s?graphtop=%s&style=json", | |
| 473 graph_id, | |
| 474 graph_top) | |
| 475 : snprintf(hg_path, sizeof(hg_path), "/graph/%s?style=json", graph_id); | |
| 476 if (length < 0 || (size_t)length >= sizeof(hg_path)) | |
| 477 return text_response(arena, "400", "application/json", "{\"error\":\"Graph request is too long\"}"); | |
| 478 | |
| 479 return forward_hg_response( | |
| 480 hg_proxy_request("GET", hg_path, NULL, 0, NULL, "application/json"), | |
| 481 "application/json", | |
| 482 NULL, | |
| 483 arena); | |
| 484 } | |
| 485 | |
| 486 Seobeo_Request_Entry *ApiGetChangeset(Seobeo_Request_Entry *request, Dowa_Arena *arena) | |
| 487 { | |
| 488 const char *changeset_id = map_value_case_insensitive(request, ":changeset_id"); | |
| 489 if (!validate_revision(changeset_id)) | |
| 490 return text_response(arena, "400", "application/json", "{\"error\":\"Invalid changeset revision\"}"); | |
| 491 | |
| 492 char hg_path[128]; | |
| 493 int length = snprintf(hg_path, sizeof(hg_path), "/json-rev/%s", changeset_id); | |
| 494 if (length < 0 || (size_t)length >= sizeof(hg_path)) | |
| 495 return text_response(arena, "400", "application/json", "{\"error\":\"Changeset request is too long\"}"); | |
| 496 | |
| 497 return forward_hg_response( | |
| 498 hg_proxy_request("GET", hg_path, NULL, 0, NULL, "application/json"), | |
| 499 "application/json", | |
| 500 NULL, | |
| 501 arena); | |
| 502 } | |
| 503 | |
| 504 static int64_t monotonic_milliseconds(void) | |
| 505 { | |
| 506 struct timespec now; | |
| 507 clock_gettime(CLOCK_MONOTONIC, &now); | |
| 508 return (int64_t)now.tv_sec * 1000 + now.tv_nsec / 1000000; | |
| 509 } | |
| 510 | |
| 511 static size_t find_http_header_length(const uint8 *buffer, size_t length) | |
| 512 { | |
| 513 if (!buffer || length < 4) | |
| 514 return 0; | |
| 515 for (size_t i = 0; i + 3 < length; i++) | |
| 516 { | |
| 517 if (buffer[i] == '\r' && buffer[i + 1] == '\n' && | |
| 518 buffer[i + 2] == '\r' && buffer[i + 3] == '\n') | |
| 519 return i + 4; | |
| 520 } | |
| 521 return 0; | |
| 522 } | |
| 523 | |
| 524 static void send_proxy_error(Seobeo_Handle *client, int status, const char *message) | |
| 525 { | |
| 526 const char *reason = status == 504 ? "Gateway Timeout" : "Bad Gateway"; | |
| 527 char response[512]; | |
| 528 int length = snprintf( | |
| 529 response, | |
| 530 sizeof(response), | |
| 531 "HTTP/1.1 %d %s\r\n" | |
| 532 "Content-Type: text/plain\r\n" | |
| 533 "Content-Length: %zu\r\n" | |
| 534 "Connection: close\r\n" | |
| 535 "\r\n" | |
| 536 "%s", | |
| 537 status, | |
| 538 reason, | |
| 539 strlen(message), | |
| 540 message); | |
| 541 if (length > 0 && (size_t)length < sizeof(response)) | |
| 542 { | |
| 543 Seobeo_Handle_Queue(client, (const uint8 *)response, (uint32)length); | |
| 544 Seobeo_Handle_Flush(client); | |
| 545 } | |
| 546 } | |
| 547 | |
| 548 static boolean parse_content_length(const char *value, size_t *length_out) | |
| 549 { | |
| 550 if (!value || !length_out || value[0] == '\0') | |
| 551 return FALSE; | |
| 552 errno = 0; | |
| 553 char *end = NULL; | |
| 554 unsigned long long parsed = strtoull(value, &end, 10); | |
| 555 if (errno != 0 || !end || *end != '\0' || parsed > SIZE_MAX) | |
| 556 return FALSE; | |
| 557 *length_out = (size_t)parsed; | |
| 558 return TRUE; | |
| 559 } | |
| 560 | |
| 561 void StreamHgWireProtocol( | |
| 562 Seobeo_Handle *client, | |
| 563 Seobeo_Request_Entry *request, | |
| 564 Dowa_Arena *arena) | |
| 565 { | |
| 566 (void)arena; | |
| 567 const char *method = map_value_case_insensitive(request, "HTTP_Method"); | |
| 568 const char *query = map_value_case_insensitive(request, "QueryString"); | |
| 569 const char *body = map_value_case_insensitive(request, "Body"); | |
| 570 const char *content_length_value = | |
| 571 map_value_case_insensitive(request, "Content-Length"); | |
| 572 const char *content_type = map_value_case_insensitive(request, "Content-Type"); | |
| 573 const char *hg_argument = map_value_case_insensitive(request, "x-hgarg-1"); | |
| 574 | |
| 575 if (!method || !query || | |
| 576 (strcmp(method, "GET") != 0 && strcmp(method, "POST") != 0) || | |
| 577 !safe_header_value(query, MAX_WIRE_QUERY_LENGTH) || | |
| 578 !safe_header_value(hg_argument, MAX_WIRE_HEADER_LENGTH) || | |
| 579 !safe_header_value(content_type, 256)) | |
| 580 { | |
| 581 send_proxy_error(client, 502, "Invalid Mercurial proxy request"); | |
| 582 return; | |
| 583 } | |
| 584 | |
| 585 size_t body_length = 0; | |
| 586 if (content_length_value && | |
| 587 !parse_content_length(content_length_value, &body_length)) | |
| 588 { | |
| 589 send_proxy_error(client, 502, "Invalid Mercurial request length"); | |
| 590 return; | |
| 591 } | |
| 592 if (body_length > 0 && !body) | |
| 593 { | |
| 594 send_proxy_error(client, 502, "Missing Mercurial request body"); | |
| 595 return; | |
| 596 } | |
| 597 if (body_length > UINT32_MAX) | |
| 598 { | |
| 599 send_proxy_error(client, 502, "Mercurial request body is too large"); | |
| 600 return; | |
| 601 } | |
| 602 | |
| 603 Seobeo_Handle *upstream = | |
| 604 Seobeo_Stream_Handle_Client_Create(HG_SERVE_HOST, HG_SERVE_PORT, FALSE); | |
| 605 if (!upstream || upstream->socket < 0) | |
| 606 { | |
| 607 if (upstream) | |
| 608 Seobeo_Handle_Destroy(upstream); | |
| 609 send_proxy_error(client, 502, "Mercurial backend unavailable"); | |
| 610 return; | |
| 611 } | |
| 612 | |
| 613 char request_header[16384]; | |
| 614 int header_length = snprintf( | |
| 615 request_header, | |
| 616 sizeof(request_header), | |
| 617 "%s /?%s HTTP/1.1\r\n" | |
| 618 "Host: %s:%s\r\n" | |
| 619 "User-Agent: Seobeo/1.0\r\n" | |
| 620 "Connection: close\r\n", | |
| 621 method, | |
| 622 query, | |
| 623 HG_SERVE_HOST, | |
| 624 HG_SERVE_PORT); | |
| 625 if (header_length < 0 || (size_t)header_length >= sizeof(request_header)) | |
| 626 { | |
| 627 Seobeo_Handle_Destroy(upstream); | |
| 628 send_proxy_error(client, 502, "Mercurial request headers are too large"); | |
| 629 return; | |
| 630 } | |
| 631 | |
| 632 #define APPEND_WIRE_HEADER(...) \ | |
| 633 do { \ | |
| 634 int appended = snprintf( \ | |
| 635 request_header + header_length, \ | |
| 636 sizeof(request_header) - (size_t)header_length, \ | |
| 637 __VA_ARGS__); \ | |
| 638 if (appended < 0 || (size_t)appended >= sizeof(request_header) - (size_t)header_length) { \ | |
| 639 Seobeo_Handle_Destroy(upstream); \ | |
| 640 send_proxy_error(client, 502, "Mercurial request headers are too large"); \ | |
| 641 return; \ | |
| 642 } \ | |
| 643 header_length += appended; \ | |
| 644 } while (0) | |
| 645 | |
| 646 if (hg_argument && hg_argument[0] != '\0') | |
| 647 APPEND_WIRE_HEADER("x-hgarg-1: %s\r\n", hg_argument); | |
| 648 if (content_type && content_type[0] != '\0') | |
| 649 APPEND_WIRE_HEADER("Content-Type: %s\r\n", content_type); | |
| 650 if (body_length > 0) | |
| 651 APPEND_WIRE_HEADER("Content-Length: %zu\r\n", body_length); | |
| 652 APPEND_WIRE_HEADER("\r\n"); | |
| 653 #undef APPEND_WIRE_HEADER | |
| 654 | |
| 655 if (Seobeo_Handle_Queue( | |
| 656 upstream, (const uint8 *)request_header, (uint32)header_length) != 0 || | |
| 657 (body_length > 0 && | |
| 658 Seobeo_Handle_Queue(upstream, (const uint8 *)body, (uint32)body_length) != 0) || | |
| 659 Seobeo_Handle_Flush(upstream) != 0) | |
| 660 { | |
| 661 Seobeo_Handle_Destroy(upstream); | |
| 662 send_proxy_error(client, 502, "Mercurial backend write failed"); | |
| 663 return; | |
| 664 } | |
| 665 | |
| 666 boolean response_started = FALSE; | |
| 667 int64_t last_progress = monotonic_milliseconds(); | |
| 668 while (!response_started) | |
| 669 { | |
| 670 int read_result = Seobeo_Handle_Read(upstream); | |
| 671 if (read_result == -2 || read_result < 0) | |
| 672 { | |
| 673 Seobeo_Handle_Destroy(upstream); | |
| 674 send_proxy_error(client, 502, "Mercurial backend closed before responding"); | |
| 675 return; | |
| 676 } | |
| 677 if (read_result > 0) | |
| 678 last_progress = monotonic_milliseconds(); | |
| 679 | |
| 680 size_t header_size = | |
| 681 find_http_header_length(upstream->read_buffer, upstream->read_buffer_len); | |
| 682 if (header_size > 0) | |
| 683 { | |
| 684 (void)header_size; | |
| 685 if (Seobeo_Handle_Queue( | |
| 686 client, upstream->read_buffer, upstream->read_buffer_len) != 0 || | |
| 687 Seobeo_Handle_Flush(client) != 0) | |
| 36 { | 688 { |
| 37 // Skip ".." | 689 Seobeo_Handle_Destroy(upstream); |
| 38 i++; | 690 return; |
| 39 continue; | |
| 40 } | 691 } |
| 41 // Skip "." | 692 Seobeo_Handle_Consume(upstream, upstream->read_buffer_len); |
| 693 response_started = TRUE; | |
| 694 break; | |
| 695 } | |
| 696 | |
| 697 if (read_result == 0) | |
| 698 { | |
| 699 if (monotonic_milliseconds() - last_progress >= HG_STREAM_IDLE_TIMEOUT_MS) | |
| 700 { | |
| 701 Seobeo_Handle_Destroy(upstream); | |
| 702 send_proxy_error(client, 504, "Mercurial backend response timed out"); | |
| 703 return; | |
| 704 } | |
| 705 usleep(1000); | |
| 706 } | |
| 707 } | |
| 708 | |
| 709 while (TRUE) | |
| 710 { | |
| 711 int read_result = Seobeo_Handle_Read(upstream); | |
| 712 if (read_result == -2) | |
| 713 break; | |
| 714 if (read_result < 0) | |
| 715 break; | |
| 716 if (read_result == 0) | |
| 717 { | |
| 718 if (monotonic_milliseconds() - last_progress >= HG_STREAM_IDLE_TIMEOUT_MS) | |
| 719 { | |
| 720 Seobeo_Log(SEOBEO_ERROR, "Mercurial response stream timed out\n"); | |
| 721 break; | |
| 722 } | |
| 723 usleep(1000); | |
| 42 continue; | 724 continue; |
| 43 } | 725 } |
| 44 result[j++] = input_path[i]; | 726 |
| 45 } | 727 last_progress = monotonic_milliseconds(); |
| 46 result[j] = '\0'; | 728 if (Seobeo_Handle_Queue( |
| 47 | 729 client, upstream->read_buffer, upstream->read_buffer_len) != 0 || |
| 48 // Remove leading/trailing slashes | 730 Seobeo_Handle_Flush(client) != 0) |
| 49 while (result[0] == '/') | |
| 50 memmove(result, result + 1, strlen(result)); | |
| 51 while (j > 0 && result[j-1] == '/') | |
| 52 result[--j] = '\0'; | |
| 53 | |
| 54 return result; | |
| 55 } | |
| 56 | |
| 57 Seobeo_Client_Response *hg_proxy_request( | |
| 58 const char *method, | |
| 59 const char *path, | |
| 60 const char *req_body, | |
| 61 const char *hg_custom) | |
| 62 { | |
| 63 char full_path[MAX_PATH]; | |
| 64 snprintf(full_path, MAX_PATH, "http://%s:%s%s", HG_SERVE_HOST, HG_SERVE_PORT, path); | |
| 65 Seobeo_Log(SEOBEO_DEBUG, "HG Proxy PATH %s\n", full_path); | |
| 66 Seobeo_Client_Request *p_req = Seobeo_Client_Request_Create(full_path); | |
| 67 Seobeo_Client_Request_Set_Method(p_req, method); | |
| 68 Seobeo_Client_Request_Add_Header_Array(p_req, "User-Agent: Seobeo/1.0"); | |
| 69 Seobeo_Client_Request_Add_Header_Array(p_req, "Accept: application/json"); | |
| 70 | |
| 71 if (hg_custom && hg_custom[0] != '\0') | |
| 72 { | |
| 73 char buffer[1024]; | |
| 74 snprintf(buffer, 1024, "x-hgarg-1: %s", hg_custom); | |
| 75 Seobeo_Client_Request_Add_Header_Array(p_req, buffer); | |
| 76 Seobeo_Log(SEOBEO_DEBUG, "HG CUSTOM %s\n", buffer); | |
| 77 } | |
| 78 | |
| 79 if (req_body) | |
| 80 Seobeo_Client_Request_Set_Body(p_req, req_body, strlen(req_body)); | |
| 81 Seobeo_Client_Response *p_resp = Seobeo_Client_Request_Execute(p_req); | |
| 82 Seobeo_Client_Request_Destroy(p_req); | |
| 83 return p_resp; | |
| 84 } | |
| 85 | |
| 86 Seobeo_Request_Entry* ApiListDirectory(Seobeo_Request_Entry *req, Dowa_Arena *arena) | |
| 87 { | |
| 88 Seobeo_Request_Entry *resp = NULL; | |
| 89 | |
| 90 void *path_kv = Dowa_HashMap_Get_Ptr(req, "query_path"); | |
| 91 const char *rel_path = path_kv ? ((Seobeo_Request_Entry*)path_kv)->value : ""; | |
| 92 | |
| 93 char *decoded_path = Dowa_Arena_Allocate(arena, strlen(rel_path) + 1); | |
| 94 Seobeo_Url_Decode(decoded_path, rel_path); | |
| 95 | |
| 96 char *safe_path = sanitize_path(decoded_path, arena); | |
| 97 | |
| 98 Seobeo_Log(SEOBEO_INFO, "ApiListDirectory: safe_path='%s'\n", safe_path); | |
| 99 | |
| 100 char hg_path[MAX_PATH]; | |
| 101 if (strlen(safe_path) > 0) | |
| 102 snprintf(hg_path, sizeof(hg_path), "/file/tip/%s?style=json", safe_path); | |
| 103 else | |
| 104 snprintf(hg_path, sizeof(hg_path), "/file/tip/?style=json"); | |
| 105 | |
| 106 Seobeo_Client_Response *hg_response = hg_proxy_request("GET", hg_path, NULL, NULL); | |
| 107 | |
| 108 Seobeo_Log(SEOBEO_DEBUG, "ApiListDirectory: status=%i body_len=%zu\n", hg_response->status_code, hg_response->body_length); | |
| 109 | |
| 110 if (hg_response->status_code != 200) | |
| 111 { | |
| 112 Seobeo_Log(SEOBEO_DEBUG, "Failed to get directory from hg serve\n"); | |
| 113 Dowa_HashMap_Push_Arena(resp, "status", "502", arena); | |
| 114 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); | |
| 115 Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Failed to connect to hg serve\"}", arena); | |
| 116 return resp; | |
| 117 } | |
| 118 | |
| 119 char *temp1 = Dowa_Arena_Copy(arena, hg_response->body, hg_response->body_length); | |
| 120 char *temp2 = Dowa_Arena_Allocate(arena, 256); | |
| 121 snprintf(temp2, 256, "%zu", hg_response->body_length); | |
| 122 | |
| 123 Dowa_HashMap_Push_Arena(resp, "status", "200", arena); | |
| 124 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); | |
| 125 Dowa_HashMap_Push_Arena(resp, "body", temp1, arena); | |
| 126 Dowa_HashMap_Push_Arena(resp, "content-length", temp2, arena); | |
| 127 return resp; | |
| 128 } | |
| 129 | |
| 130 Seobeo_Request_Entry* ApiGetFile(Seobeo_Request_Entry *req, Dowa_Arena *arena) | |
| 131 { | |
| 132 Seobeo_Request_Entry *resp = NULL; | |
| 133 | |
| 134 void *path_kv = Dowa_HashMap_Get_Ptr(req, "query_path"); | |
| 135 const char *rel_path = path_kv ? ((Seobeo_Request_Entry*)path_kv)->value : ""; | |
| 136 char *decoded_path = Dowa_Arena_Allocate(arena, strlen(rel_path) + 1); | |
| 137 Seobeo_Url_Decode(decoded_path, rel_path); | |
| 138 char *safe_path = sanitize_path(decoded_path, arena); | |
| 139 | |
| 140 Seobeo_Log(SEOBEO_INFO, "ApiGetFile: safe_path='%s'\n", safe_path); | |
| 141 | |
| 142 if (strlen(safe_path) == 0) | |
| 143 { | |
| 144 Dowa_HashMap_Push_Arena(resp, "status", "400", arena); | |
| 145 Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); | |
| 146 Dowa_HashMap_Push_Arena(resp, "body", "File path required", arena); | |
| 147 return resp; | |
| 148 } | |
| 149 | |
| 150 char hg_path[MAX_PATH]; | |
| 151 snprintf(hg_path, sizeof(hg_path), "/raw-file/tip/%s", safe_path); | |
| 152 Seobeo_Client_Response *hg_response = hg_proxy_request("GET", hg_path, NULL, NULL); | |
| 153 | |
| 154 Seobeo_Log(SEOBEO_DEBUG, "ApiGetFile: status=%i body_len=%zu\n", hg_response->status_code, hg_response->body_length); | |
| 155 | |
| 156 char status[4]; | |
| 157 snprintf(status, 4, "%i", hg_response->status_code); | |
| 158 | |
| 159 if (!hg_response->body) | |
| 160 { | |
| 161 Dowa_HashMap_Push_Arena(resp, "status", "502", arena); | |
| 162 Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); | |
| 163 Dowa_HashMap_Push_Arena(resp, "body", "Failed to connect to hg serve", arena); | |
| 164 return resp; | |
| 165 } | |
| 166 | |
| 167 if (hg_response->status_code != 200) | |
| 168 { | |
| 169 Seobeo_Log(SEOBEO_DEBUG, "ApiGetFile: error hg_response: %s\n", hg_response->body); | |
| 170 Dowa_HashMap_Push_Arena(resp, "status", status, arena); | |
| 171 Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); | |
| 172 Dowa_HashMap_Push_Arena(resp, "body", hg_response->body, arena); | |
| 173 return resp; | |
| 174 } | |
| 175 | |
| 176 | |
| 177 char *temp1 = Dowa_Arena_Copy(arena, hg_response->body, hg_response->body_length); | |
| 178 char *temp2 = Dowa_Arena_Allocate(arena, 256); | |
| 179 snprintf(temp2, 256, "%zu", hg_response->body_length); | |
| 180 | |
| 181 Dowa_HashMap_Push_Arena(resp, "status", "200", arena); | |
| 182 Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); | |
| 183 Dowa_HashMap_Push_Arena(resp, "body", temp1, arena); | |
| 184 Dowa_HashMap_Push_Arena(resp, "content-length", temp2, arena); | |
| 185 | |
| 186 return resp; | |
| 187 } | |
| 188 | |
| 189 Seobeo_Request_Entry* ApiGetReadme(Seobeo_Request_Entry *req, Dowa_Arena *arena) { | |
| 190 return ApiGetFile(req, arena); | |
| 191 } | |
| 192 | |
| 193 // Streaming handler for hg wire protocol - pipes data directly without buffering | |
| 194 void StreamHgWireProtocol(Seobeo_Handle *p_client, Seobeo_Request_Entry *req, Dowa_Arena *arena) | |
| 195 { | |
| 196 void *method_kv = Dowa_HashMap_Get_Ptr(req, "HTTP_Method"); | |
| 197 const char *method = method_kv ? ((Seobeo_Request_Entry*)method_kv)->value : "GET"; | |
| 198 | |
| 199 void *query_kv = Dowa_HashMap_Get_Ptr(req, "QueryString"); | |
| 200 const char *query_string = query_kv ? ((Seobeo_Request_Entry*)query_kv)->value : ""; | |
| 201 | |
| 202 void *body_kv = Dowa_HashMap_Get_Ptr(req, "Body"); | |
| 203 const char *req_body = body_kv ? ((Seobeo_Request_Entry*)body_kv)->value : ""; | |
| 204 | |
| 205 const char *hg_custom = req[7].value; | |
| 206 | |
| 207 Seobeo_Log(SEOBEO_DEBUG, "HG Stream Proxy: method=%s query=%s\n", method, query_string); | |
| 208 | |
| 209 // THINKING: Connect to hg serve | |
| 210 // This kinda blows, but not a good way to handle it since my client API assumes it is all stored in | |
| 211 // buffer and what not. | |
| 212 Seobeo_Handle *p_upstream = Seobeo_Stream_Handle_Client_Create(HG_SERVE_HOST, HG_SERVE_PORT, FALSE); | |
| 213 if (!p_upstream || p_upstream->socket < 0) | |
| 214 { | |
| 215 const char *err_resp = "HTTP/1.1 502 Bad Gateway\r\nContent-Length: 26\r\n\r\nFailed to connect upstream"; | |
| 216 Seobeo_Handle_Queue(p_client, (uint8*)err_resp, strlen(err_resp)); | |
| 217 Seobeo_Handle_Flush(p_client); | |
| 218 if (p_upstream) | |
| 219 Seobeo_Handle_Destroy(p_upstream); | |
| 220 return; | |
| 221 } | |
| 222 | |
| 223 // Create headers | |
| 224 // we only allow x-hgarg-1 and content-length | |
| 225 char request_buf[8192]; | |
| 226 int req_len = snprintf(request_buf, sizeof(request_buf), | |
| 227 "%s /?%s HTTP/1.1\r\n" | |
| 228 "Host: %s:%s\r\n" | |
| 229 "User-Agent: Seobeo/1.0\r\n" | |
| 230 "Connection: close\r\n", | |
| 231 method, query_string, HG_SERVE_HOST, HG_SERVE_PORT); | |
| 232 | |
| 233 if (hg_custom && hg_custom[0] != '\0') | |
| 234 req_len += snprintf(request_buf + req_len, sizeof(request_buf) - req_len, "x-hgarg-1: %s\r\n", hg_custom); | |
| 235 | |
| 236 if (req_body && req_body[0] != '\0') | |
| 237 req_len += snprintf(request_buf + req_len, sizeof(request_buf) - req_len, "Content-Length: %zu\r\n\r\n%s", strlen(req_body), req_body); | |
| 238 else | |
| 239 req_len += snprintf(request_buf + req_len, sizeof(request_buf) - req_len, "\r\n"); | |
| 240 | |
| 241 Seobeo_Handle_Queue(p_upstream, (uint8*)request_buf, req_len); | |
| 242 if (Seobeo_Handle_Flush(p_upstream) < 0) | |
| 243 { | |
| 244 const char *err_resp = "HTTP/1.1 502 Bad Gateway\r\nContent-Length: 21\r\n\r\nUpstream write failed"; | |
| 245 Seobeo_Handle_Queue(p_client, (uint8*)err_resp, strlen(err_resp)); | |
| 246 Seobeo_Handle_Flush(p_client); | |
| 247 Seobeo_Handle_Destroy(p_upstream); | |
| 248 return; | |
| 249 } | |
| 250 | |
| 251 // Responses | |
| 252 while (1) | |
| 253 { | |
| 254 int r = Seobeo_Handle_Read(p_upstream); | |
| 255 if (r < 0) | |
| 256 { | |
| 257 Seobeo_Handle_Destroy(p_upstream); | |
| 258 return; | |
| 259 } | |
| 260 if (p_upstream->read_buffer_len >= 4 && | |
| 261 strstr((char*)p_upstream->read_buffer, "\r\n\r\n") != NULL) | |
| 262 break; | 731 break; |
| 263 if (r == 0) | 732 Seobeo_Handle_Consume(upstream, upstream->read_buffer_len); |
| 264 continue; | 733 } |
| 265 } | 734 |
| 266 | 735 Seobeo_Handle_Destroy(upstream); |
| 267 // TODO: Maybe make this into a separate function instead of internal function as doing this over and over again blows. | 736 } |
| 268 char *hdr_end = strstr((char*)p_upstream->read_buffer, "\r\n\r\n"); | 737 |
| 269 if (!hdr_end) | 738 Seobeo_Request_Entry *GetReactHome(Seobeo_Request_Entry *request, Dowa_Arena *arena) |
| 270 { | 739 { |
| 271 Seobeo_Handle_Destroy(p_upstream); | 740 (void)request; |
| 272 return; | 741 size_t file_size = 0; |
| 273 } | 742 char *html = Seobeo_Web_LoadFile("/index.html", &file_size); |
| 274 size_t hdr_len = hdr_end - (char*)p_upstream->read_buffer + 4; | 743 if (!html) |
| 275 Seobeo_Handle_Queue(p_client, p_upstream->read_buffer, hdr_len); | 744 return text_response(arena, "500", "text/plain", "Application shell unavailable"); |
| 276 Seobeo_Handle_Flush(p_client); | 745 |
| 277 | 746 Seobeo_Request_Entry *response = NULL; |
| 278 // All body | 747 char *content_length = Dowa_Arena_Allocate(arena, 32); |
| 279 size_t body_in_buffer = p_upstream->read_buffer_len - hdr_len; | 748 snprintf(content_length, 32, "%zu", file_size); |
| 280 if (body_in_buffer > 0) | 749 Dowa_HashMap_Push_Arena(response, "status", "200", arena); |
| 281 { | 750 Dowa_HashMap_Push_Arena(response, "content-type", "text/html", arena); |
| 282 Seobeo_Handle_Queue(p_client, p_upstream->read_buffer + hdr_len, body_in_buffer); | 751 Dowa_HashMap_Push_Arena(response, "body", html, arena); |
| 283 Seobeo_Handle_Flush(p_client); | 752 Dowa_HashMap_Push_Arena(response, "content-length", content_length, arena); |
| 284 } | 753 return response; |
| 285 Seobeo_Handle_Consume(p_upstream, p_upstream->read_buffer_len); | 754 } |
| 286 while (1) | 755 |
| 287 { | 756 int main(void) |
| 288 int n = Seobeo_Handle_Read(p_upstream); | 757 { |
| 289 if (n > 0) | |
| 290 { | |
| 291 Seobeo_Handle_Queue(p_client, p_upstream->read_buffer, p_upstream->read_buffer_len); | |
| 292 Seobeo_Handle_Flush(p_client); | |
| 293 Seobeo_Handle_Consume(p_upstream, p_upstream->read_buffer_len); | |
| 294 } | |
| 295 else if (n == -2) | |
| 296 break; | |
| 297 else if (n < 0) | |
| 298 break; | |
| 299 } | |
| 300 | |
| 301 Seobeo_Handle_Destroy(p_upstream); | |
| 302 } | |
| 303 | |
| 304 Seobeo_Request_Entry* ApiHgWireProtocol(Seobeo_Request_Entry *req, Dowa_Arena *arena) | |
| 305 { | |
| 306 Seobeo_Request_Entry *resp = NULL; | |
| 307 | |
| 308 void *method_kv = Dowa_HashMap_Get_Ptr(req, "HTTP_Method"); | |
| 309 const char *method = method_kv ? ((Seobeo_Request_Entry*)method_kv)->value : "GET"; | |
| 310 | |
| 311 void *query_kv = Dowa_HashMap_Get_Ptr(req, "QueryString"); | |
| 312 const char *query_string = query_kv ? ((Seobeo_Request_Entry*)query_kv)->value : ""; | |
| 313 | |
| 314 void *body_kv = Dowa_HashMap_Get_Ptr(req, "Body"); | |
| 315 const char *req_body = body_kv ? ((Seobeo_Request_Entry*)body_kv)->value : ""; | |
| 316 size_t body_len = strlen(req_body); | |
| 317 | |
| 318 const char *hg_custom = req[7].value; | |
| 319 Seobeo_Log(SEOBEO_DEBUG, "HG Proxy: method=%s query=%s body_len=%zu\n", method, query_string, body_len); | |
| 320 | |
| 321 Seobeo_Client_Response *hg_response; | |
| 322 | |
| 323 char hg_path[MAX_PATH]; | |
| 324 snprintf(hg_path, sizeof(hg_path), "/?%s", query_string); | |
| 325 | |
| 326 hg_response = hg_proxy_request(method, hg_path, req_body, hg_custom); | |
| 327 | |
| 328 Seobeo_Log(SEOBEO_DEBUG, "HG Proxy: received %zu bytes\n", hg_response->body_length); | |
| 329 | |
| 330 Seobeo_Request_Entry *kv = Dowa_HashMap_Get_Ptr(hg_response->headers, "Content-Type"); | |
| 331 | |
| 332 char *status = Dowa_Arena_Allocate(arena, 5); | |
| 333 snprintf(status, 4, "%i", hg_response->status_code); | |
| 334 | |
| 335 // Use binary-safe copy to handle null bytes in mercurial bundle data | |
| 336 char *temp1 = Dowa_Arena_Copy(arena, hg_response->body, hg_response->body_length); | |
| 337 char *temp2 = Dowa_Arena_Allocate(arena, 256); | |
| 338 snprintf(temp2, 256, "%zu", hg_response->body_length); | |
| 339 | |
| 340 Dowa_HashMap_Push_Arena(resp, "status", status, arena); | |
| 341 Dowa_HashMap_Push_Arena(resp, "content-type", kv->value, arena); | |
| 342 Dowa_HashMap_Push_Arena(resp, "body", temp1, arena); | |
| 343 Dowa_HashMap_Push_Arena(resp, "content-length", temp2, arena); | |
| 344 | |
| 345 return resp; | |
| 346 } | |
| 347 | |
| 348 int main(void) { | |
| 349 Seobeo_Router_Init(); | 758 Seobeo_Router_Init(); |
| 759 | |
| 760 Seobeo_Router_Register("GET", "/", GetReactHome); | |
| 761 Seobeo_Router_Register("GET", "/directories", GetReactHome); | |
| 762 Seobeo_Router_Register("GET", "/directory", GetReactHome); | |
| 763 Seobeo_Router_Register("GET", "/graph", GetReactHome); | |
| 764 Seobeo_Router_Register("GET", "/changeset/:changeset_id", GetReactHome); | |
| 350 | 765 |
| 351 Seobeo_Router_Register("GET", "/api/repo/list", ApiListDirectory); | 766 Seobeo_Router_Register("GET", "/api/repo/list", ApiListDirectory); |
| 352 Seobeo_Router_Register("GET", "/api/repo/file", ApiGetFile); | 767 Seobeo_Router_Register("GET", "/api/repo/file", ApiGetFile); |
| 353 Seobeo_Router_Register("GET", "/api/repo/readme", ApiGetReadme); | 768 Seobeo_Router_Register("GET", "/api/repo/readme", ApiGetReadme); |
| 354 | 769 Seobeo_Router_Register("GET", "/api/graph/:graph_id", ApiGetGraph); |
| 355 // Use streaming handler for hg wire protocol... | 770 Seobeo_Router_Register("GET", "/api/changeset/:changeset_id", ApiGetChangeset); |
| 771 | |
| 356 Seobeo_Router_Register_Stream("GET", "/repo", StreamHgWireProtocol); | 772 Seobeo_Router_Register_Stream("GET", "/repo", StreamHgWireProtocol); |
| 357 Seobeo_Router_Register_Stream("POST", "/repo", StreamHgWireProtocol); | 773 Seobeo_Router_Register_Stream("POST", "/repo", StreamHgWireProtocol); |
| 358 | 774 |
| 359 printf("Starting on Port 6970...\n"); | 775 printf("Starting on Port 6970...\n"); |
| 360 | 776 int result = |
| 361 int result = Seobeo_Web_Server_Start("hg-web/src", "6970", SEOBEO_MODE_EDGE, 1); | 777 Seobeo_Web_Server_Start("hg-web/src", "6970", SEOBEO_MODE_EDGE, 1); |
| 362 | |
| 363 Seobeo_Router_Destroy(); | 778 Seobeo_Router_Destroy(); |
| 364 | |
| 365 return result; | 779 return result; |
| 366 } | 780 } |