From 02edae54e8b3953b8aeb0ff91be86d5db2169670 Mon Sep 17 00:00:00 2001 From: Stefan Eissing Date: Thu, 2 Jan 2025 16:34:52 +0100 Subject: [PATCH] websocket: fix message send corruption - Fix a bug in EAGAIN handling when sending frames that led to a corrupted last byte of the frame sent. - Restore sanity to curl_ws_send() behaviour: - Partial writes are reported as OK with the actual number of payload bytes sent. - CURLE_AGAIN is only returned when none of the payload bytes (or for 0-length frames, not all of the frame header bytes) could be sent. - curl_ws_send() now behaves like a common send() call. - Change 'ws-data' test client to allow concurrent send/recv operations and vary frame sizes and repeat count. - Add DEBUG env var CURL_WS_CHUNK_EAGAIN to simulate blocking after a chunk of an encoded websocket frame has been sent. - Add tests. Prior to this change data corruption may occur when sending websocket messages due to two bugs: 1) 3e64569a (precedes 8.10.0) caused a data corruption bug in the last byte of frame of large messages. 2) curl_ws_send had non-traditional send behavior and could return CURLE_AGAIN with bytes sent and expect the caller to adjust buffer and buflen in a subsequent call. That behavior was not documented. Reported-by: na-trium-144@users.noreply.github.com Fixes https://github.com/curl/curl/issues/15865 Fixes https://github.com/curl/curl/issues/15865#issuecomment-2569870144 Closes https://github.com/curl/curl/pull/15901 --- docs/libcurl/libcurl-env-dbg.md | 5 + lib/bufq.c | 23 --- lib/bufq.h | 8 - lib/ws.c | 156 +++++++++------- lib/ws.h | 3 +- tests/http/clients/ws-data.c | 294 +++++++++++++++++++------------ tests/http/test_20_websockets.py | 33 ++-- 7 files changed, 298 insertions(+), 224 deletions(-) diff --git a/docs/libcurl/libcurl-env-dbg.md b/docs/libcurl/libcurl-env-dbg.md index 73217ca209..0b6d66bba9 100644 --- a/docs/libcurl/libcurl-env-dbg.md +++ b/docs/libcurl/libcurl-env-dbg.md @@ -130,6 +130,11 @@ greater. There is a number of debug levels, refer to *openldap.c* comments. Used to influence the buffer chunk size used for WebSocket encoding and decoding. +## CURL_WS_CHUNK_EAGAIN + +Used to simulate blocking sends after this chunk size for WebSocket +connections. + ## CURL_FORBID_REUSE Used to set the CURLOPT_FORBID_REUSE flag on each transfer initiated diff --git a/lib/bufq.c b/lib/bufq.c index 547d4d3762..724d62f31c 100644 --- a/lib/bufq.c +++ b/lib/bufq.c @@ -45,11 +45,6 @@ static size_t chunk_len(const struct buf_chunk *chunk) return chunk->w_offset - chunk->r_offset; } -static size_t chunk_space(const struct buf_chunk *chunk) -{ - return chunk->dlen - chunk->w_offset; -} - static void chunk_reset(struct buf_chunk *chunk) { chunk->next = NULL; @@ -287,24 +282,6 @@ size_t Curl_bufq_len(const struct bufq *q) return len; } -size_t Curl_bufq_space(const struct bufq *q) -{ - size_t space = 0; - if(q->tail) - space += chunk_space(q->tail); - if(q->spare) { - struct buf_chunk *chunk = q->spare; - while(chunk) { - space += chunk->dlen; - chunk = chunk->next; - } - } - if(q->chunk_count < q->max_chunks) { - space += (q->max_chunks - q->chunk_count) * q->chunk_size; - } - return space; -} - bool Curl_bufq_is_empty(const struct bufq *q) { return !q->head || chunk_is_empty(q->head); diff --git a/lib/bufq.h b/lib/bufq.h index ec415648fd..60059deb30 100644 --- a/lib/bufq.h +++ b/lib/bufq.h @@ -150,14 +150,6 @@ void Curl_bufq_free(struct bufq *q); */ size_t Curl_bufq_len(const struct bufq *q); -/** - * Return the total amount of free space in the queue. - * The returned length is the number of bytes that can - * be expected to be written successfully to the bufq, - * providing no memory allocations fail. - */ -size_t Curl_bufq_space(const struct bufq *q); - /** * Returns TRUE iff there is no data in the buffer queue. */ diff --git a/lib/ws.c b/lib/ws.c index 0c5479b964..bae0ad6c49 100644 --- a/lib/ws.c +++ b/lib/ws.c @@ -1009,8 +1009,28 @@ static CURLcode ws_flush(struct Curl_easy *data, struct websocket *ws, CURLcode result; const unsigned char *out; size_t outlen, n; +#ifdef DEBUGBUILD + /* Simulate a blocking send after this chunk has been sent */ + bool eagain_next = FALSE; + size_t chunk_egain = 0; + char *p = getenv("CURL_WS_CHUNK_EAGAIN"); + if(p) { + long l = strtol(p, NULL, 10); + if(l > 0 && l <= (1*1024*1024)) { + chunk_egain = (size_t)l; + } + } +#endif while(Curl_bufq_peek(&ws->sendbuf, &out, &outlen)) { +#ifdef DEBUGBUILD + if(eagain_next) + return CURLE_AGAIN; + if(chunk_egain && (outlen > chunk_egain)) { + outlen = chunk_egain; + eagain_next = TRUE; + } +#endif if(blocking) { result = ws_send_raw_blocking(data, ws, (char *)out, outlen); n = result ? 0 : outlen; @@ -1119,15 +1139,15 @@ static CURLcode ws_send_raw(struct Curl_easy *data, const void *buffer, return result; } -CURL_EXTERN CURLcode curl_ws_send(CURL *d, const void *buffer, +CURL_EXTERN CURLcode curl_ws_send(CURL *d, const void *buffer_arg, size_t buflen, size_t *sent, curl_off_t fragsize, unsigned int flags) { struct websocket *ws; + const unsigned char *buffer = buffer_arg; ssize_t n; - size_t space, payload_added; - CURLcode result; + CURLcode result = CURLE_OK; struct Curl_easy *data = d; CURL_TRC_WS(data, "curl_ws_send(len=%zu, fragsize=%" FMT_OFF_T @@ -1151,13 +1171,13 @@ CURL_EXTERN CURLcode curl_ws_send(CURL *d, const void *buffer, } ws = data->conn->proto.ws; - /* try flushing any content still waiting to be sent. */ - result = ws_flush(data, ws, FALSE); - if(result) - goto out; - if(data->set.ws_raw_mode) { /* In raw mode, we write directly to the connection */ + /* try flushing any content still waiting to be sent. */ + result = ws_flush(data, ws, FALSE); + if(result) + goto out; + if(fragsize || flags) { failf(data, "ws_send, raw mode: fragsize and flags cannot be non-zero"); return CURLE_BAD_FUNCTION_ARGUMENT; @@ -1167,87 +1187,87 @@ CURL_EXTERN CURLcode curl_ws_send(CURL *d, const void *buffer, } /* Not RAW mode, buf we do the frame encoding */ - space = Curl_bufq_space(&ws->sendbuf); - CURL_TRC_WS(data, "curl_ws_send(len=%zu), sendbuf=%zu space_left=%zu", - buflen, Curl_bufq_len(&ws->sendbuf), space); - if(space < 14) { - result = CURLE_AGAIN; - goto out; - } - if(flags & CURLWS_OFFSET) { - if(fragsize) { - /* a frame series 'fragsize' bytes big, this is the first */ - n = ws_enc_write_head(data, &ws->enc, flags, fragsize, - &ws->sendbuf, &result); - if(n < 0) - goto out; + if(ws->enc.payload_remain || !Curl_bufq_is_empty(&ws->sendbuf)) { + /* a frame is ongoing with payload buffered or more payload + * that needs to be encoded into the buffer */ + if(buflen < ws->sendbuf_payload) { + /* We have been called with LESS buffer data than before. This + * is not how it's supposed too work. */ + failf(data, "curl_ws_send() called with smaller 'buflen' than " + "bytes already buffered in previous call, %zu vs %zu", + buflen, ws->sendbuf_payload); + result = CURLE_BAD_FUNCTION_ARGUMENT; + goto out; } - else { - if((curl_off_t)buflen > ws->enc.payload_remain) { - infof(data, "WS: unaligned frame size (sending %zu instead of %" - FMT_OFF_T ")", - buflen, ws->enc.payload_remain); - } + if((curl_off_t)buflen > + (ws->enc.payload_remain + (curl_off_t)ws->sendbuf_payload)) { + /* too large buflen beyond payload length of frame */ + infof(data, "WS: unaligned frame size (sending %zu instead of %" + FMT_OFF_T ")", + buflen, ws->enc.payload_remain + ws->sendbuf_payload); + result = CURLE_BAD_FUNCTION_ARGUMENT; + goto out; } } - else if(!ws->enc.payload_remain) { - n = ws_enc_write_head(data, &ws->enc, flags, (curl_off_t)buflen, + else { + /* starting a new frame, we want a clean sendbuf */ + curl_off_t payload_len = (flags & CURLWS_OFFSET) ? + fragsize : (curl_off_t)buflen; + result = ws_flush(data, ws, Curl_is_in_callback(data)); + if(result) + goto out; + + n = ws_enc_write_head(data, &ws->enc, flags, payload_len, &ws->sendbuf, &result); if(n < 0) goto out; } - n = ws_enc_write_payload(&ws->enc, data, - buffer, buflen, &ws->sendbuf, &result); - if(n < 0) - goto out; - payload_added = (size_t)n; + /* While there is either sendbuf to flush OR more payload to encode... */ + while(!Curl_bufq_is_empty(&ws->sendbuf) || (buflen > ws->sendbuf_payload)) { + /* Try to add more payload to sendbuf */ + if(buflen > ws->sendbuf_payload) { + size_t prev_len = Curl_bufq_len(&ws->sendbuf); + n = ws_enc_write_payload(&ws->enc, data, + buffer + ws->sendbuf_payload, + buflen - ws->sendbuf_payload, + &ws->sendbuf, &result); + if(n < 0 && (result != CURLE_AGAIN)) + goto out; + ws->sendbuf_payload += Curl_bufq_len(&ws->sendbuf) - prev_len; + } - while(!result && (buflen || !Curl_bufq_is_empty(&ws->sendbuf))) { /* flush, blocking when in callback */ result = ws_flush(data, ws, Curl_is_in_callback(data)); if(!result) { - DEBUGASSERT(payload_added <= buflen); - /* all buffered data sent. Try sending the rest if there is any. */ - *sent += payload_added; - buffer = (const char *)buffer + payload_added; - buflen -= payload_added; - payload_added = 0; - if(buflen) { - n = ws_enc_write_payload(&ws->enc, data, - buffer, buflen, &ws->sendbuf, &result); - if(n < 0) - goto out; - payload_added = Curl_bufq_len(&ws->sendbuf); - } + *sent += ws->sendbuf_payload; + buffer += ws->sendbuf_payload; + buflen -= ws->sendbuf_payload; + ws->sendbuf_payload = 0; } else if(result == CURLE_AGAIN) { - /* partially sent. how much of the call data has been part of it? what - * should we report to out caller so it can retry/send the rest? */ - if(payload_added < buflen) { - /* We did not add everything the caller wanted. Return just - * the partial write to our buffer. */ - *sent = payload_added; + if(ws->sendbuf_payload > Curl_bufq_len(&ws->sendbuf)) { + /* blocked, part of payload bytes remain, report length + * that we managed to send. */ + size_t flushed = (ws->sendbuf_payload - Curl_bufq_len(&ws->sendbuf)); + *sent += flushed; + ws->sendbuf_payload -= flushed; result = CURLE_OK; goto out; } - else if(!buflen) { - /* We have no payload to report a partial write. EAGAIN would make - * the caller repeat this and add the frame again. - * Flush blocking seems the only way out of this. */ - *sent = (size_t)n; - result = ws_flush(data, ws, TRUE); + else { + /* blocked before sending headers or 1st payload byte. We cannot report + * OK on 0-length send (caller counts only payload) and EAGAIN */ + CURL_TRC_WS(data, "EAGAIN flushing sendbuf, payload_encoded: %zu/%zu", + ws->sendbuf_payload, buflen); + DEBUGASSERT(*sent == 0); + result = CURLE_AGAIN; goto out; } - /* We added the complete data to our sendbuf. Report one byte less as - * sent. This partial success should make the caller invoke us again - * with the last byte. */ - *sent = payload_added - 1; - result = Curl_bufq_unwrite(&ws->sendbuf, 1); - if(!result) - result = CURLE_AGAIN; } + else + goto out; /* real error sending the data */ } out: diff --git a/lib/ws.h b/lib/ws.h index e438528359..c96bccab9f 100644 --- a/lib/ws.h +++ b/lib/ws.h @@ -53,7 +53,7 @@ struct ws_encoder { unsigned int xori; /* xor index */ unsigned char mask[4]; /* 32-bit mask for this connection */ unsigned char firstbyte; /* first byte of frame we encode */ - bool contfragment; /* set TRUE if the previous fragment sent was not final */ + BIT(contfragment); /* set TRUE if the previous fragment sent was not final */ }; /* A websocket connection with en- and decoder that treat frames @@ -65,6 +65,7 @@ struct websocket { struct bufq recvbuf; /* raw data from the server */ struct bufq sendbuf; /* raw data to be sent to the server */ struct curl_ws_frame frame; /* the current WS FRAME received */ + size_t sendbuf_payload; /* number of payload bytes in sendbuf */ }; CURLcode Curl_ws_request(struct Curl_easy *data, struct dynbuf *req); diff --git a/tests/http/clients/ws-data.c b/tests/http/clients/ws-data.c index 4a3be67bf2..9632ea2989 100644 --- a/tests/http/clients/ws-data.c +++ b/tests/http/clients/ws-data.c @@ -26,14 +26,18 @@ * */ /* curl stuff */ -#include "curl_setup.h" #include #include #include #include -#ifndef CURL_DISABLE_WEBSOCKETS +#if !defined(CURL_DISABLE_WEBSOCKETS) && !defined(_MSC_VER) + +#ifndef _MSC_VER +/* somewhat Unix-specific */ +#include /* getopt() */ +#endif #ifdef _WIN32 #ifndef WIN32_LEAN_AND_MEAN @@ -44,6 +48,7 @@ #include #endif + static void dump(const char *text, unsigned char *ptr, size_t size, char nohex) @@ -93,86 +98,45 @@ void dump(const char *text, unsigned char *ptr, size_t size, } } -static CURLcode send_binary(CURL *curl, char *buf, size_t buflen) +static CURLcode check_recv(const struct curl_ws_frame *frame, + size_t r_offset, size_t nread, size_t exp_len) { - size_t nwritten; - CURLcode result = - curl_ws_send(curl, buf, buflen, &nwritten, 0, CURLWS_BINARY); - fprintf(stderr, "ws: send_binary(len=%ld) -> %d, %ld\n", - (long)buflen, result, (long)nwritten); - return result; + if(!frame) + return CURLE_OK; + + if(frame->flags & CURLWS_CLOSE) { + fprintf(stderr, "recv_data: unexpected CLOSE frame from server, " + "got %ld bytes, offset=%ld, rflags %x\n", + (long)nread, (long)r_offset, frame->flags); + return CURLE_RECV_ERROR; + } + if(!r_offset && !(frame->flags & CURLWS_BINARY)) { + fprintf(stderr, "recv_data: wrong frame, got %ld bytes, offset=%ld, " + "rflags %x\n", + (long)nread, (long)r_offset, frame->flags); + return CURLE_RECV_ERROR; + } + if(frame->offset != (curl_off_t)r_offset) { + fprintf(stderr, "recv_data: frame offset, expected %ld, got %ld\n", + (long)r_offset, (long)frame->offset); + return CURLE_RECV_ERROR; + } + if(frame->bytesleft != (curl_off_t)(exp_len - r_offset - nread)) { + fprintf(stderr, "recv_data: frame bytesleft, expected %ld, got %ld\n", + (long)(exp_len - r_offset - nread), (long)frame->bytesleft); + return CURLE_RECV_ERROR; + } + if(r_offset + nread > exp_len) { + fprintf(stderr, "recv_data: data length, expected %ld, now at %ld\n", + (long)exp_len, (long)(r_offset + nread)); + return CURLE_RECV_ERROR; + } + return CURLE_OK; } #if defined(__TANDEM) # include #endif -static CURLcode recv_binary(CURL *curl, char *exp_data, size_t exp_len) -{ - const struct curl_ws_frame *frame; - char recvbuf[256]; - size_t r_offset, nread; - CURLcode result; - - fprintf(stderr, "recv_binary: expected payload %ld bytes\n", (long)exp_len); - r_offset = 0; - while(1) { - result = curl_ws_recv(curl, recvbuf, sizeof(recvbuf), &nread, &frame); - if(result == CURLE_AGAIN) { - fprintf(stderr, "EAGAIN, sleep, try again\n"); -#ifdef _WIN32 - Sleep(100); -#elif defined(__TANDEM) - /* NonStop only defines usleep when building for a threading model */ -# if defined(_PUT_MODEL_) || defined(_KLT_MODEL_) - usleep(100*1000); -# else - PROCESS_DELAY_(100*1000); -# endif -#else - usleep(100*1000); -#endif - continue; - } - fprintf(stderr, "ws: curl_ws_recv(offset=%ld, len=%ld) -> %d, %ld\n", - (long)r_offset, (long)sizeof(recvbuf), result, (long)nread); - if(result) { - return result; - } - if(!(frame->flags & CURLWS_BINARY)) { - fprintf(stderr, "recv_data: wrong frame, got %ld bytes rflags %x\n", - (long)nread, frame->flags); - return CURLE_RECV_ERROR; - } - if(frame->offset != (curl_off_t)r_offset) { - fprintf(stderr, "recv_data: frame offset, expected %ld, got %ld\n", - (long)r_offset, (long)frame->offset); - return CURLE_RECV_ERROR; - } - if(frame->bytesleft != (curl_off_t)(exp_len - r_offset - nread)) { - fprintf(stderr, "recv_data: frame bytesleft, expected %ld, got %ld\n", - (long)(exp_len - r_offset - nread), (long)frame->bytesleft); - return CURLE_RECV_ERROR; - } - if(r_offset + nread > exp_len) { - fprintf(stderr, "recv_data: data length, expected %ld, now at %ld\n", - (long)exp_len, (long)(r_offset + nread)); - return CURLE_RECV_ERROR; - } - if(memcmp(exp_data + r_offset, recvbuf, nread)) { - fprintf(stderr, "recv_data: data differs, offset=%ld, len=%ld\n", - (long)r_offset, (long)nread); - dump("expected:", (unsigned char *)exp_data + r_offset, nread, 0); - dump("received:", (unsigned char *)recvbuf, nread, 0); - return CURLE_RECV_ERROR; - } - r_offset += nread; - if(r_offset >= exp_len) { - fprintf(stderr, "recv_data: frame complete\n"); - break; - } - } - return CURLE_OK; -} /* just close the connection */ static void websocket_close(CURL *curl) @@ -184,73 +148,175 @@ static void websocket_close(CURL *curl) "ws: curl_ws_send returned %u, sent %u\n", (int)result, (int)sent); } -static CURLcode data_echo(CURL *curl, size_t plen_min, size_t plen_max) +static CURLcode data_echo(CURL *curl, size_t count, + size_t plen_min, size_t plen_max) { - CURLcode res = CURLE_OK; + CURLcode r = CURLE_OK; + const struct curl_ws_frame *frame; size_t len; - char *send_buf; - size_t i; + char *send_buf = NULL, *recv_buf = NULL; + size_t i, scount = count, rcount = count; + int rblock, sblock; + + send_buf = calloc(1, plen_max + 1); + recv_buf = calloc(1, plen_max + 1); + if(!send_buf || !recv_buf) { + r = CURLE_OUT_OF_MEMORY; + goto out; + } - send_buf = calloc(1, plen_max); - if(!send_buf) - return CURLE_OUT_OF_MEMORY; for(i = 0; i < plen_max; ++i) { send_buf[i] = (char)('0' + ((int)i % 10)); } for(len = plen_min; len <= plen_max; ++len) { - res = send_binary(curl, send_buf, len); - if(res) - goto out; - res = recv_binary(curl, send_buf, len); - if(res) { - fprintf(stderr, "recv_data(len=%ld) -> %d\n", (long)len, res); + size_t nwritten, nread, slen = len, rlen = len; + char *sbuf = send_buf, *rbuf = recv_buf; + + memset(recv_buf, 0, plen_max); + while(slen || rlen || scount || rcount) { + sblock = rblock = 1; + if(slen) { + r = curl_ws_send(curl, sbuf, slen, &nwritten, 0, CURLWS_BINARY); + sblock = (r == CURLE_AGAIN); + if(!r || (r == CURLE_AGAIN)) { + fprintf(stderr, "curl_ws_send(len=%ld) -> %d, %ld (%ld/%ld)\n", + (long)slen, r, (long)nwritten, + (long)(len - slen), (long)len); + sbuf += nwritten; + slen -= nwritten; + } + else + goto out; + } + if(!slen && scount) { /* go again? */ + scount--; + sbuf = send_buf; + slen = len; + } + + if(rlen) { + size_t max_recv = (64 * 1024); + r = curl_ws_recv(curl, rbuf, (rlen > max_recv) ? max_recv : rlen, + &nread, &frame); + if(!r || (r == CURLE_AGAIN)) { + rblock = (r == CURLE_AGAIN); + fprintf(stderr, "curl_ws_recv(len=%ld) -> %d, %ld (%ld/%ld) \n", + (long)rlen, r, (long)nread, (long)(len - rlen), (long)len); + if(!r) { + r = check_recv(frame, len - rlen, nread, len); + if(r) + goto out; + } + rbuf += nread; + rlen -= nread; + } + else + goto out; + } + if(!rlen && rcount) { /* go again? */ + rcount--; + rbuf = recv_buf; + rlen = len; + } + + if(rblock && sblock) { + fprintf(stderr, "EAGAIN, sleep, try again\n"); + #ifdef _WIN32 + Sleep(100); + #elif defined(__TANDEM) + /* NonStop only defines usleep when building for a threading model */ + # if defined(_PUT_MODEL_) || defined(_KLT_MODEL_) + usleep(100*1000); + # else + PROCESS_DELAY_(100*1000); + # endif + #else + usleep(100*1000); + #endif + } + } + + if(memcmp(send_buf, recv_buf, len)) { + fprintf(stderr, "recv_data: data differs\n"); + dump("expected:", (unsigned char *)send_buf, len, 0); + dump("received:", (unsigned char *)recv_buf, len, 0); + r = CURLE_RECV_ERROR; goto out; } } out: - if(!res) + if(!r) websocket_close(curl); free(send_buf); - return res; + free(recv_buf); + return r; +} + +static void usage(const char *msg) +{ + if(msg) + fprintf(stderr, "%s\n", msg); + fprintf(stderr, + "usage: [options] url\n" + " -m number minimum frame size\n" + " -M number maximum frame size\n" + ); } #endif int main(int argc, char *argv[]) { -#ifndef CURL_DISABLE_WEBSOCKETS +#if !defined(CURL_DISABLE_WEBSOCKETS) && !defined(_MSC_VER) CURL *curl; CURLcode res = CURLE_OK; const char *url; - long l1, l2; - size_t plen_min, plen_max; - + size_t plen_min = 0, plen_max = 0, count = 1; + int ch; - if(argc != 4) { - fprintf(stderr, "usage: ws-data url minlen maxlen\n"); - return 2; - } - url = argv[1]; - l1 = strtol(argv[2], NULL, 10); - if(l1 < 0) { - fprintf(stderr, "minlen must be >= 0, got %ld\n", l1); - return 2; - } - l2 = strtol(argv[3], NULL, 10); - if(l2 < 0) { - fprintf(stderr, "maxlen must be >= 0, got %ld\n", l2); - return 2; + while((ch = getopt(argc, argv, "c:hm:M:")) != -1) { + switch(ch) { + case 'h': + usage(NULL); + res = CURLE_BAD_FUNCTION_ARGUMENT; + goto cleanup; + case 'c': + count = (size_t)strtol(optarg, NULL, 10); + break; + case 'm': + plen_min = (size_t)strtol(optarg, NULL, 10); + break; + case 'M': + plen_max = (size_t)strtol(optarg, NULL, 10); + break; + default: + usage("invalid option"); + res = CURLE_BAD_FUNCTION_ARGUMENT; + goto cleanup; + } } - plen_min = l1; - plen_max = l2; + argc -= optind; + argv += optind; + + if(!plen_max) + plen_max = plen_min; + if(plen_max < plen_min) { fprintf(stderr, "maxlen must be >= minlen, got %ld-%ld\n", (long)plen_min, (long)plen_max); - return 2; + res = CURLE_BAD_FUNCTION_ARGUMENT; + goto cleanup; } + if(argc != 1) { + usage(NULL); + res = CURLE_BAD_FUNCTION_ARGUMENT; + goto cleanup; + } + url = argv[0]; + curl_global_init(CURL_GLOBAL_ALL); curl = curl_easy_init(); @@ -264,11 +330,13 @@ int main(int argc, char *argv[]) res = curl_easy_perform(curl); fprintf(stderr, "curl_easy_perform() returned %u\n", (int)res); if(res == CURLE_OK) - res = data_echo(curl, plen_min, plen_max); + res = data_echo(curl, count, plen_min, plen_max); /* always cleanup */ curl_easy_cleanup(curl); } + +cleanup: curl_global_cleanup(); return (int)res; diff --git a/tests/http/test_20_websockets.py b/tests/http/test_20_websockets.py index eb9df306b3..4d7075422f 100644 --- a/tests/http/test_20_websockets.py +++ b/tests/http/test_20_websockets.py @@ -103,40 +103,51 @@ class TestWebsockets: r = client.run(args=[url, payload]) r.check_exit_code(56) - # the python websocket server does not like 'large' control frames def test_20_04_data_small(self, env: Env, ws_echo, repeat): client = LocalClient(env=env, name='ws-data') if not client.exists(): pytest.skip(f'example client not built: {client.name}') url = f'ws://localhost:{env.ws_port}/' - r = client.run(args=[url, str(0), str(10)]) + r = client.run(args=['-m', str(0), '-M', str(10), url]) r.check_exit_code(0) - # the python websocket server does not like 'large' control frames def test_20_05_data_med(self, env: Env, ws_echo, repeat): client = LocalClient(env=env, name='ws-data') if not client.exists(): pytest.skip(f'example client not built: {client.name}') url = f'ws://localhost:{env.ws_port}/' - r = client.run(args=[url, str(120), str(130)]) + r = client.run(args=['-m', str(120), '-M', str(130), url]) r.check_exit_code(0) - # the python websocket server does not like 'large' control frames def test_20_06_data_large(self, env: Env, ws_echo, repeat): client = LocalClient(env=env, name='ws-data') if not client.exists(): pytest.skip(f'example client not built: {client.name}') url = f'ws://localhost:{env.ws_port}/' - r = client.run(args=[url, str(65535 - 5), str(65535 + 5)]) + r = client.run(args=['-m', str(65535 - 5), '-M', str(65535 + 5), url]) r.check_exit_code(0) - # the python websocket server does not like 'large' control frames def test_20_07_data_large_small_recv(self, env: Env, ws_echo, repeat): - client = LocalClient(env=env, name='ws-data', run_env={ - 'CURL_WS_CHUNK_SIZE': '1024', - }) + run_env = os.environ.copy() + run_env['CURL_WS_CHUNK_SIZE'] = '1024' + client = LocalClient(env=env, name='ws-data', run_env=run_env) + if not client.exists(): + pytest.skip(f'example client not built: {client.name}') + url = f'ws://localhost:{env.ws_port}/' + r = client.run(args=['-m', str(65535 - 5), '-M', str(65535 + 5), url]) + r.check_exit_code(0) + + # Send large frames and simulate send blocking on 8192 bytes chunks + # Simlates error reported in #15865 + def test_20_08_data_very_large(self, env: Env, ws_echo, repeat): + run_env = os.environ.copy() + run_env['CURL_WS_CHUNK_EAGAIN'] = '8192' + client = LocalClient(env=env, name='ws-data', run_env=run_env) if not client.exists(): pytest.skip(f'example client not built: {client.name}') url = f'ws://localhost:{env.ws_port}/' - r = client.run(args=[url, str(65535 - 5), str(65535 + 5)]) + count = 10 + large = 512 * 1024 + large = 20000 + r = client.run(args=['-c', str(count), '-m', str(large), url]) r.check_exit_code(0) -- 2.47.2