Mercurial
comparison hg-web/main.c @ 221:ce7f4400c2de hg-web
[hg-web] Harden forge and add changeset UI
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Sun, 02 Aug 2026 09:01:24 -0700 |
| parents | 9f4429c49733 |
| children | 0e7b9464248d |
comparison
equal
deleted
inserted
replaced
| 219:8c9bb0b0759e | 221:ce7f4400c2de |
|---|---|
| 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 { | 34 static char *arena_string(Dowa_Arena *arena, const char *value) |
| 35 if (i + 1 < len && input_path[i+1] == '.') | 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] == '%') | |
| 75 { | |
| 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 Seobeo_Client_Response *hg_proxy_request( | |
| 202 const char *method, | |
| 203 const char *path, | |
| 204 const char *request_body, | |
| 205 size_t request_body_length, | |
| 206 const char *hg_argument, | |
| 207 const char *accept) | |
| 208 { | |
| 209 char url[MAX_PATH_LENGTH]; | |
| 210 int url_length = snprintf( | |
| 211 url, sizeof(url), "http://%s:%s%s", HG_SERVE_HOST, HG_SERVE_PORT, path); | |
| 212 if (url_length < 0 || (size_t)url_length >= sizeof(url)) | |
| 213 return NULL; | |
| 214 | |
| 215 Seobeo_Client_Request *request = Seobeo_Client_Request_Create(url); | |
| 216 if (!request) | |
| 217 return NULL; | |
| 218 | |
| 219 Seobeo_Client_Request_Set_Method(request, method); | |
| 220 Seobeo_Client_Request_Add_Header_Map(request, "User-Agent", "Seobeo/1.0"); | |
| 221 Seobeo_Client_Request_Add_Header_Map( | |
| 222 request, "Accept", accept ? accept : "application/json"); | |
| 223 Seobeo_Client_Request_Set_Timeout_Milliseconds(request, HG_API_TIMEOUT_MS); | |
| 224 | |
| 225 if (hg_argument && hg_argument[0] != '\0') | |
| 226 { | |
| 227 if (!safe_header_value(hg_argument, MAX_WIRE_HEADER_LENGTH)) | |
| 228 { | |
| 229 Seobeo_Client_Request_Destroy(request); | |
| 230 return NULL; | |
| 231 } | |
| 232 Seobeo_Client_Request_Add_Header_Map(request, "x-hgarg-1", hg_argument); | |
| 233 } | |
| 234 | |
| 235 if (request_body && request_body_length > 0) | |
| 236 Seobeo_Client_Request_Set_Body(request, request_body, request_body_length); | |
| 237 | |
| 238 Seobeo_Client_Response *response = Seobeo_Client_Request_Execute(request); | |
| 239 Seobeo_Client_Request_Destroy(request); | |
| 240 return response; | |
| 241 } | |
| 242 | |
| 243 static Seobeo_Request_Entry *forward_hg_response( | |
| 244 Seobeo_Client_Response *hg_response, | |
| 245 const char *default_content_type, | |
| 246 Dowa_Arena *arena) | |
| 247 { | |
| 248 if (!hg_response) | |
| 249 return text_response( | |
| 250 arena, "502", "application/json", "{\"error\":\"Mercurial backend unavailable\"}"); | |
| 251 | |
| 252 const char *upstream_content_type = | |
| 253 map_value_case_insensitive(hg_response->headers, "Content-Type"); | |
| 254 const char *upstream_or_default_content_type = | |
| 255 upstream_content_type ? upstream_content_type : default_content_type; | |
| 256 if (!upstream_or_default_content_type) | |
| 257 upstream_or_default_content_type = "application/octet-stream"; | |
| 258 char *content_type = arena_string(arena, upstream_or_default_content_type); | |
| 259 | |
| 260 char *status = Dowa_Arena_Allocate(arena, 8); | |
| 261 snprintf(status, 8, "%d", hg_response->status_code); | |
| 262 | |
| 263 size_t body_length = hg_response->body ? hg_response->body_length : 0; | |
| 264 char *body = Dowa_Arena_Allocate(arena, body_length + 1); | |
| 265 if (body_length > 0) | |
| 266 memcpy(body, hg_response->body, body_length); | |
| 267 body[body_length] = '\0'; | |
| 268 char *content_length = Dowa_Arena_Allocate(arena, 32); | |
| 269 snprintf(content_length, 32, "%zu", body_length); | |
| 270 | |
| 271 Seobeo_Request_Entry *response = NULL; | |
| 272 Dowa_HashMap_Push_Arena(response, "status", status, arena); | |
| 273 Dowa_HashMap_Push_Arena(response, "content-type", content_type, arena); | |
| 274 Dowa_HashMap_Push_Arena(response, "body", body, arena); | |
| 275 Dowa_HashMap_Push_Arena(response, "content-length", content_length, arena); | |
| 276 Seobeo_Client_Response_Destroy(hg_response); | |
| 277 return response; | |
| 278 } | |
| 279 | |
| 280 Seobeo_Request_Entry *ApiListDirectory(Seobeo_Request_Entry *request, Dowa_Arena *arena) | |
| 281 { | |
| 282 const char *encoded_path = map_value_case_insensitive(request, "query_path"); | |
| 283 char *path = NULL; | |
| 284 if (!normalize_repository_path(encoded_path ? encoded_path : "", arena, &path)) | |
| 285 return text_response(arena, "400", "application/json", "{\"error\":\"Invalid repository path\"}"); | |
| 286 | |
| 287 char *encoded = encode_repository_path(path, arena); | |
| 288 char hg_path[MAX_PATH_LENGTH]; | |
| 289 int length = snprintf( | |
| 290 hg_path, | |
| 291 sizeof(hg_path), | |
| 292 encoded[0] ? "/file/tip/%s?style=json" : "/file/tip/?style=json", | |
| 293 encoded); | |
| 294 if (length < 0 || (size_t)length >= sizeof(hg_path)) | |
| 295 return text_response(arena, "400", "application/json", "{\"error\":\"Repository path is too long\"}"); | |
| 296 | |
| 297 return forward_hg_response( | |
| 298 hg_proxy_request("GET", hg_path, NULL, 0, NULL, "application/json"), | |
| 299 "application/json", | |
| 300 arena); | |
| 301 } | |
| 302 | |
| 303 Seobeo_Request_Entry *ApiGetFile(Seobeo_Request_Entry *request, Dowa_Arena *arena) | |
| 304 { | |
| 305 const char *encoded_path = map_value_case_insensitive(request, "query_path"); | |
| 306 char *path = NULL; | |
| 307 if (!normalize_repository_path(encoded_path ? encoded_path : "", arena, &path) || | |
| 308 path[0] == '\0') | |
| 309 return text_response(arena, "400", "text/plain", "A valid file path is required"); | |
| 310 | |
| 311 char *encoded = encode_repository_path(path, arena); | |
| 312 char hg_path[MAX_PATH_LENGTH]; | |
| 313 int length = snprintf(hg_path, sizeof(hg_path), "/raw-file/tip/%s", encoded); | |
| 314 if (length < 0 || (size_t)length >= sizeof(hg_path)) | |
| 315 return text_response(arena, "400", "text/plain", "File path is too long"); | |
| 316 | |
| 317 return forward_hg_response( | |
| 318 hg_proxy_request("GET", hg_path, NULL, 0, NULL, "application/octet-stream"), | |
| 319 "application/octet-stream", | |
| 320 arena); | |
| 321 } | |
| 322 | |
| 323 Seobeo_Request_Entry *ApiGetReadme(Seobeo_Request_Entry *request, Dowa_Arena *arena) | |
| 324 { | |
| 325 const char *encoded_path = map_value_case_insensitive(request, "query_path"); | |
| 326 char *directory = NULL; | |
| 327 if (!normalize_repository_path(encoded_path ? encoded_path : "", arena, &directory)) | |
| 328 return text_response(arena, "400", "text/plain", "Invalid repository path"); | |
| 329 | |
| 330 size_t readme_length = strlen(directory) + strlen("/README.md") + 1; | |
| 331 if (readme_length >= MAX_PATH_LENGTH) | |
| 332 return text_response(arena, "400", "text/plain", "README path is too long"); | |
| 333 | |
| 334 char *readme_path = Dowa_Arena_Allocate(arena, readme_length); | |
| 335 snprintf( | |
| 336 readme_path, | |
| 337 readme_length, | |
| 338 directory[0] ? "%s/README.md" : "README.md", | |
| 339 directory); | |
| 340 char *encoded = encode_repository_path(readme_path, arena); | |
| 341 | |
| 342 char hg_path[MAX_PATH_LENGTH]; | |
| 343 int length = snprintf(hg_path, sizeof(hg_path), "/raw-file/tip/%s", encoded); | |
| 344 if (length < 0 || (size_t)length >= sizeof(hg_path)) | |
| 345 return text_response(arena, "400", "text/plain", "README path is too long"); | |
| 346 | |
| 347 return forward_hg_response( | |
| 348 hg_proxy_request("GET", hg_path, NULL, 0, NULL, "text/markdown"), | |
| 349 "text/markdown", | |
| 350 arena); | |
| 351 } | |
| 352 | |
| 353 Seobeo_Request_Entry *ApiGetGraph(Seobeo_Request_Entry *request, Dowa_Arena *arena) | |
| 354 { | |
| 355 const char *graph_id = map_value_case_insensitive(request, ":graph_id"); | |
| 356 if (!validate_revision(graph_id)) | |
| 357 return text_response(arena, "400", "application/json", "{\"error\":\"Invalid graph revision\"}"); | |
| 358 | |
| 359 const char *encoded_graph_top = | |
| 360 map_value_case_insensitive(request, "query_graphtop"); | |
| 361 char *graph_top = NULL; | |
| 362 if (encoded_graph_top) | |
| 363 { | |
| 364 if (!decode_url_component(encoded_graph_top, arena, &graph_top, NULL) || | |
| 365 !validate_revision(graph_top)) | |
| 366 return text_response(arena, "400", "application/json", "{\"error\":\"Invalid graph top revision\"}"); | |
| 367 } | |
| 368 | |
| 369 char hg_path[MAX_PATH_LENGTH]; | |
| 370 int length = graph_top | |
| 371 ? snprintf( | |
| 372 hg_path, | |
| 373 sizeof(hg_path), | |
| 374 "/graph/%s?graphtop=%s&style=json", | |
| 375 graph_id, | |
| 376 graph_top) | |
| 377 : snprintf(hg_path, sizeof(hg_path), "/graph/%s?style=json", graph_id); | |
| 378 if (length < 0 || (size_t)length >= sizeof(hg_path)) | |
| 379 return text_response(arena, "400", "application/json", "{\"error\":\"Graph request is too long\"}"); | |
| 380 | |
| 381 return forward_hg_response( | |
| 382 hg_proxy_request("GET", hg_path, NULL, 0, NULL, "application/json"), | |
| 383 "application/json", | |
| 384 arena); | |
| 385 } | |
| 386 | |
| 387 Seobeo_Request_Entry *ApiGetChangeset(Seobeo_Request_Entry *request, Dowa_Arena *arena) | |
| 388 { | |
| 389 const char *changeset_id = map_value_case_insensitive(request, ":changeset_id"); | |
| 390 if (!validate_revision(changeset_id)) | |
| 391 return text_response(arena, "400", "application/json", "{\"error\":\"Invalid changeset revision\"}"); | |
| 392 | |
| 393 char hg_path[128]; | |
| 394 int length = snprintf(hg_path, sizeof(hg_path), "/json-rev/%s", changeset_id); | |
| 395 if (length < 0 || (size_t)length >= sizeof(hg_path)) | |
| 396 return text_response(arena, "400", "application/json", "{\"error\":\"Changeset request is too long\"}"); | |
| 397 | |
| 398 return forward_hg_response( | |
| 399 hg_proxy_request("GET", hg_path, NULL, 0, NULL, "application/json"), | |
| 400 "application/json", | |
| 401 arena); | |
| 402 } | |
| 403 | |
| 404 static int64_t monotonic_milliseconds(void) | |
| 405 { | |
| 406 struct timespec now; | |
| 407 clock_gettime(CLOCK_MONOTONIC, &now); | |
| 408 return (int64_t)now.tv_sec * 1000 + now.tv_nsec / 1000000; | |
| 409 } | |
| 410 | |
| 411 static size_t find_http_header_length(const uint8 *buffer, size_t length) | |
| 412 { | |
| 413 if (!buffer || length < 4) | |
| 414 return 0; | |
| 415 for (size_t i = 0; i + 3 < length; i++) | |
| 416 { | |
| 417 if (buffer[i] == '\r' && buffer[i + 1] == '\n' && | |
| 418 buffer[i + 2] == '\r' && buffer[i + 3] == '\n') | |
| 419 return i + 4; | |
| 420 } | |
| 421 return 0; | |
| 422 } | |
| 423 | |
| 424 static void send_proxy_error(Seobeo_Handle *client, int status, const char *message) | |
| 425 { | |
| 426 const char *reason = status == 504 ? "Gateway Timeout" : "Bad Gateway"; | |
| 427 char response[512]; | |
| 428 int length = snprintf( | |
| 429 response, | |
| 430 sizeof(response), | |
| 431 "HTTP/1.1 %d %s\r\n" | |
| 432 "Content-Type: text/plain\r\n" | |
| 433 "Content-Length: %zu\r\n" | |
| 434 "Connection: close\r\n" | |
| 435 "\r\n" | |
| 436 "%s", | |
| 437 status, | |
| 438 reason, | |
| 439 strlen(message), | |
| 440 message); | |
| 441 if (length > 0 && (size_t)length < sizeof(response)) | |
| 442 { | |
| 443 Seobeo_Handle_Queue(client, (const uint8 *)response, (uint32)length); | |
| 444 Seobeo_Handle_Flush(client); | |
| 445 } | |
| 446 } | |
| 447 | |
| 448 static boolean parse_content_length(const char *value, size_t *length_out) | |
| 449 { | |
| 450 if (!value || !length_out || value[0] == '\0') | |
| 451 return FALSE; | |
| 452 errno = 0; | |
| 453 char *end = NULL; | |
| 454 unsigned long long parsed = strtoull(value, &end, 10); | |
| 455 if (errno != 0 || !end || *end != '\0' || parsed > SIZE_MAX) | |
| 456 return FALSE; | |
| 457 *length_out = (size_t)parsed; | |
| 458 return TRUE; | |
| 459 } | |
| 460 | |
| 461 void StreamHgWireProtocol( | |
| 462 Seobeo_Handle *client, | |
| 463 Seobeo_Request_Entry *request, | |
| 464 Dowa_Arena *arena) | |
| 465 { | |
| 466 (void)arena; | |
| 467 const char *method = map_value_case_insensitive(request, "HTTP_Method"); | |
| 468 const char *query = map_value_case_insensitive(request, "QueryString"); | |
| 469 const char *body = map_value_case_insensitive(request, "Body"); | |
| 470 const char *content_length_value = | |
| 471 map_value_case_insensitive(request, "Content-Length"); | |
| 472 const char *content_type = map_value_case_insensitive(request, "Content-Type"); | |
| 473 const char *hg_argument = map_value_case_insensitive(request, "x-hgarg-1"); | |
| 474 | |
| 475 if (!method || !query || | |
| 476 (strcmp(method, "GET") != 0 && strcmp(method, "POST") != 0) || | |
| 477 !safe_header_value(query, MAX_WIRE_QUERY_LENGTH) || | |
| 478 !safe_header_value(hg_argument, MAX_WIRE_HEADER_LENGTH) || | |
| 479 !safe_header_value(content_type, 256)) | |
| 480 { | |
| 481 send_proxy_error(client, 502, "Invalid Mercurial proxy request"); | |
| 482 return; | |
| 483 } | |
| 484 | |
| 485 size_t body_length = 0; | |
| 486 if (content_length_value && | |
| 487 !parse_content_length(content_length_value, &body_length)) | |
| 488 { | |
| 489 send_proxy_error(client, 502, "Invalid Mercurial request length"); | |
| 490 return; | |
| 491 } | |
| 492 if (body_length > 0 && !body) | |
| 493 { | |
| 494 send_proxy_error(client, 502, "Missing Mercurial request body"); | |
| 495 return; | |
| 496 } | |
| 497 if (body_length > UINT32_MAX) | |
| 498 { | |
| 499 send_proxy_error(client, 502, "Mercurial request body is too large"); | |
| 500 return; | |
| 501 } | |
| 502 | |
| 503 Seobeo_Handle *upstream = | |
| 504 Seobeo_Stream_Handle_Client_Create(HG_SERVE_HOST, HG_SERVE_PORT, FALSE); | |
| 505 if (!upstream || upstream->socket < 0) | |
| 506 { | |
| 507 if (upstream) | |
| 508 Seobeo_Handle_Destroy(upstream); | |
| 509 send_proxy_error(client, 502, "Mercurial backend unavailable"); | |
| 510 return; | |
| 511 } | |
| 512 | |
| 513 char request_header[16384]; | |
| 514 int header_length = snprintf( | |
| 515 request_header, | |
| 516 sizeof(request_header), | |
| 517 "%s /?%s HTTP/1.1\r\n" | |
| 518 "Host: %s:%s\r\n" | |
| 519 "User-Agent: Seobeo/1.0\r\n" | |
| 520 "Connection: close\r\n", | |
| 521 method, | |
| 522 query, | |
| 523 HG_SERVE_HOST, | |
| 524 HG_SERVE_PORT); | |
| 525 if (header_length < 0 || (size_t)header_length >= sizeof(request_header)) | |
| 526 { | |
| 527 Seobeo_Handle_Destroy(upstream); | |
| 528 send_proxy_error(client, 502, "Mercurial request headers are too large"); | |
| 529 return; | |
| 530 } | |
| 531 | |
| 532 #define APPEND_WIRE_HEADER(...) \ | |
| 533 do { \ | |
| 534 int appended = snprintf( \ | |
| 535 request_header + header_length, \ | |
| 536 sizeof(request_header) - (size_t)header_length, \ | |
| 537 __VA_ARGS__); \ | |
| 538 if (appended < 0 || (size_t)appended >= sizeof(request_header) - (size_t)header_length) { \ | |
| 539 Seobeo_Handle_Destroy(upstream); \ | |
| 540 send_proxy_error(client, 502, "Mercurial request headers are too large"); \ | |
| 541 return; \ | |
| 542 } \ | |
| 543 header_length += appended; \ | |
| 544 } while (0) | |
| 545 | |
| 546 if (hg_argument && hg_argument[0] != '\0') | |
| 547 APPEND_WIRE_HEADER("x-hgarg-1: %s\r\n", hg_argument); | |
| 548 if (content_type && content_type[0] != '\0') | |
| 549 APPEND_WIRE_HEADER("Content-Type: %s\r\n", content_type); | |
| 550 if (body_length > 0) | |
| 551 APPEND_WIRE_HEADER("Content-Length: %zu\r\n", body_length); | |
| 552 APPEND_WIRE_HEADER("\r\n"); | |
| 553 #undef APPEND_WIRE_HEADER | |
| 554 | |
| 555 if (Seobeo_Handle_Queue( | |
| 556 upstream, (const uint8 *)request_header, (uint32)header_length) != 0 || | |
| 557 (body_length > 0 && | |
| 558 Seobeo_Handle_Queue(upstream, (const uint8 *)body, (uint32)body_length) != 0) || | |
| 559 Seobeo_Handle_Flush(upstream) != 0) | |
| 560 { | |
| 561 Seobeo_Handle_Destroy(upstream); | |
| 562 send_proxy_error(client, 502, "Mercurial backend write failed"); | |
| 563 return; | |
| 564 } | |
| 565 | |
| 566 boolean response_started = FALSE; | |
| 567 int64_t last_progress = monotonic_milliseconds(); | |
| 568 while (!response_started) | |
| 569 { | |
| 570 int read_result = Seobeo_Handle_Read(upstream); | |
| 571 if (read_result == -2 || read_result < 0) | |
| 572 { | |
| 573 Seobeo_Handle_Destroy(upstream); | |
| 574 send_proxy_error(client, 502, "Mercurial backend closed before responding"); | |
| 575 return; | |
| 576 } | |
| 577 if (read_result > 0) | |
| 578 last_progress = monotonic_milliseconds(); | |
| 579 | |
| 580 size_t header_size = | |
| 581 find_http_header_length(upstream->read_buffer, upstream->read_buffer_len); | |
| 582 if (header_size > 0) | |
| 583 { | |
| 584 (void)header_size; | |
| 585 if (Seobeo_Handle_Queue( | |
| 586 client, upstream->read_buffer, upstream->read_buffer_len) != 0 || | |
| 587 Seobeo_Handle_Flush(client) != 0) | |
| 36 { | 588 { |
| 37 // Skip ".." | 589 Seobeo_Handle_Destroy(upstream); |
| 38 i++; | 590 return; |
| 39 continue; | |
| 40 } | 591 } |
| 41 // Skip "." | 592 Seobeo_Handle_Consume(upstream, upstream->read_buffer_len); |
| 593 response_started = TRUE; | |
| 594 break; | |
| 595 } | |
| 596 | |
| 597 if (read_result == 0) | |
| 598 { | |
| 599 if (monotonic_milliseconds() - last_progress >= HG_STREAM_IDLE_TIMEOUT_MS) | |
| 600 { | |
| 601 Seobeo_Handle_Destroy(upstream); | |
| 602 send_proxy_error(client, 504, "Mercurial backend response timed out"); | |
| 603 return; | |
| 604 } | |
| 605 usleep(1000); | |
| 606 } | |
| 607 } | |
| 608 | |
| 609 while (TRUE) | |
| 610 { | |
| 611 int read_result = Seobeo_Handle_Read(upstream); | |
| 612 if (read_result == -2) | |
| 613 break; | |
| 614 if (read_result < 0) | |
| 615 break; | |
| 616 if (read_result == 0) | |
| 617 { | |
| 618 if (monotonic_milliseconds() - last_progress >= HG_STREAM_IDLE_TIMEOUT_MS) | |
| 619 { | |
| 620 Seobeo_Log(SEOBEO_ERROR, "Mercurial response stream timed out\n"); | |
| 621 break; | |
| 622 } | |
| 623 usleep(1000); | |
| 42 continue; | 624 continue; |
| 43 } | 625 } |
| 44 result[j++] = input_path[i]; | 626 |
| 45 } | 627 last_progress = monotonic_milliseconds(); |
| 46 result[j] = '\0'; | 628 if (Seobeo_Handle_Queue( |
| 47 | 629 client, upstream->read_buffer, upstream->read_buffer_len) != 0 || |
| 48 // Remove leading/trailing slashes | 630 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* ApiGetGraph(Seobeo_Request_Entry *req, Dowa_Arena *arena) | |
| 131 { | |
| 132 Seobeo_Request_Entry *resp = NULL; | |
| 133 | |
| 134 void *path_kv = Dowa_HashMap_Get_Ptr(req, "QueryString"); | |
| 135 const char *rel_path = path_kv ? ((Seobeo_Request_Entry*)path_kv)->value : ""; | |
| 136 Seobeo_Log(SEOBEO_INFO, "ApiGetGraph: rel_path='%s'\n", rel_path); | |
| 137 void *graph_id_kv = Dowa_HashMap_Get_Ptr(req, ":graph_id"); | |
| 138 char *graph_id = ((Seobeo_Request_Entry*)graph_id_kv)->value; | |
| 139 Seobeo_Log(SEOBEO_INFO, "ApiGetGraph: graph_id='%s'\n", graph_id); | |
| 140 char *decoded_path = Dowa_Arena_Allocate(arena, strlen(rel_path) + 1); | |
| 141 Seobeo_Url_Decode(decoded_path, rel_path); | |
| 142 char *safe_path = sanitize_path(decoded_path, arena); | |
| 143 | |
| 144 Seobeo_Log(SEOBEO_INFO, "ApiGetGraph: safe_path='%s'\n", safe_path); | |
| 145 | |
| 146 if (strlen(safe_path) == 0) | |
| 147 { | |
| 148 Dowa_HashMap_Push_Arena(resp, "status", "400", arena); | |
| 149 Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); | |
| 150 Dowa_HashMap_Push_Arena(resp, "body", "File path required", arena); | |
| 151 return resp; | |
| 152 } | |
| 153 | |
| 154 char hg_path[MAX_PATH]; | |
| 155 // void *graph_id_kv = Dowa_HashMap_Get_Ptr(req, ":graph_id"); | |
| 156 // char *graph_id = ((Seobeo_Request_Entry*)graph_id_kv)->value; | |
| 157 snprintf(hg_path, sizeof(hg_path), "/graph/%s?%s", graph_id, safe_path); | |
| 158 Seobeo_Client_Response *hg_response = hg_proxy_request("GET", hg_path, NULL, NULL); | |
| 159 | |
| 160 Seobeo_Log(SEOBEO_DEBUG, "ApiGetGraph: status=%i body_len=%zu\n", hg_response->status_code, hg_response->body_length); | |
| 161 | |
| 162 char status[4]; | |
| 163 snprintf(status, 4, "%i", hg_response->status_code); | |
| 164 | |
| 165 if (!hg_response->body) | |
| 166 { | |
| 167 Dowa_HashMap_Push_Arena(resp, "status", "502", arena); | |
| 168 Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); | |
| 169 Dowa_HashMap_Push_Arena(resp, "body", "Failed to connect to hg serve", arena); | |
| 170 return resp; | |
| 171 } | |
| 172 | |
| 173 if (hg_response->status_code != 200) | |
| 174 { | |
| 175 Seobeo_Log(SEOBEO_DEBUG, "ApiGetGraph: error hg_response: %s\n", hg_response->body); | |
| 176 Dowa_HashMap_Push_Arena(resp, "status", status, arena); | |
| 177 Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); | |
| 178 Dowa_HashMap_Push_Arena(resp, "body", hg_response->body, arena); | |
| 179 return resp; | |
| 180 } | |
| 181 | |
| 182 | |
| 183 char *temp1 = Dowa_Arena_Copy(arena, hg_response->body, hg_response->body_length); | |
| 184 char *temp2 = Dowa_Arena_Allocate(arena, 256); | |
| 185 snprintf(temp2, 256, "%zu", hg_response->body_length); | |
| 186 | |
| 187 Dowa_HashMap_Push_Arena(resp, "status", "200", arena); | |
| 188 Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); | |
| 189 Dowa_HashMap_Push_Arena(resp, "body", temp1, arena); | |
| 190 Dowa_HashMap_Push_Arena(resp, "content-length", temp2, arena); | |
| 191 | |
| 192 return resp; | |
| 193 } | |
| 194 | |
| 195 Seobeo_Request_Entry* ApiGetFile(Seobeo_Request_Entry *req, Dowa_Arena *arena) | |
| 196 { | |
| 197 Seobeo_Request_Entry *resp = NULL; | |
| 198 | |
| 199 void *path_kv = Dowa_HashMap_Get_Ptr(req, "query_path"); | |
| 200 const char *rel_path = path_kv ? ((Seobeo_Request_Entry*)path_kv)->value : ""; | |
| 201 char *decoded_path = Dowa_Arena_Allocate(arena, strlen(rel_path) + 1); | |
| 202 Seobeo_Url_Decode(decoded_path, rel_path); | |
| 203 char *safe_path = sanitize_path(decoded_path, arena); | |
| 204 | |
| 205 Seobeo_Log(SEOBEO_INFO, "ApiGetFile: safe_path='%s'\n", safe_path); | |
| 206 | |
| 207 if (strlen(safe_path) == 0) | |
| 208 { | |
| 209 Dowa_HashMap_Push_Arena(resp, "status", "400", arena); | |
| 210 Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); | |
| 211 Dowa_HashMap_Push_Arena(resp, "body", "File path required", arena); | |
| 212 return resp; | |
| 213 } | |
| 214 | |
| 215 char hg_path[MAX_PATH]; | |
| 216 snprintf(hg_path, sizeof(hg_path), "/raw-file/tip/%s", safe_path); | |
| 217 Seobeo_Client_Response *hg_response = hg_proxy_request("GET", hg_path, NULL, NULL); | |
| 218 | |
| 219 Seobeo_Log(SEOBEO_DEBUG, "ApiGetFile: status=%i body_len=%zu\n", hg_response->status_code, hg_response->body_length); | |
| 220 | |
| 221 char status[4]; | |
| 222 snprintf(status, 4, "%i", hg_response->status_code); | |
| 223 | |
| 224 if (!hg_response->body) | |
| 225 { | |
| 226 Dowa_HashMap_Push_Arena(resp, "status", "502", arena); | |
| 227 Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); | |
| 228 Dowa_HashMap_Push_Arena(resp, "body", "Failed to connect to hg serve", arena); | |
| 229 return resp; | |
| 230 } | |
| 231 | |
| 232 if (hg_response->status_code != 200) | |
| 233 { | |
| 234 Seobeo_Log(SEOBEO_DEBUG, "ApiGetFile: error hg_response: %s\n", hg_response->body); | |
| 235 Dowa_HashMap_Push_Arena(resp, "status", status, arena); | |
| 236 Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); | |
| 237 Dowa_HashMap_Push_Arena(resp, "body", hg_response->body, arena); | |
| 238 return resp; | |
| 239 } | |
| 240 | |
| 241 | |
| 242 char *temp1 = Dowa_Arena_Copy(arena, hg_response->body, hg_response->body_length); | |
| 243 char *temp2 = Dowa_Arena_Allocate(arena, 256); | |
| 244 snprintf(temp2, 256, "%zu", hg_response->body_length); | |
| 245 | |
| 246 Dowa_HashMap_Push_Arena(resp, "status", "200", arena); | |
| 247 Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); | |
| 248 Dowa_HashMap_Push_Arena(resp, "body", temp1, arena); | |
| 249 Dowa_HashMap_Push_Arena(resp, "content-length", temp2, arena); | |
| 250 | |
| 251 return resp; | |
| 252 } | |
| 253 | |
| 254 Seobeo_Request_Entry* ApiGetReadme(Seobeo_Request_Entry *req, Dowa_Arena *arena) { | |
| 255 return ApiGetFile(req, arena); | |
| 256 } | |
| 257 | |
| 258 // Streaming handler for hg wire protocol - pipes data directly without buffering | |
| 259 void StreamHgWireProtocol(Seobeo_Handle *p_client, Seobeo_Request_Entry *req, Dowa_Arena *arena) | |
| 260 { | |
| 261 void *method_kv = Dowa_HashMap_Get_Ptr(req, "HTTP_Method"); | |
| 262 const char *method = method_kv ? ((Seobeo_Request_Entry*)method_kv)->value : "GET"; | |
| 263 | |
| 264 void *query_kv = Dowa_HashMap_Get_Ptr(req, "QueryString"); | |
| 265 const char *query_string = query_kv ? ((Seobeo_Request_Entry*)query_kv)->value : ""; | |
| 266 | |
| 267 void *body_kv = Dowa_HashMap_Get_Ptr(req, "Body"); | |
| 268 const char *req_body = body_kv ? ((Seobeo_Request_Entry*)body_kv)->value : ""; | |
| 269 | |
| 270 const char *hg_custom = req[7].value; | |
| 271 | |
| 272 Seobeo_Log(SEOBEO_DEBUG, "HG Stream Proxy: method=%s query=%s\n", method, query_string); | |
| 273 | |
| 274 // THINKING: Connect to hg serve | |
| 275 // This kinda blows, but not a good way to handle it since my client API assumes it is all stored in | |
| 276 // buffer and what not. | |
| 277 Seobeo_Handle *p_upstream = Seobeo_Stream_Handle_Client_Create(HG_SERVE_HOST, HG_SERVE_PORT, FALSE); | |
| 278 if (!p_upstream || p_upstream->socket < 0) | |
| 279 { | |
| 280 const char *err_resp = "HTTP/1.1 502 Bad Gateway\r\nContent-Length: 26\r\n\r\nFailed to connect upstream"; | |
| 281 Seobeo_Handle_Queue(p_client, (uint8*)err_resp, strlen(err_resp)); | |
| 282 Seobeo_Handle_Flush(p_client); | |
| 283 if (p_upstream) | |
| 284 Seobeo_Handle_Destroy(p_upstream); | |
| 285 return; | |
| 286 } | |
| 287 | |
| 288 // Create headers | |
| 289 // we only allow x-hgarg-1 and content-length | |
| 290 char request_buf[8192]; | |
| 291 int req_len = snprintf(request_buf, sizeof(request_buf), | |
| 292 "%s /?%s HTTP/1.1\r\n" | |
| 293 "Host: %s:%s\r\n" | |
| 294 "User-Agent: Seobeo/1.0\r\n" | |
| 295 "Connection: close\r\n", | |
| 296 method, query_string, HG_SERVE_HOST, HG_SERVE_PORT); | |
| 297 | |
| 298 if (hg_custom && hg_custom[0] != '\0') | |
| 299 req_len += snprintf(request_buf + req_len, sizeof(request_buf) - req_len, "x-hgarg-1: %s\r\n", hg_custom); | |
| 300 | |
| 301 if (req_body && req_body[0] != '\0') | |
| 302 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); | |
| 303 else | |
| 304 req_len += snprintf(request_buf + req_len, sizeof(request_buf) - req_len, "\r\n"); | |
| 305 | |
| 306 Seobeo_Handle_Queue(p_upstream, (uint8*)request_buf, req_len); | |
| 307 if (Seobeo_Handle_Flush(p_upstream) < 0) | |
| 308 { | |
| 309 const char *err_resp = "HTTP/1.1 502 Bad Gateway\r\nContent-Length: 21\r\n\r\nUpstream write failed"; | |
| 310 Seobeo_Handle_Queue(p_client, (uint8*)err_resp, strlen(err_resp)); | |
| 311 Seobeo_Handle_Flush(p_client); | |
| 312 Seobeo_Handle_Destroy(p_upstream); | |
| 313 return; | |
| 314 } | |
| 315 | |
| 316 // Responses | |
| 317 while (1) | |
| 318 { | |
| 319 int r = Seobeo_Handle_Read(p_upstream); | |
| 320 if (r < 0) | |
| 321 { | |
| 322 Seobeo_Handle_Destroy(p_upstream); | |
| 323 return; | |
| 324 } | |
| 325 if (p_upstream->read_buffer_len >= 4 && | |
| 326 strstr((char*)p_upstream->read_buffer, "\r\n\r\n") != NULL) | |
| 327 break; | 631 break; |
| 328 if (r == 0) | 632 Seobeo_Handle_Consume(upstream, upstream->read_buffer_len); |
| 329 continue; | 633 } |
| 330 } | 634 |
| 331 | 635 Seobeo_Handle_Destroy(upstream); |
| 332 // TODO: Maybe make this into a separate function instead of internal function as doing this over and over again blows. | 636 } |
| 333 char *hdr_end = strstr((char*)p_upstream->read_buffer, "\r\n\r\n"); | 637 |
| 334 if (!hdr_end) | 638 Seobeo_Request_Entry *GetReactHome(Seobeo_Request_Entry *request, Dowa_Arena *arena) |
| 335 { | 639 { |
| 336 Seobeo_Handle_Destroy(p_upstream); | 640 (void)request; |
| 337 return; | |
| 338 } | |
| 339 size_t hdr_len = hdr_end - (char*)p_upstream->read_buffer + 4; | |
| 340 Seobeo_Handle_Queue(p_client, p_upstream->read_buffer, hdr_len); | |
| 341 Seobeo_Handle_Flush(p_client); | |
| 342 | |
| 343 // All body | |
| 344 size_t body_in_buffer = p_upstream->read_buffer_len - hdr_len; | |
| 345 if (body_in_buffer > 0) | |
| 346 { | |
| 347 Seobeo_Handle_Queue(p_client, p_upstream->read_buffer + hdr_len, body_in_buffer); | |
| 348 Seobeo_Handle_Flush(p_client); | |
| 349 } | |
| 350 Seobeo_Handle_Consume(p_upstream, p_upstream->read_buffer_len); | |
| 351 while (1) | |
| 352 { | |
| 353 int n = Seobeo_Handle_Read(p_upstream); | |
| 354 if (n > 0) | |
| 355 { | |
| 356 Seobeo_Handle_Queue(p_client, p_upstream->read_buffer, p_upstream->read_buffer_len); | |
| 357 Seobeo_Handle_Flush(p_client); | |
| 358 Seobeo_Handle_Consume(p_upstream, p_upstream->read_buffer_len); | |
| 359 } | |
| 360 else if (n == -2) | |
| 361 break; | |
| 362 else if (n < 0) | |
| 363 break; | |
| 364 } | |
| 365 | |
| 366 Seobeo_Handle_Destroy(p_upstream); | |
| 367 } | |
| 368 | |
| 369 Seobeo_Request_Entry* ApiHgWireProtocol(Seobeo_Request_Entry *req, Dowa_Arena *arena) | |
| 370 { | |
| 371 Seobeo_Request_Entry *resp = NULL; | |
| 372 | |
| 373 void *method_kv = Dowa_HashMap_Get_Ptr(req, "HTTP_Method"); | |
| 374 const char *method = method_kv ? ((Seobeo_Request_Entry*)method_kv)->value : "GET"; | |
| 375 | |
| 376 void *query_kv = Dowa_HashMap_Get_Ptr(req, "QueryString"); | |
| 377 const char *query_string = query_kv ? ((Seobeo_Request_Entry*)query_kv)->value : ""; | |
| 378 | |
| 379 void *body_kv = Dowa_HashMap_Get_Ptr(req, "Body"); | |
| 380 const char *req_body = body_kv ? ((Seobeo_Request_Entry*)body_kv)->value : ""; | |
| 381 size_t body_len = strlen(req_body); | |
| 382 | |
| 383 const char *hg_custom = req[7].value; | |
| 384 Seobeo_Log(SEOBEO_DEBUG, "HG Proxy: method=%s query=%s body_len=%zu\n", method, query_string, body_len); | |
| 385 | |
| 386 Seobeo_Client_Response *hg_response; | |
| 387 | |
| 388 char hg_path[MAX_PATH]; | |
| 389 snprintf(hg_path, sizeof(hg_path), "/?%s", query_string); | |
| 390 | |
| 391 hg_response = hg_proxy_request(method, hg_path, req_body, hg_custom); | |
| 392 | |
| 393 Seobeo_Log(SEOBEO_DEBUG, "HG Proxy: received %zu bytes\n", hg_response->body_length); | |
| 394 | |
| 395 Seobeo_Request_Entry *kv = Dowa_HashMap_Get_Ptr(hg_response->headers, "Content-Type"); | |
| 396 | |
| 397 char *status = Dowa_Arena_Allocate(arena, 5); | |
| 398 snprintf(status, 4, "%i", hg_response->status_code); | |
| 399 | |
| 400 // Use binary-safe copy to handle null bytes in mercurial bundle data | |
| 401 char *temp1 = Dowa_Arena_Copy(arena, hg_response->body, hg_response->body_length); | |
| 402 char *temp2 = Dowa_Arena_Allocate(arena, 256); | |
| 403 snprintf(temp2, 256, "%zu", hg_response->body_length); | |
| 404 | |
| 405 Dowa_HashMap_Push_Arena(resp, "status", status, arena); | |
| 406 Dowa_HashMap_Push_Arena(resp, "content-type", kv->value, arena); | |
| 407 Dowa_HashMap_Push_Arena(resp, "body", temp1, arena); | |
| 408 Dowa_HashMap_Push_Arena(resp, "content-length", temp2, arena); | |
| 409 | |
| 410 return resp; | |
| 411 } | |
| 412 | |
| 413 Seobeo_Request_Entry* GetReactHome(Seobeo_Request_Entry *req, Dowa_Arena *arena) | |
| 414 { | |
| 415 size_t file_size = 0; | 641 size_t file_size = 0; |
| 416 char *html = Seobeo_Web_LoadFile("/index.html", &file_size); | 642 char *html = Seobeo_Web_LoadFile("/index.html", &file_size); |
| 417 | 643 if (!html) |
| 418 printf("%s", html); | 644 return text_response(arena, "500", "text/plain", "Application shell unavailable"); |
| 419 Seobeo_Request_Entry *resp = NULL; | 645 |
| 420 Dowa_HashMap_Push_Arena(resp, "status", "200", arena); | 646 Seobeo_Request_Entry *response = NULL; |
| 421 Dowa_HashMap_Push_Arena(resp, "content-type", "text/html", arena); | 647 char *content_length = Dowa_Arena_Allocate(arena, 32); |
| 422 Dowa_HashMap_Push_Arena(resp, "body", html, arena); | 648 snprintf(content_length, 32, "%zu", file_size); |
| 423 return resp; | 649 Dowa_HashMap_Push_Arena(response, "status", "200", arena); |
| 424 } | 650 Dowa_HashMap_Push_Arena(response, "content-type", "text/html", arena); |
| 425 | 651 Dowa_HashMap_Push_Arena(response, "body", html, arena); |
| 426 int main(void) { | 652 Dowa_HashMap_Push_Arena(response, "content-length", content_length, arena); |
| 653 return response; | |
| 654 } | |
| 655 | |
| 656 int main(void) | |
| 657 { | |
| 427 Seobeo_Router_Init(); | 658 Seobeo_Router_Init(); |
| 428 | |
| 429 | 659 |
| 430 Seobeo_Router_Register("GET", "/", GetReactHome); | 660 Seobeo_Router_Register("GET", "/", GetReactHome); |
| 431 Seobeo_Router_Register("GET", "/directories", GetReactHome); | 661 Seobeo_Router_Register("GET", "/directories", GetReactHome); |
| 662 Seobeo_Router_Register("GET", "/directory", GetReactHome); | |
| 432 Seobeo_Router_Register("GET", "/graph", GetReactHome); | 663 Seobeo_Router_Register("GET", "/graph", GetReactHome); |
| 664 Seobeo_Router_Register("GET", "/changeset/:changeset_id", GetReactHome); | |
| 433 | 665 |
| 434 Seobeo_Router_Register("GET", "/api/repo/list", ApiListDirectory); | 666 Seobeo_Router_Register("GET", "/api/repo/list", ApiListDirectory); |
| 435 Seobeo_Router_Register("GET", "/api/repo/file", ApiGetFile); | 667 Seobeo_Router_Register("GET", "/api/repo/file", ApiGetFile); |
| 668 Seobeo_Router_Register("GET", "/api/repo/readme", ApiGetReadme); | |
| 436 Seobeo_Router_Register("GET", "/api/graph/:graph_id", ApiGetGraph); | 669 Seobeo_Router_Register("GET", "/api/graph/:graph_id", ApiGetGraph); |
| 437 Seobeo_Router_Register("GET", "/api/repo/readme", ApiGetReadme); | 670 Seobeo_Router_Register("GET", "/api/changeset/:changeset_id", ApiGetChangeset); |
| 438 | 671 |
| 439 // Use streaming handler for hg wire protocol... | |
| 440 Seobeo_Router_Register_Stream("GET", "/repo", StreamHgWireProtocol); | 672 Seobeo_Router_Register_Stream("GET", "/repo", StreamHgWireProtocol); |
| 441 Seobeo_Router_Register_Stream("POST", "/repo", StreamHgWireProtocol); | 673 Seobeo_Router_Register_Stream("POST", "/repo", StreamHgWireProtocol); |
| 442 | 674 |
| 443 printf("Starting on Port 6970...\n"); | 675 printf("Starting on Port 6970...\n"); |
| 444 | 676 int result = |
| 445 int result = Seobeo_Web_Server_Start("hg-web/src", "6970", SEOBEO_MODE_EDGE, 1); | 677 Seobeo_Web_Server_Start("hg-web/src", "6970", SEOBEO_MODE_EDGE, 1); |
| 446 | |
| 447 Seobeo_Router_Destroy(); | 678 Seobeo_Router_Destroy(); |
| 448 | |
| 449 return result; | 679 return result; |
| 450 } | 680 } |