Browse Source

ws: initial websockets support

Closes #8995
Daniel Stenberg 1 year ago
parent
commit
664249d095

+ 12 - 0
CMakeLists.txt

@@ -1176,6 +1176,16 @@ endif()
 
 set(CMAKE_REQUIRED_FLAGS)
 
+option(ENABLE_WEBSOCKETS "Set to ON to enable EXPERIMENTAL websockets" OFF)
+
+if(ENABLE_WEBSOCKETS)
+  if(${SIZEOF_CURL_OFF_T} GREATER "4")
+    set(USE_WEBSOCKETS ON)
+  else()
+    message(WARNING "curl_off_t is too small to enable WebSockets")
+  endif()
+endif()
+
 foreach(CURL_TEST
     HAVE_GLIBC_STRERROR_R
     HAVE_POSIX_STRERROR_R
@@ -1486,6 +1496,8 @@ _add_if("SFTP"          USE_LIBSSH2 OR USE_LIBSSH)
 _add_if("RTSP"          NOT CURL_DISABLE_RTSP)
 _add_if("RTMP"          USE_LIBRTMP)
 _add_if("MQTT"          NOT CURL_DISABLE_MQTT)
+_add_if("WS"            USE_WEBSOCKETS)
+_add_if("WSS"           USE_WEBSOCKETS)
 if(_items)
   list(SORT _items)
 endif()

+ 14 - 7
configure.ac

@@ -4192,14 +4192,21 @@ AS_HELP_STRING([--disable-websockets],[Disable WebSockets support]),
   no)
      AC_MSG_RESULT(no)
      ;;
-  *) AC_MSG_RESULT(yes)
-     curl_ws_msg="enabled"
-     AC_DEFINE_UNQUOTED(USE_WEBSOCKETS, [1], [enable websockets support])
-     SUPPORT_PROTOCOLS="$SUPPORT_PROTOCOLS WS"
-     if test "x$SSL_ENABLED" = "x1"; then
-       SUPPORT_PROTOCOLS="$SUPPORT_PROTOCOLS WSS"
+  *)
+     if test ${ac_cv_sizeof_curl_off_t} -gt 4; then
+         AC_MSG_RESULT(yes)
+         curl_ws_msg="enabled"
+         AC_DEFINE_UNQUOTED(USE_WEBSOCKETS, [1], [enable websockets support])
+         SUPPORT_PROTOCOLS="$SUPPORT_PROTOCOLS WS"
+         if test "x$SSL_ENABLED" = "x1"; then
+           SUPPORT_PROTOCOLS="$SUPPORT_PROTOCOLS WSS"
+         fi
+         experimental="$experimental Websockets"
+     else
+         dnl websockets requires >32 bit curl_off_t
+         AC_MSG_RESULT(no)
+         AC_MSG_WARN([Websockets disabled due to lack of >32 bit curl_off_t])
      fi
-     experimental="$experimental Websockets"
      ;;
   esac ],
      AC_MSG_RESULT(no)

+ 9 - 7
docs/WebSockets.md

@@ -12,18 +12,20 @@ The Websockets API is described in the individual man pages for the new API.
 
 Websockets with libcurl can be done two ways.
 
-1. Get the websockets frames from the server sent to a WS write callback. You
-   can then respond with `curl_ws_send()` from within the callback or outside
-   of it.
+1. Get the websockets frames from the server sent to the write callback. You
+   can then respond with `curl_ws_send()` from within the callback (or outside
+   of it).
 
 2. Set `CURLOPT_CONNECT_ONLY` to 2L (new for websockets), which makes libcurl
-   do the `Upgrade:` dance in the `curl_easy_perform()` call and then you can
-   use `curl_ws_recv()` and `curl_ws_send()` to receive and send websocket
-   frames from and to the server.
+   do a HTTP GET + `Upgrade:` request plus response in the
+   `curl_easy_perform()` call before it returns and then you can use
+   `curl_ws_recv()` and `curl_ws_send()` to receive and send websocket frames
+   from and to the server.
 
 The new options to `curl_easy_setopt()`:
 
- `CURLOPT_WS_OPTIONS` - to control specific behavior (no bits implemented yet)
+ `CURLOPT_WS_OPTIONS` - to control specific behavior. `CURLWS_RAW_MODE` makes
+ libcurl provide all websocket traffic raw in the callback.
 
 The new function calls:
 

+ 3 - 0
docs/libcurl/curl_easy_setopt.3

@@ -675,6 +675,9 @@ Custom pointer to pass to ssh key callback. See \fICURLOPT_SSH_KEYDATA(3)\fP
 Callback for checking host key handling. See \fICURLOPT_SSH_HOSTKEYFUNCTION(3)\fP
 .IP CURLOPT_SSH_HOSTKEYDATA
 Custom pointer to pass to ssh host key callback. See \fICURLOPT_SSH_HOSTKEYDATA(3)\fP
+.SH WEBSOCKETS
+.IP CURLOPT_WS_OPTIONS
+Set Websockets options. See \fICURLOPT_WS_OPTIONS(3)\fP
 .SH OTHER OPTIONS
 .IP CURLOPT_PRIVATE
 Private pointer to store. See \fICURLOPT_PRIVATE(3)\fP

+ 66 - 0
docs/libcurl/curl_ws_recv.3

@@ -0,0 +1,66 @@
+.\" **************************************************************************
+.\" *                                  _   _ ____  _
+.\" *  Project                     ___| | | |  _ \| |
+.\" *                             / __| | | | |_) | |
+.\" *                            | (__| |_| |  _ <| |___
+.\" *                             \___|\___/|_| \_\_____|
+.\" *
+.\" * Copyright (C) 1998 - 2022, 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
+.\" *
+.\" **************************************************************************
+.\"
+.TH curl_ws_recv 3 "12 Jun 2022" "libcurl 7.85.0" "libcurl Manual"
+.SH NAME
+curl_ws_recv - receive websocket data
+.SH SYNOPSIS
+.nf
+#include <curl/easy.h>
+
+CURLcode curl_ws_recv(CURL *curl, void *buffer, size_t buflen,
+                      size_t *nread, unsigned int *recvflags);
+.fi
+.SH DESCRIPTION
+This function call is EXPERIMENTAL.
+
+Retrives as much as possible of a received WebSockets data fragment into the
+\fBbuffer\fP, but not more than \fBbuflen\fP bytes. The provide
+\fIrecvflags\fP argument gets bits set to help characterize the fragment.
+.IP RECVFLAGS
+.IP CURLWS_TEXT
+The buffer contains text data. Note that this makes a difference to WebSockets
+but libcurl itself will not make any verification of the content or
+precautions that you actually receive valid UTF-8 content.
+.IP CURLWS_BINARY
+This is binary data.
+.IP CURLWS_FINAL
+This is the final fragment of the message, if this is not set, it implies that
+there will be another fragment coming as part of the same message.
+.IP CURLWS_CLOSE
+This transfer is now closed.
+.IP CURLWS_PING
+This as an incoming ping message, that expects a pong response.
+.SH EXAMPLE
+.nf
+
+.fi
+.SH AVAILABILITY
+Added in 7.85.0.
+.SH RETURN VALUE
+
+.SH "SEE ALSO"
+.BR curl_easy_setopt "(3), " curl_easy_perform "(3), "
+.BR curl_easy_getinfo "(3), "
+.BR curl_ws_send "(3) "

+ 73 - 0
docs/libcurl/curl_ws_send.3

