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 free(randstr);
723 return CURLE_FAILED_INIT;
724 }
725 strcpy(keyval, randstr);
726 free(randstr);
727 for(i = 0; !result && (i < sizeof(heads)/sizeof(heads[0])); i++) {
728 if(!Curl_checkheaders(data, STRCONST(heads[i].name))) {
729 #ifdef USE_HYPER
730 char field[128];
731 msnprintf(field, sizeof(field), "%s %s", heads[i].name,
732 heads[i].val);
733 result = Curl_hyper_header(data, req, field);
734 #else
735 (void)data;
736 result = Curl_dyn_addf(req, "%s %s\r\n", heads[i].name,
737 heads[i].val);
738 #endif
739 }
740 }
741 k->upgr101 = UPGR101_WS;
742 return result;
743 }
744
745 /*
746 * 'nread' is number of bytes of websocket data already in the buffer at
747 * 'mem'.
748 */
Curl_ws_accept(struct Curl_easy * data,const char * mem,size_t nread)749 CURLcode Curl_ws_accept(struct Curl_easy *data,
750 const char *mem, size_t nread)
751 {
752 struct SingleRequest *k = &data->req;
753 struct websocket *ws;
754 struct Curl_cwriter *ws_dec_writer;
755 CURLcode result;
756
757 DEBUGASSERT(data->conn);
758 ws = data->conn->proto.ws;
759 if(!ws) {
760 size_t chunk_size = WS_CHUNK_SIZE;
761 ws = calloc(1, sizeof(*ws));
762 if(!ws)
763 return CURLE_OUT_OF_MEMORY;
764 data->conn->proto.ws = ws;
765 #ifdef DEBUGBUILD
766 {
767 char *p = getenv("CURL_WS_CHUNK_SIZE");
768 if(p) {
769 long l = strtol(p, NULL, 10);
770 if(l > 0 && l <= (1*1024*1024)) {
771 chunk_size = (size_t)l;
772 }
773 }
774 }
775 #endif
776 DEBUGF(infof(data, "WS, using chunk size %zu", chunk_size));
777 Curl_bufq_init2(&ws->recvbuf, chunk_size, WS_CHUNK_COUNT,
778 BUFQ_OPT_SOFT_LIMIT);
779 Curl_bufq_init2(&ws->sendbuf, chunk_size, WS_CHUNK_COUNT,
780 BUFQ_OPT_SOFT_LIMIT);
781 ws_dec_init(&ws->dec);
782 ws_enc_init(&ws->enc);
783 }
784 else {
785 Curl_bufq_reset(&ws->recvbuf);
786 ws_dec_reset(&ws->dec);
787 ws_enc_reset(&ws->enc);
788 }
789 /* Verify the Sec-WebSocket-Accept response.
790
791 The sent value is the base64 encoded version of a SHA-1 hash done on the
792 |Sec-WebSocket-Key| header field concatenated with
793 the string "258EAFA5-E914-47DA-95CA-C5AB0DC85B11".
794 */
795
796 /* If the response includes a |Sec-WebSocket-Extensions| header field and
797 this header field indicates the use of an extension that was not present
798 in the client's handshake (the server has indicated an extension not
799 requested by the client), the client MUST Fail the WebSocket Connection.
800 */
801
802 /* If the response includes a |Sec-WebSocket-Protocol| header field
803 and this header field indicates the use of a subprotocol that was
804 not present in the client's handshake (the server has indicated a
805 subprotocol not requested by the client), the client MUST Fail
806 the WebSocket Connection. */
807
808 /* 4 bytes random */
809
810 result = Curl_rand(data, (unsigned char *)&ws->enc.mask,
811 sizeof(ws->enc.mask));
812 if(result)
813 return result;
814 infof(data, "Received 101, switch to WebSocket; mask %02x%02x%02x%02x",
815 ws->enc.mask[0], ws->enc.mask[1], ws->enc.mask[2], ws->enc.mask[3]);
816
817 /* Install our client writer that decodes WS frames payload */
818 result = Curl_cwriter_create(&ws_dec_writer, data, &ws_cw_decode,
819 CURL_CW_CONTENT_DECODE);
820 if(result)
821 return result;
822
823 result = Curl_cwriter_add(data, ws_dec_writer);
824 if(result) {
825 Curl_cwriter_free(data, ws_dec_writer);
826 return result;
827 }
828
829 if(data->set.connect_only) {
830 ssize_t nwritten;
831 /* In CONNECT_ONLY setup, the payloads from `mem` need to be received
832 * when using `curl_ws_recv` later on after this transfer is already
833 * marked as DONE. */
834 nwritten = Curl_bufq_write(&ws->recvbuf, (const unsigned char *)mem,
835 nread, &result);
836 if(nwritten < 0)
837 return result;
838 infof(data, "%zu bytes websocket payload", nread);
839 }
840 else { /* !connect_only */
841 /* And pass any additional data to the writers */
842 if(nread) {
843 result = Curl_client_write(data, CLIENTWRITE_BODY, (char *)mem, nread);
844 }
845 }
846 k->upgr101 = UPGR101_RECEIVED;
847
848 return result;
849 }
850
851 struct ws_collect {
852 struct Curl_easy *data;
853 unsigned char *buffer;
854 size_t buflen;
855 size_t bufidx;
856 int frame_age;
857 int frame_flags;
858 curl_off_t payload_offset;
859 curl_off_t payload_len;
860 bool written;
861 };
862
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)863 static ssize_t ws_client_collect(const unsigned char *buf, size_t buflen,
864 int frame_age, int frame_flags,
865 curl_off_t payload_offset,
866 curl_off_t payload_len,
867 void *userp,
868 CURLcode *err)
869 {
870 struct ws_collect *ctx = userp;
871 size_t nwritten;
872 curl_off_t remain = (payload_len - (payload_offset + buflen));
873
874 if(!ctx->bufidx) {
875 /* first write */
876 ctx->frame_age = frame_age;
877 ctx->frame_flags = frame_flags;
878 ctx->payload_offset = payload_offset;
879 ctx->payload_len = payload_len;
880 }
881
882 if((frame_flags & CURLWS_PING) && !remain) {
883 /* auto-respond to PINGs, only works for single-frame payloads atm */
884 size_t bytes;
885 infof(ctx->data, "WS: auto-respond to PING with a PONG");
886 /* send back the exact same content as a PONG */
887 *err = curl_ws_send(ctx->data, buf, buflen, &bytes, 0, CURLWS_PONG);
888 if(*err)
889 return -1;
890 nwritten = bytes;
891 }
892 else {
893 ctx->written = TRUE;
894 DEBUGASSERT(ctx->buflen >= ctx->bufidx);
895 nwritten = CURLMIN(buflen, ctx->buflen - ctx->bufidx);
896 if(!nwritten) {
897 if(!buflen) { /* 0 length write, we accept that */
898 *err = CURLE_OK;
899 return 0;
900 }
901 *err = CURLE_AGAIN; /* no more space */
902 return -1;
903 }
904 *err = CURLE_OK;
905 memcpy(ctx->buffer + ctx->bufidx, buf, nwritten);
906 ctx->bufidx += nwritten;
907 }
908 return nwritten;
909 }
910
nw_in_recv(void * reader_ctx,unsigned char * buf,size_t buflen,CURLcode * err)911 static ssize_t nw_in_recv(void *reader_ctx,
912 unsigned char *buf, size_t buflen,
913 CURLcode *err)
914 {
915 struct Curl_easy *data = reader_ctx;
916 size_t nread;
917
918 *err = curl_easy_recv(data, buf, buflen, &nread);
919 if(*err)
920 return -1;
921 return (ssize_t)nread;
922 }
923
curl_ws_recv(struct Curl_easy * data,void * buffer,size_t buflen,size_t * nread,const struct curl_ws_frame ** metap)924 CURL_EXTERN CURLcode curl_ws_recv(struct Curl_easy *data, void *buffer,
925 size_t buflen, size_t *nread,
926 const struct curl_ws_frame **metap)
927 {
928 struct connectdata *conn = data->conn;
929 struct websocket *ws;
930 bool done = FALSE; /* not filled passed buffer yet */
931 struct ws_collect ctx;
932 CURLcode result;
933
934 if(!conn) {
935 /* Unhappy hack with lifetimes of transfers and connection */
936 if(!data->set.connect_only) {
937 failf(data, "CONNECT_ONLY is required");
938 return CURLE_UNSUPPORTED_PROTOCOL;
939 }
940
941 Curl_getconnectinfo(data, &conn);
942 if(!conn) {
943 failf(data, "connection not found");
944 return CURLE_BAD_FUNCTION_ARGUMENT;
945 }
946 }
947 ws = conn->proto.ws;
948 if(!ws) {
949 failf(data, "connection is not setup for websocket");
950 return CURLE_BAD_FUNCTION_ARGUMENT;
951 }
952
953 *nread = 0;
954 *metap = NULL;
955
956 memset(&ctx, 0, sizeof(ctx));
957 ctx.data = data;
958 ctx.buffer = buffer;
959 ctx.buflen = buflen;
960
961 while(!done) {
962 /* receive more when our buffer is empty */
963 if(Curl_bufq_is_empty(&ws->recvbuf)) {
964 ssize_t n = Curl_bufq_slurp(&ws->recvbuf, nw_in_recv, data, &result);
965 if(n < 0) {
966 return result;
967 }
968 else if(n == 0) {
969 /* connection closed */
970 infof(data, "connection expectedly closed?");
971 return CURLE_GOT_NOTHING;
972 }
973 DEBUGF(infof(data, "curl_ws_recv, added %zu bytes from network",
974 Curl_bufq_len(&ws->recvbuf)));
975 }
976
977 result = ws_dec_pass(&ws->dec, data, &ws->recvbuf,
978 ws_client_collect, &ctx);
979 if(result == CURLE_AGAIN) {
980 if(!ctx.written) {
981 ws_dec_info(&ws->dec, data, "need more input");
982 continue; /* nothing written, try more input */
983 }
984 done = TRUE;
985 break;
986 }
987 else if(result) {
988 return result;
989 }
990 else if(ctx.written) {
991 /* The decoded frame is passed back to our caller.
992 * There are frames like PING were we auto-respond to and
993 * that we do not return. For these `ctx.written` is not set. */
994 done = TRUE;
995 break;
996 }
997 }
998
999 /* update frame information to be passed back */
1000 update_meta(ws, ctx.frame_age, ctx.frame_flags, ctx.payload_offset,
1001 ctx.payload_len, ctx.bufidx);
1002 *metap = &ws->frame;
1003 *nread = ws->frame.len;
1004 /* infof(data, "curl_ws_recv(len=%zu) -> %zu bytes (frame at %"
1005 CURL_FORMAT_CURL_OFF_T ", %" CURL_FORMAT_CURL_OFF_T " left)",
1006 buflen, *nread, ws->frame.offset, ws->frame.bytesleft); */
1007 return CURLE_OK;
1008 }
1009
ws_flush(struct Curl_easy * data,struct websocket * ws,bool complete)1010 static CURLcode ws_flush(struct Curl_easy *data, struct websocket *ws,
1011 bool complete)
1012 {
1013 if(!Curl_bufq_is_empty(&ws->sendbuf)) {
1014 CURLcode result;
1015 const unsigned char *out;
1016 size_t outlen, n;
1017
1018 while(Curl_bufq_peek(&ws->sendbuf, &out, &outlen)) {
1019 if(data->set.connect_only)
1020 result = Curl_senddata(data, out, outlen, &n);
1021 else {
1022 result = Curl_xfer_send(data, out, outlen, &n);
1023 if(!result && !n && outlen)
1024 result = CURLE_AGAIN;
1025 }
1026
1027 if(result) {
1028 if(result == CURLE_AGAIN) {
1029 if(!complete) {
1030 infof(data, "WS: flush EAGAIN, %zu bytes remain in buffer",
1031 Curl_bufq_len(&ws->sendbuf));
1032 return result;
1033 }
1034 /* TODO: the current design does not allow for buffered writes.
1035 * We need to flush the buffer now. There is no ws_flush() later */
1036 n = 0;
1037 continue;
1038 }
1039 else if(result) {
1040 failf(data, "WS: flush, write error %d", result);
1041 return result;
1042 }
1043 }
1044 else {
1045 infof(data, "WS: flushed %zu bytes", n);
1046 Curl_bufq_skip(&ws->sendbuf, n);
1047 }
1048 }
1049 }
1050 return CURLE_OK;
1051 }
1052
curl_ws_send(CURL * data,const void * buffer,size_t buflen,size_t * sent,curl_off_t fragsize,unsigned int flags)1053 CURL_EXTERN CURLcode curl_ws_send(CURL *data, const void *buffer,
1054 size_t buflen, size_t *sent,
1055 curl_off_t fragsize,
1056 unsigned int flags)
1057 {
1058 struct websocket *ws;
1059 ssize_t n;
1060 size_t nwritten, space;
1061 CURLcode result;
1062
1063 *sent = 0;
1064 if(!data->conn && data->set.connect_only) {
1065 result = Curl_connect_only_attach(data);
1066 if(result)
1067 return result;
1068 }
1069 if(!data->conn) {
1070 failf(data, "No associated connection");
1071 return CURLE_SEND_ERROR;
1072 }
1073 if(!data->conn->proto.ws) {
1074 failf(data, "Not a websocket transfer");
1075 return CURLE_SEND_ERROR;
1076 }
1077 ws = data->conn->proto.ws;
1078
1079 if(data->set.ws_raw_mode) {
1080 if(fragsize || flags) {
1081 DEBUGF(infof(data, "ws_send: "
1082 "fragsize and flags cannot be non-zero in raw mode"));
1083 return CURLE_BAD_FUNCTION_ARGUMENT;
1084 }
1085 if(!buflen)
1086 /* nothing to do */
1087 return CURLE_OK;
1088 /* raw mode sends exactly what was requested, and this is from within
1089 the write callback */
1090 if(Curl_is_in_callback(data)) {
1091 result = Curl_xfer_send(data, buffer, buflen, &nwritten);
1092 }
1093 else
1094 result = Curl_senddata(data, buffer, buflen, &nwritten);
1095
1096 infof(data, "WS: wanted to send %zu bytes, sent %zu bytes",
1097 buflen, nwritten);
1098 *sent = nwritten;
1099 return result;
1100 }
1101
1102 /* Not RAW mode, buf we do the frame encoding */
1103 result = ws_flush(data, ws, FALSE);
1104 if(result)
1105 return result;
1106
1107 /* TODO: the current design does not allow partial writes, afaict.
1108 * It is not clear how the application is supposed to react. */
1109 space = Curl_bufq_space(&ws->sendbuf);
1110 DEBUGF(infof(data, "curl_ws_send(len=%zu), sendbuf len=%zu space %zu",
1111 buflen, Curl_bufq_len(&ws->sendbuf), space));
1112 if(space < 14)
1113 return CURLE_AGAIN;
1114
1115 if(flags & CURLWS_OFFSET) {
1116 if(fragsize) {
1117 /* a frame series 'fragsize' bytes big, this is the first */
1118 n = ws_enc_write_head(data, &ws->enc, flags, fragsize,
1119 &ws->sendbuf, &result);
1120 if(n < 0)
1121 return result;
1122 }
1123 else {
1124 if((curl_off_t)buflen > ws->enc.payload_remain) {
1125 infof(data, "WS: unaligned frame size (sending %zu instead of %"
1126 CURL_FORMAT_CURL_OFF_T ")",
1127 buflen, ws->enc.payload_remain);
1128 }
1129 }
1130 }
1131 else if(!ws->enc.payload_remain) {
1132 n = ws_enc_write_head(data, &ws->enc, flags, (curl_off_t)buflen,
1133 &ws->sendbuf, &result);
1134 if(n < 0)
1135 return result;
1136 }
1137
1138 n = ws_enc_write_payload(&ws->enc, data,
1139 buffer, buflen, &ws->sendbuf, &result);
1140 if(n < 0)
1141 return result;
1142
1143 *sent = (size_t)n;
1144 return ws_flush(data, ws, TRUE);
1145 }
1146
ws_free(struct connectdata * conn)1147 static void ws_free(struct connectdata *conn)
1148 {
1149 if(conn && conn->proto.ws) {
1150 Curl_bufq_free(&conn->proto.ws->recvbuf);
1151 Curl_bufq_free(&conn->proto.ws->sendbuf);
1152 Curl_safefree(conn->proto.ws);
1153 }
1154 }
1155
ws_setup_conn(struct Curl_easy * data,struct connectdata * conn)1156 static CURLcode ws_setup_conn(struct Curl_easy *data,
1157 struct connectdata *conn)
1158 {
1159 /* websockets is 1.1 only (for now) */
1160 data->state.httpwant = CURL_HTTP_VERSION_1_1;
1161 return Curl_http_setup_conn(data, conn);
1162 }
1163
1164
ws_disconnect(struct Curl_easy * data,struct connectdata * conn,bool dead_connection)1165 static CURLcode ws_disconnect(struct Curl_easy *data,
1166 struct connectdata *conn,
1167 bool dead_connection)
1168 {
1169 (void)data;
1170 (void)dead_connection;
1171 ws_free(conn);
1172 return CURLE_OK;
1173 }
1174
curl_ws_meta(struct Curl_easy * data)1175 CURL_EXTERN const struct curl_ws_frame *curl_ws_meta(struct Curl_easy *data)
1176 {
1177 /* we only return something for websocket, called from within the callback
1178 when not using raw mode */
1179 if(GOOD_EASY_HANDLE(data) && Curl_is_in_callback(data) && data->conn &&
1180 data->conn->proto.ws && !data->set.ws_raw_mode)
1181 return &data->conn->proto.ws->frame;
1182 return NULL;
1183 }
1184
1185 const struct Curl_handler Curl_handler_ws = {
1186 "WS", /* scheme */
1187 ws_setup_conn, /* setup_connection */
1188 Curl_http, /* do_it */
1189 Curl_http_done, /* done */
1190 ZERO_NULL, /* do_more */
1191 Curl_http_connect, /* connect_it */
1192 ZERO_NULL, /* connecting */
1193 ZERO_NULL, /* doing */
1194 ZERO_NULL, /* proto_getsock */
1195 Curl_http_getsock_do, /* doing_getsock */
1196 ZERO_NULL, /* domore_getsock */
1197 ZERO_NULL, /* perform_getsock */
1198 ws_disconnect, /* disconnect */
1199 Curl_http_write_resp, /* write_resp */
1200 Curl_http_write_resp_hd, /* write_resp_hd */
1201 ZERO_NULL, /* connection_check */
1202 ZERO_NULL, /* attach connection */
1203 PORT_HTTP, /* defport */
1204 CURLPROTO_WS, /* protocol */
1205 CURLPROTO_HTTP, /* family */
1206 PROTOPT_CREDSPERREQUEST | /* flags */
1207 PROTOPT_USERPWDCTRL
1208 };
1209
1210 #ifdef USE_SSL
1211 const struct Curl_handler Curl_handler_wss = {
1212 "WSS", /* scheme */
1213 ws_setup_conn, /* setup_connection */
1214 Curl_http, /* do_it */
1215 Curl_http_done, /* done */
1216 ZERO_NULL, /* do_more */
1217 Curl_http_connect, /* connect_it */
1218 NULL, /* connecting */
1219 ZERO_NULL, /* doing */
1220 NULL, /* proto_getsock */
1221 Curl_http_getsock_do, /* doing_getsock */
1222 ZERO_NULL, /* domore_getsock */
1223 ZERO_NULL, /* perform_getsock */
1224 ws_disconnect, /* disconnect */
1225 Curl_http_write_resp, /* write_resp */
1226 Curl_http_write_resp_hd, /* write_resp_hd */
1227 ZERO_NULL, /* connection_check */
1228 ZERO_NULL, /* attach connection */
1229 PORT_HTTPS, /* defport */
1230 CURLPROTO_WSS, /* protocol */
1231 CURLPROTO_HTTP, /* family */
1232 PROTOPT_SSL | PROTOPT_CREDSPERREQUEST | /* flags */
1233 PROTOPT_USERPWDCTRL
1234 };
1235 #endif
1236
1237
1238 #else
1239
curl_ws_recv(CURL * curl,void * buffer,size_t buflen,size_t * nread,const struct curl_ws_frame ** metap)1240 CURL_EXTERN CURLcode curl_ws_recv(CURL *curl, void *buffer, size_t buflen,
1241 size_t *nread,
1242 const struct curl_ws_frame **metap)
1243 {
1244 (void)curl;
1245 (void)buffer;
1246 (void)buflen;
1247 (void)nread;
1248 (void)metap;
1249 return CURLE_NOT_BUILT_IN;
1250 }
1251
curl_ws_send(CURL * curl,const void * buffer,size_t buflen,size_t * sent,curl_off_t fragsize,unsigned int flags)1252 CURL_EXTERN CURLcode curl_ws_send(CURL *curl, const void *buffer,
1253 size_t buflen, size_t *sent,
1254 curl_off_t fragsize,
1255 unsigned int flags)
1256 {
1257 (void)curl;
1258 (void)buffer;
1259 (void)buflen;
1260 (void)sent;
1261 (void)fragsize;
1262 (void)flags;
1263 return CURLE_NOT_BUILT_IN;
1264 }
1265
curl_ws_meta(struct Curl_easy * data)1266 CURL_EXTERN const struct curl_ws_frame *curl_ws_meta(struct Curl_easy *data)
1267 {
1268 (void)data;
1269 return NULL;
1270 }
1271 #endif /* USE_WEBSOCKETS */
1272