From f0c446ab577e1452cd53f0c509720f697b1076f7 Mon Sep 17 00:00:00 2001 From: Stefan Eissing Date: Thu, 15 Feb 2024 16:39:40 +0100 Subject: [PATCH] websocket: fix curl_ws_recv() - when data arrived in several chunks, the collection into the passed buffer always started at offset 0, overwriting the data already there. adding test_20_07 to verify fix - debug environment var CURL_WS_CHUNK_SIZE can be used to influence the buffer chunk size used for en-/decoding. Closes #12945 --- docs/libcurl/libcurl-env-dbg.md | 5 +++++ lib/ws.c | 21 +++++++++++++++++---- tests/http/test_20_websockets.py | 11 +++++++++++ tests/http/testenv/client.py | 6 ++++-- 4 files changed, 37 insertions(+), 6 deletions(-) diff --git a/docs/libcurl/libcurl-env-dbg.md b/docs/libcurl/libcurl-env-dbg.md index 21b763bb4f..21ed3bf25a 100644 --- a/docs/libcurl/libcurl-env-dbg.md +++ b/docs/libcurl/libcurl-env-dbg.md @@ -116,3 +116,8 @@ Debug-version of the *ntlm-wb* executable. OpenLDAP tracing is enabled if this variable exists and its value is 1 or greater. There is a number of debug levels, refer to *openldap.c* comments. + +## CURL_WS_CHUNK_SIZE + +Used to influence the buffer chunk size used for WebSocket encoding and +decoding. diff --git a/lib/ws.c b/lib/ws.c index d9765182d9..f4675cec47 100644 --- a/lib/ws.c +++ b/lib/ws.c @@ -754,13 +754,26 @@ CURLcode Curl_ws_accept(struct Curl_easy *data, DEBUGASSERT(data->conn); ws = data->conn->proto.ws; if(!ws) { + size_t chunk_size = WS_CHUNK_SIZE; ws = calloc(1, sizeof(*ws)); if(!ws) return CURLE_OUT_OF_MEMORY; data->conn->proto.ws = ws; - Curl_bufq_init2(&ws->recvbuf, WS_CHUNK_SIZE, WS_CHUNK_COUNT, +#ifdef DEBUGBUILD + { + char *p = getenv("CURL_WS_CHUNK_SIZE"); + if(p) { + long l = strtol(p, NULL, 10); + if(l > 0 && l <= (1*1024*1024)) { + chunk_size = (size_t)l; + } + } + } +#endif + DEBUGF(infof(data, "WS, using chunk size %zu", chunk_size)); + Curl_bufq_init2(&ws->recvbuf, chunk_size, WS_CHUNK_COUNT, BUFQ_OPT_SOFT_LIMIT); - Curl_bufq_init2(&ws->sendbuf, WS_CHUNK_SIZE, WS_CHUNK_COUNT, + Curl_bufq_init2(&ws->sendbuf, chunk_size, WS_CHUNK_COUNT, BUFQ_OPT_SOFT_LIMIT); ws_dec_init(&ws->dec); ws_enc_init(&ws->enc); @@ -834,7 +847,7 @@ CURLcode Curl_ws_accept(struct Curl_easy *data, struct ws_collect { struct Curl_easy *data; - void *buffer; + unsigned char *buffer; size_t buflen; size_t bufidx; int frame_age; @@ -886,7 +899,7 @@ static ssize_t ws_client_collect(const unsigned char *buf, size_t buflen, return -1; } *err = CURLE_OK; - memcpy(ctx->buffer, buf, nwritten); + memcpy(ctx->buffer + ctx->bufidx, buf, nwritten); ctx->bufidx += nwritten; } return nwritten; diff --git a/tests/http/test_20_websockets.py b/tests/http/test_20_websockets.py index 4e70dcef00..eb9df306b3 100644 --- a/tests/http/test_20_websockets.py +++ b/tests/http/test_20_websockets.py @@ -129,3 +129,14 @@ class TestWebsockets: url = f'ws://localhost:{env.ws_port}/' r = client.run(args=[url, str(65535 - 5), str(65535 + 5)]) 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', + }) + 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.check_exit_code(0) diff --git a/tests/http/testenv/client.py b/tests/http/testenv/client.py index 098e55b9cd..e8ffb040aa 100644 --- a/tests/http/testenv/client.py +++ b/tests/http/testenv/client.py @@ -45,10 +45,12 @@ log = logging.getLogger(__name__) class LocalClient: def __init__(self, name: str, env: Env, run_dir: Optional[str] = None, - timeout: Optional[float] = None): + timeout: Optional[float] = None, + run_env: Optional[Dict[str,str]] = None): self.name = name self.path = os.path.join(env.project_dir, f'tests/http/clients/{name}') self.env = env + self._run_env= run_env self._timeout = timeout if timeout else env.test_timeout self._curl = os.environ['CURL'] if 'CURL' in os.environ else env.curl self._run_dir = run_dir if run_dir else os.path.join(env.gen_dir, name) @@ -95,7 +97,7 @@ class LocalClient: with open(self._stderrfile, 'w') as cerr: p = subprocess.run(myargs, stderr=cerr, stdout=cout, cwd=self._run_dir, shell=False, - input=None, + input=None, env=self._run_env, timeout=self._timeout) exitcode = p.returncode except subprocess.TimeoutExpired: -- 2.47.3