websockets: buffer ugprade data at connection level

When the HTTP Upgrade to websockets already carries ws frame data,
buffer that data at connection level and not in the ws decoder.

Adding new cfilter `cf_recvbuf` to buffer a fixed amont of data
to be received later. When the data is received, the filter
passes further recv call through to its subfilter.

Fixes #22107
Reported-by: sideshowbarker on github
Closes #22111
This commit is contained in:
Stefan Eissing 2026-06-19 17:01:00 +02:00 committed by Daniel Stenberg
parent 810d9535e1
commit 8ed285f06d
No known key found for this signature in database
GPG key ID: 5CC908FDB71E12C2
7 changed files with 237 additions and 8 deletions

View file

@ -165,6 +165,7 @@ LIB_CFILES = \
cf-haproxy.c \
cf-https-connect.c \
cf-ip-happy.c \
cf-recvbuf.c \
cf-setup.c \
cf-socket.c \
cfilters.c \
@ -298,6 +299,7 @@ LIB_HFILES = \
cf-haproxy.h \
cf-https-connect.h \
cf-ip-happy.h \
cf-recvbuf.h \
cf-setup.h \
cf-socket.h \
cfilters.h \

157
lib/cf-recvbuf.c Normal file
View file

@ -0,0 +1,157 @@
/***************************************************************************
* _ _ ____ _
* Project ___| | | | _ \| |
* / __| | | | |_) | |
* | (__| |_| | _ <| |___
* \___|\___/|_| \_\_____|
*
* Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
*
* This software is licensed as described in the file COPYING, which
* you should have received as part of this distribution. The terms
* are also available at https://curl.se/docs/copyright.html.
*
* You may opt to use, copy, modify, merge, publish, distribute and/or sell
* copies of the Software, and permit persons to whom the Software is
* furnished to do so, under the terms of the COPYING file.
*
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
* KIND, either express or implied.
*
* SPDX-License-Identifier: curl
*
***************************************************************************/
#include "curl_setup.h"
#ifndef CURL_DISABLE_WEBSOCKETS
/* only used for this protocol, so far */
#include "urldata.h"
#include "bufq.h"
#include "cfilters.h"
#include "cf-recvbuf.h"
#include "curl_trc.h"
#define CURL_CF_RECVBUF_CHUNK (16 * 1024)
struct cf_recvbuf_ctx {
struct bufq recvbuf;
};
static void cf_recvbuf_destroy(struct Curl_cfilter *cf,
struct Curl_easy *data)
{
struct cf_recvbuf_ctx *ctx = cf->ctx;
(void)data;
if(ctx) {
Curl_bufq_free(&ctx->recvbuf);
curlx_free(ctx);
}
}
static CURLcode cf_recvbuf_recv(struct Curl_cfilter *cf,
struct Curl_easy *data,
char *buf, size_t len,
size_t *pnread)
{
struct cf_recvbuf_ctx *ctx = cf->ctx;
if(!Curl_bufq_is_empty(&ctx->recvbuf)) {
return Curl_bufq_cread(&ctx->recvbuf, buf, len, pnread);
}
if(cf->next)
return cf->next->cft->do_recv(cf->next, data, buf, len, pnread);
*pnread = 0;
return CURLE_RECV_ERROR;
}
static bool cf_recvbuf_data_pending(struct Curl_cfilter *cf,
const struct Curl_easy *data)
{
struct cf_recvbuf_ctx *ctx = cf->ctx;
if(!Curl_bufq_is_empty(&ctx->recvbuf))
return TRUE;
return cf->next ?
cf->next->cft->has_data_pending(cf->next, data) : FALSE;
}
struct Curl_cftype Curl_cft_recvbuf = {
"RECVBUF",
0,
CURL_LOG_LVL_NONE,
cf_recvbuf_destroy,
Curl_cf_def_connect,
Curl_cf_def_shutdown,
Curl_cf_def_adjust_pollset,
cf_recvbuf_data_pending,
Curl_cf_def_send,
cf_recvbuf_recv,
Curl_cf_def_cntrl,
Curl_cf_def_conn_is_alive,
Curl_cf_def_conn_keep_alive,
Curl_cf_def_query,
};
static CURLcode cf_recvbuf_create(struct Curl_cfilter **pcf,
struct Curl_easy *data,
const uint8_t *buf, size_t blen)
{
struct Curl_cfilter *cf = NULL;
struct cf_recvbuf_ctx *ctx;
CURLcode result = CURLE_OK;
size_t nwritten = 0;
(void)data;
ctx = curlx_calloc(1, sizeof(*ctx));
if(!ctx) {
result = CURLE_OUT_OF_MEMORY;
goto out;
}
Curl_bufq_init2(&ctx->recvbuf, CURL_CF_RECVBUF_CHUNK,
(blen / CURL_CF_RECVBUF_CHUNK) + 1,
(BUFQ_OPT_SOFT_LIMIT | BUFQ_OPT_NO_SPARES));
result = Curl_bufq_write(&ctx->recvbuf, buf, blen, &nwritten);
if(result)
goto out;
if(nwritten != blen) {
result = CURLE_FAILED_INIT;
goto out;
}
result = Curl_cf_create(&cf, &Curl_cft_recvbuf, ctx);
if(result)
goto out;
ctx = NULL;
out:
*pcf = result ? NULL : cf;
if(ctx) {
Curl_bufq_free(&ctx->recvbuf);
curlx_free(ctx);
}
return result;
}
CURLcode Curl_cf_recvbuf_add(struct Curl_easy *data,
struct connectdata *conn,
int sockindex,
const uint8_t *buf, size_t blen)
{
struct Curl_cfilter *cf;
CURLcode result = CURLE_OK;
DEBUGASSERT(data);
result = cf_recvbuf_create(&cf, data, buf, blen);
if(result)
goto out;
cf->connected = Curl_conn_is_connected(conn, sockindex);
Curl_conn_cf_add(data, conn, sockindex, cf);
out:
return result;
}
#endif /* !CURL_DISABLE_WEBSOCKETS */

