]> git.ipfire.org Git - thirdparty/curl.git/commitdiff
websocket: fix message send corruption
authorStefan Eissing <stefan@eissing.org>
Thu, 2 Jan 2025 15:34:52 +0000 (16:34 +0100)
committerJay Satiro <raysatiro@yahoo.com>
Thu, 16 Jan 2025 21:19:07 +0000 (16:19 -0500)
- 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
lib/bufq.c
lib/bufq.h
lib/ws.c
lib/ws.h
tests/http/clients/ws-data.c
tests/http/test_20_websockets.py

index 73217ca2090c8d0ed7c38be8877611536f22b01b..0b6d66bba972fcc7be571269954761449c490cdc 100644 (file)
@@ -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
index 547d4d376293391f03654ee2d0b87e51f430af9f..724d62f31cdde36ae99d8d24e68ee7cd5df2ae0d 100644 (file)
@@ -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);
index ec415648fd4d2a5c53263ff1477ae1ee4a5a4707..60059deb30f27ee08f8e24227d49f59813282553 100644 (file)
@@ -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.
  */
index 0c5479b964b0b2a57898c20e5b6456a714b23e28..bae0ad6c49f060ca269ce4e3b5d3ba4f74a8ba95 100644 (file)
--- 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:
index e43852835992ede2a9262fe95d04256078862cc9..c96bccab9f4d99e63f4374eb323ee64117555230 100644 (file)
--- 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);
index 4a3be67bf2ee097a76b798281bfb00a06bba8f81..9632ea29897a518ec85f37659a953c3ecb59059c 100644 (file)
  * </DESC>
  */
 /* curl stuff */
-#include "curl_setup.h"
 #include <curl/curl.h>
 
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
 
-#ifndef CURL_DISABLE_WEBSOCKETS
+#if !defined(CURL_DISABLE_WEBSOCKETS) && !defined(_MSC_VER)
+
+#ifndef _MSC_VER
+/* somewhat Unix-specific */
+#include <unistd.h>  /* getopt() */
+#endif
 
 #ifdef _WIN32
 #ifndef WIN32_LEAN_AND_MEAN
@@ -44,6 +48,7 @@
 #include <sys/time.h>
 #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 <cextdecs.h(PROCESS_DELAY_)>
 #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;
 
index eb9df306b31c0405d673d780d7d97a956a703062..4d7075422f05fe5a59c4e0583e544b1476be50e8 100644 (file)
@@ -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)