@@ -0,0 +1,73 @@
+.\" **************************************************************************
+.\" *                                  _   _ ____  _
+.\" *  Project                     ___| | | |  _ \| |
+.\" *                             / __| | | | |_) | |
+.\" *                            | (__| |_| |  _ <| |___
+.\" *                             \___|\___/|_| \_\_____|
+.\" *
+.\" * Copyright (C) 1998 - 2022, 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
+.\" *
+.\" **************************************************************************
+.\"
+.TH curl_ws_send 3 "12 Jun 2022" "libcurl 7.85.0" "libcurl Manual"
+.SH NAME
+curl_ws_send - receive websocket data
+.SH SYNOPSIS
+.nf
+#include <curl/easy.h>
+
+CURLcode curl_ws_send(CURL *curl, char *buffer, size_t buflen, size_t *sent,
+                      unsigned int sendflags);
+.fi
+.SH DESCRIPTION
+This function call is EXPERIMENTAL.
+
+Send the specific message fragment over the established websockets connection.
+
+If \fBCURLWS_RAW_MODE\fP is enabled in \fICURLOPT_WS_OPTIONS(3)\fP, the
+\fBsendflags\fP argument should be set to 0.
+
+.SH SENDFLAGS
+.IP CURLWS_TEXT
+The buffer contains text data. Note that this makes a difference to WebSockets
+but libcurl itself will not make any verification of the content or
+precautions that you actually send valid UTF-8 content.
+.IP CURLWS_BINARY
+This is binary data.
+.IP CURLWS_NOCOMPRESS
+No-op if there’s no compression anyway.
+.IP CURLWS_CONT
+This is not the final fragment of the message, which implies that there will
+be another fragment coming as part of the same message where this bit is not
+set.
+.IP CURLWS_CLOSE
+Close this transfer.
+.IP CURLWS_PING
+This as a ping.
+.IP CURLWS_PONG
+This as a pong.
+.SH EXAMPLE
+.nf
+
+.fi
+.SH AVAILABILITY
+Added in 7.85.0.
+.SH RETURN VALUE
+
+.SH "SEE ALSO"
+.BR curl_easy_setopt "(3), " curl_easy_perform "(3), "
+.BR curl_easy_getinfo "(3), "
+.BR curl_ws_recv "(3) "

+ 4 - 0
docs/libcurl/opts/CURLOPT_CONNECT_ONLY.3

@@ -42,6 +42,10 @@ useful when used with the \fICURLINFO_ACTIVESOCKET(3)\fP option to
 the application can obtain the most recently used socket for special data
 transfers.
 
+Since 7.85.0, this option can be set to '2' and if HTTP or WebSockets are
+used, libcurl will do the request and read all response headers before handing
+over control to the application.
+
 Transfers marked connect only will not reuse any existing connections and
 connections marked connect only will not be allowed to get reused.
 

+ 73 - 0
docs/libcurl/opts/CURLOPT_WS_OPTIONS.3

@@ -0,0 +1,73 @@
+.\" **************************************************************************
+.\" *                                  _   _ ____  _
+.\" *  Project                     ___| | | |  _ \| |
+.\" *                             / __| | | | |_) | |
+.\" *                            | (__| |_| |  _ <| |___
+.\" *                             \___|\___/|_| \_\_____|
+.\" *
+.\" * Copyright (C) 1998 - 2022, 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
+.\" *
+.\" **************************************************************************
+.\"
+.TH CURLOPT_WS_OPTIONS 3 "10 Jun 2022" "libcurl 7.85.0" "curl_easy_setopt options"
+.SH NAME
+CURLOPT_WS_OPTIONS \- WebSockets behavior options
+.SH SYNOPSIS
+.nf
+#include <curl/curl.h>
+
+CURLcode curl_easy_setopt(CURL *handle, CURLOPT_WS_OPTIONS, long bitmask);
+.fi
+.SH DESCRIPTION
+Pass a long with a bitmask to tell libcurl about specific WebSockets
+behaviors.
+
+To "detatch" a websockets connection and use the \fIcurl_ws_send(3)\fP and
+\fIcurl_ws_recv(3)\fP functions after the HTTP upgrade procedure, set the
+\fICURLOPT_CONNECT_ONLY(3)\fP option to 2L.
+
+Available bits in the bitmask
+.IP "CURLWS_RAW_MODE (1)"
+Deliver "raw" websockets traffic to the \fICURLOPT_WRITEFUNCTION(3)\fP
+callback.
+
+In raw mode, libcurl does not handle pings or any other frame for the
+application.
+.IP "CURLWS_COMPRESS_MODE (2)"
+Negotiate compression for this transfer. (NOT IMPLEMENTED YET)
+.IP "CURLWS_PINGOFF_MODE (4)"
+Disable automated ping/pong handling. (NOT IMPLEMENTED YET)
+.SH DEFAULT
+0
+.SH PROTOCOLS
+WebSockets
+.SH EXAMPLE
+.nf
+CURL *curl = curl_easy_init();
+if(curl) {
+  curl_easy_setopt(curl, CURLOPT_URL, "ws://example.com/");
+  /* use the stand alone API */
+  curl_easy_setopt(curl, CURLOPT_WS_OPTIONS, CURLWS_ALONE);
+  ret = curl_easy_perform(curl);
+  curl_easy_cleanup(curl);
+}
+.fi
+.SH AVAILABILITY
+Added in 7.85.0
+.SH RETURN VALUE
+Returns CURLE_OK if the option is supported, and CURLE_UNKNOWN_OPTION if not.
+.SH "SEE ALSO"
+.BR curl_ws_recv "(3), " curl_ws_send "(3), "

+ 1 - 0
docs/libcurl/opts/Makefile.inc

@@ -404,6 +404,7 @@ man_MANS =                                      \
   CURLOPT_WILDCARDMATCH.3                       \
   CURLOPT_WRITEDATA.3                           \
   CURLOPT_WRITEFUNCTION.3                       \
+  CURLOPT_WS_OPTIONS.3                          \
   CURLOPT_XFERINFODATA.3                        \
   CURLOPT_XFERINFOFUNCTION.3                    \
   CURLOPT_XOAUTH2_BEARER.3                      \

+ 1 - 0
docs/libcurl/symbols-in-versions

@@ -874,6 +874,7 @@ CURLOPT_WRITEDATA               7.9.7
 CURLOPT_WRITEFUNCTION           7.1
 CURLOPT_WRITEHEADER             7.1
 CURLOPT_WRITEINFO               7.1
+CURLOPT_WS_OPTIONS              7.85.0
 CURLOPT_XFERINFODATA            7.32.0
 CURLOPT_XFERINFOFUNCTION        7.32.0
 CURLOPT_XOAUTH2_BEARER          7.33.0

+ 1 - 1
include/curl/Makefile.am

@@ -23,7 +23,7 @@
 ###########################################################################
 pkginclude_HEADERS = \
   curl.h curlver.h easy.h mprintf.h stdcheaders.h multi.h \
-  typecheck-gcc.h system.h urlapi.h options.h header.h
+  typecheck-gcc.h system.h urlapi.h options.h header.h websockets.h
 
 pkgincludedir= $(includedir)/curl
 

+ 4 - 0
include/curl/curl.h

@@ -2154,6 +2154,9 @@ typedef enum {
   /* specify which protocols that libcurl is allowed to follow directs to */
   CURLOPT(CURLOPT_REDIR_PROTOCOLS_STR, CURLOPTTYPE_STRINGPOINT, 319),
 
+  /* websockets options */
+  CURLOPT(CURLOPT_WS_OPTIONS, CURLOPTTYPE_LONG, 320),
+
   CURLOPT_LASTENTRY /* the last unused */
 } CURLoption;
 
@@ -3109,6 +3112,7 @@ CURL_EXTERN CURLcode curl_easy_pause(CURL *handle, int bitmask);
 #include "urlapi.h"
 #include "options.h"
 #include "header.h"
+#include "websockets.h"
 
 /* the typechecker doesn't work in C++ (yet) */
 #if defined(__GNUC__) && defined(__GNUC_MINOR__) && \

+ 68 - 0
include/curl/websockets.h

@@ -0,0 +1,68 @@
+#ifndef CURLINC_WEBSOCKETS_H
+#define CURLINC_WEBSOCKETS_H
+/***************************************************************************
+ *                                  _   _ ____  _
+ *  Project                     ___| | | |  _ \| |
+ *                             / __| | | | |_) | |
+ *                            | (__| |_| |  _ <| |___
+ *                             \___|\___/|_| \_\_____|
+ *
+ * Copyright (C) 1998 - 2022, 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
+ *
+ ***************************************************************************/
+
+/* generic in/out flag bits */
+#define CURLWS_TEXT       (1<<0)
+#define CURLWS_BINARY     (1<<1)
+#define CURLWS_CONT       (1<<2)
+#define CURLWS_CLOSE      (1<<3)
+#define CURLWS_PING       (1<<4)
+
+/*
+ * NAME curl_ws_recv()
+ *
+ * DESCRIPTION
+ *
+ * Receives data from the websocket connection. Use after successful
+ * curl_easy_perform() with CURLOPT_CONNECT_ONLY option.
+ */
+CURL_EXTERN CURLcode curl_ws_recv(CURL *curl, void *buffer, size_t buflen,
+                                  size_t *recv, unsigned int *recvflags);
+
+/* sendflags for curl_ws_send() */
+#define CURLWS_NOCOMPRESS (1<<5)
+#define CURLWS_PONG       (1<<6)
+
+/*
+ * NAME curl_easy_send()
+ *
+ * DESCRIPTION
+ *
+ * Sends data over the websocket connection. Use after successful
+ * curl_easy_perform() with CURLOPT_CONNECT_ONLY option.
+ */
+CURL_EXTERN CURLcode curl_ws_send(CURL *curl, const void *buffer,
+                                  size_t buflen, size_t *sent,
+                                  unsigned int sendflags);
+
+typedef ssize_t (*curl_ws_write_callback)(void *userdata, char *data,
+                                          size_t len,
+                                          unsigned int flags);
+
+/* bits for the CURLOPT_WS_OPTIONS bitmask: */
+#define CURLWS_RAW_MODE (1<<0)
+
+#endif /* CURLINC_WEBSOCKETS_H */

