diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7736985..4640c67 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -103,7 +103,7 @@ Email **support@wolfssl.com** instead, and please keep the issue private until a - **Heap hints on every allocation.** Allocate through `WOLFCERT_XMALLOC`, `WOLFCERT_XREALLOC` and `WOLFCERT_XFREE` from `wolfcert/memory.h` and pass the hint on to wolfSSL. A public API takes an optional `heap`, directly or in its config struct, and falls back to `wolfcert_default_heap()` when it is NULL. - **No CryptoCb registration.** wolfCert never calls `wc_CryptoCb_RegisterDevice`; the application registers its backend and passes the `dev_id` in `WolfCertKeyCfg`. -- **A closed set of error codes.** Report every error with `WOLFCERT_ERR` or `WOLFCERT_ERR_WC` from `src/internal.h`, and keep `wolfcert_strerror` covering every code in `wolfcert/errors.h`. +- **A closed set of error codes.** Report every error with `WOLFCERT_ERR`, `WOLFCERT_ERR_WC` or `WOLFCERT_ERR_RC_WC` from `src/internal.h`, and keep `wolfcert_strerror` covering every code in `wolfcert/errors.h`. - **Visibility.** Public and internal functions both use the `wolfcert_` prefix. The CMake build hides every symbol by default, so public prototypes in `wolfcert/*.h` carry `WOLFCERT_API`. Mark an internal function that the in-tree tests call `WOLFCERT_TEST_VIS`, which exports it only in a `WOLFCERT_BUILD_TESTING` build. - **Append-only public structs.** The v0.1.0 ABI is released, so a new field in a public struct goes at its end. - **License header.** Every `.c` and `.h` file opens with the full GPL copyright block, copied from an existing source file; the Lint workflow checks it. Build files (`CMakeLists.txt`, `configure.ac`, `Makefile.am`) use the `SPDX-License-Identifier: GPL-3.0-or-later` line instead. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 6bef5f6..c2a8609 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -489,6 +489,12 @@ The contract: implement the negative case with a call that lets it apply, rather than an unbounded wait around a non-blocking transfer: the second shape silently discards whatever the application configured. +- **The mode can change between requests on one connection.** wolfCert lets + an application make blocking and `_nb` requests on one HTTP or EST session, + one request at a time, so a transport can see `0` and a negative value on + the same connection. Take special care to honour each call's value if your + application needs this; otherwise keep a connection in one mode. Closing a + session always writes the TLS `close_notify` with `0`. - **Never write more than `len` bytes.** wolfCert rejects a count larger than it asked for, but that only keeps the overrun out of its own buffers - the write into yours has already happened, so honouring `len` is the transport's diff --git a/docs/EMBEDDED.md b/docs/EMBEDDED.md index 21e1b6c..24f2e88 100644 --- a/docs/EMBEDDED.md +++ b/docs/EMBEDDED.md @@ -104,7 +104,8 @@ miscompile. |----------|----------------|--------------|--------------| | wolfSSL `Cert` (CSR / cert build) | **heap** (`wc_CertNew`) | ~20+ KB | `altNames[16384]` | | wolfSSL `DecodedCert` (cert parse) | **stack** or **heap**, transient | several KB | parse scratch | -| HTTP request handling | **stack** (EST + client); **heap** (SCEP server) | 2-3 KB stack | request read buffer | +| HTTP request handling | **stack** (EST server); **heap** (SCEP server, client) | 2-3 KB stack | request read buffer | +| HTTP response (client) | **heap**, handed to the caller as `resp->body` | up to `max_response_bytes` + 8 KB | receive buffer; its allocation can exceed `body_len` | The good news: wolfCert never stack-allocates a `Cert`. Every CSR/cert build path (`src/csr.c`, `src/ca_issue.c`, `src/scep/scep_msg.c`) obtains @@ -188,15 +189,15 @@ the request completes) so it never counts against the stack budget. | `WOLFCERT_HTTP_QUERY_SZ` | `8192` | sized to hold a base64 GET `PKIOperation` message; on the SCEP server it extends the heap read buffer (`REQ_BUF_SZ + QUERY_SZ`) that `query` points into | | `WOLFCERT_HTTP_AUTH_BUF_SZ` | `512` | client Basic-auth header line (`http.c`) | | `WOLFCERT_HTTP_MAX_PATH_LEN` | `8192` | client-side ceiling on a request URL's path+query (`http.c`) | -| `WOLFCERT_HTTP_HEADER_BUDGET` | `8192` | client response allowance added to the caller's body cap, bounding the status line plus header block. Both the blocking and the non-blocking reader grow their accumulator to `max_response_bytes + this` (`http.c`) | +| `WOLFCERT_HTTP_HEADER_BUDGET` | `8192` | client limit on a response's status line plus header block; a longer block fails with `WOLFCERT_ERR_PROTOCOL`. The client's receive buffer holds at most `max_response_bytes + this` (`http.c`) | | `WOLFCERT_SCEP_MAX_GET_URL` | `8192` | client cap on a GET `PKIOperation` URL; a larger message is refused with `WOLFCERT_ERR_UNSUPPORTED` so the caller POSTs (`internal.h`) | Shrinking `WOLFCERT_HTTP_REQ_BUF_SZ` lowers the largest request header block the server accepts; `WOLFCERT_HTTP_PATH_SZ` / `WOLFCERT_HTTP_QUERY_SZ` lower the longest request path / query; `WOLFCERT_HTTP_AUTH_BUF_SZ` lowers the longest Basic-auth credential the client can send. `WOLFCERT_HTTP_HEADER_BUDGET` -trims the client's response accumulator, and with it the largest response -header block it will accept, so keep it above the headers your CA actually +sets the largest response header block the client accepts, and with it the +client's receive buffer, so keep it above the headers your CA actually sends. A POST-only SCEP deployment can trim `WOLFCERT_HTTP_QUERY_SZ` (and, on the client, `WOLFCERT_SCEP_MAX_GET_URL` and `WOLFCERT_HTTP_MAX_PATH_LEN`) back down. Example: @@ -266,6 +267,20 @@ keeping the large heap-allocated `Cert` off a general-purpose allocator. See [`ARCHITECTURE.md` section 4.1](ARCHITECTURE.md#41-heap-hints--static-memory-pools) for the three levels of hint granularity. +A pool realloc always copies, so a chunked or close-delimited HTTP response +first tries to take its whole receive buffer in one block of +`max_response_bytes + WOLFCERT_HTTP_HEADER_BUDGET`, 72 KB by default. If no +pool block is that large, the buffer doubles as bytes arrive, taking a new +block each time; with wolfSSL's default buckets it stops at 8192 bytes, +headers and read room included, and a response that needs more fails with +`WOLFCERT_ERR_MEMORY`. A `Content-Length` response takes one block of its +exact size. For larger responses, give the pool one block of that sum per +session receiving at the same time, or lower `max_response_bytes` (per +request, or per session at open) or `WOLFCERT_HTTP_HEADER_BUDGET` (at build +time) until their sum fits wolfSSL's largest bucket (`LARGEST_MEM_BUCKET`, +16128 bytes unless ML-DSA or `SESSION_CERTS` raises it): 7936 + 8192, for +example. + ## 5. Threading If your target is single-threaded, build wolfSSL with `SINGLE_THREADED`. diff --git a/src/est/est_client.c b/src/est/est_client.c index 29bb33f..d7926fa 100644 --- a/src/est/est_client.c +++ b/src/est/est_client.c @@ -372,7 +372,7 @@ struct WolfCertEstSession { enum { EST_OP_GET_CACERTS, EST_OP_SIMPLE_ENROLL } in_op; }; -static int est_session_open_common(const WolfCertServerCfg* srv, int nonblocking, +static int est_session_open_common(const WolfCertServerCfg* srv, WolfCertEstSession** out) { if (srv == NULL || srv->server_url == NULL || out == NULL) @@ -450,7 +450,6 @@ static int est_session_open_common(const WolfCertServerCfg* srv, int nonblocking .client_key = srv->client_key, .client_key_len = srv->client_key_len, .allow_post_handshake_auth = srv->proto_opts.est.allow_post_handshake_auth, - .nonblocking = nonblocking, .transport = srv->transport, .heap = heap, }; @@ -470,13 +469,13 @@ static int est_session_open_common(const WolfCertServerCfg* srv, int nonblocking int wolfcert_est_session_open(const WolfCertServerCfg* srv, WolfCertEstSession** out) { - return est_session_open_common(srv, 0, out); + return est_session_open_common(srv, out); } int wolfcert_est_session_open_async(const WolfCertServerCfg* srv, WolfCertEstSession** out) { - return est_session_open_common(srv, 1, out); + return est_session_open_common(srv, out); } int wolfcert_est_session_fd(const WolfCertEstSession* s) diff --git a/src/http.c b/src/http.c index b427340..2d8286c 100644 --- a/src/http.c +++ b/src/http.c @@ -401,7 +401,7 @@ static void conn_close(WolfCertConn* c) } } -/* Async request state; the session owns its buffers across WANT_* returns. */ +/* Request state; the session owns its buffers across WANT_* returns. */ typedef enum { SM_IDLE = 0, SM_HANDSHAKE = 1, /* TLS handshake not yet complete */ @@ -410,68 +410,41 @@ typedef enum { SM_READ_HEAD = 4, SM_READ_BODY_CL = 5, /* Content-Length known */ SM_READ_BODY_EOF= 6, /* read until close */ - SM_DONE = 7 + SM_READ_BODY_CHUNKED = 7, + SM_DONE = 8 } SmState; struct WolfCertHttpSession { WolfCertConn conn; WOLFSSL_CTX* ctx; WolfCertUrl base; - /* Bytes read past the previous response, fed to the next parse. */ - uint8_t* residual; - size_t residual_len; size_t max_body; void* heap; - int closed; + unsigned int closed : 1; + unsigned int keep_alive : 1; + unsigned int sm_head_request : 1; - int nonblocking; SmState sm_state; char* sm_head; size_t sm_head_len; - size_t sm_head_off; + size_t sm_tx_off; /* Borrowed from the caller; valid only until SM_DONE. */ const uint8_t* sm_body; size_t sm_body_len; - size_t sm_body_off; uint8_t* sm_rx; size_t sm_rx_len; size_t sm_rx_cap; size_t sm_hdr_end; + size_t sm_hdr_scan; + WolfCertChunkScan sm_scan; long sm_content_length; /* -1 if unknown */ char* sm_content_type; /* taken from headers */ int sm_status; int sm_retry_after_sec; /* 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 */ }; -static int conn_write(WolfCertConn* c, const void* buf, size_t len) -{ - const uint8_t* p = (const uint8_t*)buf; - size_t n = 0; - - while (n < len) { - int w; - if (c->ssl) { - w = wolfSSL_write(c->ssl, p + n, (int)(len - n)); - } - else { - w = c->t.write(c->t.ctx, c->handle, p + n, len - n, - c->io_timeout_ms); - if (w > 0 && (size_t)w > len - n) - return WOLFCERT_ERR_IO; - } - - if (w <= 0) - return WOLFCERT_ERR_IO; - - n += (size_t)w; - } - - return WOLFCERT_OK; -} - /* wolfSSL I/O callbacks over the transport; ctx is the WolfCertConn. */ static int wolfcert_cbio_recv(WOLFSSL* ssl, char* buf, int sz, void* ctx) { @@ -532,68 +505,6 @@ static int wolfcert_cbio_send(WOLFSSL* ssl, char* buf, int sz, void* ctx) } } -/* Returns the byte count (> 0) or a negative WOLFCERT_ERR_*; an orderly peer - * close is WOLFCERT_ERR_CONN_CLOSED, never 0. */ -static int conn_read(WolfCertConn* c, void* buf, size_t len) -{ - int r; - - if (c->ssl == NULL) { - r = c->t.read(c->t.ctx, c->handle, (uint8_t*)buf, len, - c->io_timeout_ms); - if (r == 0) - return WOLFCERT_ERR_CONN_CLOSED; - if (r > 0 && (size_t)r > len) - return WOLFCERT_ERR_IO; - return r; - } - - r = wolfSSL_read(c->ssl, buf, (int)len); - if (r > 0) - return r; - if (r == 0 || - wolfSSL_get_error(c->ssl, r) == WOLFSSL_ERROR_ZERO_RETURN) - return WOLFCERT_ERR_CONN_CLOSED; - - return WOLFCERT_ERR_IO; -} - -typedef struct { - uint8_t* buf; - size_t len; - size_t cap; - size_t max; - void* heap; -} DynBuf; - -static int dyn_append(DynBuf* d, const void* data, size_t len) -{ - if (len > d->max - d->len) - return WOLFCERT_ERR_PROTOCOL; - - if (d->len + len > d->cap) { - size_t nc = d->cap ? d->cap : 1024; - - while (nc < d->len + len) - nc *= 2; - - if (nc > d->max) - nc = d->max; - - uint8_t* nb = (uint8_t*)WOLFCERT_XREALLOC(d->buf, nc, d->heap); - if (nb == NULL) - return WOLFCERT_ERR_MEMORY; - - d->buf = nb; - d->cap = nc; - } - - memcpy(d->buf + d->len, data, len); - d->len += len; - - return WOLFCERT_OK; -} - static char* find_header(const char* headers, const char* name, void* heap) { const char* p = headers; @@ -835,30 +746,6 @@ 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]; - - while (1) { - const char* hay = (const char*)rx->buf; - size_t hay_len = rx->len; - for (size_t i = 0; i + 3 < hay_len; ++i) { - if (hay[i] == '\r' && hay[i+1] == '\n' && - hay[i+2] == '\r' && hay[i+3] == '\n') { - return (int)(i + 4); - } - } - - int r = conn_read(c, tmp, sizeof(tmp)); - if (r <= 0) - return WOLFCERT_ERR_IO; - - int rc = dyn_append(rx, tmp, (size_t)r); - if (rc != WOLFCERT_OK) - return rc; - } -} - /* Advance *at to the next CRLF; 1 if none yet, with *at where to resume. */ static int find_crlf(const uint8_t* raw, size_t raw_len, size_t* at) { @@ -940,8 +827,10 @@ WOLFCERT_TEST_VIS int wolfcert_http_chunked_complete(const uint8_t* raw, return 0; if (st->in_trailers) { - if (st->crlf == ri) + if (st->crlf == ri) { + st->pos = st->crlf + 2; return 1; /* blank line terminates the trailers */ + } } else if (parse_chunk_size(raw, ri, st->crlf, &st->chunk) != 0 || (st->max != 0 && st->chunk > st->max - st->total)) { @@ -1000,170 +889,65 @@ static int check_trailers(const uint8_t* raw, size_t raw_len) return WOLFCERT_ERR_PROTOCOL; } -int wolfcert_http_chunked_decode(const uint8_t* in, size_t in_len, - uint8_t** out, size_t* out_len, - size_t max_bytes, void* heap) +/* Decode a whole chunked body into dst, which may alias src at or below it. */ +static int chunked_decode_into(uint8_t* dst, const uint8_t* src, size_t len, + size_t* out_len) { - DynBuf body = { .heap = heap, .max = max_bytes }; size_t p = 0; - int rc; + size_t w = 0; - while (p < in_len) { + for (;;) { size_t clen = 0; size_t he = p; - if (find_crlf(in, in_len, &he) != 0 || - parse_chunk_size(in, p, he, &clen) != 0) { - WOLFCERT_XFREE(body.buf, heap); + if (find_crlf(src, len, &he) != 0 || + parse_chunk_size(src, p, he, &clen) != 0) return WOLFCERT_ERR_PROTOCOL; - } p = he + 2; if (clen == 0) { - rc = check_trailers(in + p, in_len - p); - if (rc != WOLFCERT_OK) { - WOLFCERT_XFREE(body.buf, heap); - return rc; - } - - *out = body.buf; - *out_len = body.len; - return WOLFCERT_OK; - } - - if (clen > max_bytes || p + 2 > in_len || clen > in_len - p - 2) { - WOLFCERT_XFREE(body.buf, heap); - return WOLFCERT_ERR_PROTOCOL; - } - - rc = dyn_append(&body, in + p, clen); - if (rc != WOLFCERT_OK) { - WOLFCERT_XFREE(body.buf, heap); - return rc; + *out_len = w; + return check_trailers(src + p, len - p); } - p += clen; - if (in[p] != '\r' || in[p+1] != '\n') { - WOLFCERT_XFREE(body.buf, heap); + if (p + 2 > len || clen > len - p - 2 || + src[p + clen] != '\r' || src[p + clen + 1] != '\n') return WOLFCERT_ERR_PROTOCOL; - } - p += 2; + memmove(dst + w, src + p, clen); + w += clen; + p += clen + 2; } - - WOLFCERT_XFREE(body.buf, heap); - - return WOLFCERT_ERR_PROTOCOL; } -static int read_body(WolfCertConn* c, DynBuf* rx, size_t body_start, - const char* headers, - uint8_t** out, size_t* out_len, - size_t max_bytes, void* heap) +int wolfcert_http_chunked_decode(const uint8_t* in, size_t in_len, + uint8_t** out, size_t* out_len, + size_t max_bytes, void* heap) { - char* te = find_header(headers, "Transfer-Encoding", heap); - char* cl = find_header(headers, "Content-Length", heap); - - int chunked = (te != NULL && ci_cmp(te, "chunked") == 0); - long length = -1; - if (cl != NULL) - length = strtol(cl, NULL, 10); - - WOLFCERT_XFREE(te, heap); - WOLFCERT_XFREE(cl, heap); + uint8_t* buf; + size_t len = 0; + int rc; - if (length >= 0 && (size_t)length > max_bytes) + if (in_len == 0) return WOLFCERT_ERR_PROTOCOL; - if (chunked) { - WolfCertChunkScan scan = { .max = max_bytes }; - int framed; - - for (;;) { - framed = wolfcert_http_chunked_complete(rx->buf + body_start, - rx->len - body_start, - &scan); - if (framed != 0) - break; - - uint8_t tmp[WOLFCERT_HTTP_READ_CHUNK]; - int r = conn_read(c, tmp, sizeof(tmp)); - if (r <= 0) - return WOLFCERT_ERR_IO; - - int rc = dyn_append(rx, tmp, (size_t)r); - if (rc != WOLFCERT_OK) - return rc; - } - - if (framed < 0) - return WOLFCERT_ERR_PROTOCOL; - - return wolfcert_http_chunked_decode(rx->buf + body_start, - rx->len - body_start, - out, out_len, max_bytes, heap); - } - - if (length >= 0) { - while (rx->len < body_start + (size_t)length) { - uint8_t tmp[WOLFCERT_HTTP_READ_CHUNK]; - - int r = conn_read(c, tmp, sizeof(tmp)); - if (r <= 0) - return WOLFCERT_ERR_IO; - - int rc = dyn_append(rx, tmp, (size_t)r); - if (rc != WOLFCERT_OK) - return rc; - } - - size_t n = (size_t)length; - 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); - } - - *out = b; - *out_len = n; - - return WOLFCERT_OK; - } - - /* read until close */ - while (1) { - uint8_t tmp[WOLFCERT_HTTP_READ_CHUNK]; - - int r = conn_read(c, tmp, sizeof(tmp)); - if (r == WOLFCERT_ERR_CONN_CLOSED) - break; - if (r < 0) - return WOLFCERT_ERR_IO; + buf = (uint8_t*)WOLFCERT_XMALLOC(in_len, heap); + if (buf == NULL) + return WOLFCERT_ERR_MEMORY; - int rc = dyn_append(rx, tmp, (size_t)r); - if (rc != WOLFCERT_OK) - return rc; + rc = chunked_decode_into(buf, in, in_len, &len); + if (rc == WOLFCERT_OK && len > max_bytes) + rc = WOLFCERT_ERR_PROTOCOL; + if (rc != WOLFCERT_OK || len == 0) { + WOLFCERT_XFREE(buf, heap); + buf = NULL; } - - size_t n = rx->len - body_start; - 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); + if (rc == WOLFCERT_OK) { + *out = buf; + *out_len = len; } - *out = b; - *out_len = n; - - return WOLFCERT_OK; + return rc; } typedef struct { @@ -1278,482 +1062,140 @@ static int setup_tls_ex(WolfCertConn* c, const TlsDials* dials, return WOLFCERT_OK; } -static int do_tls_handshake_blocking(WolfCertConn* c) +static size_t rx_max(size_t max_body) +{ + return max_body + WOLFCERT_HTTP_HEADER_BUDGET; +} + +int wolfcert_http_session_open(const WolfCertHttpSessionCfg* cfg, + WolfCertHttpSession** out) { - int r = wolfSSL_connect(c->ssl); + if (cfg == NULL || cfg->base_url == NULL || out == NULL) + return WOLFCERT_ERR_BAD_ARG; + + void* heap = cfg->heap ? cfg->heap : wolfcert_default_heap(); + + WolfCertHttpSession* s = (WolfCertHttpSession*)WOLFCERT_XMALLOC(sizeof(*s), heap); + if (s == NULL) + return WOLFCERT_ERR_MEMORY; + + memset(s, 0, sizeof(*s)); + s->heap = heap; + s->keep_alive = 1; + s->max_body = cfg->max_response_bytes + ? cfg->max_response_bytes : WOLFCERT_HTTP_DEFAULT_MAX_BODY; + + int rc = wolfcert_http_url_parse(cfg->base_url, &s->base, heap); + if (rc != WOLFCERT_OK) { + WOLFCERT_XFREE(s, heap); + return rc; + } + + rc = dial(&s->conn, s->base.host, s->base.port, cfg->timeout_ms, + &cfg->transport); + if (rc != WOLFCERT_OK) { + wolfcert_http_session_close(s); + return rc; + } - if (r != WOLFSSL_SUCCESS) { - int e = wolfSSL_get_error(c->ssl, r); - char buf[80]; - wolfSSL_ERR_error_string((unsigned long)e, buf); + if (s->base.tls) { + TlsDials dials = { + .trust_anchors = cfg->trust_anchors, + .trust_anchors_len = cfg->trust_anchors_len, + .verify_server = cfg->verify_server, + .client_cert = cfg->client_cert, + .client_cert_len = cfg->client_cert_len, + .client_key = cfg->client_key, + .client_key_len = cfg->client_key_len, + .allow_post_handshake_auth = cfg->allow_post_handshake_auth, + }; - return WOLFCERT_ERR_WC(e, "http", - "TLS handshake failed: wolfSSL_connect=%d err=%d (%s)", r, e, buf); + rc = setup_tls_ex(&s->conn, &dials, s->base.host, &s->ctx); + if (rc != WOLFCERT_OK) { + wolfcert_http_session_close(s); + return rc; + } + s->sm_state = SM_HANDSHAKE; } + *out = s; return WOLFCERT_OK; } -static int do_tls_handshake_step(WolfCertConn* c) +int wolfcert_http_session_fd(const WolfCertHttpSession* s) { - int r = wolfSSL_connect(c->ssl); - if (r == WOLFSSL_SUCCESS) - return WOLFCERT_OK; +#ifdef WOLFCERT_HAVE_BUILTIN_TRANSPORT + if (s == NULL || !s->conn.connected) + return -1; - int e = wolfSSL_get_error(c->ssl, r); - if (e == WOLFSSL_ERROR_WANT_READ) - return WOLFCERT_ERR_WANT_READ; - else if (e == WOLFSSL_ERROR_WANT_WRITE) - return WOLFCERT_ERR_WANT_WRITE; + return wolfcert_transport_fd(&s->conn.t, s->conn.handle); +#else + (void)s; + return -1; +#endif +} - return WOLFCERT_ERR_TLS; +/* The request head carries the Authorization line, so it is wiped. */ +static void sm_drop_head(WolfCertHttpSession* s) +{ + if (s->sm_head != NULL) + wc_ForceZero(s->sm_head, (word32)s->sm_head_len); + WOLFCERT_XFREE(s->sm_head, s->heap); + s->sm_head = NULL; + s->sm_head_len = 0; } -static int setup_tls(WolfCertConn* c, const WolfCertHttpRequest* req, - const char* sni_host, WOLFSSL_CTX** out_ctx) +static void sm_reset(WolfCertHttpSession* s) { - TlsDials dials = { - .trust_anchors = req->trust_anchors, - .trust_anchors_len = req->trust_anchors_len, - .verify_server = req->verify_server, - .client_cert = req->client_cert, - .client_cert_len = req->client_cert_len, - .client_key = req->client_key, - .client_key_len = req->client_key_len, - }; + sm_drop_head(s); + WOLFCERT_XFREE(s->sm_rx, s->heap); + WOLFCERT_XFREE(s->sm_content_type, s->heap); + s->sm_body = NULL; + s->sm_body_len = 0; + s->sm_tx_off = 0; + s->sm_rx = NULL; + s->sm_rx_len = 0; + s->sm_rx_cap = 0; + s->sm_hdr_end = 0; + s->sm_hdr_scan = 0; + s->sm_content_length = -1; + s->sm_content_type = NULL; + s->sm_status = 0; + s->sm_resp = NULL; + s->sm_retry_after_sec = 0; + s->sm_head_request = 0; + s->sm_interim = 0; + memset(&s->sm_scan, 0, sizeof(s->sm_scan)); +} - return setup_tls_ex(c, &dials, sni_host, out_ctx); +static int sm_fail(WolfCertHttpSession* s, int rc) +{ + s->closed = 1; + sm_reset(s); + return rc; } -static int http_write_request(WolfCertConn* c, const WolfCertUrl* u, - const WolfCertHttpRequest* req, - int keep_alive, void* heap) +void wolfcert_http_session_close(WolfCertHttpSession* s) { - char auth[WOLFCERT_HTTP_AUTH_BUF_SZ] = { 0 }; + if (s == NULL) + return; - if (req->basic_user != NULL) { - int n = basic_auth_header(req->basic_user, req->basic_pass, - auth, sizeof(auth), heap); - if (n < 0) { - wc_ForceZero(auth, (word32)sizeof(auth)); - return n; - } + if (s->conn.ssl) { + /* Best effort: never wait on a peer that stopped reading. */ + s->conn.io_timeout_ms = 0; + wolfSSL_shutdown(s->conn.ssl); + wolfSSL_free(s->conn.ssl); } - char port_frag[16] = { 0 }; - if ((u->tls && u->port != 443) || (!u->tls && u->port != 80)) { - snprintf(port_frag, sizeof(port_frag), ":%d", u->port); - } + if (s->ctx) + wolfSSL_CTX_free(s->ctx); - const char* open_br = host_is_ip_literal(u->host) ? "[" : ""; - const char* close_br = host_is_ip_literal(u->host) ? "]" : ""; + conn_close(&s->conn); - size_t head_cap = 1024 + (req->content_type ? strlen(req->content_type) : 0) - + (req->content_transfer_encoding ? - strlen(req->content_transfer_encoding) : 0) - + (req->accept ? strlen(req->accept) : 0) - + strlen(u->host) + strlen(u->path) + strlen(auth); - char* head = (char*)WOLFCERT_XMALLOC(head_cap, heap); - if (head == NULL) { - wc_ForceZero(auth, (word32)sizeof(auth)); - return WOLFCERT_ERR_MEMORY; - } - - int hn = snprintf(head, head_cap, - "%s %s HTTP/1.1\r\n" - "Host: %s%s%s%s\r\n" - "User-Agent: wolfCert/%s\r\n" - "Connection: %s\r\n" - "%s%s%s" - "%s%s%s" - "%s%s%s" - "Content-Length: %zu\r\n" - "%s" - "\r\n", - req->method, u->path, - open_br, u->host, close_br, port_frag, - WOLFCERT_VERSION_STRING, - keep_alive ? "keep-alive" : "close", - req->accept ? "Accept: " : "", - req->accept ? req->accept : "", - req->accept ? "\r\n" : "", - req->content_type ? "Content-Type: " : "", - req->content_type ? req->content_type : "", - req->content_type ? "\r\n" : "", - req->content_transfer_encoding ? "Content-Transfer-Encoding: " : "", - req->content_transfer_encoding ? req->content_transfer_encoding : "", - req->content_transfer_encoding ? "\r\n" : "", - req->body_len, - auth); - - wc_ForceZero(auth, (word32)sizeof(auth)); - - if (hn < 0 || (size_t)hn >= head_cap) { - /* A truncated head can still carry part of the Authorization line. */ - wc_ForceZero(head, (word32)head_cap); - WOLFCERT_XFREE(head, heap); - return WOLFCERT_ERR_MEMORY; - } - - int rc = conn_write(c, head, (size_t)hn); - - wc_ForceZero(head, (word32)hn); - WOLFCERT_XFREE(head, heap); - if (rc != WOLFCERT_OK) - return rc; - - if (req->body_len > 0) - rc = conn_write(c, req->body, req->body_len); - - return rc; -} - -static size_t rx_max(size_t max_body) -{ - return max_body + WOLFCERT_HTTP_HEADER_BUDGET; -} - -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) }; - 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; - - for (;;) { - hdr_end = read_headers(c, &rx); - if (hdr_end < 0) { - rc = hdr_end; - break; - } - - WOLFCERT_XFREE(headers_nt, heap); - headers_nt = (char*)WOLFCERT_XMALLOC((size_t)hdr_end + 1, heap); - if (headers_nt == NULL) { - rc = WOLFCERT_ERR_MEMORY; - break; - } - - memcpy(headers_nt, rx.buf, (size_t)hdr_end); - headers_nt[hdr_end] = '\0'; - - rc = parse_status_line(headers_nt, &status); - if (rc != WOLFCERT_OK) - break; - - 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; - } - - 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); - ra = find_header(headers_nt, "Retry-After", heap); - if (ra != NULL) { - retry_after = parse_retry_after(ra); - 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); - } - } - - if (rc == WOLFCERT_OK) { - resp->status_code = status; - resp->content_type = ct; - resp->retry_after_sec = retry_after; - ct = NULL; /* ownership moved */ - } - - WOLFCERT_XFREE(ct, heap); - WOLFCERT_XFREE(headers_nt, heap); - WOLFCERT_XFREE(rx.buf, heap); - - return rc; -} - -int wolfcert_http_request(const WolfCertHttpRequest* req, WolfCertHttpResponse* resp) -{ - if (req == NULL || resp == NULL || req->url == NULL || req->method == NULL) - return WOLFCERT_ERR_BAD_ARG; - - memset(resp, 0, sizeof(*resp)); - void* heap = req->heap ? req->heap : wolfcert_default_heap(); - resp->heap = heap; - size_t max_body = req->max_response_bytes - ? req->max_response_bytes : WOLFCERT_HTTP_DEFAULT_MAX_BODY; - - WolfCertUrl u; - int rc = wolfcert_http_url_parse(req->url, &u, heap); - if (rc != WOLFCERT_OK) - return rc; - - WolfCertConn c; - WOLFSSL_CTX* ctx = NULL; - - memset(&c, 0, sizeof(c)); - c.io_timeout_ms = -1; - rc = dial(&c, u.host, u.port, req->timeout_ms, &req->transport); - if (rc != WOLFCERT_OK) { - wolfcert_http_url_free(&u); - return rc; - } - - if (u.tls) { - rc = setup_tls(&c, req, u.host, &ctx); - - if (rc == WOLFCERT_OK) - rc = do_tls_handshake_blocking(&c); - - if (rc != WOLFCERT_OK) { - if (c.ssl) { - wolfSSL_free(c.ssl); - c.ssl = NULL; - } - if (ctx) { - wolfSSL_CTX_free(ctx); - ctx = NULL; - } - - conn_close(&c); - wolfcert_http_url_free(&u); - - return rc; - } - } - - rc = http_write_request(&c, &u, req, 0 /* close */, heap); - if (rc != WOLFCERT_OK) - goto out; - - rc = http_read_response(&c, req->method, max_body, resp, heap); - -out: - if (c.ssl) { - wolfSSL_shutdown(c.ssl); - wolfSSL_free(c.ssl); - } - if (ctx) - wolfSSL_CTX_free(ctx); - conn_close(&c); - wolfcert_http_url_free(&u); - - return rc; -} - -int wolfcert_http_session_open(const WolfCertHttpSessionCfg* cfg, - WolfCertHttpSession** out) -{ - if (cfg == NULL || cfg->base_url == NULL || out == NULL) - return WOLFCERT_ERR_BAD_ARG; - - void* heap = cfg->heap ? cfg->heap : wolfcert_default_heap(); - - WolfCertHttpSession* s = (WolfCertHttpSession*)WOLFCERT_XMALLOC(sizeof(*s), heap); - if (s == NULL) - return WOLFCERT_ERR_MEMORY; - - memset(s, 0, sizeof(*s)); - s->heap = heap; - s->max_body = cfg->max_response_bytes - ? cfg->max_response_bytes : WOLFCERT_HTTP_DEFAULT_MAX_BODY; - - int rc = wolfcert_http_url_parse(cfg->base_url, &s->base, heap); - if (rc != WOLFCERT_OK) { - WOLFCERT_XFREE(s, heap); - return rc; - } - - s->conn.io_timeout_ms = cfg->nonblocking ? 0 : -1; - rc = dial(&s->conn, s->base.host, s->base.port, cfg->timeout_ms, - &cfg->transport); - if (rc != WOLFCERT_OK) { - wolfcert_http_session_close(s); - return rc; - } - - if (s->base.tls) { - TlsDials dials = { - .trust_anchors = cfg->trust_anchors, - .trust_anchors_len = cfg->trust_anchors_len, - .verify_server = cfg->verify_server, - .client_cert = cfg->client_cert, - .client_cert_len = cfg->client_cert_len, - .client_key = cfg->client_key, - .client_key_len = cfg->client_key_len, - .allow_post_handshake_auth = cfg->allow_post_handshake_auth, - }; - - rc = setup_tls_ex(&s->conn, &dials, s->base.host, &s->ctx); - if (rc != WOLFCERT_OK) { - wolfcert_http_session_close(s); - return rc; - } - } - - if (cfg->nonblocking) { - /* io_timeout_ms carries the mode; the socket keeps its own state. */ - s->nonblocking = 1; - - if (s->conn.ssl) { - s->sm_state = SM_HANDSHAKE; - } - else { - s->sm_state = SM_IDLE; - } - } - else if (s->base.tls) { - rc = do_tls_handshake_blocking(&s->conn); - if (rc != WOLFCERT_OK) { - wolfcert_http_session_close(s); - return rc; - } - } - - *out = s; - return WOLFCERT_OK; -} - -int wolfcert_http_session_fd(const WolfCertHttpSession* s) -{ -#ifdef WOLFCERT_HAVE_BUILTIN_TRANSPORT - if (s == NULL || !s->conn.connected) - return -1; - - return wolfcert_transport_fd(&s->conn.t, s->conn.handle); -#else - (void)s; - return -1; -#endif -} - -int wolfcert_http_session_request(WolfCertHttpSession* s, - const WolfCertHttpRequest* req, - WolfCertHttpResponse* resp) -{ - if (s == NULL || s->closed || req == NULL || resp == NULL || - req->url == NULL || req->method == NULL) - return WOLFCERT_ERR_BAD_ARG; - - memset(resp, 0, sizeof(*resp)); - resp->heap = s->heap; - - WolfCertUrl u; - int rc = wolfcert_http_url_parse(req->url, &u, s->heap); - if (rc != WOLFCERT_OK) - return rc; - - if (u.tls != s->base.tls || u.port != s->base.port || - ci_cmp(u.host, s->base.host) != 0) { - wolfcert_http_url_free(&u); - return WOLFCERT_ERR(WOLFCERT_ERR_BAD_ARG, "http", - "http session: request URL %s does not match session base %s://%s:%d", - req->url, s->base.scheme, s->base.host, s->base.port); - } - - rc = http_write_request(&s->conn, &u, req, 1 /* keep-alive */, s->heap); - if (rc != WOLFCERT_OK) { - wolfcert_http_url_free(&u); - s->closed = 1; - return rc; - } - - 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; - - return rc; -} - -/* The request head carries the Authorization line, so it is wiped. */ -static void sm_drop_head(WolfCertHttpSession* s) -{ - if (s->sm_head != NULL) - wc_ForceZero(s->sm_head, (word32)s->sm_head_len); - WOLFCERT_XFREE(s->sm_head, s->heap); - s->sm_head = NULL; - s->sm_head_len = 0; - s->sm_head_off = 0; -} - -static void sm_reset(WolfCertHttpSession* s) -{ - sm_drop_head(s); - WOLFCERT_XFREE(s->sm_rx, s->heap); - WOLFCERT_XFREE(s->sm_content_type, s->heap); - s->sm_body = NULL; - s->sm_body_len = 0; - s->sm_body_off = 0; - s->sm_rx = NULL; - s->sm_rx_len = 0; - s->sm_rx_cap = 0; - s->sm_hdr_end = 0; - s->sm_content_length = -1; - s->sm_content_type = NULL; - s->sm_status = 0; - s->sm_resp = NULL; - s->sm_retry_after_sec = 0; - s->sm_head_request = 0; - s->sm_interim = 0; -} - -static int sm_fail(WolfCertHttpSession* s, int rc) -{ - s->closed = 1; - sm_reset(s); - return rc; -} - -void wolfcert_http_session_close(WolfCertHttpSession* s) -{ - if (s == NULL) - return; - - if (s->conn.ssl) { - wolfSSL_shutdown(s->conn.ssl); - wolfSSL_free(s->conn.ssl); - } - - if (s->ctx) - wolfSSL_CTX_free(s->ctx); - - conn_close(&s->conn); - - WOLFCERT_XFREE(s->residual, s->heap); - sm_reset(s); - wolfcert_http_url_free(&s->base); - WOLFCERT_XFREE(s, s->heap); -} + sm_reset(s); + wolfcert_http_url_free(&s->base); + WOLFCERT_XFREE(s, s->heap); +} static int nb_write(WolfCertConn* c, const uint8_t* buf, size_t len, size_t* off) { @@ -1819,16 +1261,24 @@ static int nb_rx_reserve(WolfCertHttpSession* s, size_t need) return WOLFCERT_OK; } -/* `ended` is set when the peer closed cleanly. */ -static int nb_read_some(WolfCertHttpSession* s, int* ended) +/* Append to sm_rx while it holds fewer than limit bytes; `ended` is set when + * the peer closed cleanly. */ +static int nb_read_some(WolfCertHttpSession* s, size_t limit, int* ended) { + uint8_t probe; + uint8_t* dst; + size_t room; + int probing = 0; + int rc; + int r; + int e; + *ended = 0; + if (s->sm_rx_len > limit) + return WOLFCERT_ERR_PROTOCOL; /* Clamp to the allowance left, so a response ending in it still fits. */ - size_t room = rx_max(s->max_body) - s->sm_rx_len; - uint8_t probe; - uint8_t* dst; - int probing = 0; + room = limit - s->sm_rx_len; if (room == 0) { /* An EOF-delimited body may end exactly on the allowance, so probe @@ -1841,7 +1291,13 @@ static int nb_read_some(WolfCertHttpSession* s, int* ended) if (room > WOLFCERT_HTTP_READ_CHUNK) room = WOLFCERT_HTTP_READ_CHUNK; - int rc = nb_rx_reserve(s, room); + rc = nb_rx_reserve(s, room); + if (rc == WOLFCERT_ERR_MEMORY) { + return WOLFCERT_ERR(WOLFCERT_ERR_MEMORY, "http", + "http: cannot grow the response buffer past %zu bytes; lower " + "max_response_bytes or WOLFCERT_HTTP_HEADER_BUDGET", + s->sm_rx_cap); + } if (rc != WOLFCERT_OK) return rc; @@ -1849,7 +1305,7 @@ static int nb_read_some(WolfCertHttpSession* s, int* ended) } if (s->conn.ssl) { - int r = wolfSSL_read(s->conn.ssl, dst, (int)room); + r = wolfSSL_read(s->conn.ssl, dst, (int)room); if (r > 0) { if (probing) return WOLFCERT_ERR_PROTOCOL; @@ -1863,7 +1319,7 @@ static int nb_read_some(WolfCertHttpSession* s, int* ended) return WOLFCERT_OK; } - int e = wolfSSL_get_error(s->conn.ssl, r); + e = wolfSSL_get_error(s->conn.ssl, r); if (e == WOLFSSL_ERROR_WANT_READ) return WOLFCERT_ERR_WANT_READ; if (e == WOLFSSL_ERROR_WANT_WRITE) @@ -1876,8 +1332,8 @@ static int nb_read_some(WolfCertHttpSession* s, int* ended) return WOLFCERT_ERR_IO; } - int r = s->conn.t.read(s->conn.t.ctx, s->conn.handle, dst, - room, s->conn.io_timeout_ms); + r = s->conn.t.read(s->conn.t.ctx, s->conn.handle, dst, room, + s->conn.io_timeout_ms); if (r > 0) { if ((size_t)r > room) return WOLFCERT_ERR_IO; @@ -1934,7 +1390,7 @@ static int build_head(WolfCertHttpSession* s, const WolfCertHttpRequest* req, "%s %s HTTP/1.1\r\n" "Host: %s%s%s%s\r\n" "User-Agent: wolfCert/%s\r\n" - "Connection: keep-alive\r\n" + "Connection: %s\r\n" "%s%s%s" "%s%s%s" "%s%s%s" @@ -1944,6 +1400,7 @@ static int build_head(WolfCertHttpSession* s, const WolfCertHttpRequest* req, req->method, u->path, open_br, u->host, close_br, port_frag, WOLFCERT_VERSION_STRING, + s->keep_alive ? "keep-alive" : "close", req->accept ? "Accept: " : "", req->accept ? req->accept : "", req->accept ? "\r\n" : "", @@ -1966,13 +1423,14 @@ static int build_head(WolfCertHttpSession* s, const WolfCertHttpRequest* req, s->sm_head = head; s->sm_head_len = (size_t)hn; - s->sm_head_off = 0; + s->sm_tx_off = 0; return WOLFCERT_OK; } static int inspect_headers(WolfCertHttpSession* s) { + int chunked = 0; char* hdrs = (char*)WOLFCERT_XMALLOC(s->sm_hdr_end + 1, s->heap); if (hdrs == NULL) return WOLFCERT_ERR_MEMORY; @@ -2005,13 +1463,7 @@ static int inspect_headers(WolfCertHttpSession* s) } 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)"); - } + chunked = (te != NULL && ci_cmp(te, "chunked") == 0); WOLFCERT_XFREE(te, s->heap); char* cl = find_header(hdrs, "Content-Length", s->heap); @@ -2024,6 +1476,18 @@ static int inspect_headers(WolfCertHttpSession* s) } } + if (chunked) { + memset(&s->sm_scan, 0, sizeof(s->sm_scan)); + s->sm_scan.max = s->max_body; + s->sm_state = SM_READ_BODY_CHUNKED; + } + else if (s->sm_content_length >= 0) { + s->sm_state = SM_READ_BODY_CL; + } + else { + s->sm_state = SM_READ_BODY_EOF; + } + s->sm_content_type = find_header(hdrs, "Content-Type", s->heap); s->sm_retry_after_sec = 0; char* ra = find_header(hdrs, "Retry-After", s->heap); @@ -2036,255 +1500,422 @@ static int inspect_headers(WolfCertHttpSession* s) return WOLFCERT_OK; } -static int finalize_response(WolfCertHttpSession* s) +/* Move the body to the front of sm_rx and hand the buffer over. */ +static uint8_t* sm_take_body(WolfCertHttpSession* s, size_t body_len) { - size_t body_len = s->sm_rx_len - s->sm_hdr_end; - uint8_t* body = NULL; + uint8_t* body = s->sm_rx; - if (body_len > 0) { - body = (uint8_t*)WOLFCERT_XMALLOC(body_len, s->heap); - if (body == NULL) - return WOLFCERT_ERR_MEMORY; - memcpy(body, s->sm_rx + s->sm_hdr_end, body_len); - } + if (body_len == 0) + return NULL; + + memmove(body, body + s->sm_hdr_end, body_len); + s->sm_rx = NULL; + s->sm_rx_len = 0; + s->sm_rx_cap = 0; + + return body; +} +static void sm_publish(WolfCertHttpSession* s, uint8_t* body, size_t body_len) +{ s->sm_resp->status_code = s->sm_status; s->sm_resp->body = body; s->sm_resp->body_len = body_len; s->sm_resp->content_type = s->sm_content_type; s->sm_resp->retry_after_sec = s->sm_retry_after_sec; s->sm_content_type = NULL; /* ownership moved */ - - return WOLFCERT_OK; + s->sm_state = SM_DONE; } -int wolfcert_http_session_request_nb(WolfCertHttpSession* s, - const WolfCertHttpRequest* req, - WolfCertHttpResponse* resp) +static int sm_handshake(WolfCertHttpSession* s) { - if (s == NULL || s->closed || req == NULL || resp == NULL || - req->url == NULL || req->method == NULL) { - return WOLFCERT_ERR_BAD_ARG; + char buf[WOLFSSL_MAX_ERROR_SZ]; + int r = wolfSSL_connect(s->conn.ssl); + int e; + int rc; + + if (r == WOLFSSL_SUCCESS) { + s->sm_state = SM_WRITE_HEAD; + return WOLFCERT_OK; } - if (!s->nonblocking) { - return WOLFCERT_ERR(WOLFCERT_ERR_BAD_ARG, "http", - "wolfcert_http_session_request_nb: session was not opened " - "with WolfCertHttpSessionCfg.nonblocking = 1"); + e = wolfSSL_get_error(s->conn.ssl, r); + if (e == WOLFSSL_ERROR_WANT_READ) + return WOLFCERT_ERR_WANT_READ; + if (e == WOLFSSL_ERROR_WANT_WRITE) + return WOLFCERT_ERR_WANT_WRITE; + + rc = (e == MEMORY_E || e == MEMORY_ERROR) ? WOLFCERT_ERR_MEMORY + : WOLFCERT_ERR_TLS; + wolfSSL_ERR_error_string((unsigned long)e, buf); + return WOLFCERT_ERR_RC_WC(rc, e, "http", + "TLS handshake failed: wolfSSL_connect=%d err=%d (%s)", r, e, buf); +} + +static int sm_write_head(WolfCertHttpSession* s) +{ + int rc = nb_write(&s->conn, (const uint8_t*)s->sm_head, s->sm_head_len, + &s->sm_tx_off); + + if (rc == WOLFCERT_OK) { + sm_drop_head(s); + s->sm_tx_off = 0; + s->sm_state = (s->sm_body_len > 0) ? SM_WRITE_BODY : SM_READ_HEAD; } - /* First call for this request. */ - if (s->sm_resp == NULL) { - memset(resp, 0, sizeof(*resp)); - resp->heap = s->heap; - s->sm_resp = resp; + return rc; +} - WolfCertUrl u; - int rc = wolfcert_http_url_parse(req->url, &u, s->heap); - if (rc != WOLFCERT_OK) { - s->sm_resp = NULL; - return rc; - } +static int sm_write_body(WolfCertHttpSession* s) +{ + int rc = nb_write(&s->conn, s->sm_body, s->sm_body_len, &s->sm_tx_off); + + if (rc == WOLFCERT_OK) + s->sm_state = SM_READ_HEAD; + + return rc; +} + +static int sm_read_head(WolfCertHttpSession* s) +{ + size_t i; + int ended = 0; + int rc; - if (u.tls != s->base.tls || u.port != s->base.port || - ci_cmp(u.host, s->base.host) != 0) { - wolfcert_http_url_free(&u); - s->sm_resp = NULL; - return WOLFCERT_ERR(WOLFCERT_ERR_BAD_ARG, "http", - "http session: request URL %s does not match session base %s://%s:%d", - req->url, s->base.scheme, s->base.host, s->base.port); + for (i = s->sm_hdr_scan; i + 3 < s->sm_rx_len; ++i) { + if (s->sm_rx[i] == '\r' && s->sm_rx[i+1] == '\n' && + s->sm_rx[i+2] == '\r' && s->sm_rx[i+3] == '\n') + break; + } + if (i + 3 >= s->sm_rx_len) { + if (s->sm_rx_len >= WOLFCERT_HTTP_HEADER_BUDGET) { + return WOLFCERT_ERR(WOLFCERT_ERR_PROTOCOL, "http", + "http: response header block exceeds %d bytes", + (int)WOLFCERT_HTTP_HEADER_BUDGET); + } + s->sm_hdr_scan = i; + rc = nb_read_some(s, WOLFCERT_HTTP_HEADER_BUDGET, &ended); + if (rc == WOLFCERT_OK && ended) { + rc = WOLFCERT_ERR(WOLFCERT_ERR_IO, "http", + "http: connection closed inside the response header"); } + return rc; + } - rc = build_head(s, req, &u); - wolfcert_http_url_free(&u); - if (rc != WOLFCERT_OK) { - s->sm_resp = NULL; - return rc; + s->sm_hdr_end = i + 4; + rc = inspect_headers(s); + if (rc != WOLFCERT_OK) + return rc; + + if (!status_is_interim(s->sm_status)) { +#ifdef WOLFSSL_STATIC_MEMORY + /* A pool realloc always copies, so try to grow once for an unknown + * length; without a pool block that large, grow as bytes arrive. */ + if ((s->heap != NULL || wolfSSL_GetGlobalHeapHint() != NULL) && + s->sm_state != SM_READ_BODY_CL) { + rc = nb_rx_reserve(s, rx_max(s->max_body) - s->sm_rx_len); + if (rc != WOLFCERT_ERR_MEMORY) + return rc; } +#endif + return WOLFCERT_OK; + } - s->sm_body = req->body; - s->sm_body_len = req->body_len; - s->sm_body_off = 0; + if (++s->sm_interim > WOLFCERT_HTTP_MAX_INTERIM) { + return WOLFCERT_ERR(WOLFCERT_ERR_PROTOCOL, "http", + "http: more than %d interim 1xx responses arrived before a " + "final one", WOLFCERT_HTTP_MAX_INTERIM); + } - s->sm_head_request = (strcmp(req->method, "HEAD") == 0); - s->sm_interim = 0; + 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; + s->sm_hdr_scan = 0; - if (s->residual_len > 0) { - int rr = nb_rx_reserve(s, s->residual_len); - if (rr != WOLFCERT_OK) { - /* Return via sm_fail() to zero the password in s->sm_head. */ - return sm_fail(s, rr); - } - memcpy(s->sm_rx, s->residual, s->residual_len); - s->sm_rx_len = s->residual_len; - WOLFCERT_XFREE(s->residual, s->heap); - s->residual = NULL; - s->residual_len = 0; + return WOLFCERT_OK; +} + +static int sm_read_body_cl(WolfCertHttpSession* s) +{ + uint8_t* body; + size_t body_len = (size_t)s->sm_content_length; + size_t end = s->sm_hdr_end + body_len; + int ended = 0; + int rc; + + if (s->sm_rx_len < end) { + if (s->sm_rx_cap < end) { + body = (uint8_t*)WOLFCERT_XREALLOC(s->sm_rx, end, s->heap); + if (body == NULL) + return WOLFCERT_ERR_MEMORY; + s->sm_rx = body; + s->sm_rx_cap = end; + } + rc = nb_read_some(s, end, &ended); + if (rc == WOLFCERT_OK && ended) { + rc = WOLFCERT_ERR(WOLFCERT_ERR_IO, "http", + "http: connection closed after %zu of %zu body bytes", + s->sm_rx_len - s->sm_hdr_end, body_len); } + return rc; + } - if (s->sm_state != SM_HANDSHAKE) - s->sm_state = SM_WRITE_HEAD; + /* Without pipelining, bytes past the response mean broken framing. */ + if (s->sm_rx_len > end) + s->closed = 1; + body = sm_take_body(s, body_len); + sm_publish(s, body, body_len); + + return WOLFCERT_OK; +} + +static int sm_read_body_chunked(WolfCertHttpSession* s) +{ + uint8_t* body = NULL; + size_t body_len = 0; + size_t end; + int framed; + int ended = 0; + int rc; + + framed = wolfcert_http_chunked_complete(s->sm_rx + s->sm_hdr_end, + s->sm_rx_len - s->sm_hdr_end, + &s->sm_scan); + if (framed < 0) + return WOLFCERT_ERR_PROTOCOL; + if (framed == 0) { + rc = nb_read_some(s, rx_max(s->max_body), &ended); + if (rc == WOLFCERT_OK && ended) { + rc = WOLFCERT_ERR(WOLFCERT_ERR_IO, "http", + "http: connection closed inside the chunked body"); + } + return rc; } - for (;;) { - switch (s->sm_state) { - case SM_HANDSHAKE: - { - int rc = do_tls_handshake_step(&s->conn); - if (rc != WOLFCERT_OK) { - if (rc == WOLFCERT_ERR_WANT_READ || rc == WOLFCERT_ERR_WANT_WRITE) - return rc; + end = s->sm_hdr_end + s->sm_scan.pos; + rc = chunked_decode_into(s->sm_rx, s->sm_rx + s->sm_hdr_end, + s->sm_scan.pos, &body_len); + if (rc != WOLFCERT_OK) + return rc; - return sm_fail(s, rc); - } + if (s->sm_rx_len > end) + s->closed = 1; + s->sm_hdr_end = 0; + body = sm_take_body(s, body_len); + sm_publish(s, body, body_len); - s->sm_state = SM_WRITE_HEAD; - break; - } - case SM_WRITE_HEAD: - { - int rc = nb_write(&s->conn, (const uint8_t*)s->sm_head, - s->sm_head_len, &s->sm_head_off); - if (rc == WOLFCERT_ERR_WANT_READ || rc == WOLFCERT_ERR_WANT_WRITE) - return rc; + return WOLFCERT_OK; +} + +static int sm_read_body_eof(WolfCertHttpSession* s) +{ + uint8_t* body = NULL; + size_t body_len; + int ended = 0; + int rc = nb_read_some(s, s->sm_hdr_end + s->max_body, &ended); - if (rc != WOLFCERT_OK) - return sm_fail(s, rc); + if (rc != WOLFCERT_OK || !ended) + return rc; - s->sm_state = (s->sm_body_len > 0) ? SM_WRITE_BODY : SM_READ_HEAD; - break; - } - case SM_WRITE_BODY: - { - int rc = nb_write(&s->conn, s->sm_body, - s->sm_body_len, &s->sm_body_off); - if (rc == WOLFCERT_ERR_WANT_READ || rc == WOLFCERT_ERR_WANT_WRITE) - return rc; + body_len = s->sm_rx_len - s->sm_hdr_end; + body = sm_take_body(s, body_len); + sm_publish(s, body, body_len); + s->closed = 1; + + return WOLFCERT_OK; +} + +static int sm_done(WolfCertHttpSession* s) +{ + sm_drop_head(s); + WOLFCERT_XFREE(s->sm_rx, s->heap); + s->sm_rx = NULL; + s->sm_rx_len = 0; + s->sm_rx_cap = 0; + s->sm_hdr_end = 0; + s->sm_hdr_scan = 0; + s->sm_resp = NULL; + s->sm_state = SM_IDLE; + + return WOLFCERT_OK; +} - if (rc != WOLFCERT_OK) - return sm_fail(s, rc); +/* Run the held request until it completes, fails or waits on I/O. */ +static int sm_run(WolfCertHttpSession* s) +{ + int rc; - s->sm_state = SM_READ_HEAD; + do { + switch (s->sm_state) { + case SM_HANDSHAKE: + rc = sm_handshake(s); + break; + case SM_WRITE_HEAD: + rc = sm_write_head(s); + break; + case SM_WRITE_BODY: + rc = sm_write_body(s); break; - } case SM_READ_HEAD: - { - for (size_t i = 0; i + 3 < s->sm_rx_len; ++i) { - if (s->sm_rx[i] == '\r' && s->sm_rx[i+1] == '\n' && - s->sm_rx[i+2] == '\r' && s->sm_rx[i+3] == '\n') { - s->sm_hdr_end = i + 4; - int rc = inspect_headers(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)); - } - - 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; - } - } - int ended = 0; - int rc = nb_read_some(s, &ended); - if (rc == WOLFCERT_ERR_WANT_READ || rc == WOLFCERT_ERR_WANT_WRITE) - return rc; - - if (rc != WOLFCERT_OK) - return sm_fail(s, rc); - - if (ended) - return sm_fail(s, WOLFCERT_ERR_IO); + rc = sm_read_head(s); break; - } case SM_READ_BODY_CL: - { - size_t have = s->sm_rx_len - s->sm_hdr_end; - if (have >= (size_t)s->sm_content_length) { - /* Trim rx to exactly the body; stash extra as residual. */ - size_t want = (size_t)s->sm_content_length; - size_t extra = have - want; - if (extra > 0) { - uint8_t* r = (uint8_t*)WOLFCERT_XMALLOC(extra, s->heap); - if (r == NULL) - return sm_fail(s, WOLFCERT_ERR_MEMORY); - memcpy(r, s->sm_rx + s->sm_hdr_end + want, extra); - s->residual = r; - s->residual_len = extra; - s->sm_rx_len = s->sm_hdr_end + want; - } - - int rc = finalize_response(s); - if (rc != WOLFCERT_OK) - return sm_fail(s, rc); - - s->sm_state = SM_DONE; - goto state_loop_continue; - } - - int ended = 0; - int rc = nb_read_some(s, &ended); - if (rc == WOLFCERT_ERR_WANT_READ || rc == WOLFCERT_ERR_WANT_WRITE) - return rc; - - if (rc != WOLFCERT_OK) - return sm_fail(s, rc); - - if (ended) - return sm_fail(s, WOLFCERT_ERR_IO); + rc = sm_read_body_cl(s); break; - } case SM_READ_BODY_EOF: - { - int ended = 0; - int rc = nb_read_some(s, &ended); - if (rc == WOLFCERT_ERR_WANT_READ || rc == WOLFCERT_ERR_WANT_WRITE) - return rc; - - if (rc != WOLFCERT_OK) - return sm_fail(s, rc); - - if (ended) { - int fr = finalize_response(s); - if (fr != WOLFCERT_OK) - return sm_fail(s, fr); - - s->closed = 1; - s->sm_state = SM_DONE; - } + rc = sm_read_body_eof(s); + break; + case SM_READ_BODY_CHUNKED: + rc = sm_read_body_chunked(s); break; - } case SM_DONE: - { - sm_drop_head(s); - WOLFCERT_XFREE(s->sm_rx, s->heap); - s->sm_rx = NULL; - s->sm_rx_len = 0; - s->sm_rx_cap = 0; - s->sm_hdr_end = 0; - s->sm_resp = NULL; - s->sm_state = SM_IDLE; - return WOLFCERT_OK; - } + rc = sm_done(s); + break; case SM_IDLE: - return sm_fail(s, WOLFCERT_ERR_GENERIC); - } -state_loop_continue: - (void)0; + default: + rc = WOLFCERT_ERR_GENERIC; + break; + } + } while (rc == WOLFCERT_OK && s->sm_state != SM_IDLE); + + if (rc == WOLFCERT_ERR_WANT_READ || rc == WOLFCERT_ERR_WANT_WRITE) + return rc; + if (rc != WOLFCERT_OK) + return sm_fail(s, rc); + + return WOLFCERT_OK; +} + +/* Take a new request: build its head and hold the caller's body and resp. */ +static int sm_begin(WolfCertHttpSession* s, const WolfCertHttpRequest* req, + WolfCertHttpResponse* resp) +{ + WolfCertUrl u; + int rc; + + memset(resp, 0, sizeof(*resp)); + resp->heap = s->heap; + + rc = wolfcert_http_url_parse(req->url, &u, s->heap); + if (rc != WOLFCERT_OK) + return rc; + + if (u.tls != s->base.tls || u.port != s->base.port || + ci_cmp(u.host, s->base.host) != 0) { + wolfcert_http_url_free(&u); + return WOLFCERT_ERR(WOLFCERT_ERR_BAD_ARG, "http", + "http session: request URL %s does not match session base %s://%s:%d", + req->url, s->base.scheme, s->base.host, s->base.port); + } + + rc = build_head(s, req, &u); + wolfcert_http_url_free(&u); + if (rc != WOLFCERT_OK) + return rc; + + s->sm_resp = resp; + s->sm_body = req->body; + s->sm_body_len = req->body_len; + s->sm_head_request = (strcmp(req->method, "HEAD") == 0); + s->sm_interim = 0; + + if (s->sm_state != SM_HANDSHAKE) + s->sm_state = SM_WRITE_HEAD; + + return WOLFCERT_OK; +} + +/* Send req on s, or continue the request s holds when resp is the one it + * writes to. A blocking call (nb 0) lets the transport wait for each read + * and write. */ +static int http_session_call(WolfCertHttpSession* s, + const WolfCertHttpRequest* req, + WolfCertHttpResponse* resp, int nb) +{ + int rc; + + if (s == NULL || req == NULL || resp == NULL || + req->url == NULL || req->method == NULL) { + return WOLFCERT_ERR_BAD_ARG; + } + + if (s->closed) { + return WOLFCERT_ERR(WOLFCERT_ERR_CONN_CLOSED, "http", + "http session: closed by the peer or an earlier error"); + } + + if (s->sm_resp != NULL && (!nb || resp != s->sm_resp)) { + return WOLFCERT_ERR(WOLFCERT_ERR_BAD_ARG, "http", + "http session: a request is in flight; continue it with _nb " + "and the resp it was started with"); + } + + s->conn.io_timeout_ms = nb ? 0 : -1; + if (s->sm_resp == NULL) { + rc = sm_begin(s, req, resp); + if (rc != WOLFCERT_OK) + return rc; } + + rc = sm_run(s); + if (!nb && + (rc == WOLFCERT_ERR_WANT_READ || rc == WOLFCERT_ERR_WANT_WRITE)) { + rc = sm_fail(s, WOLFCERT_ERR(WOLFCERT_ERR_IO, "http", + "transport returned WANT_READ/WANT_WRITE on a blocking call")); + } + + return rc; +} + +int wolfcert_http_session_request(WolfCertHttpSession* s, + const WolfCertHttpRequest* req, + WolfCertHttpResponse* resp) +{ + return http_session_call(s, req, resp, 0); +} + +int wolfcert_http_session_request_nb(WolfCertHttpSession* s, + const WolfCertHttpRequest* req, + WolfCertHttpResponse* resp) +{ + return http_session_call(s, req, resp, 1); +} + +int wolfcert_http_request(const WolfCertHttpRequest* req, WolfCertHttpResponse* resp) +{ + WolfCertHttpSessionCfg cfg; + WolfCertHttpSession* s = NULL; + int rc; + + if (req == NULL || resp == NULL || req->url == NULL || req->method == NULL) + return WOLFCERT_ERR_BAD_ARG; + + memset(resp, 0, sizeof(*resp)); + resp->heap = req->heap ? req->heap : wolfcert_default_heap(); + + memset(&cfg, 0, sizeof(cfg)); + cfg.base_url = req->url; + cfg.trust_anchors = req->trust_anchors; + cfg.trust_anchors_len = req->trust_anchors_len; + cfg.verify_server = req->verify_server; + cfg.timeout_ms = req->timeout_ms; + cfg.max_response_bytes = req->max_response_bytes; + cfg.client_cert = req->client_cert; + cfg.client_cert_len = req->client_cert_len; + cfg.client_key = req->client_key; + cfg.client_key_len = req->client_key_len; + cfg.heap = req->heap; + cfg.transport = req->transport; + + rc = wolfcert_http_session_open(&cfg, &s); + if (rc == WOLFCERT_OK) { + s->keep_alive = 0; + rc = http_session_call(s, req, resp, 0); + } + + wolfcert_http_session_close(s); + + return rc; } void wolfcert_http_response_free(WolfCertHttpResponse* resp) diff --git a/src/internal.h b/src/internal.h index 88bffcf..f7c8861 100644 --- a/src/internal.h +++ b/src/internal.h @@ -226,6 +226,9 @@ int wolfcert_set_error(int wolfcert_rc, int wolfssl_rc, wolfcert_set_error((rc), 0, (module), __VA_ARGS__) #define WOLFCERT_ERR_WC(wc_rc, module, ...) \ wolfcert_set_error(wolfcert_map_wc_err(wc_rc), (wc_rc), (module), __VA_ARGS__) +/* Keeps wc_rc for wolfcert_last_wolfssl_err() but returns rc unmapped. */ +#define WOLFCERT_ERR_RC_WC(rc, wc_rc, module, ...) \ + wolfcert_set_error((rc), (wc_rc), (module), __VA_ARGS__) void wolfcert_logv(WolfCertLogLevel lvl, const char* module, const char* fmt, ...); @@ -283,8 +286,9 @@ typedef struct { int in_trailers; } WolfCertChunkScan; -/* 1 once the last chunk and its trailer section have arrived, 0 when more - * bytes are needed, -1 when the framing is malformed. */ +/* 1 once the last chunk and its trailer section have arrived, with st->pos at + * the end of the message; 0 when more bytes are needed, -1 when the framing is + * malformed. */ WOLFCERT_TEST_VIS int wolfcert_http_chunked_complete(const uint8_t* raw, size_t raw_len, WolfCertChunkScan* st); diff --git a/src/scep/scep_client.c b/src/scep/scep_client.c index 8255c79..aa55f25 100644 --- a/src/scep/scep_client.c +++ b/src/scep/scep_client.c @@ -1519,7 +1519,6 @@ static int scep_session_open_common(const WolfCertServerCfg* srv, int nonblockin .client_cert_len = srv->client_cert_len, .client_key = srv->client_key, .client_key_len = srv->client_key_len, - .nonblocking = nonblocking, .transport = srv->transport, .heap = heap, }; diff --git a/tests/integration/est_client_cases.h b/tests/integration/est_client_cases.h index 4c1f09b..81aa9a6 100644 --- a/tests/integration/est_client_cases.h +++ b/tests/integration/est_client_cases.h @@ -203,7 +203,15 @@ static inline int session_basic_auth(const WolfCertServerCfg* client_cfg) } } EST_CHECK(orc == WOLFCERT_OK); - EST_CHECK(wolfcert_est_session_get_cacerts(s, &ca_pem) == WOLFCERT_OK); + if (ret == 0) { + orc = wolfcert_est_session_get_cacerts(s, &ca_pem); + if (orc != WOLFCERT_OK) { + fprintf(stderr, "session_get_cacerts: rc=%d (%s) wolfssl=%d %s\n", + orc, wolfcert_strerror(orc), wolfcert_last_wolfssl_err(), + wolfcert_last_error_message()); + } + } + EST_CHECK(orc == WOLFCERT_OK); EST_CHECK(ca_pem.len > 0); EST_CHECK(wolfcert_est_session_simple_enroll(s, csr.data, csr.len, &issued) == WOLFCERT_OK); diff --git a/tests/integration/test_est_pending_roundtrip.c b/tests/integration/test_est_pending_roundtrip.c index 897c527..465148a 100644 --- a/tests/integration/test_est_pending_roundtrip.c +++ b/tests/integration/test_est_pending_roundtrip.c @@ -298,6 +298,12 @@ static int pending_path(WolfCertServer* s) "BEGIN CERTIFICATE", 17) != NULL); wolfcert_buffer_free(&async_out); + /* A blocking call works on the same session from _open_async. */ + WolfCertBuffer async_ca = { 0 }; + REQUIRE(wolfcert_est_session_get_cacerts(asess, &async_ca) == WOLFCERT_OK); + REQUIRE(async_ca.len > 0); + wolfcert_buffer_free(&async_ca); + wolfcert_est_session_close(asess); wolfcert_buffer_free(&csr_async); wolfcert_key_free(dk_async); @@ -336,13 +342,15 @@ static int pending_path(WolfCertServer* s) "BEGIN CERTIFICATE", 17) != NULL); wolfcert_est_result_free(&sr2); - /* A failed _nb enroll on a blocking session leaves no request in flight. */ + /* An _nb enroll on a session from wolfcert_est_session_open() runs to + * PENDING (the issued CSR is queued again), then a blocking call works. */ WolfCertEstResult sr3 = { 0 }; - REQUIRE(wolfcert_est_session_simple_enroll_nb_ex(xsess, csr_sex.data, - csr_sex.len, &sr3) - == WOLFCERT_ERR_BAD_ARG); - REQUIRE(sr3.status == WOLFCERT_EST_STATUS_UNSET); + REQUIRE(pump_enroll_nb_ex(xsess, csr_sex.data, csr_sex.len, &sr3) + == WOLFCERT_OK); + REQUIRE(sr3.status == WOLFCERT_EST_STATUS_PENDING); + REQUIRE(sr3.retry_after_sec == 1); REQUIRE(sr3.cert_pem.data == NULL); + wolfcert_est_result_free(&sr3); WolfCertBuffer sess_ca = { 0 }; REQUIRE(wolfcert_est_session_get_cacerts(xsess, &sess_ca) == WOLFCERT_OK); diff --git a/tests/integration/test_tls_http.c b/tests/integration/test_tls_http.c index f4016a6..68e355b 100644 --- a/tests/integration/test_tls_http.c +++ b/tests/integration/test_tls_http.c @@ -35,6 +35,7 @@ #include #include +#include #include #include #include @@ -125,45 +126,216 @@ static void* srv_thread(void* arg) return NULL; } +/* Start srv_thread on sc and write its https URL to url; 0 once it listens. + * A thread that never reports its port is detached, since it may block. */ +static int start_srv(struct srv_ctx* sc, pthread_t* tid, char* url, + size_t url_sz) +{ + int i; + + WOLFSSL_ATOMIC_STORE(sc->port, 0); + if (pthread_create(tid, NULL, srv_thread, sc) != 0) + return -1; + for (i = 0; i < 200 && WOLFSSL_ATOMIC_LOAD(sc->port) == 0; ++i) { + test_sleep_ms(5); + } + if (WOLFSSL_ATOMIC_LOAD(sc->port) == 0) { + pthread_detach(*tid); + return -1; + } + + snprintf(url, url_sz, "https://127.0.0.1:%d/", + WOLFSSL_ATOMIC_LOAD(sc->port)); + return 0; +} + +/* Start srv_thread on sc, then GET https://127.0.0.1:port/ with verify_server + * set and ta as the only trust anchor: as a one-shot, or on a session when + * open_rc is not NULL, which then gets the session open's result. */ +static int get_once(struct srv_ctx* sc, const uint8_t* ta, size_t ta_len, + int* open_rc, WolfCertHttpResponse* resp) +{ + WolfCertHttpRequest req = { .method = "GET", .verify_server = 1 }; + WolfCertHttpSessionCfg cfg = { .verify_server = 1 }; + WolfCertHttpSession* s = NULL; + pthread_t tid; + char url[128]; + int rc; + + if (start_srv(sc, &tid, url, sizeof(url)) != 0) + return WOLFCERT_ERR_GENERIC; + req.url = url; + req.trust_anchors = ta; + req.trust_anchors_len = ta_len; + + if (open_rc == NULL) { + rc = wolfcert_http_request(&req, resp); + } + else { + cfg.base_url = url; + cfg.trust_anchors = ta; + cfg.trust_anchors_len = ta_len; + rc = *open_rc = wolfcert_http_session_open(&cfg, &s); + if (rc == WOLFCERT_OK) + rc = wolfcert_http_session_request(s, &req, resp); + wolfcert_http_session_close(s); + } + pthread_join(tid, NULL); + + return rc; +} + +#if defined(USE_WOLFSSL_MEMORY) && !defined(WOLFSSL_STATIC_MEMORY) && \ + !defined(WOLFSSL_DEBUG_MEMORY) +#define TEST_ALLOC_FAILURES +#endif + +#ifdef TEST_ALLOC_FAILURES +/* Only the main thread's allocations count, so srv_thread never fails. */ +static pthread_t g_main_thread; +/* Allocations left before failing_malloc() returns NULL; -1 never fails. */ +static int g_allocs_left = -1; +static int g_alloc_failed; + +static int alloc_fails(void) +{ + if (!pthread_equal(pthread_self(), g_main_thread)) + return 0; + if (g_allocs_left == 0) { + g_alloc_failed = 1; + return 1; + } + if (g_allocs_left > 0) + g_allocs_left--; + return 0; +} + +static void* failing_malloc(size_t sz) +{ + return alloc_fails() ? NULL : malloc(sz); +} + +static void failing_free(void* ptr) +{ + free(ptr); +} + +static void* failing_realloc(void* ptr, size_t sz) +{ + return alloc_fails() ? NULL : realloc(ptr, sz); +} + +/* Lets `n` main-thread allocations succeed and fails the rest. */ +static void oom_arm(int n) +{ + g_allocs_left = n; + g_alloc_failed = 0; +} + +/* Stops failing; 1 when an allocation was failed, 0 if not. */ +static int oom_disarm(void) +{ + g_allocs_left = -1; + return g_alloc_failed; +} + +/* Failing each allocation of a session's first request in turn: one of them + * falls in the handshake and gives WOLFCERT_ERR_MEMORY with the wolfSSL code, + * blocking (nb 0) or non-blocking. */ +static int handshake_oom_case(struct srv_ctx* sc, int nb) +{ + WolfCertHttpSessionCfg cfg = { .verify_server = 1 }; + WolfCertHttpRequest req = { .method = "GET" }; + WolfCertHttpResponse resp = { 0 }; + WolfCertHttpSession* s = NULL; + struct pollfd pfd; + pthread_t tid; + char url[128]; + int fails; + int failed = 1; + int found = 0; + int rc; + + cfg.trust_anchors = sc->cert_pem; + cfg.trust_anchors_len = sc->cert_pem_len; + for (fails = 0; !found && failed == 1; fails++) { + REQUIRE(start_srv(sc, &tid, url, sizeof(url)) == 0); + cfg.base_url = url; + req.url = url; + REQUIRE(wolfcert_http_session_open(&cfg, &s) == WOLFCERT_OK); + + wolfcert_clear_error(); + oom_arm(fails); + do { + rc = nb ? wolfcert_http_session_request_nb(s, &req, &resp) + : wolfcert_http_session_request(s, &req, &resp); + if (rc == WOLFCERT_ERR_WANT_READ || rc == WOLFCERT_ERR_WANT_WRITE) { + pfd.fd = wolfcert_http_session_fd(s); + pfd.events = (rc == WOLFCERT_ERR_WANT_READ) ? POLLIN : POLLOUT; + (void)poll(&pfd, 1, 2000); + } + } while (rc == WOLFCERT_ERR_WANT_READ || rc == WOLFCERT_ERR_WANT_WRITE); + failed = oom_disarm(); + + found = rc == WOLFCERT_ERR_MEMORY && wolfcert_last_wolfssl_err() != 0; + wolfcert_http_response_free(&resp); + wolfcert_http_session_close(s); + pthread_join(tid, NULL); + } + + REQUIRE(found); + return 0; +} +#endif + int main(void) { REQUIRE(wolfcert_init(NULL) == WOLFCERT_OK); struct srv_ctx sc = { 0 }; + struct srv_ctx other = { 0 }; + int open_rc = -1; +#ifdef TEST_ALLOC_FAILURES + /* Installed before any thread starts; they fail nothing until armed. */ + g_main_thread = pthread_self(); + REQUIRE(wolfSSL_SetAllocators(failing_malloc, failing_free, + failing_realloc) == 0); +#endif REQUIRE(gen_server_identity(&sc.cert_pem, &sc.cert_pem_len, &sc.key_pem, &sc.key_pem_len) == 0); + REQUIRE(gen_server_identity(&other.cert_pem, &other.cert_pem_len, + &other.key_pem, &other.key_pem_len) == 0); - pthread_t tid; - REQUIRE(pthread_create(&tid, NULL, srv_thread, &sc) == 0); - for (int i = 0; i < 200 && WOLFSSL_ATOMIC_LOAD(sc.port) == 0; ++i) { - test_sleep_ms(5); - } - REQUIRE(WOLFSSL_ATOMIC_LOAD(sc.port) != 0); - - char url[128]; - snprintf(url, sizeof(url), "https://127.0.0.1:%d/", WOLFSSL_ATOMIC_LOAD(sc.port)); - - WolfCertHttpRequest req = { - .method = "GET", - .url = url, - .trust_anchors = sc.cert_pem, - .trust_anchors_len = sc.cert_pem_len, - .verify_server = 1, - }; WolfCertHttpResponse resp = { 0 }; - int rc = wolfcert_http_request(&req, &resp); - REQUIRE(rc == WOLFCERT_OK); + REQUIRE(get_once(&sc, sc.cert_pem, sc.cert_pem_len, NULL, &resp) + == WOLFCERT_OK); REQUIRE(resp.status_code == 200); REQUIRE(resp.body_len == 2 && memcmp(resp.body, "ok", 2) == 0); wolfcert_http_response_free(&resp); - - pthread_join(tid, NULL); #ifdef WOLFSSL_NO_TLS12 REQUIRE(sc.version != NULL && strcmp(sc.version, "TLSv1.3") == 0); #else REQUIRE(sc.version != NULL && strcmp(sc.version, "TLSv1.2") == 0); #endif + + /* Trusting another self-signed cert fails the handshake with ERR_TLS + * and the wolfSSL code on a session's first request, after its open + * succeeds. */ + wolfcert_clear_error(); + REQUIRE(get_once(&sc, other.cert_pem, other.cert_pem_len, &open_rc, &resp) + == WOLFCERT_ERR_TLS); + REQUIRE(open_rc == WOLFCERT_OK); + REQUIRE(wolfcert_last_wolfssl_err() != 0); + REQUIRE(resp.body == NULL && resp.status_code == 0); + +#ifdef TEST_ALLOC_FAILURES + REQUIRE(handshake_oom_case(&sc, 0) == 0); + REQUIRE(handshake_oom_case(&sc, 1) == 0); +#endif + free(sc.cert_pem); free(sc.key_pem); + free(other.cert_pem); + free(other.key_pem); wolfcert_cleanup(); printf("OK\n"); return 0; diff --git a/tests/unit/test_http.c b/tests/unit/test_http.c index 235264d..d50a93e 100644 --- a/tests/unit/test_http.c +++ b/tests/unit/test_http.c @@ -416,7 +416,6 @@ static int test_session_retry_after_reset(void) WolfCertHttpSessionCfg cfg = { .base_url = base, - .nonblocking = 1, }; WolfCertHttpSession* s = NULL; REQUIRE(wolfcert_http_session_open(&cfg, &s) == WOLFCERT_OK); @@ -470,10 +469,25 @@ static int test_chunked_size_overflow(void) * max_response_bytes = NEAR_CAP_MAX_BODY reads, header budget included. */ #define NEAR_CAP_MAX_BODY 1024 #define NEAR_CAP_TOTAL (NEAR_CAP_MAX_BODY + WOLFCERT_HTTP_HEADER_BUDGET) -#if NEAR_CAP_TOTAL < 256 +#if WOLFCERT_HTTP_HEADER_BUDGET < 256 #error "test_session_near_cap_response needs WOLFCERT_HTTP_HEADER_BUDGET >= 256" #endif +/* A header block of hdr_len bytes padded from head, then body_len bytes ending + * in "ready". */ +static size_t near_cap_fill(char* out, const char* head, size_t hdr_len, + size_t body_len) +{ + size_t hl = strlen(head); + + memcpy(out, head, hl); + memset(out + hl, 'A', hdr_len - hl - 4); + memcpy(out + hdr_len - 4, "\r\n\r\n", 4); + memset(out + hdr_len, 'B', body_len - 5); + memcpy(out + hdr_len + body_len - 5, "ready", 5); + return hdr_len + body_len; +} + static void* srv_thread_near_cap(void* arg) { struct srv_ctx* sc = (struct srv_ctx*)arg; @@ -494,22 +508,19 @@ static void* srv_thread_near_cap(void* arg) break; } - const char* head = - "HTTP/1.1 200 OK\r\n" - "Content-Type: text/plain\r\n" - "Content-Length: 5\r\n" - "X-Pad: "; - const char* tail = "\r\n\r\nready"; - size_t pad = NEAR_CAP_TOTAL - strlen(head) - strlen(tail); + char head[96]; + snprintf(head, sizeof(head), "HTTP/1.1 200 OK\r\n" + "Content-Type: text/plain\r\n" + "Content-Length: %d\r\n" + "X-Pad: ", NEAR_CAP_MAX_BODY); char response[NEAR_CAP_TOTAL]; - memcpy(response, head, strlen(head)); - memset(response + strlen(head), 'A', pad); - memcpy(response + strlen(head) + pad, tail, strlen(tail)); + size_t len = near_cap_fill(response, head, WOLFCERT_HTTP_HEADER_BUDGET, + NEAR_CAP_MAX_BODY); size_t off = 0; - while (off < sizeof(response)) { - ssize_t w = send(cs, response + off, sizeof(response) - off, 0); + while (off < len) { + ssize_t w = send(cs, response + off, len - off, 0); if (w <= 0) break; off += (size_t)w; @@ -536,7 +547,6 @@ static int test_session_near_cap_response(void) WolfCertHttpSessionCfg cfg = { .base_url = base, - .nonblocking = 1, .max_response_bytes = NEAR_CAP_MAX_BODY, }; WolfCertHttpSession* s = NULL; @@ -546,8 +556,8 @@ static int test_session_near_cap_response(void) WolfCertHttpResponse resp = { 0 }; REQUIRE(drive_nb(s, &req, &resp) == WOLFCERT_OK); REQUIRE(resp.status_code == 200); - REQUIRE(resp.body_len == 5); - REQUIRE(memcmp(resp.body, "ready", 5) == 0); + REQUIRE(resp.body_len == NEAR_CAP_MAX_BODY); + REQUIRE(memcmp(resp.body + resp.body_len - 5, "ready", 5) == 0); wolfcert_http_response_free(&resp); wolfcert_http_session_close(s); @@ -581,17 +591,13 @@ static void* srv_thread_eof_cap(void* arg) "Content-Type: text/plain\r\n" "Connection: close\r\n" "X-Pad: "; - const char* tail = "\r\n\r\nready"; - size_t pad = NEAR_CAP_TOTAL - strlen(head) - strlen(tail); - char response[NEAR_CAP_TOTAL]; - memcpy(response, head, strlen(head)); - memset(response + strlen(head), 'A', pad); - memcpy(response + strlen(head) + pad, tail, strlen(tail)); + size_t len = near_cap_fill(response, head, WOLFCERT_HTTP_HEADER_BUDGET, + NEAR_CAP_MAX_BODY); size_t off = 0; - while (off < sizeof(response)) { - ssize_t w = send(cs, response + off, sizeof(response) - off, 0); + while (off < len) { + ssize_t w = send(cs, response + off, len - off, 0); if (w <= 0) break; off += (size_t)w; @@ -618,7 +624,6 @@ static int test_session_eof_cap_response(void) WolfCertHttpSessionCfg cfg = { .base_url = base, - .nonblocking = 1, .max_response_bytes = NEAR_CAP_MAX_BODY, }; WolfCertHttpSession* s = NULL; @@ -628,7 +633,7 @@ static int test_session_eof_cap_response(void) WolfCertHttpResponse resp = { 0 }; REQUIRE(drive_nb(s, &req, &resp) == WOLFCERT_OK); REQUIRE(resp.status_code == 200); - REQUIRE(resp.body_len >= 5); + REQUIRE(resp.body_len == NEAR_CAP_MAX_BODY); REQUIRE(memcmp(resp.body + resp.body_len - 5, "ready", 5) == 0); wolfcert_http_response_free(&resp); @@ -674,7 +679,7 @@ static void* srv_thread_capture(void* arg) } /* content_transfer_encoding = "base64" puts that header on the wire - * (RFC 7030 section 4.2.1). */ + * (RFC 7030 section 4.2.1), and the one-shot asks for Connection: close. */ static int test_request_transfer_encoding(void) { struct capture_ctx cc = { 0 }; @@ -701,10 +706,12 @@ static int test_request_transfer_encoding(void) pthread_join(tid, NULL); REQUIRE(strstr(cc.request, "Content-Transfer-Encoding: base64") != NULL); + REQUIRE(strstr(cc.request, "Connection: close\r\n") != NULL); return 0; } -/* GET http://[::1]:port sends Host: [::1]:port, one-shot or via session. */ +/* GET http://[::1]:port sends Host: [::1]:port, one-shot or via session, with + * Connection: close or keep-alive. */ static int ipv6_host_header(int use_session) { struct capture_ctx cc = { 0 }; @@ -745,6 +752,8 @@ static int ipv6_host_header(int use_session) pthread_join(tid, NULL); REQUIRE(strstr(cc.request, expect) != NULL); + REQUIRE(strstr(cc.request, use_session ? "Connection: keep-alive\r\n" + : "Connection: close\r\n") != NULL); return 0; } @@ -1240,6 +1249,7 @@ static int test_chunked_complete_resume(void) REQUIRE(r == 0); } REQUIRE(wolfcert_http_chunked_complete((const uint8_t*)ok, n, &st) == 1); + REQUIRE(st.pos == n); memset(&st, 0, sizeof(st)); for (n = 1; n < sizeof(bad); n++) { diff --git a/tests/unit/test_transport.c b/tests/unit/test_transport.c index d425ca8..68a4582 100644 --- a/tests/unit/test_transport.c +++ b/tests/unit/test_transport.c @@ -53,6 +53,7 @@ typedef struct { int fail_write; int bogus_connect_rc; /* positive return, breaking the contract */ int overcount; /* report more bytes than written */ + int read_timeout; /* timeout_ms of the last read */ } Peer; static int p_connect(void* ctx, const char* host, int port, int timeout_ms, @@ -76,7 +77,8 @@ static int p_read(void* ctx, void* conn, uint8_t* buf, size_t len, Peer* p = (Peer*)ctx; size_t n = strlen(p->resp) - p->off; - (void)conn; (void)timeout_ms; + (void)conn; + p->read_timeout = timeout_ms; if (p->stall) { p->stall_now = !p->stall_now; if (p->stall_now) @@ -153,6 +155,15 @@ static const char RESP_EOF0[] = static const char RESP_CL0[] = "HTTP/1.1 200 OK\r\nContent-Length: 0\r\nConnection: keep-alive\r\n\r\n"; +/* The peer closes inside the header block, after 3 of 10 body bytes, or + * inside a chunk. */ +static const char RESP_CUT_HEAD[] = + "HTTP/1.1 200 OK\r\nContent-Type: text/pl"; +static const char RESP_CUT_BODY[] = + "HTTP/1.1 200 OK\r\nContent-Length: 10\r\n\r\nhel"; +static const char RESP_CUT_CHUNKED[] = + "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhel"; + /* 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"; @@ -192,13 +203,28 @@ static const char RESP_INTERIM_HDRS[] = "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"; +/* A long interim block, then a final header block shorter than it. */ +static const char RESP_LONG_INTERIM_204[] = + "HTTP/1.1 103 Early Hints\r\n" + "Link: ; rel=preload\r\n\r\n" + "HTTP/1.1 204 No Content\r\n\r\n"; +/* A reply the server sends unasked, right behind the one requested. */ +#define STALE_500 "HTTP/1.1 500 Stale\r\nContent-Length: 0\r\n\r\n" static const char RESP_101[] = "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\n\r\n"; +static const char RESP_CHUNKED[] = + "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n" + "Connection: keep-alive\r\n\r\n" + "5\r\nhello\r\n6;x=1\r\n world\r\n0\r\nX-T: 1\r\n\r\n"; +/* A HEAD reply and a 204 that name a transfer coding but carry no body. */ +static const char RESP_HEAD_CHUNKED[] = + "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n" + "Connection: keep-alive\r\n\r\n"; +static const char RESP_204_CHUNKED[] = + "HTTP/1.1 204 No Content\r\nTransfer-Encoding: chunked\r\n\r\n"; +static const char RESP_CHUNKED_SHORT[] = + "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n" + "3\r\nabc\r\n0\r\n\r\n"; static int test_roundtrip(void) { @@ -238,6 +264,132 @@ static int test_body_ends_at_close(void) return 0; } +/* With a 16-byte cap, a body that ends at the close may be 16 bytes; 17 is + * ERR_PROTOCOL, arriving with the headers or in a later read. */ +static int test_eof_body_capped(void) +{ + static const char hdr[] = "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n"; + WolfCertTransport t = { p_connect, p_read, p_write, p_disconnect, NULL }; + WolfCertHttpRequest req = { .method = "GET", .url = "http://peer.test/" }; + WolfCertHttpResponse resp = { 0 }; + const size_t hl = sizeof(hdr) - 1; + char wire[sizeof(hdr) + 32]; + Peer p = { 0 }; + + t.ctx = &p; + req.transport = t; + req.max_response_bytes = 16; + memcpy(wire, hdr, hl); + + memset(wire + hl, 'B', 16); + wire[hl + 16] = '\0'; + p.resp = wire; + p.chunk = sizeof(wire); + REQUIRE(wolfcert_http_request(&req, &resp) == WOLFCERT_OK); + REQUIRE(resp.body_len == 16); + wolfcert_http_response_free(&resp); + + memset(wire + hl, 'B', 17); + wire[hl + 17] = '\0'; + memset(&p, 0, sizeof(p)); + p.resp = wire; + p.chunk = sizeof(wire); + REQUIRE(wolfcert_http_request(&req, &resp) == WOLFCERT_ERR_PROTOCOL); + + memset(&p, 0, sizeof(p)); + p.resp = wire; + p.chunk = hl + 16; + REQUIRE(wolfcert_http_request(&req, &resp) == WOLFCERT_ERR_PROTOCOL); + return 0; +} + +#if defined(WOLFSSL_STATIC_MEMORY) && defined(WOLFSSL_NO_MALLOC) +/* With a 256 KiB cap no pool block holds the whole allowance: a short + * chunked reply still arrives, and a 160 KiB one, larger than any block, + * fails with a message naming the settings. */ +static int test_chunked_over_pool_block(void) +{ + static const char head[] = + "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n28000\r\n"; + static const char tail[] = "\r\n0\r\n\r\n"; + static char big[sizeof(head) + 0x28000 + sizeof(tail)]; + WolfCertTransport t = { p_connect, p_read, p_write, p_disconnect, NULL }; + WolfCertHttpRequest req = { .method = "GET", .url = "http://peer.test/" }; + WolfCertHttpResponse resp = { 0 }; + const size_t hl = sizeof(head) - 1; + Peer p = { 0 }; + + t.ctx = &p; + req.transport = t; + req.max_response_bytes = 256 * 1024; + p.resp = RESP_CHUNKED; + p.chunk = sizeof(RESP_CHUNKED); + REQUIRE(wolfcert_http_request(&req, &resp) == WOLFCERT_OK); + REQUIRE(resp.body_len == 11 && memcmp(resp.body, "hello world", 11) == 0); + wolfcert_http_response_free(&resp); + + memcpy(big, head, hl); + memset(big + hl, 'A', 0x28000); + memcpy(big + hl + 0x28000, tail, sizeof(tail)); + memset(&p, 0, sizeof(p)); + p.resp = big; + p.chunk = 4096; + wolfcert_clear_error(); + REQUIRE(wolfcert_http_request(&req, &resp) == WOLFCERT_ERR_MEMORY); + REQUIRE(strstr(wolfcert_last_error_message(), "max_response_bytes") + != NULL); + REQUIRE(resp.body == NULL); + return 0; +} +#endif + +/* A header block 100 bytes over the budget, arriving 1000 bytes per read so + * one read could carry it past the budget, is refused with ERR_PROTOCOL. */ +static int test_header_block_over_budget(void) +{ + static const char head[] = + "HTTP/1.1 200 OK\r\nContent-Length: 0\r\nX-Pad: "; + static char wire[WOLFCERT_HTTP_HEADER_BUDGET + 128]; + const size_t total = WOLFCERT_HTTP_HEADER_BUDGET + 100; + const size_t hl = sizeof(head) - 1; + WolfCertHttpResponse resp = { 0 }; + Peer p = { 0 }; + + memcpy(wire, head, hl); + memset(wire + hl, 'A', total - hl - 4); + memcpy(wire + total - 4, "\r\n\r\n", 5); + wolfcert_clear_error(); + REQUIRE(fetch(&p, wire, 1000, &resp) == WOLFCERT_ERR_PROTOCOL); + REQUIRE(strstr(wolfcert_last_error_message(), "header block") != NULL); + return 0; +} + +/* A close before the headers, the Content-Length body or the chunked body + * are complete is ERR_IO with a message and no response. */ +static int test_truncated_reply_is_io(void) +{ + static const char* const cut[] = { + RESP_CUT_HEAD, RESP_CUT_BODY, RESP_CUT_CHUNKED + }; + static const char* const why[] = { + "inside the response header", "after 3 of 10 body bytes", + "inside the chunked body" + }; + WolfCertHttpResponse resp = { 0 }; + Peer p = { 0 }; + size_t i; + + for (i = 0; i < sizeof(cut) / sizeof(cut[0]); i++) { + memset(&p, 0, sizeof(p)); + wolfcert_clear_error(); + REQUIRE(fetch(&p, cut[i], strlen(cut[i]) + 1, &resp) + == WOLFCERT_ERR_IO); + REQUIRE(strstr(wolfcert_last_error_message(), why[i]) != NULL); + REQUIRE(resp.status_code == 0 && resp.body == NULL); + } + return 0; +} + /* A read() returning 0 is treated as a close. */ static int test_zero_is_not_data(void) { @@ -414,6 +566,43 @@ static int test_204_no_body(void) return 0; } +/* A HEAD reply and a 204 carrying Transfer-Encoding: chunked have no body, + * and the next GET on the session gets its own reply. */ +static int test_no_body_ignores_chunked(void) +{ + static const char* const replies[] = { + RESP_HEAD_CHUNKED, RESP_204_CHUNKED + }; + static const char* const methods[] = { "HEAD", "GET" }; + WolfCertHttpRequest req = { .url = "http://peer.test/" }; + WolfCertHttpResponse resp = { 0 }; + WolfCertHttpSession* s = NULL; + Peer p = { 0 }; + size_t i; + + p.open_rc = WOLFCERT_ERR_IO; + REQUIRE(open_scoped(&p, &s) == WOLFCERT_OK); + for (i = 0; i < sizeof(replies) / sizeof(replies[0]); i++) { + p.resp = replies[i]; + p.off = 0; + p.chunk = strlen(replies[i]); + req.method = methods[i]; + REQUIRE(wolfcert_http_session_request(s, &req, &resp) == WOLFCERT_OK); + REQUIRE(resp.body == NULL && resp.body_len == 0); + wolfcert_http_response_free(&resp); + + p.resp = RESP_CL; + p.off = 0; + p.chunk = sizeof(RESP_CL); + req.method = "GET"; + REQUIRE(wolfcert_http_session_request(s, &req, &resp) == WOLFCERT_OK); + REQUIRE(resp.body_len == 5 && memcmp(resp.body, "hello", 5) == 0); + wolfcert_http_response_free(&resp); + } + wolfcert_http_session_close(s); + return 0; +} + static int test_304_no_body(void) { WolfCertHttpResponse resp = { 0 }; @@ -512,6 +701,20 @@ static int test_interim_then_final(void) return 0; } +/* Fed one byte per read, a final header block shorter than the interim one + * before it is still found: 204. */ +static int test_short_final_after_long_interim(void) +{ + WolfCertHttpResponse resp = { 0 }; + Peer p = { 0 }; + + REQUIRE(fetch(&p, RESP_LONG_INTERIM_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_interim_headers_discarded(void) { WolfCertHttpResponse resp = { 0 }; @@ -594,7 +797,6 @@ static int nb_fetch_m(Peer* p, const char* method, const char* bytes, 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) @@ -611,31 +813,6 @@ 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_204_keeps_retry_after(void) -{ - WolfCertHttpResponse resp = { 0 }; - Peer p = { 0 }; - - REQUIRE(nb_fetch(&p, RESP_204_RETRY, &resp) == WOLFCERT_OK); - REQUIRE(resp.status_code == 204); - REQUIRE(resp.retry_after_sec == 30); - REQUIRE(resp.body == NULL && resp.body_len == 0); - wolfcert_http_response_free(&resp); - return 0; -} - static int retry_after_of(const char* value, int nb, int* out) { WolfCertHttpResponse resp = { 0 }; @@ -826,18 +1003,6 @@ static int test_retry_after_unset_clock(void) } #endif -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) { @@ -851,160 +1016,193 @@ static int test_eof_empty_body_is_null(void) return 0; } -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) +/* One byte per read with a WANT_READ between, so parsing resumes inside + * both the interim and the final header block. */ +static int test_nb_interim_fragmented(void) { WolfCertHttpResponse resp = { 0 }; Peer p = { 0 }; - REQUIRE(nb_fetch_m(&p, "head", RESP_CL, strlen(RESP_CL), &resp) - == WOLFCERT_OK); + 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_interim_at_cap_accepted(void) +static int test_nb_204_fragmented(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); + 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_interim_flood_rejected(void) +/* A stale 500 in the same read as a 204 or a chunked body leaves that reply + * intact but closes the session: the next request gets + * WOLFCERT_ERR_CONN_CLOSED with a message. */ +static int test_session_closes_on_bytes_past_body(void) { + static const char* const first[] = { RESP_204, RESP_CHUNKED }; + static const int first_status[] = { 204, 200 }; + WolfCertHttpRequest req = { .method = "GET", .url = "http://peer.test/" }; WolfCertHttpResponse resp = { 0 }; + WolfCertHttpSession* s = NULL; Peer p = { 0 }; + char wire[512]; + size_t i; + + for (i = 0; i < sizeof(first) / sizeof(first[0]); i++) { + snprintf(wire, sizeof(wire), "%s" STALE_500, first[i]); + memset(&p, 0, sizeof(p)); + p.resp = wire; + p.chunk = strlen(wire); + p.open_rc = WOLFCERT_ERR_IO; + + REQUIRE(open_scoped(&p, &s) == WOLFCERT_OK); + REQUIRE(wolfcert_http_session_request(s, &req, &resp) == WOLFCERT_OK); + REQUIRE(resp.status_code == first_status[i]); + wolfcert_http_response_free(&resp); + + wolfcert_clear_error(); + REQUIRE(wolfcert_http_session_request(s, &req, &resp) + == WOLFCERT_ERR_CONN_CLOSED); + REQUIRE(strstr(wolfcert_last_error_message(), "closed") != NULL); + wolfcert_http_session_close(s); + } - REQUIRE(nb_fetch(&p, RESP_INTERIM_FLOOD, &resp) == WOLFCERT_ERR_PROTOCOL); return 0; } -static int test_nb_switching_protocols_rejected(void) +/* Two chunked replies on one non-blocking session, one byte per read with a + * WANT_READ between: "hello world", then a shorter "abc". */ +static int test_nb_chunked_fragmented(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 resp = { 0 }; + WolfCertHttpSession* s = NULL; Peer p = { 0 }; - REQUIRE(nb_fetch(&p, RESP_101, &resp) == WOLFCERT_ERR_PROTOCOL); - return 0; -} + t.ctx = &p; + p.chunk = 1; + p.stall = 1; + p.open_rc = WOLFCERT_ERR_WANT_READ; + cfg.transport = t; -/* One byte per read with a WANT_READ between, so parsing resumes inside - * both the interim and the final header block. */ -static int test_nb_interim_fragmented(void) -{ - WolfCertHttpResponse resp = { 0 }; - Peer p = { 0 }; + REQUIRE(wolfcert_http_session_open(&cfg, &s) == WOLFCERT_OK); - p.stall = 1; - REQUIRE(nb_fetch_m(&p, "GET", RESP_INTERIM, 1, &resp) == WOLFCERT_OK); + p.resp = RESP_CHUNKED; + REQUIRE(drive_nb(s, &req, &resp) == WOLFCERT_OK); REQUIRE(resp.status_code == 200); - REQUIRE(resp.body_len == 5 && memcmp(resp.body, "hello", 5) == 0); + REQUIRE(resp.body_len == 11 && memcmp(resp.body, "hello world", 11) == 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); + p.resp = RESP_CHUNKED_SHORT; + p.off = 0; + REQUIRE(drive_nb(s, &req, &resp) == WOLFCERT_OK); + REQUIRE(resp.body_len == 3 && memcmp(resp.body, "abc", 3) == 0); wolfcert_http_response_free(&resp); + + wolfcert_http_session_close(s); return 0; } -static int test_nb_head_oversized_length_ok(void) +/* An _nb GET waits on WANT_READ with a 0 timeout. A blocking call, or an _nb + * call with another resp, gets BAD_ARG; the _nb GET then completes, and the + * next GET on the session runs blocking with a -1 timeout. */ +static int test_in_flight_request(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/" }; + WolfCertHttpRequest req = { .method = "GET", .url = "http://peer.test/" }; WolfCertHttpResponse resp = { 0 }; + WolfCertHttpResponse other = { 0 }; WolfCertHttpSession* s = NULL; Peer p = { 0 }; t.ctx = &p; - p.resp = RESP_HEAD_BIG; - p.chunk = sizeof(RESP_HEAD_BIG); + p.resp = RESP_CL; + p.chunk = sizeof(RESP_CL); p.open_rc = WOLFCERT_ERR_WANT_READ; - cfg.transport = t; - cfg.nonblocking = 1; - cfg.max_response_bytes = 16; + p.stall = 1; + cfg.transport = t; REQUIRE(wolfcert_http_session_open(&cfg, &s) == WOLFCERT_OK); + REQUIRE(wolfcert_http_session_request_nb(s, &req, &resp) + == WOLFCERT_ERR_WANT_READ); + REQUIRE(p.read_timeout == 0); + + REQUIRE(wolfcert_http_session_request_nb(s, &req, &other) + == WOLFCERT_ERR_BAD_ARG); + REQUIRE(wolfcert_http_session_request(s, &req, &resp) + == WOLFCERT_ERR_BAD_ARG); + REQUIRE(other.body == NULL && other.status_code == 0); + + p.stall = 0; REQUIRE(drive_nb(s, &req, &resp) == WOLFCERT_OK); REQUIRE(resp.status_code == 200); - REQUIRE(resp.body == NULL && resp.body_len == 0); + REQUIRE(resp.body_len == 5 && memcmp(resp.body, "hello", 5) == 0); + wolfcert_http_response_free(&resp); + + p.resp = RESP_CL; + p.off = 0; + REQUIRE(wolfcert_http_session_request(s, &req, &resp) == WOLFCERT_OK); + REQUIRE(p.read_timeout == -1); + REQUIRE(resp.body_len == 5 && memcmp(resp.body, "hello", 5) == 0); wolfcert_http_response_free(&resp); wolfcert_http_session_close(s); return 0; } -static int test_nb_interim_headers_discarded(void) +/* A WANT_READ reaching a blocking session request is ERR_IO and closes the + * session, so the next request gets CONN_CLOSED. */ +static int test_blocking_want_read_is_io(void) { + WolfCertHttpRequest req = { .method = "GET", .url = "http://peer.test/" }; WolfCertHttpResponse resp = { 0 }; + WolfCertHttpSession* s = NULL; 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); + p.resp = RESP_CL; + p.chunk = sizeof(RESP_CL); + p.stall = 1; + + REQUIRE(open_scoped(&p, &s) == WOLFCERT_OK); + wolfcert_clear_error(); + REQUIRE(wolfcert_http_session_request(s, &req, &resp) == WOLFCERT_ERR_IO); + REQUIRE(strstr(wolfcert_last_error_message(), "WANT_READ") != NULL); + REQUIRE(wolfcert_http_session_request(s, &req, &resp) + == WOLFCERT_ERR_CONN_CLOSED); + wolfcert_http_session_close(s); 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) +/* A body that ends at the close gives "bye" and closes the blocking session, + * so the next request gets CONN_CLOSED. */ +static int test_blocking_eof_body_closes(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 }; + WolfCertHttpResponse resp = { 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); + p.resp = RESP_EOF; + p.chunk = sizeof(RESP_EOF); + REQUIRE(open_scoped(&p, &s) == WOLFCERT_OK); + REQUIRE(wolfcert_http_session_request(s, &req, &resp) == WOLFCERT_OK); + REQUIRE(resp.body_len == 3 && memcmp(resp.body, "bye", 3) == 0); + wolfcert_http_response_free(&resp); + REQUIRE(wolfcert_http_session_request(s, &req, &resp) + == WOLFCERT_ERR_CONN_CLOSED); wolfcert_http_session_close(s); return 0; } @@ -1082,6 +1280,16 @@ int main(void) return 1; if (test_body_ends_at_close()) return 1; + if (test_truncated_reply_is_io()) + return 1; + if (test_header_block_over_budget()) + return 1; + if (test_eof_body_capped()) + return 1; +#if defined(WOLFSSL_STATIC_MEMORY) && defined(WOLFSSL_NO_MALLOC) + if (test_chunked_over_pool_block()) + return 1; +#endif if (test_zero_is_not_data()) return 1; if (test_read_overcount_rejected()) @@ -1100,28 +1308,26 @@ int main(void) return 1; if (test_304_no_body()) return 1; + if (test_no_body_ignores_chunked()) + return 1; if (test_204_keeps_retry_after()) 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_headers_discarded()) return 1; + if (test_short_final_after_long_interim()) + 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_204_keeps_retry_after()) - return 1; if (test_retry_after_delay_seconds()) return 1; #ifndef NO_ASN_TIME @@ -1130,29 +1336,23 @@ int main(void) if (test_retry_after_unset_clock()) return 1; #endif - if (test_nb_interim_then_final()) - return 1; if (test_eof_empty_body_is_null()) return 1; - if (test_nb_head_ignores_content_length()) + if (test_nb_interim_fragmented()) return 1; - if (test_nb_interim_at_cap_accepted()) + if (test_nb_204_fragmented()) return 1; - if (test_nb_interim_flood_rejected()) + if (test_nb_chunked_fragmented()) return 1; - if (test_nb_switching_protocols_rejected()) + if (test_in_flight_request()) return 1; - if (test_nb_interim_fragmented()) + if (test_blocking_want_read_is_io()) return 1; - if (test_nb_204_fragmented()) + if (test_blocking_eof_body_closes()) 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()) + if (test_session_closes_on_bytes_past_body()) return 1; #ifdef WOLFCERT_HAVE_BUILTIN_TRANSPORT if (test_zero_transport_takes_builtin()) diff --git a/wolfcert/est.h b/wolfcert/est.h index 63c7cfe..965e761 100644 --- a/wolfcert/est.h +++ b/wolfcert/est.h @@ -161,7 +161,9 @@ WOLFCERT_API int wolfcert_est_simple_reenroll_ex(const WolfCertServerCfg* srv, * the preloaded client cert via TLS 1.3 post-handshake auth (RFC 8446 section * 4.6.2); open then fails with WOLFCERT_ERR_UNSUPPORTED if wolfSSL lacks * WOLFSSL_POST_HANDSHAKE_AUTH. HTTP Basic credentials are copied at open and - * sent on every request. */ + * sent on every request. Open does not verify the server: the first request + * runs the TLS handshake and returns WOLFCERT_ERR_TLS if it fails, or + * WOLFCERT_ERR_MEMORY if wolfSSL runs out of memory. */ typedef struct WolfCertEstSession WolfCertEstSession; WOLFCERT_API int wolfcert_est_session_open(const WolfCertServerCfg* srv, @@ -185,12 +187,12 @@ WOLFCERT_API void wolfcert_est_session_close(WolfCertEstSession* s); * -1 when a caller-supplied WolfCertTransport backs it (no descriptor). */ WOLFCERT_API int wolfcert_est_session_fd(const WolfCertEstSession* s); -/* Non-blocking variants, for a session from wolfcert_est_session_open_async. - * Each _nb call returns WOLFCERT_OK once the output is populated, or - * WOLFCERT_ERR_WANT_READ / _WANT_WRITE to be repeated with the same arguments - * when the fd is ready; any other value ends the request. While a _nb request - * is in flight, any other request on the session returns WOLFCERT_ERR_BAD_ARG - * and leaves it intact. */ +/* Non-blocking variants, for a session from wolfcert_est_session_open or + * wolfcert_est_session_open_async. Each _nb call returns WOLFCERT_OK once the + * output is populated, or WOLFCERT_ERR_WANT_READ / _WANT_WRITE to be repeated + * with the same arguments when the fd is ready; any other value ends the + * request. While a _nb request is in flight, any other request on the session + * returns WOLFCERT_ERR_BAD_ARG and leaves it intact. */ WOLFCERT_API int wolfcert_est_session_open_async(const WolfCertServerCfg* srv, WolfCertEstSession** out); diff --git a/wolfcert/http.h b/wolfcert/http.h index f2c40b3..001cfc5 100644 --- a/wolfcert/http.h +++ b/wolfcert/http.h @@ -104,20 +104,20 @@ typedef struct { /* TLS 1.3 post-handshake auth opt-in (RFC 8446 section 4.6.2). */ int allow_post_handshake_auth; - /* wolfcert_http_session_request_nb returns WANT_READ / WANT_WRITE instead - * of blocking. DNS and the initial connect stay synchronous. */ - int nonblocking; - void* heap; WolfCertTransport transport; } WolfCertHttpSessionCfg; +/* Connects without verifying the server; the first request runs the TLS + * handshake and returns WOLFCERT_ERR_TLS if it fails, or WOLFCERT_ERR_MEMORY + * if wolfSSL runs out of memory. */ WOLFCERT_API int wolfcert_http_session_open (const WolfCertHttpSessionCfg* cfg, WolfCertHttpSession** out); -/* Send one request on the open connection. req->url must match the session's - * scheme, host and port; the TLS fields of req are ignored. */ +/* req->url must match the session's scheme, host and port; req's TLS fields + * are ignored. After a failed exchange, a body ending at the close or extra + * bytes after a reply, requests get WOLFCERT_ERR_CONN_CLOSED. */ WOLFCERT_API int wolfcert_http_session_request(WolfCertHttpSession* s, const WolfCertHttpRequest* req, WolfCertHttpResponse* resp); @@ -129,10 +129,9 @@ WOLFCERT_API void wolfcert_http_session_close(WolfCertHttpSession* s); * close. */ WOLFCERT_API int wolfcert_http_session_fd(const WolfCertHttpSession* s); -/* Non-blocking wolfcert_http_session_request, for a session opened with - * nonblocking = 1. Returns WOLFCERT_OK with resp populated, or WANT_READ / - * WANT_WRITE to be repeated with the same, unmodified req and resp once the fd - * is ready. Any other error is permanent and the caller closes the session. */ +/* wolfcert_http_session_request without waiting, on any session. WANT_READ / + * WANT_WRITE: repeat with the same req and resp once the fd is ready, keeping + * req->body valid and unchanged; other calls get WOLFCERT_ERR_BAD_ARG. */ WOLFCERT_API int wolfcert_http_session_request_nb(WolfCertHttpSession* s, const WolfCertHttpRequest* req, WolfCertHttpResponse* resp); diff --git a/wolfcert/scep.h b/wolfcert/scep.h index d10ac96..560c91f 100644 --- a/wolfcert/scep.h +++ b/wolfcert/scep.h @@ -225,7 +225,10 @@ WOLFCERT_API int wolfcert_scep_get_next_ca_cert(const WolfCertServerCfg* srv, * the _nb calls wolfcert_scep_session_open_async; a mismatch returns * WOLFCERT_ERR_BAD_ARG. A _nb call returning WANT_READ / WANT_WRITE is repeated * with the same arguments; a call with a different out pointer returns - * WOLFCERT_ERR_BAD_ARG. DNS and the initial connect stay synchronous. */ + * WOLFCERT_ERR_BAD_ARG. DNS and the initial connect stay synchronous. Over + * https:// open does not verify the server: the first request runs the TLS + * handshake and returns WOLFCERT_ERR_TLS if it fails, or WOLFCERT_ERR_MEMORY + * if wolfSSL runs out of memory. */ typedef struct WolfCertScepSession WolfCertScepSession; WOLFCERT_API int wolfcert_scep_session_open(const WolfCertServerCfg* srv,