diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index b83efa0..6eb2d71 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -434,7 +434,9 @@ The contract: - **`read` / `write` return a positive byte count**, or a negative `WOLFCERT_ERR_*`. Never `0`: an orderly peer close is `WOLFCERT_ERR_CONN_CLOSED`, which is what terminates a response body that - has neither `Content-Length` nor chunking. + has neither `Content-Length` nor chunking. A response to `HEAD`, and a + `204` or `304`, has no body at all (RFC 9112 section 6.3) and ends at the + header block, so no read follows it. - **`connect`'s `timeout_ms` is the caller's**, passed through from `WolfCertServerCfg.timeout_ms` / `WolfCertHttpRequest.timeout_ms`. A value above zero bounds the whole connect attempt. Zero or less imposes no limit diff --git a/src/http.c b/src/http.c index 20b9c41..74afd0d 100644 --- a/src/http.c +++ b/src/http.c @@ -50,6 +50,7 @@ #endif #define WOLFCERT_HTTP_DEFAULT_MAX_BODY (64 * 1024) #define WOLFCERT_HTTP_READ_CHUNK 2048 +#define WOLFCERT_HTTP_MAX_INTERIM 8 /* ASCII-only case folding. Every token compared here (scheme, host, header * name, transfer coding) is ASCII by definition, and unlike strcasecmp this @@ -446,6 +447,8 @@ struct WolfCertHttpSession { char* sm_content_type; /* taken from headers */ int sm_status; int sm_retry_after_sec; /* delta-seconds; 0 if absent */ + int sm_head_request; + int sm_interim; /* interim 1xx blocks dropped so far */ WolfCertHttpResponse* sm_resp; /* caller's resp; written to on DONE */ }; @@ -657,6 +660,20 @@ static int parse_status_line(const char* line, int* out_status) return WOLFCERT_OK; } +/* RFC 9110 section 15.2: a final response follows an interim 1xx on the same + * connection. 101 is itself the final response, so it is not interim. */ +static int status_is_interim(int status) +{ + return status >= 100 && status < 200 && status != 101; +} + +/* RFC 9112 section 6.3: a response to HEAD, and a 204 or 304, has no body + * whatever Content-Length or Transfer-Encoding say. */ +static int response_has_no_body(int status, int head_request) +{ + return head_request || status == 204 || status == 304; +} + static int read_headers(WolfCertConn* c, DynBuf* rx) { uint8_t tmp[WOLFCERT_HTTP_READ_CHUNK]; @@ -940,11 +957,16 @@ static int read_body(WolfCertConn* c, DynBuf* rx, size_t body_start, } size_t n = (size_t)length; - uint8_t* b = (uint8_t*)WOLFCERT_XMALLOC(n, heap); - if (b == NULL) - return WOLFCERT_ERR_MEMORY; + uint8_t* b = NULL; + + if (n > 0) { + b = (uint8_t*)WOLFCERT_XMALLOC(n, heap); + if (b == NULL) + return WOLFCERT_ERR_MEMORY; + + memcpy(b, rx->buf + body_start, n); + } - memcpy(b, rx->buf + body_start, n); *out = b; *out_len = n; @@ -967,12 +989,15 @@ static int read_body(WolfCertConn* c, DynBuf* rx, size_t body_start, } size_t n = rx->len - body_start; - uint8_t* b = (uint8_t*)WOLFCERT_XMALLOC(n ? n : 1, heap); - if (b == NULL) - return WOLFCERT_ERR_MEMORY; + uint8_t* b = NULL; + + if (n > 0) { + b = (uint8_t*)WOLFCERT_XMALLOC(n, heap); + if (b == NULL) + return WOLFCERT_ERR_MEMORY; - if (n > 0) memcpy(b, rx->buf + body_start, n); + } *out = b; *out_len = n; @@ -1261,68 +1286,101 @@ static size_t rx_max(size_t max_body) } static int http_read_response(WolfCertConn* c, + const char* method, size_t max_body, WolfCertHttpResponse* resp, void* heap) { DynBuf rx = { .heap = heap, .max = rx_max(max_body) }; - int hdr_end = read_headers(c, &rx); - if (hdr_end < 0) { - WOLFCERT_XFREE(rx.buf, heap); - return hdr_end; - } + char* headers_nt = NULL; + char* ct = NULL; + char* ra = NULL; + int head_request = (strcmp(method, "HEAD") == 0); + int retry_after = 0; + int interim = 0; + int hdr_end = 0; + int status = 0; + int rc = WOLFCERT_OK; - char* headers_nt = (char*)WOLFCERT_XMALLOC((size_t)hdr_end + 1, heap); - if (headers_nt == NULL) { - WOLFCERT_XFREE(rx.buf, heap); - return WOLFCERT_ERR_MEMORY; - } - - memcpy(headers_nt, rx.buf, (size_t)hdr_end); - headers_nt[hdr_end] = '\0'; + for (;;) { + hdr_end = read_headers(c, &rx); + if (hdr_end < 0) { + rc = hdr_end; + break; + } - int status = 0; - int rc = parse_status_line(headers_nt, &status); - if (rc != WOLFCERT_OK) { WOLFCERT_XFREE(headers_nt, heap); - WOLFCERT_XFREE(rx.buf, heap); - return rc; - } + headers_nt = (char*)WOLFCERT_XMALLOC((size_t)hdr_end + 1, heap); + if (headers_nt == NULL) { + rc = WOLFCERT_ERR_MEMORY; + break; + } - char* ct = find_header(headers_nt, "Content-Type", heap); - /* RFC 7231 section 7.1.3: `Retry-After` carries either delta-seconds or an - * HTTP-date. wolfCert parses delta-seconds only; a non-digit first - * character (i.e. the HTTP-date form) leaves retry_after_sec at 0. */ - char* ra = find_header(headers_nt, "Retry-After", heap); - int retry_after = 0; + memcpy(headers_nt, rx.buf, (size_t)hdr_end); + headers_nt[hdr_end] = '\0'; - if (ra != NULL) { - const char* p = ra; - while (*p == ' ' || *p == '\t') - ++p; + rc = parse_status_line(headers_nt, &status); + if (rc != WOLFCERT_OK) + break; - if (*p >= '0' && *p <= '9') { - long v = strtol(p, NULL, 10); - if (v > 0 && v <= 86400) - retry_after = (int)v; + if (status == 101) { + rc = WOLFCERT_ERR(WOLFCERT_ERR_PROTOCOL, "http", + "http: server sent 101 Switching Protocols, which wolfCert " + "never asks for with an Upgrade header"); + break; + } + + if (!status_is_interim(status)) + break; + + if (++interim > WOLFCERT_HTTP_MAX_INTERIM) { + rc = WOLFCERT_ERR(WOLFCERT_ERR_PROTOCOL, "http", + "http: more than %d interim 1xx responses arrived before a " + "final one", WOLFCERT_HTTP_MAX_INTERIM); + break; + } + + /* Drop the interim block. */ + memmove(rx.buf, rx.buf + hdr_end, rx.len - (size_t)hdr_end); + rx.len -= (size_t)hdr_end; + } + + if (rc == WOLFCERT_OK) { + ct = find_header(headers_nt, "Content-Type", heap); + /* RFC 7231 section 7.1.3: `Retry-After` carries either delta-seconds + * or an HTTP-date. wolfCert parses delta-seconds only. */ + ra = find_header(headers_nt, "Retry-After", heap); + if (ra != NULL) { + const char* p = ra; + while (*p == ' ' || *p == '\t') + ++p; + + if (*p >= '0' && *p <= '9') { + long v = strtol(p, NULL, 10); + if (v > 0 && v <= 86400) + retry_after = (int)v; + } + WOLFCERT_XFREE(ra, heap); + } + + if (!response_has_no_body(status, head_request)) { + rc = read_body(c, &rx, (size_t)hdr_end, headers_nt, + &resp->body, &resp->body_len, max_body, heap); } - WOLFCERT_XFREE(ra, heap); } - rc = read_body(c, &rx, (size_t)hdr_end, headers_nt, - &resp->body, &resp->body_len, max_body, heap); - WOLFCERT_XFREE(headers_nt, heap); - WOLFCERT_XFREE(rx.buf, heap); - if (rc != WOLFCERT_OK) { - WOLFCERT_XFREE(ct, heap); - return rc; + if (rc == WOLFCERT_OK) { + resp->status_code = status; + resp->content_type = ct; + resp->retry_after_sec = retry_after; + ct = NULL; /* ownership moved */ } - resp->status_code = status; - resp->content_type = ct; - resp->retry_after_sec = retry_after; + WOLFCERT_XFREE(ct, heap); + WOLFCERT_XFREE(headers_nt, heap); + WOLFCERT_XFREE(rx.buf, heap); - return WOLFCERT_OK; + return rc; } /* ---- main --------------------------------------------------------------- */ @@ -1381,7 +1439,7 @@ int wolfcert_http_request(const WolfCertHttpRequest* req, WolfCertHttpResponse* if (rc != WOLFCERT_OK) goto out; - rc = http_read_response(&c, max_body, resp, heap); + rc = http_read_response(&c, req->method, max_body, resp, heap); out: if (c.ssl) { @@ -1517,7 +1575,8 @@ int wolfcert_http_session_request(WolfCertHttpSession* s, return rc; } - rc = http_read_response(&s->conn, s->max_body, resp, s->heap); + rc = http_read_response(&s->conn, req->method, s->max_body, resp, + s->heap); wolfcert_http_url_free(&u); if (rc != WOLFCERT_OK) s->closed = 1; @@ -1555,6 +1614,8 @@ static void sm_reset(WolfCertHttpSession* s) s->sm_status = 0; s->sm_resp = NULL; s->sm_retry_after_sec = 0; + s->sm_head_request = 0; + s->sm_interim = 0; } /* Tear-down shortcut for state-machine error returns: drop per-request @@ -1836,23 +1897,40 @@ static int inspect_headers(WolfCertHttpSession* s) } s->sm_status = status; - char* te = find_header(hdrs, "Transfer-Encoding", s->heap); - if (te != NULL && ci_cmp(te, "chunked") == 0) { - WOLFCERT_XFREE(te, s->heap); + if (status == 101) { WOLFCERT_XFREE(hdrs, s->heap); - return WOLFCERT_ERR(WOLFCERT_ERR_UNSUPPORTED, "http", - "async session: Transfer-Encoding: chunked not supported " - "(use the blocking wolfcert_http_session_request instead)"); + return WOLFCERT_ERR(WOLFCERT_ERR_PROTOCOL, "http", + "http: server sent 101 Switching Protocols, which wolfCert " + "never asks for with an Upgrade header"); } - WOLFCERT_XFREE(te, s->heap); - char* cl = find_header(hdrs, "Content-Length", s->heap); - s->sm_content_length = (cl != NULL) ? strtol(cl, NULL, 10) : -1; - WOLFCERT_XFREE(cl, s->heap); - if (s->sm_content_length >= 0 && - (size_t)s->sm_content_length > s->max_body) { + if (status_is_interim(status)) { WOLFCERT_XFREE(hdrs, s->heap); - return WOLFCERT_ERR_PROTOCOL; + return WOLFCERT_OK; + } + + if (response_has_no_body(status, s->sm_head_request)) { + s->sm_content_length = 0; + } + else { + char* te = find_header(hdrs, "Transfer-Encoding", s->heap); + if (te != NULL && ci_cmp(te, "chunked") == 0) { + WOLFCERT_XFREE(te, s->heap); + WOLFCERT_XFREE(hdrs, s->heap); + return WOLFCERT_ERR(WOLFCERT_ERR_UNSUPPORTED, "http", + "async session: Transfer-Encoding: chunked not supported " + "(use the blocking wolfcert_http_session_request instead)"); + } + WOLFCERT_XFREE(te, s->heap); + + char* cl = find_header(hdrs, "Content-Length", s->heap); + s->sm_content_length = (cl != NULL) ? strtol(cl, NULL, 10) : -1; + WOLFCERT_XFREE(cl, s->heap); + if (s->sm_content_length >= 0 && + (size_t)s->sm_content_length > s->max_body) { + WOLFCERT_XFREE(hdrs, s->heap); + return WOLFCERT_ERR_PROTOCOL; + } } s->sm_content_type = find_header(hdrs, "Content-Type", s->heap); @@ -1949,6 +2027,9 @@ int wolfcert_http_session_request_nb(WolfCertHttpSession* s, s->sm_body_len = req->body_len; s->sm_body_off = 0; + s->sm_head_request = (strcmp(req->method, "HEAD") == 0); + s->sm_interim = 0; + /* Seed rx with any residual bytes from the previous response. */ if (s->residual_len > 0) { int rr = nb_rx_reserve(s, s->residual_len); @@ -2026,6 +2107,23 @@ int wolfcert_http_session_request_nb(WolfCertHttpSession* s, if (rc != WOLFCERT_OK) return sm_fail(s, rc); + if (status_is_interim(s->sm_status)) { + if (++s->sm_interim > WOLFCERT_HTTP_MAX_INTERIM) { + return sm_fail(s, WOLFCERT_ERR( + WOLFCERT_ERR_PROTOCOL, "http", + "http: more than %d interim 1xx responses " + "arrived before a final one", + WOLFCERT_HTTP_MAX_INTERIM)); + } + + /* Drop the interim block. */ + memmove(s->sm_rx, s->sm_rx + s->sm_hdr_end, + s->sm_rx_len - s->sm_hdr_end); + s->sm_rx_len -= s->sm_hdr_end; + s->sm_hdr_end = 0; + goto state_loop_continue; + } + s->sm_state = (s->sm_content_length >= 0) ? SM_READ_BODY_CL : SM_READ_BODY_EOF; goto state_loop_continue; diff --git a/tests/unit/test_transport.c b/tests/unit/test_transport.c index 573f1e1..098fe7d 100644 --- a/tests/unit/test_transport.c +++ b/tests/unit/test_transport.c @@ -23,8 +23,9 @@ * A caller-supplied WolfCertTransport drives the whole HTTP path with no * socket, so these run on every target. Coverage: handle 0 is valid, * disconnect runs exactly once, an incomplete vtable is rejected, the parser - * survives a byte-at-a-time feed, a body may end at CONN_CLOSED, and a build - * with no built-in transport refuses a config that supplies none. + * survives a byte-at-a-time feed, a body may end at CONN_CLOSED, a bodyless + * status and a HEAD reply end at the header block, an interim 1xx is dropped, + * and a build with no built-in transport refuses a config that supplies none. */ #include @@ -51,6 +52,9 @@ typedef struct { int connects; int disconnects; int zero_eof; /* report EOF as 0, breaking the contract */ + int open_rc; /* peer stays connected, instead of closing */ + int stall; /* alternate every read with a WANT_READ */ + int stall_now; int fail_connect; int fail_write; int bogus_connect_rc; /* positive return, breaking the contract */ @@ -79,8 +83,18 @@ static int p_read(void* ctx, void* conn, uint8_t* buf, size_t len, size_t n = strlen(p->resp) - p->off; (void)conn; (void)timeout_ms; - if (n == 0) + if (p->stall) { + p->stall_now = !p->stall_now; + if (p->stall_now) + return WOLFCERT_ERR_WANT_READ; + } + + if (n == 0) { + if (p->open_rc != 0) + return p->open_rc; + return p->zero_eof ? 0 : WOLFCERT_ERR_CONN_CLOSED; + } if (n > len) n = len; if (n > p->chunk) @@ -114,11 +128,11 @@ static int p_disconnect(void* ctx, void* conn) return WOLFCERT_OK; } -static int fetch(Peer* p, const char* resp, size_t chunk, - WolfCertHttpResponse* out) +static int fetch_m(Peer* p, const char* method, const char* resp, size_t chunk, + WolfCertHttpResponse* out) { WolfCertTransport t = { p_connect, p_read, p_write, p_disconnect, p }; - WolfCertHttpRequest req = { .method = "GET", .url = "http://peer.test/" }; + WolfCertHttpRequest req = { .method = method, .url = "http://peer.test/" }; p->resp = resp; p->off = 0; @@ -128,12 +142,67 @@ static int fetch(Peer* p, const char* resp, size_t chunk, return wolfcert_http_request(&req, out); } +static int fetch(Peer* p, const char* resp, size_t chunk, + WolfCertHttpResponse* out) +{ + return fetch_m(p, "GET", resp, chunk, out); +} + static const char RESP_CL[] = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n" "Content-Length: 5\r\n\r\nhello"; static const char RESP_EOF[] = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nbye"; +static const char RESP_EOF0[] = + "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\n"; +static const char RESP_CL0[] = + "HTTP/1.1 200 OK\r\nContent-Length: 0\r\nConnection: keep-alive\r\n\r\n"; + +/* None of these is terminated by a close. */ +static const char RESP_204[] = + "HTTP/1.1 204 No Content\r\nConnection: keep-alive\r\n\r\n"; +static const char RESP_304[] = + "HTTP/1.1 304 Not Modified\r\nETag: \"v1\"\r\n" + "Connection: keep-alive\r\n\r\n"; +static const char RESP_HEAD[] = + "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n" + "Content-Length: 5\r\nConnection: keep-alive\r\n\r\n"; + +#define INTERIM_BLOCK "HTTP/1.1 100 Continue\r\n\r\n" +static const char RESP_INTERIM[] = + INTERIM_BLOCK + "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n" + "Content-Length: 5\r\nConnection: keep-alive\r\n\r\nhello"; +/* Eight blocks, exactly WOLFCERT_HTTP_MAX_INTERIM. */ +static const char RESP_INTERIM_MAX[] = + INTERIM_BLOCK INTERIM_BLOCK INTERIM_BLOCK INTERIM_BLOCK + INTERIM_BLOCK INTERIM_BLOCK INTERIM_BLOCK INTERIM_BLOCK + "HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello"; +/* Nine blocks, one past WOLFCERT_HTTP_MAX_INTERIM. */ +static const char RESP_INTERIM_FLOOD[] = + INTERIM_BLOCK INTERIM_BLOCK INTERIM_BLOCK + INTERIM_BLOCK INTERIM_BLOCK INTERIM_BLOCK + INTERIM_BLOCK INTERIM_BLOCK INTERIM_BLOCK + "HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello"; +/* A HEAD reply names the length a GET would have returned. */ +static const char RESP_HEAD_BIG[] = + "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n" + "Content-Length: 1000000\r\nConnection: keep-alive\r\n\r\n"; +/* An interim block carrying header fields of its own, then the real ones. */ +static const char RESP_INTERIM_HDRS[] = + "HTTP/1.1 103 Early Hints\r\nLink: ; rel=preload\r\n" + "Content-Type: text/interim\r\n\r\n" + "HTTP/1.1 200 OK\r\nContent-Type: text/final\r\n" + "Content-Length: 5\r\nConnection: keep-alive\r\n\r\nhello"; +/* A 204 followed by the next response on the same connection. */ +static const char RESP_204_THEN_CL[] = + "HTTP/1.1 204 No Content\r\nConnection: keep-alive\r\n\r\n" + "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n" + "Content-Length: 5\r\nConnection: keep-alive\r\n\r\nhello"; +static const char RESP_101[] = + "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\n\r\n"; + static int test_roundtrip(void) { WolfCertHttpResponse resp = { 0 }; @@ -333,6 +402,404 @@ static int test_partial_vtable_rejected(void) return 0; } +static int test_empty_body_is_null(void) +{ + WolfCertHttpResponse resp = { 0 }; + Peer p = { 0 }; + + p.open_rc = WOLFCERT_ERR_IO; + REQUIRE(fetch(&p, RESP_CL0, sizeof(RESP_CL0), &resp) == WOLFCERT_OK); + REQUIRE(resp.status_code == 200); + REQUIRE(resp.body == NULL && resp.body_len == 0); + wolfcert_http_response_free(&resp); + return 0; +} + +static int test_204_no_body(void) +{ + WolfCertHttpResponse resp = { 0 }; + Peer p = { 0 }; + + p.open_rc = WOLFCERT_ERR_IO; + REQUIRE(fetch(&p, RESP_204, sizeof(RESP_204), &resp) == WOLFCERT_OK); + REQUIRE(resp.status_code == 204); + REQUIRE(resp.body == NULL && resp.body_len == 0); + wolfcert_http_response_free(&resp); + return 0; +} + +static int test_304_no_body(void) +{ + WolfCertHttpResponse resp = { 0 }; + Peer p = { 0 }; + + p.open_rc = WOLFCERT_ERR_IO; + REQUIRE(fetch(&p, RESP_304, sizeof(RESP_304), &resp) == WOLFCERT_OK); + REQUIRE(resp.status_code == 304); + REQUIRE(resp.body == NULL && resp.body_len == 0); + wolfcert_http_response_free(&resp); + return 0; +} + +/* A HEAD reply states the length it would have sent; those bytes never + * follow. */ +static int test_head_ignores_content_length(void) +{ + WolfCertHttpResponse resp = { 0 }; + Peer p = { 0 }; + + p.open_rc = WOLFCERT_ERR_IO; + REQUIRE(fetch_m(&p, "HEAD", RESP_HEAD, sizeof(RESP_HEAD), &resp) + == WOLFCERT_OK); + REQUIRE(resp.status_code == 200); + REQUIRE(resp.body == NULL && resp.body_len == 0); + wolfcert_http_response_free(&resp); + return 0; +} + +/* "head" is a different method from "HEAD", and its response carries a body + * like any other. */ +static int test_lowercase_head_is_not_head(void) +{ + WolfCertHttpResponse resp = { 0 }; + Peer p = { 0 }; + + REQUIRE(fetch_m(&p, "head", RESP_CL, sizeof(RESP_CL), &resp) + == WOLFCERT_OK); + REQUIRE(resp.status_code == 200); + REQUIRE(resp.body_len == 5 && memcmp(resp.body, "hello", 5) == 0); + wolfcert_http_response_free(&resp); + return 0; +} + +/* The advertised length is what a GET would have returned, so it must not be + * applied as a body-size bound. */ +static int test_head_oversized_length_ok(void) +{ + WolfCertTransport t = { p_connect, p_read, p_write, p_disconnect, NULL }; + WolfCertHttpRequest req = { .method = "HEAD", .url = "http://peer.test/" }; + WolfCertHttpResponse resp = { 0 }; + Peer p = { 0 }; + + t.ctx = &p; + p.resp = RESP_HEAD_BIG; + p.chunk = sizeof(RESP_HEAD_BIG); + p.open_rc = WOLFCERT_ERR_IO; + req.transport = t; + req.max_response_bytes = 16; + + REQUIRE(wolfcert_http_request(&req, &resp) == WOLFCERT_OK); + REQUIRE(resp.status_code == 200); + REQUIRE(resp.body == NULL && resp.body_len == 0); + wolfcert_http_response_free(&resp); + return 0; +} + +/* Fed both ways: the two responses together, then split byte by byte. */ +static int test_interim_then_final(void) +{ + WolfCertHttpResponse resp = { 0 }; + Peer p = { 0 }; + + p.open_rc = WOLFCERT_ERR_IO; + REQUIRE(fetch(&p, RESP_INTERIM, sizeof(RESP_INTERIM), &resp) + == WOLFCERT_OK); + REQUIRE(resp.status_code == 200); + REQUIRE(resp.body_len == 5 && memcmp(resp.body, "hello", 5) == 0); + wolfcert_http_response_free(&resp); + + memset(&p, 0, sizeof(p)); + p.open_rc = WOLFCERT_ERR_IO; + REQUIRE(fetch(&p, RESP_INTERIM, 1, &resp) == WOLFCERT_OK); + REQUIRE(resp.status_code == 200); + REQUIRE(resp.body_len == 5 && memcmp(resp.body, "hello", 5) == 0); + wolfcert_http_response_free(&resp); + return 0; +} + +/* The cap is a ceiling, not a limit one below it. */ +static int test_interim_at_cap_accepted(void) +{ + WolfCertHttpResponse resp = { 0 }; + Peer p = { 0 }; + + p.open_rc = WOLFCERT_ERR_IO; + REQUIRE(fetch(&p, RESP_INTERIM_MAX, sizeof(RESP_INTERIM_MAX), &resp) + == WOLFCERT_OK); + REQUIRE(resp.status_code == 200); + REQUIRE(resp.body_len == 5 && memcmp(resp.body, "hello", 5) == 0); + wolfcert_http_response_free(&resp); + return 0; +} + +static int test_interim_flood_rejected(void) +{ + WolfCertHttpResponse resp = { 0 }; + Peer p = { 0 }; + + p.open_rc = WOLFCERT_ERR_IO; + REQUIRE(fetch(&p, RESP_INTERIM_FLOOD, sizeof(RESP_INTERIM_FLOOD), &resp) + == WOLFCERT_ERR_PROTOCOL); + return 0; +} + +static int test_switching_protocols_rejected(void) +{ + WolfCertHttpResponse resp = { 0 }; + Peer p = { 0 }; + + p.open_rc = WOLFCERT_ERR_IO; + REQUIRE(fetch(&p, RESP_101, sizeof(RESP_101), &resp) + == WOLFCERT_ERR_PROTOCOL); + return 0; +} + +/* Bounded drive of one request: a response that never completes ends the loop + * rather than spinning in it. */ +static int drive_nb(WolfCertHttpSession* s, const WolfCertHttpRequest* req, + WolfCertHttpResponse* out) +{ + int rc = WOLFCERT_ERR_WANT_READ; + int i; + + for (i = 0; i < 1024; i++) { + rc = wolfcert_http_session_request_nb(s, req, out); + if (rc != WOLFCERT_ERR_WANT_READ && rc != WOLFCERT_ERR_WANT_WRITE) + break; + } + + return rc; +} + +static int nb_fetch_m(Peer* p, const char* method, const char* bytes, + size_t chunk, WolfCertHttpResponse* out) +{ + WolfCertHttpSessionCfg cfg = { .base_url = "http://peer.test/" }; + WolfCertTransport t = { p_connect, p_read, p_write, p_disconnect, p }; + WolfCertHttpRequest req = { .method = method, .url = "http://peer.test/" }; + WolfCertHttpSession* s = NULL; + int rc; + + p->resp = bytes; + p->off = 0; + p->chunk = chunk; + p->open_rc = WOLFCERT_ERR_WANT_READ; + cfg.transport = t; + cfg.nonblocking = 1; + + rc = wolfcert_http_session_open(&cfg, &s); + if (rc != WOLFCERT_OK) + return rc; + + rc = drive_nb(s, &req, out); + wolfcert_http_session_close(s); + + return rc; +} + +static int nb_fetch(Peer* p, const char* bytes, WolfCertHttpResponse* out) +{ + return nb_fetch_m(p, "GET", bytes, strlen(bytes), out); +} + +static int test_nb_204_completes(void) +{ + WolfCertHttpResponse resp = { 0 }; + Peer p = { 0 }; + + REQUIRE(nb_fetch(&p, RESP_204, &resp) == WOLFCERT_OK); + REQUIRE(resp.status_code == 204); + REQUIRE(resp.body == NULL && resp.body_len == 0); + wolfcert_http_response_free(&resp); + return 0; +} + +static int test_nb_interim_then_final(void) +{ + WolfCertHttpResponse resp = { 0 }; + Peer p = { 0 }; + + REQUIRE(nb_fetch(&p, RESP_INTERIM, &resp) == WOLFCERT_OK); + REQUIRE(resp.status_code == 200); + REQUIRE(resp.body_len == 5 && memcmp(resp.body, "hello", 5) == 0); + wolfcert_http_response_free(&resp); + return 0; +} + +/* The peer closes with no body at all. */ +static int test_eof_empty_body_is_null(void) +{ + WolfCertHttpResponse resp = { 0 }; + Peer p = { 0 }; + + REQUIRE(fetch(&p, RESP_EOF0, sizeof(RESP_EOF0), &resp) == WOLFCERT_OK); + REQUIRE(resp.status_code == 200); + REQUIRE(resp.body == NULL && resp.body_len == 0); + wolfcert_http_response_free(&resp); + return 0; +} + +/* The async path runs its own copies of these checks. */ +static int test_nb_head_ignores_content_length(void) +{ + WolfCertHttpResponse resp = { 0 }; + Peer p = { 0 }; + + REQUIRE(nb_fetch_m(&p, "HEAD", RESP_HEAD, strlen(RESP_HEAD), &resp) + == WOLFCERT_OK); + REQUIRE(resp.status_code == 200); + REQUIRE(resp.body == NULL && resp.body_len == 0); + wolfcert_http_response_free(&resp); + return 0; +} + +static int test_nb_lowercase_head_is_not_head(void) +{ + WolfCertHttpResponse resp = { 0 }; + Peer p = { 0 }; + + REQUIRE(nb_fetch_m(&p, "head", RESP_CL, strlen(RESP_CL), &resp) + == WOLFCERT_OK); + REQUIRE(resp.status_code == 200); + REQUIRE(resp.body_len == 5 && memcmp(resp.body, "hello", 5) == 0); + wolfcert_http_response_free(&resp); + return 0; +} + +static int test_nb_interim_at_cap_accepted(void) +{ + WolfCertHttpResponse resp = { 0 }; + Peer p = { 0 }; + + REQUIRE(nb_fetch(&p, RESP_INTERIM_MAX, &resp) == WOLFCERT_OK); + REQUIRE(resp.status_code == 200); + REQUIRE(resp.body_len == 5 && memcmp(resp.body, "hello", 5) == 0); + wolfcert_http_response_free(&resp); + return 0; +} + +static int test_nb_interim_flood_rejected(void) +{ + WolfCertHttpResponse resp = { 0 }; + Peer p = { 0 }; + + REQUIRE(nb_fetch(&p, RESP_INTERIM_FLOOD, &resp) == WOLFCERT_ERR_PROTOCOL); + return 0; +} + +static int test_nb_switching_protocols_rejected(void) +{ + WolfCertHttpResponse resp = { 0 }; + Peer p = { 0 }; + + REQUIRE(nb_fetch(&p, RESP_101, &resp) == WOLFCERT_ERR_PROTOCOL); + return 0; +} + +/* One byte per read with a WANT_READ between each, so the state machine + * suspends inside the interim block and inside the final header block and has + * to resume at the right offset. */ +static int test_nb_interim_fragmented(void) +{ + WolfCertHttpResponse resp = { 0 }; + Peer p = { 0 }; + + p.stall = 1; + REQUIRE(nb_fetch_m(&p, "GET", RESP_INTERIM, 1, &resp) == WOLFCERT_OK); + REQUIRE(resp.status_code == 200); + REQUIRE(resp.body_len == 5 && memcmp(resp.body, "hello", 5) == 0); + wolfcert_http_response_free(&resp); + return 0; +} + +static int test_nb_204_fragmented(void) +{ + WolfCertHttpResponse resp = { 0 }; + Peer p = { 0 }; + + p.stall = 1; + REQUIRE(nb_fetch_m(&p, "GET", RESP_204, 1, &resp) == WOLFCERT_OK); + REQUIRE(resp.status_code == 204); + REQUIRE(resp.body == NULL && resp.body_len == 0); + wolfcert_http_response_free(&resp); + return 0; +} + +static int test_nb_head_oversized_length_ok(void) +{ + WolfCertHttpSessionCfg cfg = { .base_url = "http://peer.test/" }; + WolfCertTransport t = { p_connect, p_read, p_write, p_disconnect, NULL }; + WolfCertHttpRequest req = { .method = "HEAD", .url = "http://peer.test/" }; + WolfCertHttpResponse resp = { 0 }; + WolfCertHttpSession* s = NULL; + Peer p = { 0 }; + + t.ctx = &p; + p.resp = RESP_HEAD_BIG; + p.chunk = sizeof(RESP_HEAD_BIG); + p.open_rc = WOLFCERT_ERR_WANT_READ; + cfg.transport = t; + cfg.nonblocking = 1; + cfg.max_response_bytes = 16; + + REQUIRE(wolfcert_http_session_open(&cfg, &s) == WOLFCERT_OK); + REQUIRE(drive_nb(s, &req, &resp) == WOLFCERT_OK); + REQUIRE(resp.status_code == 200); + REQUIRE(resp.body == NULL && resp.body_len == 0); + wolfcert_http_response_free(&resp); + wolfcert_http_session_close(s); + return 0; +} + +/* The header fields of an interim block must not reach the caller. */ +static int test_nb_interim_headers_discarded(void) +{ + WolfCertHttpResponse resp = { 0 }; + Peer p = { 0 }; + + REQUIRE(nb_fetch(&p, RESP_INTERIM_HDRS, &resp) == WOLFCERT_OK); + REQUIRE(resp.status_code == 200); + REQUIRE(resp.content_type != NULL); + REQUIRE(strcmp(resp.content_type, "text/final") == 0); + REQUIRE(resp.body_len == 5 && memcmp(resp.body, "hello", 5) == 0); + wolfcert_http_response_free(&resp); + return 0; +} + +/* The bytes past a bodyless response belong to the next one on the session. */ +static int test_nb_session_reuse_after_204(void) +{ + WolfCertHttpSessionCfg cfg = { .base_url = "http://peer.test/" }; + WolfCertTransport t = { p_connect, p_read, p_write, p_disconnect, NULL }; + WolfCertHttpRequest req = { .method = "GET", .url = "http://peer.test/" }; + WolfCertHttpResponse first = { 0 }; + WolfCertHttpResponse second = { 0 }; + WolfCertHttpSession* s = NULL; + Peer p = { 0 }; + + t.ctx = &p; + p.resp = RESP_204_THEN_CL; + p.chunk = strlen(RESP_204_THEN_CL); + p.open_rc = WOLFCERT_ERR_WANT_READ; + cfg.transport = t; + cfg.nonblocking = 1; + + REQUIRE(wolfcert_http_session_open(&cfg, &s) == WOLFCERT_OK); + + REQUIRE(drive_nb(s, &req, &first) == WOLFCERT_OK); + REQUIRE(first.status_code == 204); + REQUIRE(first.body == NULL && first.body_len == 0); + wolfcert_http_response_free(&first); + + REQUIRE(drive_nb(s, &req, &second) == WOLFCERT_OK); + REQUIRE(second.status_code == 200); + REQUIRE(second.body_len == 5 && memcmp(second.body, "hello", 5) == 0); + wolfcert_http_response_free(&second); + + wolfcert_http_session_close(s); + return 0; +} + #ifdef WOLFCERT_HAVE_BUILTIN_TRANSPORT /* A zeroed transport reaches the built-in one, so the failure must come from * the connect attempt rather than from validation. */ @@ -422,6 +889,52 @@ int main(void) return 1; if (test_partial_vtable_rejected()) return 1; + if (test_empty_body_is_null()) + return 1; + if (test_204_no_body()) + return 1; + if (test_304_no_body()) + return 1; + if (test_head_ignores_content_length()) + return 1; + if (test_lowercase_head_is_not_head()) + return 1; + if (test_nb_lowercase_head_is_not_head()) + return 1; + if (test_interim_then_final()) + return 1; + if (test_interim_at_cap_accepted()) + return 1; + if (test_interim_flood_rejected()) + return 1; + if (test_switching_protocols_rejected()) + return 1; + if (test_nb_204_completes()) + return 1; + if (test_nb_interim_then_final()) + return 1; + if (test_eof_empty_body_is_null()) + return 1; + if (test_nb_head_ignores_content_length()) + return 1; + if (test_nb_interim_at_cap_accepted()) + return 1; + if (test_nb_interim_flood_rejected()) + return 1; + if (test_nb_switching_protocols_rejected()) + return 1; + if (test_nb_interim_fragmented()) + return 1; + if (test_nb_204_fragmented()) + return 1; + if (test_head_oversized_length_ok()) + return 1; + if (test_nb_head_oversized_length_ok()) + return 1; + if (test_nb_interim_headers_discarded()) + return 1; + if (test_nb_session_reuse_after_204()) + return 1; #ifdef WOLFCERT_HAVE_BUILTIN_TRANSPORT if (test_zero_transport_takes_builtin()) return 1; diff --git a/wolfcert/http.h b/wolfcert/http.h index 394548a..b926551 100644 --- a/wolfcert/http.h +++ b/wolfcert/http.h @@ -39,7 +39,7 @@ WOLFCERT_API int wolfcert_posix_connect(const char* host, int port, #endif typedef struct { - const char* method; /* "GET" or "POST" */ + const char* method; /* "GET", "HEAD" or "POST" */ const char* url; /* full URL; scheme http or https */ const char* content_type; const char* content_transfer_encoding; @@ -73,6 +73,7 @@ typedef struct { typedef struct { int status_code; char* content_type; + /* May be NULL when body_len is 0; check body_len, not body. */ uint8_t* body; size_t body_len; /* `Retry-After` header parsed as delta-seconds (RFC 7231 section 7.1.3).