diff options
author | Daniel Stenberg <daniel@haxx.se> | 2022-09-09 15:11:14 +0200 |
---|---|---|
committer | Daniel Stenberg <daniel@haxx.se> | 2022-09-09 15:11:14 +0200 |
commit | 664249d095275ec532f55dd1752d80c8c1093a77 (patch) | |
tree | f8e3add4b66fb64271d22178743f3dc2f1758dcd | |
parent | 60a3b25dbf1f211f6ba5216f2d774cfb26cb3e29 (diff) | |
download | curl-664249d095275ec532f55dd1752d80c8c1093a77.tar.gz |
ws: initial websockets support
Closes #8995
-rw-r--r-- | CMakeLists.txt | 12 | ||||
-rw-r--r-- | configure.ac | 21 | ||||
-rw-r--r-- | docs/WebSockets.md | 16 | ||||
-rw-r--r-- | docs/libcurl/curl_easy_setopt.3 | 3 | ||||
-rw-r--r-- | docs/libcurl/curl_ws_recv.3 | 66 | ||||
-rw-r--r-- | docs/libcurl/curl_ws_send.3 | 73 | ||||
-rw-r--r-- | docs/libcurl/opts/CURLOPT_CONNECT_ONLY.3 | 4 | ||||
-rw-r--r-- | docs/libcurl/opts/CURLOPT_WS_OPTIONS.3 | 73 | ||||
-rw-r--r-- | docs/libcurl/opts/Makefile.inc | 1 | ||||
-rw-r--r-- | docs/libcurl/symbols-in-versions | 1 | ||||
-rw-r--r-- | include/curl/Makefile.am | 2 | ||||
-rw-r--r-- | include/curl/curl.h | 4 | ||||
-rw-r--r-- | include/curl/websockets.h | 68 | ||||
-rw-r--r-- | lib/Makefile.inc | 6 | ||||
-rw-r--r-- | lib/c-hyper.c | 22 | ||||
-rw-r--r-- | lib/conncache.c | 2 | ||||
-rw-r--r-- | lib/easy.c | 28 | ||||
-rw-r--r-- | lib/easyif.h | 3 | ||||
-rw-r--r-- | lib/easyoptions.c | 3 | ||||
-rw-r--r-- | lib/http.c | 119 | ||||
-rw-r--r-- | lib/http.h | 23 | ||||
-rw-r--r-- | lib/http2.c | 2 | ||||
-rw-r--r-- | lib/multi.c | 4 | ||||
-rw-r--r-- | lib/sendf.c | 16 | ||||
-rw-r--r-- | lib/setopt.c | 18 | ||||
-rw-r--r-- | lib/url.c | 16 | ||||
-rw-r--r-- | lib/urldata.h | 24 | ||||
-rw-r--r-- | lib/ws.c | 610 | ||||
-rw-r--r-- | lib/ws.h | 50 |
29 files changed, 1238 insertions, 52 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index ba79d9ec7..564c4dbc5 100644 --- a/CMakeLists.txt +++ b/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() diff --git a/configure.ac b/configure.ac index 919370c43..17c0a7cdc 100644 --- a/configure.ac +++ b/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) diff --git a/docs/WebSockets.md b/docs/WebSockets.md index 736f96538..5e36e1aa7 100644 --- a/docs/WebSockets.md +++ b/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: diff --git a/docs/libcurl/curl_easy_setopt.3 b/docs/libcurl/curl_easy_setopt.3 index ee95ff9f4..33be54d36 100644 --- a/docs/libcurl/curl_easy_setopt.3 +++ b/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 diff --git a/docs/libcurl/curl_ws_recv.3 b/docs/libcurl/curl_ws_recv.3 new file mode 100644 index 000000000..edbce84c4 --- /dev/null +++ b/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) " diff --git a/docs/libcurl/curl_ws_send.3 b/docs/libcurl/curl_ws_send.3 new file mode 100644 index 000000000..7cf103dbe --- /dev/null +++ b/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) " diff --git a/docs/libcurl/opts/CURLOPT_CONNECT_ONLY.3 b/docs/libcurl/opts/CURLOPT_CONNECT_ONLY.3 index 8429e37d9..b198da6c3 100644 --- a/docs/libcurl/opts/CURLOPT_CONNECT_ONLY.3 +++ b/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. diff --git a/docs/libcurl/opts/CURLOPT_WS_OPTIONS.3 b/docs/libcurl/opts/CURLOPT_WS_OPTIONS.3 new file mode 100644 index 000000000..b5c21b64b --- /dev/null +++ b/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), " diff --git a/docs/libcurl/opts/Makefile.inc b/docs/libcurl/opts/Makefile.inc index 03554e85f..a139d0676 100644 --- a/docs/libcurl/opts/Makefile.inc +++ b/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 \ diff --git a/docs/libcurl/symbols-in-versions b/docs/libcurl/symbols-in-versions index d6bcbc6b1..afe800216 100644 --- a/docs/libcurl/symbols-in-versions +++ b/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 diff --git a/include/curl/Makefile.am b/include/curl/Makefile.am index 60e26526e..29f470c09 100644 --- a/include/curl/Makefile.am +++ b/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 diff --git a/include/curl/curl.h b/include/curl/curl.h index 7a1b56196..45d7978ab 100644 --- a/include/curl/curl.h +++ b/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__) && \ diff --git a/include/curl/websockets.h b/include/curl/websockets.h new file mode 100644 index 000000000..ffb9c19b8 --- /dev/null +++ b/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 */ diff --git a/lib/Makefile.inc b/lib/Makefile.inc index 0c8972ce5..668712508 100644 --- a/lib/Makefile.inc +++ b/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 diff --git a/lib/c-hyper.c b/lib/c-hyper.c index d53b2366c..86abcdb0f 100644 --- a/lib/c-hyper.c +++ b/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; diff --git a/lib/conncache.c b/lib/conncache.c index 2a399c881..a557ac6dc 100644 --- a/lib/conncache.c +++ b/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); diff --git a/lib/easy.c b/lib/easy.c index 978ea5ac3..3a190d74d 100644 --- a/lib/easy.c +++ b/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; @@ -1272,6 +1269,19 @@ CURLcode curl_easy_send(struct Curl_easy *data, const void *buffer, } /* + * 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() * * Returns always 0. diff --git a/lib/easyif.h b/lib/easyif.h index 615df3f06..a8289e75a 100644 --- a/lib/easyif.h +++ b/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 diff --git a/lib/easyoptions.c b/lib/easyoptions.c index 412aefd99..e59b63af7 100644 --- a/lib/easyoptions.c +++ b/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 diff --git a/lib/http.c b/lib/http.c index a527b04d5..2fbd80fb2 100644 --- a/lib/http.c +++ b/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; diff --git a/lib/http.h b/lib/http.h index 2ac287eca..a335eee23 100644 --- a/lib/http.h +++ b/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 diff --git a/lib/http2.c b/lib/http2.c index 3a70528e4..b7409b027 100644 --- a/lib/http2.c +++ b/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; } diff --git a/lib/multi.c b/lib/multi.c index 6e41fb274..01b27f770 100644 --- a/lib/multi.c +++ b/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); diff --git a/lib/sendf.c b/lib/sendf.c index 2fe7169dd..66cec0597 100644 --- a/lib/sendf.c +++ b/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) { diff --git a/lib/setopt.c b/lib/setopt.c index 795c8f450..afc44013c 100644 --- a/lib/setopt.c +++ b/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; @@ -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) && \ diff --git a/lib/urldata.h b/lib/urldata.h index 0e69ce3d0..f6a644e67 100644 --- a/lib/urldata.h +++ b/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 { diff --git a/lib/ws.c b/lib/ws.c new file mode 100644 index 000000000..c8884126c --- /dev/null +++ b/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 */ diff --git a/lib/ws.h b/lib/ws.h new file mode 100644 index 000000000..2af5362cb --- /dev/null +++ b/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 */ |