]> git.ipfire.org Git - thirdparty/curl.git/commitdiff
Websocket: fixes for partial frames and buffer updates.
authorStefan Eissing <stefan@eissing.org>
Fri, 11 Nov 2022 14:15:37 +0000 (15:15 +0100)
committerDaniel Stenberg <daniel@haxx.se>
Fri, 11 Nov 2022 23:51:24 +0000 (00:51 +0100)
- buffers updated correctly when handling partial frames
- callbacks no longer invoked for incomplete payload data of 0 length
- curl_ws_recv no longer returns with 0 length partial payload

Closes #9890

lib/ws.c
lib/ws.h

index 0f5f0ef6dd66fff8f3be5ea8cda40378389582d4..708504ba62228da429516d7cfc2f091606b87bfc 100644 (file)
--- a/lib/ws.c
+++ b/lib/ws.c
@@ -172,10 +172,9 @@ CURLcode Curl_ws_accept(struct Curl_easy *data)
 
 /* remove the spent bytes from the beginning of the buffer as that part has
    now been delivered to the application */
-static void ws_decode_clear(struct Curl_easy *data)
+static void ws_decode_shift(struct Curl_easy *data, size_t spent)
 {
   struct websocket *wsp = &data->req.p.http->ws;
-  size_t spent = wsp->usedbuf;
   size_t len = Curl_dyn_len(&wsp->buf);
   size_t keep = len - spent;
   DEBUGASSERT(len >= spent);
@@ -198,42 +197,27 @@ static void ws_decode_clear(struct Curl_easy *data)
 */
 
 static CURLcode ws_decode(struct Curl_easy *data,
-                          unsigned char *wpkt, size_t ilen,
-                          unsigned char **out, size_t *olen,
+                          unsigned char *inbuf, size_t inlen,
+                          size_t *headlen, size_t *olen,
                           curl_off_t *oleft,
-                          bool *more,
                           unsigned int *flags)
 {
   bool fin;
   unsigned char opcode;
   curl_off_t total;
   size_t dataindex = 2;
-  curl_off_t plen; /* size of data in the buffer */
   curl_off_t payloadsize;
-  struct websocket *wsp = &data->req.p.http->ws;
-  unsigned char *p;
-  CURLcode result;
 
-  *olen = 0;
-
-  /* add the incoming bytes, if any */
-  if(wpkt) {
-    result = Curl_dyn_addn(&wsp->buf, wpkt, ilen);
-    if(result)
-      return result;
-  }
+  *olen = *headlen = 0;
 
-  plen = Curl_dyn_len(&wsp->buf);
-  if(plen < 2) {
+  if(inlen < 2) {
     /* the smallest possible frame is two bytes */
-    infof(data, "WS: plen == %u, EAGAIN", (int)plen);
+    infof(data, "WS: plen == %u, EAGAIN", (int)inlen);
     return CURLE_AGAIN;
   }
 
-  p = Curl_dyn_uptr(&wsp->buf);
-
-  fin = p[0] & WSBIT_FIN;
-  opcode = p[0] & WSBIT_OPCODE_MASK;
+  fin = inbuf[0] & WSBIT_FIN;
+  opcode = inbuf[0] & WSBIT_OPCODE_MASK;
   infof(data, "WS:%d received FIN bit %u", __LINE__, (int)fin);
   *flags = 0;
   switch(opcode) {
@@ -264,61 +248,56 @@ static CURLcode ws_decode(struct Curl_easy *data,
     break;
   }
 
-  if(p[1] & WSBIT_MASK) {
+  if(inbuf[1] & WSBIT_MASK) {
     /* A client MUST close a connection if it detects a masked frame. */
     failf(data, "WS: masked input frame");
     return CURLE_RECV_ERROR;
   }
-  payloadsize = p[1];
+  payloadsize = inbuf[1];
   if(payloadsize == 126) {
-    if(plen < 4) {
-      infof(data, "WS:%d plen == %u, EAGAIN", __LINE__, (int)plen);
+    if(inlen < 4) {
+      infof(data, "WS:%d plen == %u, EAGAIN", __LINE__, (int)inlen);
       return CURLE_AGAIN; /* not enough data available */
     }
-    payloadsize = (p[2] << 8) | p[3];
+    payloadsize = (inbuf[2] << 8) | inbuf[3];
     dataindex += 2;
   }
   else if(payloadsize == 127) {
     /* 64 bit payload size */
-    if(plen < 10)
+    if(inlen < 10)
       return CURLE_AGAIN;
-    if(p[2] & 80) {
+    if(inbuf[2] & 80) {
       failf(data, "WS: too large frame");
       return CURLE_RECV_ERROR;
     }
     dataindex += 8;
-    payloadsize = ((curl_off_t)p[2] << 56) |
-      (curl_off_t)p[3] << 48 |
-      (curl_off_t)p[4] << 40 |
-      (curl_off_t)p[5] << 32 |
-      (curl_off_t)p[6] << 24 |
-      (curl_off_t)p[7] << 16 |
-      (curl_off_t)p[8] << 8 |
-      p[9];
+    payloadsize = ((curl_off_t)inbuf[2] << 56) |
+      (curl_off_t)inbuf[3] << 48 |
+      (curl_off_t)inbuf[4] << 40 |
+      (curl_off_t)inbuf[5] << 32 |
+      (curl_off_t)inbuf[6] << 24 |
+      (curl_off_t)inbuf[7] << 16 |
+      (curl_off_t)inbuf[8] << 8 |
+      inbuf[9];
   }
 
   /* point to the payload */
-   *out = &p[dataindex];
-   total = dataindex + payloadsize;
-   if(total > plen) {
-     /* buffer contains partial frame */
-     wsp->usedbuf = plen;      /* when written, whole buffer is used */
-     *olen = plen - dataindex; /* bytes to write out */
-     *oleft = total - plen;    /* bytes yet to come (for this frame) */
-     payloadsize = total - dataindex;
-   }
-   else {
-     /* we have the complete frame (`total` bytes) in buffer */
-     wsp->usedbuf = total;   /* when written, total frame has been used */
-     *olen = payloadsize;    /* bytes to write out */
-     *oleft = 0;             /* bytes yet to come (for this frame) */
-     if(plen > total)
-       /* there is another fragment after */
-       *more = TRUE;
-   }
-
-   infof(data, "WS: received %zu bytes payload (%zu left, buflen was %zu)",
-         payloadsize, *oleft, plen);
+  *headlen = dataindex;
+  total = dataindex + payloadsize;
+  if(total > (curl_off_t)inlen) {
+    /* buffer contains partial frame */
+    *olen = inlen - dataindex; /* bytes to write out */
+    *oleft = total - inlen;    /* bytes yet to come (for this frame) */
+    payloadsize = total - dataindex;
+  }
+  else {
+    /* we have the complete frame (`total` bytes) in buffer */
+    *olen = payloadsize;    /* bytes to write out */
+    *oleft = 0;             /* bytes yet to come (for this frame) */
+  }
+
+  infof(data, "WS: received %zu bytes payload (%zu left, buflen was %zu)",
+        payloadsize, *oleft, inlen);
   return CURLE_OK;
 }
 
@@ -331,76 +310,83 @@ size_t Curl_ws_writecb(char *buffer, size_t size /* 1 */,
 {
   struct HTTP *ws = (struct HTTP *)userp;
   struct Curl_easy *data = ws->ws.data;
+  struct websocket *wsp = &data->req.p.http->ws;
   void *writebody_ptr = data->set.out;
   if(data->set.ws_raw_mode)
     return data->set.fwrite_func(buffer, size, nitems, writebody_ptr);
   else if(nitems) {
-    unsigned char *frame = NULL;
-    size_t flen = 0;
-    size_t wrote = 0;
+    size_t wrote = 0, headlen;
     CURLcode result;
-    bool more; /* there's is more to parse in the buffer */
-    curl_off_t oleft;
-
-    decode:
-    more = FALSE;
-    oleft = ws->ws.frame.bytesleft;
-    if(!oleft) {
-      unsigned int recvflags;
-      result = ws_decode(data, (unsigned char *)buffer, nitems,
-                         &frame, &flen, &oleft, &more, &recvflags);
-      if(result == CURLE_AGAIN)
-        /* insufficient amount of data, keep it for later */
-        return nitems;
-      else if(result) {
-        infof(data, "WS: decode error %d", (int)result);
+
+    if(buffer) {
+      result = Curl_dyn_addn(&wsp->buf, buffer, nitems);
+      if(result) {
+        infof(data, "WS: error adding data to buffer %d", (int)result);
         return nitems - 1;
       }
-      /* Store details about the frame to be reachable with curl_ws_meta()
-         from within the write callback */
-      ws->ws.frame.age = 0;
-      ws->ws.frame.offset = 0;
-      ws->ws.frame.flags = recvflags;
-      ws->ws.frame.bytesleft = oleft;
+      buffer = NULL;
     }
-    else {
-      if(nitems > (size_t)ws->ws.frame.bytesleft) {
-        nitems = ws->ws.frame.bytesleft;
-        more = TRUE;
+
+    while(Curl_dyn_len(&wsp->buf)) {
+      unsigned char *wsbuf = Curl_dyn_uptr(&wsp->buf);
+      size_t buflen = Curl_dyn_len(&wsp->buf);
+      size_t write_len = 0;
+      size_t consumed = 0;
+
+      if(!ws->ws.frame.bytesleft) {
+        unsigned int recvflags;
+        curl_off_t fb_left;
+
+        result = ws_decode(data, wsbuf, buflen,
+                           &headlen, &write_len, &fb_left, &recvflags);
+        consumed += headlen;
+        wsbuf += headlen;
+        buflen -= headlen;
+        if(result == CURLE_AGAIN)
+          /* insufficient amount of data, keep it for later.
+           * we pretend to have written all since we have a copy */
+          return nitems;
+        else if(result) {
+          infof(data, "WS: decode error %d", (int)result);
+          return nitems - 1;
+        }
+        /* New frame. store details about the frame to be reachable with
+           curl_ws_meta() from within the write callback */
+        ws->ws.frame.age = 0;
+        ws->ws.frame.offset = 0;
+        ws->ws.frame.flags = recvflags;
+        ws->ws.frame.bytesleft = fb_left;
       }
-      else
-        more = FALSE;
-      ws->ws.frame.offset += nitems;
-      ws->ws.frame.bytesleft -= nitems;
-      frame = (unsigned char *)buffer;
-      flen = nitems;
-    }
-    if((ws->ws.frame.flags & CURLWS_PING) && !oleft) {
-      /* auto-respond to PINGs, only works for single-frame payloads atm */
-      size_t bytes;
-      infof(data, "WS: auto-respond to PING with a PONG");
-      DEBUGASSERT(frame);
-      /* send back the exact same content as a PONG */
-      result = curl_ws_send(data, frame, flen, &bytes, 0, CURLWS_PONG);
-      if(result)
-        return result;
-    }
-    else {
-      /* deliver the decoded frame to the user callback */
-      Curl_set_in_callback(data, true);
-      wrote = data->set.fwrite_func((char *)frame, 1, flen, writebody_ptr);
-      Curl_set_in_callback(data, false);
-      if(wrote != flen)
-        return 0;
-    }
-    if(oleft)
-      ws->ws.frame.offset += flen;
-    /* the websocket frame has been delivered */
-    ws_decode_clear(data);
-    if(more) {
-      /* there's more websocket data to deal with in the buffer */
-      buffer = NULL; /* the buffer as been drained already */
-      goto decode;
+      else {
+        /* continuing frame */
+        write_len = (size_t)ws->ws.frame.bytesleft;
+        if(write_len > buflen)
+          write_len = buflen;
+        ws->ws.frame.offset += write_len;
+        ws->ws.frame.bytesleft -= write_len;
+      }
+      if((ws->ws.frame.flags & CURLWS_PING) && !ws->ws.frame.bytesleft) {
+        /* auto-respond to PINGs, only works for single-frame payloads atm */
+        size_t bytes;
+        infof(data, "WS: auto-respond to PING with a PONG");
+        /* send back the exact same content as a PONG */
+        result = curl_ws_send(data, wsbuf, write_len,
+                              &bytes, 0, CURLWS_PONG);
+        if(result)
+          return result;
+      }
+      else if(write_len || !wsp->frame.bytesleft) {
+        /* deliver the decoded frame to the user callback */
+        Curl_set_in_callback(data, true);
+        wrote = data->set.fwrite_func((char *)wsbuf, 1,
+                                      write_len, writebody_ptr);
+        Curl_set_in_callback(data, false);
+        if(wrote != write_len)
+          return 0;
+      }
+      /* get rid of the buffered data consumed */
+      consumed += write_len;
+      ws_decode_shift(data, consumed);
     }
   }
   return nitems;
@@ -411,9 +397,9 @@ CURL_EXTERN CURLcode curl_ws_recv(struct Curl_easy *data, void *buffer,
                                   size_t buflen, size_t *nread,
                                   struct curl_ws_frame **metap)
 {
-  size_t bytes;
   CURLcode result;
   struct websocket *wsp = &data->req.p.http->ws;
+  bool done = FALSE; /* not filled passed buffer yet */
 
   *nread = 0;
   *metap = NULL;
@@ -422,84 +408,82 @@ CURL_EXTERN CURLcode curl_ws_recv(struct Curl_easy *data, void *buffer,
   if(result)
     return result;
 
-  do {
-    bool drain = FALSE; /* if there is pending buffered data to drain */
-    char *inbuf = data->state.buffer;
-    bytes = wsp->stillbuffer;
-    if(!bytes) {
+  while(!done) {
+    size_t write_len;
+    unsigned int recvflags;
+
+    if(!wsp->stillblen) {
+      /* try to get more data */
+      size_t n;
       result = curl_easy_recv(data, data->state.buffer,
-                              data->set.buffer_size, &bytes);
+                              data->set.buffer_size, &n);
       if(result)
         return result;
+      if(!n)
+        /* still have nothing */
+        goto out;
+      wsp->stillb = data->state.buffer;
+      wsp->stillblen = n;
+    }
+
+    infof(data, "WS: got %u websocket bytes to decode",
+          (int)wsp->stillblen);
+    if(!wsp->frame.bytesleft) {
+      size_t headlen;
+      curl_off_t oleft;
+      /* detect new frame */
+      result = ws_decode(data, (unsigned char *)wsp->stillb, wsp->stillblen,
+                         &headlen, &write_len, &oleft, &recvflags);
+      if(result == CURLE_AGAIN)
+        /* a packet fragment only */
+        break;
+      else if(result)
+        return result;
+      wsp->stillb += headlen;
+      wsp->stillblen -= headlen;
+      wsp->frame.offset = 0;
+      wsp->frame.bytesleft = oleft;
+      wsp->frame.flags = recvflags;
     }
     else {
-      /* the pending bytes can be found here */
-      inbuf = wsp->stillb;
-      drain = TRUE;
+      /* existing frame, remaining payload handling */
+      write_len = wsp->frame.bytesleft;
+      if(write_len > wsp->stillblen)
+        write_len = wsp->stillblen;
     }
-    if(bytes) {
-      unsigned char *out;
-      size_t olen;
-      bool more;
-      unsigned int recvflags;
-      curl_off_t oleft = wsp->frame.bytesleft;
-
-      infof(data, "WS: got %u websocket bytes to decode", (int)bytes);
-      if(!oleft && !drain) {
-        result = ws_decode(data, (unsigned char *)inbuf, bytes,
-                           &out, &olen, &oleft, &more, &recvflags);
-        if(result == CURLE_AGAIN)
-          /* a packet fragment only */
-          break;
-        else if(result)
-          return result;
-        wsp->frame.offset = 0;
-        wsp->frame.bytesleft = oleft;
-        wsp->frame.flags = recvflags;
-      }
-      else {
-        olen = oleft;
-        out = (unsigned char *)wsp->stillb;
-        recvflags = wsp->frame.flags;
-        if((curl_off_t)buflen < oleft)
-          /* there is still data left after this */
-          wsp->frame.bytesleft -= buflen;
-        else
-          wsp->frame.bytesleft = 0;
-      }
 
-      /* auto-respond to PINGs */
-      if((recvflags & CURLWS_PING) && !oleft) {
-        infof(data, "WS: auto-respond to PING with a PONG");
-        /* send back the exact same content as a PONG */
-        result = curl_ws_send(data, out, olen, &bytes, 0, CURLWS_PONG);
-        if(result)
-          return result;
-      }
-      else {
-        if(olen < buflen) {
-          /* copy the payload to the user buffer */
-          memcpy(buffer, out, olen);
-          *nread = olen;
-          if(!oleft)
-            /*  websocket frame has been delivered */
-            ws_decode_clear(data);
-        }
-        else {
-          /* copy a partial payload */
-          memcpy(buffer, out, buflen);
-          *nread = buflen;
-          /* remember what is left and where */
-          wsp->stillbuffer = olen - buflen;
-          wsp->stillb = (char *)buffer + buflen;
-        }
-        wsp->frame.offset += *nread;
-      }
+    /* auto-respond to PINGs */
+    if((wsp->frame.flags & CURLWS_PING) && !wsp->frame.bytesleft) {
+      infof(data, "WS: auto-respond to PING with a PONG");
+      /* send back the exact same content as a PONG */
+      result = curl_ws_send(data, wsp->stillb, write_len,
+                            &write_len, 0, CURLWS_PONG);
+      if(result)
+        return result;
     }
-    else
-      *nread = bytes;
-    break;
-  } while(1);
+    else if(write_len || !wsp->frame.bytesleft) {
+      if(write_len > buflen)
+        write_len = buflen;
+      /* copy the payload to the user buffer */
+      memcpy(buffer, wsp->stillb, write_len);
+      *nread = write_len;
+      done = TRUE;
+    }
+    if(write_len) {
+      /* update buffer and frame info */
+      wsp->frame.offset += write_len;
+      DEBUGASSERT(wsp->frame.bytesleft >= (curl_off_t)write_len);
+      if(wsp->frame.bytesleft)
+        wsp->frame.bytesleft -= write_len;
+      DEBUGASSERT(write_len <= wsp->stillblen);
+      wsp->stillblen -= write_len;
+      if(wsp->stillblen)
+        wsp->stillb += write_len;
+      else
+        wsp->stillb = NULL;
+    }
+  }
+out:
   *metap = &wsp->frame;
   return CURLE_OK;
 }
index 341242e50eadf61e028c02e348ecfc6d8a327ba0..2f3ed2d1e6879a8d792c73d38553df7322eab626 100644 (file)
--- a/lib/ws.h
+++ b/lib/ws.h
@@ -48,9 +48,9 @@ struct websocket {
   struct curl_ws_frame frame; /* the struct used for frame state */
   curl_off_t oleft; /* outstanding number of payload bytes left from the
                        server */
-  curl_off_t stillbuffer; /* number of bytes left in the buffer to deliver in
-                             the next curl_ws_recv() call */
-  char *stillb; /* the stillbuffer pending bytes are here */
+  size_t stillblen; /* number of bytes left in the buffer to deliver in
+                         the next curl_ws_recv() call */
+  char *stillb; /* the stillblen pending bytes are here */
   curl_off_t sleft; /* outstanding number of payload bytes left to send */
   unsigned int xori; /* xor index */
 };