40
lib/cf-recvbuf.h Normal file
View file

@ -0,0 +1,40 @@
#ifndef HEADER_CURL_CF_RECVBUF_H
#define HEADER_CURL_CF_RECVBUF_H
/***************************************************************************
* _ _ ____ _
* Project ___| | | | _ \| |
* / __| | | | |_) | |
* | (__| |_| | _ <| |___
* \___|\___/|_| \_\_____|
*
* Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
*
* This software is licensed as described in the file COPYING, which
* you should have received as part of this distribution. The terms
* are also available at https://curl.se/docs/copyright.html.
*
* You may opt to use, copy, modify, merge, publish, distribute and/or sell
* copies of the Software, and permit persons to whom the Software is
* furnished to do so, under the terms of the COPYING file.
*
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
* KIND, either express or implied.
*
* SPDX-License-Identifier: curl
*
***************************************************************************/
#include "curl_setup.h"
#ifndef CURL_DISABLE_WEBSOCKETS
/* only used for this protocol, so far */
CURLcode Curl_cf_recvbuf_add(struct Curl_easy *data,
struct connectdata *conn,
int sockindex,
const uint8_t *buf, size_t blen);
extern struct Curl_cftype Curl_cft_recvbuf;
#endif /* !CURL_DISABLE_WEBSOCKETS */
#endif /* HEADER_CURL_CF_RECVBUF_H */

View file

@ -33,6 +33,27 @@
#include "select.h"
#include "curlx/strparse.h"
CURLcode Curl_cf_def_connect(struct Curl_cfilter *cf,
struct Curl_easy *data, bool *done)
{
CURLcode result;
if(cf->connected) {
*done = TRUE;
return CURLE_OK;
}
if(cf->next) {
result = cf->next->cft->do_connect(cf->next, data, done);
if(result || !*done)
return result;
}
cf->connected = TRUE;
*done = TRUE;
return CURLE_OK;
}
CURLcode Curl_cf_def_shutdown(struct Curl_cfilter *cf,
struct Curl_easy *data, bool *done)
{

View file

@ -265,6 +265,8 @@ CURLcode Curl_cf_def_query(struct Curl_cfilter *cf,
int query, int *pres1, void *pres2);
CURLcode Curl_cf_def_shutdown(struct Curl_cfilter *cf,
struct Curl_easy *data, bool *done);
CURLcode Curl_cf_def_connect(struct Curl_cfilter *cf,
struct Curl_easy *data, bool *done);
/**
* Create a new filter instance, unattached to the filter chain.

View file

@ -29,6 +29,7 @@
#include "multiif.h"
#include "cf-dns.h"
#include "cf-recvbuf.h"
#include "cf-socket.h"
#include "cf-setup.h"
#include "http2.h"
@ -563,6 +564,9 @@ static struct trc_cft_def trc_cfts[] = {
{ &Curl_cft_unix, TRC_CT_NETWORK },
{ &Curl_cft_tcp_accept, TRC_CT_NETWORK },
{ &Curl_cft_ip_happy, TRC_CT_NETWORK },
#ifndef CURL_DISABLE_WEBSOCKETS
{ &Curl_cft_recvbuf, TRC_CT_PROTOCOL },
#endif
{ &Curl_cft_setup, TRC_CT_PROTOCOL },
#if !defined(CURL_DISABLE_HTTP) && defined(USE_NGHTTP2)
{ &Curl_cft_nghttp2, TRC_CT_PROTOCOL },

View file

@ -32,6 +32,7 @@
#include "curlx/dynbuf.h"
#include "rand.h"
#include "curlx/base64.h"
#include "cf-recvbuf.h"
#include "connect.h"
#include "sendf.h"
#include "curl_trc.h"
@ -1392,15 +1393,17 @@ CURLcode Curl_ws_accept(struct Curl_easy *data,
k->header = FALSE; /* we will not get more response headers */
if(data->set.connect_only) {
size_t nwritten;
/* In CONNECT_ONLY setup, the payloads from `mem` need to be received
* when using `curl_ws_recv` later on after this transfer is already
* marked as DONE. */
result = Curl_bufq_write(&ws->recvbuf, (const uint8_t *)mem,
nread, &nwritten);
if(result)
goto out;
DEBUGASSERT(nread == nwritten);
* when using `curl_ws_recv/curl_easy_recv` later on, after this transfer
* is already marked as DONE.
* Since `curl_easy_recv()` is also supposed to work, we need
* to buffer the data at connection level. See #22107 */
if(nread) {
result = Curl_cf_recvbuf_add(data, data->conn, FIRSTSOCKET,
(const uint8_t *)mem, nread);
if(result)
goto out;
}
CURL_REQ_CLEAR_RECV(data); /* read no more content */
}
else { /* !connect_only */