comparison seobeo/s_websocket_server.c @ 121:7b1719fa918c

[Seobeo] Added web socket server.
author June Park <parkjune1995@gmail.com>
date Thu, 08 Jan 2026 06:45:10 -0800
parents
children f236c895604e
comparison
equal deleted inserted replaced
120:cbbf78b17cfa 121:7b1719fa918c
1 #include "seobeo/seobeo.h"
2 #include <time.h>
3
4 #ifndef SEOBEO_NO_SSL
5 #include <openssl/sha.h>
6 #include <openssl/bio.h>
7 #include <openssl/evp.h>
8 #include <openssl/buffer.h>
9 #endif
10
11 #define SEOBEO_WS_GUID "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
12
13 static Seobeo_WebSocket_Server_Route *g_ws_routes = NULL;
14 static Seobeo_WebSocket_Server_Connection *g_ws_connections = NULL;
15
16 void Seobeo_WebSocket_Server_Init()
17 {
18 Dowa_Array_Reserve(g_ws_routes, 10);
19 }
20
21 void Seobeo_WebSocket_Server_Register(const char *path, Seobeo_WebSocket_Server_Handler handler, void *p_user_data)
22 {
23 Seobeo_WebSocket_Server_Route route = {0};
24 route.path = strdup(path);
25 route.handler = handler;
26 route.p_user_data = p_user_data;
27
28 Dowa_Array_Push(g_ws_routes, route);
29 }
30
31 static void Seobeo_WebSocket_Server_Compute_Accept_Key(const char *client_key, char *accept_key, size_t accept_key_size)
32 {
33 #ifdef SEOBEO_NO_SSL
34 snprintf(accept_key, accept_key_size, "dGhlIHNhbXBsZSBub25jZQ==");
35 (void)client_key;
36 #else
37 char concatenated[256];
38 snprintf(concatenated, sizeof(concatenated), "%s%s", client_key, SEOBEO_WS_GUID);
39
40 unsigned char hash[SHA_DIGEST_LENGTH];
41 SHA1((unsigned char*)concatenated, strlen(concatenated), hash);
42
43 BIO *b64 = BIO_new(BIO_f_base64());
44 BIO *bio = BIO_new(BIO_s_mem());
45 bio = BIO_push(b64, bio);
46 BIO_set_flags(bio, BIO_FLAGS_BASE64_NO_NL);
47 BIO_write(bio, hash, SHA_DIGEST_LENGTH);
48 BIO_flush(bio);
49
50 BUF_MEM *buffer_ptr;
51 BIO_get_mem_ptr(bio, &buffer_ptr);
52
53 size_t copy_len = buffer_ptr->length < accept_key_size - 1 ? buffer_ptr->length : accept_key_size - 1;
54 memcpy(accept_key, buffer_ptr->data, copy_len);
55 accept_key[copy_len] = '\0';
56
57 BIO_free_all(bio);
58 #endif
59 }
60
61 boolean Seobeo_WebSocket_Server_Handle_Upgrade(Seobeo_Handle *p_handle, Seobeo_Request_Entry *p_req_map, const char *path)
62 {
63 void *p_upgrade_kv = Dowa_HashMap_Get_Ptr(p_req_map, "Upgrade");
64 if (!p_upgrade_kv)
65 return FALSE;
66
67 const char *upgrade_value = ((Seobeo_Request_Entry*)p_upgrade_kv)->value;
68 if (strcasecmp(upgrade_value, "websocket") != 0)
69 return FALSE;
70
71 void *p_connection_kv = Dowa_HashMap_Get_Ptr(p_req_map, "Connection");
72 if (!p_connection_kv)
73 return FALSE;
74
75 const char *connection_value = ((Seobeo_Request_Entry*)p_connection_kv)->value;
76 if (!strstr(connection_value, "Upgrade"))
77 return FALSE;
78
79 void *p_key_kv = Dowa_HashMap_Get_Ptr(p_req_map, "Sec-WebSocket-Key");
80 if (!p_key_kv)
81 return FALSE;
82
83 const char *client_key = ((Seobeo_Request_Entry*)p_key_kv)->value;
84
85 char accept_key[64];
86 Seobeo_WebSocket_Server_Compute_Accept_Key(client_key, accept_key, sizeof(accept_key));
87
88 char response[512];
89 int response_len = snprintf(response, sizeof(response),
90 "HTTP/1.1 101 Switching Protocols\r\n"
91 "Upgrade: websocket\r\n"
92 "Connection: Upgrade\r\n"
93 "Sec-WebSocket-Accept: %s\r\n"
94 "\r\n",
95 accept_key);
96
97 Seobeo_Handle_Queue(p_handle, (uint8*)response, (uint32)response_len);
98 if (Seobeo_Handle_Flush(p_handle) < 0)
99 return FALSE;
100
101 Seobeo_WebSocket_Server_Connection *p_conn = malloc(sizeof(Seobeo_WebSocket_Server_Connection));
102 memset(p_conn, 0, sizeof(Seobeo_WebSocket_Server_Connection));
103
104 p_conn->p_handle = p_handle;
105 p_conn->is_active = TRUE;
106 p_conn->fragment_capacity = 4096;
107 p_conn->fragment_buffer = malloc(p_conn->fragment_capacity);
108
109 char client_id[64];
110 snprintf(client_id, sizeof(client_id), "client_%p", (void*)p_handle);
111 p_conn->client_id = strdup(client_id);
112
113 p_conn->next = g_ws_connections;
114 g_ws_connections = p_conn;
115
116 Seobeo_Log(SEOBEO_INFO, "WebSocket upgraded on path: %s\n", path);
117
118 Seobeo_WebSocket_Server_Handle_Connection(p_conn);
119
120 return TRUE;
121 }
122
123 static void Seobeo_WebSocket_Unmask_Data(uint8 *data, size_t length, const uint8 *mask)
124 {
125 for (size_t i = 0; i < length; i++)
126 data[i] ^= mask[i % 4];
127 }
128
129 static int32 Seobeo_WebSocket_Server_Send_Frame(Seobeo_WebSocket_Server_Connection *p_conn, Seobeo_WebSocket_Opcode opcode, const uint8 *payload, size_t payload_length, boolean fin)
130 {
131 if (!p_conn || !p_conn->is_active)
132 return -1;
133
134 uint8 frame[14];
135 size_t frame_len = 0;
136
137 frame[0] = (fin ? 0x80 : 0x00) | (opcode & 0x0F);
138 frame_len++;
139
140 // within 1 byte, so max chunk would be 125 bytes
141 if (payload_length < 126)
142 {
143 frame[1] = (uint8)payload_length;
144 frame_len++;
145 }
146 // from now frame 1 is thrown away just keeping it 1
147 // within 4 bytes
148 else if (payload_length < MAX_INT_16)
149 {
150 frame[1] = 126;
151 frame[2] = (uint8)((payload_length >> 8) & 0xFF);
152 frame[3] = (uint8)(payload_length & 0xFF);
153 frame_len += 3;
154 }
155 else
156 {
157 frame[1] = 127;
158 for (int i = 0; i < 8; i++)
159 frame[2 + i] = (uint8)((payload_length >> (56 - i * 8)) & 0xFF);
160 frame_len += 9;
161 }
162
163 Seobeo_Handle_Queue(p_conn->p_handle, frame, (uint32)frame_len);
164
165 if (payload_length > 0)
166 Seobeo_Handle_Queue(p_conn->p_handle, payload, (uint32)payload_length);
167
168 return Seobeo_Handle_Flush(p_conn->p_handle);
169 }
170
171 static int32 Seobeo_WebSocket_Server_Send_Fragmented(Seobeo_WebSocket_Server_Connection *p_conn, Seobeo_WebSocket_Opcode opcode,
172 const uint8 *payload, size_t total_length)
173 {
174 if (!payload || total_length == 0)
175 return -1;
176
177 if (total_length <= MAX_FRAGMENT_SIZE)
178 return Seobeo_WebSocket_Server_Send_Frame(p_conn, opcode, payload, total_length, TRUE);
179
180 size_t sent = 0;
181 int32 result;
182
183 result = Seobeo_WebSocket_Server_Send_Frame(p_conn, opcode, payload, MAX_FRAGMENT_SIZE, FALSE);
184 if (result < 0)
185 return result;
186
187 sent += MAX_FRAGMENT_SIZE;
188
189 while (sent + MAX_FRAGMENT_SIZE < total_length)
190 {
191 result = Seobeo_WebSocket_Server_Send_Frame(p_conn, SEOBEO_WS_OPCODE_CONTINUATION, payload + sent, MAX_FRAGMENT_SIZE, FALSE);
192 if (result < 0)
193 return result;
194 sent += MAX_FRAGMENT_SIZE;
195 }
196
197 size_t remaining = total_length - sent;
198 return Seobeo_WebSocket_Server_Send_Frame(p_conn, SEOBEO_WS_OPCODE_CONTINUATION, payload + sent, remaining, TRUE);
199 }
200
201 int32 Seobeo_WebSocket_Server_Send_Text(Seobeo_WebSocket_Server_Connection *p_conn, const char *text)
202 {
203 if (!text)
204 return -1;
205
206 return Seobeo_WebSocket_Server_Send_Fragmented(p_conn, SEOBEO_WS_OPCODE_TEXT, (const uint8*)text, strlen(text));
207 }
208
209 int32 Seobeo_WebSocket_Server_Send_Binary(Seobeo_WebSocket_Server_Connection *p_conn, const uint8 *data, size_t length)
210 {
211 if (!data)
212 return -1;
213
214 return Seobeo_WebSocket_Server_Send_Fragmented(p_conn, SEOBEO_WS_OPCODE_BINARY, data, length);
215 }
216
217 static Seobeo_WebSocket_Message *Seobeo_WebSocket_Server_Receive_Frame(Seobeo_WebSocket_Server_Connection *p_conn)
218 {
219 if (!p_conn || !p_conn->is_active)
220 return NULL;
221
222 int r = Seobeo_Handle_Read(p_conn->p_handle);
223 if (r < 0)
224 {
225 Seobeo_Log(SEOBEO_ERROR, "WebSocket server read error\n");
226 p_conn->is_active = FALSE;
227 return NULL;
228 }
229
230 if (r == -2)
231 {
232 Seobeo_Log(SEOBEO_INFO, "WebSocket client disconnected\n");
233 p_conn->is_active = FALSE;
234 return NULL;
235 }
236
237 if (p_conn->p_handle->read_buffer_len < 2)
238 return NULL;
239
240 uint8 *buf = p_conn->p_handle->read_buffer;
241
242 uint8 byte1 = buf[0];
243 uint8 byte2 = buf[1];
244
245 boolean fin = (byte1 & 0x80) != 0;
246 Seobeo_WebSocket_Opcode opcode = (Seobeo_WebSocket_Opcode)(byte1 & 0x0F);
247 boolean masked = (byte2 & 0x80) != 0;
248 uint64 payload_len = byte2 & 0x7F;
249
250 size_t header_len = 2;
251
252 if (payload_len == 126)
253 {
254 if (p_conn->p_handle->read_buffer_len < 4)
255 return NULL;
256 payload_len = (buf[2] << 8) | buf[3];
257 header_len += 2;
258 }
259 else if (payload_len == 127)
260 {
261 if (p_conn->p_handle->read_buffer_len < 10)
262 return NULL;
263 payload_len = 0;
264 for (int i = 0; i < 8; i++)
265 payload_len = (payload_len << 8) | buf[2 + i];
266 header_len += 8;
267 }
268
269 uint8 mask_key[4] = {0};
270 if (masked)
271 {
272 if (p_conn->p_handle->read_buffer_len < header_len + 4)
273 return NULL;
274 memcpy(mask_key, buf + header_len, 4);
275 header_len += 4;
276 }
277
278 if (p_conn->p_handle->read_buffer_len < header_len + payload_len)
279 return NULL;
280
281 uint8 *payload = NULL;
282 if (payload_len > 0)
283 {
284 payload = malloc(payload_len);
285 memcpy(payload, buf + header_len, payload_len);
286
287 if (masked)
288 Seobeo_WebSocket_Unmask_Data(payload, payload_len, mask_key);
289 }
290
291 Seobeo_Handle_Consume(p_conn->p_handle, (uint32)(header_len + payload_len));
292
293 if (opcode == SEOBEO_WS_OPCODE_PING)
294 {
295 Seobeo_WebSocket_Server_Send_Frame(p_conn, SEOBEO_WS_OPCODE_PONG, payload, payload_len, TRUE);
296 if (payload)
297 free(payload);
298 return NULL;
299 }
300
301 if (opcode == SEOBEO_WS_OPCODE_PONG)
302 {
303 if (payload)
304 free(payload);
305 return NULL;
306 }
307
308 if (opcode == SEOBEO_WS_OPCODE_CLOSE)
309 {
310 uint16 close_code = 1000;
311 if (payload_len >= 2)
312 close_code = (payload[0] << 8) | payload[1];
313
314 Seobeo_Log(SEOBEO_INFO, "WebSocket close received from client with code %d\n", close_code);
315
316 Seobeo_WebSocket_Server_Send_Frame(p_conn, SEOBEO_WS_OPCODE_CLOSE, payload, payload_len, TRUE);
317 p_conn->is_active = FALSE;
318
319 if (payload)
320 free(payload);
321 return NULL;
322 }
323
324 if (opcode == SEOBEO_WS_OPCODE_CONTINUATION)
325 {
326 if (p_conn->fragment_length + payload_len > p_conn->fragment_capacity)
327 {
328 p_conn->fragment_capacity = (p_conn->fragment_length + payload_len) * 2;
329 p_conn->fragment_buffer = realloc(p_conn->fragment_buffer, p_conn->fragment_capacity);
330 }
331
332 if (payload_len > 0)
333 {
334 memcpy(p_conn->fragment_buffer + p_conn->fragment_length, payload, payload_len);
335 p_conn->fragment_length += payload_len;
336 }
337
338 if (payload)
339 free(payload);
340
341 if (!fin)
342 return NULL;
343
344 Seobeo_WebSocket_Message *p_msg = malloc(sizeof(Seobeo_WebSocket_Message));
345 p_msg->opcode = p_conn->fragment_opcode;
346 p_msg->data = malloc(p_conn->fragment_length);
347 memcpy(p_msg->data, p_conn->fragment_buffer, p_conn->fragment_length);
348 p_msg->length = p_conn->fragment_length;
349 p_msg->is_final = TRUE;
350
351 p_conn->fragment_length = 0;
352
353 return p_msg;
354 }
355
356 if (!fin)
357 {
358 p_conn->fragment_opcode = opcode;
359 p_conn->fragment_length = 0;
360
361 if (payload_len > 0)
362 {
363 if (payload_len > p_conn->fragment_capacity)
364 {
365 p_conn->fragment_capacity = payload_len * 2;
366 p_conn->fragment_buffer = realloc(p_conn->fragment_buffer, p_conn->fragment_capacity);
367 }
368
369 memcpy(p_conn->fragment_buffer, payload, payload_len);
370 p_conn->fragment_length = payload_len;
371 }
372
373 if (payload)
374 free(payload);
375
376 return NULL;
377 }
378
379 Seobeo_WebSocket_Message *p_msg = malloc(sizeof(Seobeo_WebSocket_Message));
380 p_msg->opcode = opcode;
381 p_msg->data = payload;
382 p_msg->length = payload_len;
383 p_msg->is_final = fin;
384
385 return p_msg;
386 }
387
388 void Seobeo_WebSocket_Server_Handle_Connection(Seobeo_WebSocket_Server_Connection *p_conn)
389 {
390 if (!g_ws_routes)
391 return;
392
393 size_t route_count = Dowa_Array_Length(g_ws_routes);
394 if (route_count == 0)
395 return;
396
397 Seobeo_WebSocket_Server_Handler handler = g_ws_routes[0].handler;
398 void *p_user_data = g_ws_routes[0].p_user_data;
399
400 while (p_conn->is_active)
401 {
402 Seobeo_WebSocket_Message *p_msg = Seobeo_WebSocket_Server_Receive_Frame(p_conn);
403 if (p_msg)
404 {
405 if (handler)
406 handler(p_conn, p_msg, p_user_data);
407
408 Seobeo_WebSocket_Message_Destroy(p_msg);
409 }
410
411 usleep(1000);
412 }
413
414 Seobeo_WebSocket_Server_Connection_Destroy(p_conn);
415 }
416
417 void Seobeo_WebSocket_Server_Broadcast_Text(const char *text)
418 {
419 if (!text)
420 return;
421
422 Seobeo_WebSocket_Server_Connection *p_conn = g_ws_connections;
423 while (p_conn)
424 {
425 if (p_conn->is_active)
426 Seobeo_WebSocket_Server_Send_Text(p_conn, text);
427
428 p_conn = p_conn->next;
429 }
430 }
431
432 void Seobeo_WebSocket_Server_Broadcast_Binary(const uint8 *data, size_t length)
433 {
434 if (!data)
435 return;
436
437 Seobeo_WebSocket_Server_Connection *p_conn = g_ws_connections;
438 while (p_conn)
439 {
440 if (p_conn->is_active)
441 Seobeo_WebSocket_Server_Send_Binary(p_conn, data, length);
442
443 p_conn = p_conn->next;
444 }
445 }
446
447 void Seobeo_WebSocket_Server_Connection_Close(Seobeo_WebSocket_Server_Connection *p_conn, uint16 code, const char *reason)
448 {
449 if (!p_conn || !p_conn->is_active)
450 return;
451
452 size_t reason_len = reason ? strlen(reason) : 0;
453 size_t payload_len = 2 + reason_len;
454 uint8 *payload = malloc(payload_len);
455
456 payload[0] = (uint8)((code >> 8) & 0xFF);
457 payload[1] = (uint8)(code & 0xFF);
458
459 if (reason_len > 0)
460 memcpy(payload + 2, reason, reason_len);
461
462 Seobeo_WebSocket_Server_Send_Frame(p_conn, SEOBEO_WS_OPCODE_CLOSE, payload, payload_len, TRUE);
463
464 free(payload);
465
466 p_conn->is_active = FALSE;
467 }
468
469 void Seobeo_WebSocket_Server_Connection_Destroy(Seobeo_WebSocket_Server_Connection *p_conn)
470 {
471 if (!p_conn)
472 return;
473
474 if (p_conn->is_active)
475 Seobeo_WebSocket_Server_Connection_Close(p_conn, 1000, "Server closing connection");
476
477 if (p_conn->p_handle)
478 Seobeo_Handle_Destroy(p_conn->p_handle);
479
480 if (p_conn->client_id)
481 free(p_conn->client_id);
482
483 if (p_conn->fragment_buffer)
484 free(p_conn->fragment_buffer);
485
486 Seobeo_WebSocket_Server_Connection **pp = &g_ws_connections;
487 while (*pp)
488 {
489 if (*pp == p_conn)
490 {
491 *pp = p_conn->next;
492 break;
493 }
494 pp = &(*pp)->next;
495 }
496
497 free(p_conn);
498 }