/Users/alexjokela/projects/lattice/src/http.c
Line | Count | Source |
1 | | #include "http.h" |
2 | | #include "net.h" |
3 | | #include "tls.h" |
4 | | #include <stdlib.h> |
5 | | #include <string.h> |
6 | | #include <stdio.h> |
7 | | #include <ctype.h> |
8 | | |
9 | | #ifdef __EMSCRIPTEN__ |
10 | | |
11 | | bool http_parse_url(const char *url, HttpUrl *out, char **err) { |
12 | | (void)url; (void)out; |
13 | | *err = strdup("HTTP not available in WASM"); |
14 | | return false; |
15 | | } |
16 | | |
17 | | void http_url_free(HttpUrl *url) { (void)url; } |
18 | | |
19 | | HttpResponse *http_execute(const HttpRequest *req, char **err) { |
20 | | (void)req; |
21 | | *err = strdup("HTTP not available in WASM"); |
22 | | return NULL; |
23 | | } |
24 | | |
25 | | void http_response_free(HttpResponse *resp) { (void)resp; } |
26 | | |
27 | | #else /* !__EMSCRIPTEN__ */ |
28 | | |
29 | | /* ── URL parsing ── */ |
30 | | |
31 | 9 | bool http_parse_url(const char *url, HttpUrl *out, char **err) { |
32 | 9 | memset(out, 0, sizeof(*out)); |
33 | | |
34 | | /* Parse scheme */ |
35 | 9 | if (strncmp(url, "https://", 8) == 0) { |
36 | 0 | out->scheme = strdup("https"); |
37 | 0 | url += 8; |
38 | 9 | } else if (strncmp(url, "http://", 7) == 0) { |
39 | 0 | out->scheme = strdup("http"); |
40 | 0 | url += 7; |
41 | 9 | } else { |
42 | 9 | *err = strdup("invalid URL: must start with http:// or https://"); |
43 | 9 | return false; |
44 | 9 | } |
45 | | |
46 | | /* Find path separator */ |
47 | 0 | const char *slash = strchr(url, '/'); |
48 | 0 | size_t host_len = slash ? (size_t)(slash - url) : strlen(url); |
49 | | |
50 | | /* Extract host[:port] */ |
51 | 0 | char *host_part = strndup(url, host_len); |
52 | 0 | const char *colon = strchr(host_part, ':'); |
53 | 0 | if (colon) { |
54 | 0 | out->host = strndup(host_part, (size_t)(colon - host_part)); |
55 | 0 | out->port = atoi(colon + 1); |
56 | 0 | if (out->port <= 0 || out->port > 65535) { |
57 | 0 | free(host_part); |
58 | 0 | http_url_free(out); |
59 | 0 | *err = strdup("invalid URL: bad port number"); |
60 | 0 | return false; |
61 | 0 | } |
62 | 0 | } else { |
63 | 0 | out->host = strdup(host_part); |
64 | 0 | out->port = (strcmp(out->scheme, "https") == 0) ? 443 : 80; |
65 | 0 | } |
66 | 0 | free(host_part); |
67 | |
|
68 | 0 | if (strlen(out->host) == 0) { |
69 | 0 | http_url_free(out); |
70 | 0 | *err = strdup("invalid URL: empty host"); |
71 | 0 | return false; |
72 | 0 | } |
73 | | |
74 | | /* Path (default to "/") */ |
75 | 0 | out->path = strdup(slash ? slash : "/"); |
76 | 0 | return true; |
77 | 0 | } |
78 | | |
79 | 0 | void http_url_free(HttpUrl *url) { |
80 | 0 | free(url->scheme); |
81 | 0 | free(url->host); |
82 | 0 | free(url->path); |
83 | 0 | memset(url, 0, sizeof(*url)); |
84 | 0 | } |
85 | | |
86 | | /* ── Request formatting ── */ |
87 | | |
88 | 0 | static char *format_request(const HttpRequest *req, const HttpUrl *url) { |
89 | | /* Calculate buffer size */ |
90 | 0 | size_t cap = 256; |
91 | 0 | cap += strlen(req->method) + strlen(url->path) + strlen(url->host); |
92 | 0 | for (size_t i = 0; i < req->header_count; i++) |
93 | 0 | cap += strlen(req->header_keys[i]) + strlen(req->header_values[i]) + 8; |
94 | 0 | if (req->body) cap += 64 + req->body_len; /* Content-Length header + body */ |
95 | |
|
96 | 0 | char *buf = malloc(cap); |
97 | 0 | size_t pos = 0; |
98 | | |
99 | | /* Request line */ |
100 | 0 | pos += (size_t)snprintf(buf + pos, cap - pos, "%s %s HTTP/1.1\r\n", req->method, url->path); |
101 | | |
102 | | /* Host header */ |
103 | 0 | if ((strcmp(url->scheme, "http") == 0 && url->port != 80) || |
104 | 0 | (strcmp(url->scheme, "https") == 0 && url->port != 443)) { |
105 | 0 | pos += (size_t)snprintf(buf + pos, cap - pos, "Host: %s:%d\r\n", url->host, url->port); |
106 | 0 | } else { |
107 | 0 | pos += (size_t)snprintf(buf + pos, cap - pos, "Host: %s\r\n", url->host); |
108 | 0 | } |
109 | | |
110 | | /* User headers */ |
111 | 0 | for (size_t i = 0; i < req->header_count; i++) |
112 | 0 | pos += (size_t)snprintf(buf + pos, cap - pos, "%s: %s\r\n", req->header_keys[i], req->header_values[i]); |
113 | | |
114 | | /* Content-Length for request body */ |
115 | 0 | if (req->body && req->body_len > 0) |
116 | 0 | pos += (size_t)snprintf(buf + pos, cap - pos, "Content-Length: %zu\r\n", req->body_len); |
117 | | |
118 | | /* Connection close */ |
119 | 0 | pos += (size_t)snprintf(buf + pos, cap - pos, "Connection: close\r\n\r\n"); |
120 | | |
121 | | /* Body */ |
122 | 0 | if (req->body && req->body_len > 0) { |
123 | 0 | memcpy(buf + pos, req->body, req->body_len); |
124 | 0 | pos += req->body_len; |
125 | 0 | } |
126 | 0 | buf[pos] = '\0'; |
127 | 0 | return buf; |
128 | 0 | } |
129 | | |
130 | | /* ── Response parsing ── */ |
131 | | |
132 | 0 | static HttpResponse *parse_response(const char *raw, size_t raw_len, char **err) { |
133 | | /* Find end of headers */ |
134 | 0 | const char *hdr_end = strstr(raw, "\r\n\r\n"); |
135 | 0 | if (!hdr_end) { |
136 | 0 | *err = strdup("invalid HTTP response: no header/body separator"); |
137 | 0 | return NULL; |
138 | 0 | } |
139 | | |
140 | 0 | HttpResponse *resp = calloc(1, sizeof(HttpResponse)); |
141 | | |
142 | | /* Parse status line: HTTP/1.x STATUS REASON */ |
143 | 0 | const char *line_end = strstr(raw, "\r\n"); |
144 | 0 | if (!line_end || strncmp(raw, "HTTP/", 5) != 0) { |
145 | 0 | *err = strdup("invalid HTTP response: bad status line"); |
146 | 0 | free(resp); |
147 | 0 | return NULL; |
148 | 0 | } |
149 | | |
150 | | /* Find status code (skip "HTTP/x.x ") */ |
151 | 0 | const char *p = raw; |
152 | 0 | while (p < line_end && *p != ' ') p++; |
153 | 0 | if (p < line_end) p++; /* skip space */ |
154 | 0 | resp->status_code = atoi(p); |
155 | | |
156 | | /* Parse headers */ |
157 | 0 | size_t hdr_cap = 16; |
158 | 0 | resp->header_keys = malloc(hdr_cap * sizeof(char *)); |
159 | 0 | resp->header_values = malloc(hdr_cap * sizeof(char *)); |
160 | 0 | resp->header_count = 0; |
161 | |
|
162 | 0 | const char *hdr = line_end + 2; /* skip first \r\n */ |
163 | 0 | while (hdr < hdr_end) { |
164 | 0 | const char *next = strstr(hdr, "\r\n"); |
165 | 0 | if (!next || next == hdr) break; |
166 | | |
167 | 0 | const char *colon = memchr(hdr, ':', (size_t)(next - hdr)); |
168 | 0 | if (colon) { |
169 | 0 | if (resp->header_count >= hdr_cap) { |
170 | 0 | hdr_cap *= 2; |
171 | 0 | resp->header_keys = realloc(resp->header_keys, hdr_cap * sizeof(char *)); |
172 | 0 | resp->header_values = realloc(resp->header_values, hdr_cap * sizeof(char *)); |
173 | 0 | } |
174 | | /* Key: lowercase for easy lookup */ |
175 | 0 | size_t klen = (size_t)(colon - hdr); |
176 | 0 | char *key = strndup(hdr, klen); |
177 | 0 | for (size_t i = 0; i < klen; i++) key[i] = (char)tolower((unsigned char)key[i]); |
178 | | |
179 | | /* Value: skip leading whitespace */ |
180 | 0 | const char *val = colon + 1; |
181 | 0 | while (val < next && *val == ' ') val++; |
182 | 0 | char *value = strndup(val, (size_t)(next - val)); |
183 | |
|
184 | 0 | resp->header_keys[resp->header_count] = key; |
185 | 0 | resp->header_values[resp->header_count] = value; |
186 | 0 | resp->header_count++; |
187 | 0 | } |
188 | 0 | hdr = next + 2; |
189 | 0 | } |
190 | | |
191 | | /* Body */ |
192 | 0 | const char *body_start = hdr_end + 4; |
193 | 0 | size_t body_len = raw_len - (size_t)(body_start - raw); |
194 | | |
195 | | /* Check for chunked transfer encoding */ |
196 | 0 | bool chunked = false; |
197 | 0 | for (size_t i = 0; i < resp->header_count; i++) { |
198 | 0 | if (strcmp(resp->header_keys[i], "transfer-encoding") == 0 && |
199 | 0 | strstr(resp->header_values[i], "chunked")) { |
200 | 0 | chunked = true; |
201 | 0 | break; |
202 | 0 | } |
203 | 0 | } |
204 | |
|
205 | 0 | if (chunked) { |
206 | | /* Decode chunked body */ |
207 | 0 | size_t bcap = body_len > 0 ? body_len : 128; |
208 | 0 | char *body = malloc(bcap); |
209 | 0 | size_t bpos = 0; |
210 | 0 | const char *cp = body_start; |
211 | 0 | const char *end = raw + raw_len; |
212 | |
|
213 | 0 | while (cp < end) { |
214 | | /* Read chunk size (hex) */ |
215 | 0 | char *endptr; |
216 | 0 | unsigned long chunk_size = strtoul(cp, &endptr, 16); |
217 | 0 | if (endptr == cp) break; |
218 | | |
219 | | /* Skip to after \r\n */ |
220 | 0 | cp = strstr(cp, "\r\n"); |
221 | 0 | if (!cp) break; |
222 | 0 | cp += 2; |
223 | |
|
224 | 0 | if (chunk_size == 0) break; /* Final chunk */ |
225 | | |
226 | 0 | while (bpos + chunk_size + 1 > bcap) { bcap *= 2; body = realloc(body, bcap); } |
227 | 0 | size_t avail = (size_t)(end - cp); |
228 | 0 | size_t to_copy = chunk_size < avail ? chunk_size : avail; |
229 | 0 | memcpy(body + bpos, cp, to_copy); |
230 | 0 | bpos += to_copy; |
231 | 0 | cp += to_copy; |
232 | | |
233 | | /* Skip trailing \r\n */ |
234 | 0 | if (cp + 2 <= end && cp[0] == '\r' && cp[1] == '\n') cp += 2; |
235 | 0 | } |
236 | 0 | body[bpos] = '\0'; |
237 | 0 | resp->body = body; |
238 | 0 | resp->body_len = bpos; |
239 | 0 | } else { |
240 | 0 | resp->body = malloc(body_len + 1); |
241 | 0 | memcpy(resp->body, body_start, body_len); |
242 | 0 | resp->body[body_len] = '\0'; |
243 | 0 | resp->body_len = body_len; |
244 | 0 | } |
245 | |
|
246 | 0 | return resp; |
247 | 0 | } |
248 | | |
249 | 0 | void http_response_free(HttpResponse *resp) { |
250 | 0 | if (!resp) return; |
251 | 0 | for (size_t i = 0; i < resp->header_count; i++) { |
252 | 0 | free(resp->header_keys[i]); |
253 | 0 | free(resp->header_values[i]); |
254 | 0 | } |
255 | 0 | free(resp->header_keys); |
256 | 0 | free(resp->header_values); |
257 | 0 | free(resp->body); |
258 | 0 | free(resp); |
259 | 0 | } |
260 | | |
261 | | /* ── Execute request ── */ |
262 | | |
263 | 9 | HttpResponse *http_execute(const HttpRequest *req, char **err) { |
264 | 9 | HttpUrl url; |
265 | 9 | if (!http_parse_url(req->url, &url, err)) |
266 | 9 | return NULL; |
267 | | |
268 | 9 | bool use_tls = (strcmp(url.scheme, "https") == 0); |
269 | | |
270 | | /* Connect */ |
271 | 0 | int fd; |
272 | 0 | if (use_tls) { |
273 | 0 | fd = net_tls_connect(url.host, url.port, err); |
274 | 0 | } else { |
275 | 0 | fd = net_tcp_connect(url.host, url.port, err); |
276 | 0 | } |
277 | 0 | if (fd < 0) { |
278 | 0 | http_url_free(&url); |
279 | 0 | return NULL; |
280 | 0 | } |
281 | | |
282 | | /* Set timeout */ |
283 | 0 | int timeout_s = (req->timeout_ms > 0) ? (req->timeout_ms / 1000) : 30; |
284 | 0 | if (timeout_s < 1) timeout_s = 1; |
285 | 0 | net_tcp_set_timeout(fd, timeout_s, err); |
286 | | |
287 | | /* Format and send request */ |
288 | 0 | char *raw_req = format_request(req, &url); |
289 | 0 | bool ok; |
290 | 0 | if (use_tls) { |
291 | 0 | ok = net_tls_write(fd, raw_req, strlen(raw_req), err); |
292 | 0 | } else { |
293 | 0 | ok = net_tcp_write(fd, raw_req, strlen(raw_req), err); |
294 | 0 | } |
295 | 0 | free(raw_req); |
296 | |
|
297 | 0 | if (!ok) { |
298 | 0 | if (use_tls) net_tls_close(fd); else net_tcp_close(fd); |
299 | 0 | http_url_free(&url); |
300 | 0 | return NULL; |
301 | 0 | } |
302 | | |
303 | | /* Read response */ |
304 | 0 | size_t resp_cap = 8192; |
305 | 0 | size_t resp_len = 0; |
306 | 0 | char *resp_buf = malloc(resp_cap); |
307 | |
|
308 | 0 | for (;;) { |
309 | 0 | char *chunk; |
310 | 0 | if (use_tls) { |
311 | 0 | chunk = net_tls_read(fd, err); |
312 | 0 | } else { |
313 | 0 | chunk = net_tcp_read(fd, err); |
314 | 0 | } |
315 | 0 | if (!chunk) { |
316 | | /* Read error — but if we already have data, try to parse it */ |
317 | 0 | if (resp_len > 0) { free(*err); *err = NULL; break; } |
318 | 0 | free(resp_buf); |
319 | 0 | if (use_tls) net_tls_close(fd); else net_tcp_close(fd); |
320 | 0 | http_url_free(&url); |
321 | 0 | return NULL; |
322 | 0 | } |
323 | 0 | size_t clen = strlen(chunk); |
324 | 0 | if (clen == 0) { free(chunk); break; } /* EOF */ |
325 | | |
326 | 0 | while (resp_len + clen + 1 > resp_cap) { resp_cap *= 2; resp_buf = realloc(resp_buf, resp_cap); } |
327 | 0 | memcpy(resp_buf + resp_len, chunk, clen); |
328 | 0 | resp_len += clen; |
329 | 0 | resp_buf[resp_len] = '\0'; |
330 | 0 | free(chunk); |
331 | 0 | } |
332 | | |
333 | | /* Close connection */ |
334 | 0 | if (use_tls) net_tls_close(fd); else net_tcp_close(fd); |
335 | 0 | http_url_free(&url); |
336 | | |
337 | | /* Parse response */ |
338 | 0 | HttpResponse *resp = parse_response(resp_buf, resp_len, err); |
339 | 0 | free(resp_buf); |
340 | 0 | return resp; |
341 | 0 | } |
342 | | |
343 | | #endif /* !__EMSCRIPTEN__ */ |