ws: initial websockets support

Closes #8995
This commit is contained in:
Daniel Stenberg 2022-09-09 15:11:14 +02:00
parent 60a3b25dbf
commit 664249d095
No known key found for this signature in database
GPG Key ID: 5CC908FDB71E12C2
29 changed files with 1238 additions and 52 deletions

View File

@ -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()

View File

@ -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)

View File

@ -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:

View File

@ -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

View File

@ -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) "

View File

@ -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 theres 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) "

View File

@ -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.

View File

@ -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), "

View File

@ -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 \

View File

@ -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

View File

@ -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

View File

@ -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
include/curl/websockets.h Normal file
View File

@ -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 */

View File

@ -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

View File

@ -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;

View File

@ -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);

View File

@ -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()
*

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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

View File

@ -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;
}

View File

@ -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);

View File

@ -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) {

View File

@ -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;

View File

@ -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) && \

View File

@ -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
lib/ws.c Normal file
View File

@ -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
lib/ws.h Normal file
View File

@ -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 */