+ 4 - 2
lib/Makefile.inc

@@ -216,7 +216,8 @@ LIB_CFILES =         \
   version.c          \
   version_win32.c    \
   warnless.c         \
-  wildcard.c
+  wildcard.c         \
+  ws.c
 
 LIB_HFILES =         \
   altsvc.h           \
@@ -338,7 +339,8 @@ LIB_HFILES =         \
   urldata.h          \
   version_win32.h    \
   warnless.h         \
-  wildcard.h
+  wildcard.h         \
+  ws.h
 
 LIB_RCFILES = libcurl.rc
 

+ 22 - 0
lib/c-hyper.c

@@ -54,6 +54,7 @@
 #include "multiif.h"
 #include "progress.h"
 #include "content_encoding.h"
+#include "ws.h"
 
 /* The last 3 #include files should be in this order */
 #include "curl_printf.h"
@@ -471,6 +472,24 @@ CURLcode Curl_hyper_stream(struct Curl_easy *data,
     if(result)
       break;
 
+    k->deductheadercount =
+      (100 <= http_status && 199 >= http_status)?k->headerbytecount:0;
+#ifdef USE_WEBSOCKETS
+    if(k->upgr101 == UPGR101_WS) {
+      if(http_status == 101) {
+        /* verify the response */
+        result = Curl_ws_accept(data);
+        if(result)
+          return result;
+      }
+      else {
+        failf(data, "Expected 101, got %u", k->httpcode);
+        result = CURLE_HTTP_RETURNED_ERROR;
+        break;
+      }
+    }
+#endif
+
     /* Curl_http_auth_act() checks what authentication methods that are
      * available and decides which one (if any) to use. It will set 'newurl'
      * if an auth method was picked. */
@@ -1123,6 +1142,9 @@ CURLcode Curl_http(struct Curl_easy *data, bool *done)
   if(result)
     goto error;
 
+  if(!result && conn->handler->protocol&(CURLPROTO_WS|CURLPROTO_WSS))
+    result = Curl_ws_request(data, headers);
+
   result = Curl_add_timecondition(data, headers);
   if(result)
     goto error;

+ 1 - 1
lib/conncache.c

@@ -498,7 +498,7 @@ Curl_conncache_extract_oldest(struct Curl_easy *data)
       conn = curr->ptr;
 
       if(!CONN_INUSE(conn) && !conn->bits.close &&
-         !conn->bits.connect_only) {
+         !conn->connect_only) {
         /* Set higher score for the age passed since the connection was used */
         score = Curl_timediff(now, conn->lastused);
 

+ 19 - 9
lib/easy.c

@@ -1170,8 +1170,7 @@ CURLcode curl_easy_pause(struct Curl_easy *data, int action)
 }
 
 
-static CURLcode easy_connection(struct Curl_easy *data,
-                                curl_socket_t *sfd,
+static CURLcode easy_connection(struct Curl_easy *data, curl_socket_t *sfd,
                                 struct connectdata **connp)
 {
   if(!data)
@@ -1230,11 +1229,12 @@ CURLcode curl_easy_recv(struct Curl_easy *data, void *buffer, size_t buflen,
 }
 
 /*
- * Sends data over the connected socket. Use after successful
- * curl_easy_perform() with CURLOPT_CONNECT_ONLY option.
+ * Sends data over the connected socket.
+ *
+ * This is the private internal version of curl_easy_send()
  */
-CURLcode curl_easy_send(struct Curl_easy *data, const void *buffer,
-                        size_t buflen, size_t *n)
+CURLcode Curl_senddata(struct Curl_easy *data, const void *buffer,
+                       size_t buflen, size_t *n)
 {
   curl_socket_t sfd;
   CURLcode result;
@@ -1242,9 +1242,6 @@ CURLcode curl_easy_send(struct Curl_easy *data, const void *buffer,
   struct connectdata *c = NULL;
   SIGPIPE_VARIABLE(pipe_st);
 
-  if(Curl_is_in_callback(data))
-    return CURLE_RECURSIVE_API_CALL;
-
   result = easy_connection(data, &sfd, &c);
   if(result)
     return result;
@@ -1271,6 +1268,19 @@ CURLcode curl_easy_send(struct Curl_easy *data, const void *buffer,
   return result;
 }
 
+/*
+ * Sends data over the connected socket. Use after successful
+ * curl_easy_perform() with CURLOPT_CONNECT_ONLY option.
+ */
+CURLcode curl_easy_send(struct Curl_easy *data, const void *buffer,
+                        size_t buflen, size_t *n)
+{
+  if(Curl_is_in_callback(data))
+    return CURLE_RECURSIVE_API_CALL;
+
+  return Curl_senddata(data, buffer, buflen, n);
+}
+
 /*
  * Wrapper to call functions in Curl_conncache_foreach()
  *

+ 3 - 0
lib/easyif.h

@@ -27,6 +27,9 @@
 /*
  * Prototypes for library-wide functions provided by easy.c
  */
+CURLcode Curl_senddata(struct Curl_easy *data, const void *buffer,
+                       size_t buflen, size_t *n);
+
 #ifdef CURLDEBUG
 CURL_EXTERN CURLcode curl_easy_perform_ev(struct Curl_easy *easy);
 #endif

+ 2 - 1
lib/easyoptions.c

@@ -354,6 +354,7 @@ struct curl_easyoption Curl_easyopts[] = {
   {"WRITEDATA", CURLOPT_WRITEDATA, CURLOT_CBPTR, 0},
   {"WRITEFUNCTION", CURLOPT_WRITEFUNCTION, CURLOT_FUNCTION, 0},
   {"WRITEHEADER", CURLOPT_HEADERDATA, CURLOT_CBPTR, CURLOT_FLAG_ALIAS},
+  {"WS_OPTIONS", CURLOPT_WS_OPTIONS, CURLOT_LONG, 0},
   {"XFERINFODATA", CURLOPT_XFERINFODATA, CURLOT_CBPTR, 0},
   {"XFERINFOFUNCTION", CURLOPT_XFERINFOFUNCTION, CURLOT_FUNCTION, 0},
   {"XOAUTH2_BEARER", CURLOPT_XOAUTH2_BEARER, CURLOT_STRING, 0},
@@ -367,6 +368,6 @@ struct curl_easyoption Curl_easyopts[] = {
  */
 int Curl_easyopts_check(void)
 {
-  return ((CURLOPT_LASTENTRY%10000) != (319 + 1));
+  return ((CURLOPT_LASTENTRY%10000) != (320 + 1));
 }
 #endif

+ 110 - 9
lib/http.c

@@ -84,6 +84,7 @@
 #include "strdup.h"
 #include "altsvc.h"
 #include "hsts.h"
+#include "ws.h"
 #include "c-hyper.h"
 
 /* The last 3 #include files should be in this order */
@@ -114,6 +115,10 @@ static int https_getsock(struct Curl_easy *data,
 #endif
 static CURLcode http_setup_conn(struct Curl_easy *data,
                                 struct connectdata *conn);
+#ifdef USE_WEBSOCKETS
+static CURLcode ws_setup_conn(struct Curl_easy *data,
+                              struct connectdata *conn);
+#endif
 
 /*
  * HTTP handler interface.
@@ -142,6 +147,32 @@ const struct Curl_handler Curl_handler_http = {
   PROTOPT_USERPWDCTRL
 };
 
+#ifdef USE_WEBSOCKETS
+const struct Curl_handler Curl_handler_ws = {
+  "WS",                                 /* scheme */
+  ws_setup_conn,                        /* setup_connection */
+  Curl_http,                            /* do_it */
+  Curl_http_done,                       /* done */
+  ZERO_NULL,                            /* do_more */
+  Curl_http_connect,                    /* connect_it */
+  ZERO_NULL,                            /* connecting */
+  ZERO_NULL,                            /* doing */
+  ZERO_NULL,                            /* proto_getsock */
+  http_getsock_do,                      /* doing_getsock */
+  ZERO_NULL,                            /* domore_getsock */
+  ZERO_NULL,                            /* perform_getsock */
+  ZERO_NULL,                            /* disconnect */
+  ZERO_NULL,                            /* readwrite */
+  ZERO_NULL,                            /* connection_check */
+  ZERO_NULL,                            /* attach connection */
+  PORT_HTTP,                            /* defport */
+  CURLPROTO_WS,                         /* protocol */
+  CURLPROTO_HTTP,                       /* family */
+  PROTOPT_CREDSPERREQUEST |             /* flags */
+  PROTOPT_USERPWDCTRL
+};
+#endif
+
 #ifdef USE_SSL
 /*
  * HTTPS handler interface.
@@ -169,6 +200,33 @@ const struct Curl_handler Curl_handler_https = {
   PROTOPT_SSL | PROTOPT_CREDSPERREQUEST | PROTOPT_ALPN | /* flags */
   PROTOPT_USERPWDCTRL
 };
+
+#ifdef USE_WEBSOCKETS
+const struct Curl_handler Curl_handler_wss = {
+  "WSS",                                /* scheme */
+  ws_setup_conn,                        /* setup_connection */
+  Curl_http,                            /* do_it */
+  Curl_http_done,                       /* done */
+  ZERO_NULL,                            /* do_more */
+  Curl_http_connect,                    /* connect_it */
+  https_connecting,                     /* connecting */
+  ZERO_NULL,                            /* doing */
+  https_getsock,                        /* proto_getsock */
+  http_getsock_do,                      /* doing_getsock */
+  ZERO_NULL,                            /* domore_getsock */
+  ZERO_NULL,                            /* perform_getsock */
+  ZERO_NULL,                            /* disconnect */
+  ZERO_NULL,                            /* readwrite */
+  ZERO_NULL,                            /* connection_check */
+  ZERO_NULL,                            /* attach connection */
+  PORT_HTTPS,                           /* defport */
+  CURLPROTO_WSS,                        /* protocol */
+  CURLPROTO_HTTP,                       /* family */
+  PROTOPT_SSL | PROTOPT_CREDSPERREQUEST | /* flags */
+  PROTOPT_USERPWDCTRL
+};
+#endif
+
 #endif
 
 static CURLcode http_setup_conn(struct Curl_easy *data,
@@ -205,6 +263,16 @@ static CURLcode http_setup_conn(struct Curl_easy *data,
   return CURLE_OK;
 }
 
+#ifdef USE_WEBSOCKETS
+static CURLcode ws_setup_conn(struct Curl_easy *data,
+                              struct connectdata *conn)
+{
+  /* websockets is 1.1 only (for now) */
+  data->state.httpwant = CURL_HTTP_VERSION_1_1;
+  return http_setup_conn(data, conn);
+}
+#endif
+
 #ifndef CURL_DISABLE_PROXY
 /*
  * checkProxyHeaders() checks the linked list of custom proxy headers
@@ -1518,7 +1586,7 @@ CURLcode Curl_http_connect(struct Curl_easy *data, bool *done)
   }
 #endif
 
-  if(conn->given->protocol & CURLPROTO_HTTPS) {
+  if(conn->given->flags & PROTOPT_SSL) {
     /* perform SSL initialization */
     result = https_connecting(data, done);
     if(result)
@@ -1643,6 +1711,7 @@ CURLcode Curl_http_done(struct Curl_easy *data,
   Curl_mime_cleanpart(&http->form);
   Curl_dyn_reset(&data->state.headerb);
   Curl_hyper_done(data);
+  Curl_ws_done(data);
 
   if(status)
     return status;
@@ -2151,9 +2220,9 @@ CURLcode Curl_http_host(struct Curl_easy *data, struct connectdata *conn)
        [brackets] if the host name is a plain IPv6-address. RFC2732-style. */
     const char *host = conn->host.name;
 
-    if(((conn->given->protocol&CURLPROTO_HTTPS) &&
+    if(((conn->given->protocol&(CURLPROTO_HTTPS|CURLPROTO_WSS)) &&
         (conn->remote_port == PORT_HTTPS)) ||
-       ((conn->given->protocol&CURLPROTO_HTTP) &&
+       ((conn->given->protocol&(CURLPROTO_HTTP|CURLPROTO_WS)) &&
         (conn->remote_port == PORT_HTTP)) )
       /* if(HTTPS on port 443) OR (HTTP on port 80) then don't include
          the port number in the host string */
@@ -2702,6 +2771,13 @@ CURLcode Curl_http_bodysend(struct Curl_easy *data, struct connectdata *conn,
                               FIRSTSOCKET);
     if(result)
       failf(data, "Failed sending HTTP request");
+#ifdef USE_WEBSOCKETS
+    else if((conn->handler->protocol & (CURLPROTO_WS|CURLPROTO_WSS)) &&
+            !(data->set.connect_only))
+      /* Set up the transfer for two-way since without CONNECT_ONLY set, this
+         request probably wants to send data too post upgrade */
+      Curl_setup_transfer(data, FIRSTSOCKET, -1, TRUE, FIRSTSOCKET);
+#endif
     else
       /* HTTP GET/HEAD download: */
       Curl_setup_transfer(data, FIRSTSOCKET, -1, TRUE, -1);
@@ -2731,7 +2807,7 @@ CURLcode Curl_http_cookies(struct Curl_easy *data,
       const char *host = data->state.aptr.cookiehost ?
         data->state.aptr.cookiehost : conn->host.name;
       const bool secure_context =
-        conn->handler->protocol&CURLPROTO_HTTPS ||
+        conn->handler->protocol&(CURLPROTO_HTTPS|CURLPROTO_WSS) ||
         strcasecompare("localhost", host) ||
         !strcmp(host, "127.0.0.1") ||
         !strcmp(host, "[::1]") ? TRUE : FALSE;
@@ -3256,6 +3332,8 @@ CURLcode Curl_http(struct Curl_easy *data, bool *done)
   }
 
   result = Curl_http_cookies(data, conn, &req);
+  if(!result && conn->handler->protocol&(CURLPROTO_WS|CURLPROTO_WSS))
+    result = Curl_ws_request(data, &req);
   if(!result)
     result = Curl_add_timecondition(data, &req);
   if(!result)
@@ -3568,7 +3646,7 @@ CURLcode Curl_http_header(struct Curl_easy *data, struct connectdata *conn,
     const char *host = data->state.aptr.cookiehost?
       data->state.aptr.cookiehost:conn->host.name;
     const bool secure_context =
-      conn->handler->protocol&CURLPROTO_HTTPS ||
+      conn->handler->protocol&(CURLPROTO_HTTPS|CURLPROTO_WSS) ||
       strcasecompare("localhost", host) ||
       !strcmp(host, "127.0.0.1") ||
       !strcmp(host, "[::1]") ? TRUE : FALSE;
@@ -3734,7 +3812,7 @@ CURLcode Curl_http_statusline(struct Curl_easy *data,
     connclose(conn, "HTTP/1.0 close after body");
   }
   else if(conn->httpversion == 20 ||
-          (k->upgr101 == UPGR101_REQUESTED && k->httpcode == 101)) {
+          (k->upgr101 == UPGR101_H2 && k->httpcode == 101)) {
     DEBUGF(infof(data, "HTTP/2 found, allow multiplexing"));
     /* HTTP/2 cannot avoid multiplexing since it is a core functionality
        of the protocol */
@@ -3960,9 +4038,9 @@ CURLcode Curl_http_readwrite_headers(struct Curl_easy *data,
           break;
         case 101:
           /* Switching Protocols */
-          if(k->upgr101 == UPGR101_REQUESTED) {
+          if(k->upgr101 == UPGR101_H2) {
             /* Switching to HTTP/2 */
-            infof(data, "Received 101");
+            infof(data, "Received 101, Switching to HTTP/2");
             k->upgr101 = UPGR101_RECEIVED;
 
             /* we'll get more headers (HTTP/2 response) */
@@ -3976,8 +4054,21 @@ CURLcode Curl_http_readwrite_headers(struct Curl_easy *data,
               return result;
             *nread = 0;
           }
+#ifdef USE_WEBSOCKETS
+          else if(k->upgr101 == UPGR101_WS) {
+            /* verify the response */
+            result = Curl_ws_accept(data);
+            if(result)
+              return result;
+            k->header = FALSE; /* no more header to parse! */
+            if(data->set.connect_only) {
+              k->keepon &= ~KEEP_RECV; /* read no more content */
+              *nread = 0;
+            }
+          }
+#endif
           else {
-            /* Switching to another protocol (e.g. WebSocket) */
+            /* Not switching to another protocol */
             k->header = FALSE; /* no more header to parse! */
           }
           break;
@@ -4070,6 +4161,16 @@ CURLcode Curl_http_readwrite_headers(struct Curl_easy *data,
         return CURLE_HTTP_RETURNED_ERROR;
       }
 
+#ifdef USE_WEBSOCKETS
+      /* All non-101 HTTP status codes are bad when wanting to upgrade to
+         websockets */
+      if(data->req.upgr101 == UPGR101_WS) {
+        failf(data, "Refused WebSockets upgrade: %d", k->httpcode);
+        return CURLE_HTTP_RETURNED_ERROR;
+      }
+#endif
+
+
       data->req.deductheadercount =
         (100 <= k->httpcode && 199 >= k->httpcode)?data->req.headerbytecount:0;
 

+ 23 - 0
lib/http.h

@@ -24,6 +24,7 @@
  *
  ***************************************************************************/
 #include "curl_setup.h"
+#include "ws.h"
 
 typedef enum {
   HTTPREQ_GET,
@@ -50,6 +51,15 @@ extern const struct Curl_handler Curl_handler_http;
 extern const struct Curl_handler Curl_handler_https;
 #endif
 
+#ifdef USE_WEBSOCKETS
+extern const struct Curl_handler Curl_handler_ws;
+
+#ifdef USE_SSL
+extern const struct Curl_handler Curl_handler_wss;
+#endif
+#endif /* websockets */
+
+
 /* Header specific functions */
 bool Curl_compareheader(const char *headerline,  /* line to check */
                         const char *header,   /* header keyword _with_ colon */
@@ -192,6 +202,15 @@ struct h3out; /* see ngtcp2 */
 #endif /* _WIN32 */
 #endif /* USE_MSH3 */
 
+struct websockets {
+  bool contfragment; /* set TRUE if the previous fragment sent was not final */
+  unsigned char mask[4]; /* 32 bit mask for this connection */
+  struct Curl_easy *data; /* used for write callback handling */
+  struct dynbuf buf;
+  size_t usedbuf; /* number of leading bytes in 'buf' the most recent complete
+                     websocket frame uses */
+};
+
 /****************************************************************************
  * HTTP unique setup
  ***************************************************************************/
@@ -218,6 +237,10 @@ struct HTTP {
     HTTPSEND_BODY     /* sending body */
   } sending;
 
+#ifdef USE_WEBSOCKETS
+  struct websockets ws;
+#endif
+
 #ifndef CURL_DISABLE_HTTP
   struct dynbuf send_buffer; /* used if the request couldn't be sent in one
                                 chunk, points to an allocated send_buffer

+ 1 - 1
lib/http2.c

@@ -1392,7 +1392,7 @@ CURLcode Curl_http2_request_upgrade(struct dynbuf *req,
                          NGHTTP2_CLEARTEXT_PROTO_VERSION_ID, base64);
   free(base64);
 
-  k->upgr101 = UPGR101_REQUESTED;
+  k->upgr101 = UPGR101_H2;
 
   return result;
 }

+ 2 - 2
lib/multi.c

@@ -753,7 +753,7 @@ static int close_connect_only(struct Curl_easy *data,
   if(data->state.lastconnect_id != conn->connection_id)
     return 0;
 
-  if(!conn->bits.connect_only)
+  if(!conn->connect_only)
     return 1;
 
   connclose(conn, "Removing connect-only easy handle");
@@ -2144,7 +2144,7 @@ static CURLMcode multi_runsingle(struct Curl_multi *multi,
         }
       }
 
-      if(data->set.connect_only) {
+      if(data->set.connect_only == 1) {
         /* keep connection open for application to use the socket */
         connkeep(data->conn, "CONNECT_ONLY");
         multistate(data, MSTATE_DONE);

+ 14 - 2
lib/sendf.c

@@ -48,6 +48,7 @@
 #include "strdup.h"
 #include "http2.h"
 #include "headers.h"
+#include "ws.h"
 
 /* The last 3 #include files should be in this order */
 #include "curl_printf.h"
@@ -534,6 +535,7 @@ static CURLcode chop_write(struct Curl_easy *data,
   curl_write_callback writebody = NULL;
   char *ptr = optr;
   size_t len = olen;
+  void *writebody_ptr = data->set.out;
 
   if(!len)
     return CURLE_OK;
@@ -544,8 +546,18 @@ static CURLcode chop_write(struct Curl_easy *data,
     return pausewrite(data, type, ptr, len);
 
   /* Determine the callback(s) to use. */
-  if(type & CLIENTWRITE_BODY)
+  if(type & CLIENTWRITE_BODY) {
+#ifdef USE_WEBSOCKETS
+    if(conn->handler->protocol & (CURLPROTO_WS|CURLPROTO_WSS)) {
+      struct HTTP *ws = data->req.p.http;
+      writebody = Curl_ws_writecb;
+      ws->ws.data = data;
+      writebody_ptr = ws;
+    }
+    else
+#endif
     writebody = data->set.fwrite_func;
+  }
   if((type & CLIENTWRITE_HEADER) &&
      (data->set.fwrite_header || data->set.writeheader)) {
     /*
@@ -563,7 +575,7 @@ static CURLcode chop_write(struct Curl_easy *data,
     if(writebody) {
       size_t wrote;
       Curl_set_in_callback(data, true);
-      wrote = writebody(ptr, 1, chunklen, data->set.out);
+      wrote = writebody(ptr, 1, chunklen, writebody_ptr);
       Curl_set_in_callback(data, false);
 
       if(CURL_WRITEFUNC_PAUSE == wrote) {

+ 16 - 2
lib/setopt.c

@@ -2430,9 +2430,14 @@ CURLcode Curl_vsetopt(struct Curl_easy *data, CURLoption option, va_list param)
 
   case CURLOPT_CONNECT_ONLY:
     /*
-     * No data transfer, set up connection and let application use the socket
+     * No data transfer.
+     * (1) - only do connection
+     * (2) - do first get request but get no content
      */
-    data->set.connect_only = (0 != va_arg(param, long)) ? TRUE : FALSE;
+    arg = va_arg(param, long);
+    if(arg > 2)
+      return CURLE_BAD_FUNCTION_ARGUMENT;
+    data->set.connect_only = (unsigned char)arg;
     break;
 
   case CURLOPT_SOCKOPTFUNCTION:
@@ -3127,6 +3132,15 @@ CURLcode Curl_vsetopt(struct Curl_easy *data, CURLoption option, va_list param)
   case CURLOPT_PREREQDATA:
     data->set.prereq_userp = va_arg(param, void *);
     break;
+#ifdef USE_WEBSOCKETS
+  case CURLOPT_WS_OPTIONS: {
+    bool raw;
+    arg = va_arg(param, long);
+    raw = (arg & CURLWS_RAW_MODE);
+    data->set.ws_raw_mode = raw;
+    break;
+  }
+#endif
   default:
     /* unknown tag and its companion, just ignore: */
     result = CURLE_UNKNOWN_OPTION;

+ 13 - 3
lib/url.c

@@ -191,6 +191,16 @@ static const struct Curl_handler * const protocols[] = {
   &Curl_handler_http,
 #endif
 
+#ifdef USE_WEBSOCKETS
+#if defined(USE_SSL) && !defined(CURL_DISABLE_HTTP)
+  &Curl_handler_wss,
+#endif
+
+#ifndef CURL_DISABLE_HTTP
+  &Curl_handler_ws,
+#endif
+#endif
+
 #ifndef CURL_DISABLE_FTP
   &Curl_handler_ftp,
 #endif
@@ -867,7 +877,7 @@ void Curl_disconnect(struct Curl_easy *data,
   /* Cleanup NEGOTIATE connection-related data */
   Curl_http_auth_cleanup_negotiate(conn);
 
-  if(conn->bits.connect_only)
+  if(conn->connect_only)
     /* treat the connection as dead in CONNECT_ONLY situations */
     dead_connection = TRUE;
 
@@ -1215,7 +1225,7 @@ ConnectionExists(struct Curl_easy *data,
       check = curr->ptr;
       curr = curr->next;
 
-      if(check->bits.connect_only || check->bits.close)
+      if(check->connect_only || check->bits.close)
         /* connect-only or to-be-closed connections will not be reused */
         continue;
 
@@ -1799,7 +1809,7 @@ static struct connectdata *allocate_conn(struct Curl_easy *data)
   conn->proxy_ssl_config.ssl_options = data->set.proxy_ssl.primary.ssl_options;
 #endif
   conn->ip_version = data->set.ipver;
-  conn->bits.connect_only = data->set.connect_only;
+  conn->connect_only = data->set.connect_only;
   conn->transport = TRNSPRT_TCP; /* most of them are TCP streams */
 
 #if !defined(CURL_DISABLE_HTTP) && defined(USE_NTLM) && \

+ 19 - 5
lib/urldata.h

@@ -53,6 +53,14 @@
 #define PORT_GOPHER 70
 #define PORT_MQTT 1883
 
+#ifdef USE_WEBSOCKETS
+#define CURLPROTO_WS     (1<<30)
+#define CURLPROTO_WSS    (1LL<<31)
+#else
+#define CURLPROTO_WS 0
+#define CURLPROTO_WSS 0
+#endif
+
 #define DICT_MATCH "/MATCH:"
 #define DICT_MATCH2 "/M:"
 #define DICT_MATCH3 "/FIND:"
@@ -66,7 +74,8 @@
 /* Convenience defines for checking protocols or their SSL based version. Each
    protocol handler should only ever have a single CURLPROTO_ in its protocol
    field. */
-#define PROTO_FAMILY_HTTP (CURLPROTO_HTTP|CURLPROTO_HTTPS)
+#define PROTO_FAMILY_HTTP (CURLPROTO_HTTP|CURLPROTO_HTTPS|CURLPROTO_WS| \
+                           CURLPROTO_WSS)
 #define PROTO_FAMILY_FTP  (CURLPROTO_FTP|CURLPROTO_FTPS)
 #define PROTO_FAMILY_POP3 (CURLPROTO_POP3|CURLPROTO_POP3S)
 #define PROTO_FAMILY_SMB  (CURLPROTO_SMB|CURLPROTO_SMBS)
@@ -508,7 +517,6 @@ struct ConnectBits {
   BIT(multiplex); /* connection is multiplexed */
   BIT(tcp_fastopen); /* use TCP Fast Open */
   BIT(tls_enable_alpn); /* TLS ALPN extension? */
-  BIT(connect_only);
 #ifndef CURL_DISABLE_DOH
   BIT(doh);
 #endif
@@ -574,8 +582,9 @@ enum expect100 {
 
 enum upgrade101 {
   UPGR101_INIT,               /* default state */
-  UPGR101_REQUESTED,          /* upgrade requested */
-  UPGR101_RECEIVED,           /* response received */
+  UPGR101_WS,                 /* upgrade to WebSockets requested */
+  UPGR101_H2,                 /* upgrade to HTTP/2 requested */
+  UPGR101_RECEIVED,           /* 101 response received */
   UPGR101_WORKING             /* talking upgraded protocol */
 };
 
@@ -1122,6 +1131,7 @@ struct connectdata {
   unsigned char transport; /* one of the TRNSPRT_* defines */
   unsigned char ip_version; /* copied from the Curl_easy at creation time */
   unsigned char httpversion; /* the HTTP version*10 reported by the server */
+  unsigned char connect_only;
 };
 
 /* The end of connectdata. */
@@ -1816,6 +1826,8 @@ struct UserDefined {
   BIT(mail_rcpt_allowfails); /* allow RCPT TO command to fail for some
                                 recipients */
 #endif
+  unsigned char connect_only; /* make connection/request, then let
+                                 application use the socket */
   BIT(is_fread_set); /* has read callback been set to non-NULL? */
 #ifndef CURL_DISABLE_TFTP
   BIT(tftp_no_options); /* do not send TFTP options requests */
@@ -1861,7 +1873,6 @@ struct UserDefined {
   BIT(no_signal);      /* do not use any signal/alarm handler */
   BIT(tcp_nodelay);    /* whether to enable TCP_NODELAY or not */
   BIT(ignorecl);       /* ignore content length */
-  BIT(connect_only);   /* make connection, let application use the socket */
   BIT(http_te_skip);   /* pass the raw body data to the user, even when
                           transfer-encoded (chunked, compressed) */
   BIT(http_ce_skip);   /* pass the raw body data to the user, even when
@@ -1893,6 +1904,9 @@ struct UserDefined {
   BIT(doh_verifystatus);   /* DoH certificate status verification */
 #endif
   BIT(http09_allowed); /* allow HTTP/0.9 responses */
+#ifdef USE_WEBSOCKETS
+  BIT(ws_raw_mode);
+#endif
 };
 
 struct Names {

+ 610 - 0
lib/ws.c

@@ -0,0 +1,610 @@
+/***************************************************************************
+ *                                  _   _ ____  _
+ *  Project                     ___| | | |  _ \| |
+ *                             / __| | | | |_) | |
+ *                            | (__| |_| |  _ <| |___
+ *                             \___|\___/|_| \_\_____|
+ *
+ * Copyright (C) 1998 - 2022, 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"
+
+#ifdef USE_WEBSOCKETS
+
+#include "urldata.h"
+#include "dynbuf.h"
+#include "rand.h"
+#include "curl_base64.h"
+#include "sendf.h"
+#include "multiif.h"
+#include "ws.h"
+#include "easyif.h"
+#include "transfer.h"
+#include "nonblock.h"
+
+/* The last 3 #include files should be in this order */
+#include "curl_printf.h"
+#include "curl_memory.h"
+#include "memdebug.h"
+
+struct wsfield {
+  const char *name;
+  const char *val;
+};
+
+CURLcode Curl_ws_request(struct Curl_easy *data, REQTYPE *req)
+{
+  unsigned int i;
+  CURLcode result = CURLE_OK;
+  unsigned char rand[16];
+  char *randstr;
+  size_t randlen;
+  char keyval[40];
+  struct SingleRequest *k = &data->req;
+  const struct wsfield heads[]= {
+    {
+      /* The request MUST contain an |Upgrade| header field whose value
+         MUST include the "websocket" keyword. */
+      "Upgrade:", "websocket"
+    },
+    {
+      /* The request MUST contain a |Connection| header field whose value
+         MUST include the "Upgrade" token. */
+      "Connection:", "Upgrade",
+    },
+    {
+      /* The request MUST include a header field with the name
+         |Sec-WebSocket-Version|. The value of this header field MUST be
+         13. */
+      "Sec-WebSocket-Version:", "13",
+    },
+    {
+      /* The request MUST include a header field with the name
+         |Sec-WebSocket-Key|. The value of this header field MUST be a nonce
+         consisting of a randomly selected 16-byte value that has been
+         base64-encoded (see Section 4 of [RFC4648]). The nonce MUST be
+         selected randomly for each connection. */
+      "Sec-WebSocket-Key:", &keyval[0]
+    }
+  };
+
+  /* 16 bytes random */
+  result = Curl_rand(data, (unsigned char *)rand, sizeof(rand));
+  if(result)
+    return result;
+  result = Curl_base64_encode((char *)rand, sizeof(rand), &randstr, &randlen);
+  if(result)
+    return result;
+  DEBUGASSERT(randlen < sizeof(keyval));
+  if(randlen >= sizeof(keyval))
+    return CURLE_FAILED_INIT;
+  strcpy(keyval, randstr);
+  free(randstr);
+  for(i = 0; !result && (i < sizeof(heads)/sizeof(heads[0])); i++) {
+    if(!Curl_checkheaders(data, STRCONST(heads[i].name))) {
+#ifdef USE_HYPER
+      char field[128];
+      msnprintf(field, sizeof(field), "%s %s", heads[i].name,
+                heads[i].val);
+      result = Curl_hyper_header(data, req, field);
+#else
+      (void)data;
+      result = Curl_dyn_addf(req, "%s %s\r\n", heads[i].name,
+                             heads[i].val);
+#endif
+    }
+  }
+  k->upgr101 = UPGR101_WS;
+  Curl_dyn_init(&data->req.p.http->ws.buf, MAX_WS_SIZE * 2);
+  return result;
+}
+
+CURLcode Curl_ws_accept(struct Curl_easy *data)
+{
+  struct SingleRequest *k = &data->req;
+  struct HTTP *ws = data->req.p.http;
+  struct connectdata *conn = data->conn;
+  CURLcode result;
+
+  /* Verify the Sec-WebSocket-Accept response.
+
+     The sent value is the base64 encoded version of a SHA-1 hash done on the
+     |Sec-WebSocket-Key| header field concatenated with
+     the string "258EAFA5-E914-47DA-95CA-C5AB0DC85B11".
+  */
+
+  /* If the response includes a |Sec-WebSocket-Extensions| header field and
+     this header field indicates the use of an extension that was not present
+     in the client's handshake (the server has indicated an extension not
+     requested by the client), the client MUST Fail the WebSocket Connection.
+  */
+
+  /* If the response includes a |Sec-WebSocket-Protocol| header field
+     and this header field indicates the use of a subprotocol that was
+     not present in the client's handshake (the server has indicated a
+     subprotocol not requested by the client), the client MUST Fail
+     the WebSocket Connection. */
+
+  /* 4 bytes random */
+  result = Curl_rand(data, (unsigned char *)&ws->ws.mask, sizeof(ws->ws.mask));
+  if(result)
+    return result;
+
+  infof(data, "Recevied 101, switch to WebSockets; mask %02x%02x%02x%02x",
+        ws->ws.mask[0], ws->ws.mask[1], ws->ws.mask[2], ws->ws.mask[3]);
+  k->upgr101 = UPGR101_RECEIVED;
+
+  if(data->set.connect_only)
+    /* switch off non-blocking sockets */
+    (void)curlx_nonblock(conn->sock[FIRSTSOCKET], FALSE);
+
+  return result;
+}
+
+#define WSBIT_FIN 0x80
+#define WSBIT_OPCODE_CONT  0
+#define WSBIT_OPCODE_TEXT  (1)
+#define WSBIT_OPCODE_BIN   (2)
+#define WSBIT_OPCODE_CLOSE (8)
+#define WSBIT_OPCODE_PING  (9)
+#define WSBIT_OPCODE_PONG  (0xa)
+#define WSBIT_OPCODE_MASK  (0xf)
+
+#define WSBIT_MASK 0x80
+
+/* 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)
+{
+  struct websockets *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);
+  Curl_dyn_tail(&wsp->buf, keep);
+}
+
+/* ws_decode() decodes a binary frame into structured WebSocket data,
+
+   wpkt - the incoming raw data. If NULL, work on the already buffered data.
+   ilen - the size of the provided data, perhaps too little, perhaps too much
+   out - stored pointed to extracted data
+   olen - stored length of the extracted data
+   endp - stored pointer to data immediately following the parsed data, if
+          there is more data in there. NULL if there's no more data.
+   flags - stored bitmask about the frame
+
+   Returns CURLE_AGAIN if there is only a partial frame in the buffer. Then it
+   stores the first part in the ->extra buffer to be used in the next call
+   when more data is provided.
+*/
+
+static CURLcode ws_decode(struct Curl_easy *data,
+                          unsigned char *wpkt, size_t ilen,
+                          unsigned char **out, size_t *olen,
+                          unsigned char **endp,
+                          unsigned int *flags)
+{
+  bool fin;
+  unsigned char opcode;
+  size_t total;
+  size_t dataindex = 2;
+  size_t plen; /* size of data in the buffer */
+  size_t payloadssize;
+  struct websockets *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;
+  }
+
+  plen = Curl_dyn_len(&wsp->buf);
+  if(plen < 2) {
+    /* the smallest possible frame is two bytes */
+    infof(data, "WS: plen == %u, EAGAIN", (int)plen);
+    return CURLE_AGAIN;
+  }
+
+  p = Curl_dyn_uptr(&wsp->buf);
+
+  fin = p[0] & WSBIT_FIN;
+  opcode = p[0] & WSBIT_OPCODE_MASK;
+  infof(data, "WS:%d received FIN bit %u", __LINE__, (int)fin);
+  *flags = 0;
+  switch(opcode) {
+  case WSBIT_OPCODE_CONT:
+    if(!fin)
+      *flags |= CURLWS_CONT;
+    infof(data, "WS: received OPCODE CONT");
+    break;
+  case WSBIT_OPCODE_TEXT:
+    infof(data, "WS: received OPCODE TEXT");
+    *flags |= CURLWS_TEXT;
+    break;
+  case WSBIT_OPCODE_BIN:
+    infof(data, "WS: received OPCODE BINARY");
+    *flags |= CURLWS_BINARY;
+    break;
+  case WSBIT_OPCODE_CLOSE:
+    infof(data, "WS: received OPCODE CLOSE");
+    *flags |= CURLWS_CLOSE;
+    break;
+  case WSBIT_OPCODE_PING:
+    infof(data, "WS: received OPCODE PING");
+    *flags |= CURLWS_PING;
+    break;
+  case WSBIT_OPCODE_PONG:
+    infof(data, "WS: received OPCODE PONG");
+    *flags |= CURLWS_PONG;
+    break;
+  }
+
+  if(p[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;
+  }
+  payloadssize = p[1];
+  if(payloadssize == 126) {
+    if(plen < 4) {
+      infof(data, "WS:%d plen == %u, EAGAIN", __LINE__, (int)plen);
+      return CURLE_AGAIN; /* not enough data available */
+    }
+    payloadssize = (p[2] << 8) | p[3];
+    dataindex += 2;
+  }
+  else if(payloadssize == 127) {
+    failf(data, "WS: too large frame received");
+    return CURLE_RECV_ERROR;
+  }
+
+  total = dataindex + payloadssize;
+  if(total > plen) {
+    /* not enough data in buffer yet */
+    infof(data, "WS:%d plen == %u (%u), EAGAIN", __LINE__, (int)plen,
+          (int)total);
+    return CURLE_AGAIN;
+  }
+
+  /* point to the payload */
+  *out = &p[dataindex];
+
+  /* return the payload length */
+  *olen = payloadssize;
+  wsp->usedbuf = total; /* number of bytes "used" from the buffer */
+  *endp = &p[total];
+  infof(data, "WS: received %u bytes payload", payloadssize);
+  return CURLE_OK;
+}
+
+/* Curl_ws_writecb() is the write callback for websocket traffic. The
+   websocket data is provided to this raw, in chunks. This function should
+   handle/decode the data and call the "real" underlying callback accordingly.
+*/
+size_t Curl_ws_writecb(char *buffer, size_t size /* 1 */,
+                       size_t nitems, void *userp)
+{
+  struct HTTP *ws = (struct HTTP *)userp;
+  struct Curl_easy *data = ws->ws.data;
+  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 *wsp;
+    size_t wslen;
+    unsigned int recvflags;
+    CURLcode result;
+    unsigned char *endp;
+    decode:
+    result = ws_decode(data, (unsigned char *)buffer, nitems,
+                       &wsp, &wslen, &endp, &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);
+      return nitems - 1;
+    }
+    /* auto-respond to PINGs */
+    if(recvflags & CURLWS_PING) {
+      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, wsp, wslen, &bytes, CURLWS_PONG);
+      if(result)
+        return result;
+    }
+    else {
+      /* TODO: store details about the frame in a struct to be reachable with
+         curl_ws_meta() from within the write callback */
+
+      /* deliver the decoded frame to the user callback */
+      if(data->set.fwrite_func((char *)wsp, 1, wslen, writebody_ptr) != wslen)
+        return 0;
+    }
+    /* the websocket frame has been delivered */
+    ws_decode_clear(data);
+    if(endp) {
+      /* there's more websocket data to deal with in the buffer */
+      buffer = NULL; /* don't pass in the data again */
+      goto decode;
+    }
+  }
+  return nitems;
+}
+
+
+CURLcode curl_ws_recv(struct Curl_easy *data, void *buffer, size_t buflen,
+                      size_t *nread, unsigned int *recvflags)
+{
+  size_t bytes;
+  CURLcode result;
+
+  *nread = 0;
+  *recvflags = 0;
+  /* get a download buffer */
+  result = Curl_preconnect(data);
+  if(result)
+    return result;
+
+  do {
+    result = curl_easy_recv(data, data->state.buffer,
+                            data->set.buffer_size, &bytes);
+    if(result)
+      return result;
+
+    if(bytes) {
+      unsigned char *out;
+      size_t olen;
+      unsigned char *endp;
+      infof(data, "WS: got %u websocket bytes to decode", (int)bytes);
+      result = ws_decode(data, (unsigned char *)data->state.buffer,
+                         bytes, &out, &olen, &endp, recvflags);
+      if(result == CURLE_AGAIN)
+        /* a packet fragment only */
+        break;
+      else if(result)
+        return result;
+
+      /* auto-respond to PINGs */
+      if(*recvflags & CURLWS_PING) {
+        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, CURLWS_PONG);
+        if(result)
+          return result;
+      }
+      else {
+        if(olen < buflen) {
+          /* copy the payload to the user buffer */
+          memcpy(buffer, out, olen);
+          *nread = olen;
+        }
+        else {
+          /* Received a larger websocket frame than what could fit in the user
+             provided buffer! */
+          infof(data, "WS: too large websocket frame received");
+          return CURLE_RECV_ERROR;
+        }
+      }
+      /* the websocket frame has been delivered */
+      ws_decode_clear(data);
+    }
+    else
+      *nread = bytes;
+    break;
+  } while(1);
+  return CURLE_OK;
+}
+
+/***
+    RFC 6455 Section 5.2
+
+      0                   1                   2                   3
+      0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+     +-+-+-+-+-------+-+-------------+-------------------------------+
+     |F|R|R|R| opcode|M| Payload len |    Extended payload length    |
+     |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
+     |N|V|V|V|       |S|             |   (if payload len==126/127)   |
+     | |1|2|3|       |K|             |                               |
+     +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
+     |     Extended payload length continued, if payload len == 127  |
+     + - - - - - - - - - - - - - - - +-------------------------------+
+     |                               |Masking-key, if MASK set to 1  |
+     +-------------------------------+-------------------------------+
+     | Masking-key (continued)       |          Payload Data         |
+     +-------------------------------- - - - - - - - - - - - - - - - +
+     :                     Payload Data continued ...                :
+     + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
+     |                     Payload Data continued ...                |
+     +---------------------------------------------------------------+
+*/
+
+static size_t ws_packet(struct Curl_easy *data,
+                        const unsigned char *payload, size_t len,
+                        unsigned int flags)
+{
+  struct HTTP *ws = data->req.p.http;
+  unsigned char *out = (unsigned char *)data->state.ulbuf;
+  unsigned char firstbyte = 0;
+  int outi;
+  unsigned char opcode;
+  unsigned int xori;
+  unsigned int i;
+  if(flags & CURLWS_TEXT) {
+    opcode = WSBIT_OPCODE_TEXT;
+    infof(data, "WS: send OPCODE TEXT");
+  }
+  else if(flags & CURLWS_CLOSE) {
+    opcode = WSBIT_OPCODE_CLOSE;
+    infof(data, "WS: send OPCODE CLOSE");
+  }
+  else if(flags & CURLWS_PING) {
+    opcode = WSBIT_OPCODE_PING;
+    infof(data, "WS: send OPCODE PING");
+  }
+  else if(flags & CURLWS_PONG) {
+    opcode = WSBIT_OPCODE_PONG;
+    infof(data, "WS: send OPCODE PONG");
+  }
+  else {
+    opcode = WSBIT_OPCODE_BIN;
+    infof(data, "WS: send OPCODE BINARY");
+  }
+
+  if(!(flags & CURLWS_CONT)) {
+    /* if not marked as continuing, assume this is the final fragment */
+    firstbyte |= WSBIT_FIN | opcode;
+    ws->ws.contfragment = FALSE;
+  }
+  else if(ws->ws.contfragment) {
+    /* the previous fragment was not a final one and this isn't either, keep a
+       CONT opcode and no FIN bit */
+    firstbyte |= WSBIT_OPCODE_CONT;
+  }
+  else {
+    ws->ws.contfragment = TRUE;
+  }
+  out[0] = firstbyte;
+  if(len > 126) {
+    /* no support for > 16 bit fragment sizes */
+    out[1] = 126 | WSBIT_MASK;
+    out[2] = (len >> 8) & 0xff;
+    out[3] = len & 0xff;
+    outi = 4;
+  }
+  else {
+    out[1] = (unsigned char)len | WSBIT_MASK;
+    outi = 2;
+  }
+
+  infof(data, "WS: send FIN bit %u (byte %02x)",
+        firstbyte & WSBIT_FIN ? 1 : 0,
+        firstbyte);
+  infof(data, "WS: send payload len %u", (int)len);
+
+  /* 4 bytes mask */
+  memcpy(&out[outi], &ws->ws.mask, 4);
+
+  if(data->set.upload_buffer_size < (len + 10))
+    return 0;
+
+  /* pass over the mask */
+  outi += 4;
+
+  /* append payload after the mask, XOR appropriately */
+  for(i = 0, xori = 0; i < len; i++, outi++) {
+    out[outi] = payload[i] ^ ws->ws.mask[xori];
+    xori++;
+    xori &= 3;
+  }
+
+  /* return packet size */
+  return outi;
+}
+
+CURLcode curl_ws_send(struct Curl_easy *data, const void *buffer,
+                      size_t buflen, size_t *sent,
+                      unsigned int sendflags)
+{
+  size_t bytes;
+  CURLcode result;
+  size_t plen;
+  char *out;
+
+  if(buflen > MAX_WS_SIZE) {
+    failf(data, "too large packet");
+    return CURLE_BAD_FUNCTION_ARGUMENT;
+  }
+
+  if(!data->set.ws_raw_mode) {
+    result = Curl_get_upload_buffer(data);
+    if(result)
+      return result;
+  }
+
+  if(Curl_is_in_callback(data)) {
+    ssize_t written;
+    if(data->set.ws_raw_mode) {
+      /* raw mode sends exactly what was requested, and this is from within
+         the write callback */
+      result = Curl_write(data, data->conn->writesockfd, buffer, buflen,
+                          &written);
+      infof(data, "WS: wanted to send %u bytes, sent %u bytes",
+            (int)buflen, (int)written);
+    }
+    else {
+      plen = ws_packet(data, buffer, buflen, sendflags);
+      out = data->state.ulbuf;
+      result = Curl_write(data, data->conn->writesockfd, out, plen,
+                          &written);
+      infof(data, "WS: wanted to send %u bytes, sent %u bytes",
+            (int)plen, (int)written);
+    }
+    bytes = written;
+  }
+  else {
+    plen = ws_packet(data, buffer, buflen, sendflags);
+
+    out = data->state.ulbuf;
+    result = Curl_senddata(data, out, plen, &bytes);
+    (void)sendflags;
+  }
+  *sent = bytes;
+
+  return result;
+}
+
+void Curl_ws_done(struct Curl_easy *data)
+{
+  struct websockets *wsp = &data->req.p.http->ws;
+  DEBUGASSERT(wsp);
+  Curl_dyn_free(&wsp->buf);
+}
+
+#else
+
+CURL_EXTERN CURLcode curl_ws_recv(CURL *curl, void *buffer, size_t buflen,
+                                  size_t *nread, unsigned int *recvflags)
+{
+  (void)curl;
+  (void)buffer;
+  (void)buflen;
+  (void)nread;
+  (void)recvflags;
+  return CURLE_OK;
+}
+
+CURL_EXTERN CURLcode curl_ws_send(CURL *curl, const void *buffer,
+                                  size_t buflen, size_t *sent,
+                                  unsigned int sendflags)
+{
+  (void)curl;
+  (void)buffer;
+  (void)buflen;
+  (void)sent;
+  (void)sendflags;
+  return CURLE_OK;
+}
+
+#endif /* USE_WEBSOCKETS */

+ 50 - 0
lib/ws.h

@@ -0,0 +1,50 @@
+#ifndef HEADER_CURL_WS_H
+#define HEADER_CURL_WS_H
+/***************************************************************************
+ *                                  _   _ ____  _
+ *  Project                     ___| | | |  _ \| |
+ *                             / __| | | | |_) | |
+ *                            | (__| |_| |  _ <| |___
+ *                             \___|\___/|_| \_\_____|
+ *
+ * Copyright (C) 1998 - 2022, 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"
+
+#ifdef USE_WEBSOCKETS
+
+#ifdef USE_HYPER
+#define REQTYPE void
+#else
+#define REQTYPE struct dynbuf
+#endif
+
+/* this is the largest single fragment size we support */
+#define MAX_WS_SIZE 65535
+
+CURLcode Curl_ws_request(struct Curl_easy *data, REQTYPE *req);
+CURLcode Curl_ws_accept(struct Curl_easy *data);
+
+size_t Curl_ws_writecb(char *buffer, size_t size, size_t nitems, void *userp);
+void Curl_ws_done(struct Curl_easy *data);
+
+#else
+#define Curl_ws_request(x,y) CURLE_OK
+#define Curl_ws_done(x) Curl_nop_stmt
+#endif
+
+#endif /* HEADER_CURL_WS_H */