master
c 505 lines 19 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 #include "web/server/web_client.h"
4 #include "websocket-internal.h"
5 #include "websocket-jsonrpc.h"
6 #include "websocket-echo.h"
7 #include "../mcp/adapters/mcp-websocket.h"
8 #include "web/api/mcp_auth.h"
9
10 // Global array of WebSocket threads
11 WEBSOCKET_THREAD websocket_threads[WEBSOCKET_MAX_THREADS];
12
13 // Initialize WebSocket thread system
14 void websocket_threads_init(void) {
15 for(size_t i = 0; i < WEBSOCKET_MAX_THREADS; i++) {
16 websocket_threads[i].id = i;
17 websocket_threads[i].thread = NULL;
18 websocket_threads[i].running = false;
19 spinlock_init(&websocket_threads[i].spinlock);
20 websocket_threads[i].clients_current = 0;
21 spinlock_init(&websocket_threads[i].clients_spinlock);
22 websocket_threads[i].clients = NULL;
23 websocket_threads[i].ndpl = NULL;
24 websocket_threads[i].cmd.pipe[PIPE_READ] = -1;
25 websocket_threads[i].cmd.pipe[PIPE_WRITE] = -1;
26 websocket_threads[i].cmd.buffer = NULL;
27 websocket_threads[i].cmd.buffer_size = 0;
28 }
29 }
30
31 // Find the thread with the minimum client load and atomically increment its count
32 NEVERNULL
33 static WEBSOCKET_THREAD *websocket_thread_get_min_load(void) {
34 // Static spinlock to protect the critical section of thread selection
35 static SPINLOCK assign_spinlock = SPINLOCK_INITIALIZER;
36 size_t slot = 0;
37
38 // Critical section: find thread with minimum load and increment its count atomically
39 spinlock_lock(&assign_spinlock);
40
41 // Find the minimum load thread
42 size_t min_clients = websocket_threads[0].clients_current;
43
44 for(size_t i = 1; i < WEBSOCKET_MAX_THREADS; i++) {
45 // Check if this thread has fewer clients
46 if(websocket_threads[i].clients_current < min_clients) {
47 min_clients = websocket_threads[i].clients_current;
48 slot = i;
49 }
50 }
51
52 // Preemptively increment the client count to prevent race conditions
53 // This ensures concurrent client assignments will be properly distributed
54 websocket_threads[slot].clients_current++;
55
56 spinlock_unlock(&assign_spinlock);
57
58 return &websocket_threads[slot];
59 }
60
61 // Handle socket takeover from web client - similar to stream_receiver_takeover_web_connection
62 static void websocket_takeover_web_connection(struct web_client *w, WS_CLIENT *wsc) {
63 // Set the file descriptor and ssl from the web client
64 wsc->sock.fd = w->fd;
65 wsc->sock.ssl = w->ssl;
66
67 w->ssl = NETDATA_SSL_UNSET_CONNECTION;
68
69 WEB_CLIENT_IS_DEAD(w);
70
71 if(web_server_mode == WEB_SERVER_MODE_STATIC_THREADED) {
72 web_client_flag_set(w, WEB_CLIENT_FLAG_DONT_CLOSE_SOCKET);
73 }
74 else {
75 w->fd = -1;
76 }
77
78 // Clear web client buffer
79 buffer_flush(w->response.data);
80
81 web_server_remove_current_socket_from_poll();
82 }
83
84 // Initialize a thread's poll
85 static bool websocket_thread_init_poll(WEBSOCKET_THREAD *wth) {
86 // Create poll instance
87 if(!wth->ndpl) {
88 wth->ndpl = nd_poll_create();
89 if (!wth->ndpl) {
90 netdata_log_error("WEBSOCKET[%zu]: Failed to create poll", wth->id);
91 goto cleanup;
92 }
93 }
94
95 // Create command pipe
96 if(wth->cmd.pipe[PIPE_READ] == -1 || wth->cmd.pipe[PIPE_WRITE] == -1) {
97 if (pipe(wth->cmd.pipe) == -1) {
98 netdata_log_error("WEBSOCKET[%zu]: Failed to create command pipe: %s", wth->id, strerror(errno));
99 goto cleanup;
100 }
101
102 // Set pipe to non-blocking
103 if(fcntl(wth->cmd.pipe[PIPE_READ], F_SETFL, O_NONBLOCK) == -1) {
104 netdata_log_error("WEBSOCKET[%zu]: Failed to set command pipe to non-blocking: %s", wth->id, strerror(errno));
105 goto cleanup;
106 }
107
108 // Add command pipe to poll
109 bool added = nd_poll_add(wth->ndpl, wth->cmd.pipe[PIPE_READ], ND_POLL_READ, &wth->cmd);
110 if(!added) {
111 netdata_log_error("WEBSOCKET[%zu]: Failed to add command pipe to poll", wth->id);
112 goto cleanup;
113 }
114 }
115
116 return true;
117
118 cleanup:
119 if(wth->cmd.pipe[PIPE_READ] != -1) {
120 close(wth->cmd.pipe[PIPE_READ]);
121 wth->cmd.pipe[PIPE_READ] = -1;
122 }
123 if(wth->cmd.pipe[PIPE_WRITE] != -1) {
124 close(wth->cmd.pipe[PIPE_WRITE]);
125 wth->cmd.pipe[PIPE_WRITE] = -1;
126 }
127 if(wth->ndpl) {
128 nd_poll_destroy(wth->ndpl);
129 wth->ndpl = NULL;
130 }
131 return false;
132 }
133
134 // Assign a client to a thread
135 static WEBSOCKET_THREAD *websocket_thread_assign_client(WS_CLIENT *wsc) {
136 // Get the thread with the minimum load
137 // Note: client count is already atomically incremented inside this function
138 WEBSOCKET_THREAD *wth = websocket_thread_get_min_load();
139
140 // Lock the thread for initialization
141 spinlock_lock(&wth->spinlock);
142
143 // Start the thread if not running
144 if(!wth->thread) {
145 // Initialize poll
146 if(!websocket_thread_init_poll(wth)) {
147 spinlock_unlock(&wth->spinlock);
148 netdata_log_error("WEBSOCKET[%zu]: Failed to initialize poll", wth->id);
149 goto undo;
150 }
151
152 char thread_name[32];
153 snprintf(thread_name, sizeof(thread_name), "WEBSOCK[%zu]", wth->id);
154 wth->thread = nd_thread_create(thread_name, NETDATA_THREAD_OPTION_DEFAULT, websocket_thread, wth);
155 wth->running = true;
156 }
157
158 // Release the thread lock
159 spinlock_unlock(&wth->spinlock);
160
161 // Link thread to client
162 wsc->wth = wth;
163
164 // Send command to add client
165 if(!websocket_thread_send_command(wth, WEBSOCKET_THREAD_CMD_ADD_CLIENT, wsc->id)) {
166 netdata_log_error("WEBSOCKET[%zu]: Failed to send add client command", wth->id);
167 goto undo;
168 }
169
170 return wth;
171
172 undo:
173 // Roll back the client count increment since assignment failed
174 wsc->wth = NULL;
175
176 spinlock_lock(&wth->clients_spinlock);
177 if (wth->clients_current > 0)
178 wth->clients_current--;
179 spinlock_unlock(&wth->clients_spinlock);
180
181 return NULL;
182 }
183
184 // Cancel all WebSocket threads
185 void websocket_threads_join(void) {
186 for(size_t i = 0; i < WEBSOCKET_MAX_THREADS; i++) {
187 if(websocket_threads[i].thread) {
188 // Send exit command
189 websocket_thread_send_command(&websocket_threads[i], WEBSOCKET_THREAD_CMD_EXIT, 0);
190
191 // Signal thread to cancel
192 nd_thread_signal_cancel(websocket_threads[i].thread);
193 }
194 }
195
196 // Wait for all threads to exit
197 for(size_t i = 0; i < WEBSOCKET_MAX_THREADS; i++) {
198 if(websocket_threads[i].thread) {
199 nd_thread_join(websocket_threads[i].thread);
200 websocket_threads[i].thread = NULL;
201 websocket_threads[i].running = false;
202 }
203 }
204 }
205
206 // Check if the current HTTP request is a WebSocket handshake request
207 static bool websocket_detect_handshake_request(struct web_client *w) {
208 // We need a valid key and to be flagged as a WebSocket request
209 if (!web_client_is_websocket(w) || !w->websocket.key)
210 return false;
211
212 return true;
213 }
214
215 // Generate the WebSocket accept key as per RFC 6455
216 static char *websocket_generate_handshake_key(const char *client_key) {
217 if (!client_key)
218 return NULL;
219
220 // Concatenate the key with the WebSocket GUID
221 char concat_key[256];
222 snprintfz(concat_key, sizeof(concat_key), "%s%s", client_key, WS_GUID);
223
224 // Create SHA-1 hash
225 unsigned char sha_hash[SHA_DIGEST_LENGTH];
226 SHA1((unsigned char *)concat_key, strlen(concat_key), sha_hash);
227
228 // Convert to base64
229 char *accept_key = mallocz(33); // Base64 of SHA-1 is 28 chars + null term
230 netdata_base64_encode((unsigned char *)accept_key, sha_hash, SHA_DIGEST_LENGTH);
231
232 return accept_key;
233 }
234
235 static bool websocket_send_first_response(WS_CLIENT *wsc, const char *accept_key, WEBSOCKET_EXTENSION ext_flags, bool url_protocol) {
236 CLEAN_BUFFER *wb = buffer_create(1024, NULL);
237
238 buffer_sprintf(wb,
239 "HTTP/1.1 101 Switching Protocols\r\n"
240 "Server: Netdata\r\n"
241 "Upgrade: websocket\r\n"
242 "Connection: Upgrade\r\n"
243 "Sec-WebSocket-Accept: %s\r\n",
244 accept_key
245 );
246
247 // Add the selected subprotocol
248 if(!url_protocol && wsc->protocol != WS_PROTOCOL_UNKNOWN && wsc->protocol != WS_PROTOCOL_DEFAULT)
249 buffer_sprintf(wb, "Sec-WebSocket-Protocol: %s\r\n", WEBSOCKET_PROTOCOL_2str(wsc->protocol));
250
251 switch (wsc->compression.type) {
252 case WS_COMPRESS_DEFLATE:
253 buffer_strcat(wb, "Sec-WebSocket-Extensions: permessage-deflate");
254
255 // Add parameters if different from defaults
256 if (!wsc->compression.client_context_takeover)
257 buffer_strcat(wb, "; client_no_context_takeover");
258
259 if (!wsc->compression.server_context_takeover)
260 buffer_strcat(wb, "; server_no_context_takeover");
261
262 if(ext_flags & WS_EXTENSION_SERVER_MAX_WINDOW_BITS)
263 buffer_sprintf(wb, "; server_max_window_bits=%d", wsc->compression.server_max_window_bits);
264
265 if(ext_flags & WS_EXTENSION_CLIENT_MAX_WINDOW_BITS)
266 buffer_sprintf(wb, "; client_max_window_bits=%d", wsc->compression.client_max_window_bits);
267
268 buffer_strcat(wb, "\r\n");
269 break;
270
271 default:
272 break;
273 }
274
275 // End of headers
276 buffer_strcat(wb, "Sec-WebSocket-Version: 13\r\n");
277 buffer_strcat(wb, "\r\n");
278
279 // Send the handshake response using ND_SOCK - we're still in the web server thread,
280 // so we need to use the persist version to ensure the complete handshake is sent
281 const char *header_str = buffer_tostring(wb);
282 size_t header_len = buffer_strlen(wb);
283 ssize_t bytes = nd_sock_write_persist(&wsc->sock, header_str, header_len, 20);
284
285 websocket_debug(wsc, "Sent WebSocket handshake response: %zd bytes out of %zu bytes", bytes, header_len);
286 return bytes == (ssize_t)header_len;
287 }
288
289 // Handle the WebSocket handshake procedure
290 short int websocket_handle_handshake(struct web_client *w) {
291 web_client_ensure_proper_authorization(w);
292
293 if (!websocket_detect_handshake_request(w))
294 return HTTP_RESP_BAD_REQUEST;
295
296 // Generate the accept key
297 char *accept_key = websocket_generate_handshake_key(w->websocket.key);
298 if (!accept_key)
299 return HTTP_RESP_INTERNAL_SERVER_ERROR;
300
301 // Create the WebSocket client object early so we can set up compression
302 WS_CLIENT *wsc = websocket_client_create();
303
304 // Copy client information
305 strncpyz(wsc->client_ip, w->user_auth.client_ip, sizeof(wsc->client_ip));
306 strncpyz(wsc->client_port, w->client_port, sizeof(wsc->client_port));
307
308 // Copy user authentication and authorization information
309 wsc->user_auth = w->user_auth;
310
311 // Check for max_frame_size parameter in the URL query string
312 if (w->url_query_string_decoded && buffer_strlen(w->url_query_string_decoded) > 0) {
313 const char *query = buffer_tostring(w->url_query_string_decoded);
314 char *max_frame_size_str = strstr(query, "max_frame_size=");
315
316 if (max_frame_size_str) {
317 max_frame_size_str += strlen("max_frame_size=");
318
319 char *end_ptr;
320 size_t max_frame_size = strtoull(max_frame_size_str, &end_ptr, 10);
321
322 // Validate the max frame size with reasonable bounds
323 if (max_frame_size > 0) {
324 // Set minimum and maximum limits
325 if (max_frame_size < 1024) // Minimum 1KB
326 max_frame_size = 1024;
327 else if (max_frame_size > (20ULL * 1024 * 1024)) // Maximum 20MB
328 max_frame_size = 20ULL * 1024 * 1024;
329
330 // Set the client's max outbound frame size
331 wsc->max_outbound_frame_size = max_frame_size;
332 websocket_debug(wsc, "Setting custom max outbound frame size: %zu bytes", max_frame_size);
333 }
334 }
335
336 #ifdef NETDATA_MCP_DEV_PREVIEW_API_KEY
337 if (web_client_has_mcp_preview_key(w)) {
338 wsc->user_auth.access = HTTP_ACCESS_ALL;
339 wsc->user_auth.method = USER_AUTH_METHOD_GOD;
340 wsc->user_auth.user_role = HTTP_USER_ROLE_ADMIN;
341 websocket_debug(wsc, "MCP developer preview API key verified via Authorization header - enabling full access");
342 } else {
343 // Check for api_key parameter for MCP developer preview
344 char *api_key_str = strstr(query, "api_key=");
345 if (api_key_str) {
346 api_key_str += strlen("api_key=");
347
348 // Extract the API key value (until & or end of string)
349 char api_key_buffer[MCP_DEV_PREVIEW_API_KEY_LENGTH + 1];
350 size_t i = 0;
351 while (api_key_str[i] && api_key_str[i] != '&' && i < MCP_DEV_PREVIEW_API_KEY_LENGTH) {
352 api_key_buffer[i] = api_key_str[i];
353 i++;
354 }
355 api_key_buffer[i] = '\0';
356
357 // Verify the API key
358 if (mcp_api_key_verify(api_key_buffer, false)) { // silent=false for websocket MCP requests
359 // Override authentication with god mode
360 wsc->user_auth.access = HTTP_ACCESS_ALL;
361 wsc->user_auth.method = USER_AUTH_METHOD_GOD;
362 wsc->user_auth.user_role = HTTP_USER_ROLE_ADMIN;
363 websocket_debug(wsc, "MCP developer preview API key verified - enabling full access");
364 } else {
365 websocket_debug(wsc, "Invalid MCP developer preview API key provided");
366 }
367 }
368 }
369 #endif
370 }
371
372 bool url_protocol = false;
373 wsc->protocol = w->websocket.protocol;
374
375 if(wsc->protocol == WS_PROTOCOL_DEFAULT) {
376 const char *path = buffer_tostring(w->url_path_decoded);
377 if (path && path[0] == '/' && path[1])
378 wsc->protocol = WEBSOCKET_PROTOCOL_2id(&path[1]);
379
380 url_protocol = true;
381 }
382
383 // If no protocol is selected by either URL or subprotocol, reject the connection
384 if(wsc->protocol == WS_PROTOCOL_UNKNOWN || wsc->protocol == WS_PROTOCOL_DEFAULT) {
385 netdata_log_error("WEBSOCKET: No valid protocol selected by either URL or subprotocol");
386 freez(accept_key);
387 websocket_client_free(wsc);
388 return HTTP_RESP_BAD_REQUEST;
389 }
390
391 // Take over the connection immediately
392 websocket_takeover_web_connection(w, wsc);
393
394 if((w->websocket.ext_flags & WS_EXTENSION_PERMESSAGE_DEFLATE)) {
395 wsc->compression.enabled = true;
396 wsc->compression.type = WS_COMPRESS_DEFLATE;
397
398 if (w->websocket.ext_flags & WS_EXTENSION_CLIENT_NO_CONTEXT_TAKEOVER)
399 wsc->compression.client_context_takeover = false;
400 else
401 wsc->compression.client_context_takeover = true;
402
403 if (w->websocket.ext_flags & WS_EXTENSION_SERVER_NO_CONTEXT_TAKEOVER)
404 wsc->compression.server_context_takeover = false;
405 else
406 wsc->compression.server_context_takeover = true;
407
408 // Set window bits for both client-to-server and server-to-client directions
409 wsc->compression.client_max_window_bits = w->websocket.client_max_window_bits ? w->websocket.client_max_window_bits : WS_COMPRESS_WINDOW_BITS;
410 wsc->compression.server_max_window_bits = w->websocket.server_max_window_bits ? w->websocket.server_max_window_bits : WS_COMPRESS_WINDOW_BITS;
411 }
412
413 if(!websocket_send_first_response(wsc, accept_key, w->websocket.ext_flags, url_protocol)) {
414 netdata_log_error("WEBSOCKET: Failed to send complete WebSocket handshake response"); // No client yet
415 freez(accept_key);
416 websocket_client_free(wsc);
417 return HTTP_RESP_INTERNAL_SERVER_ERROR;
418 }
419
420 freez(accept_key);
421
422 // Now that we've sent the handshake response successfully, set the connection state to open
423 wsc->state = WS_STATE_OPEN;
424
425 // Set up protocol-specific callbacks based on the selected protocol
426 switch (wsc->protocol) {
427 case WS_PROTOCOL_MCP:
428 // Set up callbacks for MCP protocol
429 wsc->on_connect = mcp_websocket_on_connect;
430 wsc->on_message = mcp_websocket_on_message;
431 wsc->on_close = mcp_websocket_on_close;
432 wsc->on_disconnect = mcp_websocket_on_disconnect;
433 websocket_debug(wsc, "Setting up MCP protocol callbacks");
434 break;
435
436 #ifdef NETDATA_INTERNAL_CHECKS
437 case WS_PROTOCOL_JSONRPC:
438 // Set up callbacks for jsonrpc protocol
439 wsc->on_connect = jsonrpc_on_connect;
440 wsc->on_message = jsonrpc_on_message_callback;
441 wsc->on_close = jsonrpc_on_close;
442 wsc->on_disconnect = jsonrpc_on_disconnect;
443 websocket_debug(wsc, "Setting up jsonrpc protocol callbacks");
444 break;
445
446 case WS_PROTOCOL_ECHO:
447 // Set up callbacks for echo protocol
448 wsc->on_connect = echo_on_connect;
449 wsc->on_message = echo_on_message_callback;
450 wsc->on_close = echo_on_close;
451 wsc->on_disconnect = echo_on_disconnect;
452 websocket_debug(wsc, "Setting up echo protocol callbacks");
453 break;
454 #endif
455
456 default:
457 // No protocol handler available - this shouldn't happen as we check earlier
458 netdata_log_error("WEBSOCKET: No handler available for protocol %d", wsc->protocol);
459 websocket_client_free(wsc);
460 return HTTP_RESP_BAD_REQUEST;
461 }
462
463 // Register the client in our registry
464 if (!websocket_client_register(wsc)) {
465 websocket_error(wsc, "Failed to register WebSocket client");
466 websocket_client_free(wsc);
467 return HTTP_RESP_WEBSOCKET_HANDSHAKE;
468 }
469
470 // Message structures are already initialized in websocket_client_create()
471
472 // Set socket to non-blocking mode
473 if (fcntl(wsc->sock.fd, F_SETFL, O_NONBLOCK) == -1) {
474 websocket_error(wsc, "Failed to set WebSocket socket to non-blocking mode");
475 websocket_client_free(wsc);
476 return HTTP_RESP_WEBSOCKET_HANDSHAKE;
477 }
478
479 // Assign to a thread
480 WEBSOCKET_THREAD *wth = websocket_thread_assign_client(wsc);
481 if (!wth) {
482 websocket_error(wsc, "Failed to assign WebSocket client to a thread");
483 websocket_client_free(wsc);
484 return HTTP_RESP_WEBSOCKET_HANDSHAKE;
485 }
486
487 nd_log(NDLS_DAEMON, NDLP_DEBUG,
488 "WebSocket connection established with %s:%s using protocol: %s (client ID: %u, thread: %zu), "
489 "compression: %s (client context takeover: %s, server context takeover: %s, "
490 "client window bits: %d, server window bits: %d), "
491 "max outbound frame size: %zu bytes",
492 wsc->client_ip, wsc->client_port,
493 WEBSOCKET_PROTOCOL_2str(wsc->protocol),
494 wsc->id, wth->id,
495 wsc->compression.enabled ? "enabled" : "disabled",
496 wsc->compression.client_context_takeover ? "enabled" : "disabled",
497 wsc->compression.server_context_takeover ? "enabled" : "disabled",
498 wsc->compression.client_max_window_bits,
499 wsc->compression.server_max_window_bits,
500 wsc->max_outbound_frame_size);
501
502 // Important: This code doesn't actually get sent to the client since we've already
503 // taken over the socket. It's just used by the caller to identify what happened.
504 return HTTP_RESP_WEBSOCKET_HANDSHAKE;
505 }