Coverage Report

Created: 2026-02-23 20:32

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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__ */