diff options
Diffstat (limited to 'lib/ws.c')
-rw-r--r-- | lib/ws.c | 610 |
1 files changed, 610 insertions, 0 deletions
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 */ |