]> git.ipfire.org Git - thirdparty/curl.git/commitdiff
websockets: buffer ugprade data at connection level
authorStefan Eissing <stefan@eissing.org>
Fri, 19 Jun 2026 15:01:00 +0000 (17:01 +0200)
committerDaniel Stenberg <daniel@haxx.se>
Mon, 22 Jun 2026 06:54:50 +0000 (08:54 +0200)
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

lib/Makefile.inc
lib/cf-recvbuf.c [new file with mode: 0644]
lib/cf-recvbuf.h [new file with mode: 0644]
lib/cfilters.c
lib/cfilters.h
lib/curl_trc.c
lib/ws.c

index 061a317b8d651f73857429e982db02f63d3832e0..266ba52af3775a1b04934886aae0a3050aedb02b 100644 (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         \
diff --git a/lib/cf-recvbuf.c b/lib/cf-recvbuf.c
new file mode 100644 (file)
index 0000000..8ccc148
--- /dev/null
@@ -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 */
diff --git a/lib/cf-recvbuf.h b/lib/cf-recvbuf.h
new file mode 100644 (file)
index 0000000..1b08fcc
--- /dev/null
@@ -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 */
index b6278eff12c9ca77960308e494ed94966c0e2364..fb1914d133e84f5b566b6496aa14de78ec42ec22 100644 (file)
 #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)
 {
index a9e00d489904733a2a082ad247ded3d22e1b04f2..b1aad029e070a4ea6d4121610129addba8895b03 100644 (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.
index 127bc8c5ccd7daa54b16d076ec5d8c2476befa9b..f8287e420fd594d7c66e7cccb57548c06d6b8fd8 100644 (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 },
index 3f3a07466d42db14a793be29261be7221b824f94..9820c3e4bd269e8b224e43754461a42ff3264d50 100644 (file)
--- a/lib/ws.c
+++ b/lib/ws.c
@@ -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 */