1 /***************************************************************************
2 * _ _ ____ _
3 * Project ___| | | | _ \| |
4 * / __| | | | |_) | |
5 * | (__| |_| | _ <| |___
6 * \___|\___/|_| \_\_____|
7 *
8 * Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
9 *
10 * This software is licensed as described in the file COPYING, which
11 * you should have received as part of this distribution. The terms
12 * are also available at https://curl.se/docs/copyright.html.
13 *
14 * You may opt to use, copy, modify, merge, publish, distribute and/or sell
15 * copies of the Software, and permit persons to whom the Software is
16 * furnished to do so, under the terms of the COPYING file.
17 *
18 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
19 * KIND, either express or implied.
20 *
21 * SPDX-License-Identifier: curl
22 *
23 ***************************************************************************/
24 #include "curl_setup.h"
25 #include <curl/curl.h>
26
27 #if defined(USE_WEBSOCKETS) && !defined(CURL_DISABLE_HTTP)
28
29 #include "urldata.h"
30 #include "bufq.h"
31 #include "dynbuf.h"
32 #include "rand.h"
33 #include "curl_base64.h"
34 #include "connect.h"
35 #include "sendf.h"
36 #include "multiif.h"
37 #include "ws.h"
38 #include "easyif.h"
39 #include "transfer.h"
40 #include "nonblock.h"
41
42 /* The last 3 #include files should be in this order */
43 #include "curl_printf.h"
44 #include "curl_memory.h"
45 #include "memdebug.h"
46
47
48 #define WSBIT_FIN 0x80
49 #define WSBIT_OPCODE_CONT 0
50 #define WSBIT_OPCODE_TEXT (1)
51 #define WSBIT_OPCODE_BIN (2)
52 #define WSBIT_OPCODE_CLOSE (8)
53 #define WSBIT_OPCODE_PING (9)
54 #define WSBIT_OPCODE_PONG (0xa)
55 #define WSBIT_OPCODE_MASK (0xf)
56
57 #define WSBIT_MASK 0x80
58
59 /* buffer dimensioning */
60 #define WS_CHUNK_SIZE 65535
61 #define WS_CHUNK_COUNT 2
62
63 struct ws_frame_meta {
64 char proto_opcode;
65 int flags;
66 const char *name;
67 };
68
69 static struct ws_frame_meta WS_FRAMES[] = {
70 { WSBIT_OPCODE_CONT, CURLWS_CONT, "CONT" },
71 { WSBIT_OPCODE_TEXT, CURLWS_TEXT, "TEXT" },
72 { WSBIT_OPCODE_BIN, CURLWS_BINARY, "BIN" },
73 { WSBIT_OPCODE_CLOSE, CURLWS_CLOSE, "CLOSE" },
74 { WSBIT_OPCODE_PING, CURLWS_PING, "PING" },
75 { WSBIT_OPCODE_PONG, CURLWS_PONG, "PONG" },
76 };
77
ws_frame_name_of_op(unsigned char proto_opcode)78 static const char *ws_frame_name_of_op(unsigned char proto_opcode)
79 {
80 unsigned char opcode = proto_opcode & WSBIT_OPCODE_MASK;
81 size_t i;
82 for(i = 0; i < sizeof(WS_FRAMES)/sizeof(WS_FRAMES[0]); ++i) {
83 if(WS_FRAMES[i].proto_opcode == opcode)
84 return WS_FRAMES[i].name;
85 }
86 return "???";
87 }
88
ws_frame_op2flags(unsigned char proto_opcode)89 static int ws_frame_op2flags(unsigned char proto_opcode)
90 {
91 unsigned char opcode = proto_opcode & WSBIT_OPCODE_MASK;
92 size_t i;
93 for(i = 0; i < sizeof(WS_FRAMES)/sizeof(WS_FRAMES[0]); ++i) {
94 if(WS_FRAMES[i].proto_opcode == opcode)
95 return WS_FRAMES[i].flags;
96 }
97 return 0;
98 }
99
ws_frame_flags2op(int flags)100 static unsigned char ws_frame_flags2op(int flags)
101 {
102 size_t i;
103 for(i = 0; i < sizeof(WS_FRAMES)/sizeof(WS_FRAMES[0]); ++i) {
104 if(WS_FRAMES[i].flags & flags)
105 return WS_FRAMES[i].proto_opcode;
106 }
107 return 0;
108 }
109
ws_dec_info(struct ws_decoder * dec,struct Curl_easy * data,const char * msg)110 static void ws_dec_info(struct ws_decoder *dec, struct Curl_easy *data,
111 const char *msg)
112 {
113 switch(dec->head_len) {
114 case 0:
115 break;
116 case 1:
117 CURL_TRC_WRITE(data, "websocket, decoded %s [%s%s]", msg,
118 ws_frame_name_of_op(dec->head[0]),
119 (dec->head[0] & WSBIT_FIN)? "" : " NON-FINAL");
120 break;
121 default:
122 if(dec->head_len < dec->head_total) {
123 CURL_TRC_WRITE(data, "websocket, decoded %s [%s%s](%d/%d)", msg,
124 ws_frame_name_of_op(dec->head[0]),
125 (dec->head[0] & WSBIT_FIN)? "" : " NON-FINAL",
126 dec->head_len, dec->head_total);
127 }
128 else {
129 CURL_TRC_WRITE(data, "websocket, decoded %s [%s%s payload=%"
130 CURL_FORMAT_CURL_OFF_T "/%" CURL_FORMAT_CURL_OFF_T "]",
131 msg, ws_frame_name_of_op(dec->head[0]),
132 (dec->head[0] & WSBIT_FIN)? "" : " NON-FINAL",
133 dec->payload_offset, dec->payload_len);
134 }
135 break;
136 }
137 }
138
139 typedef ssize_t ws_write_payload(const unsigned char *buf, size_t buflen,
140 int frame_age, int frame_flags,
141 curl_off_t payload_offset,
142 curl_off_t payload_len,
143 void *userp,
144 CURLcode *err);
145
146
ws_dec_reset(struct ws_decoder * dec)147 static void ws_dec_reset(struct ws_decoder *dec)
148 {
149 dec->frame_age = 0;
150 dec->frame_flags = 0;
151 dec->payload_offset = 0;
152 dec->payload_len = 0;
153 dec->head_len = dec->head_total = 0;
154 dec->state = WS_DEC_INIT;
155 }
156
ws_dec_init(struct ws_decoder * dec)157 static void ws_dec_init(struct ws_decoder *dec)
158 {
159 ws_dec_reset(dec);
160 }
161
ws_dec_read_head(struct ws_decoder * dec,struct Curl_easy * data,struct bufq * inraw)162 static CURLcode ws_dec_read_head(struct ws_decoder *dec,
163 struct Curl_easy *data,
164 struct bufq *inraw)
165 {
166 const unsigned char *inbuf;
167 size_t inlen;
168
169 while(Curl_bufq_peek(inraw, &inbuf, &inlen)) {
170 if(dec->head_len == 0) {
171 dec->head[0] = *inbuf;
172 Curl_bufq_skip(inraw, 1);
173
174 dec->frame_flags = ws_frame_op2flags(dec->head[0]);
175 if(!dec->frame_flags) {
176 failf(data, "WS: unknown opcode: %x", dec->head[0]);
177 ws_dec_reset(dec);
178 return CURLE_RECV_ERROR;
179 }
180 dec->head_len = 1;
181 /* ws_dec_info(dec, data, "seeing opcode"); */
182 continue;
183 }
184 else if(dec->head_len == 1) {
185 dec->head[1] = *inbuf;
186 Curl_bufq_skip(inraw, 1);
187 dec->head_len = 2;
188
189 if(dec->head[1] & WSBIT_MASK) {
190 /* A client MUST close a connection if it detects a masked frame. */
191 failf(data, "WS: masked input frame");
192 ws_dec_reset(dec);
193 return CURLE_RECV_ERROR;
194 }
195 /* How long is the frame head? */
196 if(dec->head[1] == 126) {
197 dec->head_total = 4;
198 continue;
199 }
200 else if(dec->head[1] == 127) {
201 dec->head_total = 10;
202 continue;
203 }
204 else {
205 dec->head_total = 2;
206 }
207 }
208
209 if(dec->head_len < dec->head_total) {
210 dec->head[dec->head_len] = *inbuf;
211 Curl_bufq_skip(inraw, 1);
212 ++dec->head_len;
213 if(dec->head_len < dec->head_total) {
214 /* ws_dec_info(dec, data, "decoding head"); */
215 continue;
216 }
217 }
218 /* got the complete frame head */
219 DEBUGASSERT(dec->head_len == dec->head_total);
220 switch(dec->head_total) {
221 case 2:
222 dec->payload_len = dec->head[1];
223 break;
224 case 4:
225 dec->payload_len = (dec->head[2] << 8) | dec->head[3];
226 break;
227 case 10:
228 if(dec->head[2] > 127) {
229 failf(data, "WS: frame length longer than 64 signed not supported");
230 return CURLE_RECV_ERROR;
231 }
232 dec->payload_len = ((curl_off_t)dec->head[2] << 56) |
233 (curl_off_t)dec->head[3] << 48 |
234 (curl_off_t)dec->head[4] << 40 |
235 (curl_off_t)dec->head[5] << 32 |
236 (curl_off_t)dec->head[6] << 24 |
237 (curl_off_t)dec->head[7] << 16 |
238 (curl_off_t)dec->head[8] << 8 |
239 dec->head[9];
240 break;
241 default:
242 /* this should never happen */
243 DEBUGASSERT(0);
244 failf(data, "WS: unexpected frame header length");
245 return CURLE_RECV_ERROR;
246 }
247
248 dec->frame_age = 0;
249 dec->payload_offset = 0;
250 ws_dec_info(dec, data, "decoded");
251 return CURLE_OK;
252 }
253 return CURLE_AGAIN;
254 }
255
ws_dec_pass_payload(struct ws_decoder * dec,struct Curl_easy * data,struct bufq * inraw,ws_write_payload * write_payload,void * write_ctx)256 static CURLcode ws_dec_pass_payload(struct ws_decoder *dec,
257 struct Curl_easy *data,
258 struct bufq *inraw,
259 ws_write_payload *write_payload,
260 void *write_ctx)
261 {
262 const unsigned char *inbuf;
263 size_t inlen;
264 ssize_t nwritten;
265 CURLcode result;
266 curl_off_t remain = dec->payload_len - dec->payload_offset;
267
268 (void)data;
269 while(remain && Curl_bufq_peek(inraw, &inbuf, &inlen)) {
270 if((curl_off_t)inlen > remain)
271 inlen = (size_t)remain;
272 nwritten = write_payload(inbuf, inlen, dec->frame_age, dec->frame_flags,
273 dec->payload_offset, dec->payload_len,
274 write_ctx, &result);
275 if(nwritten < 0)
276 return result;
277 Curl_bufq_skip(inraw, (size_t)nwritten);
278 dec->payload_offset += (curl_off_t)nwritten;
279 remain = dec->payload_len - dec->payload_offset;
280 CURL_TRC_WRITE(data, "websocket, passed %zd bytes payload, %"
281 CURL_FORMAT_CURL_OFF_T " remain", nwritten, remain);
282 }
283
284 return remain? CURLE_AGAIN : CURLE_OK;
285 }
286
ws_dec_pass(struct ws_decoder * dec,struct Curl_easy * data,struct bufq * inraw,ws_write_payload * write_payload,void * write_ctx)287 static CURLcode ws_dec_pass(struct ws_decoder *dec,
288 struct Curl_easy *data,
289 struct bufq *inraw,
290 ws_write_payload *write_payload,
291 void *write_ctx)
292 {
293 CURLcode result;
294
295 if(Curl_bufq_is_empty(inraw))
296 return CURLE_AGAIN;
297
298 switch(dec->state) {
299 case WS_DEC_INIT:
300 ws_dec_reset(dec);
301 dec->state = WS_DEC_HEAD;
302 FALLTHROUGH();
303 case WS_DEC_HEAD:
304 result = ws_dec_read_head(dec, data, inraw);
305 if(result) {
306 if(result != CURLE_AGAIN) {
307 infof(data, "WS: decode error %d", (int)result);
308 break; /* real error */
309 }
310 /* incomplete ws frame head */
311 DEBUGASSERT(Curl_bufq_is_empty(inraw));
312 break;
313 }
314 /* head parsing done */
315 dec->state = WS_DEC_PAYLOAD;
316 if(dec->payload_len == 0) {
317 ssize_t nwritten;
318 const unsigned char tmp = '\0';
319 /* special case of a 0 length frame, need to write once */
320 nwritten = write_payload(&tmp, 0, dec->frame_age, dec->frame_flags,
321 0, 0, write_ctx, &result);
322 if(nwritten < 0)
323 return result;
324 dec->state = WS_DEC_INIT;
325 break;
326 }
327 FALLTHROUGH();
328 case WS_DEC_PAYLOAD:
329 result = ws_dec_pass_payload(dec, data, inraw, write_payload, write_ctx);
330 ws_dec_info(dec, data, "passing");
331 if(result)
332 return result;
333 /* paylod parsing done */
334 dec->state = WS_DEC_INIT;
335 break;
336 default:
337 /* we covered all enums above, but some code analyzers are whimps */
338 result = CURLE_FAILED_INIT;
339 }
340 return result;
341 }
342
update_meta(struct websocket * ws,int frame_age,int frame_flags,curl_off_t payload_offset,curl_off_t payload_len,size_t cur_len)343 static void update_meta(struct websocket *ws,
344 int frame_age, int frame_flags,
345 curl_off_t payload_offset,
346 curl_off_t payload_len,
347 size_t cur_len)
348 {
349 ws->frame.age = frame_age;
350 ws->frame.flags = frame_flags;
351 ws->frame.offset = payload_offset;
352 ws->frame.len = cur_len;
353 ws->frame.bytesleft = (payload_len - payload_offset - cur_len);
354 }
355
356 /* WebSockets decoding client writer */
357 struct ws_cw_ctx {
358 struct Curl_cwriter super;
359 struct bufq buf;
360 };
361
ws_cw_init(struct Curl_easy * data,struct Curl_cwriter * writer)362 static CURLcode ws_cw_init(struct Curl_easy *data,
363 struct Curl_cwriter *writer)
364 {
365 struct ws_cw_ctx *ctx = writer->ctx;
366 (void)data;
367 Curl_bufq_init2(&ctx->buf, WS_CHUNK_SIZE, 1, BUFQ_OPT_SOFT_LIMIT);
368 return CURLE_OK;
369 }
370
ws_cw_close(struct Curl_easy * data,struct Curl_cwriter * writer)371 static void ws_cw_close(struct Curl_easy *data, struct Curl_cwriter *writer)
372 {
373 struct ws_cw_ctx *ctx = writer->ctx;
374 (void) data;
375 Curl_bufq_free(&ctx->buf);
376 }
377
378 struct ws_cw_dec_ctx {
379 struct Curl_easy *data;
380 struct websocket *ws;
381 struct Curl_cwriter *next_writer;
382 int cw_type;
383 };
384
ws_cw_dec_next(const unsigned char * buf,size_t buflen,int frame_age,int frame_flags,curl_off_t payload_offset,curl_off_t payload_len,void * user_data,CURLcode * err)385 static ssize_t ws_cw_dec_next(const unsigned char *buf, size_t buflen,
386 int frame_age, int frame_flags,
387 curl_off_t payload_offset,
388 curl_off_t payload_len,
389 void *user_data,
390 CURLcode *err)
391 {
392 struct ws_cw_dec_ctx *ctx = user_data;
393 struct Curl_easy *data = ctx->data;
394 struct websocket *ws = ctx->ws;
395 curl_off_t remain = (payload_len - (payload_offset + buflen));
396
397 (void)frame_age;
398 if((frame_flags & CURLWS_PING) && !remain) {
399 /* auto-respond to PINGs, only works for single-frame payloads atm */
400 size_t bytes;
401 infof(data, "WS: auto-respond to PING with a PONG");
402 /* send back the exact same content as a PONG */
403 *err = curl_ws_send(data, buf, buflen, &bytes, 0, CURLWS_PONG);
404 if(*err)
405 return -1;
406 }
407 else if(buflen || !remain) {
408 /* forward the decoded frame to the next client writer. */
409 update_meta(ws, frame_age, frame_flags, payload_offset,
410 payload_len, buflen);
411
412 *err = Curl_cwriter_write(data, ctx->next_writer, ctx->cw_type,
413 (const char *)buf, buflen);
414 if(*err)
415 return -1;
416 }
417 *err = CURLE_OK;
418 return (ssize_t)buflen;
419 }
420
ws_cw_write(struct Curl_easy * data,struct Curl_cwriter * writer,int type,const char * buf,size_t nbytes)421 static CURLcode ws_cw_write(struct Curl_easy *data,
422 struct Curl_cwriter *writer, int type,
423 const char *buf, size_t nbytes)
424 {
425 struct ws_cw_ctx *ctx = writer->ctx;
426 struct websocket *ws;
427 CURLcode result;
428
429 if(!(type & CLIENTWRITE_BODY) || data->set.ws_raw_mode)
430 return Curl_cwriter_write(data, writer->next, type, buf, nbytes);
431
432 ws = data->conn->proto.ws;
433 if(!ws) {
434 failf(data, "WS: not a websocket transfer");
435 return CURLE_FAILED_INIT;
436 }
437
438 if(nbytes) {
439 ssize_t nwritten;
440 nwritten = Curl_bufq_write(&ctx->buf, (const unsigned char *)buf,
441 nbytes, &result);
442 if(nwritten < 0) {
443 infof(data, "WS: error adding data to buffer %d", result);
444 return result;
445 }
446 }
447
448 while(!Curl_bufq_is_empty(&ctx->buf)) {
449 struct ws_cw_dec_ctx pass_ctx;
450 pass_ctx.data = data;
451 pass_ctx.ws = ws;
452 pass_ctx.next_writer = writer->next;
453 pass_ctx.cw_type = type;
454 result = ws_dec_pass(&ws->dec, data, &ctx->buf,
455 ws_cw_dec_next, &pass_ctx);
456 if(result == CURLE_AGAIN) {
457 /* insufficient amount of data, keep it for later.
458 * we pretend to have written all since we have a copy */
459 CURL_TRC_WRITE(data, "websocket, buffered incomplete frame head");
460 return CURLE_OK;
461 }
462 else if(result) {
463 infof(data, "WS: decode error %d", (int)result);
464 return result;
465 }
466 }
467
468 if((type & CLIENTWRITE_EOS) && !Curl_bufq_is_empty(&ctx->buf)) {
469 infof(data, "WS: decode ending with %zd frame bytes remaining",
470 Curl_bufq_len(&ctx->buf));
471 return CURLE_RECV_ERROR;
472 }
473
474 return CURLE_OK;
475 }
476
477 /* WebSocket payload decoding client writer. */
478 static const struct Curl_cwtype ws_cw_decode = {
479 "ws-decode",
480 NULL,
481 ws_cw_init,
482 ws_cw_write,
483 ws_cw_close,
484 sizeof(struct ws_cw_ctx)
485 };
486
487
ws_enc_info(struct ws_encoder * enc,struct Curl_easy * data,const char * msg)488 static void ws_enc_info(struct ws_encoder *enc, struct Curl_easy *data,
489 const char *msg)
490 {
491 infof(data, "WS-ENC: %s [%s%s%s payload=%" CURL_FORMAT_CURL_OFF_T
492 "/%" CURL_FORMAT_CURL_OFF_T "]",
493 msg, ws_frame_name_of_op(enc->firstbyte),
494 (enc->firstbyte & WSBIT_OPCODE_MASK) == WSBIT_OPCODE_CONT ?
495 " CONT" : "",
496 (enc->firstbyte & WSBIT_FIN)? "" : " NON-FIN",
497 enc->payload_len - enc->payload_remain, enc->payload_len);
498 }
499
ws_enc_reset(struct ws_encoder * enc)500 static void ws_enc_reset(struct ws_encoder *enc)
501 {
502 enc->payload_remain = 0;
503 enc->xori = 0;
504 enc->contfragment = FALSE;
505 }
506
ws_enc_init(struct ws_encoder * enc)507 static void ws_enc_init(struct ws_encoder *enc)
508 {
509 ws_enc_reset(enc);
510 }
511
512 /***
513 RFC 6455 Section 5.2
514
515 0 1 2 3
516 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
517 +-+-+-+-+-------+-+-------------+-------------------------------+
518 |F|R|R|R| opcode|M| Payload len | Extended payload length |
519 |I|S|S|S| (4) |A| (7) | (16/64) |
520 |N|V|V|V| |S| | (if payload len==126/127) |
521 | |1|2|3| |K| | |
522 +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
523 | Extended payload length continued, if payload len == 127 |
524 + - - - - - - - - - - - - - - - +-------------------------------+
525 | |Masking-key, if MASK set to 1 |
526 +-------------------------------+-------------------------------+
527 | Masking-key (continued) | Payload Data |
528 +-------------------------------- - - - - - - - - - - - - - - - +
529 : Payload Data continued ... :
530 + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
531 | Payload Data continued ... |
532 +---------------------------------------------------------------+
533 */
534
ws_enc_write_head(struct Curl_easy * data,struct ws_encoder * enc,unsigned int flags,curl_off_t payload_len,struct bufq * out,CURLcode * err)535 static ssize_t ws_enc_write_head(struct Curl_easy *data,
536 struct ws_encoder *enc,
537 unsigned int flags,
538 curl_off_t payload_len,
539 struct bufq *out,
540 CURLcode *err)
541 {
542 unsigned char firstbyte = 0;
543 unsigned char opcode;
544 unsigned char head[14];
545 size_t hlen;
546 ssize_t n;
547
548 if(payload_len < 0) {
549 failf(data, "WS: starting new frame with negative payload length %"
550 CURL_FORMAT_CURL_OFF_T, payload_len);
551 *err = CURLE_SEND_ERROR;
552 return -1;
553 }
554
555 if(enc->payload_remain > 0) {
556 /* trying to write a new frame before the previous one is finished */
557 failf(data, "WS: starting new frame with %zd bytes from last one"
558 "remaining to be sent", (ssize_t)enc->payload_remain);
559 *err = CURLE_SEND_ERROR;
560 return -1;
561 }
562
563 opcode = ws_frame_flags2op(flags);
564 if(!opcode) {
565 failf(data, "WS: provided flags not recognized '%x'", flags);
566 *err = CURLE_SEND_ERROR;
567 return -1;
568 }
569
570 if(!(flags & CURLWS_CONT)) {
571 if(!enc->contfragment)
572 /* not marked as continuing, this is the final fragment */
573 firstbyte |= WSBIT_FIN | opcode;
574 else
575 /* marked as continuing, this is the final fragment; set CONT
576 opcode and FIN bit */
577 firstbyte |= WSBIT_FIN | WSBIT_OPCODE_CONT;
578
579 enc->contfragment = FALSE;
580 }
581 else if(enc->contfragment) {
582 /* the previous fragment was not a final one and this isn't either, keep a
583 CONT opcode and no FIN bit */
584 firstbyte |= WSBIT_OPCODE_CONT;
585 }
586 else {
587 firstbyte = opcode;
588 enc->contfragment = TRUE;
589 }
590
591 head[0] = enc->firstbyte = firstbyte;
592 if(payload_len > 65535) {
593 head[1] = 127 | WSBIT_MASK;
594 head[2] = (unsigned char)((payload_len >> 56) & 0xff);
595 head[3] = (unsigned char)((payload_len >> 48) & 0xff);
596 head[4] = (unsigned char)((payload_len >> 40) & 0xff);
597 head[5] = (unsigned char)((payload_len >> 32) & 0xff);
598 head[6] = (unsigned char)((payload_len >> 24) & 0xff);
599 head[7] = (unsigned char)((payload_len >> 16) & 0xff);
600 head[8] = (unsigned char)((payload_len >> 8) & 0xff);
601 head[9] = (unsigned char)(payload_len & 0xff);
602 hlen = 10;
603 }
604 else if(payload_len >= 126) {
605 head[1] = 126 | WSBIT_MASK;
606 head[2] = (unsigned char)((payload_len >> 8) & 0xff);
607 head[3] = (unsigned char)(payload_len & 0xff);
608 hlen = 4;
609 }
610 else {
611 head[1] = (unsigned char)payload_len | WSBIT_MASK;
612 hlen = 2;
613 }
614
615 enc->payload_remain = enc->payload_len = payload_len;
616 ws_enc_info(enc, data, "sending");
617
618 /* add 4 bytes mask */
619 memcpy(&head[hlen], &enc->mask, 4);
620 hlen += 4;
621 /* reset for payload to come */
622 enc->xori = 0;
623
624 n = Curl_bufq_write(out, head, hlen, err);
625 if(n < 0)
626 return -1;
627 if((size_t)n != hlen) {
628 /* We use a bufq with SOFT_LIMIT, writing should always succeed */
629 DEBUGASSERT(0);
630 *err = CURLE_SEND_ERROR;
631 return -1;
632 }
633 return n;
634 }
635
ws_enc_write_payload(struct ws_encoder * enc,struct Curl_easy * data,const unsigned char * buf,size_t buflen,struct bufq * out,CURLcode * err)636 static ssize_t ws_enc_write_payload(struct ws_encoder *enc,
637 struct Curl_easy *data,
638 const unsigned char *buf, size_t buflen,
639 struct bufq *out, CURLcode *err)
640 {
641 ssize_t n;
642 size_t i, len;
643
644 if(Curl_bufq_is_full(out)) {
645 *err = CURLE_AGAIN;
646 return -1;
647 }
648
649 /* not the most performant way to do this */
650 len = buflen;
651 if((curl_off_t)len > enc->payload_remain)
652 len = (size_t)enc->payload_remain;
653
654 for(i = 0; i < len; ++i) {
655 unsigned char c = buf[i] ^ enc->mask[enc->xori];
656 n = Curl_bufq_write(out, &c, 1, err);
657 if(n < 0) {
658 if((*err != CURLE_AGAIN) || !i)
659 return -1;
660 break;
661 }
662 enc->xori++;
663 enc->xori &= 3;
664 }
665 enc->payload_remain -= (curl_off_t)i;
666 ws_enc_info(enc, data, "buffered");
667 return (ssize_t)i;
668 }
669
670
671 struct wsfield {
672 const char *name;
673 const char *val;
674 };
675
Curl_ws_request(struct Curl_easy * data,REQTYPE * req)676 CURLcode Curl_ws_request(struct Curl_easy *data, REQTYPE *req)
677 {
678 unsigned int i;
679 CURLcode result = CURLE_OK;
680 unsigned char rand[16];
681 char *randstr;
682 size_t randlen;
683 char keyval[40];
684 struct SingleRequest *k = &data->req;
685 struct wsfield heads[]= {
686 {
687 /* The request MUST contain an |Upgrade| header field whose value
688 MUST include the "websocket" keyword. */
689 "Upgrade:", "websocket"
690 },
691 {
692 /* The request MUST contain a |Connection| header field whose value
693 MUST include the "Upgrade" token. */
694 "Connection:", "Upgrade",
695 },
696 {
697 /* The request MUST include a header field with the name
698 |Sec-WebSocket-Version|. The value of this header field MUST be
699 13. */
700 "Sec-WebSocket-Version:", "13",
701 },
702 {
703 /* The request MUST include a header field with the name
704 |Sec-WebSocket-Key|. The value of this header field MUST be a nonce
705 consisting of a randomly selected 16-byte value that has been
706 base64-encoded (see Section 4 of [RFC4648]). The nonce MUST be
707 selected randomly for each connection. */
708 "Sec-WebSocket-Key:", NULL,
709 }
710 };
711 heads[3].val = &keyval[0];
712
713 /* 16 bytes random */
714 result = Curl_rand(data, (unsigned char *)rand, sizeof(rand));
715 if(result)
716 return result;
717 result = Curl_base64_encode((char *)rand, sizeof(rand), &randstr, &randlen);
718 if(result)
719 return result;
720 DEBUGASSERT(randlen < sizeof(keyval));
721 if(randlen >= sizeof(keyval))
722 return CURLE_FAILED_INIT;
723 strcpy(keyval, randstr);
724 free(randstr);
725 for(i = 0; !result && (i < sizeof(heads)/sizeof(heads[0])); i++) {
726 if(!Curl_checkheaders(data, STRCONST(heads[i].name))) {
727 #ifdef USE_HYPER
728 char field[128];
729 msnprintf(field, sizeof(field), "%s %s", heads[i].name,
730 heads[i].val);
731 result = Curl_hyper_header(data, req, field);
732 #else
733 (void)data;
734 result = Curl_dyn_addf(req, "%s %s\r\n", heads[i].name,
735 heads[i].val);
736 #endif
737 }
738 }
739 k->upgr101 = UPGR101_WS;
740 return result;
741 }
742
743 /*
744 * 'nread' is number of bytes of websocket data already in the buffer at
745 * 'mem'.
746 */
Curl_ws_accept(struct Curl_easy * data,const char * mem,size_t nread)747 CURLcode Curl_ws_accept(struct Curl_easy *data,
748 const char *mem, size_t nread)
749 {
750 struct SingleRequest *k = &data->req;
751 struct websocket *ws;
752 struct Curl_cwriter *ws_dec_writer;
753 CURLcode result;
754
755 DEBUGASSERT(data->conn);
756 ws = data->conn->proto.ws;
757 if(!ws) {
758 size_t chunk_size = WS_CHUNK_SIZE;
759 ws = calloc(1, sizeof(*ws));
760 if(!ws)
761 return CURLE_OUT_OF_MEMORY;
762 data->conn->proto.ws = ws;
763 #ifdef DEBUGBUILD
764 {
765 char *p = getenv("CURL_WS_CHUNK_SIZE");
766 if(p) {
767 long l = strtol(p, NULL, 10);
768 if(l > 0 && l <= (1*1024*1024)) {
769 chunk_size = (size_t)l;
770 }
771 }
772 }
773 #endif
774 DEBUGF(infof(data, "WS, using chunk size %zu", chunk_size));
775 Curl_bufq_init2(&ws->recvbuf, chunk_size, WS_CHUNK_COUNT,
776 BUFQ_OPT_SOFT_LIMIT);
777 Curl_bufq_init2(&ws->sendbuf, chunk_size, WS_CHUNK_COUNT,
778 BUFQ_OPT_SOFT_LIMIT);
779 ws_dec_init(&ws->dec);
780 ws_enc_init(&ws->enc);
781 }
782 else {
783 Curl_bufq_reset(&ws->recvbuf);
784 ws_dec_reset(&ws->dec);
785 ws_enc_reset(&ws->enc);
786 }
787 /* Verify the Sec-WebSocket-Accept response.
788
789 The sent value is the base64 encoded version of a SHA-1 hash done on the
790 |Sec-WebSocket-Key| header field concatenated with
791 the string "258EAFA5-E914-47DA-95CA-C5AB0DC85B11".
792 */
793
794 /* If the response includes a |Sec-WebSocket-Extensions| header field and
795 this header field indicates the use of an extension that was not present
796 in the client's handshake (the server has indicated an extension not
797 requested by the client), the client MUST Fail the WebSocket Connection.
798 */
799
800 /* If the response includes a |Sec-WebSocket-Protocol| header field
801 and this header field indicates the use of a subprotocol that was
802 not present in the client's handshake (the server has indicated a
803 subprotocol not requested by the client), the client MUST Fail
804 the WebSocket Connection. */
805
806 /* 4 bytes random */
807
808 result = Curl_rand(data, (unsigned char *)&ws->enc.mask,
809 sizeof(ws->enc.mask));
810 if(result)
811 return result;
812 infof(data, "Received 101, switch to WebSocket; mask %02x%02x%02x%02x",
813 ws->enc.mask[0], ws->enc.mask[1], ws->enc.mask[2], ws->enc.mask[3]);
814
815 /* Install our client writer that decodes WS frames payload */
816 result = Curl_cwriter_create(&ws_dec_writer, data, &ws_cw_decode,
817 CURL_CW_CONTENT_DECODE);
818 if(result)
819 return result;
820
821 result = Curl_cwriter_add(data, ws_dec_writer);
822 if(result) {
823 Curl_cwriter_free(data, ws_dec_writer);
824 return result;
825 }
826
827 if(data->set.connect_only) {
828 ssize_t nwritten;
829 /* In CONNECT_ONLY setup, the payloads from `mem` need to be received
830 * when using `curl_ws_recv` later on after this transfer is already
831 * marked as DONE. */
832 nwritten = Curl_bufq_write(&ws->recvbuf, (const unsigned char *)mem,
833 nread, &result);
834 if(nwritten < 0)
835 return result;
836 infof(data, "%zu bytes websocket payload", nread);
837 }
838 else { /* !connect_only */
839 /* And pass any additional data to the writers */
840 if(nread) {
841 result = Curl_client_write(data, CLIENTWRITE_BODY, (char *)mem, nread);
842 }
843 }
844 k->upgr101 = UPGR101_RECEIVED;
845
846 return result;
847 }
848
849 struct ws_collect {
850 struct Curl_easy *data;
851 unsigned char *buffer;
852 size_t buflen;
853 size_t bufidx;
854 int frame_age;
855 int frame_flags;
856 curl_off_t payload_offset;
857 curl_off_t payload_len;
858 bool written;
859 };
860
ws_client_collect(const unsigned char * buf,size_t buflen,int frame_age,int frame_flags,curl_off_t payload_offset,curl_off_t payload_len,void * userp,CURLcode * err)861 static ssize_t ws_client_collect(const unsigned char *buf, size_t buflen,
862 int frame_age, int frame_flags,
863 curl_off_t payload_offset,
864 curl_off_t payload_len,
865 void *userp,
866 CURLcode *err)
867 {
868 struct ws_collect *ctx = userp;
869 size_t nwritten;
870 curl_off_t remain = (payload_len - (payload_offset + buflen));
871
872 if(!ctx->bufidx) {
873 /* first write */
874 ctx->frame_age = frame_age;
875 ctx->frame_flags = frame_flags;
876 ctx->payload_offset = payload_offset;
877 ctx->payload_len = payload_len;
878 }
879
880 if((frame_flags & CURLWS_PING) && !remain) {
881 /* auto-respond to PINGs, only works for single-frame payloads atm */
882 size_t bytes;
883 infof(ctx->data, "WS: auto-respond to PING with a PONG");
884 /* send back the exact same content as a PONG */
885 *err = curl_ws_send(ctx->data, buf, buflen, &bytes, 0, CURLWS_PONG);
886 if(*err)
887 return -1;
888 nwritten = bytes;
889 }
890 else {
891 ctx->written = TRUE;
892 DEBUGASSERT(ctx->buflen >= ctx->bufidx);
893 nwritten = CURLMIN(buflen, ctx->buflen - ctx->bufidx);
894 if(!nwritten) {
895 if(!buflen) { /* 0 length write, we accept that */
896 *err = CURLE_OK;
897 return 0;
898 }
899 *err = CURLE_AGAIN; /* no more space */
900 return -1;
901 }
902 *err = CURLE_OK;
903 memcpy(ctx->buffer + ctx->bufidx, buf, nwritten);
904 ctx->bufidx += nwritten;
905 }
906 return nwritten;
907 }
908
nw_in_recv(void * reader_ctx,unsigned char * buf,size_t buflen,CURLcode * err)909 static ssize_t nw_in_recv(void *reader_ctx,
910 unsigned char *buf, size_t buflen,
911 CURLcode *err)
912 {
913 struct Curl_easy *data = reader_ctx;
914 size_t nread;
915
916 *err = curl_easy_recv(data, buf, buflen, &nread);
917 if(*err)
918 return -1;
919 return (ssize_t)nread;
920 }
921
curl_ws_recv(struct Curl_easy * data,void * buffer,size_t buflen,size_t * nread,const struct curl_ws_frame ** metap)922 CURL_EXTERN CURLcode curl_ws_recv(struct Curl_easy *data, void *buffer,
923 size_t buflen, size_t *nread,
924 const struct curl_ws_frame **metap)
925 {
926 struct connectdata *conn = data->conn;
927 struct websocket *ws;
928 bool done = FALSE; /* not filled passed buffer yet */
929 struct ws_collect ctx;
930 CURLcode result;
931
932 if(!conn) {
933 /* Unhappy hack with lifetimes of transfers and connection */
934 if(!data->set.connect_only) {
935 failf(data, "CONNECT_ONLY is required");
936 return CURLE_UNSUPPORTED_PROTOCOL;
937 }
938
939 Curl_getconnectinfo(data, &conn);
940 if(!conn) {
941 failf(data, "connection not found");
942 return CURLE_BAD_FUNCTION_ARGUMENT;
943 }
944 }
945 ws = conn->proto.ws;
946 if(!ws) {
947 failf(data, "connection is not setup for websocket");
948 return CURLE_BAD_FUNCTION_ARGUMENT;
949 }
950
951 *nread = 0;
952 *metap = NULL;
953
954 memset(&ctx, 0, sizeof(ctx));
955 ctx.data = data;
956 ctx.buffer = buffer;
957 ctx.buflen = buflen;
958
959 while(!done) {
960 /* receive more when our buffer is empty */
961 if(Curl_bufq_is_empty(&ws->recvbuf)) {
962 ssize_t n = Curl_bufq_slurp(&ws->recvbuf, nw_in_recv, data, &result);
963 if(n < 0) {
964 return result;
965 }
966 else if(n == 0) {
967 /* connection closed */
968 infof(data, "connection expectedly closed?");
969 return CURLE_GOT_NOTHING;
970 }
971 DEBUGF(infof(data, "curl_ws_recv, added %zu bytes from network",
972 Curl_bufq_len(&ws->recvbuf)));
973 }
974
975 result = ws_dec_pass(&ws->dec, data, &ws->recvbuf,
976 ws_client_collect, &ctx);
977 if(result == CURLE_AGAIN) {
978 if(!ctx.written) {
979 ws_dec_info(&ws->dec, data, "need more input");
980 continue; /* nothing written, try more input */
981 }
982 done = TRUE;
983 break;
984 }
985 else if(result) {
986 return result;
987 }
988 else if(ctx.written) {
989 /* The decoded frame is passed back to our caller.
990 * There are frames like PING were we auto-respond to and
991 * that we do not return. For these `ctx.written` is not set. */
992 done = TRUE;
993 break;
994 }
995 }
996
997 /* update frame information to be passed back */
998 update_meta(ws, ctx.frame_age, ctx.frame_flags, ctx.payload_offset,
999 ctx.payload_len, ctx.bufidx);
1000 *metap = &ws->frame;
1001 *nread = ws->frame.len;
1002 /* infof(data, "curl_ws_recv(len=%zu) -> %zu bytes (frame at %"
1003 CURL_FORMAT_CURL_OFF_T ", %" CURL_FORMAT_CURL_OFF_T " left)",
1004 buflen, *nread, ws->frame.offset, ws->frame.bytesleft); */
1005 return CURLE_OK;
1006 }
1007
ws_flush(struct Curl_easy * data,struct websocket * ws,bool complete)1008 static CURLcode ws_flush(struct Curl_easy *data, struct websocket *ws,
1009 bool complete)
1010 {
1011 if(!Curl_bufq_is_empty(&ws->sendbuf)) {
1012 CURLcode result;
1013 const unsigned char *out;
1014 size_t outlen, n;
1015
1016 while(Curl_bufq_peek(&ws->sendbuf, &out, &outlen)) {
1017 if(data->set.connect_only)
1018 result = Curl_senddata(data, out, outlen, &n);
1019 else {
1020 result = Curl_xfer_send(data, out, outlen, &n);
1021 if(!result && !n && outlen)
1022 result = CURLE_AGAIN;
1023 }
1024
1025 if(result) {
1026 if(result == CURLE_AGAIN) {
1027 if(!complete) {
1028 infof(data, "WS: flush EAGAIN, %zu bytes remain in buffer",
1029 Curl_bufq_len(&ws->sendbuf));
1030 return result;
1031 }
1032 /* TODO: the current design does not allow for buffered writes.
1033 * We need to flush the buffer now. There is no ws_flush() later */
1034 n = 0;
1035 continue;
1036 }
1037 else if(result) {
1038 failf(data, "WS: flush, write error %d", result);
1039 return result;
1040 }
1041 }
1042 else {
1043 infof(data, "WS: flushed %zu bytes", n);
1044 Curl_bufq_skip(&ws->sendbuf, n);
1045 }
1046 }
1047 }
1048 return CURLE_OK;
1049 }
1050
curl_ws_send(CURL * data,const void * buffer,size_t buflen,size_t * sent,curl_off_t fragsize,unsigned int flags)1051 CURL_EXTERN CURLcode curl_ws_send(CURL *data, const void *buffer,
1052 size_t buflen, size_t *sent,
1053 curl_off_t fragsize,
1054 unsigned int flags)
1055 {
1056 struct websocket *ws;
1057 ssize_t n;
1058 size_t nwritten, space;
1059 CURLcode result;
1060
1061 *sent = 0;
1062 if(!data->conn && data->set.connect_only) {
1063 result = Curl_connect_only_attach(data);
1064 if(result)
1065 return result;
1066 }
1067 if(!data->conn) {
1068 failf(data, "No associated connection");
1069 return CURLE_SEND_ERROR;
1070 }
1071 if(!data->conn->proto.ws) {
1072 failf(data, "Not a websocket transfer");
1073 return CURLE_SEND_ERROR;
1074 }
1075 ws = data->conn->proto.ws;
1076
1077 if(data->set.ws_raw_mode) {
1078 if(fragsize || flags) {
1079 DEBUGF(infof(data, "ws_send: "
1080 "fragsize and flags cannot be non-zero in raw mode"));
1081 return CURLE_BAD_FUNCTION_ARGUMENT;
1082 }
1083 if(!buflen)
1084 /* nothing to do */
1085 return CURLE_OK;
1086 /* raw mode sends exactly what was requested, and this is from within
1087 the write callback */
1088 if(Curl_is_in_callback(data)) {
1089 result = Curl_xfer_send(data, buffer, buflen, &nwritten);
1090 }
1091 else
1092 result = Curl_senddata(data, buffer, buflen, &nwritten);
1093
1094 infof(data, "WS: wanted to send %zu bytes, sent %zu bytes",
1095 buflen, nwritten);
1096 *sent = nwritten;
1097 return result;
1098 }
1099
1100 /* Not RAW mode, buf we do the frame encoding */
1101 result = ws_flush(data, ws, FALSE);
1102 if(result)
1103 return result;
1104
1105 /* TODO: the current design does not allow partial writes, afaict.
1106 * It is not clear how the application is supposed to react. */
1107 space = Curl_bufq_space(&ws->sendbuf);
1108 DEBUGF(infof(data, "curl_ws_send(len=%zu), sendbuf len=%zu space %zu",
1109 buflen, Curl_bufq_len(&ws->sendbuf), space));
1110 if(space < 14)
1111 return CURLE_AGAIN;
1112
1113 if(flags & CURLWS_OFFSET) {
1114 if(fragsize) {
1115 /* a frame series 'fragsize' bytes big, this is the first */
1116 n = ws_enc_write_head(data, &ws->enc, flags, fragsize,
1117 &ws->sendbuf, &result);
1118 if(n < 0)
1119 return result;
1120 }
1121 else {
1122 if((curl_off_t)buflen > ws->enc.payload_remain) {
1123 infof(data, "WS: unaligned frame size (sending %zu instead of %"
1124 CURL_FORMAT_CURL_OFF_T ")",
1125 buflen, ws->enc.payload_remain);
1126 }
1127 }
1128 }
1129 else if(!ws->enc.payload_remain) {
1130 n = ws_enc_write_head(data, &ws->enc, flags, (curl_off_t)buflen,
1131 &ws->sendbuf, &result);
1132 if(n < 0)
1133 return result;
1134 }
1135
1136 n = ws_enc_write_payload(&ws->enc, data,
1137 buffer, buflen, &ws->sendbuf, &result);
1138 if(n < 0)
1139 return result;
1140
1141 *sent = (size_t)n;
1142 return ws_flush(data, ws, TRUE);
1143 }
1144
ws_free(struct connectdata * conn)1145 static void ws_free(struct connectdata *conn)
1146 {
1147 if(conn && conn->proto.ws) {
1148 Curl_bufq_free(&conn->proto.ws->recvbuf);
1149 Curl_bufq_free(&conn->proto.ws->sendbuf);
1150 Curl_safefree(conn->proto.ws);
1151 }
1152 }
1153
ws_setup_conn(struct Curl_easy * data,struct connectdata * conn)1154 static CURLcode ws_setup_conn(struct Curl_easy *data,
1155 struct connectdata *conn)
1156 {
1157 /* websockets is 1.1 only (for now) */
1158 data->state.httpwant = CURL_HTTP_VERSION_1_1;
1159 return Curl_http_setup_conn(data, conn);
1160 }
1161
1162
ws_disconnect(struct Curl_easy * data,struct connectdata * conn,bool dead_connection)1163 static CURLcode ws_disconnect(struct Curl_easy *data,
1164 struct connectdata *conn,
1165 bool dead_connection)
1166 {
1167 (void)data;
1168 (void)dead_connection;
1169 ws_free(conn);
1170 return CURLE_OK;
1171 }
1172
curl_ws_meta(struct Curl_easy * data)1173 CURL_EXTERN const struct curl_ws_frame *curl_ws_meta(struct Curl_easy *data)
1174 {
1175 /* we only return something for websocket, called from within the callback
1176 when not using raw mode */
1177 if(GOOD_EASY_HANDLE(data) && Curl_is_in_callback(data) && data->conn &&
1178 data->conn->proto.ws && !data->set.ws_raw_mode)
1179 return &data->conn->proto.ws->frame;
1180 return NULL;
1181 }
1182
1183 const struct Curl_handler Curl_handler_ws = {
1184 "WS", /* scheme */
1185 ws_setup_conn, /* setup_connection */
1186 Curl_http, /* do_it */
1187 Curl_http_done, /* done */
1188 ZERO_NULL, /* do_more */
1189 Curl_http_connect, /* connect_it */
1190 ZERO_NULL, /* connecting */
1191 ZERO_NULL, /* doing */
1192 ZERO_NULL, /* proto_getsock */
1193 Curl_http_getsock_do, /* doing_getsock */
1194 ZERO_NULL, /* domore_getsock */
1195 ZERO_NULL, /* perform_getsock */
1196 ws_disconnect, /* disconnect */
1197 Curl_http_write_resp, /* write_resp */
1198 Curl_http_write_resp_hd, /* write_resp_hd */
1199 ZERO_NULL, /* connection_check */
1200 ZERO_NULL, /* attach connection */
1201 PORT_HTTP, /* defport */
1202 CURLPROTO_WS, /* protocol */
1203 CURLPROTO_HTTP, /* family */
1204 PROTOPT_CREDSPERREQUEST | /* flags */
1205 PROTOPT_USERPWDCTRL
1206 };
1207
1208 #ifdef USE_SSL
1209 const struct Curl_handler Curl_handler_wss = {
1210 "WSS", /* scheme */
1211 ws_setup_conn, /* setup_connection */
1212 Curl_http, /* do_it */
1213 Curl_http_done, /* done */
1214 ZERO_NULL, /* do_more */
1215 Curl_http_connect, /* connect_it */
1216 NULL, /* connecting */
1217 ZERO_NULL, /* doing */
1218 NULL, /* proto_getsock */
1219 Curl_http_getsock_do, /* doing_getsock */
1220 ZERO_NULL, /* domore_getsock */
1221 ZERO_NULL, /* perform_getsock */
1222 ws_disconnect, /* disconnect */
1223 Curl_http_write_resp, /* write_resp */
1224 Curl_http_write_resp_hd, /* write_resp_hd */
1225 ZERO_NULL, /* connection_check */
1226 ZERO_NULL, /* attach connection */
1227 PORT_HTTPS, /* defport */
1228 CURLPROTO_WSS, /* protocol */
1229 CURLPROTO_HTTP, /* family */
1230 PROTOPT_SSL | PROTOPT_CREDSPERREQUEST | /* flags */
1231 PROTOPT_USERPWDCTRL
1232 };
1233 #endif
1234
1235
1236 #else
1237
curl_ws_recv(CURL * curl,void * buffer,size_t buflen,size_t * nread,const struct curl_ws_frame ** metap)1238 CURL_EXTERN CURLcode curl_ws_recv(CURL *curl, void *buffer, size_t buflen,
1239 size_t *nread,
1240 const struct curl_ws_frame **metap)
1241 {
1242 (void)curl;
1243 (void)buffer;
1244 (void)buflen;
1245 (void)nread;
1246 (void)metap;
1247 return CURLE_NOT_BUILT_IN;
1248 }
1249
curl_ws_send(CURL * curl,const void * buffer,size_t buflen,size_t * sent,curl_off_t fragsize,unsigned int flags)1250 CURL_EXTERN CURLcode curl_ws_send(CURL *curl, const void *buffer,
1251 size_t buflen, size_t *sent,
1252 curl_off_t fragsize,
1253 unsigned int flags)
1254 {
1255 (void)curl;
1256 (void)buffer;
1257 (void)buflen;
1258 (void)sent;
1259 (void)fragsize;
1260 (void)flags;
1261 return CURLE_NOT_BUILT_IN;
1262 }
1263
curl_ws_meta(struct Curl_easy * data)1264 CURL_EXTERN const struct curl_ws_frame *curl_ws_meta(struct Curl_easy *data)
1265 {
1266 (void)data;
1267 return NULL;
1268 }
1269 #endif /* USE_WEBSOCKETS */
1270