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 }