comparison seobeo/s_websocket.c @ 120:cbbf78b17cfa

[Seobeo][Websocket] Created Web socket client logic.
author June Park <parkjune1995@gmail.com>
date Thu, 08 Jan 2026 03:19:59 -0800
parents
children 7b1719fa918c
comparison
equal deleted inserted replaced
119:c39582f937e5 120:cbbf78b17cfa
1 #include "seobeo/seobeo.h"
2 #include <time.h>
3
4 #define SEOBEO_WS_GUID "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
5 #define MAX_INT_16 65536
6 #define MAX_INT_64 18446744073709551615
7
8 static void Seobeo_WebSocket_Parse_Url(const char *url, char **p_host, char **p_port, char **p_path, boolean *p_use_tls, Dowa_Arena *p_arena)
9 {
10 if (!url)
11 return;
12
13 const char *start = url;
14 *p_use_tls = FALSE;
15
16 if (strncmp(url, "wss://", 6) == 0)
17 {
18 *p_use_tls = TRUE;
19 start = url + 6;
20 }
21 else if (strncmp(url, "ws://", 5) == 0)
22 {
23 *p_use_tls = FALSE;
24 start = url + 5;
25 }
26
27 const char *slash = strchr(start, '/');
28 const char *colon = strchr(start, ':');
29
30 if (colon && (!slash || colon < slash))
31 {
32 size_t host_len = colon - start;
33 *p_host = Dowa_Arena_Allocate(p_arena, host_len + 1);
34 memcpy(*p_host, start, host_len);
35 (*p_host)[host_len] = '\0';
36
37 const char *port_start = colon + 1;
38 size_t port_len = slash ? (slash - port_start) : strlen(port_start);
39 *p_port = Dowa_Arena_Allocate(p_arena, port_len + 1);
40 memcpy(*p_port, port_start, port_len);
41 (*p_port)[port_len] = '\0';
42 }
43 else
44 {
45 size_t host_len = slash ? (slash - start) : strlen(start);
46 *p_host = Dowa_Arena_Allocate(p_arena, host_len + 1);
47 memcpy(*p_host, start, host_len);
48 (*p_host)[host_len] = '\0';
49
50 *p_port = Dowa_Arena_Allocate(p_arena, 8);
51 strcpy(*p_port, *p_use_tls ? "443" : "80");
52 }
53
54 if (slash)
55 {
56 size_t path_len = strlen(slash);
57 *p_path = Dowa_Arena_Allocate(p_arena, path_len + 1);
58 strcpy(*p_path, slash);
59 }
60 else
61 {
62 *p_path = Dowa_Arena_Allocate(p_arena, 2);
63 strcpy(*p_path, "/");
64 }
65 }
66
67 static void Seobeo_WebSocket_Generate_Key(char *key_out, size_t key_size)
68 {
69 static const char base64_chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
70
71 srand((unsigned int)time(NULL) ^ (unsigned int)getpid());
72
73 uint8 random_bytes[16];
74 for (int i = 0; i < 16; i++)
75 random_bytes[i] = (uint8)(rand() % 256);
76
77 int out_idx = 0;
78 for (int i = 0; i < 16; i += 3)
79 {
80 uint32 triple = (random_bytes[i] << 16) |
81 (i + 1 < 16 ? random_bytes[i + 1] << 8 : 0) |
82 (i + 2 < 16 ? random_bytes[i + 2] : 0);
83
84 key_out[out_idx++] = base64_chars[(triple >> 18) & 0x3F];
85 key_out[out_idx++] = base64_chars[(triple >> 12) & 0x3F];
86 key_out[out_idx++] = base64_chars[(triple >> 6) & 0x3F];
87 key_out[out_idx++] = base64_chars[triple & 0x3F];
88 }
89
90 key_out[out_idx] = '\0';
91 }
92
93 Seobeo_WebSocket *Seobeo_WebSocket_Connect(const char *url)
94 {
95 Seobeo_WebSocket *p_ws = malloc(sizeof(Seobeo_WebSocket));
96 if (!p_ws)
97 return NULL;
98
99 memset(p_ws, 0, sizeof(Seobeo_WebSocket));
100
101 p_ws->p_arena = Dowa_Arena_Create(1024 * 1024);
102 if (!p_ws->p_arena)
103 {
104 free(p_ws);
105 return NULL;
106 }
107
108 size_t url_len = strlen(url);
109 p_ws->url = Dowa_Arena_Allocate(p_ws->p_arena, url_len + 1);
110 strcpy(p_ws->url, url);
111
112 Seobeo_WebSocket_Parse_Url(url, &p_ws->host, &p_ws->port, &p_ws->path, &p_ws->use_tls, p_ws->p_arena);
113
114 p_ws->p_handle = Seobeo_Stream_Handle_Client_Create(p_ws->host, p_ws->port, p_ws->use_tls);
115 if (!p_ws->p_handle || p_ws->p_handle->socket < 0)
116 {
117 Seobeo_Log(SEOBEO_ERROR, "Failed to create socket connection\n");
118 if (p_ws->p_handle)
119 Seobeo_Handle_Destroy(p_ws->p_handle);
120 Dowa_Arena_Free(p_ws->p_arena);
121 free(p_ws);
122 return NULL;
123 }
124
125 char ws_key[32];
126 Seobeo_WebSocket_Generate_Key(ws_key, sizeof(ws_key));
127
128 char handshake[2048];
129 int handshake_len = snprintf(handshake, sizeof(handshake),
130 "GET %s HTTP/1.1\r\n"
131 "Host: %s\r\n"
132 "Upgrade: websocket\r\n"
133 "Connection: Upgrade\r\n"
134 "Sec-WebSocket-Key: %s\r\n"
135 "Sec-WebSocket-Version: 13\r\n"
136 "\r\n",
137 p_ws->path, p_ws->host, ws_key);
138
139 Seobeo_Handle_Queue(p_ws->p_handle, (uint8*)handshake, (uint32)handshake_len);
140 if (Seobeo_Handle_Flush(p_ws->p_handle) < 0)
141 {
142 Seobeo_Log(SEOBEO_ERROR, "Failed to send WebSocket handshake\n");
143 Seobeo_Handle_Destroy(p_ws->p_handle);
144 Dowa_Arena_Free(p_ws->p_arena);
145 free(p_ws);
146 return NULL;
147 }
148
149 while (1)
150 {
151 int r = Seobeo_Handle_Read(p_ws->p_handle);
152 if (r < 0)
153 {
154 Seobeo_Log(SEOBEO_ERROR, "Failed to read handshake response\n");
155 Seobeo_Handle_Destroy(p_ws->p_handle);
156 Dowa_Arena_Free(p_ws->p_arena);
157 free(p_ws);
158 return NULL;
159 }
160
161 if (p_ws->p_handle->read_buffer_len >= 4 &&
162 strstr((char*)p_ws->p_handle->read_buffer, "\r\n\r\n") != NULL)
163 break;
164
165 if (r == 0)
166 continue;
167 }
168
169 char *response = (char*)p_ws->p_handle->read_buffer;
170 if (strstr(response, "HTTP/1.1 101") == NULL)
171 {
172 Seobeo_Log(SEOBEO_ERROR, "WebSocket handshake failed: %s\n", response);
173 Seobeo_Handle_Destroy(p_ws->p_handle);
174 Dowa_Arena_Free(p_ws->p_arena);
175 free(p_ws);
176 return NULL;
177 }
178
179 char *end_of_headers = strstr(response, "\r\n\r\n");
180 if (end_of_headers)
181 {
182 size_t header_len = end_of_headers - response + 4;
183 Seobeo_Handle_Consume(p_ws->p_handle, (uint32)header_len);
184 }
185
186 p_ws->state = SEOBEO_WS_STATE_OPEN;
187 p_ws->fragment_capacity = 4096;
188 p_ws->fragment_buffer = malloc(p_ws->fragment_capacity);
189
190 Seobeo_Log(SEOBEO_INFO, "WebSocket connected to %s\n", url);
191
192 return p_ws;
193 }
194
195 static void Seobeo_WebSocket_Mask_Data(uint8 *data, size_t length, const uint8 *mask)
196 {
197 for (size_t i = 0; i < length; i++)
198 data[i] ^= mask[i % 4];
199 }
200
201 static int32 Seobeo_WebSocket_Send_Frame(Seobeo_WebSocket *p_ws, Seobeo_WebSocket_Opcode opcode,
202 const uint8 *payload, size_t payload_length, boolean fin)
203 {
204 if (!p_ws || p_ws->state != SEOBEO_WS_STATE_OPEN)
205 return -1;
206
207 uint8 frame[14];
208 size_t frame_len = 0;
209
210 // Big endian
211 frame[0] = (fin ? 0x80 : 0x00) | (opcode & 0x0F);
212 frame_len++;
213
214 uint8 mask_key[4];
215 for (int i = 0; i < 4; i++)
216 mask_key[i] = (uint8)(rand() % 256);
217
218 // within 1 byte
219 if (payload_length < 126)
220 {
221 frame[1] = 0x80 | (uint8)payload_length;
222 frame_len++;
223 }
224 // from now frame 1 is thrown away just keeping it 1
225 // within 4 bytes
226 else if (payload_length < MAX_INT_16)
227 {
228 frame[1] = 0x80 | 126;
229 frame[2] = (uint8)((payload_length >> 8) & 0xFF);
230 frame[3] = (uint8)(payload_length & 0xFF);
231 frame_len += 3;
232 }
233 // within 8 bytes
234 else
235 {
236 frame[1] = 0x80 | 127;
237 for (int i = 0; i < 8; i++)
238 frame[2 + i] = (uint8)((payload_length >> (56 - i * 8)) & 0xFF);
239 frame_len += 9;
240 }
241
242 memcpy(frame + frame_len, mask_key, 4);
243 frame_len += 4;
244
245 Seobeo_Handle_Queue(p_ws->p_handle, frame, (uint32)frame_len);
246
247 if (payload_length > 0)
248 {
249 uint8 *masked_payload = malloc(payload_length);
250 memcpy(masked_payload, payload, payload_length);
251 Seobeo_WebSocket_Mask_Data(masked_payload, payload_length, mask_key);
252 Seobeo_Handle_Queue(p_ws->p_handle, masked_payload, (uint32)payload_length);
253 free(masked_payload);
254 }
255
256 return Seobeo_Handle_Flush(p_ws->p_handle);
257 }
258
259 static int32 Seobeo_WebSocket_Send_Fragmented(Seobeo_WebSocket *p_ws, Seobeo_WebSocket_Opcode opcode, const uint8 *payload, size_t total_length)
260 {
261 if (!payload || total_length == 0)
262 return -1;
263
264 const size_t max_fragment_size = 1024 * 1024;
265
266 if (total_length <= max_fragment_size)
267 return Seobeo_WebSocket_Send_Frame(p_ws, opcode, payload, total_length, TRUE);
268
269 size_t sent = 0;
270 int32 result;
271
272 result = Seobeo_WebSocket_Send_Frame(p_ws, opcode, payload, max_fragment_size, FALSE);
273 if (result < 0)
274 return result;
275
276 sent += max_fragment_size;
277
278 while (sent + max_fragment_size < total_length)
279 {
280 result = Seobeo_WebSocket_Send_Frame(p_ws, SEOBEO_WS_OPCODE_CONTINUATION, payload + sent, max_fragment_size, FALSE);
281 if (result < 0)
282 return result;
283 sent += max_fragment_size;
284 }
285
286 size_t remaining = total_length - sent;
287 return Seobeo_WebSocket_Send_Frame(p_ws, SEOBEO_WS_OPCODE_CONTINUATION, payload + sent, remaining, TRUE);
288 }
289
290 int32 Seobeo_WebSocket_Send_Text(Seobeo_WebSocket *p_ws, const char *text)
291 {
292 if (!text)
293 return -1;
294
295 return Seobeo_WebSocket_Send_Fragmented(p_ws, SEOBEO_WS_OPCODE_TEXT, (const uint8*)text, strlen(text));
296 }
297
298 int32 Seobeo_WebSocket_Send_Binary(Seobeo_WebSocket *p_ws, const uint8 *data, size_t length)
299 {
300 if (!data)
301 return -1;
302
303 return Seobeo_WebSocket_Send_Fragmented(p_ws, SEOBEO_WS_OPCODE_BINARY, data, length);
304 }
305
306 int32 Seobeo_WebSocket_Send_Ping(Seobeo_WebSocket *p_ws, const char *payload)
307 {
308 size_t len = payload ? strlen(payload) : 0;
309 return Seobeo_WebSocket_Send_Frame(p_ws, SEOBEO_WS_OPCODE_PING, (const uint8*)payload, len, TRUE);
310 }
311
312 int32 Seobeo_WebSocket_Send_Pong(Seobeo_WebSocket *p_ws, const char *payload)
313 {
314 size_t len = payload ? strlen(payload) : 0;
315 return Seobeo_WebSocket_Send_Frame(p_ws, SEOBEO_WS_OPCODE_PONG, (const uint8*)payload, len, TRUE);
316 }
317
318 Seobeo_WebSocket_Message *Seobeo_WebSocket_Receive(Seobeo_WebSocket *p_ws)
319 {
320 if (!p_ws || p_ws->state == SEOBEO_WS_STATE_CLOSED)
321 return NULL;
322
323 int r = Seobeo_Handle_Read(p_ws->p_handle);
324 if (r < 0)
325 {
326 Seobeo_Log(SEOBEO_ERROR, "WebSocket read error\n");
327 p_ws->state = SEOBEO_WS_STATE_CLOSED;
328 return NULL;
329 }
330
331 if (r == -2)
332 {
333 Seobeo_Log(SEOBEO_INFO, "WebSocket connection closed\n");
334 p_ws->state = SEOBEO_WS_STATE_CLOSED;
335 return NULL;
336 }
337
338 if (p_ws->p_handle->read_buffer_len < 2)
339 return NULL;
340
341 uint8 *buf = p_ws->p_handle->read_buffer;
342
343 uint8 byte1 = buf[0];
344 uint8 byte2 = buf[1];
345
346 boolean fin = (byte1 & 0x80) != 0;
347 Seobeo_WebSocket_Opcode opcode = (Seobeo_WebSocket_Opcode)(byte1 & 0x0F);
348 boolean masked = (byte2 & 0x80) != 0;
349 uint64 payload_len = byte2 & 0x7F;
350
351 size_t header_len = 2;
352
353 if (payload_len == 126)
354 {
355 if (p_ws->p_handle->read_buffer_len < 4)
356 return NULL;
357 payload_len = (buf[2] << 8) | buf[3];
358 header_len += 2;
359 }
360 else if (payload_len == 127)
361 {
362 if (p_ws->p_handle->read_buffer_len < 10)
363 return NULL;
364 payload_len = 0;
365 for (int i = 0; i < 8; i++)
366 payload_len = (payload_len << 8) | buf[2 + i];
367 header_len += 8;
368 }
369
370 uint8 mask_key[4] = {0};
371 if (masked)
372 {
373 if (p_ws->p_handle->read_buffer_len < header_len + 4)
374 return NULL;
375 memcpy(mask_key, buf + header_len, 4);
376 header_len += 4;
377 }
378
379 if (p_ws->p_handle->read_buffer_len < header_len + payload_len)
380 return NULL;
381
382 uint8 *payload = NULL;
383 if (payload_len > 0)
384 {
385 payload = malloc(payload_len);
386 memcpy(payload, buf + header_len, payload_len);
387
388 if (masked)
389 Seobeo_WebSocket_Mask_Data(payload, payload_len, mask_key);
390 }
391
392 Seobeo_Handle_Consume(p_ws->p_handle, (uint32)(header_len + payload_len));
393
394 if (opcode == SEOBEO_WS_OPCODE_PING)
395 {
396 Seobeo_WebSocket_Send_Pong(p_ws, (char*)payload);
397 if (payload)
398 free(payload);
399 return NULL;
400 }
401
402 if (opcode == SEOBEO_WS_OPCODE_PONG)
403 {
404 if (payload)
405 free(payload);
406 return NULL;
407 }
408
409 if (opcode == SEOBEO_WS_OPCODE_CLOSE)
410 {
411 uint16 close_code = 1000;
412 if (payload_len >= 2)
413 close_code = (payload[0] << 8) | payload[1];
414
415 Seobeo_Log(SEOBEO_INFO, "WebSocket close received with code %d\n", close_code);
416 p_ws->state = SEOBEO_WS_STATE_CLOSING;
417
418 Seobeo_WebSocket_Send_Frame(p_ws, SEOBEO_WS_OPCODE_CLOSE, payload, payload_len, TRUE);
419 p_ws->state = SEOBEO_WS_STATE_CLOSED;
420
421 if (payload)
422 free(payload);
423 return NULL;
424 }
425
426 if (opcode == SEOBEO_WS_OPCODE_CONTINUATION)
427 {
428 if (p_ws->fragment_length + payload_len > p_ws->fragment_capacity)
429 {
430 p_ws->fragment_capacity = (p_ws->fragment_length + payload_len) * 2;
431 p_ws->fragment_buffer = realloc(p_ws->fragment_buffer, p_ws->fragment_capacity);
432 }
433
434 if (payload_len > 0)
435 {
436 memcpy(p_ws->fragment_buffer + p_ws->fragment_length, payload, payload_len);
437 p_ws->fragment_length += payload_len;
438 }
439
440 if (payload)
441 free(payload);
442
443 if (!fin)
444 return NULL;
445
446 Seobeo_WebSocket_Message *p_msg = malloc(sizeof(Seobeo_WebSocket_Message));
447 p_msg->opcode = p_ws->fragment_opcode;
448 p_msg->data = malloc(p_ws->fragment_length);
449 memcpy(p_msg->data, p_ws->fragment_buffer, p_ws->fragment_length);
450 p_msg->length = p_ws->fragment_length;
451 p_msg->is_final = TRUE;
452
453 p_ws->fragment_length = 0;
454
455 return p_msg;
456 }
457
458 if (!fin)
459 {
460 p_ws->fragment_opcode = opcode;
461 p_ws->fragment_length = 0;
462
463 if (payload_len > 0)
464 {
465 if (payload_len > p_ws->fragment_capacity)
466 {
467 p_ws->fragment_capacity = payload_len * 2;
468 p_ws->fragment_buffer = realloc(p_ws->fragment_buffer, p_ws->fragment_capacity);
469 }
470
471 memcpy(p_ws->fragment_buffer, payload, payload_len);
472 p_ws->fragment_length = payload_len;
473 }
474
475 if (payload)
476 free(payload);
477
478 return NULL;
479 }
480
481 Seobeo_WebSocket_Message *p_msg = malloc(sizeof(Seobeo_WebSocket_Message));
482 p_msg->opcode = opcode;
483 p_msg->data = payload;
484 p_msg->length = payload_len;
485 p_msg->is_final = fin;
486
487 return p_msg;
488 }
489
490 void Seobeo_WebSocket_Message_Destroy(Seobeo_WebSocket_Message *p_msg)
491 {
492 if (!p_msg)
493 return;
494
495 if (p_msg->data)
496 free(p_msg->data);
497
498 free(p_msg);
499 }
500
501 int32 Seobeo_WebSocket_Close(Seobeo_WebSocket *p_ws, uint16 code, const char *reason)
502 {
503 if (!p_ws || p_ws->state == SEOBEO_WS_STATE_CLOSED)
504 return -1;
505
506 p_ws->state = SEOBEO_WS_STATE_CLOSING;
507
508 size_t reason_len = reason ? strlen(reason) : 0;
509 size_t payload_len = 2 + reason_len;
510 uint8 *payload = malloc(payload_len);
511
512 payload[0] = (uint8)((code >> 8) & 0xFF);
513 payload[1] = (uint8)(code & 0xFF);
514
515 if (reason_len > 0)
516 memcpy(payload + 2, reason, reason_len);
517
518 int32 result = Seobeo_WebSocket_Send_Frame(p_ws, SEOBEO_WS_OPCODE_CLOSE, payload, payload_len, TRUE);
519
520 free(payload);
521
522 p_ws->state = SEOBEO_WS_STATE_CLOSED;
523
524 return result;
525 }
526
527 void Seobeo_WebSocket_Destroy(Seobeo_WebSocket *p_ws)
528 {
529 if (!p_ws)
530 return;
531
532 if (p_ws->state == SEOBEO_WS_STATE_OPEN)
533 Seobeo_WebSocket_Close(p_ws, 1000, "Normal closure");
534
535 if (p_ws->p_handle)
536 Seobeo_Handle_Destroy(p_ws->p_handle);
537
538 if (p_ws->fragment_buffer)
539 free(p_ws->fragment_buffer);
540
541 if (p_ws->p_arena)
542 Dowa_Arena_Free(p_ws->p_arena);
543
544 free(p_ws);
545 }