Mercurial
comparison hg-web/main.c @ 195:f8f5004a920a
Merging back hg-web-tip
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Tue, 27 Jan 2026 06:51:44 -0800 |
| parents | 9f4429c49733 |
| children |
comparison
equal
deleted
inserted
replaced
| 189:14cc84ba35a0 | 195:f8f5004a920a |
|---|---|
| 9 #include <netinet/in.h> | 9 #include <netinet/in.h> |
| 10 #include <arpa/inet.h> | 10 #include <arpa/inet.h> |
| 11 #include <netdb.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 |
| 16 #define MAX_PATH 4096 | 16 #define MAX_PATH 4096 |
| 17 | |
| 18 // TODO: Move this to seobeo.... | |
| 19 // Asked AI to create this lol, probably should learn to decode it myself.. | |
| 20 static void url_decode(char *dst, const char *src) | |
| 21 { | |
| 22 char a, b; | |
| 23 while (*src) { | |
| 24 if ((*src == '%') && | |
| 25 ((a = src[1]) && (b = src[2])) && | |
| 26 (isxdigit(a) && isxdigit(b))) { | |
| 27 if (a >= 'a') a -= 'a'-'A'; | |
| 28 if (a >= 'A') a -= ('A' - 10); | |
| 29 else a -= '0'; | |
| 30 if (b >= 'a') b -= 'a'-'A'; | |
| 31 if (b >= 'A') b -= ('A' - 10); | |
| 32 else b -= '0'; | |
| 33 *dst++ = 16*a+b; | |
| 34 src+=3; | |
| 35 } else if (*src == '+') { | |
| 36 *dst++ = ' '; | |
| 37 src++; | |
| 38 } else { | |
| 39 *dst++ = *src++; | |
| 40 } | |
| 41 } | |
| 42 *dst = '\0'; | |
| 43 } | |
| 44 | 17 |
| 45 static char* sanitize_path(const char *input_path, Dowa_Arena *arena) | 18 static char* sanitize_path(const char *input_path, Dowa_Arena *arena) |
| 46 { | 19 { |
| 47 if (!input_path || strlen(input_path) == 0) | 20 if (!input_path || strlen(input_path) == 0) |
| 48 { | 21 { |
| 55 char *result = Dowa_Arena_Allocate(arena, len + 1); | 28 char *result = Dowa_Arena_Allocate(arena, len + 1); |
| 56 size_t j = 0; | 29 size_t j = 0; |
| 57 | 30 |
| 58 for (size_t i = 0; i < len; i++) | 31 for (size_t i = 0; i < len; i++) |
| 59 { | 32 { |
| 60 if (input_path[i] == '.' && (i == 0 || input_path[i-1] == '/')) { | 33 if (input_path[i] == '.' && (i == 0 || input_path[i-1] == '/')) |
| 61 if (i + 1 < len && input_path[i+1] == '.') { | 34 { |
| 35 if (i + 1 < len && input_path[i+1] == '.') | |
| 36 { | |
| 62 // Skip ".." | 37 // Skip ".." |
| 63 i++; | 38 i++; |
| 64 continue; | 39 continue; |
| 65 } | 40 } |
| 66 // Skip "." | 41 // Skip "." |
| 77 result[--j] = '\0'; | 52 result[--j] = '\0'; |
| 78 | 53 |
| 79 return result; | 54 return result; |
| 80 } | 55 } |
| 81 | 56 |
| 82 // Helper to connect to hg serve | 57 Seobeo_Client_Response *hg_proxy_request( |
| 83 static int hg_proxy_connect(void) | 58 const char *method, |
| 84 { | 59 const char *path, |
| 85 int sock = socket(AF_INET, SOCK_STREAM, 0); | 60 const char *req_body, |
| 86 if (sock < 0) | 61 const char *hg_custom) |
| 87 { | 62 { |
| 88 Seobeo_Log(SEOBEO_DEBUG, "Failed to create socket\n"); | 63 char full_path[MAX_PATH]; |
| 89 return -1; | 64 snprintf(full_path, MAX_PATH, "http://%s:%s%s", HG_SERVE_HOST, HG_SERVE_PORT, path); |
| 90 } | 65 Seobeo_Log(SEOBEO_DEBUG, "HG Proxy PATH %s\n", full_path); |
| 91 | 66 Seobeo_Client_Request *p_req = Seobeo_Client_Request_Create(full_path); |
| 92 struct sockaddr_in server_addr; | 67 Seobeo_Client_Request_Set_Method(p_req, method); |
| 93 memset(&server_addr, 0, sizeof(server_addr)); | 68 Seobeo_Client_Request_Add_Header_Array(p_req, "User-Agent: Seobeo/1.0"); |
| 94 server_addr.sin_family = AF_INET; | 69 Seobeo_Client_Request_Add_Header_Array(p_req, "Accept: application/json"); |
| 95 server_addr.sin_port = htons(HG_SERVE_PORT); | 70 |
| 96 inet_pton(AF_INET, HG_SERVE_HOST, &server_addr.sin_addr); | 71 if (hg_custom && hg_custom[0] != '\0') |
| 97 | 72 { |
| 98 if (connect(sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) | 73 char buffer[1024]; |
| 99 { | 74 snprintf(buffer, 1024, "x-hgarg-1: %s", hg_custom); |
| 100 Seobeo_Log(SEOBEO_DEBUG, "Failed to connect to hg serve at %s:%d\n", HG_SERVE_HOST, HG_SERVE_PORT); | 75 Seobeo_Client_Request_Add_Header_Array(p_req, buffer); |
| 101 close(sock); | 76 Seobeo_Log(SEOBEO_DEBUG, "HG CUSTOM %s\n", buffer); |
| 102 return -1; | 77 } |
| 103 } | 78 |
| 104 | 79 if (req_body) |
| 105 return sock; | 80 Seobeo_Client_Request_Set_Body(p_req, req_body, strlen(req_body)); |
| 106 } | 81 Seobeo_Client_Response *p_resp = Seobeo_Client_Request_Execute(p_req); |
| 107 | 82 Seobeo_Client_Request_Destroy(p_req); |
| 108 // Generic helper to proxy a request to hg serve and get the response body | 83 return p_resp; |
| 109 // Returns allocated body on success, NULL on failure | |
| 110 // out_status and out_content_type are optional output parameters | |
| 111 // out_body_len returns the actual body length (for binary content) | |
| 112 static char* hg_proxy_request( | |
| 113 const char *method, | |
| 114 const char *path, | |
| 115 const char *req_body, | |
| 116 size_t body_len, | |
| 117 char *out_status, // should be at least 4 bytes | |
| 118 char *out_content_type, // should be at least 256 bytes | |
| 119 size_t *out_body_len, // optional: returns actual body length | |
| 120 Dowa_Arena *arena) | |
| 121 { | |
| 122 int sock = hg_proxy_connect(); | |
| 123 if (sock < 0) return NULL; | |
| 124 | |
| 125 // Build HTTP request | |
| 126 char http_request[MAX_PATH * 2]; | |
| 127 snprintf(http_request, sizeof(http_request), | |
| 128 "%s %s HTTP/1.1\r\n" | |
| 129 "Host: %s:%d\r\n" | |
| 130 "Connection: close\r\n" | |
| 131 "Accept: application/json, text/plain, */*\r\n" | |
| 132 "Content-Length: %zu\r\n" | |
| 133 "\r\n", | |
| 134 method, path, HG_SERVE_HOST, HG_SERVE_PORT, body_len); | |
| 135 | |
| 136 Seobeo_Log(SEOBEO_DEBUG, "HG Proxy request: %s %s\n", method, path); | |
| 137 | |
| 138 if (send(sock, http_request, strlen(http_request), 0) < 0) | |
| 139 { | |
| 140 close(sock); | |
| 141 return NULL; | |
| 142 } | |
| 143 | |
| 144 if (body_len > 0 && req_body) | |
| 145 { | |
| 146 send(sock, req_body, body_len, 0); | |
| 147 } | |
| 148 | |
| 149 // Read response | |
| 150 int buffer_size = 1024 * 1024 * 5; // 5MB | |
| 151 char *response_buf = Dowa_Arena_Allocate(arena, buffer_size); | |
| 152 size_t total_read = 0; | |
| 153 ssize_t bytes_read; | |
| 154 | |
| 155 while ((bytes_read = recv(sock, response_buf + total_read, buffer_size - total_read - 1, 0)) > 0) | |
| 156 { | |
| 157 total_read += bytes_read; | |
| 158 if (total_read >= (size_t)(buffer_size - 1)) break; | |
| 159 } | |
| 160 response_buf[total_read] = '\0'; | |
| 161 close(sock); | |
| 162 | |
| 163 Seobeo_Log(SEOBEO_DEBUG, "HG Proxy: received %zu bytes\n", total_read); | |
| 164 | |
| 165 // Parse response headers - use memmem to handle binary content | |
| 166 char *headers_end = NULL; | |
| 167 for (size_t i = 0; i + 3 < total_read; i++) | |
| 168 { | |
| 169 if (response_buf[i] == '\r' && response_buf[i+1] == '\n' && | |
| 170 response_buf[i+2] == '\r' && response_buf[i+3] == '\n') | |
| 171 { | |
| 172 headers_end = response_buf + i; | |
| 173 break; | |
| 174 } | |
| 175 } | |
| 176 if (!headers_end) return NULL; | |
| 177 | |
| 178 // Extract status | |
| 179 if (out_status && strncmp(response_buf, "HTTP/", 5) == 0) | |
| 180 { | |
| 181 char *status_start = strchr(response_buf, ' '); | |
| 182 if (status_start) | |
| 183 { | |
| 184 strncpy(out_status, status_start + 1, 3); | |
| 185 out_status[3] = '\0'; | |
| 186 } | |
| 187 } | |
| 188 | |
| 189 // Extract content-type | |
| 190 if (out_content_type) | |
| 191 { | |
| 192 out_content_type[0] = '\0'; | |
| 193 char *ct_header = strcasestr(response_buf, "Content-Type:"); | |
| 194 if (ct_header && ct_header < headers_end) | |
| 195 { | |
| 196 ct_header += 13; | |
| 197 while (*ct_header == ' ') ct_header++; | |
| 198 char *ct_end = strpbrk(ct_header, "\r\n"); | |
| 199 if (ct_end) | |
| 200 { | |
| 201 size_t ct_len = ct_end - ct_header; | |
| 202 if (ct_len < 256) | |
| 203 { | |
| 204 strncpy(out_content_type, ct_header, ct_len); | |
| 205 out_content_type[ct_len] = '\0'; | |
| 206 } | |
| 207 } | |
| 208 } | |
| 209 } | |
| 210 | |
| 211 // Return body (copy to fresh allocation for clean pointer) | |
| 212 char *body = headers_end + 4; | |
| 213 size_t body_size = total_read - (body - response_buf); | |
| 214 | |
| 215 if (out_body_len) *out_body_len = body_size; | |
| 216 | |
| 217 char *result = Dowa_Arena_Allocate(arena, body_size + 1); | |
| 218 memcpy(result, body, body_size); | |
| 219 result[body_size] = '\0'; | |
| 220 | |
| 221 return result; | |
| 222 } | 84 } |
| 223 | 85 |
| 224 Seobeo_Request_Entry* ApiListDirectory(Seobeo_Request_Entry *req, Dowa_Arena *arena) | 86 Seobeo_Request_Entry* ApiListDirectory(Seobeo_Request_Entry *req, Dowa_Arena *arena) |
| 225 { | 87 { |
| 226 Seobeo_Request_Entry *resp = NULL; | 88 Seobeo_Request_Entry *resp = NULL; |
| 227 | 89 |
| 228 void *path_kv = Dowa_HashMap_Get_Ptr(req, "query_path"); | 90 void *path_kv = Dowa_HashMap_Get_Ptr(req, "query_path"); |
| 229 const char *rel_path = path_kv ? ((Seobeo_Request_Entry*)path_kv)->value : ""; | 91 const char *rel_path = path_kv ? ((Seobeo_Request_Entry*)path_kv)->value : ""; |
| 230 | 92 |
| 231 char *decoded_path = Dowa_Arena_Allocate(arena, strlen(rel_path) + 1); | 93 char *decoded_path = Dowa_Arena_Allocate(arena, strlen(rel_path) + 1); |
| 232 url_decode(decoded_path, rel_path); | 94 Seobeo_Url_Decode(decoded_path, rel_path); |
| 233 | 95 |
| 234 char *safe_path = sanitize_path(decoded_path, arena); | 96 char *safe_path = sanitize_path(decoded_path, arena); |
| 235 | 97 |
| 236 Seobeo_Log(SEOBEO_INFO, "ApiListDirectory: safe_path='%s'\n", safe_path); | 98 Seobeo_Log(SEOBEO_INFO, "ApiListDirectory: safe_path='%s'\n", safe_path); |
| 237 | 99 |
| 239 if (strlen(safe_path) > 0) | 101 if (strlen(safe_path) > 0) |
| 240 snprintf(hg_path, sizeof(hg_path), "/file/tip/%s?style=json", safe_path); | 102 snprintf(hg_path, sizeof(hg_path), "/file/tip/%s?style=json", safe_path); |
| 241 else | 103 else |
| 242 snprintf(hg_path, sizeof(hg_path), "/file/tip/?style=json"); | 104 snprintf(hg_path, sizeof(hg_path), "/file/tip/?style=json"); |
| 243 | 105 |
| 244 char status[4] = "200"; | 106 Seobeo_Client_Response *hg_response = hg_proxy_request("GET", hg_path, NULL, NULL); |
| 245 char content_type[256] = ""; | 107 |
| 246 size_t body_len = 0; | 108 Seobeo_Log(SEOBEO_DEBUG, "ApiListDirectory: status=%i body_len=%zu\n", hg_response->status_code, hg_response->body_length); |
| 247 char *hg_response = hg_proxy_request("GET", hg_path, NULL, 0, status, content_type, &body_len, arena); | 109 |
| 248 | 110 if (hg_response->status_code != 200) |
| 249 Seobeo_Log(SEOBEO_DEBUG, "ApiListDirectory: status=%s body_len=%zu\n", status, body_len); | |
| 250 | |
| 251 if (!hg_response || status[0] != '2') | |
| 252 { | 111 { |
| 253 Seobeo_Log(SEOBEO_DEBUG, "Failed to get directory from hg serve\n"); | 112 Seobeo_Log(SEOBEO_DEBUG, "Failed to get directory from hg serve\n"); |
| 254 Dowa_HashMap_Push_Arena(resp, "status", "502", arena); | 113 Dowa_HashMap_Push_Arena(resp, "status", "502", arena); |
| 255 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); | 114 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); |
| 256 Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Failed to connect to hg serve\"}", arena); | 115 Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Failed to connect to hg serve\"}", arena); |
| 257 return resp; | 116 return resp; |
| 258 } | 117 } |
| 259 char *json = hg_response; | 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); | |
| 260 | 122 |
| 261 Dowa_HashMap_Push_Arena(resp, "status", "200", arena); | 123 Dowa_HashMap_Push_Arena(resp, "status", "200", arena); |
| 262 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); | 124 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); |
| 263 Dowa_HashMap_Push_Arena(resp, "body", 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); | |
| 264 | 191 |
| 265 return resp; | 192 return resp; |
| 266 } | 193 } |
| 267 | 194 |
| 268 Seobeo_Request_Entry* ApiGetFile(Seobeo_Request_Entry *req, Dowa_Arena *arena) | 195 Seobeo_Request_Entry* ApiGetFile(Seobeo_Request_Entry *req, Dowa_Arena *arena) |
| 270 Seobeo_Request_Entry *resp = NULL; | 197 Seobeo_Request_Entry *resp = NULL; |
| 271 | 198 |
| 272 void *path_kv = Dowa_HashMap_Get_Ptr(req, "query_path"); | 199 void *path_kv = Dowa_HashMap_Get_Ptr(req, "query_path"); |
| 273 const char *rel_path = path_kv ? ((Seobeo_Request_Entry*)path_kv)->value : ""; | 200 const char *rel_path = path_kv ? ((Seobeo_Request_Entry*)path_kv)->value : ""; |
| 274 char *decoded_path = Dowa_Arena_Allocate(arena, strlen(rel_path) + 1); | 201 char *decoded_path = Dowa_Arena_Allocate(arena, strlen(rel_path) + 1); |
| 275 url_decode(decoded_path, rel_path); | 202 Seobeo_Url_Decode(decoded_path, rel_path); |
| 276 char *safe_path = sanitize_path(decoded_path, arena); | 203 char *safe_path = sanitize_path(decoded_path, arena); |
| 277 | 204 |
| 278 Seobeo_Log(SEOBEO_INFO, "ApiGetFile: safe_path='%s'\n", safe_path); | 205 Seobeo_Log(SEOBEO_INFO, "ApiGetFile: safe_path='%s'\n", safe_path); |
| 279 | 206 |
| 280 if (strlen(safe_path) == 0) | 207 if (strlen(safe_path) == 0) |
| 283 Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); | 210 Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); |
| 284 Dowa_HashMap_Push_Arena(resp, "body", "File path required", arena); | 211 Dowa_HashMap_Push_Arena(resp, "body", "File path required", arena); |
| 285 return resp; | 212 return resp; |
| 286 } | 213 } |
| 287 | 214 |
| 288 // Build hg serve URL: /raw-file/tip/<path> | |
| 289 char hg_path[MAX_PATH]; | 215 char hg_path[MAX_PATH]; |
| 290 snprintf(hg_path, sizeof(hg_path), "/raw-file/tip/%s", safe_path); | 216 snprintf(hg_path, sizeof(hg_path), "/raw-file/tip/%s", safe_path); |
| 291 | 217 Seobeo_Client_Response *hg_response = hg_proxy_request("GET", hg_path, NULL, NULL); |
| 292 char status[4] = "200"; | 218 |
| 293 char content_type[256] = ""; | 219 Seobeo_Log(SEOBEO_DEBUG, "ApiGetFile: status=%i body_len=%zu\n", hg_response->status_code, hg_response->body_length); |
| 294 size_t body_len = 0; | 220 |
| 295 char *body = hg_proxy_request("GET", hg_path, NULL, 0, status, content_type, &body_len, arena); | 221 char status[4]; |
| 296 | 222 snprintf(status, 4, "%i", hg_response->status_code); |
| 297 Seobeo_Log(SEOBEO_DEBUG, "ApiGetFile: status=%s body_len=%zu\n", status, body_len); | 223 |
| 298 | 224 if (!hg_response->body) |
| 299 if (!body) | |
| 300 { | 225 { |
| 301 Dowa_HashMap_Push_Arena(resp, "status", "502", arena); | 226 Dowa_HashMap_Push_Arena(resp, "status", "502", arena); |
| 302 Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); | 227 Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); |
| 303 Dowa_HashMap_Push_Arena(resp, "body", "Failed to connect to hg serve", arena); | 228 Dowa_HashMap_Push_Arena(resp, "body", "Failed to connect to hg serve", arena); |
| 304 return resp; | 229 return resp; |
| 305 } | 230 } |
| 306 | 231 |
| 307 if (status[0] != '2') | 232 if (hg_response->status_code != 200) |
| 308 { | 233 { |
| 309 Seobeo_Log(SEOBEO_DEBUG, "ApiGetFile: error response: %s\n", body); | 234 Seobeo_Log(SEOBEO_DEBUG, "ApiGetFile: error hg_response: %s\n", hg_response->body); |
| 310 Dowa_HashMap_Push_Arena(resp, "status", status, arena); | 235 Dowa_HashMap_Push_Arena(resp, "status", status, arena); |
| 311 Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); | 236 Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); |
| 312 // Return actual error from hg serve if available | 237 Dowa_HashMap_Push_Arena(resp, "body", hg_response->body, arena); |
| 313 Dowa_HashMap_Push_Arena(resp, "body", body_len > 0 ? body : "File not found", arena); | 238 return resp; |
| 314 return resp; | 239 } |
| 315 } | 240 |
| 316 | 241 |
| 317 // Use content-type from hg serve, or determine from extension | 242 char *temp1 = Dowa_Arena_Copy(arena, hg_response->body, hg_response->body_length); |
| 318 const char *final_content_type = content_type; | 243 char *temp2 = Dowa_Arena_Allocate(arena, 256); |
| 319 if (strlen(content_type) == 0 || strcmp(content_type, "application/octet-stream") == 0) | 244 snprintf(temp2, 256, "%zu", hg_response->body_length); |
| 320 { | |
| 321 final_content_type = "text/plain"; | |
| 322 if (strstr(safe_path, ".md")) final_content_type = "text/markdown"; | |
| 323 else if (strstr(safe_path, ".html")) final_content_type = "text/html"; | |
| 324 else if (strstr(safe_path, ".css")) final_content_type = "text/css"; | |
| 325 else if (strstr(safe_path, ".js")) final_content_type = "application/javascript"; | |
| 326 else if (strstr(safe_path, ".json")) final_content_type = "application/json"; | |
| 327 } | |
| 328 | 245 |
| 329 Dowa_HashMap_Push_Arena(resp, "status", "200", arena); | 246 Dowa_HashMap_Push_Arena(resp, "status", "200", arena); |
| 330 Dowa_HashMap_Push_Arena(resp, "content-type", final_content_type, arena); | 247 Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); |
| 331 Dowa_HashMap_Push_Arena(resp, "body", body, arena); | 248 Dowa_HashMap_Push_Arena(resp, "body", temp1, arena); |
| 249 Dowa_HashMap_Push_Arena(resp, "content-length", temp2, arena); | |
| 332 | 250 |
| 333 return resp; | 251 return resp; |
| 334 } | 252 } |
| 335 | 253 |
| 336 Seobeo_Request_Entry* ApiGetReadme(Seobeo_Request_Entry *req, Dowa_Arena *arena) { | 254 Seobeo_Request_Entry* ApiGetReadme(Seobeo_Request_Entry *req, Dowa_Arena *arena) { |
| 337 return ApiGetFile(req, arena); | 255 return ApiGetFile(req, arena); |
| 338 } | 256 } |
| 339 | 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; | |
| 328 if (r == 0) | |
| 329 continue; | |
| 330 } | |
| 331 | |
| 332 // TODO: Maybe make this into a separate function instead of internal function as doing this over and over again blows. | |
| 333 char *hdr_end = strstr((char*)p_upstream->read_buffer, "\r\n\r\n"); | |
| 334 if (!hdr_end) | |
| 335 { | |
| 336 Seobeo_Handle_Destroy(p_upstream); | |
| 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 | |
| 340 Seobeo_Request_Entry* ApiHgWireProtocol(Seobeo_Request_Entry *req, Dowa_Arena *arena) | 369 Seobeo_Request_Entry* ApiHgWireProtocol(Seobeo_Request_Entry *req, Dowa_Arena *arena) |
| 341 { | 370 { |
| 342 Seobeo_Request_Entry *resp = NULL; | 371 Seobeo_Request_Entry *resp = NULL; |
| 343 | 372 |
| 344 // Get method | 373 void *method_kv = Dowa_HashMap_Get_Ptr(req, "HTTP_Method"); |
| 345 void *method_kv = Dowa_HashMap_Get_Ptr(req, "HTTP_Method"); | 374 const char *method = method_kv ? ((Seobeo_Request_Entry*)method_kv)->value : "GET"; |
| 346 const char *method = method_kv ? ((Seobeo_Request_Entry*)method_kv)->value : "GET"; | 375 |
| 347 | 376 void *query_kv = Dowa_HashMap_Get_Ptr(req, "QueryString"); |
| 348 // Get query string | 377 const char *query_string = query_kv ? ((Seobeo_Request_Entry*)query_kv)->value : ""; |
| 349 void *query_kv = Dowa_HashMap_Get_Ptr(req, "QueryString"); | 378 |
| 350 const char *query_string = query_kv ? ((Seobeo_Request_Entry*)query_kv)->value : ""; | 379 void *body_kv = Dowa_HashMap_Get_Ptr(req, "Body"); |
| 351 | 380 const char *req_body = body_kv ? ((Seobeo_Request_Entry*)body_kv)->value : ""; |
| 352 // Get request body for POST | 381 size_t body_len = strlen(req_body); |
| 353 void *body_kv = Dowa_HashMap_Get_Ptr(req, "Body"); | 382 |
| 354 const char *req_body = body_kv ? ((Seobeo_Request_Entry*)body_kv)->value : ""; | 383 const char *hg_custom = req[7].value; |
| 355 size_t body_len = strlen(req_body); | 384 Seobeo_Log(SEOBEO_DEBUG, "HG Proxy: method=%s query=%s body_len=%zu\n", method, query_string, body_len); |
| 356 | 385 |
| 357 Seobeo_Log(SEOBEO_DEBUG, "HG Proxy: method=%s query=%s body_len=%zu\n", method, query_string, body_len); | 386 Seobeo_Client_Response *hg_response; |
| 358 | 387 |
| 359 // Connect to hg serve | 388 char hg_path[MAX_PATH]; |
| 360 int sock = hg_proxy_connect(); | 389 snprintf(hg_path, sizeof(hg_path), "/?%s", query_string); |
| 361 if (sock < 0) | 390 |
| 362 { | 391 hg_response = hg_proxy_request(method, hg_path, req_body, hg_custom); |
| 363 Dowa_HashMap_Push_Arena(resp, "status", "502", arena); | 392 |
| 364 Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); | 393 Seobeo_Log(SEOBEO_DEBUG, "HG Proxy: received %zu bytes\n", hg_response->body_length); |
| 365 Dowa_HashMap_Push_Arena(resp, "body", "Failed to connect to hg serve", arena); | 394 |
| 366 return resp; | 395 Seobeo_Request_Entry *kv = Dowa_HashMap_Get_Ptr(hg_response->headers, "Content-Type"); |
| 367 } | 396 |
| 368 | 397 char *status = Dowa_Arena_Allocate(arena, 5); |
| 369 // Build the HTTP request to forward to hg serve | 398 snprintf(status, 4, "%i", hg_response->status_code); |
| 370 char http_request[MAX_PATH * 2]; | 399 |
| 371 if (strlen(query_string) > 0) | 400 // Use binary-safe copy to handle null bytes in mercurial bundle data |
| 372 { | 401 char *temp1 = Dowa_Arena_Copy(arena, hg_response->body, hg_response->body_length); |
| 373 snprintf(http_request, sizeof(http_request), | 402 char *temp2 = Dowa_Arena_Allocate(arena, 256); |
| 374 "%s /?%s HTTP/1.1\r\n" | 403 snprintf(temp2, 256, "%zu", hg_response->body_length); |
| 375 "Host: %s:%d\r\n" | 404 |
| 376 "Connection: close\r\n" | 405 Dowa_HashMap_Push_Arena(resp, "status", status, arena); |
| 377 "Content-Length: %zu\r\n" | 406 Dowa_HashMap_Push_Arena(resp, "content-type", kv->value, arena); |
| 378 "\r\n", | 407 Dowa_HashMap_Push_Arena(resp, "body", temp1, arena); |
| 379 method, query_string, HG_SERVE_HOST, HG_SERVE_PORT, body_len); | 408 Dowa_HashMap_Push_Arena(resp, "content-length", temp2, arena); |
| 380 } | 409 |
| 381 else | 410 return resp; |
| 382 { | 411 } |
| 383 snprintf(http_request, sizeof(http_request), | 412 |
| 384 "%s / HTTP/1.1\r\n" | 413 Seobeo_Request_Entry* GetReactHome(Seobeo_Request_Entry *req, Dowa_Arena *arena) |
| 385 "Host: %s:%d\r\n" | 414 { |
| 386 "Connection: close\r\n" | 415 size_t file_size = 0; |
| 387 "Content-Length: %zu\r\n" | 416 char *html = Seobeo_Web_LoadFile("/index.html", &file_size); |
| 388 "\r\n", | 417 |
| 389 method, HG_SERVE_HOST, HG_SERVE_PORT, body_len); | 418 printf("%s", html); |
| 390 } | 419 Seobeo_Request_Entry *resp = NULL; |
| 391 | 420 Dowa_HashMap_Push_Arena(resp, "status", "200", arena); |
| 392 // Send HTTP request headers | 421 Dowa_HashMap_Push_Arena(resp, "content-type", "text/html", arena); |
| 393 if (send(sock, http_request, strlen(http_request), 0) < 0) | 422 Dowa_HashMap_Push_Arena(resp, "body", html, arena); |
| 394 { | 423 return resp; |
| 395 Seobeo_Log(SEOBEO_DEBUG, "Failed to send request to hg serve\n"); | |
| 396 close(sock); | |
| 397 Dowa_HashMap_Push_Arena(resp, "status", "502", arena); | |
| 398 Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); | |
| 399 Dowa_HashMap_Push_Arena(resp, "body", "Failed to send to hg serve", arena); | |
| 400 return resp; | |
| 401 } | |
| 402 | |
| 403 // Send body if present | |
| 404 if (body_len > 0) | |
| 405 { | |
| 406 send(sock, req_body, body_len, 0); | |
| 407 } | |
| 408 | |
| 409 // Read response from hg serve | |
| 410 int buffer_size = 1024 * 1024 * 5; // 5MB | |
| 411 char *response_buf = Dowa_Arena_Allocate(arena, buffer_size); | |
| 412 size_t total_read = 0; | |
| 413 ssize_t bytes_read; | |
| 414 | |
| 415 while ((bytes_read = recv(sock, response_buf + total_read, buffer_size - total_read - 1, 0)) > 0) | |
| 416 { | |
| 417 total_read += bytes_read; | |
| 418 if (total_read >= (size_t)(buffer_size - 1)) break; | |
| 419 } | |
| 420 response_buf[total_read] = '\0'; | |
| 421 close(sock); | |
| 422 | |
| 423 Seobeo_Log(SEOBEO_DEBUG, "HG Proxy: received %zu bytes\n", total_read); | |
| 424 | |
| 425 // Parse HTTP response - find headers end | |
| 426 char *headers_end = strstr(response_buf, "\r\n\r\n"); | |
| 427 if (!headers_end) | |
| 428 { | |
| 429 Dowa_HashMap_Push_Arena(resp, "status", "502", arena); | |
| 430 Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); | |
| 431 Dowa_HashMap_Push_Arena(resp, "body", "Invalid response from hg serve", arena); | |
| 432 return resp; | |
| 433 } | |
| 434 | |
| 435 // Extract status code from first line (e.g., "HTTP/1.1 200 OK") | |
| 436 char status_code[4] = "200"; | |
| 437 if (strncmp(response_buf, "HTTP/", 5) == 0) | |
| 438 { | |
| 439 char *status_start = strchr(response_buf, ' '); | |
| 440 if (status_start) | |
| 441 { | |
| 442 strncpy(status_code, status_start + 1, 3); | |
| 443 status_code[3] = '\0'; | |
| 444 } | |
| 445 } | |
| 446 | |
| 447 // Extract content-type from headers | |
| 448 const char *content_type = "application/mercurial-0.1"; | |
| 449 char *ct_header = strcasestr(response_buf, "Content-Type:"); | |
| 450 if (ct_header && ct_header < headers_end) | |
| 451 { | |
| 452 ct_header += 13; // Skip "Content-Type:" | |
| 453 while (*ct_header == ' ') ct_header++; | |
| 454 char *ct_end = strpbrk(ct_header, "\r\n"); | |
| 455 if (ct_end) | |
| 456 { | |
| 457 size_t ct_len = ct_end - ct_header; | |
| 458 char *ct_copy = Dowa_Arena_Allocate(arena, ct_len + 1); | |
| 459 strncpy(ct_copy, ct_header, ct_len); | |
| 460 ct_copy[ct_len] = '\0'; | |
| 461 content_type = ct_copy; | |
| 462 } | |
| 463 } | |
| 464 | |
| 465 // Body starts after \r\n\r\n | |
| 466 char *body = headers_end + 4; | |
| 467 | |
| 468 Dowa_HashMap_Push_Arena(resp, "status", status_code, arena); | |
| 469 Dowa_HashMap_Push_Arena(resp, "content-type", content_type, arena); | |
| 470 Dowa_HashMap_Push_Arena(resp, "body", body, arena); | |
| 471 | |
| 472 return resp; | |
| 473 } | 424 } |
| 474 | 425 |
| 475 int main(void) { | 426 int main(void) { |
| 476 Seobeo_Router_Init(); | 427 Seobeo_Router_Init(); |
| 477 | 428 |
| 429 | |
| 430 Seobeo_Router_Register("GET", "/", GetReactHome); | |
| 431 Seobeo_Router_Register("GET", "/directories", GetReactHome); | |
| 432 Seobeo_Router_Register("GET", "/graph", GetReactHome); | |
| 433 | |
| 478 Seobeo_Router_Register("GET", "/api/repo/list", ApiListDirectory); | 434 Seobeo_Router_Register("GET", "/api/repo/list", ApiListDirectory); |
| 479 Seobeo_Router_Register("GET", "/api/repo/file", ApiGetFile); | 435 Seobeo_Router_Register("GET", "/api/repo/file", ApiGetFile); |
| 436 Seobeo_Router_Register("GET", "/api/graph/:graph_id", ApiGetGraph); | |
| 480 Seobeo_Router_Register("GET", "/api/repo/readme", ApiGetReadme); | 437 Seobeo_Router_Register("GET", "/api/repo/readme", ApiGetReadme); |
| 481 | 438 |
| 482 Seobeo_Router_Register("GET", "/repo", ApiHgWireProtocol); | 439 // Use streaming handler for hg wire protocol... |
| 483 Seobeo_Router_Register("POST", "/repo", ApiHgWireProtocol); | 440 Seobeo_Router_Register_Stream("GET", "/repo", StreamHgWireProtocol); |
| 441 Seobeo_Router_Register_Stream("POST", "/repo", StreamHgWireProtocol); | |
| 484 | 442 |
| 485 printf("Starting on Port 6970...\n"); | 443 printf("Starting on Port 6970...\n"); |
| 486 printf("Repository: %s\n", REPO_ROOT); | 444 |
| 487 | 445 int result = Seobeo_Web_Server_Start("hg-web/src", "6970", SEOBEO_MODE_EDGE, 1); |
| 488 int result = Seobeo_Web_Server_Start("hg-web/src", "6970", SEOBEO_MODE_EDGE, 4); | |
| 489 | 446 |
| 490 Seobeo_Router_Destroy(); | 447 Seobeo_Router_Destroy(); |
| 491 | 448 |
| 492 return result; | 449 return result; |
| 493 } | 450 } |